737 lines
26 KiB
JavaScript
737 lines
26 KiB
JavaScript
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<EntityDict, 'pay', BRC>, */
|
||
{
|
||
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<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;
|
||
// },
|
||
// }
|
||
// ]
|