import { CreateTriggerInTxn, Trigger, UpdateTriggerInTxn, UpdateTriggerCrossTxn } from 'oak-domain/lib/types/Trigger'; import { generateNewId, generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; import { EntityDict } from '../oak-app-domain'; import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity'; import { BRC } from '../types/RuntimeCxt'; import { getAccountEntity, getPayClazz } from '../utils/payClazz'; import assert from 'assert'; import { updateWithdrawState } from './withdraw'; import { RefundExceedMax, PayUnRefundable } from '@project/types/Exception'; /** * 开始退款的逻辑 * @param context * @param data */ async function startRefunding(context: BRC, data: EntityDict['refund']['CreateSingle']['data']) { assert(!data.pay && data.payId); const [pay] = await context.select('pay', { data: { id: 1, paid: 1, refunded: 1, iState: 1, entity: 1, entityId: 1, refundable: 1, deposit: { id: 1, accountId: 1, }, order: { id: 1, settled: 1, } }, filter: { id: data.payId, } }, { dontCollect: true }); const { paid, refunded, deposit, order } = pay; if (paid! - refunded! < data.price!) { throw new RefundExceedMax(); } if (!pay.refundable) { throw new PayUnRefundable(); } if (deposit) { // 充值不可能从account渠道支付 assert(pay.entity !== 'account'); if (!data.withdrawId && !order?.id) { //充值退款需创建关联的accountOper,若为提现则在withdraw中创建 data.accountOper$entity = [ { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), accountId: deposit.accountId, type: 'refund', totalPlus: -data.price!, availPlus: -data.price!, refundablePlus: -data.price!, }, } ] } } else { assert(order); if (order.settled) { // 如果已经分账,退款的钱要有明确的来源account const { accountOper$entity: opers } = data; assert(opers && opers instanceof Array, '订单退款一定要有相应的account来源'); let amount = 0; opers.forEach( ({ action, data }) => { assert(action === 'create'); const { type, totalPlus, availPlus, refundablePlus } = data as EntityDict['accountOper']['CreateSingle']['data']; assert(type === 'refund'); assert(totalPlus === availPlus); amount += totalPlus!; } ); assert(amount === data.price); } } data.pay = { id: generateNewId(), action: 'startRefunding', data: {}, }; } /** * 退款成功的逻辑 * @param context * @param data */ async function succeedRefunding(context: BRC, refundId: string) { const pays = await context.select('pay', { data: { id: 1, price: 1, refunded: 1, entity: 1, entityId: 1, iState: 1, paid: 1, applicationId: 1, application: { systemId: 1, }, deposit: { id: 1, accountId: 1, }, refund$pay: { $entity: 'refund', data: { id: 1, price: 1, iState: 1, loss: 1, withdrawId: 1, }, filter: { id: refundId, }, } }, filter: { refund$pay: { '#sqp': 'in', id: refundId, }, } }, { forUpdate: true, dontCollect: true }); assert(pays.length === 1); const [pay] = pays; const { price, paid, refunded, refund$pay: refunds, application, applicationId, entity, entityId } = pay; assert(refunds!.length === 1); const [refund] = refunds!; const { price: refundPrice, loss: refundLoss, withdrawId } = refund; const refunded2 = refunded! + refundPrice!; assert(refunded2 <= paid!); const action: EntityDict['pay']['Action'] = refunded2 === paid ? 'refundAll' : 'refundPartially'; await context.operate('pay', { id: await generateNewIdAsync(), action, data: { refunded: refunded2, refundable: refunded2 < paid!, }, filter: { id: pay!.id!, } }, {}); let d = undefined; //退回至account的退款无渠道退款补偿 if (entity === 'account') { //退回至account中的退款,创建accountOper,更新account余额 await context.operate('accountOper', { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), accountId: pay!.entityId!, entity: 'refund', entityId: refund.id, type: 'consumeBack', totalPlus: refundPrice! - refundLoss!, availPlus: refundPrice! - refundLoss!, } }, {}); } else { //订单退回至非account的退款会使得系统对应账户的资金发生流出 const payClazz = await getPayClazz(entity!, entityId!, context); // 实际执行的退款的额度应该是price - loss const [delta, sysAccountEntity, sysAccountEntityId] = payClazz.calcRefundTax(refundPrice - refundLoss!); // sysAccount上减掉实际退款的额度-税费 await context.operate('sysAccountOper', { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), delta: refundLoss! - refundPrice! - delta!, entity: sysAccountEntity, entityId: sysAccountEntityId, refundId: refund.id, type: 'refund', } }, {}); d = delta; } let cnt = 2; if (refundLoss || d) { // 如果有退回或者要缴纳的税费,进入system相关联的account账户 const [account] = await context.select('account', { data: { id: 1, }, filter: { entity: 'system', entityId: application!.systemId!, }, }, { dontCollect: true }); if (d) { await context.operate('accountOper', { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), accountId: account!.id, type: d < 0 ? 'taxRefund' : 'tax', totalPlus: -d, availPlus: -d, entity: 'refund', entityId: refund.id!, }, }, {}); } cnt++; if (refundLoss) { assert(refundLoss > 0); await context.operate('accountOper', { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), accountId: account!.id, type: 'earn', totalPlus: refundLoss, availPlus: refundLoss, entity: 'refund', entityId: refund.id!, }, }, {}); } } // 如果有withdraw,尝试去同步withdraw的状态 if (withdrawId) { return cnt + await updateWithdrawState(context, withdrawId); } return cnt; } async function failRefunding(context: BRC, refundId: string) { const pays = await context.select('pay', { data: { id: 1, price: 1, refunded: 1, entity: 1, entityId: 1, iState: 1, paid: 1, applicationId: 1, application: { systemId: 1, }, deposit: { id: 1, accountId: 1, }, refund$pay: { $entity: 'refund', data: { id: 1, price: 1, loss: 1, iState: 1, withdrawId: 1, }, filter: { id: refundId, }, } }, filter: { refund$pay: { '#sqp': 'in', id: refundId, }, } }, { forUpdate: true, dontCollect: true }); assert(pays.length === 1); const [pay] = pays; const { refunded, refund$pay: refunds } = pay; assert(refunds!.length === 1); const [refund] = refunds!; const { withdrawId } = refund; const action: EntityDict['pay']['Action'] = refunded === 0 ? 'stopRefunding' : 'refundPartially'; await context.operate('pay', { id: await generateNewIdAsync(), action, data: { refundable: true, }, filter: { id: pay!.id!, } }, {}); // 如果有withdraw,尝试去同步withdraw的状态 if (withdrawId) { return 1 + await updateWithdrawState(context, withdrawId); } else { // 说明是从order那条线产生的refund,此时要把当时扣除掉的account部分返还 const accountOpers = await context.select('accountOper', { data: { id: 1, totalPlus: 1, availPlus: 1, accountId: 1, }, filter: { type: 'refund', entity: 'refund', entityId: refundId, }, }, { dontCollect: true }); let amount = 0; for (const oper of accountOpers) { const { totalPlus, availPlus, accountId } = oper; assert(totalPlus! < 0 && totalPlus === availPlus); await context.operate('accountOper', { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), totalPlus: -totalPlus!, availPlus: -availPlus!, type: 'refundFailure', entity: 'refund', entityId: refundId, accountId, } }, {}); amount += -availPlus!; } assert(amount === refund!.price!); return 1 + accountOpers.length; } } const triggers: Trigger[] = [ { entity: 'refund', action: 'create', when: 'commit', name: '当refund建立时,尝试触发外部退款操作', strict: 'makeSure', fn: async ({ ids }, context) => { assert(ids.length === 1); const [refund] = await context.select('refund', { data: { id: 1, price: 1, loss: 1, pay: { id: 1, price: 1, iState: 1, refunded: 1, entity: 1, entityId: 1, applicationId: 1, }, }, filter: { id: ids[0], }, }, {}); const { id, price, pay } = refund; const { price: payPrice, refunded, entity, entityId, applicationId } = pay!; const payClazz = await getPayClazz( entity!, entityId!, context, ); // 若对应entity的账户里钱不够,则不发起退款 const accountEntity = getAccountEntity(entity); if (entity !== 'account') { const accountAmount = await payClazz.getAccountAmount(context); if (accountAmount < price!) { return; } } const data = await payClazz.refund(refund as EntityDict['refund']['Schema'], context); if (data) { assert(data.externalId); const closeFn = context.openRootMode(); await context.operate('refund', { id: await generateNewIdAsync(), data, action: 'update', filter: { id, } }, { dontCollect: true }); closeFn(); } return; }, }, { entity: 'refund', action: 'succeed', when: 'after', asRoot: true, name: '退款成功时,更新对应的pay状态以及对应的withdraw状态', fn: async ({ operation }, context) => { const { filter } = operation; const refundId = filter?.id; assert(typeof refundId === 'string'); return succeedRefunding(context, refundId); } }, { entity: 'refund', action: 'fail', when: 'after', asRoot: true, name: '退款失败时,更新对应的pay状态以及对应的withdraw状态', fn: async ({ operation }, context) => { const { filter } = operation; const refundId = filter?.id; assert(typeof refundId === 'string'); return failRefunding(context, refundId); } }, { entity: 'refund', name: '当发起退款时,置pay的状态并做相应检查', action: 'create', asRoot: true, when: 'before', fn: async ({ operation }, context) => { const { data } = operation; assert(!(data instanceof Array)); await startRefunding(context, data); return 1; } } as CreateTriggerInTxn, ]; export default triggers;