oak-pay-business/es/triggers/pay.js

518 lines
18 KiB
JavaScript
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 { 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';
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,
},
},
},
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 && iState === 'paid') {
//已支付完成的订单的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 && payPaid === orderPaid && 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) {
// 已经在退款流程当中
assert(payPaid === orderPrice && payPaid === orderPaid);
const iState = payPaid === orderPrice ? 'refunded' : 'partiallyRefunded';
if (orderIState !== iState || orderRefunded !== payRefunded) {
const action = payPaid === orderPrice ? 'refundAll' : 'refundPartially';
await context.operate('order', {
id: await generateNewIdAsync(),
action,
data: {
refunded: payRefunded,
},
filter: {
id: orderId,
},
}, option);
}
}
else if (payPaid) {
// 在支付流程当中
//存在已完成的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,
},
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;
}
}
const triggers = [
{
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;
},
},
{
name: '当pay的状态发生变化时修改相应的order的状态或者deposit的状态同时尝试向订阅者推送',
entity: 'pay',
action: ['startPaying', 'succeedPaying', 'continuePaying', 'startClosing', 'succeedClosing', 'startRefunding',
'refundAll', 'refundPartially'],
when: 'after',
asRoot: true,
fn: async ({ operation }, context, option) => {
const { data, filter, action, id } = operation;
assert(typeof filter.id === 'string');
const [pay] = 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 });
const { orderId, depositId, iState, deposit, application } = pay;
context.saveOperationToEvent(id, `${DATA_SUBSCRIBER_KEYS.payStateChanged}-${filter.id}`);
if (orderId) {
return await changeOrderStateByPay({ id: orderId }, context, option);
}
else {
assert(depositId && deposit);
// 如果是支付成功则使deposit成功生成对应的accountOper这里应该只有这条路径会让deposit成功
if (action === 'succeedPaying') {
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',
});
}
await context.operate('deposit', {
id: await generateNewIdAsync(),
action: 'succeed',
data: {
accountOper$entity: accountOpers,
},
filter: {
id: depositId,
},
}, {});
return 1;
}
return 0;
}
},
},
{
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(applicationId, 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,
}, {});
assert(pays.length === 1);
const [pay] = pays;
const { applicationId, entity, entityId, iState, depositId } = pay;
assert(iState === 'unpaid' || iState === 'paying');
let cnt = 0;
if (iState === 'paying') {
const payClazz = await getPayClazz(applicationId, 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以及syste account中的余额变化',
entity: 'pay',
action: 'succeedPaying',
when: 'after',
asRoot: true,
fn: async ({ operation }, context) => {
const { data, filter } = operation;
const projection = {
id: 1,
paid: 1,
price: 1,
entity: 1,
entityId: 1,
iState: 1,
depositId: 1,
orderId: 1,
application: {
systemId: 1,
}
};
const pays = await context.select('pay', {
data: projection,
filter,
}, {});
assert(pays.length === 1);
const [pay] = pays;
const { price, paid, entity, entityId, iState, applicationId, application } = pay;
assert(iState === 'paid' && paid === price);
const clazz = await getPayClazz(applicationId, 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,
},
}, {});
return 1;
}
return 0;
},
},
{
name: '当account类型的pay的paid达到price改为支付成功',
entity: 'pay',
asRoot: true,
action: ['startPaying', 'continuePaying'],
check(operation) {
return (!!operation.data.paid) && operation.data.paid > 0;
},
filter: {
entity: 'account',
},
when: 'commit',
fn: async ({ ids }, context) => {
for (const id of ids) {
const [row] = await context.select('pay', {
data: {
id: 1,
orderId: 1,
entity: 1,
price: 1,
paid: 1,
},
filter: {
id,
}
}, { dontCollect: true });
assert(row && row.entity === 'account');
if (row.paid && row.paid === row.price) {
const closeFn = context.openRootMode();
await context.operate('pay', {
id: await generateNewIdAsync(),
action: 'succeedPaying',
data: {
successAt: Date.now(),
},
filter: {
id,
}
}, {});
closeFn();
}
}
}
},
{
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(applicationId, entity, entityId, context);
const forbidRefundAt = payClazz.getRefundableAt(data.successAt);
data.forbidRefundAt = forbidRefundAt;
data.refundable = forbidRefundAt > Date.now();
return 1;
},
},
];
export default triggers;