123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- # -*- 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, Union, Any
- from apilib.monetary import VirtualCoin, RMB
- from apps.web.common.transaction import OrderNoMaker, OrderMainType, RefundSubType
- from apps.web.common.transaction.pay import RefundManager
- from apps.web.constant import USER_RECHARGE_TYPE, PARTITION_ROLE, 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.jd.exceptions import JDPayException
- from library.jdopen import JdOpenException
- 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<orderNo = {}, totalFee={}>, sub<orderNo = {}, totalFee={}> '
- '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<orderNo = {}, totalFee={}>, sub<orderNo = {}, totalFee={}> '
- '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:
- 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_order_record = self.paySubOrder.new_refund_cash_order(
- self.refundOrder, split_map) # type: RechargeRecord
- # 将订单的状态切换为 正在处理中
- self.refundOrder.processing()
- # 扣除实体的金额(用户或者实体卡)
- frozen_callable(self.refundOrder, **kwargs)
- # self.deduct_from_refund_order(self.refundOrder)
- 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))
- elif payGateway.pay_app_type == PayAppType.JD_AGGR:
- 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'退费',
- refund_url = REFUND_NOTIFY_URL.WECHAT_REFUND_BACK,
- billSplitList = payGateway.refund_bill_split_list(refund_order_record.partition_map))
- except JDPayException 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('JDAGGRE Refund request successfully! return = {}'.format(result))
- elif payGateway.pay_app_type == PayAppType.RCU:
- # RUC 河南农村信用社 这个暂时不修改 不做回调处理 申请成功即认为成功
- 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'退费')
- if result['TransStatus'] not in ['1', '2']: # 只要结果不是交易失败 或者交易撤销 都算退款成功
- finishedTime = datetime.datetime.now()
- matched = self.refundOrder.succeed("", finishedTime = finishedTime)
- if matched:
- refund_post_pay(self.refundOrder, finishedTime)
- self.refundOrder.update(attachParas__refund_to_user = result)
- logger.debug('RCU Refund request successfully! return = {}'.format(result))
- else:
- raise UserServerException(u'已经有退款订单正在运行')
- else:
- logger.info('refund failed , refund orderNo = {} reason = {}'.format(self.refundOrder.orderNo,
- result['TransStatus']))
- self.refundOrder.fail(errorCode = result["TransStatus"])
- raise UserServerException(result["TransStatus"])
- elif payGateway.pay_app_type == PayAppType.JD_OPEN:
- 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'退费',
- callbackUrl = REFUND_NOTIFY_URL.JDOPEN_REFUND_BACK,
- ledgerInfoList = payGateway.refund_bill_split_list(refund_order_record.partition_map))
- except JdOpenException 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('JDAGGRE 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)
|