import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import test from 'node:test' import { fileURLToPath } from 'node:url' import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs' import { run } from './phase5-terminal-drayage.mjs' const source = await readFile(fileURLToPath(new URL('./phase5-terminal-drayage.mjs', import.meta.url)), 'utf8') test('four real command payloads mutate contract fields and preserve remarks and state on conflict', async () => { const marker = 'offline-terminal' const containerBookingId = '9223372036854775806' const containerNo = 'TEST0000001' const actions = ['SELECT_FLEET', 'SCHEDULE_PICKUP', 'RECORD_PICKUP', 'RECORD_EMPTY_RETURN'] const paths = ['fleet', 'pickup-appointment', 'pickup', 'empty-return'] let state = { containerBookingId, containerNo, version: 0, allowedActions: ['SELECT_FLEET'] } const history = [] const commands = new Map() let conflicts = 0 let replays = 0 const response = (data, code = '200') => ({ http: 200, body: { code, data }, requestId: 'offline-request', traceId: 'offline-trace' }) const context = { marker, token: 'offline-only', createdResources: { phase4InboundProducerFixtures: { marker, containers: [{ containerBookingId, containerNo, shippingReady: true, carrierScheduleQueryId: '9223372036854775805', carrierLatestActualAt: '2026-09-09T01:00:00' }] }, phase4OverseasPda: { marker, finalStatus: 'COMPLETED', completedContainerFacts: [{ containerBookingId, containerNo }] }, }, apiSuccess(_module, name, result) { assert.equal(result.body.code, '200', name); return result.body.data }, assert(_module, name, _response, passed) { assert.ok(passed, name) }, addStep(_module, name, _response, passed) { assert.ok(passed, name) }, addCoverageGap() { assert.fail('unexpected missing fixture') }, async request(method, path, body, _token, headers) { if (path.includes('/suppliers?')) return response({ records: [{ id: '9223372036854775804', name: 'Offline fleet', status: 'ACTIVE' }] }) if (method === 'GET') { if (path.endsWith('/history')) return response(history) if (path.includes('?containerBookingIds=')) return response([state]) return response(state) } const actionPath = path.split('/').at(-1) const index = paths.indexOf(actionPath) assert.ok(index >= 0) const field = index < 2 ? 'reason' : 'remark' const forbidden = index < 2 ? 'remark' : 'reason' assert.equal(Object.hasOwn(body, forbidden), false, `${actionPath} must not send ${forbidden}`) const key = headers['Idempotency-Key'] const saved = commands.get(key) if (saved) { if (JSON.stringify(saved.body) === JSON.stringify(body)) { replays++; return response(saved.state) } assert.deepEqual(body, { ...saved.body, [field]: `${marker}-DIFFERENT` }) conflicts++ return response(null, 'A0409') } assert.equal(body[field], marker) assert.equal(body.expectedVersion, state.version) state = { ...state, version: state.version + 1 } if (index === 0) Object.assign(state, { fleetSupplierId: body.fleetSupplierId, fleetSupplierName: body.fleetSupplierName, allowedActions: ['SCHEDULE_PICKUP'] }) if (index === 1) state.pickupAppointmentAt = body.pickupAppointmentAt if (index === 2) Object.assign(state, { status: 'UNLOADED', actualPickupAt: body.actualAt, unloadingCompletedAt: '2026-09-09T04:00:00', allowedActions: ['RECORD_EMPTY_RETURN'] }) if (index === 3) Object.assign(state, { status: 'EMPTY_RETURNED', actualEmptyReturnAt: body.actualAt, allowedActions: [], emptyReturnWarning: { status: 'RESOLVED' } }) history.push({ action: actions[index], fromVersion: index, toVersion: index + 1, reason: body[field] }) commands.set(key, { body: structuredClone(body), state: structuredClone(state) }) return response(state) }, } await run(context) assert.equal(commands.size, 4) assert.equal(replays, 4) assert.equal(conflicts, 4) assert.equal(state.version, 4) assert.equal(context.createdResources.phase5TerminalDrayage.finalStatus, 'EMPTY_RETURNED') }) test('terminal drayage runs after pickup-ready and overseas unloading but before final carrier validation', () => { const modules = normalizeModuleOrder(['phase4-carrier-nodes']) const pickup = modules.indexOf('phase4-carrier-pickup-ready') const pda = modules.indexOf('phase4-overseas-pda') const drayage = modules.indexOf('phase5-terminal-drayage') const finalCarrier = modules.indexOf('phase4-carrier-nodes') assert.ok(pickup >= 0 && pickup < pda) assert.ok(pda < drayage && drayage < finalCarrier) }) test('module uses only public business endpoints and reuses public trucking master data', () => { for (const path of [ '/api/platform/v1/terminal-drayages', "'fleet'", "'pickup-appointment'", "'pickup'", "'empty-return'", '/history', 'category=TRUCKING', ]) assert.ok(source.includes(path), `missing ${path}`) assert.doesNotMatch(source, /\/internal\//) assert.doesNotMatch(source, /POST[^\n]*\/api\/platform\/v1\/suppliers/) assert.match(source, /脚本不临时创建主数据/) }) test('all four commands cover idempotent replay hash conflict and lifecycle semantics', () => { for (const token of [ '选择车队', '预约提柜', '确认实际提柜', '确认实际还柜', '同键重放', '同键异载荷返回A0409', 'PICKED_UP', 'UNLOADED', 'EMPTY_RETURNED', 'SELECT_FLEET', 'SCHEDULE_PICKUP', 'RECORD_PICKUP', 'RECORD_EMPTY_RETURN', 'RESOLVED', ]) assert.ok(source.includes(token), `missing ${token}`) assert.match(source, /JSON\.stringify\(replay\.data\) === JSON\.stringify\(first\.data\)/) }) test('target is strictly current-marker pickup and completed-PDA container', () => { assert.match(source, /matrix\?\.marker !== context\.marker/) assert.match(source, /pda\?\.marker !== context\.marker/) assert.match(source, /pda\?\.finalStatus !== 'COMPLETED'/) assert.match(source, /completedContainerFacts/) assert.match(source, /item\?\.shippingReady === true/) assert.match(source, /carrierScheduleQueryId/) assert.match(source, /carrierLatestActualAt/) assert.match(source, /phase4CarrierPickupReady =/) assert.match(source, /禁止借历史货柜/) })