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

549 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 { 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 } = order;
let settlePlanAmount = 0;
settlePlans.forEach((settlePlan) => settlePlanAmount += settlePlan.price);
const allowPlanPrice = paid - refunded - settled - data.price;
if (settlePlanAmount > allowPlanPrice) {
throw new settlePlanExceed(settlePlanAmount - allowPlanPrice);
}
const settlements = settlePlans?.map((plan) => plan.settlement$plan).flat();
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 = settlements?.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 = settlements?.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;