Files
client-web/src/modules/first-leg/store/firstLegOrders.test.mjs
T

282 lines
18 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { runInNewContext } from 'node:vm'
import ts from 'typescript'
const storeSource = readFileSync(new URL('./firstLegOrders.ts', import.meta.url), 'utf8')
const apiSource = readFileSync(new URL('../api/firstLeg.ts', import.meta.url), 'utf8')
const unifiedApiSource = readFileSync(new URL('../api/unifiedOrders.ts', import.meta.url), 'utf8')
test('saves optional express tracking, trims it, and omits it for other pickup methods', () => {
const functions = storeSource.slice(storeSource.indexOf('function apiTransport('), storeSource.indexOf('function prepareDraftForSave('))
const javascript = ts.transpileModule(functions, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
const save = runInNewContext(`${javascript}; savePayload`)
const draft = { pickupMethod: 'SELF_EXPRESS', trackingNumber: ' SF123456789 ', destinationCountryCode: 'US', destinationType: 'OVERSEAS', items: [] }
const warehouse = { id: '1', name: '收货仓' }
assert.equal(save(draft, warehouse, '客户').expressTrackingNo, 'SF123456789')
for (const trackingNumber of ['', ' ']) {
assert.equal(save({ ...draft, trackingNumber }, warehouse, '客户').expressTrackingNo, undefined)
}
for (const pickupMethod of ['SELF_TRUCK', 'PICKUP']) {
assert.equal(save({ ...draft, pickupMethod }, warehouse, '客户').expressTrackingNo, undefined)
}
})
test('detail fetches authoritative progress on direct entry and refresh, and clears stale progress on failure', async () => {
const functionSource = storeSource.slice(storeSource.indexOf(' async function loadDetail('), storeSource.indexOf(' async function loadWarehouseDetail('))
const progressSource = readFileSync(new URL('../utils/orderProgress.ts', import.meta.url), 'utf8')
const validationSource = progressSource.slice(progressSource.indexOf('export function requireOrderProgress(')).replace('export ', '')
const javascript = ts.transpileModule(`${validationSource}\n${functionSource}`, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
const id = '2110663764278312961'
const orderProgress = { value: {} }
let calls = 0
let response = { progress: { orderId: id, stage: 'scheduled', partial: true } }
const loadDetail = runInNewContext(`${javascript}; loadDetail`, {
orderProgress, warehouses: { value: [] },
getMerchantUnifiedOrder: async requested => { assert.equal(requested, id); calls++; return response },
mapUnifiedOrderDetail: detail => ({ id, detail }), upsertOrder: order => order,
})
await loadDetail(id)
assert.equal(orderProgress.value[id].stage, 'scheduled')
assert.equal(orderProgress.value[id].partial, true)
response = { progress: { orderId: id, stage: 'delivering', partial: false } }
await loadDetail(id)
assert.equal(calls, 2)
assert.equal(orderProgress.value[id].stage, 'delivering')
response = { progress: { orderId: 'another-order', stage: 'delivered', partial: false } }
await assert.rejects(loadDetail(id), /订单履约进度不完整/)
assert.equal(orderProgress.value[id], undefined)
})
test('renders estimated sailing from the current API without substituting actual departure', () => {
const mapper = storeSource.slice(storeSource.indexOf('function businessSummary('), storeSource.indexOf('\nconst unifiedActionLabels'))
const javascript = ts.transpileModule(mapper, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
const map = runInNewContext(`${javascript}; businessSummary`)
const transport = {
vesselName: 'REASSIGNED', voyageNo: 'V02',
estimatedSailingAt: '2026-10-07T08:00:00', actualSailingAt: '2026-10-08T09:00:00',
effectiveSailingAt: '2026-10-08T09:00:00', effectiveSailingAtEstimated: false,
estimatedPortArrivalAt: '2026-10-27T08:00:00', actualPortArrivalAt: null,
}
const mapped = map({ transport })
assert.equal(mapped.estimatedSailingAt, transport.estimatedSailingAt)
assert.equal(mapped.estimatedPortArrivalAt, transport.estimatedPortArrivalAt)
assert.equal(mapped.vesselName, 'REASSIGNED')
assert.equal(mapped.voyageNo, 'V02')
assert.equal(map().estimatedSailingAt, '')
assert.equal(map({ transport: null }).estimatedPortArrivalAt, '')
})
test('maps the current workbench row without loading detail rows', () => {
assert.match(storeSource, /result\.page\.records\.map\(item => mapUnifiedOrderRow\(item, warehouses\.value\)\)/)
assert.match(storeSource, /version: summary\.orderVersion/)
assert.doesNotMatch(storeSource, /function mapFirstLegOrderRow/)
})
test('declares the documented merchant warehouse and first-leg endpoints', () => {
assert.match(apiSource, /url: '\/merchant\/v1\/warehouses'/)
assert.match(apiSource, /url: '\/merchant\/v1\/orders\/first-leg'/)
assert.match(apiSource, /url: '\/merchant\/v1\/orders\/first-leg\/inbound-code-policy'/)
assert.match(apiSource, /url: '\/merchant\/v1\/orders\/first-leg\/batch'/)
assert.match(apiSource, /url: '\/merchant\/v1\/orders\/first-leg\/addresses'/)
assert.match(apiSource, /`\/merchant\/v1\/orders\/first-leg\/addresses\/\$\{id\}`/)
assert.doesNotMatch(apiSource, /merchant\/v1\/order-addresses/)
assert.match(apiSource, /url: '\/merchant\/v1\/orders\/first-leg\/batch-imports\/template'/)
assert.match(apiSource, /url: '\/merchant\/v1\/orders\/first-leg\/batch-imports'/)
assert.match(apiSource, /`\/merchant\/v1\/files\/order-batch-imports\/\$\{encodeURIComponent\(batchNo\)\}\/error-reports\/\$\{encodeURIComponent\(String\(fileId\)\)\}`/)
assert.match(apiSource, /params: \{ mode \}/)
assert.match(apiSource, /data\.append\('file', file\)/)
assert.match(apiSource, /`\/merchant\/v1\/orders\/first-leg\/\$\{id\}`/)
assert.match(apiSource, /`\/merchant\/v1\/orders\/first-leg\/\$\{id\}\/submit`/)
assert.match(apiSource, /`\/merchant\/v1\/warehouses\/\$\{id\}`/)
assert.match(apiSource, /url: '\/merchant\/v1\/first-leg\/pricing\/channels'/)
assert.match(apiSource, /params: \{ originWarehouseId \}/)
assert.match(apiSource, /url: '\/merchant\/v1\/first-leg\/pricing\/estimate'/)
assert.match(apiSource, /`\/merchant\/v1\/first-leg\/inbounds\/\$\{encodeURIComponent\(String\(inboundId\)\)\}\/carton-label-pdf`/)
assert.match(apiSource, /`\/merchant\/v1\/first-leg\/orders\/\$\{encodeURIComponent\(String\(orderId\)\)\}\/carton-label-pdf`/)
assert.match(apiSource, /url: '\/merchant\/v1\/first-leg\/orders\/carton-label-pdf'/)
assert.match(apiSource, /data: orderIds\.map\(String\)/)
assert.doesNotMatch(apiSource, /`\/merchant\/v1\/inbounds\/\$\{encodeURIComponent\(String\(inboundId\)\)\}\/carton-label-pdf`/)
assert.match(apiSource, /signature === '%PDF-'/)
assert.match(apiSource, /throw new ApiError\('箱唛服务未返回有效的 PDF 文件', 'INVALID_PDF'\)/)
})
test('maps transport and status fields instead of treating container mode as transport mode', () => {
assert.match(storeSource, /order\.transportMode === 'SEA'.*order\.containerMode === 'FULL_CONTAINER' \? 'FCL' : 'LCL'/s)
assert.match(storeSource, /order\.id \?\? order\.orderId \?\? ''/)
assert.match(storeSource, /order\.transportMode \? clientTransport\(order\) : null/)
assert.match(storeSource, /draft\.transportMode === 'ROAD'\) return 'TRUCK'/)
assert.match(storeSource, /status === 'SUBMITTED'\) return 'PENDING_REVIEW'/)
assert.match(storeSource, /status === 'REJECTED'\) return 'PENDING_REVIEW'/)
assert.match(storeSource, /status === 'APPROVED' \|\| status === 'ACCEPTED' \|\| status === 'PROCESSING'\) return 'WAIT_MEASURE'/)
assert.match(storeSource, /status === 'COMPLETED'\) return 'COMPLETED'/)
assert.doesNotMatch(storeSource, /status === 'COMPLETED'\) return 'DELIVERED'/)
})
test('uses only the formal workbench read and surfaces failures', () => {
assert.match(storeSource, /getMerchantOrderWorkbench\(query\)/)
assert.match(storeSource, /orders\.value = \[\]/)
assert.doesNotMatch(storeSource, /isUnifiedOrderQueryUnavailable|getOrdersForClient|getMerchantFirstLegOrders|initialOrders/)
})
test('declares the final C02 row and detail contract in the API adapter', () => {
assert.match(unifiedApiSource, /const unifiedOrderBaseUrl = '\/merchant\/v1\/orders'/)
assert.doesNotMatch(unifiedApiSource, /orders\/unified/)
assert.match(unifiedApiSource, /orderId: number \| string/)
assert.match(unifiedApiSource, /mainStatus: ApiUnifiedOrderMainStatus/)
assert.match(unifiedApiSource, /nextAction: string \| null/)
assert.match(unifiedApiSource, /fulfillmentType\?: 'STANDARD' \| 'ONE_CLICK_DROPSHIP' \| null/)
assert.match(unifiedApiSource, /lxCode\?: string \| null/)
assert.match(unifiedApiSource, /customerRemark: string \| null/)
for (const field of ['pickupMethod', 'requestedPickupAt', 'containerMode', 'destinationType', 'destinationCode', 'skuNos', 'totalQuantity', 'totalNetWeightKg', 'chargeableVolumeCbm', 'inboundCompletedAt', 'shipmentId', 'referenceId']) {
assert.match(unifiedApiSource, new RegExp(`${field}:`))
}
assert.match(unifiedApiSource, /taxMode: ApiTaxMode \| null/)
assert.match(unifiedApiSource, /customsInspection: boolean \| null/)
assert.match(unifiedApiSource, /loadingVersion: number/)
assert.match(unifiedApiSource, /pickingVersion: number/)
assert.match(unifiedApiSource, /summary: MerchantUnifiedOrderRow/)
assert.match(unifiedApiSource, /order: MerchantFirstLegOrderDetail/)
assert.match(unifiedApiSource, /timeline: MerchantUnifiedTimelineEvent\[\]/)
assert.match(unifiedApiSource, /customNodes\?: MerchantUnifiedCustomNode\[\]/)
})
test('keeps unified order pagination valid when the store omits page and size', () => {
assert.match(unifiedApiSource, /const \{ page, size, \.\.\.filters \} = params/)
assert.match(unifiedApiSource, /params: \{ page: page \?\? 1, size: size \?\? 200, \.\.\.filters \}/)
})
test('draft editing reads the order aggregate while navigation uses unified detail', () => {
assert.match(storeSource, /return loadOrderAggregate\(id\)/)
assert.match(storeSource, /const detail = await getMerchantUnifiedOrder\(id\)/)
assert.doesNotMatch(storeSource, /loadLegacyDetail|draftOnly|includeDrafts/)
})
test('maps C02 authoritative status, transport, cargo and pickup snapshots', () => {
assert.match(storeSource, /id: String\(summary\.orderId\)/)
assert.match(storeSource, /transportMode: unifiedTransport\(summary\.containerMode\)/)
assert.match(storeSource, /status: summary\.mainStatus/)
assert.match(storeSource, /taxMode: summary\.taxMode \?\? 'BUY_DECLARATION'/)
assert.match(storeSource, /customsInspection: summary\.customsInspection \?\? false/)
assert.match(storeSource, /fulfillmentType: summary\.fulfillmentType \?\? 'STANDARD'/)
assert.match(storeSource, /lxCode: summary\.lxCode \?\? ''/)
assert.match(storeSource, /mapped\.fulfillmentType = detail\.summary\.fulfillmentType \?\? mapped\.fulfillmentType/)
assert.match(storeSource, /mapped\.lxCode = detail\.summary\.lxCode \?\? mapped\.lxCode/)
assert.match(storeSource, /requestedPickupAt: summary\.requestedPickupAt \?\? ''/)
assert.match(storeSource, /chargeableVolumeCbm: summary\.chargeableVolumeCbm \?\? null/)
assert.match(storeSource, /inboundedAt: summary\.inboundCompletedAt \?\? ''/)
assert.match(storeSource, /pickupMethod: summary\.pickupMethod \?\? 'SELF_EXPRESS'/)
assert.match(storeSource, /destinationType: summary\.destinationType \?\? 'PRIVATE'/)
assert.match(storeSource, /mapped\.requestedPickupAt = summary\.requestedPickupAt/)
assert.match(storeSource, /summary\?\.loading\?\.loadingVersion/)
assert.match(storeSource, /summary\?\.loading\?\.pickingVersion/)
assert.match(storeSource, /summary\?\.inboundId == null \? '' : String\(summary\.inboundId\)/)
assert.match(storeSource, /customerRemark: summary\.customerRemark \?\? ''/)
assert.match(storeSource, /PALLETIZE_DROPSHIP: '一件代发组托'/)
})
test('sends a manually entered old-system inbound code with the draft payload', () => {
assert.match(storeSource, /inboundCode: draft\.inboundCode\?\.trim\(\) \|\| undefined/)
})
test('sends backend-required channel, pricing, customs, destination and cargo fields', () => {
for (const field of ['pickupMethod', 'channelCode', 'channelName', 'pricingTaxMode', 'taxMode', 'customsInspection', 'destinationType', 'packageType']) {
assert.match(storeSource, new RegExp(field))
}
assert.match(storeSource, /containerMode: draft\.transportMode === 'FCL' \? 'FULL_CONTAINER'/)
assert.doesNotMatch(storeSource, /customerId: draft\.shipperPartyId/)
assert.doesNotMatch(apiSource, /interface FirstLegOrderSaveRequest \{[^}]*customerId\??:/s)
assert.match(storeSource, /customerName: customerName\.trim\(\)/)
assert.match(storeSource, /destinationWarehouseId: draft\.destinationType === 'PRIVATE' \? undefined : draft\.destinationWarehouseId/)
assert.match(storeSource, /senderAddressId: draft\.pickupMethod === 'PICKUP' \? draft\.senderAddressId : undefined/)
assert.match(storeSource, /privateDestinationAddressId: draft\.destinationType === 'PRIVATE' \? draft\.privateDestinationAddressId : undefined/)
assert.doesNotMatch(storeSource, /sender: partyPayload|receiver: partyPayload/)
for (const field of [
'customsHsCode', 'lengthCm', 'widthCm', 'heightCm', 'unitsPerPackage', 'totalQuantity',
'netWeightKg', 'unitDeclaredValue', 'material', 'usage', 'model', 'brand',
'productLengthCm', 'productWidthCm', 'productHeightCm', 'shipmentId', 'referenceId',
'productImageFileId', 'woodenProduct',
]) {
assert.match(storeSource, new RegExp(`${field}: item\\.`))
}
assert.match(apiSource, /POST[^\n]*product-images|method: 'POST', url: '\/merchant\/v1\/files\/product-images'/)
assert.match(apiSource, /GET[^\n]*product-images|method: 'GET', url: `\/merchant\/v1\/files\/product-images\/\$\{fileId\}`/)
})
test('separates domestic receiving warehouses from public destinations', () => {
assert.match(apiSource, /warehouseType\?: MerchantWarehouseType/)
assert.match(apiSource, /params: \{ page: 1, size: 200, status: 'ACTIVE', \.\.\.params \}/)
assert.match(storeSource, /loadAllWarehouseRecords\(warehouseType, getMerchantWarehouses\)/)
assert.match(storeSource, /loadWarehouseType\('DOMESTIC'\)/)
assert.match(storeSource, /warehouse\.warehouseType !== 'DOMESTIC'/)
assert.match(storeSource, /item\.warehouseType === draft\.destinationType/)
assert.match(storeSource, /draft\.amazonWarehouseCode = draft\.destinationType === 'FBA' \? destinationWarehouse\.code : ''/)
})
test('only blocks submission when a known company audit status is not active', () => {
assert.match(storeSource, /companyAuditStatus\?: MerchantTenantStatus/)
assert.match(storeSource, /if \(companyAuditStatus && companyAuditStatus !== 'ACTIVE'\)/)
})
test('uses the versioned merchant cancellation contract', () => {
assert.match(storeSource, /cancelMerchantFirstLegOrder\(id, payload, createIdempotencyKey\(/)
assert.match(apiSource, /cancelMerchantFirstLegOrder\(id: number \| string, payload: \{ expectedVersion: number; reason: string \}, idempotencyKey: string\)/)
})
test('sends an idempotency key for create and submit writes', () => {
assert.match(apiSource, /'Idempotency-Key': createIdempotencyKey\('first-leg-create'\)/)
assert.match(apiSource, /submitMerchantFirstLegOrder\(id: number \| string, payload: \{ expectedVersion: number \}, idempotencyKey: string\)/)
assert.match(apiSource, /'Idempotency-Key': idempotencyKey/)
assert.match(apiSource, /createMerchantFirstLegOrderBatch\(items: MerchantBatchDraftItem\[\], idempotencyKey: string\)/)
assert.match(apiSource, /headers: \{ 'Idempotency-Key': idempotencyKey \}/)
assert.match(storeSource, /createMerchantFirstLegOrderBatch\(items, idempotencyKey\)/)
assert.doesNotMatch(storeSource, /pendingSubmissions|pendingEdits|pendingCancellation/)
assert.match(storeSource, /submitMerchantFirstLegOrder\(id, \{ expectedVersion: order\.version \}, createIdempotencyKey\(/)
})
test('does not expose the contradictory backend rejected state in the client model', () => {
assert.match(storeSource, /status === 'REJECTED'\) return 'PENDING_REVIEW'/)
assert.match(storeSource, /item\.toStatus === 'REJECTED' \? '订单信息已补充'/)
})
test('maps the final backend timeline and keeps missing custom nodes empty', () => {
assert.match(storeSource, /\(detail\.timeline \?\? \[\]\)\.flatMap/)
assert.match(storeSource, /item\.mainStatus/)
assert.match(storeSource, /\(detail\.customNodes \?\? \[\]\)/)
assert.match(storeSource, /\.filter\(\(item\) => item\.customerVisible\)/)
assert.match(storeSource, /kind: 'CUSTOM_NODE'/)
assert.match(storeSource, /status: null/)
assert.match(storeSource, /\.sort\(\(left, right\)/)
})
test('keeps unified detail mapping recoverable for historical partial responses', () => {
assert.match(storeSource, /\(detail\.parties \?\? \[\]\)\.find/)
assert.match(storeSource, /\(detail\.statusHistory \?\? \[\]\)\.map/)
assert.match(storeSource, /item\.attachmentIds\?\.length \?\? 0/)
assert.match(storeSource, /if \(!detail\?\.summary \|\| !detail\.order\?\.order\) throw new Error\('订单详情数据不完整,请刷新列表后重试'\)/)
})
test('maps customer-visible carrier and order lifecycle snapshots into detail', () => {
assert.match(apiSource, /export interface MerchantFirstLegOrder[\s\S]*carrierId\?: number \| string \| null/)
assert.match(storeSource, /carrierName: order\.carrierName \?\? ''/)
assert.match(storeSource, /receivingTime: order\.receivingTime \?\? ''/)
assert.match(storeSource, /submittedAt: order\.submittedAt \?\? ''/)
assert.match(storeSource, /completedAt: order\.completedAt \?\? ''/)
})
test('maps backend warehouse snapshots and fulfillment courier fields', () => {
assert.match(storeSource, /order\.receivingWarehouseAddress/)
assert.match(storeSource, /order\.expressCompany \?\? ''/)
assert.match(storeSource, /order\.expressTrackingNo \?\? ''/)
assert.match(storeSource, /getMerchantWarehouseDetail\(id\)/)
assert.match(storeSource, /receivingCapability\.cutoffTime/)
})
test('keeps failed order loads distinct from empty results', () => {
assert.match(storeSource, /const loadError = ref\(''\)/)
assert.match(storeSource, /orderResult\.reason instanceof Error/)
assert.match(storeSource, /warehouseLoadError/)
assert.match(storeSource, /return \{ orders, orderDetails, total, queueCounts, orderProgress, queryChannelNames, warehouses, loading, loadedFromApi, loadError, warehouseLoadError/)
})