物流对账单

This commit is contained in:
2026-07-26 09:51:33 +08:00
parent 2b7ef65cce
commit 19cb312465
21 changed files with 1909 additions and 27 deletions
+32
View File
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
import {
calcBenefitAmount,
calcRedeemSettleAmount,
calcLogisticsFeeByBottles,
validateMinPurchase,
validateRedeemAmount,
allocateBenefitCoupons,
@@ -67,6 +68,37 @@ describe('calcRedeemSettleAmount', () => {
});
});
describe('calcLogisticsFeeByBottles', () => {
const xfx = {
baseBottles: 2,
baseFee: 6,
extraBottleFee: 2,
boxBottles: 6,
boxFee: 14,
};
it('charges base for 1~2 bottles', () => {
expect(calcLogisticsFeeByBottles(1, xfx)).toBe(6);
expect(calcLogisticsFeeByBottles(2, xfx)).toBe(6);
});
it('adds extra bottle fee', () => {
expect(calcLogisticsFeeByBottles(3, xfx)).toBe(8);
expect(calcLogisticsFeeByBottles(5, xfx)).toBe(12);
});
it('uses box fee for full boxes', () => {
expect(calcLogisticsFeeByBottles(6, xfx)).toBe(14);
expect(calcLogisticsFeeByBottles(12, xfx)).toBe(28);
});
it('combines boxes with remainder ladder', () => {
expect(calcLogisticsFeeByBottles(7, xfx)).toBe(20);
expect(calcLogisticsFeeByBottles(8, xfx)).toBe(20);
expect(calcLogisticsFeeByBottles(9, xfx)).toBe(22);
});
});
describe('allocateBenefitCoupons', () => {
const coupons = [
{ id: '1', balance: 300, createdAt: 1 },
+41
View File
@@ -95,6 +95,47 @@ export function calcRedeemSettleAmount(amount: number, settlementRate: number):
return Math.round(amount * settlementRate * 100) / 100;
}
/** 物流(快递)按瓶计价规则,如小飞侠:2瓶6元、加一瓶+2元、6瓶一箱14元 */
export type LogisticsPricingRule = {
baseBottles: number;
baseFee: number;
extraBottleFee: number;
boxBottles?: number;
boxFee?: number;
};
function roundMoney(n: number): number {
return Math.round(n * 100) / 100;
}
function calcLogisticsBottleLadder(quantity: number, rule: LogisticsPricingRule): number {
const qty = Math.floor(quantity);
if (qty <= 0) return 0;
if (qty <= rule.baseBottles) return rule.baseFee;
return rule.baseFee + (qty - rule.baseBottles) * rule.extraBottleFee;
}
/**
* 按承运商计价标准计算单票物流费。
* 配置了箱规时:整箱按 boxFee,余瓶按「起送瓶数/加瓶」阶梯。
*/
export function calcLogisticsFeeByBottles(quantity: number, rule: LogisticsPricingRule): number {
const qty = Math.floor(Number(quantity) || 0);
if (qty <= 0) return 0;
if (!(rule.baseBottles > 0) || !(rule.baseFee >= 0) || !(rule.extraBottleFee >= 0)) {
throw new Error('计价标准不完整');
}
const boxBottles = rule.boxBottles && rule.boxBottles > 0 ? rule.boxBottles : 0;
const boxFee = rule.boxFee != null && rule.boxFee >= 0 ? rule.boxFee : null;
if (boxBottles > 0 && boxFee != null) {
const boxes = Math.floor(qty / boxBottles);
const rem = qty % boxBottles;
return roundMoney(boxes * boxFee + calcLogisticsBottleLadder(rem, rule));
}
return roundMoney(calcLogisticsBottleLadder(qty, rule));
}
export function generateUserNo(): string {
const suffix = Math.floor(10000000 + Math.random() * 90000000);
return `DK${suffix}`;
+11
View File
@@ -179,6 +179,17 @@ export enum FulfillmentProviderStatus {
DISABLED = 'DISABLED',
}
/** 物流承运商结算方式:前期充值,后期挂账月结 */
export enum LogisticsSettlementMethod {
PREPAID = 'PREPAID',
MONTHLY_CREDIT = 'MONTHLY_CREDIT',
}
export const LOGISTICS_SETTLEMENT_METHOD_LABELS: Record<LogisticsSettlementMethod, string> = {
[LogisticsSettlementMethod.PREPAID]: '充值扣款',
[LogisticsSettlementMethod.MONTHLY_CREDIT]: '挂账月结',
};
export enum WarehouseFulfillmentMode {
API_AUTO = 'API_AUTO',
MANUAL = 'MANUAL',
@@ -1,5 +1,11 @@
import type { FulfillmentProviderStatus, FulfillmentProviderType } from './enums';
import type {
FulfillmentProviderStatus,
FulfillmentProviderType,
LogisticsSettlementMethod,
} from './enums';
import type { WarehouseFulfillmentMode } from './enums';
import type { LogisticsPricingRuleDto } from './settlement';
import { DEFAULT_XFX_LOGISTICS_PRICING } from './settlement';
/** 小飞侠仓配凭证(存 FulfillmentProvider.configJson */
export interface XiaofeixiaProviderConfig {
@@ -19,6 +25,13 @@ export interface XiaofeixiaProviderConfigPublic {
hasApiKey: boolean;
}
export interface FulfillmentProviderBankDto {
bankAccountName?: string | null;
bankName?: string | null;
bankBranch?: string | null;
bankAccountNo?: string | null;
}
export interface FulfillmentProviderDto {
id: string;
code: string;
@@ -34,6 +47,14 @@ export interface FulfillmentProviderDto {
hasConfig: boolean;
/** 小飞侠等承运商结构化配置(脱敏) */
xiaofeixiaConfig?: XiaofeixiaProviderConfigPublic | null;
/** 结算银行账户 */
bankAccountName?: string | null;
bankName?: string | null;
bankBranch?: string | null;
bankAccountNo?: string | null;
settlementMethod: LogisticsSettlementMethod;
pricingRules?: LogisticsPricingRuleDto | null;
prepaidBalance: number;
createdAt: string;
updatedAt: string;
}
@@ -47,6 +68,12 @@ export interface CreateFulfillmentProviderInput {
capabilitiesJson?: string;
/** 结构化小飞侠配置;有则覆盖写入 configJson */
xiaofeixiaConfig?: Partial<XiaofeixiaProviderConfig>;
bankAccountName?: string | null;
bankName?: string | null;
bankBranch?: string | null;
bankAccountNo?: string | null;
settlementMethod?: LogisticsSettlementMethod;
pricingRules?: LogisticsPricingRuleDto | null;
}
export interface UpdateFulfillmentProviderInput {
@@ -56,8 +83,16 @@ export interface UpdateFulfillmentProviderInput {
configJson?: string;
capabilitiesJson?: string;
xiaofeixiaConfig?: Partial<XiaofeixiaProviderConfig>;
bankAccountName?: string | null;
bankName?: string | null;
bankBranch?: string | null;
bankAccountNo?: string | null;
settlementMethod?: LogisticsSettlementMethod;
pricingRules?: LogisticsPricingRuleDto | null;
}
export { DEFAULT_XFX_LOGISTICS_PRICING };
export interface ManualShipOrderInput {
logisticsCompany: string;
trackingNo: string;
+49
View File
@@ -90,6 +90,53 @@ export const STORE_SETTLEMENT_DEFAULT_RATE = 0.6;
/** 酒厂账单结算比例(酒单实付 × 比例),暂定 30% */
export const WINERY_SETTLEMENT_RATE = 0.3;
export type { LogisticsSettlementMethod } from './enums';
export { LOGISTICS_SETTLEMENT_METHOD_LABELS } from './enums';
import type { LogisticsSettlementMethod } from './enums';
/** 小飞侠默认计价:2瓶6元,加一瓶+2元,6瓶一箱14元 */
export const DEFAULT_XFX_LOGISTICS_PRICING = {
baseBottles: 2,
baseFee: 6,
extraBottleFee: 2,
boxBottles: 6,
boxFee: 14,
} as const;
export type LogisticsPricingRuleDto = {
baseBottles: number;
baseFee: number;
extraBottleFee: number;
boxBottles?: number;
boxFee?: number;
};
export interface LogisticsBillDto {
id: string;
billNo: string;
fulfillmentProviderId: string;
providerCode?: string;
providerName?: string;
periodStart: string;
periodEnd: string;
orderCount: number;
bottleCount: number;
logisticsAmount: number;
settlementMethod: LogisticsSettlementMethod;
status: FinancePayStatus;
paidAt?: string | null;
pricingSnapshot?: LogisticsPricingRuleDto | null;
}
export interface LogisticsBillItemDto {
id: string;
orderId: string;
orderNo: string;
quantity: number;
logisticsAmount: number;
shippedAt: string;
}
export type FinanceBillSummary = {
count: number;
redeemAmount?: number;
@@ -98,6 +145,8 @@ export type FinanceBillSummary = {
orderCommission?: number;
redeemCommission?: number;
wineryAmount?: number;
logisticsAmount?: number;
bottleCount?: number;
totalAmount: number;
};