474 lines
14 KiB
TypeScript
474 lines
14 KiB
TypeScript
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;
|