562 lines
19 KiB
JavaScript
562 lines
19 KiB
JavaScript
import { generateNewId, generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||
import { getAccountEntity, getPayClazz } from '../utils/payClazz';
|
||
import assert from 'assert';
|
||
import { updateWithdrawState } from './withdraw';
|
||
import { RefundExceedMax, PayUnRefundable, settlePlanExceed, refundOpersNotEnough, refundOpersExceed } from '../types/Exception';
|
||
/**
|
||
* 开始退款的逻辑
|
||
* @param context
|
||
* @param data
|
||
*/
|
||
async function startRefunding(context, 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,
|
||
paid: 1,
|
||
refunded: 1,
|
||
settled: 1,
|
||
settlePlanned: 1,
|
||
settlePlan$order: {
|
||
$entity: 'settlePlan',
|
||
data: {
|
||
id: 1,
|
||
price: 1,
|
||
iState: 1,
|
||
settlement$plan: {
|
||
$entity: 'settlement',
|
||
data: {
|
||
id: 1,
|
||
accountId: 1,
|
||
price: 1,
|
||
iState: 1,
|
||
},
|
||
filter: {
|
||
iState: 'unsettled'
|
||
}
|
||
}
|
||
},
|
||
filter: {
|
||
iState: 'unsettled'
|
||
}
|
||
}
|
||
}
|
||
},
|
||
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);
|
||
// 对settlePlan检查
|
||
// 未结算的settlePlan的金额总和 = order.paid - order.refunded - order.settled - refunded.price
|
||
const { settlePlan$order: settlePlans, paid, refunded, settled, id: orderId } = order;
|
||
let settlePlanAmount = 0;
|
||
settlePlans.forEach((settlePlan) => settlePlanAmount += settlePlan.price);
|
||
const allowPlanPrice = Math.max((paid - refunded - settled - data.price), 0);
|
||
if (settlePlanAmount > allowPlanPrice) {
|
||
throw new settlePlanExceed(settlePlanAmount - allowPlanPrice);
|
||
}
|
||
const settledSettlements = await context.select('settlement', {
|
||
data: {
|
||
id: 1,
|
||
accountId: 1,
|
||
price: 1,
|
||
iState: 1,
|
||
},
|
||
filter: {
|
||
iState: 'settled',
|
||
plan: {
|
||
orderId,
|
||
}
|
||
}
|
||
}, { forUpdate: true });
|
||
const { accountOper$entity: opers, } = data;
|
||
// 对opers检查
|
||
if (order.settled === (order.paid - order.refunded)) {
|
||
// 已全部结算
|
||
// 已全部结算的订单由refund传入的opers决定退款,检查opers的金额总和等于refund的金额
|
||
let amount = 0;
|
||
// 有明确的来源account
|
||
assert(opers && opers instanceof Array, '已结算的订单退款一定要有相应的account来源');
|
||
opers.forEach(({ action, data }) => {
|
||
assert(action === 'create');
|
||
const { type, totalPlus, availPlus, accountId } = data;
|
||
assert(type === 'refund');
|
||
assert(totalPlus === availPlus);
|
||
const settlement = settledSettlements?.find((ele) => ele?.accountId === accountId);
|
||
assert(!!settlement, '请从订单分账的相关账户中选择订单退款的account来源');
|
||
amount += -totalPlus;
|
||
});
|
||
assert(amount === data.price);
|
||
//将order的settled更新
|
||
const newSettled = order.settled - data.price;
|
||
await context.operate('order', {
|
||
id: await generateNewIdAsync(),
|
||
action: 'update',
|
||
data: {
|
||
settled: newSettled,
|
||
},
|
||
filter: {
|
||
id: order.id,
|
||
}
|
||
}, {});
|
||
}
|
||
else if (order.settled === 0) {
|
||
//未结算
|
||
//未结算的订单无opers
|
||
assert(!(opers && opers.length > 0));
|
||
}
|
||
else {
|
||
//部分结算
|
||
/**
|
||
* 部分结算的订单:
|
||
* 1. opers的金额总和 <= 订单已结算金额
|
||
* 2. opers的金额总和 + unsettled的金额 >= 退款金额
|
||
*/
|
||
let opersPrice = 0;
|
||
if (opers && opers.length > 0) {
|
||
opers.forEach(({ action, data }) => {
|
||
assert(action === 'create');
|
||
const { type, totalPlus, availPlus, accountId } = data;
|
||
assert(type === 'refund');
|
||
assert(totalPlus === availPlus);
|
||
const settlement = settledSettlements?.find((ele) => ele?.accountId === accountId);
|
||
assert(!!settlement, '请从订单分账的相关账户中选择订单退款的account来源');
|
||
opersPrice += -totalPlus;
|
||
});
|
||
}
|
||
const { paid, refunded, settled, settlePlan$order: settlePlans } = order;
|
||
const orderUnsettledPrice = paid - refunded - settled;
|
||
if (opersPrice > settled) {
|
||
throw new refundOpersExceed(opersPrice - settled);
|
||
}
|
||
if (opersPrice < data.price - orderUnsettledPrice) {
|
||
throw new refundOpersNotEnough(opersPrice - (data.price - orderUnsettledPrice));
|
||
}
|
||
//将order的settled更新
|
||
const newSettled = order.settled - opersPrice;
|
||
await context.operate('order', {
|
||
id: await generateNewIdAsync(),
|
||
action: 'update',
|
||
data: {
|
||
settled: newSettled,
|
||
},
|
||
filter: {
|
||
id: order.id,
|
||
}
|
||
}, {});
|
||
}
|
||
}
|
||
data.pay = {
|
||
id: generateNewId(),
|
||
action: 'startRefunding',
|
||
data: {},
|
||
};
|
||
}
|
||
/**
|
||
* 退款成功的逻辑
|
||
* @param context
|
||
* @param data
|
||
*/
|
||
async function succeedRefunding(context, refundId) {
|
||
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 = 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, refundId) {
|
||
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,
|
||
},
|
||
},
|
||
order: {
|
||
id: 1,
|
||
settled: 1,
|
||
}
|
||
},
|
||
filter: {
|
||
refund$pay: {
|
||
'#sqp': 'in',
|
||
id: refundId,
|
||
},
|
||
}
|
||
}, { forUpdate: true, dontCollect: true });
|
||
assert(pays.length === 1);
|
||
const [pay] = pays;
|
||
const { refunded, refund$pay: refunds, order } = pay;
|
||
assert(refunds.length === 1);
|
||
const [refund] = refunds;
|
||
const { withdrawId } = refund;
|
||
const 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 {
|
||
assert(order);
|
||
// 说明是从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 opersPrice = 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,
|
||
}
|
||
}, {});
|
||
opersPrice += -availPlus;
|
||
}
|
||
//如果refund有accountOper回退order.settled变化
|
||
if (opersPrice > 0) {
|
||
const newSettled = order.settled + opersPrice;
|
||
await context.operate('order', {
|
||
id: await generateNewIdAsync(),
|
||
action: 'update',
|
||
data: {
|
||
settled: newSettled
|
||
},
|
||
filter: {
|
||
id: order.id,
|
||
}
|
||
}, {});
|
||
}
|
||
return 1 + accountOpers.length;
|
||
}
|
||
}
|
||
const triggers = [
|
||
{
|
||
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, 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;
|
||
}
|
||
},
|
||
];
|
||
export default triggers;
|