import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; import assert from 'assert'; import { getPayClazz } from '../utils/payClazz'; import { fullPayProjection } from '../utils/pay'; import { DATA_SUBSCRIBER_KEYS } from '../config/constants'; import { getRelevantIds } from 'oak-domain/lib/store/filter'; async function changeOrderStateByPay(filter, context, option) { const orders = await context.select('order', { data: { id: 1, iState: 1, price: 1, paid: 1, refunded: 1, pay$order: { $entity: 'pay', data: { id: 1, price: 1, paid: 1, refunded: 1, iState: 1, entity: 1, }, filter: { iState: { $ne: 'closed', } } }, }, filter, }, { dontCollect: true }); assert(orders.length === 1); const [{ id: orderId, pay$order: pays, price: orderPrice, paid: orderPaid, refunded: orderRefunded, iState: orderIState }] = orders; let hasPaying = false, hasRefunding = false; let payPaid = 0, payRefunded = 0; for (const pay of pays) { const { price, iState, paid, refunded } = pay; switch (iState) { case 'paying': { hasPaying = true; break; } case 'refunding': { hasRefunding = true; break; } default: { break; } } if (paid) { payPaid += paid; } if (refunded) { payRefunded += refunded; } } const closeFn = context.openRootMode(); try { if (hasPaying) { // 一定是在支付状态 assert(!hasRefunding && payRefunded === 0); if (orderIState !== 'paying') { //若订单未开始支付 await context.operate('order', { id: await generateNewIdAsync(), action: 'startPaying', data: { paid: payPaid, }, filter: { id: orderId, }, }, option); } } else if (hasRefunding) { assert(!hasPaying && payPaid === orderPrice && payRefunded <= orderPrice); if (orderIState !== 'refunding' || orderRefunded !== payRefunded) { await context.operate('order', { id: await generateNewIdAsync(), action: 'startRefunding', data: { refunded: payRefunded, }, filter: { id: orderId, }, }, option); } } else if (payRefunded > 0 || (pays?.length === 1 && pays?.[0].entity === 'account' && orderPrice === 0 && pays?.[0].iState === 'refunded')) { // 已经在退款流程当中 assert(payPaid === orderPrice); const iState = payPaid === orderPrice ? 'refunded' : 'partiallyRefunded'; if (orderIState !== iState || orderRefunded !== payRefunded) { const action = payRefunded === orderPrice ? 'refundAll' : 'refundPartially'; await context.operate('order', { id: await generateNewIdAsync(), action, data: { refunded: payRefunded, }, filter: { id: orderId, }, }, option); } } else if (payPaid > 0 || (pays?.length === 1 && pays?.[0].entity === 'account' && orderPrice === 0 && pays?.[0].iState === 'paid')) { // 在支付流程当中 // 存在已完成的pay更新order的paid和iState assert(orderRefunded === 0); const iState = payPaid === orderPrice ? 'paid' : 'partiallyPaid'; if (orderIState !== iState || orderPaid !== payPaid) { const action = payPaid === orderPrice ? 'payAll' : 'payPartially'; await context.operate('order', { id: await generateNewIdAsync(), action, data: { paid: payPaid, payAt: action === 'payAll' ? Date.now() : null, }, filter: { id: orderId, }, }, option); } } else { const iState = 'unpaid'; assert(orderRefunded === 0 && orderPaid === 0); if (orderIState !== iState) { await context.operate('order', { id: await generateNewIdAsync(), action: 'payNone', data: {}, filter: { id: orderId, }, }, option); } } closeFn(); return 1; } catch (err) { closeFn(); throw err; } } /** * 当订单上其它的支付全部完成/失败时,将对应的order上类型为account的pay处理掉 * 现在的逻辑是:account类型的pay必须等所有其它的pay都完成了,自己才能完成,如果其它都失败了,自己就也失败,钱返回付款的account * @param payId * @param context */ async function tryCompleteAccountPay(payId, context) { const [pay] = await context.select('pay', { data: { id: 1, paid: 1, price: 1, iState: 1, order: { iState: 1, price: 1, pay$order: { $entity: 'pay', data: { id: 1, iState: 1, price: 1, entity: 1, paid: 1, }, filter: { iState: { $in: ['paid', 'paying'], }, }, }, }, }, filter: { id: payId, } }, {}); const { order } = pay; if (order) { const { price, iState, pay$order: pays } = order; assert(iState === 'paying'); const successfulPayIds = []; const accountPayIds = []; let totalPaid = 0; pays?.forEach((pay) => { const { iState, entity, paid, price } = pay; if (iState === 'paid') { assert(paid === price); totalPaid += paid; } else if (entity === 'account') { totalPaid += paid; accountPayIds.push(pay.id); if (price === paid) { successfulPayIds.push(pay.id); } } }); if (totalPaid === price) { for (const payId2 of successfulPayIds) { await context.operate('pay', { id: await generateNewIdAsync(), action: 'succeedPaying', data: { successAt: Date.now(), }, filter: { id: payId2, } }, {}); } return successfulPayIds.length; } else if (pay.iState === 'closed') { // 本条支付关闭,则其它的account pay也跟随关闭 for (const payId2 of accountPayIds) { await context.operate('pay', { id: await generateNewIdAsync(), action: 'close', data: {}, filter: { id: payId2, } }, {}); } return accountPayIds.length; } } return 0; } const triggers = [ /** 改为commit的trigger { name: '当生成pay时,自动开始支付流程', entity: 'pay', action: 'create', when: 'after', asRoot: true, fn: async ({ operation }, context, option) => { const { data } = operation; assert(!(data instanceof Array)); const { id } = data; await context.operate('pay', { id: await generateNewIdAsync(), action: 'startPaying', data: { }, filter: { id, } }, option); return 1; }, } as CreateTriggerInTxn, */ { name: '当生成autoStart的pay后,自动开始支付流程', entity: 'pay', action: 'create', when: 'commit', asRoot: true, check: (operation) => !!operation.data.autoStart, fn: async ({ ids }, context, option) => { assert(ids.length === 1); const [id] = ids; await context.operate('pay', { id: await generateNewIdAsync(), action: 'startPaying', data: {}, filter: { id, } }, option); return; }, }, { name: '当pay的状态发生变化时,修改相应的order的状态或者deposit的状态,同时尝试向订阅者推送', entity: 'pay', action: ['startPaying', 'succeedPaying', 'continuePaying', 'close', 'startRefunding', 'refundAll', 'refundPartially'], when: 'after', asRoot: true, priority: 99, fn: async ({ operation }, context, option) => { const { data, filter, action, id } = operation; const pays = await context.select('pay', { data: { id: 1, orderId: 1, price: 1, refundable: 1, depositId: 1, deposit: { id: 1, accountId: 1, price: 1, loss: 1, }, application: { systemId: 1, }, iState: 1, }, filter, }, { dontCollect: true }); for (const pay of pays) { const { orderId, depositId, iState, deposit, application } = pay; context.saveOperationToEvent(id, `${DATA_SUBSCRIBER_KEYS.payStateChanged}-${filter.id}`); if (orderId) { await changeOrderStateByPay({ id: orderId }, context, option); } } return pays.length; }, }, { name: '当pay变为paying状态时,调用外部下单接口', entity: 'pay', action: ['startPaying'], when: 'before', priority: 99, fn: async ({ operation }, context, option) => { const { data, filter } = operation; const pays = await context.select('pay', { data: fullPayProjection, filter, }, {}); assert(pays.length === 1); const [pay] = pays; const { applicationId, entity, entityId, iState } = pay; assert(iState === 'unpaid'); const payClazz = await getPayClazz(entity, entityId, context); await payClazz.prepay(pay, data, context); return 1; }, }, { name: '当pay开始close时,调用外部关闭订单接口,并中止充值', entity: 'pay', action: 'close', when: 'before', priority: 99, asRoot: true, fn: async ({ operation }, context, option) => { const { data, filter } = operation; const pays = await context.select('pay', { data: fullPayProjection, filter, }, {}); let cnt = 0; for (const pay of pays) { const { applicationId, entity, entityId, iState, depositId } = pay; assert(iState === 'unpaid' || iState === 'paying'); if (iState === 'paying') { const payClazz = await getPayClazz(entity, entityId, context); await payClazz.close(pay); cnt++; } if (depositId) { await context.operate('deposit', { id: await generateNewIdAsync(), action: 'fail', data: {}, filter: { id: depositId, }, }, {}); cnt++; } } return cnt; }, }, { name: '当充值类的pay变成unrefundable时,计算并扣除account上的refundable', entity: 'pay', action: 'closeRefund', filter: { orderId: { $exists: false, }, }, when: 'before', fn: async ({ operation }, context, option) => { const { filter, action, id } = operation; const pays = await context.select('pay', { data: { id: 1, orderId: 1, price: 1, refunded: 1, deposit: { id: 1, accountId: 1, } }, filter, }, { dontCollect: true }); let count = 0; for (const pay of pays) { const { orderId, deposit, price, refunded } = pay; assert(!orderId); if (price - refunded > 0) { // 减少account上可以refund的部分 await context.operate('accountOper', { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), type: 'cutoffRefundable', availPlus: 0, totalPlus: 0, refundablePlus: refunded - price, accountId: deposit.accountId, entity: 'pay', entityId: pay.id, }, }, {}); count++; } } return count; }, }, { name: '当pay完成支付时,计算相应的account以及system account中的余额变化,使deposit完成,对于受发货限制的微信支付创建ship', entity: 'pay', action: 'succeedPaying', when: 'after', asRoot: true, fn: async ({ operation }, context) => { const { data, filter } = operation; assert(typeof filter.id === 'string'); const projection = { id: 1, paid: 1, price: 1, entity: 1, entityId: 1, iState: 1, depositId: 1, orderId: 1, application: { systemId: 1, }, deposit: { id: 1, price: 1, loss: 1, accountId: 1, }, refundable: 1, wpProduct: { id: 1, type: 1, wechatMpShip$wpProduct: { $entity: 'wechatMpShip', data: { id: 1, }, filter: { disabled: false, } } } }; const pays = await context.select('pay', { data: projection, filter, }, {}); assert(pays.length === 1); const [pay] = pays; const { price, paid, entity, entityId, iState, applicationId, application, depositId, deposit, wpProduct } = pay; assert(iState === 'paid' && paid === price); let cnt = 1; if (entity === 'wpProduct' && wpProduct && wpProduct.type === 'mp') { //小程序支付受到发货限制,对deposit执行ship并级联创建虚拟发货的ship,系统账户的金额变化延后至确认收货后操作 const { wechatMpShip$wpProduct: wmShips } = wpProduct; if (wmShips?.length) { if (depositId) { //充值deposit执行ship,并创建关联的虚拟ship await context.operate('deposit', { id: await generateNewIdAsync(), action: 'ship', data: { ship: { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), type: 'virtual' } } }, filter: { id: depositId, }, }, {}); cnt++; } return cnt; } } if (entity !== 'account') { const clazz = await getPayClazz(entity, entityId, context); const [tax, sysAccountEntity, sysAccountEntityId] = clazz.calcPayTax(paid); await context.operate('sysAccountOper', { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), delta: paid - tax, entity: sysAccountEntity, entityId: sysAccountEntityId, payId: pay.id, type: 'pay', } }, {}); if (tax !== 0) { // tax产生的损失由sys account来承担 const [account] = await context.select('account', { data: { id: 1, }, filter: { entity: 'system', entityId: application.systemId, } }, { dontCollect: true }); await context.operate('accountOper', { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), accountId: account.id, type: 'tax', totalPlus: -tax, availPlus: -tax, entity: 'pay', entityId: pay.id, }, }, {}); cnt++; } } if (depositId) { const payPrice = pay.price; const { price, loss } = deposit; assert(price === payPrice); const accountOpers = [ { id: await generateNewIdAsync(), data: { id: await generateNewIdAsync(), totalPlus: price - loss, availPlus: price - loss, refundablePlus: pay.refundable ? price : 0, accountId: deposit.accountId, type: 'deposit', }, action: 'create', } ]; if (loss > 0) { // 如果有loss就充入system的account账户 const [account] = await context.select('account', { data: { id: 1, }, filter: { entity: 'system', entityId: application?.systemId, }, }, { dontCollect: true }); accountOpers.push({ id: await generateNewIdAsync(), data: { id: await generateNewIdAsync(), totalPlus: loss, availPlus: loss, accountId: account.id, type: 'earn', }, action: 'create', }); cnt++; } await context.operate('deposit', { id: await generateNewIdAsync(), action: 'succeed', data: { accountOper$entity: accountOpers, }, filter: { id: depositId, }, }, {}); cnt++; } return cnt; }, }, { name: '当pay完成支付时,计算其refundable和forbidRefundAt', entity: 'pay', action: 'succeedPaying', when: 'before', fn: async ({ operation }, context) => { const { data, filter } = operation; assert(data.successAt); const pays = await context.select('pay', { data: { id: 1, entity: 1, entityId: 1, applicationId: 1, }, filter }, { dontCollect: true, forUpdate: true }); assert(pays.length === 1); const [pay] = pays; const { applicationId, entity, entityId } = pay; const payClazz = await getPayClazz(entity, entityId, context); const forbidRefundAt = payClazz.getRefundableAt(data.successAt); data.forbidRefundAt = forbidRefundAt; data.refundable = forbidRefundAt > Date.now(); return 1; }, }, { entity: 'pay', name: '当有pay支付成功时,尝试将account类型的pay完成', action: ['startPaying', 'continuePaying', 'succeedPaying'], attributes: ['paid'], when: 'after', fn: async ({ operation }, context, option) => { const { filter } = operation; const ids = getRelevantIds(filter); assert(ids.length === 1); return tryCompleteAccountPay(ids[0], context); } }, { entity: 'pay', name: '当有pay关闭时,如果是account类型的,将金额还给account,否则检查会否测试相关Account pay是否需要关闭', action: 'close', when: 'after', fn: async ({ operation }, context, option) => { const { filter } = operation; const pays = await context.select('pay', { data: { id: 1, price: 1, paid: 1, entity: 1, entityId: 1, }, filter, }, {}); let cnt = 0; for (const pay of pays) { if (pay.entity === 'account') { await context.operate('accountOper', { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), accountId: pay.entityId, entity: 'pay', entityId: pay.id, type: 'consumeBack', totalPlus: pay.paid, availPlus: pay.paid, }, }, {}); cnt++; } else { cnt += await tryCompleteAccountPay(pay.id, context); } } return cnt; } }, /* 影响的地方有点多,先不做了 { entity: 'pay', name: '当选择pay并发现pay的状态是paying时,尝试刷新其状态', action: 'select', when: 'after', fn: async ({ result }, context) => { if (result.length === 1 && result[0].iState === 'paying') { await refreshPayState(result[0] as EntityDict['pay']['OpSchema'], context); } return 0; } } as SelectTriggerAfter, */ ]; export default triggers; // export const optionalTriggers: Trigger[] = [ // { // name: '当生成pay后,自动开始支付流程', // entity: 'pay', // action: 'create', // when: 'commit', // asRoot: true, // fn: async ({ ids }, context, option) => { // assert(ids.length === 1); // const [id] = ids; // await context.operate('pay', { // id: await generateNewIdAsync(), // action: 'startPaying', // data: { // }, // filter: { // id, // } // }, option); // return; // }, // } // ]