物流对账单
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user