This commit is contained in:
2026-08-15 10:35:54 +08:00
parent 555acca779
commit c4282020b5
11 changed files with 100 additions and 11 deletions
@@ -101,3 +101,13 @@ test('HTTP fixtures cover unassigned CONFIRMED persistence and assigned customer
assert.match(containerLoading, /id\(candidate\?\.accountId\) === tenantId/)
assert.match(containerLoading, /id\(item\.accountId\) === tenantId/)
})
test('independent ocean booking release reuses the server-frozen transport snapshot', () => {
assert.match(bookingModule, /const releaseSnapshotResponse = await request\(context, 'GET', path\)/)
assert.match(bookingModule, /const frozenTransport = Object\.fromEntries\(transportFields\.map/)
assert.match(bookingModule, /const frozenTransportValid = transportFields\.every/)
assert.match(bookingModule, /if \(!frozenTransportValid\) return/)
assert.match(bookingModule, /expectedVersion: version, \.\.\.frozenTransport, reason: context\.marker/)
assert.doesNotMatch(bookingModule, /expectedVersion: version, billOfLadingNo: `BL-\$\{suffix\}`/)
assert.doesNotMatch(bookingModule, /estimatedSailingAt: '2026-09-01T08:00:00'/)
})
@@ -68,10 +68,14 @@ async function releaseLoadingBooking(context, containerFlow, suffix) {
allocationMatches && id(flow?.oceanBooking?.customerId) === id(containerFlow?.tenantId),
`oceanBookingId=${flowOceanBookingId}, containerBookingId=${id(containerFlow.containerBookingId)}, allocations=${flow?.containerAllocations?.length ?? 'n/a'}`, OPTIONS)
if (allocationMatches && flow?.oceanBooking?.status === 'CONFIRMED') {
const transportFields = ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']
const frozenTransport = Object.fromEntries(transportFields.map((field) => [field, flow?.oceanBooking?.[field]]))
const frozenTransportValid = transportFields.every((field) => frozenTransport[field] != null && String(frozenTransport[field]).trim() !== '')
context.assert(MODULE, '实际装货柜次订舱回读完整冻结运输快照', flowResponse, frozenTransportValid,
`oceanBookingId=${flowOceanBookingId}, ${transportFields.map((field) => `${field}=${frozenTransport[field] || 'missing'}`).join(', ')}`, OPTIONS)
if (!frozenTransportValid) return null
const flowRelease = await request(context, 'POST', `${flowPath}/release`, {
expectedVersion: Number(flow.oceanBooking.version), billOfLadingNo: `BL-FLOW-${suffix}`,
vesselName: `装货船-${suffix}`, voyageNo: `FV${suffix}`,
estimatedSailingAt: '2026-09-01T08:00:00', estimatedArrivalAt: '2026-09-20T08:00:00',
expectedVersion: Number(flow.oceanBooking.version), ...frozenTransport,
reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-ocean-flow-release`.slice(0, 128) })
context.apiSuccess(MODULE, '放舱实际装货柜次所属订舱', flowRelease, '', OPTIONS)
@@ -140,6 +144,15 @@ export async function run(context) {
const changed = await request(context, 'POST', `${path}/change-request`, { expectedVersion: version, vesselName: `改船-${suffix}`, voyageNo: `CV${suffix}`, reason: context.marker }, { 'Idempotency-Key': `${context.marker}-change` })
context.apiSuccess(MODULE, '申请改船', changed, '', OPTIONS); version = Number(data(changed)?.oceanBooking?.version)
const changeConfirmed = await action('确认改船', 'change-confirm', version, 'confirm change')
const releaseSnapshotResponse = await request(context, 'GET', path)
const releaseSnapshot = data(releaseSnapshotResponse)
const transportFields = ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']
const frozenTransport = Object.fromEntries(transportFields.map((field) => [field, releaseSnapshot?.oceanBooking?.[field]]))
const frozenTransportValid = transportFields.every((field) => frozenTransport[field] != null && String(frozenTransport[field]).trim() !== '')
context.assert(MODULE, '改船后回读完整冻结运输快照供放舱', releaseSnapshot, frozenTransportValid
&& transportFields.every((field) => String(frozenTransport[field]) === String(changeConfirmed?.oceanBooking?.[field])),
`bookingId=${bookingId}, ${transportFields.map((field) => `${field}=${frozenTransport[field] || 'missing'}`).join(', ')}`, OPTIONS)
if (!frozenTransportValid) return
const allocatedContainerBookingId = ''
if (containerFlow?.finalStatus === 'COMPLETED' || containerFlow?.finalStatus === 'LOADED') {
context.assert(MODULE, '订舱与本轮柜次客户归属一致', changed,
@@ -154,9 +167,7 @@ export async function run(context) {
`allocations=${changeConfirmed?.containerAllocations?.length ?? 'n/a'}, version=${version}`, OPTIONS)
}
const released = await request(context, 'POST', `${path}/release`, {
expectedVersion: version, billOfLadingNo: `BL-${suffix}`, vesselName: `放舱船-${suffix}`,
voyageNo: `RV${suffix}`, estimatedSailingAt: '2026-09-01T08:00:00',
estimatedArrivalAt: '2026-09-20T08:00:00', reason: 'release',
expectedVersion: version, ...frozenTransport, reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-ocean-release` })
const releasedDetail = data(released)
context.apiSuccess(MODULE, '放舱', released, `status=${releasedDetail?.oceanBooking?.status || 'missing'}`, OPTIONS)
@@ -21,8 +21,10 @@ export async function run(context) {
// AWAITING_REVIEW is a PDA workflow state, not evidence permission. Evidence is
// opened only after the server-side inbound aggregate has entered LOADING.
// The list endpoint only exposes tasks that are currently in the loading queue.
// Once PDA completion advances the task, use its stable task identity instead.
const taskResponse = await context.request('GET',
`/api/platform/v1/first-leg/pda-loading/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
`/api/platform/v1/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}?warehouseId=${encodeURIComponent(warehouseId)}`)
const task = successful(taskResponse) ? taskResponse.body?.data : null
const taskItem = task?.items?.find?.((item) => id(item?.inboundId) === inboundId)
const taskIdentityValid = id(task?.id) === taskId
@@ -8,7 +8,8 @@ test('evidence gate requires stable PDA identity and server LOADING before any e
for (const field of ['taskId', 'inboundId', 'loadingPlanId', 'containerBookingId', 'warehouseId']) {
assert.match(source, new RegExp(`${field}`), `${field} must be part of the preflight identity`)
}
assert.match(source, /pda-loading\/tasks\?loadingPlanId=/)
assert.match(source, /pda-loading\/tasks\/\$\{encodeURIComponent\(taskId\)\}\?warehouseId=/)
assert.doesNotMatch(source, /pda-loading\/tasks\?loadingPlanId=/)
assert.match(source, /task\?\.id\) === taskId/)
assert.match(source, /task\?\.loadingPlanId\) === loadingPlanId/)
assert.match(source, /task\?\.containerBookingId\) === containerBookingId/)