# -*- coding: utf-8 -*- # !/usr/bin/env python import datetime import logging import time import uuid from pymongo.errors import DuplicateKeyError from typing import TYPE_CHECKING, Dict, Any from apilib.monetary import VirtualCoin, RMB from apps.web.common.transaction.pay import RefundManager from apps.web.constant import USER_RECHARGE_TYPE, RechargeRecordVia from apps.web.core import PayAppType, ROLE from apps.web.core.exceptions import ParameterError from apps.web.core.payment import PaymentGateway from apps.web.dealer.define import DEALER_INCOME_SOURCE from apps.web.dealer.proxy import DealerIncomeProxy from apps.web.device.models import Group from apps.web.exceptions import UserServerException from apps.web.user.conf import REFUND_NOTIFY_URL from apps.web.user.models import MyUser, RechargeRecord, RefundMoneyRecord from library.alipay import AliException from library.wechatbase.exceptions import WeChatPayException logger = logging.getLogger(__name__) if TYPE_CHECKING: pass 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, deductCoins, **kwargs): # type:(RechargeRecord, RMB, VirtualCoin, Dict[str, Any])->RefundMoneyRecord """ 新的执行退款 为了保持导包顺序不变 :param deductCoins: :param recharge_record: :param refundFee: :param kwargs 用户为资金实体的情况下, 传入user和minus_total_consume参数 :return: """ if recharge_record.via in [RechargeRecordVia.Balance, RechargeRecordVia.Cash, RechargeRecordVia.StartDevice]: return RefundCash(recharge_record, refundFee, deductCoins).execute( frozen_callable = frozen_refund_for_balance, **kwargs) else: raise UserServerException(u'不支持该类型订单退款') class RefundCash(object): # 最长的查询分账时间 MAX_LEDGER_CHECK_TIME = 15 def __init__(self, rechargeOrder, refundFee, deductCoins): # type:(RechargeRecord, RMB, VirtualCoin) -> None self.paySubOrder = rechargeOrder self.payOrder = self.paySubOrder.payOrder self.refundFee = refundFee self.deductCoins = deductCoins # self._nextSeq = 1 @property def outTradeNo(self): """ 交易单号 :return: """ return self.payOrder.orderNo @property def totalFee(self): return self.payOrder.money @property def totalCoins(self): return self.payOrder.coins @property def subTotalFee(self): return self.paySubOrder.money @property def subTotalCoins(self): return self.paySubOrder.coins def pre_check(self): """ 退款的预检查 :return: """ if self.refundFee <= RMB(0) or self.refundFee > self.totalFee or self.refundFee > self.subTotalFee: raise ParameterError(u"退费金额错误") if self.deductCoins < VirtualCoin( 0) or self.deductCoins > self.totalCoins or self.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))) # TODO 考虑回调的方式进行 time.sleep(5) self.paySubOrder.reload() proxy = DealerIncomeProxy.objects.filter( ref_id = self.paySubOrder.id).first() # type: DealerIncomeProxy if not proxy: raise UserServerException(u"订单尚未分账,无法退款") return proxy def create_refund_order(self): refundOrder = RefundMoneyRecord.issue(self.paySubOrder, self.refundFee, self.deductCoins) refundOrder.pay_sub_order = self.paySubOrder return refundOrder def execute(self, frozen_callable, **kwargs): """ 执行退款的动作 对于经销商商户的流程: 检查 >> 建单 >> 扣除用户金额 >> 退款 >> 收到退款成功通知后建立负收益单和扣除经销商的记录金额 对于资金池商的流程: 检查 >> 建单 >> 扣除用户金额 >> 建立负单和扣除经销商的记录金额 >> 退款 :return: """ proxy = self.pre_check() payGateway = PaymentGateway.clone_from_order(self.payOrder) # type: PaymentGateway try: refundOrder = self.create_refund_order() # type: RefundMoneyRecord except DuplicateKeyError: raise UserServerException(u'已经有退款订单正在进行') if str(self.paySubOrder.id) == str(self.payOrder.id): logger.info( 'refund paras, orderNo = {} refundOrderNo = {} refundFee = {} totalFee = {}'.format( self.paySubOrder.orderNo, refundOrder.orderNo, self.refundFee, self.subTotalFee) ) else: logger.info( 'refund paras, mix, sub ' 'refundOrderNo = {} refundFee = {} '.format( self.payOrder.orderNo, self.totalFee, self.paySubOrder.orderNo, self.subTotalFee, refundOrder.orderNo, self.refundFee) ) refund_recharge_order = self.paySubOrder.new_refund_cash_order(refundOrder) # type: RechargeRecord frozen_callable(refundOrder, **kwargs) # 对资金实体进行退款(用户余额,卡余额等) try: if payGateway.pay_app_type == PayAppType.ALIPAY: # 支付宝的退款方式 # 支付宝的退款很特殊,接口状态以及业务状态均在同步接口中返回 其中 code = 10000 表示接口成功 即申请成功了 fund_change= Y 表示退款成功 # 而当接口状态成功 code=10000 但是资金未发生变动 fund_change=N 的时候,则退款是不成功的(最好需要轮询一次),此时不改变退款单的状态 try: result = payGateway.refund_to_user( out_trade_no = self.outTradeNo, out_refund_no = refundOrder.orderNo, refund_fee = self.refundFee, total_fee = self.totalFee, refund_reason = u'退费') except AliException as e: logger.info('refund failed , refund orderNo = {} reason = {}'.format(refundOrder.orderNo, e)) raise UserServerException('{}({})'.format(e.errMsg, e.errCode)) if result["code"] != "10000": refundOrder.fail(errorCode = "{}-{}".format(result["code"], result.get("sub_code")), errorDesc = "{}-{}".format(result["msg"], result.get("sub_msg"))) logger.info('ALIPAY Refund request successfully! return = {}'.format(result)) elif payGateway.pay_app_type in [PayAppType.WECHAT, PayAppType.WECHAT_MINI]: try: result = payGateway.refund_to_user( out_trade_no = self.outTradeNo, out_refund_no = refundOrder.orderNo, refund_fee = self.refundFee, total_fee = self.totalFee, refund_reason = u'退费', notify_url = REFUND_NOTIFY_URL.WECHAT_REFUND_BACK) except WeChatPayException as e: logger.info('refund failed , refund orderNo = {} reason = {}'.format(refundOrder.orderNo, e)) refundOrder.fail(errorCode = e.errCode, errorDesc = e.errMsg) raise UserServerException('{}({})'.format(e.errMsg, e.errCode)) logger.info('WECHAT Refund request successfully! return = {}'.format(result)) except UserServerException as se: logger.error(se.message) raise se except Exception as ee: # 这一步就不再更改订单的状态 由于不知道是退款前出错还是退款后出错 使用poll拉取订单状态来更新 logger.exception(ee) raise UserServerException(ee.message) finally: # 资金池方式下,直接记录负单.所有对账时间都以系统内时间为准 if payGateway.occupant.role == ROLE.agent: from apps.web.report.ledger import Ledger ledger = Ledger(USER_RECHARGE_TYPE.REFUND_CASH, refund_recharge_order) ledger.execute(journal = False, stats = True, check = False) return refundOrder class RetryRefundCash(object): def __init__(self, refundOrder): # type:(RefundMoneyRecord) -> None self.paySubOrder = refundOrder.pay_sub_order self.payOrder = self.paySubOrder.payOrder self.refundFee = refundOrder.money self.deductCoins = refundOrder.coins self.refundOrder = refundOrder # self._nextSeq = 1 @property def outTradeNo(self): """ 交易单号 :return: """ return self.payOrder.orderNo @property def totalFee(self): return self.payOrder.money @property def totalCoins(self): return self.payOrder.coins @property def subTotalFee(self): return self.paySubOrder.money @property def subTotalCoins(self): return self.paySubOrder.coins def pre_check(self): """ 退款的预检查 :return: """ if self.refundFee <= RMB(0) or self.refundFee > self.totalFee or self.refundFee > self.subTotalFee: raise ParameterError(u"退费金额错误") if self.deductCoins < VirtualCoin( 0) or self.deductCoins > self.totalCoins or self.deductCoins > self.subTotalCoins: raise ParameterError(u"扣除用户金币数目错误") if self.refundOrder.is_closed or self.refundOrder.is_success: raise UserServerException(u'已经完结订单不能重试') proxy = DealerIncomeProxy.objects.filter( ref_id = self.paySubOrder.id).first() # type: DealerIncomeProxy if not proxy: raise UserServerException(u"订单尚未分账,无法退款") return proxy def execute(self, frozen_callable, **kwargs): """ 执行退款的动作 对于经销商商户的流程: 检查 >> 建单 >> 扣除用户金额 >> 退款 >> 收到退款成功通知后建立负收益单和扣除经销商的记录金额 对于资金池商的流程: 检查 >> 建单 >> 扣除用户金额 >> 建立负单和扣除经销商的记录金额 >> 退款 :return: """ proxy = self.pre_check() payGateway = PaymentGateway.clone_from_order(self.payOrder) # type: PaymentGateway if str(self.paySubOrder.id) == str(self.payOrder.id): logger.info( 'retry refund paras, orderNo = {} refundOrderNo = {} refundFee = {} totalFee = {}'.format( self.paySubOrder.orderNo, self.refundOrder.orderNo, self.refundFee, self.subTotalFee) ) else: logger.info( 'retry refund paras, mix, sub ' 'refundOrderNo = {} refundFee = {} '.format( self.payOrder.orderNo, self.totalFee, self.paySubOrder.orderNo, self.subTotalFee, self.refundOrder.orderNo, self.refundFee) ) puller = RefundManager().get_poller(payGateway.pay_app_type) puller(self.refundOrder).pull(payGateway, self.payOrder, refund_post_pay) self.refundOrder.reload() if self.refundOrder.is_success or self.refundOrder.is_closed: logger.debug('refund order {} has been finished.'.format(str(self.refundOrder))) return refund_order_record = self.refundOrder.refund_order_record if not refund_order_record: split_map = proxy.partition_map refund_order_record = self.paySubOrder.new_refund_cash_order( self.refundOrder, split_map) # type: RechargeRecord # 将订单的状态切换为 正在处理中 self.refundOrder.processing() # 扣除实体的金额(用户或者实体卡) frozen_callable(self.refundOrder, **kwargs) try: if payGateway.pay_app_type == PayAppType.ALIPAY: # 支付宝的退款方式 # 支付宝的退款很特殊,接口状态以及业务状态均在同步接口中返回 其中 code = 10000 表示接口成功 即申请成功了 fund_change= Y 表示退款成功 # 而当接口状态成功 code=10000 但是资金未发生变动 fund_change=N 的时候,则退款是不成功的(最好需要轮询一次),此时不改变退款单的状态 try: result = payGateway.refund_to_user( out_trade_no = self.outTradeNo, out_refund_no = self.refundOrder.orderNo, refund_fee = self.refundFee, total_fee = self.totalFee, refund_reason = u'退费') except AliException as e: logger.info('refund failed , refund orderNo = {} reason = {}'.format(self.refundOrder.orderNo, e)) raise UserServerException('{}({})'.format(e.errMsg, e.errCode)) if result["code"] != "10000": self.refundOrder.fail(errorCode = "{}-{}".format(result["code"], result.get("sub_code")), errorDesc = "{}-{}".format(result["msg"], result.get("sub_msg"))) logger.info('ALIPAY Refund request successfully! return = {}'.format(result)) elif payGateway.pay_app_type == PayAppType.WECHAT: try: result = payGateway.refund_to_user( out_trade_no = self.outTradeNo, out_refund_no = self.refundOrder.orderNo, refund_fee = self.refundFee, total_fee = self.totalFee, refund_reason = u'退费', notify_url = REFUND_NOTIFY_URL.WECHAT_REFUND_BACK) except WeChatPayException as e: logger.info('refund failed , refund orderNo = {} reason = {}'.format(self.refundOrder.orderNo, e)) self.refundOrder.fail(errorCode = e.errCode, errorDesc = e.errMsg) raise UserServerException('{}({})'.format(e.errMsg, e.errCode)) logger.info('WECHAT Refund request successfully! return = {}'.format(result)) else: self.refundOrder.fail(errorDesc = u"不支持的退款模式") raise UserServerException(u"不支持的退款模式") except UserServerException as se: logger.error(se.message) raise se except Exception as ee: # 这一步就不再更改订单的状态 由于不知道是退款前出错还是退款后出错 使用poll拉取订单状态来更新 logger.exception(ee) raise UserServerException(ee.message) finally: # 资金池方式下,直接记录负单.所有对账时间都以系统内时间为准 if payGateway.occupant.role == ROLE.agent: from apps.web.report.ledger import Ledger ledger = Ledger(USER_RECHARGE_TYPE.REFUND_CASH, refund_order_record) ledger.execute(journal = False, stats = True, check = False) return self.refundOrder def refund_post_pay(refundOrder, finishedTime): # type: (RefundMoneyRecord, datetime)->None refundOrder.user.commit_refund_cash(refundOrder) refund_order_record = refundOrder.refund_order_record # type: RechargeRecord refund_order_record.finishedTime = finishedTime refund_order_record.result = RechargeRecord.PayResult.SUCCESS refund_order_record.save() # 记录资金池的变动 if not refund_order_record.is_ledgered: from apps.web.report.ledger import Ledger ledger = Ledger(DEALER_INCOME_SOURCE.REFUND_CASH, refund_order_record) ledger.execute(stats=True) def frozen_refund_for_balance(refundOrder, user = None, minus_total_consume = VirtualCoin(0)): # type: (RefundMoneyRecord, MyUser, VirtualCoin)->bool if user: if user.openId != refundOrder.pay_sub_order.openId: raise UserServerException(u"用户参数错误") else: if user.groupId != refundOrder.pay_sub_order.groupId: user = refundOrder.pay_sub_order.user else: user = refundOrder.pay_sub_order.user if not user: raise UserServerException(u'用户不存在') return user.prepare_refund_cash(refundOrder, minus_total_consume)