# -*- coding: utf-8 -*- # !/usr/bin/env python import datetime import logging import time import uuid from bson import ObjectId from mongoengine import NotUniqueError from typing import TYPE_CHECKING, Dict from apilib.monetary import VirtualCoin, RMB from apilib.utils import flatten from apps.web.common.proxy import ClientDealerIncomeModelProxy from apps.web.common.transaction.refund import RefundCashMixin, RefundManager from apps.web.constant import USER_RECHARGE_TYPE, PARTITION_ROLE, RechargeRecordVia, AppPlatformType from apps.web.core import PayAppType, ROLE from apps.web.core.exceptions import ParameterError from apps.web.core.payment import WithdrawGateway from apps.web.dealer.define import DEALER_INCOME_SOURCE, DEALER_INCOME_TYPE from apps.web.device.models import Group from apps.web.exceptions import UserServerException from apps.web.user.models import MyUser, RechargeRecord, RefundMoneyRecord, Card, UserVirtualCard, MonthlyPackage logger = logging.getLogger(__name__) if TYPE_CHECKING: from apps.web.dealer.proxy import DealerIncomeProxy def refund_money(device, money, openId): if VirtualCoin(money) <= VirtualCoin(0): logger.debug('{} is zero. not to refund.'.format(VirtualCoin(money))) return groupId = device['groupId'] group = Group.get_group(groupId) if group.is_free: logger.info('group(%s) is set to free, refund_money will not proceed' % (device['groupId'],)) return user = MyUser.objects(openId = openId, groupId = groupId).get() # type: MyUser user.refund(VirtualCoin(money)) # 添加一条充值记录 orderNo = str(uuid.uuid4()) try: newRcd = RechargeRecord(orderNo = orderNo, coins = money, openId = openId, groupId = device['groupId'], devNo = device['devNo'], logicalCode = device['logicalCode'], ownerId = device['ownerId'], groupName = group['groupName'], groupNumber = device['groupNumber'], address = group['address'], wxOrderNo = u'设备退币', devTypeName = device.devTypeName, nickname = '', result = 'success', via = 'refund') newRcd.save() except Exception, e: logger.exception('update record for feedback coins error=%s,orderNo=%s' % (e, orderNo)) def refund_cash(recharge_record, refundFee, **kwargs): # type:(RechargeRecord, RMB, Dict)->RefundMoneyRecord """ 新的执行退款 为了保持导包顺序不变 :param recharge_record: :param refundFee: :type kwargs: object :return: """ if recharge_record.via in [ RechargeRecordVia.Balance, RechargeRecordVia.Cash, RechargeRecordVia.Card, RechargeRecordVia.VirtualCard, RechargeRecordVia.MonthlyPackage, ]: return RefundCash(recharge_record, refundFee, **kwargs).execute( frozen_callable = frozen_refund_func, refund_callable = refund_post_pay) else: raise UserServerException(u'不支持该类型订单退款') class RefundCash(RefundCashMixin): MAX_LEDGER_CHECK_TIME = 15 # 最长的查询分账时间 def __init__(self, rechargeOrder, refundFee, **kwargs): # type:(RechargeRecord, RMB, dict) -> None super(RefundCash, self).__init__(rechargeOrder, refundFee) self.extraInfo = kwargs # self._nextSeq = 1 def check_wallet(self, proxy, order): partition_map = proxy.partition_map for partition in list(flatten(partition_map.values())): if partition['role'] == PARTITION_ROLE.OWNER: leftBalance = order.owner.sub_balance( income_type = DEALER_INCOME_TYPE.DEVICE_INCOME, source_key = order.withdraw_source_key, only_ledger = True) if abs(RMB(partition['money'])) > leftBalance: raise UserServerException(u"您的钱包余额不足,无法退款。") elif partition['role'] == PARTITION_ROLE.PARTNER: from apps.web.dealer.models import Dealer dealer = Dealer.objects(id = partition['id']).first() leftBalance = dealer.sub_balance( income_type = DEALER_INCOME_TYPE.DEVICE_INCOME, source_key = order.withdraw_source_key) if abs(RMB(partition['money'])) > leftBalance: raise UserServerException(u"您的分账合伙人钱包余额不足,无法退款(1001)。") else: from apps.web.agent.models import Agent from apps.web.agent.define import AGENT_INCOME_TYPE agent = Agent.objects(id = partition['id']).first() leftBalance = agent.sub_balance( income_type = AGENT_INCOME_TYPE.DEALER_DEVICE_FEE, source_key = order.withdraw_source_key) if abs(RMB(partition['money'])) > leftBalance: raise UserServerException(u"您的分账合伙人钱包余额不足,无法退款(1002)。") def pre_check(self): """ 退款的预检查 :return: """ if self.refundFee <= RMB(0) or self.refundFee > self.subTotalFee: raise ParameterError(u"退费金额错误") if 'deductCoins' in self.extraInfo: deductCoins = self.extraInfo['deductCoins'] if deductCoins < VirtualCoin( 0) or deductCoins > self.subTotalCoins: raise ParameterError(u"扣除用户金币数目错误") check_end_time = int(time.time()) + self.MAX_LEDGER_CHECK_TIME if not self.paySubOrder.is_success: raise UserServerException(u'非成功订单无法进行退款') while not self.paySubOrder.is_ledgered and int(time.time()) < check_end_time: logger.debug('{} is not allocated. wait to be allocated.'.format(repr(self.paySubOrder))) time.sleep(5) self.paySubOrder.reload() proxy = ClientDealerIncomeModelProxy.get_one(ref_id = self.paySubOrder.id) # type: DealerIncomeProxy if not proxy: raise UserServerException(u"订单尚未分账,无法退款(10002)") if self.paySubOrder.gateway == AppPlatformType.ALIPAY: over_time = 90 * 24 * 60 * 60 else: over_time = 365 * 24 * 60 * 60 if (datetime.datetime.now() - self.paySubOrder.finishedTime).total_seconds() >= over_time: raise UserServerException(u'超期订单不允许退款') checkWallet = self.extraInfo.get('checkWallet', False) if checkWallet and WithdrawGateway.is_ledger(source_key = self.paySubOrder.withdraw_source_key): self.check_wallet(proxy, self.paySubOrder) return proxy def create_refund_order(self, **extraInfo): refundOrder = RefundMoneyRecord.issue( self.paySubOrder, self.refundFee, **extraInfo) refundOrder.pay_sub_order = self.paySubOrder return refundOrder def execute(self, frozen_callable, refund_callable, notify_url = None): """ 执行退款的动作 对于经销商商户的流程: 检查 >> 建单 >> 扣除用户金额 >> 退款 >> 收到退款成功通知后建立负收益单和扣除经销商的记录金额 对于资金池商的流程: 检查 >> 建单 >> 扣除用户金额 >> 建立负单和扣除经销商的记录金额 >> 退款 :return: """ proxy = self.pre_check() try: refundOrder = self.create_refund_order(**self.extraInfo) # type: RefundMoneyRecord except NotUniqueError: raise UserServerException(u'已经有退款订单正在进行') logger.info('refund paras: {} {}'.format(refundOrder.orderNo, self.refund_paras)) split_map = proxy.partition_map refund_income_order = self.paySubOrder.issue_refund_income_order( refundOrder, split_map) # type: RechargeRecord frozen_callable(refundOrder) # 对资金实体进行退款冻结(用户余额,卡余额等) try: self.submit_refund( refundOrder, refund_income_order.partition_map, u'现金退款', notify_url or refundOrder.notify_url, refund_callable) except Exception: import traceback logger.warning( 'Refund request failure! orderNo = {}; e = {}'.format(refundOrder.orderNo, traceback.format_exc())) finally: refundOrder.reload() if refundOrder.is_closed or refundOrder.is_success: # 终态已经调用了post_pay, 所以不在做任何处理 pass elif refundOrder.my_payment_gateway.occupant.role == ROLE.agent: # 资金池情况下认为成功, 冻结运营商金额 refund_income_order.result = RechargeRecord.PayResult.SUCCESS refund_income_order.finishedTime = datetime.datetime.now() refund_income_order.save() from apps.web.report.ledger import Ledger ledger = Ledger(refund_income_order.via, refund_income_order) ledger.execute(stats = True) return refundOrder class RetryRefundCash(RefundCashMixin): def __init__(self, refundOrder): # type:(RefundMoneyRecord) -> None super(RetryRefundCash, self).__init__(refundOrder.pay_sub_order, refundOrder.money) self.refundOrder = refundOrder def pre_check(self): """ 退款的预检查 :return: """ if self.refundFee <= RMB(0) or self.refundFee > self.subTotalFee: raise ParameterError(u"退费金额错误") deductCoins = self.refundOrder.deductCoins if deductCoins < VirtualCoin(0) or deductCoins > self.subTotalCoins: raise ParameterError(u"扣除用户金币数目错误") if not self.refundOrder.is_fail and not self.refundOrder.is_no_order: raise UserServerException(u'状态非错误的订单不能重试') proxy = ClientDealerIncomeModelProxy.get_one(ref_id = self.paySubOrder.id) if not proxy: raise UserServerException(u"订单尚未分账,无法退款(10002)") if self.refundOrder.checkWallet and WithdrawGateway.is_ledger(source_key = self.paySubOrder.withdraw_source_key): self.check_wallet(proxy, self.paySubOrder) return proxy def execute(self, frozen_callable, refund_callable, notify_url = None): """ 执行退款的动作 对于经销商商户的流程: 检查 >> 建单 >> 扣除用户金额 >> 退款 >> 收到退款成功通知后建立负收益单和扣除经销商的记录金额 对于资金池商的流程: 检查 >> 建单 >> 扣除用户金额 >> 建立负单和扣除经销商的记录金额 >> 退款 :return: """ proxy = self.pre_check() logger.info('retry refund paras: {} {}'.format(self.refundOrder.orderNo, self.refund_paras)) puller = RefundManager().get_poller(self.refundOrder.pay_app_type) done = puller(self.refundOrder).pull(refund_post_pay) if done: return self.refundOrder.reload() if not self.refundOrder.is_fail and not self.refundOrder.is_no_order: logger.debug('refund order {} is not in fail status.'.format(str(self.refundOrder))) return if self.refundOrder.retryCount > 10: matched = self.refundOrder.closed(errorCode = 'TIMEOUT', errorDesc = u'重试次数超限,退款失败') if matched: return refund_post_pay(self.refundOrder, False) refund_income_order = self.refundOrder.refund_income_order if not refund_income_order: if 'billSplitOfOwner' in self.paySubOrder.attachParas: # 老的商户分账模式 if "billSplitList" in self.paySubOrder.attachParas: owner_split = self.paySubOrder.attachParas['billSplitOfOwner'] owner_split['merchantId'] = owner_split.pop('splitBillMerchantEmail') owner_split['money'] = owner_split.pop('splitBillAmount') split_map = { PARTITION_ROLE.OWNER: [owner_split], PARTITION_ROLE.AGENT: [], PARTITION_ROLE.PARTNER: [] } for spliter in self.paySubOrder.attachParas['billSplitList']: spliter['merchantId'] = spliter.pop('splitBillMerchantEmail') spliter['money'] = spliter.pop('splitBillAmount') if spliter['role'] == PARTITION_ROLE.AGENT: split_map[PARTITION_ROLE.AGENT].append(spliter) elif spliter['role'] == PARTITION_ROLE.PARTNER: split_map[PARTITION_ROLE.PARTNER].append(spliter) else: raise UserServerException(u'错误的分账角色') else: owner_split = self.paySubOrder.attachParas['billSplitOfOwner'] owner_split['merchantId'] = owner_split.pop('splitBillMerchantEmail') owner_split['money'] = owner_split.pop('splitBillAmount') split_map = { PARTITION_ROLE.OWNER: [owner_split], PARTITION_ROLE.AGENT: [], PARTITION_ROLE.PARTNER: [] } else: split_map = proxy.partition_map refund_income_order = self.paySubOrder.issue_refund_income_order( self.refundOrder, split_map) # type: RechargeRecord succeed = self.refundOrder.retry_processing( changeOrderNo = (not self.refundOrder.is_no_order) and self.refundOrder.pay_app_type in [PayAppType.JD_OPEN]) if not succeed: logger.info( 'refund ignored. refund orderNo = {} reason = unique check failure.'.format(self.refundOrder.orderNo)) return self.refundOrder.reload() frozen_callable(self.refundOrder) try: self.submit_refund( self.refundOrder, refund_income_order.partition_map, u'现金退款', notify_url or self.refundOrder.notify_url, refund_callable) except Exception: import traceback logger.warning( 'Refund request failure! orderNo = {}; e = {}'.format(self.refundOrder, traceback.format_exc())) finally: self.refundOrder.reload() if self.refundOrder.is_closed or self.refundOrder.is_success: # 终态已经调用了post_pay, 所以不在做任何处理 pass elif self.refundOrder.my_payment_gateway.occupant.role == ROLE.agent: # 资金池情况下认为成功, 冻结运营商金额 if refund_income_order.result != RechargeRecord.PayResult.SUCCESS: refund_income_order.result = RechargeRecord.PayResult.SUCCESS refund_income_order.finishedTime = datetime.datetime.now() refund_income_order.save() if not refund_income_order.is_ledgered: from apps.web.report.ledger import Ledger ledger = Ledger(USER_RECHARGE_TYPE.REFUND_CASH, refund_income_order) ledger.execute(journal = False, stats = True, check = False) return self.refundOrder def refund_post_pay(refundOrder, success): # type: (RefundMoneyRecord, bool)->None try: refund_income_order = refundOrder.refund_income_order # type: RechargeRecord try: refPay = refund_income_order.extraInfo['refPay'] if isinstance(refund_income_order.extraInfo['refPay'], dict): refund_income_order.extraInfo['refPay'] = ObjectId(refPay.pop('objId')) refund_income_order.save() except: pass if success: refund_success_callback(refundOrder, refundOrder.finishedTime, refund_income_order) else: refund_fail_callback(refundOrder, refundOrder.finishedTime, refund_income_order) except Exception: import traceback logger.warning( 'Refund callback failure. orderNo = {}; e = {}'.format(refundOrder.orderNo, traceback.format_exc())) def refund_success_callback(refundOrder, finishedTime, refund_income_order): # type: (RefundMoneyRecord, datetime, RechargeRecord)->None rechargeOrder = refundOrder.pay_sub_order if rechargeOrder.via in [USER_RECHARGE_TYPE.RECHARGE, USER_RECHARGE_TYPE.RECHARGE_CASH]: refundOrder.user.commit_refund_cash(refundOrder) elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_CARD: if rechargeOrder.attachParas.get('terminalRecharge', False): pass else: card = Card.objects(id = rechargeOrder.attachParas['cardId']).first() # type: Card card.clear_frozen_balance(card.freeze_transaction_id('r'), VirtualCoin(0)) elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_VIRTUAL_CARD: userVirtualCard = UserVirtualCard.objects(id = rechargeOrder.attachParas['cardId']).first() userVirtualCard.commit_refund(refundOrder) elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_MONTHLY_PACKAGE: pass else: logger.debug('via({}) is not support.'.format(rechargeOrder.via)) return if not refund_income_order.is_ledgered: # 记录收益 refund_income_order.finishedTime = finishedTime refund_income_order.result = RechargeRecord.PayResult.SUCCESS refund_income_order.save() from apps.web.report.ledger import Ledger ledger = Ledger(refund_income_order.via, refund_income_order) ledger.execute(stats=True) else: # FIX预扣单状态.不是SUCCESS先改成SUCCESS if refund_income_order.result != RechargeRecord.PayResult.SUCCESS: refund_income_order.finishedTime = refund_income_order.dateTimeAdded refund_income_order.result = RechargeRecord.PayResult.SUCCESS refund_income_order.save() for item in rechargeOrder.extraInfo['refRefund']: if 'deductId' in item: if str(item['deductId']) == str(refund_income_order.id): item['status'] = RefundMoneyRecord.Status.SUCCESS item['finishedTime'] = refundOrder.finishedTime rechargeOrder.save() break elif str(item['objId']) == str(refund_income_order.id): item['status'] = RefundMoneyRecord.Status.SUCCESS item['deductId'] = ObjectId(item.pop('objId')) item['finishedTime'] = refundOrder.finishedTime rechargeOrder.save() break def refund_fail_callback(refundOrder, finishedTime, refund_income_order): rechargeOrder = refundOrder.pay_sub_order if rechargeOrder.via in [USER_RECHARGE_TYPE.RECHARGE, USER_RECHARGE_TYPE.RECHARGE_CASH]: user = refundOrder.pay_sub_order.myuser # type: MyUser user.revoke_refund_cash(refundOrder) elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_CARD: if rechargeOrder.attachParas.get('terminalRecharge', False): pass else: card = Card.objects(id = rechargeOrder.attachParas['cardId']).first() if not card: raise UserServerException(u'充值卡不存在') card.recover_frozen_balance(transaction_id = card.freeze_transaction_id('r'), fee = refundOrder.deductCoins) elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_VIRTUAL_CARD: userVirtualCard = UserVirtualCard.objects(id = rechargeOrder.attachParas['cardId']).first() if not userVirtualCard: raise UserServerException(u'虚拟卡不存在') userVirtualCard.revoke_refund(refundOrder) elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_MONTHLY_PACKAGE: monthlyPackage = MonthlyPackage.objects(id = rechargeOrder.attachParas['cardId']).first() if not monthlyPackage: raise UserServerException(u'包月套餐不存在') monthlyPackage.toggle_disable(isDisable = 0) else: raise UserServerException(u'不支持的退款订单类型') if refund_income_order.is_ledgered: # 如果已经扣款分账则建立一个退单收益单 # FIX预扣单状态.不是SUCCESS先改成SUCCESS if refund_income_order.result != RechargeRecord.PayResult.SUCCESS: refund_income_order.finishedTime = refund_income_order.dateTimeAdded refund_income_order.result = RechargeRecord.PayResult.SUCCESS refund_income_order.save() revoke_income_order = refund_income_order.issue_refund_revoke_order() from apps.web.report.ledger import Ledger ledger = Ledger(DEALER_INCOME_SOURCE.REVOKE_REFUND_CASH, revoke_income_order) ledger.execute(journal = False, stats = True, check = False) for item in rechargeOrder.extraInfo['refRefund']: if 'deductId' in item: if str(item['deductId']) == str(refund_income_order.id): item['status'] = RefundMoneyRecord.Status.CLOSED item['revokeId'] = revoke_income_order.id item['finishedTime'] = refundOrder.finishedTime rechargeOrder.save() break elif str(item['objId']) == str(refund_income_order.id): item['status'] = RefundMoneyRecord.Status.CLOSED item['deductId'] = ObjectId(item.pop('objId')) item['revokeId'] = revoke_income_order.id item['finishedTime'] = refundOrder.finishedTime rechargeOrder.save() break else: refund_income_order.finishedTime = finishedTime refund_income_order.result = RechargeRecord.PayResult.CANCEL refund_income_order.save() for item in rechargeOrder.extraInfo['refRefund']: if 'deductId' in item: if str(item['deductId']) == str(refund_income_order.id): item['status'] = RefundMoneyRecord.Status.CLOSED item['finishedTime'] = refundOrder.finishedTime rechargeOrder.save() break elif str(item['objId']) == str(refund_income_order.id): item['status'] = RefundMoneyRecord.Status.CLOSED item['deductId'] = item.pop('objId') item['finishedTime'] = refundOrder.finishedTime rechargeOrder.save() break def frozen_refund_func(refundOrder): # type: (RefundMoneyRecord)->None """ :param refundOrder: :return: """ rechargeOrder = refundOrder.pay_sub_order if rechargeOrder.via in [USER_RECHARGE_TYPE.RECHARGE, USER_RECHARGE_TYPE.RECHARGE_CASH]: user = refundOrder.pay_sub_order.myuser if not user: raise UserServerException(u'用户不存在') minus_total_consume = VirtualCoin(refundOrder.extraInfo.get('minus_total_consume', 0)) deduct_coins = refundOrder.deductCoins frozen_coins = refundOrder.frozenCoins logger.debug('MyUser prepare refund cash. money = {}, coins = {}, before = {}'.format( str(user.id), refundOrder.money, deduct_coins, user.balance )) user.prepare_refund_cash(refundOrder, deduct_coins, frozen_coins, minus_total_consume) elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_CARD: if rechargeOrder.attachParas.get('terminalRecharge', False): logger.debug('Card prepare refund cash. money = {}'.format( 'terminalRecharge', refundOrder.money )) else: card = Card.objects(id = rechargeOrder.attachParas['cardId']).first() if not card: raise UserServerException(u'充值卡不存在') logger.debug('Card prepare refund cash. money = {}, coins = {}, before = {}'.format( str(card.id), refundOrder.money, refundOrder.deductCoins, card.balance )) card.freeze_balance(transaction_id = card.freeze_transaction_id('r'), fee = refundOrder.deductCoins) elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_VIRTUAL_CARD: userVirtualCard = UserVirtualCard.objects(id=rechargeOrder.attachParas['cardId']).first() if not userVirtualCard: raise UserServerException(u'虚拟卡不存在') userVirtualCard.prepare_refund(refundOrder) elif rechargeOrder.via == USER_RECHARGE_TYPE.RECHARGE_MONTHLY_PACKAGE: monthlyPackage = MonthlyPackage.objects(id = rechargeOrder.attachParas['cardId']).first() if not monthlyPackage: raise UserServerException(u'包月套餐不存在') monthlyPackage.toggle_disable(isDisable = 1) else: logger.debug('via({}) is not support.'.format(rechargeOrder.via))