Files
baiyue-firstleg/scripts/admin-test-env-modules/phase3-port-schedules-retention-contract.test.mjs
T

94 lines
5.4 KiB
JavaScript
Raw Normal View History

2026-09-30 17:13:11 +08:00
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/)
}
})
2026-09-30 17:13:11 +08:00
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 }
})