oak-pay-business/src/triggers/pay.ts

801 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { CreateTriggerInTxn, Trigger, UpdateTriggerInTxn, SelectTriggerAfter } from 'oak-domain/lib/types/Trigger';
import { 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 assert from 'assert';
import { OperateOption } from 'oak-domain/lib/types';
import { getPayClazz } from '../utils/payClazz';
import { fullPayProjection, refreshPayState } from '../utils/pay';
import { DATA_SUBSCRIBER_KEYS } from '../config/constants';
import { Update } from '../oak-app-domain/Pay/Schema';
import { getRelevantIds } from 'oak-domain/lib/store/filter';
async function changeOrderStateByPay(
filter: NonNullable<EntityDict['order']['Update']['filter']>,
context: BRC,
option: OperateOption) {
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: any) {
closeFn();
throw err;
}
}
/**
* 当订单上其它的支付全部完成/失败时将对应的order上类型为account的pay处理掉
* 现在的逻辑是account类型的pay必须等所有其它的pay都完成了自己才能完成如果其它都失败了自己就也失败钱返回付款的account
* @param payId
* @param context
*/
async function tryCompleteAccountPay(payId: string, context: BRC) {
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: string[] = [];
const accountPayIds: string[] = [];
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: Trigger<EntityDict, 'pay', BRC>[] = [
/** 改为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<EntityDict, 'pay', BRC>, */
{
name: '当生成autoStart的pay后自动开始支付流程',
entity: 'pay',
action: 'create',
when: 'commit',
asRoot: true,
check: (operation: EntityDict['pay']['Create']) => !!(operation as EntityDict['pay']['CreateSingle']).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 as EntityDict['pay']['Update'];
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;
},
} as UpdateTriggerInTxn<EntityDict, 'pay', BRC>,
{
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 as EntityDict['pay']['Schema'], data, context);
return 1;
},
} as UpdateTriggerInTxn<EntityDict, 'pay', BRC>,
{
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 as EntityDict['pay']['OpSchema']);
cnt++;
}
if (depositId) {
await context.operate('deposit', {
id: await generateNewIdAsync(),
action: 'fail',
data: {},
filter: {
id: depositId,
},
}, {});
cnt++;
}
}
return cnt;
},
} as UpdateTriggerInTxn<EntityDict, 'pay', BRC>,
{
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 as EntityDict['pay']['Update'];
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;
},
} as UpdateTriggerInTxn<EntityDict, 'pay', BRC>,
{
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: EntityDict['pay']['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: EntityDict['deposit']['Update']['data']['accountOper$entity'] = [
{
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;
},
} as UpdateTriggerInTxn<EntityDict, 'pay', BRC>,
{
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 as number);
data.forbidRefundAt = forbidRefundAt;
data.refundable = forbidRefundAt > Date.now();
return 1;
},
} as UpdateTriggerInTxn<EntityDict, 'pay', BRC>,
{
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);
}
} as UpdateTriggerInTxn<EntityDict, 'pay', BRC>,
{
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;
}
} as UpdateTriggerInTxn<EntityDict, 'pay', BRC>,
/* 影响的地方有点多,先不做了
{
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<EntityDict, 'pay', BRC>, */
];
export default triggers;
// export const optionalTriggers: Trigger<EntityDict, 'pay', BRC>[] = [
// {
// 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;
// },
// }
// ]