物流对账单
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { FulfillmentProviderStatus, FulfillmentProviderType } from '@prisma/client';
|
||||
import {
|
||||
FulfillmentProviderStatus,
|
||||
FulfillmentProviderType,
|
||||
LogisticsSettlementMethod,
|
||||
Prisma,
|
||||
} from '@prisma/client';
|
||||
import {
|
||||
DEFAULT_XFX_LOGISTICS_PRICING,
|
||||
isXfxProviderCode,
|
||||
type LogisticsPricingRuleDto,
|
||||
type XiaofeixiaProviderConfig,
|
||||
type XiaofeixiaProviderConfigPublic,
|
||||
} from '@dukang/shared-types';
|
||||
@@ -17,6 +24,12 @@ export type CreateFulfillmentProviderInput = {
|
||||
configJson?: string;
|
||||
capabilitiesJson?: string;
|
||||
xiaofeixiaConfig?: Partial<XiaofeixiaProviderConfig>;
|
||||
bankAccountName?: string | null;
|
||||
bankName?: string | null;
|
||||
bankBranch?: string | null;
|
||||
bankAccountNo?: string | null;
|
||||
settlementMethod?: LogisticsSettlementMethod | string;
|
||||
pricingRules?: LogisticsPricingRuleDto | null;
|
||||
};
|
||||
|
||||
export type UpdateFulfillmentProviderInput = Partial<CreateFulfillmentProviderInput>;
|
||||
@@ -106,6 +119,13 @@ export class FulfillmentProviderService {
|
||||
this.assertXiaofeixiaConfigComplete(configJson, true);
|
||||
}
|
||||
|
||||
const pricingRulesJson = this.resolvePricingRulesJson(
|
||||
code,
|
||||
input.pricingRules,
|
||||
isXfxProviderCode(code) ? DEFAULT_XFX_LOGISTICS_PRICING : null,
|
||||
);
|
||||
const settlementMethod = this.parseSettlementMethod(input.settlementMethod) ?? 'PREPAID';
|
||||
|
||||
const row = await this.prisma.fulfillmentProvider.create({
|
||||
data: {
|
||||
code,
|
||||
@@ -123,6 +143,12 @@ export class FulfillmentProviderService {
|
||||
cancel: true,
|
||||
})
|
||||
: null),
|
||||
bankAccountName: this.normOptional(input.bankAccountName),
|
||||
bankName: this.normOptional(input.bankName),
|
||||
bankBranch: this.normOptional(input.bankBranch),
|
||||
bankAccountNo: this.normOptional(input.bankAccountNo),
|
||||
settlementMethod,
|
||||
pricingRulesJson,
|
||||
},
|
||||
});
|
||||
return this.toDto(row);
|
||||
@@ -142,6 +168,11 @@ export class FulfillmentProviderService {
|
||||
this.assertXiaofeixiaConfigComplete(configJson, false);
|
||||
}
|
||||
|
||||
const pricingRulesJson =
|
||||
input.pricingRules !== undefined
|
||||
? this.resolvePricingRulesJson(code, input.pricingRules, null)
|
||||
: undefined;
|
||||
|
||||
const row = await this.prisma.fulfillmentProvider.update({
|
||||
where: { id },
|
||||
data: {
|
||||
@@ -152,11 +183,87 @@ export class FulfillmentProviderService {
|
||||
...(input.capabilitiesJson !== undefined
|
||||
? { capabilitiesJson: input.capabilitiesJson?.trim() || null }
|
||||
: {}),
|
||||
...(input.bankAccountName !== undefined
|
||||
? { bankAccountName: this.normOptional(input.bankAccountName) }
|
||||
: {}),
|
||||
...(input.bankName !== undefined ? { bankName: this.normOptional(input.bankName) } : {}),
|
||||
...(input.bankBranch !== undefined ? { bankBranch: this.normOptional(input.bankBranch) } : {}),
|
||||
...(input.bankAccountNo !== undefined
|
||||
? { bankAccountNo: this.normOptional(input.bankAccountNo) }
|
||||
: {}),
|
||||
...(input.settlementMethod !== undefined
|
||||
? { settlementMethod: this.parseSettlementMethod(input.settlementMethod)! }
|
||||
: {}),
|
||||
...(pricingRulesJson !== undefined ? { pricingRulesJson } : {}),
|
||||
},
|
||||
});
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
/** 充值(结算模块可复用) */
|
||||
async rechargePrepaid(providerId: bigint, amount: number, remark?: string) {
|
||||
if (!(amount > 0)) throw new BadRequestException('充值金额须大于 0');
|
||||
const rounded = Math.round(amount * 100) / 100;
|
||||
|
||||
const result = await this.prisma.$transaction(async (tx) => {
|
||||
const row = await tx.fulfillmentProvider.findUnique({ where: { id: providerId } });
|
||||
if (!row) throw new NotFoundException('仓配承运商不存在');
|
||||
const balanceAfter = Math.round((Number(row.prepaidBalance) + rounded) * 100) / 100;
|
||||
const updated = await tx.fulfillmentProvider.update({
|
||||
where: { id: providerId },
|
||||
data: { prepaidBalance: balanceAfter },
|
||||
});
|
||||
const ledger = await tx.logisticsPrepaidLedger.create({
|
||||
data: {
|
||||
fulfillmentProviderId: providerId,
|
||||
type: 'RECHARGE',
|
||||
amount: rounded,
|
||||
balanceAfter,
|
||||
remark: remark?.trim() || '充值',
|
||||
},
|
||||
});
|
||||
return { provider: updated, ledger };
|
||||
});
|
||||
|
||||
return serializeBigInt({
|
||||
provider: this.toDto(result.provider),
|
||||
ledger: result.ledger,
|
||||
});
|
||||
}
|
||||
|
||||
/** 账单扣减充值余额;余额不足返回 false */
|
||||
async deductPrepaid(
|
||||
providerId: bigint,
|
||||
amount: number,
|
||||
logisticsBillId: bigint,
|
||||
tx?: Prisma.TransactionClient,
|
||||
): Promise<{ ok: true; balanceAfter: number } | { ok: false; balance: number }> {
|
||||
const client = tx ?? this.prisma;
|
||||
const rounded = Math.round(amount * 100) / 100;
|
||||
const row = await client.fulfillmentProvider.findUnique({ where: { id: providerId } });
|
||||
if (!row) throw new NotFoundException('仓配承运商不存在');
|
||||
const balance = Number(row.prepaidBalance);
|
||||
if (balance + 1e-9 < rounded) {
|
||||
return { ok: false, balance };
|
||||
}
|
||||
const balanceAfter = Math.round((balance - rounded) * 100) / 100;
|
||||
await client.fulfillmentProvider.update({
|
||||
where: { id: providerId },
|
||||
data: { prepaidBalance: balanceAfter },
|
||||
});
|
||||
await client.logisticsPrepaidLedger.create({
|
||||
data: {
|
||||
fulfillmentProviderId: providerId,
|
||||
type: 'DEDUCT',
|
||||
amount: rounded,
|
||||
balanceAfter,
|
||||
logisticsBillId,
|
||||
remark: '物流月账单扣款',
|
||||
},
|
||||
});
|
||||
return { ok: true, balanceAfter };
|
||||
}
|
||||
|
||||
parseCapabilities(raw: string | null): Capabilities | null {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
@@ -183,6 +290,63 @@ export class FulfillmentProviderService {
|
||||
}
|
||||
}
|
||||
|
||||
parsePricingRules(raw: string | null): LogisticsPricingRuleDto | null {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<LogisticsPricingRuleDto>;
|
||||
if (!parsed || typeof parsed !== 'object') return null;
|
||||
const baseBottles = Number(parsed.baseBottles);
|
||||
const baseFee = Number(parsed.baseFee);
|
||||
const extraBottleFee = Number(parsed.extraBottleFee);
|
||||
if (!(baseBottles > 0) || !(baseFee >= 0) || !(extraBottleFee >= 0)) return null;
|
||||
const rule: LogisticsPricingRuleDto = { baseBottles, baseFee, extraBottleFee };
|
||||
if (parsed.boxBottles != null && Number(parsed.boxBottles) > 0) {
|
||||
rule.boxBottles = Number(parsed.boxBottles);
|
||||
}
|
||||
if (parsed.boxFee != null && Number(parsed.boxFee) >= 0) {
|
||||
rule.boxFee = Number(parsed.boxFee);
|
||||
}
|
||||
return rule;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private resolvePricingRulesJson(
|
||||
code: string,
|
||||
input: LogisticsPricingRuleDto | null | undefined,
|
||||
fallback: LogisticsPricingRuleDto | null,
|
||||
): string | null {
|
||||
if (input === null) return null;
|
||||
const rule = input ?? fallback;
|
||||
if (!rule) return isXfxProviderCode(code) ? JSON.stringify(DEFAULT_XFX_LOGISTICS_PRICING) : null;
|
||||
if (!(rule.baseBottles > 0)) throw new BadRequestException('计价标准:起送瓶数须大于 0');
|
||||
if (!(rule.baseFee >= 0)) throw new BadRequestException('计价标准:起送费用无效');
|
||||
if (!(rule.extraBottleFee >= 0)) throw new BadRequestException('计价标准:加瓶费用无效');
|
||||
return JSON.stringify({
|
||||
baseBottles: Number(rule.baseBottles),
|
||||
baseFee: Number(rule.baseFee),
|
||||
extraBottleFee: Number(rule.extraBottleFee),
|
||||
...(rule.boxBottles != null ? { boxBottles: Number(rule.boxBottles) } : {}),
|
||||
...(rule.boxFee != null ? { boxFee: Number(rule.boxFee) } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
private parseSettlementMethod(
|
||||
raw?: LogisticsSettlementMethod | string | null,
|
||||
): LogisticsSettlementMethod | null {
|
||||
if (raw == null || raw === '') return null;
|
||||
if (raw === 'PREPAID' || raw === 'MONTHLY_CREDIT') return raw;
|
||||
throw new BadRequestException('结算方式仅支持 PREPAID / MONTHLY_CREDIT');
|
||||
}
|
||||
|
||||
private normOptional(v?: string | null) {
|
||||
if (v === undefined) return undefined;
|
||||
if (v == null) return null;
|
||||
const t = String(v).trim();
|
||||
return t || null;
|
||||
}
|
||||
|
||||
private resolveConfigJsonForWrite(
|
||||
code: string,
|
||||
existingRaw: string | null,
|
||||
@@ -242,6 +406,13 @@ export class FulfillmentProviderService {
|
||||
status: string;
|
||||
configJson: string | null;
|
||||
capabilitiesJson: string | null;
|
||||
bankAccountName?: string | null;
|
||||
bankName?: string | null;
|
||||
bankBranch?: string | null;
|
||||
bankAccountNo?: string | null;
|
||||
settlementMethod?: string;
|
||||
pricingRulesJson?: string | null;
|
||||
prepaidBalance?: Prisma.Decimal | number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}) {
|
||||
@@ -256,6 +427,13 @@ export class FulfillmentProviderService {
|
||||
xiaofeixiaConfig: isXfxProviderCode(row.code)
|
||||
? this.toPublicXiaofeixiaConfig(row.configJson)
|
||||
: null,
|
||||
bankAccountName: row.bankAccountName ?? null,
|
||||
bankName: row.bankName ?? null,
|
||||
bankBranch: row.bankBranch ?? null,
|
||||
bankAccountNo: row.bankAccountNo ?? null,
|
||||
settlementMethod: row.settlementMethod ?? 'PREPAID',
|
||||
pricingRules: this.parsePricingRules(row.pricingRulesJson ?? null),
|
||||
prepaidBalance: Number(row.prepaidBalance ?? 0),
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user