feat: close preload confirmation and loading flow
This commit is contained in:
@@ -144,38 +144,6 @@ export async function run(context) {
|
||||
&& simulation?.oceanBookingId == null,
|
||||
`containerBookingId=${containerBookingId || 'missing'}, cabinetNo=${simulation?.cabinetNo || 'missing'}`, OPTIONS)
|
||||
if (!containerBookingId) return
|
||||
const bookingCreateResponse = await request(context, 'POST', '/api/platform/v1/ocean-bookings',
|
||||
{ expectedVersion: 0, data: bookingDraft },
|
||||
{ 'Idempotency-Key': `${context.marker}-p3-booking-create`.slice(0, 128) })
|
||||
const bookingCreated = context.apiSuccess(MODULE, '创建带渠道仓库快照的三期订舱', bookingCreateResponse, '', OPTIONS)
|
||||
const bookingId = id(bookingCreated?.oceanBooking?.id)
|
||||
if (!bookingId) return
|
||||
const bookingConfirmed = bookingCreated
|
||||
const transportFields = ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']
|
||||
const frozenTransport = Object.fromEntries(transportFields.map((field) => [field, bookingConfirmed?.oceanBooking?.[field]]))
|
||||
const frozenTransportValid = transportFields.every((field) => frozenTransport[field] != null && String(frozenTransport[field]).trim() !== '')
|
||||
const frozenTransportMatches = frozenTransportValid
|
||||
&& transportFields.every((field) => String(frozenTransport[field]) === String(bookingDraft[field]))
|
||||
context.assert(MODULE, 'CONFIRMED 订舱回显完整且冻结五项运输快照', bookingCreateResponse,
|
||||
frozenTransportMatches,
|
||||
`bookingId=${bookingId}, ${transportFields.map((field) => `${field}=${frozenTransport[field] || 'missing'}`).join(', ')}`, OPTIONS)
|
||||
if (!frozenTransportMatches) return
|
||||
const requirement = bookingConfirmed?.containerRequirements?.find?.((item) => item.containerType === '40HQ')
|
||||
context.assert(MODULE, '一次登记直接返回 CONFIRMED 且不自动生成三期内部柜', bookingCreateResponse,
|
||||
bookingConfirmed?.oceanBooking?.status === 'CONFIRMED' && Boolean(requirement?.id)
|
||||
&& bookingConfirmed?.containerAllocations?.length === 0,
|
||||
`status=${bookingConfirmed?.oceanBooking?.status || 'missing'}, requirementId=${id(requirement?.id) || 'missing'}, allocations=${bookingConfirmed?.containerAllocations?.length ?? 'n/a'}`, OPTIONS)
|
||||
if (!requirement?.id) return
|
||||
const linkResponse = await request(context, 'POST', `/api/platform/v1/containers/${containerBookingId}/ocean-booking-link`, {
|
||||
oceanBookingId: bookingId, requirementId: id(requirement.id), expectedVersion: Number(simulation.version),
|
||||
})
|
||||
const linked = context.apiSuccess(MODULE, '关联三期模拟柜与真实订舱', linkResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '三期订舱关联保持内部柜次身份', linkResponse,
|
||||
id(linked?.id) === containerBookingId && linked?.cabinetNo === simulation.cabinetNo
|
||||
&& id(linked?.oceanBookingId) === bookingId,
|
||||
`containerBookingId=${containerBookingId}, cabinetNo=${linked?.cabinetNo || 'missing'}`, OPTIONS)
|
||||
if (id(linked?.id) !== containerBookingId) return
|
||||
|
||||
const candidateResponse = await context.request('GET', `${smartLoadingBase}/candidates?bookingId=${encodeURIComponent(containerBookingId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const candidates = context.apiSuccess(MODULE, '查询本轮入库单柜次装柜候选', candidateResponse, '', OPTIONS)
|
||||
const candidate = Array.isArray(candidates) ? candidates.find((item) => id(item.inboundId) === inboundId
|
||||
@@ -204,93 +172,149 @@ export async function run(context) {
|
||||
&& Number(assignedLoadingPlan?.version) === 1,
|
||||
`loadingPlanId=${loadingPlanId}, version=${assignedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const smart = await request(context, 'POST', `${base}/split/smart?tenantId=${encodeURIComponent(tenantId)}`, {
|
||||
expectedVersion: 0,
|
||||
items: [{ containerCode: `P3-${suffix}`, cargoReference: `REF-${suffix}`, packageCount: 1, volumeCbm: 0.01, remark: context.marker }],
|
||||
remark: context.marker,
|
||||
}, { 'Idempotency-Key': `${context.marker}-p3-smart`.slice(0, 128) })
|
||||
const smartData = context.apiSuccess(MODULE, '智能分柜并生成预装清单', smart, `inboundId=${inboundId}`, OPTIONS)
|
||||
const planId = id(smartData?.preloadPlan?.id)
|
||||
const smartPreloadVersion = Number(smartData?.preloadPlan?.version)
|
||||
context.assert(MODULE, '智能分柜预装计划 PENDING、版本和明细正确', smart,
|
||||
smartData?.mainStatus === 'WAIT_SPLIT' && Boolean(planId) && smartData?.preloadPlan?.status === 'PENDING'
|
||||
&& Number.isInteger(smartPreloadVersion) && smartPreloadVersion >= 0
|
||||
&& smartData?.preloadPlan?.items?.length === 1,
|
||||
`mainStatus=${smartData?.mainStatus || 'missing'}, planId=${planId || 'missing'}, status=${smartData?.preloadPlan?.status || 'missing'}`, OPTIONS)
|
||||
const pendingPreloadResponse = await context.request('GET', `${base}/preload?tenantId=${encodeURIComponent(tenantId)}`)
|
||||
const pendingPreload = context.apiSuccess(MODULE, '智能排货自动建立 PENDING 预装聚合', pendingPreloadResponse, '', OPTIONS)
|
||||
const planId = id(pendingPreload?.id)
|
||||
context.assert(MODULE, '推送前入库单保持 WAIT_SPLIT 且预装聚合为 PENDING', pendingPreloadResponse,
|
||||
Boolean(planId) && pendingPreload?.status === 'PENDING' && Number.isInteger(Number(pendingPreload?.version)),
|
||||
`preloadPlanId=${planId || 'missing'}, status=${pendingPreload?.status || 'missing'}`, OPTIONS)
|
||||
if (!planId) return
|
||||
|
||||
const replay = await request(context, 'POST', `${base}/split/smart?tenantId=${encodeURIComponent(tenantId)}`, {
|
||||
expectedVersion: 0, items: [], remark: 'replay must not replace',
|
||||
}, { 'Idempotency-Key': `${context.marker}-p3-smart`.slice(0, 128) })
|
||||
context.assert(MODULE, '智能分柜幂等重放返回同一预装计划', replay,
|
||||
codeOf(replay) === '200' && id(replay.body?.data?.preloadPlan?.id) === planId
|
||||
&& replay.body?.data?.preloadPlan?.items?.length === 1,
|
||||
`HTTP=${replay.http}, code=${codeOf(replay)}, planId=${id(replay.body?.data?.preloadPlan?.id) || 'missing'}`, OPTIONS)
|
||||
|
||||
const bindVersions = Object.freeze({
|
||||
expectedPreloadVersion: smartPreloadVersion,
|
||||
expectedLoadingPlanVersion: Number(assignedLoadingPlan.version),
|
||||
const pushRequest = Object.freeze({
|
||||
path: `${smartLoadingBase}/plans/${loadingPlanId}/push-preload`,
|
||||
body: Object.freeze({ expectedVersion: Number(assignedLoadingPlan.version) }),
|
||||
headers: Object.freeze({ 'Idempotency-Key': `${context.marker}-bridge-push`.slice(0, 128) }),
|
||||
})
|
||||
const bindResponse = await request(context, 'POST',
|
||||
`${base}/preload/loading-plans/${loadingPlanId}?tenantId=${encodeURIComponent(tenantId)}`,
|
||||
bindVersions, { 'Idempotency-Key': `${context.marker}-bridge-bind`.slice(0, 128) })
|
||||
const link = context.apiSuccess(MODULE, '原子关联入库预装与柜次装柜计划', bindResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '桥接双版本递增且关联归属唯一', bindResponse,
|
||||
id(link?.inboundId) === inboundId && id(link?.loadingPlanId) === loadingPlanId
|
||||
&& id(link?.preloadPlanId) === planId && Number(link?.allocatedPackageCount) === 1
|
||||
&& Number(link?.preloadVersion) > bindVersions.expectedPreloadVersion
|
||||
&& Number(link?.loadingPlanVersion) > bindVersions.expectedLoadingPlanVersion,
|
||||
`linkId=${id(link?.id) || 'missing'}, preloadVersion=${link?.preloadVersion ?? 'missing'}, loadingVersion=${link?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
|
||||
const pushResponse = await request(context, 'POST', pushRequest.path, pushRequest.body, pushRequest.headers)
|
||||
const pushedLoadingPlan = context.apiSuccess(MODULE, '无真实柜号推送预装成功并冻结柜次明细', pushResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '无号推送保持 EDITING、身份稳定且版本递增', pushResponse,
|
||||
id(pushedLoadingPlan?.id) === loadingPlanId && pushedLoadingPlan?.status === 'EDITING'
|
||||
&& pushedLoadingPlan?.containerNo == null
|
||||
&& Number(pushedLoadingPlan?.version) > Number(assignedLoadingPlan.version),
|
||||
`status=${pushedLoadingPlan?.status || 'missing'}, version=${pushedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
if (pushedLoadingPlan?.status !== 'EDITING') return
|
||||
|
||||
const pushedDetailResponse = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const pushedDetail = context.apiSuccess(MODULE, '无号推送后预装清单正式可见', pushedDetailResponse, '', OPTIONS)
|
||||
const pushedLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
|
||||
const pushedLinks = context.apiSuccess(MODULE, '无号推送自动建立 bridge 关联', pushedLinksResponse, '', OPTIONS)
|
||||
const link = Array.isArray(pushedLinks) ? pushedLinks.find((item) => id(item.inboundId) === inboundId) : null
|
||||
context.assert(MODULE, 'push 自动建立唯一预装关联且分配身份稳定', pushedLinksResponse,
|
||||
Boolean(link?.id) && id(link?.loadingPlanId) === loadingPlanId && id(link?.preloadPlanId) === planId
|
||||
&& Number(link?.allocatedPackageCount) === 1,
|
||||
`linkId=${id(link?.id) || 'missing'}, preloadPlanId=${id(link?.preloadPlanId) || 'missing'}`, OPTIONS)
|
||||
if (!link?.id) return
|
||||
|
||||
const missingContainerPushResponse = await request(context, 'POST',
|
||||
`${smartLoadingBase}/plans/${loadingPlanId}/push-preload`,
|
||||
{ expectedVersion: Number(link.loadingPlanVersion) },
|
||||
{ 'Idempotency-Key': `${context.marker}-bridge-push-without-container-no`.slice(0, 128) })
|
||||
const missingContainerPushRejected = failure(missingContainerPushResponse)
|
||||
context.addStep(MODULE, '无真实柜号拒绝推送柜次计划', missingContainerPushResponse,
|
||||
missingContainerPushRejected,
|
||||
`HTTP=${missingContainerPushResponse.http}, code=${codeOf(missingContainerPushResponse)}, requestId=${missingContainerPushResponse.requestId || 'missing'}, traceId=${missingContainerPushResponse.traceId || 'missing'}`, OPTIONS)
|
||||
if (!missingContainerPushRejected) return
|
||||
|
||||
const missingContainerTaskResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const missingContainerTaskAbsent = failure(missingContainerTaskResponse)
|
||||
|| !id(missingContainerTaskResponse?.body?.id || missingContainerTaskResponse?.body?.data?.id)
|
||||
context.addStep(MODULE, '无真实柜号推送失败后不生成 PDA 任务', missingContainerTaskResponse,
|
||||
const missingContainerTaskAbsent = missingContainerTaskResponse.http === 200 && codeOf(missingContainerTaskResponse) === 'A0404'
|
||||
context.addStep(MODULE, '无号 push 成功后 PDA 查询明确 A0404', missingContainerTaskResponse,
|
||||
missingContainerTaskAbsent,
|
||||
`HTTP=${missingContainerTaskResponse.http}, code=${codeOf(missingContainerTaskResponse)}, task=${id(missingContainerTaskResponse?.body?.id || missingContainerTaskResponse?.body?.data?.id) || 'none'}`, OPTIONS)
|
||||
`HTTP=${missingContainerTaskResponse.http}, code=${codeOf(missingContainerTaskResponse)}, requestId=${missingContainerTaskResponse.requestId || 'missing'}, traceId=${missingContainerTaskResponse.traceId || 'missing'}`, OPTIONS)
|
||||
if (!missingContainerTaskAbsent) return
|
||||
|
||||
const containerBeforeTransportResponse = await context.request('GET',
|
||||
`/api/platform/v1/containers/${containerBookingId}`)
|
||||
const containerBeforeTransport = context.apiSuccess(MODULE, '读取补真实柜号前柜次最新版本',
|
||||
containerBeforeTransportResponse, '', OPTIONS)
|
||||
const transportVersion = Number(containerBeforeTransport?.version)
|
||||
context.assert(MODULE, '补真实柜号使用订舱关联后的最新柜次版本', containerBeforeTransportResponse,
|
||||
id(containerBeforeTransport?.id) === containerBookingId && Number.isInteger(transportVersion),
|
||||
`containerBookingId=${id(containerBeforeTransport?.id) || 'missing'}, version=${containerBeforeTransport?.version ?? 'missing'}`, OPTIONS)
|
||||
if (!Number.isInteger(transportVersion)) return
|
||||
const replayBaseline = JSON.stringify({
|
||||
plan: pushedLoadingPlan, detail: pushedDetail, links: pushedLinks,
|
||||
task: { http: missingContainerTaskResponse.http, code: codeOf(missingContainerTaskResponse) },
|
||||
})
|
||||
const pushReplayResponse = await request(context, 'POST', pushRequest.path, pushRequest.body, pushRequest.headers)
|
||||
context.apiSuccess(MODULE, 'push exact replay 返回首次结果', pushReplayResponse, '', OPTIONS)
|
||||
const replayPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
||||
const replayPlan = context.apiSuccess(MODULE, '重放后回查柜次计划', replayPlanResponse, '', OPTIONS)
|
||||
const replayDetailResponse = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const replayDetail = context.apiSuccess(MODULE, '重放后回查预装历史', replayDetailResponse, '', OPTIONS)
|
||||
const replayLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
|
||||
const replayLinks = context.apiSuccess(MODULE, '重放后回查 bridge', replayLinksResponse, '', OPTIONS)
|
||||
const replayTaskResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const replaySnapshot = JSON.stringify({
|
||||
plan: replayPlan, detail: replayDetail, links: replayLinks,
|
||||
task: { http: replayTaskResponse.http, code: codeOf(replayTaskResponse) },
|
||||
})
|
||||
context.assert(MODULE, 'push exact replay 对计划版本历史关联和任务零漂移', pushReplayResponse,
|
||||
replayBaseline === replaySnapshot && codeOf(replayTaskResponse) === 'A0404',
|
||||
`baselineBytes=${replayBaseline.length}, replayBytes=${replaySnapshot.length}`, OPTIONS)
|
||||
|
||||
const sameKeyDifferentBody = await request(context, 'POST', pushRequest.path,
|
||||
{ expectedVersion: Number(pushedLoadingPlan.version) }, pushRequest.headers)
|
||||
context.addStep(MODULE, 'push 同 key 异参返回冲突', sameKeyDifferentBody,
|
||||
failure(sameKeyDifferentBody), `HTTP=${sameKeyDifferentBody.http}, code=${codeOf(sameKeyDifferentBody)}`, OPTIONS)
|
||||
const newKeyDuplicate = await request(context, 'POST', pushRequest.path, pushRequest.body,
|
||||
{ 'Idempotency-Key': `${context.marker}-bridge-push-duplicate`.slice(0, 128) })
|
||||
context.addStep(MODULE, '已推送计划使用新 key 重复 push 返回冲突', newKeyDuplicate,
|
||||
failure(newKeyDuplicate), `HTTP=${newKeyDuplicate.http}, code=${codeOf(newKeyDuplicate)}`, OPTIONS)
|
||||
|
||||
const rejectedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
||||
const rejectedPlan = context.apiSuccess(MODULE, '非法 push 后回查柜次计划', rejectedPlanResponse, '', OPTIONS)
|
||||
const rejectedDetailResponse = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const rejectedDetail = context.apiSuccess(MODULE, '非法 push 后回查预装历史', rejectedDetailResponse, '', OPTIONS)
|
||||
const rejectedLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
|
||||
const rejectedLinks = context.apiSuccess(MODULE, '非法 push 后回查 bridge', rejectedLinksResponse, '', OPTIONS)
|
||||
const rejectedTaskResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const rejectedSnapshot = JSON.stringify({
|
||||
plan: rejectedPlan, detail: rejectedDetail, links: rejectedLinks,
|
||||
task: { http: rejectedTaskResponse.http, code: codeOf(rejectedTaskResponse) },
|
||||
})
|
||||
context.assert(MODULE, '两类非法 push 后计划版本历史关联和任务仍零漂移', rejectedTaskResponse,
|
||||
failure(sameKeyDifferentBody) && failure(newKeyDuplicate)
|
||||
&& replayBaseline === rejectedSnapshot && codeOf(rejectedTaskResponse) === 'A0404',
|
||||
`baselineBytes=${replayBaseline.length}, rejectedBytes=${rejectedSnapshot.length}`, OPTIONS)
|
||||
if (replayBaseline !== rejectedSnapshot || codeOf(rejectedTaskResponse) !== 'A0404') return
|
||||
|
||||
const transportResponse = await request(context, 'PATCH',
|
||||
`/api/platform/v1/containers/${containerBookingId}/transport`, {
|
||||
containerNo, voyageNo: `P3V-${suffix}`, version: transportVersion,
|
||||
reason: `${context.marker}-bind-container-no`,
|
||||
})
|
||||
const transported = context.apiSuccess(MODULE, '绑定真实柜号后开放三期 PDA 推送门禁', transportResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '真实柜号绑定到稳定柜次且计划身份不变', transportResponse,
|
||||
transported?.containerNo === containerNo && id(transported?.id) === containerBookingId
|
||||
&& transported?.cabinetNo === linked.cabinetNo,
|
||||
`containerNo=${transported?.containerNo || 'missing'}, containerBookingId=${id(transported?.id) || 'missing'}, loadingPlanId=${loadingPlanId}`, OPTIONS)
|
||||
if (transported?.containerNo !== containerNo) return
|
||||
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}/transport?warehouseId=${encodeURIComponent(warehouseId)}`, {
|
||||
containerNo, voyageNo: `P3V-${suffix}`, expectedVersion: Number(pushedLoadingPlan.version),
|
||||
operationReason: `${context.marker}-bind-container-no`,
|
||||
}, { 'Idempotency-Key': `${context.marker}-preload-transport`.slice(0, 128) })
|
||||
const transported = context.apiSuccess(MODULE, '预装 EDITING 清单绑定真实柜号', transportResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '预装补号保持 loadingPlanId/containerBookingId/cabinetNo 身份稳定', transportResponse,
|
||||
transported?.summary?.containerNo === containerNo && id(transported?.summary?.planId) === loadingPlanId
|
||||
&& id(transported?.summary?.containerBookingId) === containerBookingId,
|
||||
`containerNo=${transported?.summary?.containerNo || 'missing'}, loadingPlanId=${id(transported?.summary?.planId) || 'missing'}`, OPTIONS)
|
||||
if (transported?.summary?.containerNo !== containerNo) return
|
||||
|
||||
const unlinkedTaskResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
context.addStep(MODULE, '已补号但未关联订舱仍无 PDA 任务', unlinkedTaskResponse,
|
||||
unlinkedTaskResponse.http === 200 && codeOf(unlinkedTaskResponse) === 'A0404',
|
||||
`HTTP=${unlinkedTaskResponse.http}, code=${codeOf(unlinkedTaskResponse)}`, OPTIONS)
|
||||
|
||||
const bookingCreateResponse = await request(context, 'POST', '/api/platform/v1/ocean-bookings',
|
||||
{ expectedVersion: 0, data: bookingDraft },
|
||||
{ 'Idempotency-Key': `${context.marker}-p3-booking-create`.slice(0, 128) })
|
||||
const bookingConfirmed = context.apiSuccess(MODULE, '创建带渠道仓库快照的三期 CONFIRMED 订舱', bookingCreateResponse, '', OPTIONS)
|
||||
const bookingId = id(bookingConfirmed?.oceanBooking?.id)
|
||||
const transportFields = ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']
|
||||
const frozenTransport = Object.fromEntries(transportFields.map((field) => [field, bookingConfirmed?.oceanBooking?.[field]]))
|
||||
const frozenTransportValid = transportFields.every((field) => frozenTransport[field] != null && String(frozenTransport[field]).trim() !== '')
|
||||
const frozenTransportMatches = frozenTransportValid
|
||||
&& transportFields.every((field) => String(frozenTransport[field]) === String(bookingDraft[field]))
|
||||
const requirement = bookingConfirmed?.containerRequirements?.find?.((item) => item.containerType === '40HQ')
|
||||
context.assert(MODULE, 'CONFIRMED 订舱回显完整且冻结五项运输快照', bookingCreateResponse,
|
||||
Boolean(bookingId) && frozenTransportMatches && Boolean(requirement?.id)
|
||||
&& bookingConfirmed?.containerAllocations?.length === 0,
|
||||
`bookingId=${bookingId || 'missing'}, status=${bookingConfirmed?.oceanBooking?.status || 'missing'}`, OPTIONS)
|
||||
if (!bookingId || !requirement?.id || !frozenTransportMatches) return
|
||||
const latestContainerResponse = await context.request('GET', `/api/platform/v1/containers/${containerBookingId}`)
|
||||
const latestContainer = context.apiSuccess(MODULE, '补号后读取模拟柜最新版本用于订舱关联', latestContainerResponse, '', OPTIONS)
|
||||
const linkResponse = await request(context, 'POST', `/api/platform/v1/containers/${containerBookingId}/ocean-booking-link`, {
|
||||
oceanBookingId: bookingId, requirementId: id(requirement.id), expectedVersion: Number(latestContainer?.version),
|
||||
})
|
||||
const linked = context.apiSuccess(MODULE, '预装补号后关联三期 CONFIRMED 订舱', linkResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '订舱关联保持柜次计划与真实柜号身份', linkResponse,
|
||||
id(linked?.id) === containerBookingId && linked?.cabinetNo === simulation.cabinetNo
|
||||
&& linked?.containerNo === containerNo && id(linked?.oceanBookingId) === bookingId,
|
||||
`containerBookingId=${containerBookingId}, loadingPlanId=${loadingPlanId}`, OPTIONS)
|
||||
if (id(linked?.id) !== containerBookingId) return
|
||||
|
||||
const confirmedBookingTaskResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const confirmedBookingTaskAbsent = failure(confirmedBookingTaskResponse)
|
||||
|| !id(confirmedBookingTaskResponse?.body?.id || confirmedBookingTaskResponse?.body?.data?.id)
|
||||
context.addStep(MODULE, '订舱 CONFIRMED 即使已绑定真实柜号也不生成 PDA 任务', confirmedBookingTaskResponse,
|
||||
confirmedBookingTaskAbsent,
|
||||
`HTTP=${confirmedBookingTaskResponse.http}, code=${codeOf(confirmedBookingTaskResponse)}, task=${id(confirmedBookingTaskResponse?.body?.id || confirmedBookingTaskResponse?.body?.data?.id) || 'none'}`, OPTIONS)
|
||||
if (!confirmedBookingTaskAbsent) return
|
||||
context.addStep(MODULE, '订舱已关联但未 RELEASED 仍无 PDA 任务', confirmedBookingTaskResponse,
|
||||
confirmedBookingTaskResponse.http === 200 && codeOf(confirmedBookingTaskResponse) === 'A0404',
|
||||
`HTTP=${confirmedBookingTaskResponse.http}, code=${codeOf(confirmedBookingTaskResponse)}`, OPTIONS)
|
||||
|
||||
const releaseResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`, {
|
||||
expectedVersion: Number(bookingConfirmed?.oceanBooking?.version),
|
||||
@@ -301,24 +325,66 @@ export async function run(context) {
|
||||
bookingReleased?.oceanBooking?.status === 'RELEASED'
|
||||
&& bookingReleased?.containerAllocations?.some?.((item) => id(item.containerBookingId) === containerBookingId)
|
||||
&& transportFields.every((field) => String(bookingReleased?.oceanBooking?.[field]) === String(frozenTransport[field])),
|
||||
`status=${bookingReleased?.oceanBooking?.status || 'missing'}, containerBookingId=${containerBookingId}, loadingPlanId=${loadingPlanId}, transportFrozen=${transportFields.every((field) => String(bookingReleased?.oceanBooking?.[field]) === String(frozenTransport[field]))}`, OPTIONS)
|
||||
`status=${bookingReleased?.oceanBooking?.status || 'missing'}, containerBookingId=${containerBookingId}`, OPTIONS)
|
||||
if (bookingReleased?.oceanBooking?.status !== 'RELEASED') return
|
||||
|
||||
const pushResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/push-preload`,
|
||||
{ expectedVersion: Number(link.loadingPlanVersion) },
|
||||
{ 'Idempotency-Key': `${context.marker}-bridge-push`.slice(0, 128) })
|
||||
const pushedLoadingPlan = context.apiSuccess(MODULE, '真实柜号与 RELEASED 订舱就绪后推送并生成 PDA 任务', pushResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '推送后柜次计划保持 EDITING 且版本递增', pushResponse,
|
||||
pushedLoadingPlan?.status === 'EDITING'
|
||||
&& Number(pushedLoadingPlan?.version) === Number(link.loadingPlanVersion) + 1,
|
||||
`status=${pushedLoadingPlan?.status || 'missing'}, version=${pushedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
const unconfirmedTaskResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
context.addStep(MODULE, '订舱 RELEASED 但预装未 CONFIRMED 仍无 PDA 任务', unconfirmedTaskResponse,
|
||||
unconfirmedTaskResponse.http === 200 && codeOf(unconfirmedTaskResponse) === 'A0404',
|
||||
`HTTP=${unconfirmedTaskResponse.http}, code=${codeOf(unconfirmedTaskResponse)}`, OPTIONS)
|
||||
|
||||
const blockedBeforePreloadConfirm = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`,
|
||||
{ expectedVersion: initialVersion, remark: 'must confirm preload first' },
|
||||
{ 'Idempotency-Key': `${context.marker}-p3-complete-before-preload-confirm`.slice(0, 128) })
|
||||
context.addStep(MODULE, '预装未确认时拒绝完成装柜', blockedBeforePreloadConfirm,
|
||||
failure(blockedBeforePreloadConfirm),
|
||||
`HTTP=${blockedBeforePreloadConfirm.http}, code=${codeOf(blockedBeforePreloadConfirm)}`, OPTIONS)
|
||||
const confirmableResponse = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const confirmable = context.apiSuccess(MODULE, '计划级确认前读取即时版本与就绪门禁', confirmableResponse, '', OPTIONS)
|
||||
const confirmableVersion = Number(confirmable?.summary?.version)
|
||||
context.assert(MODULE, '补号关联放舱后整份计划允许确认且 PDA 尚未创建', confirmableResponse,
|
||||
Number.isInteger(confirmableVersion) && confirmable?.summary?.status === 'EDITING'
|
||||
&& confirmable?.summary?.readiness?.canConfirm === true
|
||||
&& confirmable?.summary?.readiness?.readyForPda === false
|
||||
&& confirmable?.summary?.readiness?.pdaTaskId == null,
|
||||
`version=${confirmable?.summary?.version ?? 'missing'}, canConfirm=${confirmable?.summary?.readiness?.canConfirm}, pdaTaskId=${id(confirmable?.summary?.readiness?.pdaTaskId) || 'none'}`, OPTIONS)
|
||||
if (!Number.isInteger(confirmableVersion) || confirmable?.summary?.readiness?.canConfirm !== true) return
|
||||
const confirmPath = `/api/platform/v1/first-leg/loading-plans/${loadingPlanId}/confirm`
|
||||
const confirmBody = Object.freeze({ expectedVersion: confirmableVersion })
|
||||
const confirmHeaders = Object.freeze({ 'Idempotency-Key': `${context.marker}-p3-plan-confirm`.slice(0, 128) })
|
||||
const confirm = await request(context, 'POST', confirmPath, confirmBody, confirmHeaders)
|
||||
const confirmed = context.apiSuccess(MODULE, '计划级原子确认整份预装并触发唯一 PDA 任务', confirm, '', OPTIONS)
|
||||
const confirmedTaskId = id(confirmed?.readiness?.pdaTaskId)
|
||||
context.assert(MODULE, '计划确认返回 CONFIRMED、readyForPda 与唯一 taskId', confirm,
|
||||
id(confirmed?.planId) === loadingPlanId && id(confirmed?.containerBookingId) === containerBookingId
|
||||
&& confirmed?.confirmationStatus === 'CONFIRMED'
|
||||
&& confirmed?.readiness?.confirmationStatus === 'CONFIRMED'
|
||||
&& confirmed?.readiness?.readyForPda === true && Boolean(confirmedTaskId)
|
||||
&& Number(confirmed?.version) > confirmableVersion,
|
||||
`confirmationStatus=${confirmed?.confirmationStatus || 'missing'}, readyForPda=${confirmed?.readiness?.readyForPda}, pdaTaskId=${confirmedTaskId || 'missing'}`, OPTIONS)
|
||||
if (!confirmedTaskId || confirmed?.confirmationStatus !== 'CONFIRMED') return
|
||||
const confirmReplay = await request(context, 'POST', confirmPath, confirmBody, confirmHeaders)
|
||||
context.assert(MODULE, '计划级确认同键同参精确重放同一 taskId 与版本', confirmReplay,
|
||||
confirmReplay.http === 200 && codeOf(confirmReplay) === '200'
|
||||
&& id(confirmReplay.body?.data?.readiness?.pdaTaskId) === confirmedTaskId
|
||||
&& Number(confirmReplay.body?.data?.version) === Number(confirmed.version),
|
||||
`HTTP=${confirmReplay.http}, code=${codeOf(confirmReplay)}, requestId=${confirmReplay.requestId || 'missing'}, traceId=${confirmReplay.traceId || 'missing'}`, OPTIONS)
|
||||
const confirmSameKeyDifferentBody = await request(context, 'POST', confirmPath,
|
||||
{ expectedVersion: Number(confirmed.version) }, confirmHeaders)
|
||||
context.addStep(MODULE, '计划级确认同 key 异参返回冲突', confirmSameKeyDifferentBody,
|
||||
confirmSameKeyDifferentBody.http === 200 && codeOf(confirmSameKeyDifferentBody) === 'A0409',
|
||||
`HTTP=${confirmSameKeyDifferentBody.http}, code=${codeOf(confirmSameKeyDifferentBody)}, requestId=${confirmSameKeyDifferentBody.requestId || 'missing'}, traceId=${confirmSameKeyDifferentBody.traceId || 'missing'}`, OPTIONS)
|
||||
const confirmNewKeyDuplicate = await request(context, 'POST', confirmPath, confirmBody,
|
||||
{ 'Idempotency-Key': `${context.marker}-p3-plan-confirm-duplicate`.slice(0, 128) })
|
||||
context.addStep(MODULE, '已确认计划使用新 key 重复确认返回冲突', confirmNewKeyDuplicate,
|
||||
confirmNewKeyDuplicate.http === 200 && codeOf(confirmNewKeyDuplicate) === 'A0409',
|
||||
`HTTP=${confirmNewKeyDuplicate.http}, code=${codeOf(confirmNewKeyDuplicate)}, requestId=${confirmNewKeyDuplicate.requestId || 'missing'}, traceId=${confirmNewKeyDuplicate.traceId || 'missing'}`, OPTIONS)
|
||||
|
||||
const confirmedInboundResponse = await context.request('GET', detailPath)
|
||||
const confirmedInbound = context.apiSuccess(MODULE, '计划确认后 claim 前回查入库状态', confirmedInboundResponse, '', OPTIONS)
|
||||
const confirmedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
||||
const confirmedPlan = context.apiSuccess(MODULE, '计划确认后 claim 前回查柜次计划', confirmedPlanResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '计划确认后 task 待领取且 claim 前仍 WAIT_SPLIT/EDITING', confirmedPlanResponse,
|
||||
confirmedInbound?.summary?.processStatus === 'WAIT_SPLIT' && confirmedPlan?.status === 'EDITING'
|
||||
&& Number(confirmedPlan?.version) === Number(confirmed.version),
|
||||
`inbound=${confirmedInbound?.summary?.processStatus || 'missing'}, plan=${confirmedPlan?.status || 'missing'}`, OPTIONS)
|
||||
if (confirmedInbound?.summary?.processStatus !== 'WAIT_SPLIT' || confirmedPlan?.status !== 'EDITING') return
|
||||
|
||||
const canonicalTaskResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
@@ -326,11 +392,17 @@ export async function run(context) {
|
||||
const canonicalTaskId = id(canonicalTask?.id)
|
||||
const canonicalItem = canonicalTask?.items?.find?.((item) => id(item.inboundId) === inboundId)
|
||||
context.assert(MODULE, '柜次 PDA 任务与桥接入库明细一致', canonicalTaskResponse,
|
||||
Boolean(canonicalTaskId) && id(canonicalTask?.loadingPlanId) === loadingPlanId
|
||||
Boolean(canonicalTaskId) && canonicalTaskId === confirmedTaskId && id(canonicalTask?.loadingPlanId) === loadingPlanId
|
||||
&& id(canonicalTask?.containerBookingId) === containerBookingId
|
||||
&& canonicalTask?.workflowStatus === 'UNASSIGNED' && Boolean(canonicalItem?.id),
|
||||
`taskId=${canonicalTaskId || 'missing'}, itemId=${id(canonicalItem?.id) || 'missing'}`, OPTIONS)
|
||||
if (!canonicalTaskId || !canonicalItem?.id) return
|
||||
|
||||
const beforeClaimPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
||||
const beforeClaimPlan = context.apiSuccess(MODULE, '领取前读取柜次计划即时版本', beforeClaimPlanResponse, '', OPTIONS)
|
||||
const beforeClaimPlanVersion = Number(beforeClaimPlan?.version)
|
||||
if (!Number.isInteger(beforeClaimPlanVersion)) return
|
||||
|
||||
const claim = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/claim`, {
|
||||
expectedAssignmentVersion: Number(canonicalTask?.assignment?.version || 0),
|
||||
operationId: `${context.marker}-bridge-claim`.slice(0, 128),
|
||||
@@ -343,7 +415,7 @@ export async function run(context) {
|
||||
const claimedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
||||
const claimedPlan = context.apiSuccess(MODULE, '领取后回查柜次计划', claimedPlanResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '领取 PDA 任务后计划进入 LOADING 且版本递增', claimedPlanResponse,
|
||||
claimedPlan?.status === 'LOADING' && Number(claimedPlan?.version) === Number(pushedLoadingPlan?.version) + 1,
|
||||
claimedPlan?.status === 'LOADING' && Number(claimedPlan?.version) > beforeClaimPlanVersion,
|
||||
`status=${claimedPlan?.status || 'missing'}, version=${claimedPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const inventoryResponse = await context.request('GET',
|
||||
@@ -607,34 +679,11 @@ export async function run(context) {
|
||||
directPdaCompletion && pickedPlan?.status === 'COMPLETED',
|
||||
`status=${pickedPlan?.status || 'missing'}, version=${pickedPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const confirming = await request(context, 'POST', `${base}/preload/confirming?tenantId=${encodeURIComponent(tenantId)}`,
|
||||
{ expectedVersion: Number(link.preloadVersion) }, { 'Idempotency-Key': `${context.marker}-p3-confirming`.slice(0, 128) })
|
||||
const confirmingData = context.apiSuccess(MODULE, '提交预装清单确认', confirming, '', OPTIONS)
|
||||
context.assert(MODULE, '预装清单进入 CONFIRMING 且版本递增', confirming,
|
||||
confirmingData?.preloadPlan?.status === 'CONFIRMING'
|
||||
&& Number(confirmingData?.preloadPlan?.version) > Number(link.preloadVersion),
|
||||
`status=${confirmingData?.preloadPlan?.status || 'missing'}, version=${confirmingData?.preloadPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
if (confirmingData?.preloadPlan?.status !== 'CONFIRMING') return
|
||||
|
||||
const confirm = await request(context, 'POST', `${base}/preload/confirm?tenantId=${encodeURIComponent(tenantId)}`,
|
||||
{ expectedVersion: Number(confirmingData.preloadPlan.version) }, { 'Idempotency-Key': `${context.marker}-p3-confirm`.slice(0, 128) })
|
||||
const confirmed = context.apiSuccess(MODULE, '确认预装并进入装柜中', confirm, '', OPTIONS)
|
||||
context.assert(MODULE, directPdaCompletion
|
||||
? '装柜已开始时预装确认保持 LOADING 和 CONFIRMING'
|
||||
: '预装确认推进主状态 LOADING 和计划 CONFIRMED', confirm,
|
||||
directPdaCompletion
|
||||
? confirmed?.mainStatus === 'LOADING' && confirmed?.preloadPlan?.status === 'CONFIRMING'
|
||||
&& Number(confirmed?.preloadPlan?.version) === Number(confirmingData.preloadPlan.version)
|
||||
: confirmed?.mainStatus === 'LOADING' && confirmed?.preloadPlan?.status === 'CONFIRMED'
|
||||
&& Number(confirmed?.preloadPlan?.version) > Number(confirmingData.preloadPlan.version),
|
||||
`mainStatus=${confirmed?.mainStatus || 'missing'}, planStatus=${confirmed?.preloadPlan?.status || 'missing'}, version=${confirmed?.preloadPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
if (confirmed?.mainStatus !== 'LOADING') return
|
||||
|
||||
const preload = await context.request('GET', `${base}/preload?tenantId=${encodeURIComponent(tenantId)}`)
|
||||
const preloadData = context.apiSuccess(MODULE, '回查已确认预装清单', preload, '', OPTIONS)
|
||||
context.assert(MODULE, '预装清单归属、明细和 marker 持久化', preload,
|
||||
context.assert(MODULE, 'PDA 完成后预装清单仍为 CONFIRMED 且归属稳定', preload,
|
||||
id(preloadData?.id) === planId && id(preloadData?.inboundId) === inboundId && id(preloadData?.tenantId) === tenantId
|
||||
&& preloadData?.items?.[0]?.containerCode === `P3-${suffix}` && preloadData?.remark === context.marker,
|
||||
&& preloadData?.status === 'CONFIRMED',
|
||||
`planId=${id(preloadData?.id) || 'missing'}, items=${preloadData?.items?.length ?? 'n/a'}`, OPTIONS)
|
||||
|
||||
const loadingResponse = await context.request('GET', detailPath)
|
||||
|
||||
Reference in New Issue
Block a user