152 lines
9.7 KiB
JavaScript
152 lines
9.7 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import test from 'node:test'
|
|
import { bookingPayload } from './phase3-order-splitting.mjs'
|
|
|
|
const source = readFileSync(new URL('./phase3-order-splitting.mjs', import.meta.url), 'utf8')
|
|
|
|
test('fresh completed child runs retain distinct bill numbers despite identical long marker suffixes', () => {
|
|
const sourceOrder = { tenantId: '12', sourceOrderId: '34', channelCode: 'MATSON_CLX', warehouseId: '56' }
|
|
const agent = { id: '78', name: 'agent' }, carrier = { id: '90', name: 'carrier', carrierCode: 'MATSON' }
|
|
const markers = ['ADMIN-API-20261002030454498-COMPLETED-F40-CHILD',
|
|
'ADMIN-API-20261002034957394-COMPLETED-F40-CHILD']
|
|
const draft = (marker, sequence = 1, order = sourceOrder) => bookingPayload(marker, sequence, order, agent, carrier)
|
|
assert.equal(`${markers[0]}-1`.replace(/[^A-Za-z0-9]/g, '').slice(-18),
|
|
`${markers[1]}-1`.replace(/[^A-Za-z0-9]/g, '').slice(-18))
|
|
assert.notEqual(draft(markers[0]).billOfLadingNo, draft(markers[1]).billOfLadingNo)
|
|
assert.equal(draft(markers[0]).billOfLadingNo, draft(markers[0]).billOfLadingNo)
|
|
assert.notEqual(draft(markers[0]).billOfLadingNo, draft(markers[0], 2).billOfLadingNo)
|
|
assert.notEqual(draft(markers[0]).billOfLadingNo,
|
|
draft(markers[0], 1, { ...sourceOrder, sourceOrderId: '35' }).billOfLadingNo)
|
|
assert.match(draft(markers[0]).billOfLadingNo, /^BL-[a-f0-9]{18}$/)
|
|
})
|
|
|
|
test('each remainder uses a simulation linked to a released booking requirement', () => {
|
|
assert.match(source, /POST', CONTAINERS/)
|
|
assert.match(source, /POST', '\/api\/platform\/v1\/ocean-bookings'/)
|
|
assert.match(source, /订舱为 RELEASED 且不预生成真实柜/)
|
|
assert.doesNotMatch(source, /ocean-bookings\/\$\{oceanBookingId\}\/(?:submit|confirm)/)
|
|
const createPayload = source.slice(source.indexOf('function bookingPayload'), source.indexOf('async function loadOneFragment'))
|
|
assert.match(createPayload, /oceanBookingDraft\(\{ marker \}/)
|
|
assert.match(createPayload, /customerId: source\.tenantId/)
|
|
assert.match(createPayload, /channelCode: source\.channelCode/)
|
|
assert.match(source, /ocean-booking-link/)
|
|
assert.match(source, /oceanBookingId, requirementId: id\(requirement\.id\), containerNo: realContainerNo/)
|
|
assert.match(source, /ocean-bookings\/\$\{oceanBookingId\}\/release/)
|
|
const releaseStart = source.indexOf('const releaseResponse')
|
|
const releaseBlock = source.slice(releaseStart, source.indexOf('bookingDetailResponse = await', releaseStart))
|
|
assert.match(source, /bookingDocumentForm\.append\('expectedVersion', String\(Number\(bookingCreated\?\.oceanBooking\?\.version\)\)\)/)
|
|
assert.match(releaseBlock, /expectedVersion: releaseVersion/)
|
|
assert.match(releaseBlock, /'Idempotency-Key': `\$\{operationBase\}-booking-release`\.slice\(0, 128\)/)
|
|
assert.match(source, /oceanBooking\?\.status === 'RELEASED' && releasedAllocationMatches/)
|
|
assert.match(source, /id\(item\?\.containerBookingId\) === containerId/)
|
|
assert.match(source, /candidates\?bookingId=\$\{encodeURIComponent\(containerId\)\}&warehouseId=\$\{encodeURIComponent\(warehouseId\)\}/)
|
|
assert.match(source, /`\$\{SMART\}\/plans\/by-booking\/\$\{containerId\}`/)
|
|
assert.doesNotMatch(source, /candidates\?warehouseId=/)
|
|
})
|
|
|
|
test('release freezes and detail replays the same complete transport snapshot', () => {
|
|
assert.match(source, /const BOOKING_TRANSPORT_FIELDS = \[/)
|
|
assert.match(source, /const createdTransport = bookingTransportSnapshot\(bookingConfirmed\?\.oceanBooking\)/)
|
|
assert.match(source, /const persistedTransport = bookingTransportSnapshot\(bookingDetail\?\.oceanBooking\)/)
|
|
assert.match(source, /completeBookingTransport\(createdTransport\)/)
|
|
assert.match(source, /sameBookingTransport\(createdTransport, persistedTransport\)/)
|
|
assert.match(source, /if \(!transportFrozen\) return null/)
|
|
})
|
|
|
|
test('PDA opens only after linking a released requirement and uses stable plan identity', () => {
|
|
const pushIndex = source.indexOf("`${SMART}/plans/${planId}`")
|
|
const linkIndex = source.indexOf("`${CONTAINERS}/${containerId}/ocean-booking-link`")
|
|
const releaseIndex = source.indexOf("`/api/platform/v1/ocean-bookings/${oceanBookingId}/release`")
|
|
const confirmIndex = source.indexOf("`/api/platform/v1/first-leg/loading-plans/${encodeURIComponent(planId)}/confirm`")
|
|
const taskIndex = source.indexOf("`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`")
|
|
const canonicalTaskIndex = source.indexOf("`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`", taskIndex + 1)
|
|
assert.ok(releaseIndex >= 0 && linkIndex > releaseIndex && pushIndex > linkIndex
|
|
&& taskIndex > pushIndex && confirmIndex > taskIndex && canonicalTaskIndex > confirmIndex)
|
|
assert.match(source, /const realContainerNo = iso6346ContainerNumber/)
|
|
assert.match(source, /const transportStable = linked\?\.containerNo === realContainerNo/)
|
|
assert.match(source, /linked\?\.cabinetNo === simulation\.cabinetNo/)
|
|
assert.match(source, /if \(!transportStable\) return null/)
|
|
assert.match(source, /计划确认前不生成 PDA 任务/)
|
|
assert.match(source, /summary\?\.readiness\?\.canConfirm === true/)
|
|
assert.match(source, /Object\.freeze\(\{ expectedVersion: confirmableVersion, assigneeId: id\(pdaOperator.userId\) \}\)/)
|
|
assert.match(source, /confirmationStatus === 'CONFIRMED'/)
|
|
assert.match(source, /readiness\?\.readyForPda === true/)
|
|
assert.match(source, /taskId === confirmedTaskId/)
|
|
assert.match(source, /计划确认同键同参精确重放/)
|
|
assert.doesNotMatch(source, /\/preload\/confirming|\/preload\/confirm\?tenantId=/)
|
|
assert.match(source, /id\(task\?\.loadingPlanId\) === planId/)
|
|
assert.match(source, /id\(task\?\.containerBookingId\) === containerId/)
|
|
assert.doesNotMatch(source, /\$\{PDA\}\/tasks\?containerNo=/)
|
|
assert.doesNotMatch(source, /push-preload/)
|
|
})
|
|
|
|
test('plan confirmation prepares the remainder initial fee first', () => {
|
|
const feeIndex = source.indexOf('ensureInitialCalculated(context')
|
|
const confirmIndex = source.indexOf("`/api/platform/v1/first-leg/loading-plans/${encodeURIComponent(planId)}/confirm`")
|
|
assert.match(source, /import \{ ensureInitialCalculated \} from '\.\/inbound-fees\.mjs'/)
|
|
assert.ok(feeIndex >= 0 && confirmIndex > feeIndex)
|
|
assert.match(source, /inboundId: id\(candidate\.inboundId\)/)
|
|
assert.match(source, /finalStatus: 'APPROVED'/)
|
|
assert.match(source, /processStatus: 'WAIT_SPLIT'/)
|
|
assert.match(source, /if \(!initialFee\) return null/)
|
|
})
|
|
|
|
test('each remainder settles only through inventory snapshot, atomic batch and explicit complete', () => {
|
|
const claimIndex = source.indexOf("`${PDA}/tasks/${taskId}/claim`")
|
|
const snapshotIndex = source.indexOf('inventory-snapshot?warehouseId=')
|
|
const batchIndex = source.indexOf('const batchPath = `${PDA}/tasks/${taskId}/batch`')
|
|
const completeIndex = source.indexOf('const completePath = `${PDA}/tasks/${taskId}/complete`')
|
|
const treeIndex = source.indexOf('`回查 ${sourceOrderNo} 拆分后的根聚合`')
|
|
assert.ok(claimIndex >= 0 && snapshotIndex > claimIndex && batchIndex > snapshotIndex
|
|
&& completeIndex > batchIndex && treeIndex > completeIndex)
|
|
assert.match(source, /loadingPlanId: planId, containerBookingId: containerId/)
|
|
assert.match(source, /expectedVersion: Number\(inventory\.taskVersion\)/)
|
|
assert.match(source, /inventorySnapshotToken: inventory\.snapshotToken/)
|
|
assert.match(source, /action: 'PICK', pickedPackageCount: 1/)
|
|
assert.doesNotMatch(source, /releaseMode|locationIds|reservationIds/)
|
|
assert.match(source, /'Idempotency-Key': batchKey/)
|
|
assert.match(source, /differentReplay/)
|
|
assert.match(source, /workflowStatus === 'AWAITING_REVIEW'/)
|
|
assert.match(source, /expectedTaskVersion: Number\(picked\?\.version\)/)
|
|
assert.match(source, /operationId: `\$\{operationBase\}-complete`/)
|
|
assert.match(source, /completeReplay/)
|
|
assert.match(source, /workflowStatus === 'COMPLETED'/)
|
|
assert.doesNotMatch(source, /\/items\/\$\{[^}]+\}\/pick/)
|
|
})
|
|
|
|
test('booking freezes the source customer channel and warehouse scope', () => {
|
|
assert.match(source, /customerId: source\.tenantId/)
|
|
assert.match(source, /channelCode: source\.channelCode/)
|
|
assert.match(source, /warehouseId: source\.warehouseId/)
|
|
assert.match(source, /containerType === '40HQ'/)
|
|
assert.match(source, /containerType: '40HQ', capacityCbm: 68, maxWeightKg: 19500/)
|
|
})
|
|
|
|
test('split chain starts from the partial inbound and derives quantities from the real tree', () => {
|
|
assert.match(source, /fixture\?\.incrementalInboundId \|\| fixture\?\.inboundId/)
|
|
assert.match(source, /const originalPackageCount = Number\(firstTree\?\.originalPackageCount\)/)
|
|
assert.match(source, /while \(activeRemainders\(tree\)\.length > 0\)/)
|
|
assert.match(source, /sourcePackageCount = Number\(source\?\.packageCount\)/)
|
|
assert.doesNotMatch(source, /snapshotMatches\(firstTree, 4, 1, 3, 1\)/)
|
|
})
|
|
|
|
test('booking fixture never invents supplier names without formal IDs', () => {
|
|
assert.match(source, /oceanBookingDraft/)
|
|
assert.doesNotMatch(source, /SPLIT-SHIPPING|SPLIT-AGENT/)
|
|
assert.doesNotMatch(source, /shippingCompanyName: `|bookingAgentName: `/)
|
|
})
|
|
|
|
|
|
test('H5 preparation stops after real labels but before claim and refuses already picked data', () => {
|
|
const checkpoint = source.indexOf('if (context.context?.prepareDomesticPdaH5)')
|
|
const claim = source.indexOf('const claim = await request', checkpoint)
|
|
assert.ok(checkpoint > source.indexOf('const labelResponse ='))
|
|
assert.ok(claim > checkpoint)
|
|
const boundary = source.slice(checkpoint, claim)
|
|
assert.match(boundary, /pickedPackageCount\) !== 0/)
|
|
assert.match(boundary, /throw new Error/)
|
|
assert.match(boundary, /return \{ preparedForH5: true/)
|
|
assert.doesNotMatch(boundary, /request\(/)
|
|
})
|