transaction_deprecated.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. import datetime
  4. import logging
  5. import time
  6. import uuid
  7. from pymongo.errors import DuplicateKeyError
  8. from typing import TYPE_CHECKING, Dict, Any
  9. from apilib.monetary import VirtualCoin, RMB
  10. from apps.web.common.transaction.pay import RefundManager
  11. from apps.web.constant import USER_RECHARGE_TYPE, RechargeRecordVia
  12. from apps.web.core import PayAppType, ROLE
  13. from apps.web.core.exceptions import ParameterError
  14. from apps.web.core.payment import PaymentGateway
  15. from apps.web.dealer.define import DEALER_INCOME_SOURCE
  16. from apps.web.dealer.proxy import DealerIncomeProxy
  17. from apps.web.device.models import Group
  18. from apps.web.exceptions import UserServerException
  19. from apps.web.user.conf import REFUND_NOTIFY_URL
  20. from apps.web.user.models import MyUser, RechargeRecord, RefundMoneyRecord
  21. from library.alipay import AliException
  22. from library.wechatbase.exceptions import WeChatPayException
  23. logger = logging.getLogger(__name__)
  24. if TYPE_CHECKING:
  25. pass
  26. def refund_money(device, money, openId):
  27. if VirtualCoin(money) <= VirtualCoin(0):
  28. logger.debug('{} is zero. not to refund.'.format(VirtualCoin(money)))
  29. return
  30. groupId = device['groupId']
  31. group = Group.get_group(groupId)
  32. if group.is_free:
  33. logger.info('group(%s) is set to free, refund_money will not proceed' % (device['groupId'],))
  34. return
  35. user = MyUser.objects(openId = openId, groupId = groupId).get() # type: MyUser
  36. user.refund(VirtualCoin(money))
  37. # 添加一条充值记录
  38. orderNo = str(uuid.uuid4())
  39. try:
  40. newRcd = RechargeRecord(orderNo = orderNo,
  41. coins = money, openId = openId, groupId = device['groupId'],
  42. devNo = device['devNo'], logicalCode = device['logicalCode'],
  43. ownerId = device['ownerId'],
  44. groupName = group['groupName'], groupNumber = device['groupNumber'],
  45. address = group['address'], wxOrderNo = u'设备退币',
  46. devTypeName = device.devTypeName, nickname = '',
  47. result = 'success', via = 'refund')
  48. newRcd.save()
  49. except Exception, e:
  50. logger.exception('update record for feedback coins error=%s,orderNo=%s' % (e, orderNo))
  51. def refund_cash(recharge_record, refundFee, deductCoins, **kwargs):
  52. # type:(RechargeRecord, RMB, VirtualCoin, Dict[str, Any])->RefundMoneyRecord
  53. """
  54. 新的执行退款 为了保持导包顺序不变
  55. :param deductCoins:
  56. :param recharge_record:
  57. :param refundFee:
  58. :param kwargs 用户为资金实体的情况下, 传入user和minus_total_consume参数
  59. :return:
  60. """
  61. if recharge_record.via in [RechargeRecordVia.Balance, RechargeRecordVia.Cash, RechargeRecordVia.StartDevice]:
  62. return RefundCash(recharge_record, refundFee, deductCoins).execute(
  63. frozen_callable = frozen_refund_for_balance, **kwargs)
  64. else:
  65. raise UserServerException(u'不支持该类型订单退款')
  66. class RefundCash(object):
  67. # 最长的查询分账时间
  68. MAX_LEDGER_CHECK_TIME = 15
  69. def __init__(self, rechargeOrder, refundFee, deductCoins): # type:(RechargeRecord, RMB, VirtualCoin) -> None
  70. self.paySubOrder = rechargeOrder
  71. self.payOrder = self.paySubOrder.payOrder
  72. self.refundFee = refundFee
  73. self.deductCoins = deductCoins
  74. # self._nextSeq = 1
  75. @property
  76. def outTradeNo(self):
  77. """
  78. 交易单号
  79. :return:
  80. """
  81. return self.payOrder.orderNo
  82. @property
  83. def totalFee(self):
  84. return self.payOrder.money
  85. @property
  86. def totalCoins(self):
  87. return self.payOrder.coins
  88. @property
  89. def subTotalFee(self):
  90. return self.paySubOrder.money
  91. @property
  92. def subTotalCoins(self):
  93. return self.paySubOrder.coins
  94. def pre_check(self):
  95. """
  96. 退款的预检查
  97. :return:
  98. """
  99. if self.refundFee <= RMB(0) or self.refundFee > self.totalFee or self.refundFee > self.subTotalFee:
  100. raise ParameterError(u"退费金额错误")
  101. if self.deductCoins < VirtualCoin(
  102. 0) or self.deductCoins > self.totalCoins or self.deductCoins > self.subTotalCoins:
  103. raise ParameterError(u"扣除用户金币数目错误")
  104. check_end_time = int(time.time()) + self.MAX_LEDGER_CHECK_TIME
  105. if not self.paySubOrder.is_success():
  106. raise UserServerException(u'非成功订单无法进行退款')
  107. while not self.paySubOrder.is_ledgered and int(time.time()) < check_end_time:
  108. logger.debug('{} is not allocated. wait to be allocated.'.format(repr(self.paySubOrder)))
  109. # TODO 考虑回调的方式进行
  110. time.sleep(5)
  111. self.paySubOrder.reload()
  112. proxy = DealerIncomeProxy.objects.filter(
  113. ref_id = self.paySubOrder.id).first() # type: DealerIncomeProxy
  114. if not proxy:
  115. raise UserServerException(u"订单尚未分账,无法退款")
  116. return proxy
  117. def create_refund_order(self):
  118. refundOrder = RefundMoneyRecord.issue(self.paySubOrder, self.refundFee, self.deductCoins)
  119. refundOrder.pay_sub_order = self.paySubOrder
  120. return refundOrder
  121. def execute(self, frozen_callable, **kwargs):
  122. """
  123. 执行退款的动作
  124. 对于经销商商户的流程: 检查 >> 建单 >> 扣除用户金额 >> 退款 >> 收到退款成功通知后建立负收益单和扣除经销商的记录金额
  125. 对于资金池商的流程: 检查 >> 建单 >> 扣除用户金额 >> 建立负单和扣除经销商的记录金额 >> 退款
  126. :return:
  127. """
  128. proxy = self.pre_check()
  129. payGateway = PaymentGateway.clone_from_order(self.payOrder) # type: PaymentGateway
  130. try:
  131. refundOrder = self.create_refund_order() # type: RefundMoneyRecord
  132. except DuplicateKeyError:
  133. raise UserServerException(u'已经有退款订单正在进行')
  134. if str(self.paySubOrder.id) == str(self.payOrder.id):
  135. logger.info(
  136. 'refund paras, orderNo = {} refundOrderNo = {} refundFee = {} totalFee = {}'.format(
  137. self.paySubOrder.orderNo, refundOrder.orderNo, self.refundFee, self.subTotalFee)
  138. )
  139. else:
  140. logger.info(
  141. 'refund paras, mix<orderNo = {}, totalFee={}>, sub<orderNo = {}, totalFee={}> '
  142. 'refundOrderNo = {} refundFee = {} '.format(
  143. self.payOrder.orderNo, self.totalFee, self.paySubOrder.orderNo, self.subTotalFee,
  144. refundOrder.orderNo, self.refundFee)
  145. )
  146. refund_recharge_order = self.paySubOrder.new_refund_cash_order(refundOrder) # type: RechargeRecord
  147. frozen_callable(refundOrder, **kwargs) # 对资金实体进行退款(用户余额,卡余额等)
  148. try:
  149. if payGateway.pay_app_type == PayAppType.ALIPAY:
  150. # 支付宝的退款方式
  151. # 支付宝的退款很特殊,接口状态以及业务状态均在同步接口中返回 其中 code = 10000 表示接口成功 即申请成功了 fund_change= Y 表示退款成功
  152. # 而当接口状态成功 code=10000 但是资金未发生变动 fund_change=N 的时候,则退款是不成功的(最好需要轮询一次),此时不改变退款单的状态
  153. try:
  154. result = payGateway.refund_to_user(
  155. out_trade_no = self.outTradeNo, out_refund_no = refundOrder.orderNo,
  156. refund_fee = self.refundFee, total_fee = self.totalFee, refund_reason = u'退费')
  157. except AliException as e:
  158. logger.info('refund failed , refund orderNo = {} reason = {}'.format(refundOrder.orderNo, e))
  159. raise UserServerException('{}({})'.format(e.errMsg, e.errCode))
  160. if result["code"] != "10000":
  161. refundOrder.fail(errorCode = "{}-{}".format(result["code"], result.get("sub_code")),
  162. errorDesc = "{}-{}".format(result["msg"], result.get("sub_msg")))
  163. logger.info('ALIPAY Refund request successfully! return = {}'.format(result))
  164. elif payGateway.pay_app_type in [PayAppType.WECHAT, PayAppType.WECHAT_MINI]:
  165. try:
  166. result = payGateway.refund_to_user(
  167. out_trade_no = self.outTradeNo, out_refund_no = refundOrder.orderNo,
  168. refund_fee = self.refundFee, total_fee = self.totalFee, refund_reason = u'退费',
  169. notify_url = REFUND_NOTIFY_URL.WECHAT_REFUND_BACK)
  170. except WeChatPayException as e:
  171. logger.info('refund failed , refund orderNo = {} reason = {}'.format(refundOrder.orderNo, e))
  172. refundOrder.fail(errorCode = e.errCode, errorDesc = e.errMsg)
  173. raise UserServerException('{}({})'.format(e.errMsg, e.errCode))
  174. logger.info('WECHAT Refund request successfully! return = {}'.format(result))
  175. except UserServerException as se:
  176. logger.error(se.message)
  177. raise se
  178. except Exception as ee:
  179. # 这一步就不再更改订单的状态 由于不知道是退款前出错还是退款后出错 使用poll拉取订单状态来更新
  180. logger.exception(ee)
  181. raise UserServerException(ee.message)
  182. finally:
  183. # 资金池方式下,直接记录负单.所有对账时间都以系统内时间为准
  184. if payGateway.occupant.role == ROLE.agent:
  185. from apps.web.report.ledger import Ledger
  186. ledger = Ledger(USER_RECHARGE_TYPE.REFUND_CASH, refund_recharge_order)
  187. ledger.execute(journal = False, stats = True, check = False)
  188. return refundOrder
  189. class RetryRefundCash(object):
  190. def __init__(self, refundOrder): # type:(RefundMoneyRecord) -> None
  191. self.paySubOrder = refundOrder.pay_sub_order
  192. self.payOrder = self.paySubOrder.payOrder
  193. self.refundFee = refundOrder.money
  194. self.deductCoins = refundOrder.coins
  195. self.refundOrder = refundOrder
  196. # self._nextSeq = 1
  197. @property
  198. def outTradeNo(self):
  199. """
  200. 交易单号
  201. :return:
  202. """
  203. return self.payOrder.orderNo
  204. @property
  205. def totalFee(self):
  206. return self.payOrder.money
  207. @property
  208. def totalCoins(self):
  209. return self.payOrder.coins
  210. @property
  211. def subTotalFee(self):
  212. return self.paySubOrder.money
  213. @property
  214. def subTotalCoins(self):
  215. return self.paySubOrder.coins
  216. def pre_check(self):
  217. """
  218. 退款的预检查
  219. :return:
  220. """
  221. if self.refundFee <= RMB(0) or self.refundFee > self.totalFee or self.refundFee > self.subTotalFee:
  222. raise ParameterError(u"退费金额错误")
  223. if self.deductCoins < VirtualCoin(
  224. 0) or self.deductCoins > self.totalCoins or self.deductCoins > self.subTotalCoins:
  225. raise ParameterError(u"扣除用户金币数目错误")
  226. if self.refundOrder.is_closed or self.refundOrder.is_success:
  227. raise UserServerException(u'已经完结订单不能重试')
  228. proxy = DealerIncomeProxy.objects.filter(
  229. ref_id = self.paySubOrder.id).first() # type: DealerIncomeProxy
  230. if not proxy:
  231. raise UserServerException(u"订单尚未分账,无法退款")
  232. return proxy
  233. def execute(self, frozen_callable, **kwargs):
  234. """
  235. 执行退款的动作
  236. 对于经销商商户的流程: 检查 >> 建单 >> 扣除用户金额 >> 退款 >> 收到退款成功通知后建立负收益单和扣除经销商的记录金额
  237. 对于资金池商的流程: 检查 >> 建单 >> 扣除用户金额 >> 建立负单和扣除经销商的记录金额 >> 退款
  238. :return:
  239. """
  240. proxy = self.pre_check()
  241. payGateway = PaymentGateway.clone_from_order(self.payOrder) # type: PaymentGateway
  242. if str(self.paySubOrder.id) == str(self.payOrder.id):
  243. logger.info(
  244. 'retry refund paras, orderNo = {} refundOrderNo = {} refundFee = {} totalFee = {}'.format(
  245. self.paySubOrder.orderNo, self.refundOrder.orderNo, self.refundFee, self.subTotalFee)
  246. )
  247. else:
  248. logger.info(
  249. 'retry refund paras, mix<orderNo = {}, totalFee={}>, sub<orderNo = {}, totalFee={}> '
  250. 'refundOrderNo = {} refundFee = {} '.format(
  251. self.payOrder.orderNo, self.totalFee, self.paySubOrder.orderNo, self.subTotalFee,
  252. self.refundOrder.orderNo, self.refundFee)
  253. )
  254. puller = RefundManager().get_poller(payGateway.pay_app_type)
  255. puller(self.refundOrder).pull(payGateway, self.payOrder, refund_post_pay)
  256. self.refundOrder.reload()
  257. if self.refundOrder.is_success or self.refundOrder.is_closed:
  258. logger.debug('refund order {} has been finished.'.format(str(self.refundOrder)))
  259. return
  260. refund_order_record = self.refundOrder.refund_order_record
  261. if not refund_order_record:
  262. split_map = proxy.partition_map
  263. refund_order_record = self.paySubOrder.new_refund_cash_order(
  264. self.refundOrder, split_map) # type: RechargeRecord
  265. # 将订单的状态切换为 正在处理中
  266. self.refundOrder.processing()
  267. # 扣除实体的金额(用户或者实体卡)
  268. frozen_callable(self.refundOrder, **kwargs)
  269. try:
  270. if payGateway.pay_app_type == PayAppType.ALIPAY:
  271. # 支付宝的退款方式
  272. # 支付宝的退款很特殊,接口状态以及业务状态均在同步接口中返回 其中 code = 10000 表示接口成功 即申请成功了 fund_change= Y 表示退款成功
  273. # 而当接口状态成功 code=10000 但是资金未发生变动 fund_change=N 的时候,则退款是不成功的(最好需要轮询一次),此时不改变退款单的状态
  274. try:
  275. result = payGateway.refund_to_user(
  276. out_trade_no = self.outTradeNo, out_refund_no = self.refundOrder.orderNo,
  277. refund_fee = self.refundFee, total_fee = self.totalFee, refund_reason = u'退费')
  278. except AliException as e:
  279. logger.info('refund failed , refund orderNo = {} reason = {}'.format(self.refundOrder.orderNo, e))
  280. raise UserServerException('{}({})'.format(e.errMsg, e.errCode))
  281. if result["code"] != "10000":
  282. self.refundOrder.fail(errorCode = "{}-{}".format(result["code"], result.get("sub_code")),
  283. errorDesc = "{}-{}".format(result["msg"], result.get("sub_msg")))
  284. logger.info('ALIPAY Refund request successfully! return = {}'.format(result))
  285. elif payGateway.pay_app_type == PayAppType.WECHAT:
  286. try:
  287. result = payGateway.refund_to_user(
  288. out_trade_no = self.outTradeNo, out_refund_no = self.refundOrder.orderNo,
  289. refund_fee = self.refundFee, total_fee = self.totalFee, refund_reason = u'退费',
  290. notify_url = REFUND_NOTIFY_URL.WECHAT_REFUND_BACK)
  291. except WeChatPayException as e:
  292. logger.info('refund failed , refund orderNo = {} reason = {}'.format(self.refundOrder.orderNo, e))
  293. self.refundOrder.fail(errorCode = e.errCode, errorDesc = e.errMsg)
  294. raise UserServerException('{}({})'.format(e.errMsg, e.errCode))
  295. logger.info('WECHAT Refund request successfully! return = {}'.format(result))
  296. else:
  297. self.refundOrder.fail(errorDesc = u"不支持的退款模式")
  298. raise UserServerException(u"不支持的退款模式")
  299. except UserServerException as se:
  300. logger.error(se.message)
  301. raise se
  302. except Exception as ee:
  303. # 这一步就不再更改订单的状态 由于不知道是退款前出错还是退款后出错 使用poll拉取订单状态来更新
  304. logger.exception(ee)
  305. raise UserServerException(ee.message)
  306. finally:
  307. # 资金池方式下,直接记录负单.所有对账时间都以系统内时间为准
  308. if payGateway.occupant.role == ROLE.agent:
  309. from apps.web.report.ledger import Ledger
  310. ledger = Ledger(USER_RECHARGE_TYPE.REFUND_CASH, refund_order_record)
  311. ledger.execute(journal = False, stats = True, check = False)
  312. return self.refundOrder
  313. def refund_post_pay(refundOrder, finishedTime):
  314. # type: (RefundMoneyRecord, datetime)->None
  315. refundOrder.user.commit_refund_cash(refundOrder)
  316. refund_order_record = refundOrder.refund_order_record # type: RechargeRecord
  317. refund_order_record.finishedTime = finishedTime
  318. refund_order_record.result = RechargeRecord.PayResult.SUCCESS
  319. refund_order_record.save()
  320. # 记录资金池的变动
  321. if not refund_order_record.is_ledgered:
  322. from apps.web.report.ledger import Ledger
  323. ledger = Ledger(DEALER_INCOME_SOURCE.REFUND_CASH, refund_order_record)
  324. ledger.execute(stats=True)
  325. def frozen_refund_for_balance(refundOrder, user = None, minus_total_consume = VirtualCoin(0)):
  326. # type: (RefundMoneyRecord, MyUser, VirtualCoin)->bool
  327. if user:
  328. if user.openId != refundOrder.pay_sub_order.openId:
  329. raise UserServerException(u"用户参数错误")
  330. else:
  331. if user.groupId != refundOrder.pay_sub_order.groupId:
  332. user = refundOrder.pay_sub_order.user
  333. else:
  334. user = refundOrder.pay_sub_order.user
  335. if not user:
  336. raise UserServerException(u'用户不存在')
  337. return user.prepare_refund_cash(refundOrder, minus_total_consume)