test: sequence pickup readiness before overseas receiving
This commit is contained in:
@@ -5,7 +5,7 @@ import { dirname, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import process from 'node:process'
|
||||
|
||||
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'master-data', 'suppliers', 'domestic-container', 'orders', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-loading-finalize', 'phase3-customs', 'phase3-port-schedules', 'phase3-carrier-schedule-queries', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'phase4-overseas-warehouse', 'phase4-overseas-pda', 'phase4-overseas-inbound-operations', 'phase4-overseas-client-services', 'phase4-overseas-dispatch', 'phase4-cross-region-events', 'phase4-overseas-business', 'phase4-overseas-master-data', 'phase4-carrier-nodes', 'finance', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-downloads', 'customer-operating-statistics']
|
||||
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'master-data', 'suppliers', 'domestic-container', 'orders', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-loading-finalize', 'phase3-customs', 'phase3-port-schedules', 'phase3-carrier-schedule-queries', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'phase4-overseas-warehouse', 'phase4-carrier-pickup-ready', 'phase4-overseas-pda', 'phase4-overseas-inbound-operations', 'phase4-overseas-client-services', 'phase4-overseas-dispatch', 'phase4-cross-region-events', 'phase4-overseas-business', 'phase4-overseas-master-data', 'phase4-carrier-nodes', 'finance', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-downloads', 'customer-operating-statistics']
|
||||
|
||||
const MODULE_DEPENDENCIES = new Map([
|
||||
['orders', ['merchant-qualification-fixture']],
|
||||
@@ -29,12 +29,13 @@ const MODULE_DEPENDENCIES = new Map([
|
||||
['phase3-carrier-schedule-queries', ['phase3-outbound-dispatch']],
|
||||
['phase3-order-splitting', ['phase3-loading-finalize']],
|
||||
['manual-state-matrix', ['phase3-loading-finalize', 'phase3-customs', 'ocean-bookings']],
|
||||
['phase4-overseas-pda', ['phase3-loading-finalize']],
|
||||
['phase4-carrier-pickup-ready', ['phase3-carrier-schedule-queries']],
|
||||
['phase4-overseas-pda', ['phase4-carrier-pickup-ready']],
|
||||
['phase4-overseas-inbound-operations', ['phase4-overseas-pda']],
|
||||
['phase4-overseas-client-services', ['phase4-overseas-pda']],
|
||||
['phase4-overseas-dispatch', ['phase4-overseas-pda']],
|
||||
['phase4-cross-region-events', ['system', 'phase3-customs', 'phase4-overseas-pda']],
|
||||
['phase4-carrier-nodes', ['phase3-carrier-schedule-queries']],
|
||||
['phase4-carrier-nodes', ['phase4-overseas-pda']],
|
||||
])
|
||||
|
||||
function stableTopologicalOrder(modules) {
|
||||
|
||||
@@ -31,13 +31,25 @@ test('focused carrier node execution injects the complete outbound-confirm produ
|
||||
'phase3-container-loading', 'phase3-loading', 'phase3-pda-convergence', 'phase3-evidence-gate',
|
||||
'phase3-loading-finalize', 'phase3-customs', 'ocean-bookings', 'phase3-port-schedules',
|
||||
'truck-appointments', 'service-center-flow', 'phase3-outbound-dispatch',
|
||||
'phase3-carrier-schedule-queries', 'phase4-carrier-nodes']
|
||||
'phase3-carrier-schedule-queries', 'phase4-carrier-pickup-ready',
|
||||
'phase4-overseas-pda', 'phase4-carrier-nodes']
|
||||
for (const module of required) assert.ok(modules.includes(module), module)
|
||||
for (let index = 1; index < required.length; index += 1) {
|
||||
assert.ok(modules.indexOf(required[index - 1]) < modules.indexOf(required[index]), required[index])
|
||||
}
|
||||
})
|
||||
|
||||
test('real lifecycle opens the shipping gate before PDA and records pickup and empty return afterwards', () => {
|
||||
const modules = normalizeModuleOrder(['phase4-overseas-pda', 'phase4-carrier-nodes'])
|
||||
const pickup = modules.indexOf('phase4-carrier-pickup-ready')
|
||||
const pda = modules.indexOf('phase4-overseas-pda')
|
||||
const completion = modules.indexOf('phase4-carrier-nodes')
|
||||
assert.ok(pickup >= 0 && pickup < pda)
|
||||
assert.ok(pda < completion)
|
||||
assert.match(source, /phase4CarrierPickupReady/)
|
||||
assert.match(source, /STANDARD_NODES\.slice\(STANDARD_NODES\.indexOf\('PICKED_UP'\)\)/)
|
||||
})
|
||||
|
||||
test('canonical states, ten ordered nodes, exception codes and complete payload are frozen', () => {
|
||||
assert.deepEqual(QUERY_STATUSES, ['READY', 'MANUAL_REQUIRED', 'STOPPED'])
|
||||
assert.deepEqual(STANDARD_NODES, [
|
||||
|
||||
@@ -100,7 +100,7 @@ function blocker(context, name, evidence, response = null) {
|
||||
context.addStep(MODULE, name, response, false, evidence, OPTIONS)
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
export async function runUntilPickupReady(context) {
|
||||
const carriersResponse = await context.request('GET', `${BASE}/carriers`)
|
||||
const carriers = context.apiSuccess(MODULE, '查询船司只读辅助信息', carriersResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '船司辅助信息结构稳定', carriersResponse,
|
||||
@@ -188,7 +188,8 @@ export async function run(context) {
|
||||
const frozenLatestActualMs = frozenLatestActual == null ? Number.NaN
|
||||
: Date.parse(`${frozenLatestActual}Z`)
|
||||
const baseActual = (Number.isNaN(frozenLatestActualMs) ? Date.now() : frozenLatestActualMs) + 60 * 60_000
|
||||
for (let index = 0; index < STANDARD_NODES.length; index += 1) {
|
||||
const pickupReadyIndex = STANDARD_NODES.indexOf('PICKUP_READY')
|
||||
for (let index = 0; index <= pickupReadyIndex; index += 1) {
|
||||
const standardNode = STANDARD_NODES[index]
|
||||
const actualAt = localIso(new Date(baseActual + index * 60_000))
|
||||
const body = manualNodePayload({ expectedVersion: version, standardNode,
|
||||
@@ -214,6 +215,67 @@ export async function run(context) {
|
||||
detail = updated
|
||||
}
|
||||
|
||||
const pickupTask = detail.query.query
|
||||
const actualNodes = new Set(detail.nodes.filter((item) => item.actualAt != null).map((item) => item.standardNode))
|
||||
const expectedPickupNodes = STANDARD_NODES.slice(0, pickupReadyIndex + 1)
|
||||
context.assert(MODULE, 'PDA开始前已覆盖至PICKUP_READY且任务尚未停止', detailResponse,
|
||||
expectedPickupNodes.every((node) => actualNodes.has(node)) && pickupTask.status !== 'STOPPED'
|
||||
&& pickupTask.latestActualNode === 'PICKUP_READY',
|
||||
`actualNodes=${[...actualNodes].join(',')}, status=${pickupTask.status}`, OPTIONS)
|
||||
context.createdResources.phase4CarrierPickupReady = {
|
||||
marker: context.marker, queryId, version: Number(pickupTask.version),
|
||||
latestActualAt: pickupTask.latestActualAt, ready: true,
|
||||
}
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
let pickup = context.createdResources?.phase4CarrierPickupReady
|
||||
if (pickup?.marker !== context.marker || pickup?.ready !== true) {
|
||||
await runUntilPickupReady(context)
|
||||
pickup = context.createdResources?.phase4CarrierPickupReady
|
||||
}
|
||||
if (!pickup?.queryId) return
|
||||
|
||||
const queryId = id(pickup.queryId)
|
||||
const detailResponse = await context.request('GET', `${BASE}/${encodeURIComponent(queryId)}`)
|
||||
let detail = context.apiSuccess(MODULE, 'PDA收货完成后恢复本轮船节点任务', detailResponse,
|
||||
`queryId=${queryId}`, OPTIONS)
|
||||
assertDetail(context, detailResponse, detail, '恢复任务详情DTO完整')
|
||||
const pickupTask = detail?.query?.query
|
||||
if (pickupTask?.latestActualNode !== 'PICKUP_READY' || pickupTask?.status === 'STOPPED') {
|
||||
blocker(context, 'PDA收货后船节点未停留在PICKUP_READY,禁止伪造后续节点',
|
||||
`latest=${pickupTask?.latestActualNode || 'missing'}, status=${pickupTask?.status || 'missing'}`, detailResponse)
|
||||
return
|
||||
}
|
||||
|
||||
let version = Number(pickupTask.version)
|
||||
const frozenLatestActualMs = Date.parse(`${pickupTask.latestActualAt}Z`)
|
||||
const baseActual = (Number.isNaN(frozenLatestActualMs) ? Date.now() : frozenLatestActualMs) + 60 * 60_000
|
||||
const remainingNodes = STANDARD_NODES.slice(STANDARD_NODES.indexOf('PICKED_UP'))
|
||||
for (let index = 0; index < remainingNodes.length; index += 1) {
|
||||
const standardNode = remainingNodes[index]
|
||||
const actualAt = localIso(new Date(baseActual + index * 60_000))
|
||||
const body = manualNodePayload({ expectedVersion: version, standardNode,
|
||||
observedAt: localIso(new Date()), actualAt, marker: context.marker,
|
||||
index: STANDARD_NODES.indexOf(standardNode) + 1 })
|
||||
const key = `${context.marker}-carrier-actual-${String(STANDARD_NODES.indexOf(standardNode) + 1).padStart(2, '0')}`.slice(0, 128)
|
||||
const response = await context.request('POST', `${BASE}/${queryId}/manual-nodes`, body,
|
||||
context.token, { 'Idempotency-Key': key })
|
||||
const updated = context.apiSuccess(MODULE, `PDA完成后补录实际节点${standardNode}`, response,
|
||||
`queryId=${queryId}`, OPTIONS)
|
||||
assertDetail(context, response, updated, `实际节点${standardNode}响应DTO完整`)
|
||||
const updatedTask = updated?.query?.query
|
||||
const isEmptyReturned = standardNode === 'EMPTY_RETURNED'
|
||||
context.assert(MODULE, `${standardNode}实际事实单调推进且停止语义正确`, response,
|
||||
updatedTask?.latestActualNode === standardNode && updatedTask?.latestActualAt === actualAt
|
||||
&& (isEmptyReturned
|
||||
? updatedTask.status === 'STOPPED' && updatedTask.stopReason === '已还柜' && updatedTask.stoppedAt != null
|
||||
: updatedTask.status !== 'STOPPED'),
|
||||
`status=${updatedTask?.status || 'missing'}, latest=${updatedTask?.latestActualNode || 'none'}`, OPTIONS)
|
||||
version = Number(updatedTask.version)
|
||||
detail = updated
|
||||
}
|
||||
|
||||
const finalTask = detail.query.query
|
||||
const actualNodes = new Set(detail.nodes.filter((item) => item.actualAt != null).map((item) => item.standardNode))
|
||||
context.assert(MODULE, '最终详情覆盖十标准实际节点且只有EMPTY_RETURNED触发停止', detailResponse,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { runUntilPickupReady } from './phase4-carrier-nodes.mjs'
|
||||
|
||||
export async function run(context) {
|
||||
await runUntilPickupReady(context)
|
||||
}
|
||||
Reference in New Issue
Block a user