feat(settlement): 门店未出账手动提现与总部审核(OPT-010)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -96,6 +96,85 @@ export function calcRedeemSettleAmount(amount: number, settlementRate: number):
|
||||
return Math.round(amount * settlementRate * 100) / 100;
|
||||
}
|
||||
|
||||
export function sumUnbilledPayoutAmount(payouts: Array<{ payoutAmount: number }>): number {
|
||||
return Math.round(payouts.reduce((sum, p) => sum + Number(p.payoutAmount || 0), 0) * 100) / 100;
|
||||
}
|
||||
|
||||
export type ValidateStoreWithdrawInput = {
|
||||
whitelistEnabled: boolean;
|
||||
availableAmount: number;
|
||||
requestAmount: number;
|
||||
todayApplied: number;
|
||||
dailyLimit: number;
|
||||
hasPendingRequest: boolean;
|
||||
hasBankAccount: boolean;
|
||||
};
|
||||
|
||||
/** 门店未出账提现护栏(FIN-001/002 + 幂等/账户) */
|
||||
export function validateStoreWithdraw(
|
||||
input: ValidateStoreWithdrawInput,
|
||||
): { ok: boolean; message?: string } {
|
||||
if (!input.whitelistEnabled) {
|
||||
return { ok: false, message: '该门店未开通未出账提现(需总部白名单)' };
|
||||
}
|
||||
if (!input.hasBankAccount) {
|
||||
return { ok: false, message: '请先完善入驻收款账户后再提现' };
|
||||
}
|
||||
if (input.hasPendingRequest) {
|
||||
return { ok: false, message: '已有待审核提现申请,请等待处理完成' };
|
||||
}
|
||||
if (!(input.requestAmount > 0)) {
|
||||
return { ok: false, message: '提现金额必须大于 0' };
|
||||
}
|
||||
if (input.requestAmount > input.availableAmount + 1e-9) {
|
||||
return { ok: false, message: '提现金额不能超过可提未出账余额' };
|
||||
}
|
||||
const remaining = Math.round((input.dailyLimit - input.todayApplied) * 100) / 100;
|
||||
if (remaining <= 0) {
|
||||
return { ok: false, message: `已达单店单日提现上限 ¥${input.dailyLimit.toFixed(2)}` };
|
||||
}
|
||||
if (input.requestAmount > remaining + 1e-9) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `超过单日剩余额度 ¥${remaining.toFixed(2)}(上限 ¥${input.dailyLimit.toFixed(2)})`,
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 FIFO 选取未出账 payout,使合计尽量等于 targetAmount(不超过目标)。
|
||||
* 若无法精确凑齐,返回合计 ≤ target 的最长前缀子集。
|
||||
*/
|
||||
export function pickPayoutsForWithdrawAmount<T extends { payoutAmount: number }>(
|
||||
payoutsAsc: T[],
|
||||
targetAmount: number,
|
||||
): { ok: true; selected: T[]; amount: number } | { ok: false; message: string } {
|
||||
if (!(targetAmount > 0)) return { ok: false, message: '提现金额必须大于 0' };
|
||||
const selected: T[] = [];
|
||||
let sum = 0;
|
||||
for (const p of payoutsAsc) {
|
||||
const next = Math.round((sum + Number(p.payoutAmount)) * 100) / 100;
|
||||
if (next <= targetAmount + 1e-9) {
|
||||
selected.push(p);
|
||||
sum = next;
|
||||
if (Math.abs(sum - targetAmount) < 1e-9) break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!selected.length) {
|
||||
return { ok: false, message: '无可匹配的未出账结算明细,请调整提现金额' };
|
||||
}
|
||||
if (Math.abs(sum - targetAmount) > 1e-9) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `无法按 ¥${targetAmount.toFixed(2)} 精确匹配明细,请按可提总额全额提现或调整金额`,
|
||||
};
|
||||
}
|
||||
return { ok: true, selected, amount: sum };
|
||||
}
|
||||
|
||||
const TIME_HM_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
||||
|
||||
export type BusinessHoursSegment = { open: string; close: string };
|
||||
|
||||
Reference in New Issue
Block a user