114 lines
7.1 KiB
JavaScript
114 lines
7.1 KiB
JavaScript
import assert from 'node:assert/strict'
|
||
import test from 'node:test'
|
||
import { normalizeRows, channelName, matchRoute, measurementShape, buildSourceDraft, buildPayload,
|
||
requireSuccess, executeCandidates, totalPreservingShapes } from './real-order-xlsx-test-env.mjs'
|
||
|
||
const header = ['进仓编号', '客户', '渠道', '亚马逊仓', '实际件数', '实际毛重', '实际立方', '预报件数', '买单/退税', '品名', 'SKU', 'FBA货件编号', '跟踪编号']
|
||
const row = ['ABCD2606010001', 'Example', '合德海卡包税', 'ABC1', 31, 100, 2, 31, '买单', 'Goods', 'SKU1', 'SHIP1', 'REF1']
|
||
const order = () => normalizeRows([header, row]).orders[0]
|
||
const catalog = { receiving: { id: '1', warehouseName: 'Origin' },
|
||
channels: [{ channelName: '合德海卡', channelCode: 'CHANNEL', pricingTaxModes: ['TAX_INCLUDED'] }],
|
||
destinations: [{ id: '2', warehouseType: 'FBA', warehouseCode: 'ABC1', warehouseName: 'ABC1', countryCode: 'US' }] }
|
||
|
||
test('source counts remain actual, including after forecast differs', () => {
|
||
const modified = [...row]; modified[7] = 99
|
||
assert.equal(normalizeRows([header, modified]).orders[0].packageCount, 31)
|
||
})
|
||
test('missing and zero actual data do not fall back to forecast', () => {
|
||
for (const missing of [null, '', 0]) {
|
||
const modified = [...row]; modified[4] = missing
|
||
assert.ok(normalizeRows([header, modified]).orders[0].problems.includes('MISSING_实际件数'))
|
||
}
|
||
})
|
||
test('invalid source codes are excluded, not converted into orders', () => {
|
||
const modified = [...row]; modified[0] = 'container subtotal'
|
||
const actual = normalizeRows([header, modified])
|
||
assert.equal(actual.orders.length, 0); assert.equal(actual.excluded.length, 1)
|
||
})
|
||
test('explicit declaration changes use the stated final mode', () => {
|
||
for (const [source, target] of [['买单改退税', 'TAX_REFUND'], ['退税改买单', 'BUY_DECLARATION']]) {
|
||
const modified = [...row]; modified[8] = source
|
||
const result = normalizeRows([header, modified]).orders[0]
|
||
assert.equal(result.taxMode, target); assert.deepEqual(result.problems, [])
|
||
}
|
||
})
|
||
test('split rows aggregate actual counts once and reconcile forecast', () => {
|
||
const a = [...row], b = [...row]; a[4] = 15; b[4] = 16
|
||
const result = normalizeRows([header, a, b]).orders[0]
|
||
assert.equal(result.packageCount, 31); assert.deepEqual(result.rows, [2, 3]); assert.deepEqual(result.problems, [])
|
||
b[4] = 15
|
||
assert.ok(normalizeRows([header, a, b]).orders[0].problems.includes('SPLIT_COUNT_MISMATCH'))
|
||
})
|
||
test('conflicting channels on same inbound code block import', () => {
|
||
const b = [...row]; b[2] = '其他包税'
|
||
assert.ok(normalizeRows([header, row, b]).orders[0].problems.includes('CONFLICT_渠道'))
|
||
})
|
||
test('channel suffix normalization never substitutes another base channel', () => {
|
||
assert.equal(channelName('合德海派包税\n(FedEx)'), '合德海派')
|
||
assert.equal(channelName('合德海卡不包税'), '合德海卡')
|
||
assert.throws(() => matchRoute({ ...order(), channelName: 'Unknown' }, catalog))
|
||
})
|
||
test('excluded-tax estimation uses source billing unit and never guesses missing units', () => {
|
||
const modified = [...row]; modified[2] = '合德海卡不包税'
|
||
const columns = [...header, '单位']
|
||
const result = normalizeRows([columns, [...modified, 'CBM']]).orders[0]
|
||
assert.equal(result.chargeMode, 'CBM'); assert.deepEqual(result.problems, [])
|
||
assert.ok(normalizeRows([columns, [...modified, '']]).orders[0].problems.includes('UNKNOWN_OR_CONFLICTING_CHARGE_MODE'))
|
||
})
|
||
test('ambiguous destinations and unavailable tax modes fail closed', () => {
|
||
assert.throws(() => matchRoute(order(), { ...catalog, destinations: [...catalog.destinations, ...catalog.destinations] }))
|
||
assert.throws(() => matchRoute({ ...order(), pricingTaxMode: 'TAX_EXCLUDED' }, catalog))
|
||
})
|
||
test('private addresses and dropship are explicit gaps, not random replacements', () => {
|
||
assert.throws(() => matchRoute({ ...order(), destinationType: 'PRIVATE' }, catalog), /PRIVATE_ADDRESS/)
|
||
assert.throws(() => matchRoute({ ...order(), fulfillmentType: 'ONE_CLICK_DROPSHIP' }, catalog), /WORK_PALLET/)
|
||
})
|
||
test('logical pallet count uses ceiling of boxes divided by 30', () => {
|
||
for (const [boxes, expected] of [[1, 1], [30, 1], [31, 2], [300, 10], [1000, 34]]) {
|
||
assert.equal(measurementShape({ ...order(), packageCount: boxes }).palletCount, expected)
|
||
}
|
||
})
|
||
test('inferred groups conserve volume after ADD and server per-carton rounding', () => {
|
||
const rules = [[30, 20, 20], [40, 30, 30], [50, 40, 40], [60, 50, 50]].map((dims, index) =>
|
||
({ boxType: String(index), innerLengthCm: dims[0], innerWidthCm: dims[1], innerHeightCm: dims[2], edgeAdditionCm: 2 }))
|
||
for (const [packageCount, volumeCbm] of [[8, 0.403], [17, 1.545], [7, 0.626], [31, 2], [300, 23.457], [1, 0.1], [1000, 98.7654]]) {
|
||
const plan = totalPreservingShapes({ ...order(), packageCount, volumeCbm }, rules)
|
||
assert.ok(plan.length <= 2)
|
||
assert.equal(plan.reduce((n, g) => n + g.count, 0), packageCount)
|
||
const ticks = plan.reduce((n, g) => {
|
||
const raw = Number(((g.length + 2 * g.edgeAdditionCm) ** 3 / 1e6).toFixed(6))
|
||
return n + Math.ceil(raw * 10000 - 1e-8) * g.count
|
||
}, 0)
|
||
assert.equal(ticks, Math.round(volumeCbm * 10000))
|
||
if (Number.isInteger(Math.round(volumeCbm * 1e6) / 1000)) {
|
||
const detailWeight = plan.reduce((sum, group) => sum + Math.ceil(group.unitVolumeCbm * group.count * 167 * 1000 - 1e-8), 0)
|
||
assert.equal(detailWeight, Math.ceil(volumeCbm * 167 * 1000 - 1e-8))
|
||
}
|
||
}
|
||
assert.throws(() => totalPreservingShapes({ ...order(), volumeCbm: 0.123456 }, rules), /PRECISION/)
|
||
})
|
||
test('source-only draft does not invent measurement or product evidence', () => {
|
||
const payload = buildSourceDraft(order(), matchRoute(order(), catalog), 'Example', 'MARKER')
|
||
assert.equal(payload.cargoes[0].packageCount, 31)
|
||
assert.equal(payload.cargoes[0].grossWeightKg, 100)
|
||
assert.equal(payload.cargoes[0].volumeCbm, 2)
|
||
assert.equal(payload.cargoes[0].woodenProduct, false)
|
||
for (const key of ['lengthCm', 'productImageFileId', 'hsCode', 'dangerousGoods']) assert.equal(key in payload.cargoes[0], false)
|
||
})
|
||
test('simulated payload is labeled, preserving source measures without randomization', () => {
|
||
const payload = buildPayload(order(), matchRoute(order(), catalog), 'Example', 'image', 'MARKER')
|
||
assert.match(payload.customerRemark, /非真实测量证据/)
|
||
assert.equal(payload.cargoes[0].volumeCbm, 2)
|
||
assert.match(payload.cargoes[0].cargoRemark, /禁止用于真实报关/)
|
||
})
|
||
test('HTTP business errors preserve diagnostic evidence', () => {
|
||
assert.throws(() => requireSuccess({ http: 200, path: '/example', body: { code: 'A0409' }, requestId: 'req' }),
|
||
(error) => error.evidence.code === 'A0409' && error.evidence.requestId === 'req')
|
||
})
|
||
test('same batch never recreates an attempted order, even when interrupted', async () => {
|
||
const value = order(), state = { identity: { batchId: 'test' }, orders: { [value.key]: { status: 'STARTED' } } }
|
||
await executeCandidates({ candidates: [{ order: value, route: catalog }], limit: 1, state, save: async () => {},
|
||
api: async () => { throw Error('must not call HTTP') } })
|
||
assert.equal(state.lastRun.attempted, 0); assert.equal(state.lastRun.skipped, 1)
|
||
})
|