94 lines
5.4 KiB
JavaScript
94 lines
5.4 KiB
JavaScript
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 }
|
|
})
|