fix: restore phase five development acceptance chain
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
|
||||
import { isPureStatusNoop, visibleLoadingHistories } from './loading-history-noop.mjs'
|
||||
import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs'
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
@@ -7,7 +5,7 @@ const MODULE = 'phase3-loading'
|
||||
const OPTIONS = { scenario: 'phase3-split-preload-loading', deploymentBlocker: true }
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
const MATSON_SUPPLIER_NAME = 'MATSON 开发联调船司'
|
||||
const MATSON_SUPPLIER_NAME = 'MATSON 开发联调订舱代理'
|
||||
const LATE_CONTAINER_GATE_ONLY = process.env.LATE_CONTAINER_GATE_ONLY === '1'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
@@ -168,7 +166,7 @@ export async function run(context) {
|
||||
const smartLoadingBase = '/api/platform/v1/first-leg/smart-loading'
|
||||
const pdaBase = '/api/platform/v1/first-leg/pda-loading'
|
||||
const supplierResponse = await context.request('GET',
|
||||
'/api/platform/v1/suppliers?tenantId=0&category=BOOKING&page=1&size=200')
|
||||
'/api/platform/v1/suppliers?tenantId=0&category=BOOKING&status=ACTIVE&carrierCode=MATSON&page=1&size=200')
|
||||
const supplierPage = context.apiSuccess(MODULE, '读取订舱代理与船司供应商事实源', supplierResponse, '', OPTIONS)
|
||||
const supplierRecords = Array.isArray(supplierPage?.records) ? supplierPage.records : []
|
||||
const supplier = await ensureMatsonSupplier(context, supplierRecords)
|
||||
@@ -223,10 +221,9 @@ export async function run(context) {
|
||||
if (!candidate?.inboundDetailId || !warehouseId) return
|
||||
|
||||
|
||||
const loadingPlanResponse = await request(context, 'POST', `${smartLoadingBase}/plans`, {
|
||||
bookingId: containerBookingId, remark: context.marker,
|
||||
})
|
||||
const loadingPlan = context.apiSuccess(MODULE, '创建柜次级智能装柜计划', loadingPlanResponse, '', OPTIONS)
|
||||
const loadingPlanResponse = await context.request('GET',
|
||||
`${smartLoadingBase}/plans/by-booking/${encodeURIComponent(containerBookingId)}`)
|
||||
const loadingPlan = context.apiSuccess(MODULE, '读取柜次创建时原子生成的智能装柜计划', loadingPlanResponse, '', OPTIONS)
|
||||
const loadingPlanId = id(loadingPlan?.id)
|
||||
if (!loadingPlanId) return
|
||||
const assignmentResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/assignments`, {
|
||||
@@ -310,119 +307,37 @@ export async function run(context) {
|
||||
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,
|
||||
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 pushRequest = Object.freeze({
|
||||
path: `${smartLoadingBase}/plans/${loadingPlanId}/push-preload`,
|
||||
body: Object.freeze({ expectedVersion: Number(persistedRemarkPlan.version) }),
|
||||
headers: Object.freeze({ 'Idempotency-Key': `${context.marker}-bridge-push`.slice(0, 128) }),
|
||||
})
|
||||
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',
|
||||
const synchronizedDetailResponse = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const pushedDetail = context.apiSuccess(MODULE, '无号推送后预装清单正式可见', pushedDetailResponse, '', OPTIONS)
|
||||
const pushedHistories = visibleLoadingHistories(pushedDetail?.histories)
|
||||
context.assert(MODULE, '首次 push 只保留一条 PUSH_PRELOAD 且无纯状态 no-op', pushedDetailResponse,
|
||||
pushedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1
|
||||
&& !(pushedDetail?.histories || []).some(isPureStatusNoop),
|
||||
`raw=${pushedDetail?.histories?.length ?? 'invalid'}, effective=${pushedHistories.length}, push=${pushedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length}`,
|
||||
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,
|
||||
const synchronizedDetail = context.apiSuccess(MODULE, '分配后预装清单正式可见', synchronizedDetailResponse, '', OPTIONS)
|
||||
const synchronizedLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
|
||||
const synchronizedLinks = context.apiSuccess(MODULE, '分配同步建立 bridge 关联', synchronizedLinksResponse, '', OPTIONS)
|
||||
const link = Array.isArray(synchronizedLinks) ? synchronizedLinks.find((item) => id(item.inboundId) === inboundId) : null
|
||||
context.assert(MODULE, '分配自动建立唯一预装关联且身份稳定', synchronizedLinksResponse,
|
||||
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 postPushPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
||||
const postPushPlan = context.apiSuccess(MODULE, '首次 push 后读取事务完成态柜次计划', postPushPlanResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '事务完成态计划版本与 bridge 一致且保持 EDITING', postPushPlanResponse,
|
||||
postPushPlan?.status === 'EDITING' && Number(postPushPlan?.version) === Number(link?.loadingPlanVersion)
|
||||
&& Number(postPushPlan?.version) > Number(assignedLoadingPlan.version),
|
||||
`pushResponseVersion=${pushedLoadingPlan?.version ?? 'missing'}, persistedVersion=${postPushPlan?.version ?? 'missing'}, bridgeVersion=${link?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
|
||||
if (postPushPlan?.status !== 'EDITING' || !Number.isInteger(Number(postPushPlan?.version))) return
|
||||
const synchronizedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
||||
const synchronizedPlan = context.apiSuccess(MODULE, '读取分配事务完成态柜次计划', synchronizedPlanResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '分配事务完成态计划与 bridge 一致且保持 EDITING', synchronizedPlanResponse,
|
||||
synchronizedPlan?.status === 'EDITING' && Number(synchronizedPlan?.version) === Number(link?.loadingPlanVersion)
|
||||
&& Number(synchronizedPlan?.version) === Number(persistedRemarkPlan.version),
|
||||
`persistedVersion=${synchronizedPlan?.version ?? 'missing'}, bridgeVersion=${link?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
|
||||
if (synchronizedPlan?.status !== 'EDITING' || !Number.isInteger(Number(synchronizedPlan?.version))) return
|
||||
const missingContainerTaskResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const missingContainerTaskAbsent = missingContainerTaskResponse.http === 200 && codeOf(missingContainerTaskResponse) === 'A0404'
|
||||
context.addStep(MODULE, '无号 push 成功后 PDA 查询明确 A0404', missingContainerTaskResponse,
|
||||
context.addStep(MODULE, '无真实柜号时 PDA 查询明确 A0404', missingContainerTaskResponse,
|
||||
missingContainerTaskAbsent,
|
||||
`HTTP=${missingContainerTaskResponse.http}, code=${codeOf(missingContainerTaskResponse)}, requestId=${missingContainerTaskResponse.requestId || 'missing'}, traceId=${missingContainerTaskResponse.traceId || 'missing'}`, OPTIONS)
|
||||
if (!missingContainerTaskAbsent) return
|
||||
|
||||
const replayBaseline = projectPushPreloadStability({
|
||||
plan: postPushPlan, 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 replayHistories = visibleLoadingHistories(replayDetail?.histories)
|
||||
context.assert(MODULE, 'push 同幂等请求重放历史零新增', replayDetailResponse,
|
||||
replayHistories.length === pushedHistories.length
|
||||
&& replayHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1,
|
||||
`before=${pushedHistories.length}, after=${replayHistories.length}, push=${replayHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length}`,
|
||||
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 = projectPushPreloadStability({
|
||||
plan: replayPlan, detail: replayDetail, links: replayLinks,
|
||||
task: { http: replayTaskResponse.http, code: codeOf(replayTaskResponse) },
|
||||
})
|
||||
const replayDiff = diffPushPreloadStability(replayBaseline, replaySnapshot)
|
||||
context.assert(MODULE, 'push exact replay 对计划版本历史关联和任务零漂移', pushReplayResponse,
|
||||
replayDiff.length === 0 && codeOf(replayTaskResponse) === 'A0404',
|
||||
`changed=${replayDiff.join('|') || 'none'}; replayTask=${responseEvidence(replayTaskResponse)}`, OPTIONS)
|
||||
|
||||
const sameKeyDifferentBody = await request(context, 'POST', pushRequest.path,
|
||||
{ expectedVersion: Number(postPushPlan.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 rejectedHistories = visibleLoadingHistories(rejectedDetail?.histories)
|
||||
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 = projectPushPreloadStability({
|
||||
plan: rejectedPlan, detail: rejectedDetail, links: rejectedLinks,
|
||||
task: { http: rejectedTaskResponse.http, code: codeOf(rejectedTaskResponse) },
|
||||
})
|
||||
const rejectedDiff = diffPushPreloadStability(replayBaseline, rejectedSnapshot)
|
||||
context.assert(MODULE, '两类非法 push 后计划版本历史关联和任务仍零漂移', rejectedTaskResponse,
|
||||
failure(sameKeyDifferentBody) && failure(newKeyDuplicate)
|
||||
&& rejectedDiff.length === 0 && codeOf(rejectedTaskResponse) === 'A0404'
|
||||
&& rejectedHistories.length === pushedHistories.length
|
||||
&& rejectedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1,
|
||||
`changed=${rejectedDiff.join('|') || 'none'}; sameKey=${responseEvidence(sameKeyDifferentBody)}; newKey=${responseEvidence(newKeyDuplicate)}; task=${responseEvidence(rejectedTaskResponse)}`, OPTIONS)
|
||||
if (rejectedDiff.length !== 0 || codeOf(rejectedTaskResponse) !== 'A0404') return
|
||||
|
||||
const unlinkedTaskResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
context.addStep(MODULE, '无号且未关联订舱仍无 PDA 任务', unlinkedTaskResponse,
|
||||
@@ -695,7 +610,7 @@ export async function run(context) {
|
||||
`taskVersion=${unchangedAfterInvalid?.version ?? 'missing'}, items=${taskItemsSignature(unchangedAfterInvalid)}, operations=${afterInvalidOperations?.length ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const merchantLogin = await context.request('POST', '/api/auth/login', {
|
||||
username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
|
||||
loginId: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
|
||||
}, '')
|
||||
const merchantToken = merchantLogin.http === 200 && codeOf(merchantLogin) === '200'
|
||||
? merchantLogin.body?.data?.accessToken : null
|
||||
|
||||
Reference in New Issue
Block a user