103 lines
6.4 KiB
JavaScript
103 lines
6.4 KiB
JavaScript
import test from 'node:test'
|
|||
|
|
import assert from 'node:assert/strict'
|
||
|
|
import { CARRIER_EXCEPTION_CODES, carrierExceptionShapeErrors, carrierExceptionTransitionErrors,
|
||
|
|
captureCarrierExceptionEvidence } from './phase3-carrier-schedule-queries.mjs'
|
||
|
|
|
||
|
|
const fixture = () => ({ marker: 'current', queryId: '2105227585104384001', detail: {
|
||
|
|
query: { query: { id: '2105227585104384001', customerId: '0', oceanBookingId: '2105227599885111297',
|
||
|
|
containerBookingId: '2105227585104384002', latestStandardNode: 'IN_TRANSIT', latestActualAt: '2026-10-02T09:00:00' } },
|
||
|
|
nodes: [{ id: 'actual', standardNode: 'IN_TRANSIT', actualAt: '2026-10-02T09:00:00' },
|
||
|
|
{ id: 'estimated', standardNode: 'ARRIVED_DESTINATION_PORT', estimatedAt: '2026-10-20T09:00:00' }],
|
||
|
|
exceptions: [], histories: [],
|
||
|
|
} })
|
||
|
|
const fact = (code, active = true, sequence = 1) => ({ id: `210522758510438401${sequence}`,
|
||
|
|
providerCode: 'TEST_PROVIDER', providerEventId: `${code}-${active}`, providerStatus: active ? 'HOLD' : 'RELEASE',
|
||
|
|
exceptionCode: code, active, observedAt: '2026-10-02T10:00:00', locationCode: 'USLAX', locationName: 'Los Angeles',
|
||
|
|
sourceReference: 'controlled-provider', rawPayloadHash: (active ? 'a' : 'b').repeat(64), createdAt: '2026-10-02T10:00:01' })
|
||
|
|
function append(before, value) {
|
||
|
|
const after = structuredClone(before)
|
||
|
|
after.detail.exceptions.push(value)
|
||
|
|
after.detail.histories.push({ id: value.id, action: 'EXCEPTION_OBSERVED', reason: value.exceptionCode })
|
||
|
|
return after
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const code of CARRIER_EXCEPTION_CODES) {
|
||
|
|
test(`${code}: append and release retain original exception and both normal time facts`, () => {
|
||
|
|
const before = fixture(), active = fact(code), released = fact(code, false, 2)
|
||
|
|
const after = append(before, active)
|
||
|
|
assert.deepEqual(carrierExceptionTransitionErrors(before, after, { kind: 'APPEND', expected: active }), [])
|
||
|
|
assert.deepEqual(carrierExceptionTransitionErrors(after, append(after, released), { kind: 'RELEASE', expected: released }), [])
|
||
|
|
assert.deepEqual(carrierExceptionTransitionErrors(after, structuredClone(after), { kind: 'REPLAY', expected: active }), [])
|
||
|
|
assert.deepEqual(carrierExceptionTransitionErrors(after, structuredClone(after), { kind: 'CONFLICT', expected: { ...active, rawPayloadHash: 'c'.repeat(64) } }), [])
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
test('replay/conflict fail on changed fact, extra history, missing original event or wrong payload hash', () => {
|
||
|
|
const original = fact('CUSTOMS_HOLD'), before = append(fixture(), original)
|
||
|
|
for (const mutate of [
|
||
|
|
value => { value.detail.exceptions[0].active = false },
|
||
|
|
value => { value.detail.histories.push({ id: 'extra', action: 'EXCEPTION_OBSERVED' }) },
|
||
|
|
value => { value.detail.nodes[1].estimatedAt = '2026-10-21T09:00:00' },
|
||
|
|
value => { value.detail.query.query.latestStandardNode = 'PICKUP_READY' },
|
||
|
|
value => { value.marker = 'other' },
|
||
|
|
]) {
|
||
|
|
const after = structuredClone(before); mutate(after)
|
||
|
|
assert.ok(carrierExceptionTransitionErrors(before, after, { kind: 'REPLAY', expected: original }).length)
|
||
|
|
}
|
||
|
|
assert.ok(carrierExceptionTransitionErrors(fixture(), fixture(), { kind: 'REPLAY', expected: original }).length)
|
||
|
|
assert.ok(carrierExceptionTransitionErrors(before, before, { kind: 'CONFLICT', expected: original }).length)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('release replacing the active row, deleting original history or absent estimated baseline is rejected', () => {
|
||
|
|
const active = fact('TERMINAL_HOLD'), release = fact('TERMINAL_HOLD', false, 2)
|
||
|
|
const before = append(fixture(), active)
|
||
|
|
for (const mutate of [
|
||
|
|
value => { value.detail.exceptions.shift() },
|
||
|
|
value => { value.detail.histories.shift() },
|
||
|
|
value => { value.detail.nodes.pop() },
|
||
|
|
]) {
|
||
|
|
const after = append(before, release); mutate(after)
|
||
|
|
assert.ok(carrierExceptionTransitionErrors(before, after, { kind: 'RELEASE', expected: release }).length)
|
||
|
|
}
|
||
|
|
const empty = fixture(); empty.detail.nodes.pop()
|
||
|
|
assert.ok(carrierExceptionTransitionErrors(empty, append(empty, active), { kind: 'APPEND', expected: active }).length)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('reject customs declaration codes, duplicate supplier event, malformed hash and absent source fields', () => {
|
||
|
|
for (const mutate of [
|
||
|
|
value => { value.detail.exceptions[0].exceptionCode = 'INSPECTION' },
|
||
|
|
value => { value.detail.exceptions.push({ ...value.detail.exceptions[0], id: '2105227585104384019' }) },
|
||
|
|
value => { value.detail.exceptions[0].rawPayloadHash = 'bad' },
|
||
|
|
value => { value.detail.exceptions[0].providerCode = '' },
|
||
|
|
value => { value.detail.nodes[0].standardNode = 'CUSTOMS_HOLD' },
|
||
|
|
]) {
|
||
|
|
const value = append(fixture(), fact('UNKNOWN_HOLD')); mutate(value)
|
||
|
|
assert.ok(carrierExceptionShapeErrors(value.detail).length)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
test('provider DTO permits absent location and source reference; supplied values still match exactly', () => {
|
||
|
|
const active = { ...fact('UNKNOWN_HOLD'), locationCode: null, locationName: null, sourceReference: null }
|
||
|
|
const before = fixture(), after = append(before, structuredClone(active))
|
||
|
|
assert.deepEqual(carrierExceptionShapeErrors(after.detail), [])
|
||
|
|
assert.deepEqual(carrierExceptionTransitionErrors(before, after, { kind: 'APPEND', expected: active }), [])
|
||
|
|
after.detail.exceptions[0].sourceReference = 'changed'
|
||
|
|
assert.ok(carrierExceptionTransitionErrors(before, after, { kind: 'APPEND', expected: active }).length)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('real API checkpoint uses only original detail GET and rejects another marker before any call', async () => {
|
||
|
|
const before = fixture(), calls = [], original = globalThis.fetch
|
||
|
|
const context = { marker: 'current', options: { gateway: 'https://offline.invalid' }, token: 'offline',
|
||
|
|
apiSuccess: (_module, _name, response) => response.body.data, assert: (_module, _name, _response, passed) => assert.ok(passed) }
|
||
|
|
const resource = { queryId: before.queryId, marker: 'current', ownedByModule: true, triggerSource: 'OUTBOUND_CONFIRM' }
|
||
|
|
globalThis.fetch = async (url, options) => { calls.push({ url, method: options.method }); return new Response(JSON.stringify({ code: '200', data: before.detail })) }
|
||
|
|
try {
|
||
|
|
const captured = await captureCarrierExceptionEvidence(context, resource)
|
||
|
|
assert.deepEqual(captured.detail, before.detail)
|
||
|
|
assert.deepEqual(calls.map(item => item.method), ['GET'])
|
||
|
|
assert.ok(calls[0].url.endsWith(`/api/platform/v1/carrier-schedule-queries/${before.queryId}`))
|
||
|
|
await assert.rejects(captureCarrierExceptionEvidence(context, { ...resource, marker: 'other' }), /current-marker/)
|
||
|
|
assert.equal(calls.length, 1)
|
||
|
|
} finally { globalThis.fetch = original }
|
||
|
|
})
|