feat(pricing): 开发定价方案
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user