优化构架
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { createFirstLegPartySnapshot } from '../data/firstLegParties.ts'
|
||||
import {
|
||||
canClientCancelFirstLegOrder,
|
||||
canClientEditFirstLegOrder,
|
||||
createEmptyCargoItem,
|
||||
getFirstLegStatusLabel,
|
||||
getFirstLegTransportModeLabel,
|
||||
summarizeFirstLegCargo,
|
||||
validateFirstLegOrderDraft,
|
||||
} from './firstLegOrder.ts'
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
function validDraft() {
|
||||
return {
|
||||
referenceNo: 'REF-001',
|
||||
transportMode: 'FCL',
|
||||
businessType: 'EXPORT',
|
||||
origin: '宁波',
|
||||
destination: '洛杉矶',
|
||||
receivingWarehouseId: 'WH005',
|
||||
expectedDepartureDate: '2026-08-08',
|
||||
shipperPartyId: 'CP001',
|
||||
shipperPartySnapshot: createFirstLegPartySnapshot(shipperParty, 'SHIPPER'),
|
||||
shipper: shipperParty.name,
|
||||
consigneePartyId: 'CP003',
|
||||
consigneePartySnapshot: createFirstLegPartySnapshot(consigneeParty, 'CONSIGNEE'),
|
||||
consignee: consigneeParty.name,
|
||||
priority: 'NORMAL',
|
||||
customerRemark: '',
|
||||
tradeTerm: 'FOB',
|
||||
destinationCountryCode: 'US',
|
||||
destinationWarehouseName: 'ONT8',
|
||||
pickupMethod: 'SELF_EXPRESS',
|
||||
channelCode: 'SEA-US-FBA',
|
||||
channelName: '美国海运渠道',
|
||||
taxMode: 'BUY_DECLARATION',
|
||||
customsInspection: false,
|
||||
destinationType: 'FBA',
|
||||
amazonWarehouseCode: 'ONT8',
|
||||
shipmentId: 'FBA-001',
|
||||
items: [{ ...createEmptyCargoItem('cargo-1'), sku: 'SKU-001', upc: '012345678905', cargoNameCn: '家居用品', pieces: 100, grossWeightKg: 1200, volumeCbm: 3.5, containerType: '40HQ', containerCount: 1 }],
|
||||
}
|
||||
}
|
||||
|
||||
test('uses the confirmed status and transport labels', () => {
|
||||
assert.equal(getFirstLegStatusLabel('BOOKED'), '已订舱')
|
||||
assert.equal(getFirstLegStatusLabel('IN_TRANSIT'), '运输中')
|
||||
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)
|
||||
assert.equal(canClientCancelFirstLegOrder('ACCEPTED'), false)
|
||||
assert.equal(canClientCancelFirstLegOrder('BOOKED'), false)
|
||||
})
|
||||
|
||||
test('validates the confirmed minimum submission fields', () => {
|
||||
assert.deepEqual(validateFirstLegOrderDraft(validDraft()), [])
|
||||
|
||||
const invalid = validDraft()
|
||||
invalid.origin = ''
|
||||
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('请选择平台收货仓'))
|
||||
assert.ok(errors.includes('第 1 条货物毛重必须大于 0'))
|
||||
assert.ok(errors.includes('第 1 条货物需要填写箱型和柜量'))
|
||||
})
|
||||
|
||||
test('requires enabled-party snapshots and order addresses before submission', () => {
|
||||
const draft = validDraft()
|
||||
draft.shipperPartyId = ''
|
||||
draft.shipperPartySnapshot = null
|
||||
draft.consigneePartySnapshot.address = ''
|
||||
|
||||
const errors = validateFirstLegOrderDraft(draft)
|
||||
assert.ok(errors.includes('请选择发货单位'))
|
||||
assert.ok(errors.includes('请填写本单收货地址'))
|
||||
})
|
||||
|
||||
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'))
|
||||
})
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
FIRST_LEG_ORDER_STATUSES,
|
||||
FIRST_LEG_TRANSPORT_MODES,
|
||||
type FirstLegOrderStatus,
|
||||
type FirstLegTransportMode,
|
||||
} from '@chongchong/shared'
|
||||
import type { FirstLegCargoItem, FirstLegOrderDraft } from '../types/firstLegOrder.ts'
|
||||
|
||||
const statusLabels = new Map<FirstLegOrderStatus, string>(
|
||||
FIRST_LEG_ORDER_STATUSES.map((item) => [item.code, item.label]),
|
||||
)
|
||||
|
||||
const transportModeLabels = new Map<FirstLegTransportMode, string>(
|
||||
FIRST_LEG_TRANSPORT_MODES.map((item) => [item.code, item.label]),
|
||||
)
|
||||
|
||||
export function getFirstLegStatusLabel(status: FirstLegOrderStatus) {
|
||||
return statusLabels.get(status) ?? status
|
||||
}
|
||||
|
||||
export function getFirstLegTransportModeLabel(mode: FirstLegTransportMode) {
|
||||
return transportModeLabels.get(mode) ?? mode
|
||||
}
|
||||
|
||||
export function getFirstLegStatusTone(status: FirstLegOrderStatus): 'success' | 'warning' | 'info' | 'danger' {
|
||||
if (status === 'COMPLETED') return 'success'
|
||||
if (status === 'DRAFT' || status === 'PENDING_REVIEW') return 'warning'
|
||||
if (status === 'CANCELLED') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
export function canClientEditFirstLegOrder(status: FirstLegOrderStatus) {
|
||||
return status === 'DRAFT'
|
||||
}
|
||||
|
||||
export function canClientCancelFirstLegOrder(status: FirstLegOrderStatus) {
|
||||
return status === 'DRAFT' || status === 'PENDING_REVIEW'
|
||||
}
|
||||
|
||||
export function createEmptyCargoItem(id = `cargo-${Date.now()}`): FirstLegCargoItem {
|
||||
return {
|
||||
id,
|
||||
sku: '',
|
||||
upc: '',
|
||||
cargoNameCn: '',
|
||||
cargoNameEn: '',
|
||||
pieces: 1,
|
||||
packageCount: 1,
|
||||
grossWeightKg: 0,
|
||||
volumeCbm: 0,
|
||||
containerType: '',
|
||||
containerCount: 0,
|
||||
remark: '',
|
||||
packageType: 'CARTON',
|
||||
hsCode: '',
|
||||
declaredValue: 0,
|
||||
currency: 'USD',
|
||||
dangerousGoods: false,
|
||||
}
|
||||
}
|
||||
|
||||
export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!draft.origin.trim()) errors.push('请填写起运地')
|
||||
if (!draft.destination.trim()) errors.push('请填写目的地')
|
||||
if (!/^[A-Za-z]{2}$/.test(draft.destinationCountryCode.trim())) errors.push('请输入两位英文字母的目的国代码')
|
||||
if (!draft.destinationWarehouseName.trim()) errors.push('请填写目的仓名称')
|
||||
if (!draft.channelCode.trim() || !draft.channelName.trim()) errors.push('请填写渠道编码和渠道名称')
|
||||
if (!draft.receivingWarehouseId) errors.push('请选择平台收货仓')
|
||||
if (!draft.shipperPartyId || !draft.shipperPartySnapshot || !draft.shipper.trim()) errors.push('请选择发货单位')
|
||||
if (draft.shipperPartySnapshot && !draft.shipperPartySnapshot.contactName.trim()) errors.push('请填写发货联系人')
|
||||
if (draft.shipperPartySnapshot && !draft.shipperPartySnapshot.phone.trim()) errors.push('请填写发货联系电话')
|
||||
if (draft.shipperPartySnapshot && !draft.shipperPartySnapshot.address.trim()) errors.push('请填写本单发货地址')
|
||||
if (!draft.consigneePartyId || !draft.consigneePartySnapshot || !draft.consignee.trim()) errors.push('请选择收货单位')
|
||||
if (draft.consigneePartySnapshot && !draft.consigneePartySnapshot.contactName.trim()) errors.push('请填写收货联系人')
|
||||
if (draft.consigneePartySnapshot && !draft.consigneePartySnapshot.phone.trim()) errors.push('请填写收货联系电话')
|
||||
if (draft.consigneePartySnapshot && !draft.consigneePartySnapshot.address.trim()) errors.push('请填写本单收货地址')
|
||||
if (draft.items.length === 0) errors.push('请至少添加一条货物明细')
|
||||
|
||||
if (draft.destinationType === 'FBA') {
|
||||
if (!draft.referenceNo.trim()) errors.push('FBA 订单请填写客户参考号')
|
||||
if (!draft.amazonWarehouseCode.trim()) errors.push('FBA 订单请填写 Amazon 仓库编码')
|
||||
if (!draft.shipmentId.trim()) errors.push('FBA 订单请填写 Shipment ID')
|
||||
}
|
||||
|
||||
draft.items.forEach((item, index) => {
|
||||
const line = `第 ${index + 1} 条货物`
|
||||
if (!item.cargoNameCn.trim()) errors.push(`${line}缺少中文品名`)
|
||||
if (!item.packageType.trim()) errors.push(`${line}缺少包装类型`)
|
||||
if (draft.destinationType === 'FBA' && !item.sku.trim()) errors.push(`${line}缺少客户 SKU`)
|
||||
if (draft.destinationType === 'FBA' && !item.upc.trim()) errors.push(`${line}缺少 UPC`)
|
||||
if (!Number.isInteger(item.pieces) || item.pieces <= 0) errors.push(`${line}件数必须为正整数`)
|
||||
if (item.grossWeightKg <= 0) errors.push(`${line}毛重必须大于 0`)
|
||||
if (item.volumeCbm <= 0) errors.push(`${line}体积必须大于 0`)
|
||||
if (draft.transportMode === 'FCL' && (!item.containerType || item.containerCount <= 0)) {
|
||||
errors.push(`${line}需要填写箱型和柜量`)
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
export function summarizeFirstLegCargo(items: FirstLegCargoItem[]) {
|
||||
return items.reduce(
|
||||
(summary, item) => ({
|
||||
pieces: summary.pieces + item.pieces,
|
||||
packageCount: summary.packageCount + item.packageCount,
|
||||
grossWeightKg: summary.grossWeightKg + item.grossWeightKg,
|
||||
volumeCbm: summary.volumeCbm + item.volumeCbm,
|
||||
}),
|
||||
{ pieces: 0, packageCount: 0, grossWeightKg: 0, volumeCbm: 0 },
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user