@@ -8,7 +8,9 @@ import {
|
||||
} from '@dukang/shared-types';
|
||||
import {
|
||||
calcLogisticsFeeByBottles,
|
||||
calcRedeemSettleAmount,
|
||||
pickPayoutsForWithdrawAmount,
|
||||
resolveSettlementRate,
|
||||
sumUnbilledPayoutAmount,
|
||||
validateStoreWithdraw,
|
||||
type LogisticsPricingRule,
|
||||
@@ -96,6 +98,12 @@ export class SettlementService {
|
||||
settlementRate: number,
|
||||
tx?: Prisma.TransactionClient,
|
||||
) {
|
||||
const redeemAmountNum = Number(redeemAmount);
|
||||
const payoutAmountNum = Number(payoutAmount);
|
||||
const settlementRateNum = resolveSettlementRate(settlementRate);
|
||||
if (!(redeemAmountNum > 0) || !(payoutAmountNum > 0)) {
|
||||
throw new BadRequestException('结算金额无效,无法入账');
|
||||
}
|
||||
const expectedPayAt = new Date();
|
||||
expectedPayAt.setDate(expectedPayAt.getDate() + 1);
|
||||
const client = tx ?? this.prisma;
|
||||
@@ -103,9 +111,9 @@ export class SettlementService {
|
||||
data: {
|
||||
redeemRecordId,
|
||||
storeId,
|
||||
redeemAmount,
|
||||
payoutAmount,
|
||||
settlementRate,
|
||||
redeemAmount: redeemAmountNum,
|
||||
payoutAmount: payoutAmountNum,
|
||||
settlementRate: settlementRateNum,
|
||||
status: 'PENDING',
|
||||
expectedPayAt,
|
||||
},
|
||||
@@ -152,17 +160,65 @@ export class SettlementService {
|
||||
}
|
||||
|
||||
private async listAvailableUnbilledPayouts(storeId: bigint) {
|
||||
await this.backfillMissingStorePayouts(storeId);
|
||||
return this.prisma.storePayout.findMany({
|
||||
where: {
|
||||
storeId,
|
||||
status: 'PENDING',
|
||||
storeBillId: null,
|
||||
withdrawItem: null,
|
||||
withdrawItem: { is: null },
|
||||
},
|
||||
orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 补写历史核销缺失的 store_payout(含测试流水)。
|
||||
* 线上曾出现核销成功但未入账、可提余额为 0 的情况。
|
||||
*/
|
||||
async backfillMissingStorePayouts(storeId: bigint) {
|
||||
const store = await this.prisma.store.findUnique({
|
||||
where: { id: storeId },
|
||||
select: { settlementRate: true },
|
||||
});
|
||||
if (!store) return 0;
|
||||
|
||||
const orphans = await this.prisma.redeemRecord.findMany({
|
||||
where: {
|
||||
storeId,
|
||||
payout: { is: null },
|
||||
},
|
||||
select: { id: true, amount: true, settleAmount: true },
|
||||
take: 200,
|
||||
});
|
||||
if (!orphans.length) return 0;
|
||||
|
||||
const settlementRate = resolveSettlementRate(store.settlementRate);
|
||||
let created = 0;
|
||||
for (const row of orphans) {
|
||||
const redeemAmount = Number(row.amount);
|
||||
let payoutAmount = Number(row.settleAmount);
|
||||
if (!(redeemAmount > 0)) continue;
|
||||
if (!(payoutAmount > 0)) {
|
||||
payoutAmount = calcRedeemSettleAmount(redeemAmount, settlementRate);
|
||||
}
|
||||
if (!(payoutAmount > 0)) continue;
|
||||
try {
|
||||
await this.createStorePayout(
|
||||
row.id,
|
||||
storeId,
|
||||
redeemAmount,
|
||||
payoutAmount,
|
||||
settlementRate,
|
||||
);
|
||||
created += 1;
|
||||
} catch {
|
||||
// 并发补写时可能已存在 payout
|
||||
}
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
private async todayWithdrawAppliedAmount(storeId: bigint, now = new Date()) {
|
||||
const start = startOfDay(now);
|
||||
const end = new Date(start);
|
||||
@@ -291,7 +347,7 @@ export class SettlementService {
|
||||
storeId,
|
||||
status: 'PENDING',
|
||||
storeBillId: null,
|
||||
withdrawItem: null,
|
||||
withdrawItem: { is: null },
|
||||
},
|
||||
select: { id: true, payoutAmount: true },
|
||||
});
|
||||
@@ -782,7 +838,7 @@ export class SettlementService {
|
||||
storeBillId: null,
|
||||
createdAt: { gte: start, lt: end },
|
||||
// 排除已锁定在待审提现单中的明细,避免出账与提现双占
|
||||
withdrawItem: null,
|
||||
withdrawItem: { is: null },
|
||||
status: 'PENDING',
|
||||
},
|
||||
include: { store: { select: { id: true, settlementRate: true } } },
|
||||
@@ -1322,12 +1378,9 @@ export class SettlementService {
|
||||
cityId: primary.cityId,
|
||||
payStatus: 'PAID',
|
||||
paidAt: { gte: periodStart, lte: periodEnd },
|
||||
isTest: false,
|
||||
},
|
||||
});
|
||||
const orderCommission = primary.isTest
|
||||
? 0
|
||||
: orders.reduce((sum, o) => {
|
||||
const orderCommission = orders.reduce((sum, o) => {
|
||||
if (o.partnerAccountIdAtPay) {
|
||||
if (o.partnerAccountIdAtPay !== primary.id) return sum;
|
||||
const rate = o.orderCommissionRateAtPay != null ? Number(o.orderCommissionRateAtPay) : 0;
|
||||
@@ -1341,14 +1394,12 @@ export class SettlementService {
|
||||
select: { id: true },
|
||||
});
|
||||
const storeIds = stores.map((s) => s.id);
|
||||
const redeems =
|
||||
primary.isTest || !storeIds.length
|
||||
? []
|
||||
: await this.prisma.redeemRecord.findMany({
|
||||
const redeems = !storeIds.length
|
||||
? []
|
||||
: await this.prisma.redeemRecord.findMany({
|
||||
where: {
|
||||
storeId: { in: storeIds },
|
||||
createdAt: { gte: periodStart, lte: periodEnd },
|
||||
isTest: false,
|
||||
},
|
||||
});
|
||||
const redeemCommission = redeems.reduce(
|
||||
@@ -1593,7 +1644,6 @@ export class SettlementService {
|
||||
payStatus: 'PAID',
|
||||
deliveryType: { in: ['LOCAL', 'CROSS_CITY'] },
|
||||
completedAt: { gte: start, lt: end },
|
||||
isTest: false,
|
||||
},
|
||||
orderBy: { completedAt: 'asc' },
|
||||
});
|
||||
@@ -1944,7 +1994,6 @@ export class SettlementService {
|
||||
quantity: true,
|
||||
deliveryType: true,
|
||||
payStatus: true,
|
||||
isTest: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1953,7 +2002,6 @@ export class SettlementService {
|
||||
|
||||
const eligible = deliveries.filter(
|
||||
(d) =>
|
||||
!d.order.isTest &&
|
||||
d.order.payStatus === 'PAID' &&
|
||||
(d.order.deliveryType === 'LOCAL' || d.order.deliveryType === 'CROSS_CITY'),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user