# -*- coding: utf-8 -*- # !/usr/bin/env python import datetime import json import logging import traceback from arrow import Arrow from django.conf import settings from typing import TYPE_CHECKING from apilib.monetary import RMB, VirtualCoin from apilib.utils_string import make_title_from_dict from apps.web.constant import Const, DEALER_CONSUMPTION_AGG_KIND, CONSUMETYPE from apps.web.eventer.base import ComNetPayAckEvent, AckEventProcessorIntf, IdStartAckEvent from apps.web.user.models import VCardConsumeRecord, ConsumeRecord, RechargeRecord, UserVirtualCard, RefundMoneyRecord from apps.web.user.transaction_deprecated import refund_cash if TYPE_CHECKING: from apps.web.device.models import DeviceDict logger = logging.getLogger(__name__) class StartAckEventPreProcessor(AckEventProcessorIntf): def analysis_reason(self, reason, fault_code=None): FINISHED_CHARGE_REASON_MAP = { # 服务器定义的停止事件 0xC0: u'计费方式无效', 0xC1: u'订购套餐已用完', 0xC2: u'订购时间已用完', 0xC3: u'订购电量已用完', 0xC4: u'动态计算功率后, 时间已用完', 0xC5: u'订单异常,设备可能离线超过1小时, 平台结单', 0xC6: u'系统检测到充电已结束, 平台结单(0x21)', 0xC7: u'系统检测到充电已结束, 平台结单(0x15)', 0xC8: u'用户远程停止订单', 0xC9: u'经销商远程停止订单', 0xCA: u'系统检测到订单已结束, 平台结单(0xCA)', 0xCB: u'充电时长已达到最大限制(0xCB)', 0xCC: u'充电电量已达到最大限制(0xCC)', 0xCD: u'设备升级中... 关闭当前订单', } return FINISHED_CHARGE_REASON_MAP.get(reason, '充电结束') def pre_processing(self, device, event_data): # type:(DeviceDict, dict)->dict source = json.dumps(event_data, indent=4) event_data['source'] = source if 'duration' in event_data: event_data['duration'] = round(event_data['duration'] / 60.0, 1) if 'elec' in event_data: event_data['elec'] = round(event_data['elec'] / 3600000.0, 3) if 'rule' in event_data: rule = event_data['rule'] if 'need_time' in rule: event_data['needTime'] = round(rule['need_time'] / 60.0, 2) if 'need_elec' in rule: event_data['needElec'] = round(rule['need_elec'] / 3600000.0, 3) if 'amount' in rule: event_data['amount'] = round(rule['amount'] / 100.0, 2) if 'need_pay' in event_data: if event_data['need_pay'] == -1: # 由服务器计算, 模块计算由精度损失 event_data['needPay'] = 0.0 else: event_data['needPay'] = round(event_data['need_pay'] / 3600.0 / 100.0, 2) if 'status' in event_data and event_data['status'] == 'finished': event_data['reasonDesc'] = self.analysis_reason(event_data.get('reason')) if event_data.get('duration', 5) < 5 and event_data.get('reason') in [0xC1, 0xC2, 0xC3, 0xC4]: event_data['reasonDesc'] = '充电结束(可能为异常导致, 如有疑问, 请联系经销商.)' if 'fee' in event_data: # 仅仅是本笔订单的金额,续充单的扣费使用的 event_data['fee'] = round(event_data['fee'] * 0.01, 2) if 'card_id' in event_data: event_data['cardNo'] = format(int(event_data['card_id'], 16), 'd') if 'sub' in event_data: for sub in event_data['sub']: if 'fee' in sub: sub['fee'] = round(sub['fee'] * 0.01, 2) return event_data class PolicyComNetPayAckEvent(ComNetPayAckEvent): def __init__(self, smartBox, event_data): super(PolicyComNetPayAckEvent, self).__init__(smartBox, event_data, StartAckEventPreProcessor()) def post_before_start(self, order=None): # 记录处理的源数据报文 uart_source = getattr(order, 'uart_source', []) uart_source.append({ 'rece_running': self.event_data.get('source'), 'time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') }) order.uart_source = uart_source order.save() def post_after_start(self, order=None): pass def post_before_finish(self, order=None): # 记录处理的源数据报文 uart_source = getattr(order, 'uart_source', []) uart_source.append({ 'rece_finished': self.event_data.get('source'), 'time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') }) order.uart_source = uart_source order.save() def post_after_finish(self, order=None): pass def merge_order(self, master_order, sub_orders): # type:(ConsumeRecord, list)->dict start_time = Arrow.fromdatetime(master_order.startTime, tzinfo=settings.TIME_ZONE) port_info = { 'start_time': start_time.format(Const.DATETIME_FMT), 'estimatedTs': int(start_time.timestamp + 3600 * 12), } billing_method = self.event_data['billing_method'] if billing_method == CONSUMETYPE.POSTPAID: if 'needTime' in self.event_data: port_info.update({ 'needKind': 'needTime', 'needValue': self.event_data['needTime'] }) elif 'needElec' in self.event_data: port_info.update({ 'needKind': 'needElec', 'needValue': self.event_data['needElec'] }) port_info.update({ 'coins': str(master_order.coin), 'consumeType': CONSUMETYPE.POSTPAID, }) else: coins = VirtualCoin(master_order.coin) for sub_order in sub_orders: coins += VirtualCoin(sub_order.coin) port_info.update({ 'coins': coins.mongo_amount, 'consumeType': CONSUMETYPE.MOBILE, }) # 免费地址组更新 master_order.attachParas.get('isFree') and port_info.update({'consumeType': 'isFree'}) return port_info def do_finished_event(self, master_order, sub_orders, merge_order_info): # type: (ConsumeRecord, [ConsumeRecord], dict)->None billing_method = self.event_data['billing_method'] if billing_method == CONSUMETYPE.POSTPAID: self.do_postpaid_time_finished(master_order, sub_orders, merge_order_info) else: self.do_prepaid_time_finished(master_order, sub_orders, merge_order_info) def insert_vCard_consume_record(self, vCard, order, success, consumeTotal, consumeDay): try: if success and consumeDay['count'] > 0: record = VCardConsumeRecord( orderNo=VCardConsumeRecord.make_no(order.logicalCode), openId=order.openId, nickname=order.nickname, cardId=str(vCard.id), dealerId=vCard.dealerId, devNo=order.devNo, devTypeCode = self.device.devTypeCode, devTypeName = self.device.devTypeName, logicalCode=order.logicalCode, groupId=order.groupId, address=order.address, groupNumber=order.groupNumber, groupName=order.groupName, attachParas=order.attachParas, consumeData=consumeTotal, consumeDayData=consumeDay ) record.save() except Exception as e: logger.exception(e) def do_postpaid_time_finished(self, master_order, sub_orders=None, merge_order_info=None): duration = self.event_data.get('duration', 0) elec = self.event_data.get('elec', 0) needPay = VirtualCoin(self.event_data.get('needPay', 0)) # 套餐最小使用金额 minFee = VirtualCoin(master_order.package.get('minFee') or 0) needPay = max(needPay, minFee) # 保护时间判断 refundProtectionTime = int(self.device.otherConf.get('serverConfigs', {}).get('refundProtectionTime', 5)) # 免费地址判断 if duration < refundProtectionTime or master_order.is_free: needPay = VirtualCoin(0) consumeDict = master_order.servicedInfo consumeDict.update({ 'reason': self.event_data.get('reasonDesc'), 'chargeIndex': str(master_order.used_port), DEALER_CONSUMPTION_AGG_KIND.DURATION: duration, DEALER_CONSUMPTION_AGG_KIND.ELEC: elec, }) logger.debug( 'orderNo = {}, orderType = isPostpaid, isFree={} usedTime = {}, needPayMoney = {}'.format( master_order.orderNo, master_order.is_free, duration, needPay)) # 1 获取金币汇率 coinRatio = float(self.device.otherConf.get('serverConfigs', {}).get('coinRatio', 1)) # 2 更新订单金额 master_order.coin = (needPay * coinRatio).mongo_amount master_order.money = needPay master_order.servicedInfo = consumeDict master_order.save() # 3 使用金币支付 self.pay_order(master_order) master_order.reload() extra = [{u'使用详情': '{}号端口, {}分钟'.format(master_order.used_port, duration)}] if master_order.is_free: extra.append({u'消费详情': u'免费使用'}) else: if master_order.status == ConsumeRecord.Status.FINISHED: if master_order.paymentInfo.get('via') == 'virtualCard': desc = u'(已使用优惠卡券抵扣本次消费)' master_order.update(coin=VirtualCoin(0), money=RMB(0)) # 结算完了进行退虚拟卡额度处理: try: if "rcdId" in master_order.paymentInfo: consumeRcdId = master_order.paymentInfo['rcdId'] else: consumeRcdId = master_order.paymentInfo['itemId'] vCardConsumeRcd = VCardConsumeRecord.objects.get(id=consumeRcdId) vCard = UserVirtualCard.objects.get(id=vCardConsumeRcd.cardId) vCard.refund_quota(vCardConsumeRcd, duration, 0.0, VirtualCoin(0).mongo_amount) except: pass else: desc = u'(已使用账户余额自动结算本次消费)' if needPay > VirtualCoin(0) else '' else: desc = u'(您的账户余额已不足以抵扣本次消费,请前往账单中心进行支付)' self.event_data['reasonDesc'] += desc extra.append({u'消费金额': u'{}元'.format(needPay * coinRatio)}) extra.append({u'用户余额': u'{}元'.format(master_order.user.calc_currency_balance(self.device.owner, self.device.group))}) self.notify_to_user(master_order.user.managerialOpenId, extra, url=self.device.deviceAdapter.custom_push_url(master_order, master_order.user)) def notify_to_user(self, openId, extra, url=None): group = self.device.group self.notify_user_service_complete( service_name='充电', openid=openId, port='', address=group['address'], reason=self.event_data.get('reasonDesc'), finished_time=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), extra=extra, url=url ) def pay_order(self, order): order.status = 'running' order.attachParas['packageId'] = order.package.get('packageId') order.save() order.s_to_e() def do_prepaid_time_finished(self, master_order, sub_orders, merge_order_info): duration, elec = self.event_data.get('duration', 0), self.event_data.get('elec', 0) usedFee = VirtualCoin(self.event_data['needPay']) coins = VirtualCoin(merge_order_info['coins']) # 保护时间判断 refundProtectionTime = int(self.device.otherConf.get('serverConfigs', {}).get('refundProtectionTime', 5)) if duration < refundProtectionTime: usedFee = VirtualCoin(0) backCoins = coins - usedFee logger.debug( 'orderNo = {}, orderType = isPostpaid, usedTime = {},coins = {}, usedFee = {}, backCoins = {}'.format( master_order.orderNo, duration, coins, usedFee, backCoins)) if master_order.is_temp_package: extra = [{u'使用详情': '{}号端口, {}分钟(临时套餐)'.format(master_order.used_port, duration)}] else: extra = [{u'使用详情': '{}号端口, {}分钟'.format(master_order.used_port, duration)}] if master_order.paymentInfo['via'] == 'free': extra.append({u'消费详情': u'免费使用'}) elif master_order.paymentInfo['via'] in ['netPay', 'coins', 'cash', 'coin']: all_money = RMB(0) all_refund_money = RMB(0) orders = [master_order] + sub_orders for _order in orders[::-1]: consumeDict = { 'reason': self.event_data.get('reasonDesc'), 'chargeIndex': str(master_order.used_port), DEALER_CONSUMPTION_AGG_KIND.DURATION: duration, DEALER_CONSUMPTION_AGG_KIND.ELEC: elec, } all_money += RMB(_order.money) need_back_coins, need_consume_coins, backCoins = self._calc_refund_info(backCoins, _order.coin) isTempPackage = 'isTempPackage' in _order.attachParas is_cash = (isTempPackage and _order.package.get('autoRefund', False)) or ( duration < refundProtectionTime) # 临时套餐 + 套餐内退费开关已打开 if is_cash: self.clear_frozen_user_balance(_order, need_back_coins, consumeDict, True) all_refund_money += RMB(consumeDict[DEALER_CONSUMPTION_AGG_KIND.REFUNDED_CASH]) else: self.clear_frozen_user_balance(_order, VirtualCoin(0), consumeDict, True) _order.update_service_info(consumeDict) if all_refund_money > RMB(0): extra.append({u'消费详情': '支付{}元, 退款{}元'.format(all_money, all_refund_money)}) else: extra.append({u'消费详情': '支付{}元'.format(all_money)}) else: logger.error('not net pay rather user virtual card pay. something is wrong.') return extra.append({u'用户余额': u'{}元'.format(master_order.user.calc_currency_balance(self.device.owner, self.device.group))}) self.notify_to_user(master_order.user.managerialOpenId, extra) def _calc_refund_info(self, backCoins, orderCoin): if backCoins >= orderCoin: need_back_coins = orderCoin need_consume_coins = VirtualCoin(0) backCoins -= orderCoin else: need_back_coins = backCoins need_consume_coins = orderCoin - need_back_coins backCoins = VirtualCoin(0) return need_back_coins, need_consume_coins, backCoins def clear_frozen_user_balance(self, order, backCoins, consumeDict, is_cash): # type:(ConsumeRecord, VirtualCoin, dict, bool) -> None paymentInfo = order.paymentInfo if not paymentInfo or paymentInfo['via'] not in ['netPay', 'coins', 'cash', 'coin']: raise Exception('method is not this process..') payCoins = VirtualCoin(paymentInfo['coins']) user = order.user if not user: logger.error( 'refund coins<{}> failure(no found user). consumeRecord = {}'.format(backCoins, repr(order))) return # 金币做一个保护 backCoins = min(payCoins, backCoins) if backCoins <= VirtualCoin(0): if is_cash: consumeDict.update({DEALER_CONSUMPTION_AGG_KIND.REFUNDED_CASH: RMB(0).mongo_amount}) else: consumeDict.update({DEALER_CONSUMPTION_AGG_KIND.REFUNDED_COINS: RMB(0).mongo_amount}) logger.error('refund coins<{}>. consumeRecord = {}'.format(backCoins, repr(order))) return itemId = paymentInfo.get('itemId') if itemId: rechargeRcd = RechargeRecord.objects.filter(id=itemId, isQuickPay=True, result='success', devNo=order.devNo).first() else: rechargeRcd = None if is_cash and rechargeRcd: payRMB = RMB(rechargeRcd.money) refundRMB = payRMB * (float(backCoins) / float(payCoins)) # 冻结金额全部扣除 user.clear_frozen_balance(str(order.id), paymentInfo['deduct'], back_coins=VirtualCoin(0), consume_coins=VirtualCoin(payCoins)) try: refund_order = refund_cash( rechargeRcd, refundRMB, VirtualCoin(0), user = user) # type: RefundMoneyRecord if not refund_order: logger.error( 'refund cash<{}> failure. recharge = {}'.format(refundRMB, repr(rechargeRcd))) except Exception: logger.exception(traceback.format_exc()) consumeDict.update({DEALER_CONSUMPTION_AGG_KIND.REFUNDED_CASH: refundRMB.mongo_amount}) else: user.clear_frozen_balance(str(order.id), paymentInfo['deduct'], back_coins=backCoins, consume_coins=(payCoins - backCoins)) consumeDict.update({DEALER_CONSUMPTION_AGG_KIND.REFUNDED_COINS: backCoins.mongo_amount}) class PolicyOnlineCardStartAckEvent(IdStartAckEvent): def __init__(self, smartBox, event_data): super(PolicyOnlineCardStartAckEvent, self).__init__(smartBox, event_data, StartAckEventPreProcessor()) def post_before_start(self, order=None): # 记录处理的源数据报文 uart_source = getattr(order, 'uart_source', []) uart_source.append({ 'rece_running': self.event_data.get('source'), 'time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') }) order.uart_source = uart_source order.save() def post_after_start(self, order=None): self.card.reload() # 通知用户,已经扣费 title = make_title_from_dict([ {u'设备地址': u'{}'.format(self.device.group.address)}, {u'设备编号': u'{}-{}'.format(self.device['logicalCode'], order.used_port)}, {u'实体卡': u'{}--No:{}'.format(self.card.cardName or self.card.nickName, self.card.cardNo)}, {u'本次消费': u'{} 元'.format(order.coin)}, {u'卡余额': u'{} 元'.format(self.card.balance)}, ]) start_time_stamp = self.event_data.get('sts') start_time = datetime.datetime.fromtimestamp(start_time_stamp) self.notify_user( self.card.managerialOpenId, 'dev_start', **{ 'title': title, 'things': u'刷卡消费', 'remark': u'感谢您的支持!', 'time': start_time.strftime(Const.DATETIME_FMT), 'url': self.deviceAdapter.custom_push_url(order, order.user) } ) def post_before_finish(self, order=None): # 记录处理的源数据报文 uart_source = getattr(order, 'uart_source', []) uart_source.append({ 'rece_finished': self.event_data.get('source'), 'time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') }) order.uart_source = uart_source order.save() def post_after_finish(self, order=None): pass def merge_order(self, master_order, sub_orders): # type:(ConsumeRecord, list)->dict billing_method = self.event_data.get('billing_method') start_time = Arrow.fromdatetime(master_order.startTime, tzinfo=settings.TIME_ZONE) if billing_method == CONSUMETYPE.POSTPAID: portDict = { 'coins': '0.0', 'money': '0.0', 'start_time': start_time.format(Const.DATETIME_FMT), 'estimatedTs': int(start_time.timestamp + 3600 * 12), 'consumeType': CONSUMETYPE.POSTPAID } else: portDict = { 'coins': str(VirtualCoin(self.event_data.get('amount', 0))), 'money': str(VirtualCoin(self.event_data.get('amount', 0))), 'start_time': start_time.format(Const.DATETIME_FMT), 'estimatedTs': int(start_time.timestamp + 3600 * 12), 'consumeType': CONSUMETYPE.CARD } return portDict def _do_prepaid_finish(self, order, merge_order_info): # type: (ConsumeRecord, dict)->None duration, elec = self.event_data.get('duration', 0.0), self.event_data.get('elec', 0.0), consumeDict = { 'reason': self.event_data.get('reasonDesc', None), } # 默认状态不退费 coins = VirtualCoin(self.event_data['amount']) useFee = VirtualCoin(self.event_data['amount']) backCoins = VirtualCoin(0) # 保护时间判断 refundProtectionTime = int(self.device.otherConf.get('serverConfigs', {}).get('refundProtectionTime', 5)) auto_refund = self.device.policyTemp.get('forIdcard', {}).get('autoRefund', False) if duration < refundProtectionTime: useFee = VirtualCoin(0) backCoins = coins - useFee else: if auto_refund: useFee = VirtualCoin(self.event_data['needPay']) backCoins = coins - useFee logger.debug('{} auto refund enable switch is {}, coins={}, backCoins={}'.format( repr(self.device), auto_refund, coins, backCoins)) # 分批塞入订单信息 master_info = { 'order_id': self.event_data['order_id'], 'fee': order.coin, } order_processing_list = [master_info] if 'sub' in self.event_data: order_processing_list += self.event_data['sub'] # 订单服务信息与退款处理 for _info in order_processing_list[::-1]: _order = ConsumeRecord.objects.filter(devNo=self.device.devNo, startKey=_info['order_id']).first() if not _order: continue consumeDict.update({ DEALER_CONSUMPTION_AGG_KIND.CONSUME_CARD: _order.coin, }) # 全退 if backCoins >= VirtualCoin(_order.coin): consumeDict.update({ DEALER_CONSUMPTION_AGG_KIND.CONSUME_CARD: _order.coin.mongo_amount, DEALER_CONSUMPTION_AGG_KIND.REFUND_CARD: _order.coin.mongo_amount, }) self.card.clear_frozen_balance(str(_order.id), _order.coin) self.record_refund_money_for_card(_order.coin, str(self.card.id), orderNo=order.orderNo) backCoins -= _order.coin else: # 部分退 consumeDict.update({ DEALER_CONSUMPTION_AGG_KIND.CONSUME_CARD: _order.coin.mongo_amount, DEALER_CONSUMPTION_AGG_KIND.REFUND_CARD: backCoins.mongo_amount, }) self.card.clear_frozen_balance(str(_order.id), backCoins) self.record_refund_money_for_card(backCoins, str(self.card.id), orderNo=order.orderNo) backCoins = VirtualCoin(0) _order.update_service_info(consumeDict) consumeDict.update({ DEALER_CONSUMPTION_AGG_KIND.DURATION: duration, DEALER_CONSUMPTION_AGG_KIND.ELEC: elec, # DEALER_CONSUMPTION_AGG_KIND.ELECFEE: self.deviceAdapter.calc_elec_fee(elec), }) order.update_service_info(consumeDict) self.card.reload() extra = [ {u'在线卡片': '{}--No:{}'.format(self.card.cardName, self.card.cardNo)}, {u'使用详情': '{}号端口, {}分钟'.format(order.used_port, duration)} ] if (coins - useFee) > VirtualCoin(0): extra.append({u'消费详情': '支付{}元,退费{}元'.format(coins, coins - useFee)}) else: extra.append({u'消费详情': '支付{}元'.format(coins)}) extra.append({u'卡片余额': '{}元'.format(self.card.balance.mongo_amount)}) self.notify_user_service_complete( service_name='充电', openid=self.card.managerialOpenId, port='', address=order.address, reason=self.event_data.get('reasonDesc'), finished_time=order.finishedTime.strftime('%Y-%m-%d %H:%M:%S'), extra=extra) # 更新一次缓存 self.deviceAdapter.async_update_portinfo_from_dev() def do_finished_event(self, order, merge_order_info): # type:(ConsumeRecord, dict)->None self._do_prepaid_finish(order, merge_order_info) def checkout_order(self, order): # 在线卡 执行扣费 fee = VirtualCoin(order.coin) self.card.freeze_balance(transaction_id=str(order.id), fee=fee) def _calc_refund_info(self, backCoins, orderCoin): if backCoins >= orderCoin: need_back_coins = orderCoin need_consume_coins = VirtualCoin(0) backCoins -= orderCoin else: need_back_coins = backCoins need_consume_coins = orderCoin - need_back_coins backCoins = VirtualCoin(0) return need_back_coins, need_consume_coins, backCoins