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

474 lines
14 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, 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<EntityDict, 'refund', BRC>[] = [
{
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<EntityDict, 'refund', BRC>,
];
export default triggers;