Files
atlas-seller-kit/apps/api/test/pricing-engine.unit.test.ts
T

326 lines
10 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
calculatePricing,
type LogisticsRule,
type CommissionRule,
} from '../src/pricing/pricing-engine.js';
import { pricingPreviewRequestSchema, pricingInputKey } from '@atlas/contracts';
const id = 'ad877f74-36d0-4ef9-a711-cd904b37912f';
const input = () =>
pricingPreviewRequestSchema.parse({
store_id: id,
platform_product_category_id: id,
fulfillment: 'rfbs',
purchase_cost_cny: '28',
package: {
shipping_weight_g: '380',
length_mm: '220',
width_mm: '160',
height_mm: '90',
status: 'confirmed',
},
pricing: {
rate_card_id: id,
transport_method_ids: [id],
default_transport_method_id: id,
domestic_shipping_and_label_fee_cny: '3',
ad_rate: '0.03',
last_mile_rate: '0.02',
withdrawal_rate: '0.01',
other_rate: '0.02',
desired_markup_rate: '0.5',
list_price_discount_rate: '0.2',
},
});
const rule: LogisticsRule = {
id,
product_type_id: id,
transport_method_id: id,
transport_name: '标准',
product_type_name: 'ExtraSmall',
fixed_fee_cny: '3.12',
per_billable_kg_fee_cny: '36.4',
min_charge_cny: null,
surcharge_rules: [],
merchandise_currency_code: 'CNY',
min_merchandise_value: null,
max_merchandise_value: '135',
min_value_inclusive: true,
max_value_inclusive: true,
min_actual_weight_kg: '0.001',
min_actual_weight_inclusive: true,
max_actual_weight_kg: '0.5',
max_sum_dimensions_cm: '90',
max_longest_side_cm: '60',
max_second_longest_side_cm: null,
billing_basis: 'actual_only',
volume_divisor: null,
volume_condition_sum_gt_cm: null,
volume_round_scale: null,
billing_round_scale: null,
max_volume_weight_kg: null,
volume_limit_round_scale: null,
};
const commission: CommissionRule = {
id,
currency_code: 'RUB',
min_price: null,
max_price: null,
min_inclusive: true,
max_inclusive: true,
commission_rate: '0.12',
version: 'test',
};
describe('十进制定价引擎', () => {
it('目标加价与明细一致,CNY / RUB 都按最终币种舍入', () => {
const c = calculatePricing(input(), 'CNY', '12.3', [rule], [commission])
.options[0];
expect(c.selling_price).toBe('77.44');
expect(c.profit_cny).toBe('14.00');
expect(c.list_price).toBe('96.80');
expect(c.fees.logistics_cny).toBe('16.9520');
const rub = calculatePricing(
input(),
'RUB',
'12.3',
[rule],
[commission],
).options[0];
expect(rub.selling_price).toBe('952.52');
expect(rub.can_confirm).toBe(true);
});
it('固定售价保持原价并识别亏损', () => {
const i = input();
i.pricing.mode = 'fixed_sale';
i.pricing.selling_price = '10';
const o = calculatePricing(i, 'CNY', '12.3', [rule], [commission])
.options[0];
expect(o.selling_price).toBe('10.00');
expect(o.can_confirm).toBe(false);
expect(Number(o.profit_cny)).toBeLessThan(0);
});
it('零佣金仍生成正式报价,平台佣金费用为零', () => {
const o = calculatePricing(
input(),
'RUB',
'12.3',
[rule],
[{ ...commission, commission_rate: '0.000000' }],
).options[0];
expect(o.commission_rate).toBe('0.000000');
expect(o.fees.commission_cny).toBe('0.0000');
});
it('实际重、尺寸和开区间边界精确生效', () => {
const i = input();
i.package.shipping_weight_g = '500';
expect(
calculatePricing(i, 'CNY', '12.3', [rule], [commission]).options,
).toHaveLength(1);
i.package.shipping_weight_g = '500.001';
expect(
calculatePricing(i, 'CNY', '12.3', [rule], [commission]).options,
).toHaveLength(0);
i.package.shipping_weight_g = '380';
expect(
calculatePricing(
i,
'CNY',
'12.3',
[
{
...rule,
min_actual_weight_kg: '0.380',
min_actual_weight_inclusive: false,
},
],
[commission],
).options,
).toHaveLength(0);
});
it('先舍入售价再检查货值及佣金档,不跨档蒙混', () => {
const i = input();
expect(
calculatePricing(
i,
'CNY',
'12.3',
[{ ...rule, max_merchandise_value: '77.439' }],
[commission],
).options,
).toHaveLength(0);
expect(
calculatePricing(
i,
'CNY',
'12.3',
[rule],
[{ ...commission, max_price: '952.51' }],
).options,
).toHaveLength(0);
});
it('总费率达到 100% 拒绝,未知附加费不能漏算', () => {
const i = input();
i.pricing.ad_rate = '0.9';
expect(
calculatePricing(i, 'CNY', '12.3', [rule], [commission]).options,
).toHaveLength(0);
expect(
calculatePricing(
input(),
'CNY',
'12.3',
[{ ...rule, surcharge_rules: [{ unknown: true }] }],
[commission],
).options,
).toHaveLength(0);
});
it('体积重按指定位数进位并检查进位后的上限', () => {
const i = input();
i.package = {
shipping_weight_g: '5000',
length_mm: '500',
width_mm: '400',
height_mm: '301',
status: 'confirmed',
};
const volume = {
...rule,
max_actual_weight_kg: '30',
max_sum_dimensions_cm: '310',
billing_basis: 'max_actual_volume',
volume_divisor: '12000',
volume_round_scale: 3,
max_volume_weight_kg: '6',
volume_limit_round_scale: 0,
};
expect(
calculatePricing(
i,
'CNY',
'12.3',
[{ ...volume, max_merchandise_value: null }],
[commission],
).options[0].billable_weight_kg,
).toBe('5.017');
expect(
calculatePricing(
i,
'CNY',
'12.3',
[{ ...volume, max_volume_weight_kg: '5' }],
[commission],
).options,
).toHaveLength(0);
});
it('默认运输失效时不偷偷选择其他方式', () => {
const i = input();
i.pricing.default_transport_method_id =
'8d6df67c-1fa3-45b9-9766-d4f5c813b903';
expect(
calculatePricing(i, 'CNY', '12.3', [rule], [commission])
.recommended_option_id,
).toBeNull();
});
it('格式化金额与 JSONB 字段重排不会使同一输入失效', () => {
const original = input();
const reordered = JSON.parse(
JSON.stringify(original, (_key, value) =>
value && typeof value === 'object' && !Array.isArray(value)
? Object.fromEntries(Object.entries(value).reverse())
: value,
),
);
reordered.purchase_cost_cny = '28.00';
reordered.package.shipping_weight_g = '380.000';
reordered.pricing.ad_rate = '0.030000';
expect(pricingInputKey(original)).toBe(pricingInputKey(reordered));
reordered.package.length_mm = '221';
expect(pricingInputKey(original)).not.toBe(pricingInputKey(reordered));
});
it('显示利润舍入为零时,真实微亏仍禁止确认', () => {
const i = input();
i.pricing.mode = 'fixed_sale';
i.pricing.selling_price = '59.94';
const option = calculatePricing(
i,
'CNY',
'12.3',
[{ ...rule, fixed_fee_cny: '3.121' }],
[commission],
).options[0];
expect(option.profit_cny).toBe('-0.00');
expect(option.can_confirm).toBe(false);
});
});
describe('导入佣金的卢布分档边界', () => {
const bands: CommissionRule[] = [
{
...commission,
min_price: '0',
max_price: '1500',
min_inclusive: false,
commission_rate: '0.12',
},
{
...commission,
min_price: '1500',
max_price: '5000',
min_inclusive: false,
commission_rate: '0.14',
},
{
...commission,
min_price: '5000',
max_price: null,
min_inclusive: false,
commission_rate: '0.205',
},
];
it.each([
['1500', '0.12'],
['1500.01', '0.14'],
['5000', '0.14'],
['5000.01', '0.205'],
])('售价 %s 只命中对应佣金档', (price, rate) => {
const i = input();
i.pricing.mode = 'fixed_sale';
i.pricing.selling_price = price;
const result = calculatePricing(
i,
'RUB',
'12.3',
[{ ...rule, max_merchandise_value: null }],
bands,
);
expect(result.options).toHaveLength(1);
expect(Number(result.options[0].commission_rate)).toBe(Number(rate));
});
it('人民币售价换算卢布后匹配费率', () => {
const i = input();
i.pricing.mode = 'fixed_sale';
i.pricing.selling_price = '150';
const result = calculatePricing(
i,
'CNY',
'10',
[{ ...rule, max_merchandise_value: null }],
bands,
);
expect(result.options).toHaveLength(1);
expect(Number(result.options[0].commission_rate)).toBe(0.12);
i.pricing.selling_price = '150.01';
expect(
Number(
calculatePricing(
i,
'CNY',
'10',
[{ ...rule, max_merchandise_value: null }],
bands,
).options[0].commission_rate,
),
).toBe(0.14);
});
});