import { normalizePricingInput } from '@atlas/contracts'; import { randomUUID, createHash } from 'node:crypto'; import { Decimal } from 'decimal.js'; import type { PricingOption, PricingPreviewRequest } from '@atlas/contracts'; Decimal.set({ precision: 40, rounding: Decimal.ROUND_HALF_UP }); export const D = (value: Decimal.Value) => new Decimal(value); export function fingerprint(value: unknown): string { const sort = (v: unknown): unknown => v instanceof Date ? v.toISOString() : Array.isArray(v) ? v.map(sort) : v && typeof v === 'object' ? Object.fromEntries( Object.entries(v) .sort(([a], [b]) => a.localeCompare(b)) .map(([k, x]) => [k, sort(x)]), ) : v; return createHash('sha256') .update(JSON.stringify(sort(value))) .digest('hex'); } export type LogisticsRule = { id: string; product_type_id: string; transport_method_id: string; transport_name: string; product_type_name: string; fixed_fee_cny: string; per_billable_kg_fee_cny: string; min_charge_cny: string | null; surcharge_rules: unknown[]; merchandise_currency_code: string; min_merchandise_value: string | null; max_merchandise_value: string | null; min_value_inclusive: boolean; max_value_inclusive: boolean; min_actual_weight_kg: string; min_actual_weight_inclusive: boolean; max_actual_weight_kg: string | null; max_sum_dimensions_cm: string | null; max_longest_side_cm: string | null; max_second_longest_side_cm: string | null; billing_basis: string; volume_divisor: string | null; volume_condition_sum_gt_cm: string | null; volume_round_scale: number | null; billing_round_scale: number | null; max_volume_weight_kg: string | null; volume_limit_round_scale: number | null; }; export type CommissionRule = { category_name?: string; source?: string; effective_from?: string | Date; id: string | null; currency_code: string; min_price: string | null; max_price: string | null; min_inclusive: boolean; max_inclusive: boolean; commission_rate: string; version: string; }; const inRange = ( n: Decimal, min: string | null, max: string | null, includeMin: boolean, includeMax: boolean, ) => (min === null || (includeMin ? n.gte(min) : n.gt(min))) && (max === null || (includeMax ? n.lte(max) : n.lt(max))); const ceil = (n: Decimal, scale: number | null) => scale === null ? n : n.toDecimalPlaces(scale, Decimal.ROUND_CEIL); export const normalizeInput = normalizePricingInput; /** 审核样本与正式报价共用包装适用性和运费计算,避免两套规则产生不同结果。 */ export function calculateLogisticsFee( r: LogisticsRule, packageSpec: PricingPreviewRequest['package'], ): { billable: Decimal; logistics: Decimal } | { reason: string } { if (r.surcharge_rules.length) return { reason: '此规则含尚未支持的附加费,需完成审核后使用' }; const weight = D(packageSpec.shipping_weight_g).div(1000); const sides = [ packageSpec.length_mm, packageSpec.width_mm, packageSpec.height_mm, ] .map((v) => D(v).div(10)) .sort((a, b) => b.cmp(a)); const sum = sides.reduce((a, b) => a.add(b), D(0)); if ( !inRange( weight, r.min_actual_weight_kg, r.max_actual_weight_kg, r.min_actual_weight_inclusive, true, ) || (r.max_sum_dimensions_cm && sum.gt(r.max_sum_dimensions_cm)) || (r.max_longest_side_cm && sides[0].gt(r.max_longest_side_cm)) || (r.max_second_longest_side_cm && sides[1].gt(r.max_second_longest_side_cm)) ) return { reason: '包装重量或尺寸超出物流范围' }; let billable = weight; if (r.billing_basis !== 'actual_only') { if (!r.volume_divisor || D(r.volume_divisor).lte(0)) return { reason: '体积重规则无效' }; const volume = sides .reduce((a, b) => a.mul(b), D(1)) .div(r.volume_divisor); if ( r.max_volume_weight_kg && ceil(volume, r.volume_limit_round_scale).gt(r.max_volume_weight_kg) ) return { reason: '体积重超限' }; if ( r.billing_basis === 'max_actual_volume' || (r.volume_condition_sum_gt_cm && sum.gt(r.volume_condition_sum_gt_cm)) ) billable = Decimal.max(weight, ceil(volume, r.volume_round_scale)); } billable = ceil(billable, r.billing_round_scale); const logistics = Decimal.max( billable.mul(r.per_billable_kg_fee_cny).add(r.fixed_fee_cny), r.min_charge_cny ?? '0', ); return { billable, logistics }; } export function logisticsMerchandiseFits( r: LogisticsRule, valueCny: string, ): boolean { return ( r.merchandise_currency_code === 'CNY' && inRange( D(valueCny), r.min_merchandise_value, r.max_merchandise_value, r.min_value_inclusive, r.max_value_inclusive, ) ); } /** 枚举自洽候选;先舍入目标币种售价,再回验佣金档与货值档,避免跨档误报。 */ export function calculatePricing( input: PricingPreviewRequest, currency: string, fx: string, rules: LogisticsRule[], commissions: CommissionRule[], ) { if (!['CNY', 'RUB'].includes(currency)) throw new Error('门店币种仅支持 CNY / RUB'); const p = input.pricing, exchange = D(fx), purchase = D(input.purchase_cost_cny), domestic = D(p.domestic_shipping_and_label_fee_cny); const options: PricingOption[] = [], unavailable_options: Array<{ transport_method_id: string; reason: string; }> = []; for (const transport of p.transport_method_ids) { const reasons = new Set(); for (const r of rules.filter( (r) => r.transport_method_id === transport, )) { const fee = calculateLogisticsFee(r, input.package); if ('reason' in fee) { reasons.add(fee.reason); continue; } const { billable, logistics } = fee; for (const c of commissions) { const totalRate = D(c.commission_rate) .add(p.ad_rate) .add(p.last_mile_rate) .add(p.withdrawal_rate) .add(p.other_rate); if (totalRate.gte(1)) { reasons.add('总扣费率达到或超过 100%'); continue; } const factor = currency === 'RUB' ? exchange : D(1); const sale = p.mode === 'fixed_sale' ? D(p.selling_price!) : purchase .mul(D(1).add(p.desired_markup_rate)) .add(logistics) .add(domestic) .div(D(1).sub(totalRate)) .mul(factor); const rounded = sale.toDecimalPlaces( 2, p.mode === 'fixed_sale' ? Decimal.ROUND_HALF_UP : Decimal.ROUND_CEIL, ), cny = rounded.div(factor), rub = cny.mul(exchange); if ( rounded.lte(0) || !inRange( c.currency_code === 'RUB' ? rub : cny, c.min_price, c.max_price, c.min_inclusive, c.max_inclusive, ) || !inRange( r.merchandise_currency_code === 'RUB' ? rub : cny, r.min_merchandise_value, r.max_merchandise_value, r.min_value_inclusive, r.max_value_inclusive, ) ) { reasons.add('售价不在该物流货值区间或佣金档内'); continue; } const profit = cny .mul(D(1).sub(totalRate)) .sub(purchase) .sub(logistics) .sub(domestic), list = rounded .div(D(1).sub(p.list_price_discount_rate)) .toDecimalPlaces(2, Decimal.ROUND_CEIL); if ( [ rounded, list, rub, list.div(factor).mul(exchange), profit.mul(exchange), logistics, ].some((v) => v.abs().gte('100000000000000')) ) { reasons.add('金额超过可保存范围,请调整费率或输入'); continue; } options.push({ id: randomUUID(), can_confirm: profit.gte(0), transport_method_id: transport, transport_name: r.transport_name, product_type_id: r.product_type_id, product_type_name: r.product_type_name, logistics_rate_rule_id: r.id, commission_rule_id: c.id, commission_rate: c.commission_rate, ...(c.id ? {commission_details: { category_name: c.category_name ?? '',currency_code:c.currency_code,version:c.version,source:c.source ?? 'manual_rule', min_price:c.min_price,max_price:c.max_price,min_inclusive:c.min_inclusive,max_inclusive:c.max_inclusive,commission_rate:c.commission_rate, }} : {}), selling_price: rounded.toFixed(2), list_price: list.toFixed(2), currency_code: currency, selling_price_rub: rub.toFixed(4), list_price_rub: list.div(factor).mul(exchange).toFixed(4), merchandise_value_cny: cny.toFixed(4), profit_cny: profit.toFixed(2), profit_rub: profit.mul(exchange).toFixed(4), margin_rate: profit.div(cny).toFixed(6), billable_weight_kg: billable.toFixed(3), fees: { logistics_cny: logistics.toFixed(4), domestic_cny: domestic.toFixed(4), commission_cny: cny.mul(c.commission_rate).toFixed(4), ad_cny: cny.mul(p.ad_rate).toFixed(4), last_mile_cny: cny.mul(p.last_mile_rate).toFixed(4), withdrawal_cny: cny.mul(p.withdrawal_rate).toFixed(4), other_cny: cny.mul(p.other_rate).toFixed(4), }, warnings: [ ...(profit.lt(0) ? ['亏损报价不能确认'] : []), ...(profit.lt(purchase.mul(p.desired_markup_rate)) ? ['未达到目标加价利润'] : []), ...(!c.id ? ['使用临时佣金率'] : []), ], }); } } if (!options.some((o) => o.transport_method_id === transport)) unavailable_options.push({ transport_method_id: transport, reason: [...reasons].join(';') || '没有可用规则', }); } options.sort((a, b) => D(a.selling_price).cmp(b.selling_price)); const recommended = options.find( (o) => o.transport_method_id === p.default_transport_method_id, ) ?? (!p.default_transport_method_id ? options[0] : undefined); return { options, unavailable_options, recommended_option_id: recommended?.id ?? null, }; }