2026-07-30 17:59:42 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import test from 'node:test'
|
2026-07-31 15:34:22 +08:00
|
|
|
import { createFirstLegPartySnapshot } from '../data/firstLegParties.ts'
|
2026-07-30 17:59:42 +08:00
|
|
|
import {
|
2026-08-07 18:15:59 +08:00
|
|
|
applyCargoBoxSize,
|
2026-07-30 17:59:42 +08:00
|
|
|
canClientCancelFirstLegOrder,
|
|
|
|
|
canClientEditFirstLegOrder,
|
|
|
|
|
createEmptyCargoItem,
|
|
|
|
|
getFirstLegStatusLabel,
|
|
|
|
|
getFirstLegTransportModeLabel,
|
|
|
|
|
summarizeFirstLegCargo,
|
2026-08-07 18:15:59 +08:00
|
|
|
updateCargoBoxVolume,
|
2026-07-30 17:59:42 +08:00
|
|
|
validateFirstLegOrderDraft,
|
|
|
|
|
} from './firstLegOrder.ts'
|
|
|
|
|
|
2026-07-31 15:34:22 +08:00
|
|
|
const shipperParty = {
|
|
|
|
|
id: 'CP001', code: 'SHIPPER-001', name: '测试发货企业', roles: ['SHIPPER'], contactName: '测试联系人',
|
|
|
|
|
phone: '13800000000', email: 'shipper@example.test', countryRegion: '中国', province: '浙江省', city: '宁波市',
|
|
|
|
|
postalCode: '315000', address: '测试发货地址', status: 'ACTIVE',
|
|
|
|
|
}
|
|
|
|
|
const consigneeParty = {
|
|
|
|
|
id: 'CP002', code: 'CONSIGNEE-001', name: 'TEST CONSIGNEE LLC', roles: ['CONSIGNEE'], contactName: 'Test Receiver',
|
|
|
|
|
phone: '+1 555 0100', email: 'receiver@example.test', countryRegion: '美国', province: 'California', city: 'Los Angeles',
|
|
|
|
|
postalCode: '90001', address: '100 Test Logistics Ave', status: 'ACTIVE',
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 17:59:42 +08:00
|
|
|
function validDraft() {
|
|
|
|
|
return {
|
|
|
|
|
referenceNo: 'REF-001',
|
|
|
|
|
transportMode: 'FCL',
|
|
|
|
|
businessType: 'EXPORT',
|
|
|
|
|
origin: '宁波',
|
|
|
|
|
destination: '洛杉矶',
|
|
|
|
|
receivingWarehouseId: 'WH005',
|
|
|
|
|
expectedDepartureDate: '2026-08-08',
|
|
|
|
|
shipperPartyId: 'CP001',
|
2026-07-31 15:34:22 +08:00
|
|
|
shipperPartySnapshot: createFirstLegPartySnapshot(shipperParty, 'SHIPPER'),
|
|
|
|
|
shipper: shipperParty.name,
|
2026-07-30 17:59:42 +08:00
|
|
|
consigneePartyId: 'CP003',
|
2026-07-31 15:34:22 +08:00
|
|
|
consigneePartySnapshot: createFirstLegPartySnapshot(consigneeParty, 'CONSIGNEE'),
|
|
|
|
|
consignee: consigneeParty.name,
|
2026-07-30 17:59:42 +08:00
|
|
|
priority: 'NORMAL',
|
|
|
|
|
customerRemark: '',
|
|
|
|
|
tradeTerm: 'FOB',
|
|
|
|
|
destinationCountryCode: 'US',
|
2026-08-04 11:14:29 +08:00
|
|
|
destinationWarehouseId: 'WH-FBA-ONT8',
|
2026-07-30 17:59:42 +08:00
|
|
|
destinationWarehouseName: 'ONT8',
|
|
|
|
|
pickupMethod: 'SELF_EXPRESS',
|
|
|
|
|
channelCode: 'SEA-US-FBA',
|
|
|
|
|
channelName: '美国海运渠道',
|
2026-08-07 14:47:00 +08:00
|
|
|
pricingTaxMode: 'TAX_INCLUDED',
|
2026-07-30 17:59:42 +08:00
|
|
|
taxMode: 'BUY_DECLARATION',
|
|
|
|
|
customsInspection: false,
|
|
|
|
|
destinationType: 'FBA',
|
|
|
|
|
amazonWarehouseCode: 'ONT8',
|
|
|
|
|
shipmentId: 'FBA-001',
|
2026-08-07 18:15:59 +08:00
|
|
|
items: [{ ...createEmptyCargoItem('cargo-1'), sku: 'SKU-001', upc: '012345678905', cargoNameCn: '家居用品', pieces: 100, grossWeightKg: 1200, volumeCbm: 3.5, boxSizeType: 'SMALL', lengthCm: 34, widthCm: 24, heightCm: 24, containerType: '40HQ', containerCount: 1 }],
|
2026-07-30 17:59:42 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 18:15:59 +08:00
|
|
|
test('applies the four fixed box sizes and recalculates total CBM from package count', () => {
|
|
|
|
|
const item = createEmptyCargoItem('cargo-box')
|
|
|
|
|
item.packageCount = 2
|
|
|
|
|
applyCargoBoxSize(item, 'SMALL')
|
|
|
|
|
assert.deepEqual([item.lengthCm, item.widthCm, item.heightCm, item.volumeCbm], [34, 24, 24, 0.039])
|
|
|
|
|
|
|
|
|
|
item.packageCount = 3
|
|
|
|
|
updateCargoBoxVolume(item)
|
|
|
|
|
assert.equal(item.volumeCbm, 0.059)
|
|
|
|
|
|
|
|
|
|
applyCargoBoxSize(item, '')
|
|
|
|
|
assert.deepEqual([item.lengthCm, item.widthCm, item.heightCm, item.volumeCbm], [0, 0, 0, 0])
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-30 17:59:42 +08:00
|
|
|
test('uses the confirmed status and transport labels', () => {
|
2026-08-06 13:56:13 +08:00
|
|
|
assert.equal(getFirstLegStatusLabel('WAIT_MEASURE'), '待测量数据')
|
|
|
|
|
assert.equal(getFirstLegStatusLabel('INBOUNDED'), '已入库')
|
|
|
|
|
assert.equal(getFirstLegStatusLabel('WAIT_SPLIT'), '待分柜')
|
|
|
|
|
assert.equal(getFirstLegStatusLabel('LOADING'), '装柜中')
|
|
|
|
|
assert.equal(getFirstLegStatusLabel('LOADED'), '装柜完成')
|
|
|
|
|
assert.equal(getFirstLegStatusLabel('DELIVERED'), '派送完成')
|
2026-07-30 17:59:42 +08:00
|
|
|
assert.equal(getFirstLegTransportModeLabel('FCL'), '海运整箱 FCL')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('only allows draft orders to be edited in the confirmed phase-one flow', () => {
|
|
|
|
|
assert.equal(canClientEditFirstLegOrder('DRAFT'), true)
|
|
|
|
|
assert.equal(canClientEditFirstLegOrder('PENDING_REVIEW'), false)
|
|
|
|
|
assert.equal(canClientCancelFirstLegOrder('DRAFT'), true)
|
|
|
|
|
assert.equal(canClientCancelFirstLegOrder('PENDING_REVIEW'), true)
|
2026-08-06 13:56:13 +08:00
|
|
|
assert.equal(canClientCancelFirstLegOrder('WAIT_MEASURE'), false)
|
|
|
|
|
assert.equal(canClientCancelFirstLegOrder('WAIT_SPLIT'), false)
|
2026-07-30 17:59:42 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('validates the confirmed minimum submission fields', () => {
|
|
|
|
|
assert.deepEqual(validateFirstLegOrderDraft(validDraft()), [])
|
|
|
|
|
|
|
|
|
|
const invalid = validDraft()
|
|
|
|
|
invalid.receivingWarehouseId = ''
|
|
|
|
|
invalid.items[0].grossWeightKg = 0
|
|
|
|
|
invalid.items[0].containerType = ''
|
|
|
|
|
invalid.items[0].containerCount = 0
|
|
|
|
|
|
|
|
|
|
const errors = validateFirstLegOrderDraft(invalid)
|
|
|
|
|
assert.ok(errors.includes('请选择平台收货仓'))
|
|
|
|
|
assert.ok(errors.includes('第 1 条货物毛重必须大于 0'))
|
|
|
|
|
assert.ok(errors.includes('第 1 条货物需要填写箱型和柜量'))
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-04 12:01:05 +08:00
|
|
|
test('only requires sender for pickup and receiver for a private destination', () => {
|
|
|
|
|
const publicDraft = validDraft()
|
|
|
|
|
publicDraft.shipperPartySnapshot = null
|
|
|
|
|
publicDraft.consigneePartySnapshot = null
|
|
|
|
|
assert.deepEqual(validateFirstLegOrderDraft(publicDraft), [])
|
2026-07-30 17:59:42 +08:00
|
|
|
|
2026-08-04 12:01:05 +08:00
|
|
|
const pickupDraft = validDraft()
|
|
|
|
|
pickupDraft.pickupMethod = 'PICKUP'
|
|
|
|
|
pickupDraft.shipperPartySnapshot = null
|
|
|
|
|
assert.ok(validateFirstLegOrderDraft(pickupDraft).includes('请填写提货联系人'))
|
|
|
|
|
|
|
|
|
|
const privateDraft = validDraft()
|
|
|
|
|
privateDraft.destinationType = 'PRIVATE'
|
|
|
|
|
privateDraft.destinationWarehouseId = ''
|
|
|
|
|
privateDraft.amazonWarehouseCode = ''
|
|
|
|
|
privateDraft.shipmentId = ''
|
|
|
|
|
privateDraft.referenceNo = ''
|
|
|
|
|
privateDraft.consigneePartySnapshot = null
|
|
|
|
|
assert.ok(validateFirstLegOrderDraft(privateDraft).includes('请填写收件人姓名'))
|
2026-07-30 17:59:42 +08:00
|
|
|
})
|
|
|
|
|
|
2026-08-01 12:17:33 +08:00
|
|
|
test('requires FBA reference, warehouse, shipment, SKU and UPC fields', () => {
|
|
|
|
|
const draft = validDraft()
|
|
|
|
|
draft.referenceNo = ''
|
|
|
|
|
draft.amazonWarehouseCode = ''
|
|
|
|
|
draft.shipmentId = ''
|
|
|
|
|
draft.items[0].sku = ''
|
|
|
|
|
draft.items[0].upc = ''
|
|
|
|
|
|
|
|
|
|
const errors = validateFirstLegOrderDraft(draft)
|
|
|
|
|
assert.ok(errors.includes('FBA 订单请填写客户参考号'))
|
|
|
|
|
assert.ok(errors.includes('FBA 订单请填写 Amazon 仓库编码'))
|
|
|
|
|
assert.ok(errors.includes('FBA 订单请填写 Shipment ID'))
|
|
|
|
|
assert.ok(errors.includes('第 1 条货物缺少客户 SKU'))
|
|
|
|
|
assert.ok(errors.includes('第 1 条货物缺少 UPC'))
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-04 11:14:29 +08:00
|
|
|
test('requires public destination ids while keeping private addresses detached from public warehouses', () => {
|
|
|
|
|
const publicDraft = validDraft()
|
|
|
|
|
publicDraft.destinationWarehouseId = ''
|
|
|
|
|
assert.ok(validateFirstLegOrderDraft(publicDraft).includes('请选择平台维护的目的仓'))
|
|
|
|
|
|
|
|
|
|
const privateDraft = validDraft()
|
|
|
|
|
privateDraft.destinationType = 'PRIVATE'
|
|
|
|
|
privateDraft.destinationWarehouseId = 'WH-FBA-ONT8'
|
|
|
|
|
privateDraft.amazonWarehouseCode = ''
|
|
|
|
|
privateDraft.shipmentId = ''
|
|
|
|
|
privateDraft.referenceNo = ''
|
|
|
|
|
assert.ok(validateFirstLegOrderDraft(privateDraft).includes('私人地址不能引用平台公共仓库'))
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-30 17:59:42 +08:00
|
|
|
test('does not require container details for non-FCL cargo', () => {
|
|
|
|
|
const draft = validDraft()
|
|
|
|
|
draft.transportMode = 'AIR'
|
|
|
|
|
draft.items[0].containerType = ''
|
|
|
|
|
draft.items[0].containerCount = 0
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(validateFirstLegOrderDraft(draft), [])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('summarizes cargo values from line items', () => {
|
|
|
|
|
const first = validDraft().items[0]
|
|
|
|
|
const second = { ...first, id: 'cargo-2', pieces: 40, packageCount: 4, grossWeightKg: 300, volumeCbm: 1.25 }
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(summarizeFirstLegCargo([first, second]), {
|
|
|
|
|
pieces: 140,
|
|
|
|
|
packageCount: 5,
|
|
|
|
|
grossWeightKg: 1500,
|
|
|
|
|
volumeCbm: 4.75,
|
|
|
|
|
})
|
|
|
|
|
})
|