import test from 'node:test' import assert from 'node:assert/strict' import { customsRetentionDigest, verifyTaxCustomsAfterShipEdit, portScheduleCustomerScope } from './phase3-port-schedules.mjs' test('public booking null scope becomes platform zero; positive IDs retain exact precision', () => { assert.equal(portScheduleCustomerScope(null), '0') assert.equal(portScheduleCustomerScope(undefined), '0') assert.equal(portScheduleCustomerScope('2105227486329671682'), '2105227486329671682') assert.equal(portScheduleCustomerScope(123), '123') for (const invalid of ['', '0', 0, '-1', 'customer']) { assert.throws(() => portScheduleCustomerScope(invalid), /null or a positive ID/) } }) test('customs retention digest includes documents, receipt versions, confirmations and completion facts', () => { const before = { status: 'COMPLETED', documents: [{ fileId: '1', tenantId: 'A', version: 2 }], receipts: [{ fileId: '2', version: 3 }], confirmations: [{ tenantId: 'A', status: 'CONFIRMED' }], completionFiles: [{ fileId: '3' }] } assert.equal(customsRetentionDigest(before), customsRetentionDigest({ ...before })) for (const mutate of [ value => { value.documents[0].tenantId = 'B' }, value => { value.receipts[0].version++ }, value => { value.confirmations[0].status = 'RETURNED' }, value => { value.completionFiles = [] }, ]) { const after = structuredClone(before) mutate(after) assert.notEqual(customsRetentionDigest(before), customsRetentionDigest(after)) } }) test('object key ordering does not obscure stable customs facts', () => { assert.equal(customsRetentionDigest({ a: 1, b: { x: 2, y: 3 } }), customsRetentionDigest({ b: { y: 3, x: 2 }, a: 1 })) }) test('ship edit keeps original completed customs, query and node facts and sends only formal DTO fields', async () => { const marker = 'F0124-SHIP-TEST' const customs = { loadingPlanId: 'plan', containerBookingId: 'container', status: 'COMPLETED', taxRefundCount: 1, documents: [{ source: { fileId: 'source' }, receipt: { fileId: 'receipt', version: 2 }, receiptStatus: 'CONFIRMED', confirmationHistory: [{ id: 'confirmation' }], finalDocument: { fileId: 'final' } }] } const schedule = { id: 'schedule', version: 2, customerId: 'customer', oceanBookingId: 'ocean', containerBookingId: 'container', vesselName: 'old', voyageNo: 'old', sourceType: 'MANUAL', sourceName: 'source', sourceRef: 'ref', observedAt: '2026-09-30T12:00:00', expiresAt: '2026-10-01T12:00:00' } const query = { id: 'query', containerBookingId: 'container', oceanBookingId: 'ocean', trackingNo: 'BOX', latestActualNode: 'DEPARTED_WAREHOUSE', latestActualAt: '2026-09-30T12:00:00' } const nodes = [{ id: 'original-node', standardNode: 'DEPARTED_WAREHOUSE', rawPayloadHash: 'original-hash' }] let edited const steps = [] const context = { marker, token: 'offline-placeholder', options: { gateway: 'https://offline.invalid' }, createdResources: {}, apiSuccess(_module, name, response) { steps.push({ name, passed: response.http === 200 }); return response.body.data }, assert(_module, name, _response, passed) { steps.push({ name, passed }) } } const originalFetch = globalThis.fetch globalThis.fetch = async (url, options) => { const path = new URL(url).pathname let data if (options.method === 'PUT') { edited = JSON.parse(options.body) data = {} } else if (path.includes('/customs/')) data = structuredClone(customs) else if (path.endsWith('/carrier-schedule-queries/query')) data = { query: { query }, nodes } else if (path.endsWith('/carrier-schedule-queries')) data = { total: 1, records: [{ query }] } else data = { portSchedule: { schedule: edited ? { ...schedule, ...edited.data, version: 3 } : schedule }, histories: edited ? [{ id: 'history', action: 'UPDATE', operatorId: 'operator', createdAt: '2026-09-30T13:00:00', toVersion: 3 }] : [] } return new Response(JSON.stringify({ code: '200', data }), { status: 200, headers: { 'content-type': 'application/json' } }) } try { await verifyTaxCustomsAfterShipEdit(context, { marker, loadingPlanId: 'plan', containerBookingId: 'container', queryId: 'query', scheduleId: 'schedule' }) assert.equal(steps.every(step => step.passed), true, JSON.stringify(steps)) assert.equal(edited.expectedVersion, 2) assert.equal('id' in edited.data, false) assert.equal('version' in edited.data, false) assert.equal(edited.data.customerId, 'customer') assert.equal(context.createdResources.taxCustomsShipRetention.customsBeforeDigest, context.createdResources.taxCustomsShipRetention.customsAfterDigest) } finally { globalThis.fetch = originalFetch } }) test('ship edit refuses incomplete tax customs before any PUT', async () => { const calls = [] const originalFetch = globalThis.fetch globalThis.fetch = async (_url, options) => { calls.push(options.method) return new Response(JSON.stringify({ code: '200', data: { status: 'DECLARING', taxRefundCount: 1 } }), { status: 200 }) } const context = { marker: 'own', token: '', options: { gateway: 'https://offline.invalid' }, apiSuccess(_module, _name, response) { return response.body.data }, assert() {} } try { await assert.rejects(verifyTaxCustomsAfterShipEdit(context, { marker: 'own', loadingPlanId: 'plan', containerBookingId: 'container', queryId: 'query', scheduleId: 'schedule' }), /completed tax customs/) assert.deepEqual(calls, ['GET']) } finally { globalThis.fetch = originalFetch } })