oak-pay-business/es/aspects/withdraw.js

209 lines
7.7 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 { OakException, OakInputIllegalException } from 'oak-domain/lib/types';
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import assert from 'assert';
import { getPayClazz } from '../utils/payClazz';
function calcWithdrawLossByConfig(price, withdrawLossConfig) {
const { ratio, lowest, highest, trim } = withdrawLossConfig;
assert(ratio);
let value = Math.round(price * ratio / 100);
if (highest && value > highest) {
value = highest;
}
if (lowest && value < lowest) {
value = lowest;
}
switch (trim) {
case 'jiao': {
value = Math.ceil(value / 10) * 10;
break;
}
case 'yuan': {
value = Math.ceil(value / 100) * 100;
break;
}
}
return value;
}
export async function getWithdrawCreateData(params, context) {
const { system } = context.getApplication();
const withdrawLoss = system?.payConfig?.withdrawLoss;
if (!withdrawLoss || (!withdrawLoss.conservative && typeof withdrawLoss.ratio !== 'number')) {
throw new OakException('error::system.withdrawLossUnSet');
}
const { accountId, price: totalPrice, withdrawAccountId } = params;
const [account] = await context.select('account', {
data: {
id: 1,
avail: 1,
refundable: 1,
},
filter: {
id: accountId,
},
}, { dontCollect: true, forUpdate: true });
const { avail, refundable } = account;
if (totalPrice > avail) {
throw new OakInputIllegalException('withdraw', ['price'], 'error::withdraw.overflow');
}
if (totalPrice > refundable && !withdrawAccountId) {
throw new OakInputIllegalException('withdraw', ['price'], 'error::withdraw.needWithdrawAccountId');
}
const data = {
id: await generateNewIdAsync(),
accountId,
creatorId: context.getCurrentUserId(),
dealLoss: 0,
dealPrice: 0,
};
let preComputeLoss = !withdrawLoss.conservative ? calcWithdrawLossByConfig(totalPrice, withdrawLoss) : 0;
let totalLoss = 0;
let price2 = 0; // 计算过程中累积已经退款的金额
data.price = totalPrice;
if (refundable) {
const refundData = [];
const pays = await context.select('pay', {
data: {
id: 1,
price: 1,
paid: 1,
refunded: 1,
entity: 1,
entityId: 1,
offlineAccount: {
id: 1,
type: 1,
channel: 1,
},
applicationId: 1,
},
filter: {
refundable: true,
deposit: {
accountId,
},
orderId: {
$exists: false,
},
},
sorter: [
{
$attr: {
forbidRefundAt: 1,
},
$direction: 'asc',
}
],
}, { forUpdate: true });
for (const pay of pays) {
const { price, paid, refunded, refundable, entity, entityId, applicationId, offlineAccount } = pay;
assert(price === paid && refundable);
assert(!['account'].includes(entity));
const rest = paid - refunded;
assert(rest > 0);
let refundPrice = rest;
if (totalPrice && totalPrice - price2 < rest) {
refundPrice = totalPrice - price2;
}
price2 += refundPrice;
let loss = 0;
let lossExplanation = 'withdraw::refund.lossReason.preCompute';
// 如果是保守策略则按照渠道配置的规则来计算这次提现的损失并转嫁到整体的loss上
if (withdrawLoss.conservative) {
// tax + delta即是这次refund损失的税费如果配置是conservative就按这个价格退
const payClazz = await getPayClazz(applicationId, entity, entityId, context);
const [delta] = payClazz.calcRefundTax(refundPrice);
const [tax] = payClazz.calcPayTax(refundPrice);
loss = tax + delta;
lossExplanation = loss ? 'withdraw::refund.lossReason.noBack' : 'withdraw::refund.lossReason.back';
}
else {
// 如果是预计算策略,这里就按比例折算,这样成功或者失败的时候易于计算
loss = Math.ceil(preComputeLoss * refundPrice / totalPrice);
}
totalLoss += loss;
refundData.push({
id: await generateNewIdAsync(),
price: refundPrice,
loss,
creatorId: context.getCurrentUserId(),
payId: pay.id,
iState: 'refunding',
meta: {
channel: entity === 'offlineAccount' ? `withdraw::channel.offlineAccount.${offlineAccount.type}` : `${entity}:name`,
lossExplanation,
}
});
if (price2 === totalPrice) {
break;
}
}
data.refund$entity = await Promise.all(refundData.map(async (ele) => ({
id: await generateNewIdAsync(),
action: 'create',
data: ele,
})));
}
else {
data.refund$entity = [];
}
if (totalPrice > price2) {
// 如果还有要退的部分就从withdrawAccount来进行转账
const rest = totalPrice - price2;
assert(price2 === refundable); // 可退款的额度计算必须匹配
assert(withdrawAccountId);
const [withdrawAccount] = await context.select('withdrawAccount', {
data: {
id: 1,
entity: 1,
enabled: 1,
entityId: 1,
channel: {
id: 1,
entity: 1,
entityId: 1,
},
},
filter: {
id: withdrawAccountId,
}
}, { dontCollect: true });
const { enabled, entity, entityId, channel } = withdrawAccount;
assert(enabled);
let loss = 0;
let lossExplanation = 'withdraw::transfer.lossReason.preCompute';
// 如果是保守策略则按照渠道配置的规则来计算这次提现的损失并转嫁到整体的loss上
if (withdrawLoss.conservative) {
// 如果配置是conservative就按计算所选渠道的费用损失值
const payClazz = await getPayClazz(context.getApplicationId(), channel.entity, channel.entityId, context);
loss = payClazz.calcTransferTax(rest)[0];
lossExplanation = loss ? 'withdraw::transfer.lossReason.fee' : 'withdraw::transfer.lossReason.noFee';
}
else {
// 如果是预计算策略,这里就按比例折算,这样成功或者失败的时候易于计算
loss = Math.ceil(preComputeLoss * rest / totalPrice);
}
totalLoss += loss;
data.withdrawTransfer$withdraw = [
{
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
price: rest,
loss,
withdrawAccountId,
meta: {
lossExplanation,
}
}
}
];
}
else {
// 保持结构完整让上层可以和withdraw$detail同构渲染
data.withdrawTransfer$withdraw = [];
}
data.loss = totalLoss;
return data;
}