test: stabilize preload push replay assertions
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { publishPlatformLogisticsChannels } from './ocean-booking-fixture.mjs'
|
||||
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
|
||||
|
||||
const MODULE = 'phase3-container-loading'
|
||||
const OPTIONS = { scenario: 'phase3-container-smart-loading-pda', deploymentBlocker: true }
|
||||
@@ -258,14 +259,21 @@ export async function run(context) {
|
||||
Boolean(pushedLink?.id) && Number(pushedLink?.allocatedPackageCount) === 1,
|
||||
`linkId=${id(pushedLink?.id) || 'missing'}, preloadPlanId=${id(pushedLink?.preloadPlanId) || 'missing'}`, OPTIONS)
|
||||
if (!pushedLink?.id) return
|
||||
const postPushPlanResponse = await context.request('GET', `${SMART}/plans/${planId}`)
|
||||
const postPushPlan = context.apiSuccess(MODULE, '首次 push 后读取事务完成态柜次计划', postPushPlanResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '事务完成态计划版本与 bridge 一致且保持 EDITING', postPushPlanResponse,
|
||||
postPushPlan?.status === 'EDITING' && Number(postPushPlan?.version) === Number(pushedLink?.loadingPlanVersion)
|
||||
&& Number(postPushPlan?.version) > Number(assigned.version),
|
||||
`pushResponseVersion=${pushed?.version ?? 'missing'}, persistedVersion=${postPushPlan?.version ?? 'missing'}, bridgeVersion=${pushedLink?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
|
||||
if (postPushPlan?.status !== 'EDITING' || !Number.isInteger(Number(postPushPlan?.version))) return
|
||||
const pushedTaskResponse = await context.request('GET',
|
||||
`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
context.addStep(MODULE, '无号 push 成功后 PDA 查询 A0404', pushedTaskResponse,
|
||||
pushedTaskResponse.http === 200 && codeOf(pushedTaskResponse) === 'A0404',
|
||||
responseEvidence(pushedTaskResponse), OPTIONS)
|
||||
|
||||
const pushBaseline = JSON.stringify({
|
||||
plan: pushed, detail: pushedDetail, links: pushedLinks,
|
||||
const pushBaseline = projectPushPreloadStability({
|
||||
plan: postPushPlan, detail: pushedDetail, links: pushedLinks,
|
||||
task: { http: pushedTaskResponse.http, code: codeOf(pushedTaskResponse) },
|
||||
})
|
||||
const pushReplayResponse = await request(context, 'POST', pushRequest.path, pushRequest.body, pushRequest.headers)
|
||||
@@ -279,15 +287,16 @@ export async function run(context) {
|
||||
const replayLinks = context.apiSuccess(MODULE, '重放后回查 bridge', replayLinksResponse, '', OPTIONS)
|
||||
const replayTaskResponse = await context.request('GET',
|
||||
`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const pushReplaySnapshot = JSON.stringify({
|
||||
const pushReplaySnapshot = projectPushPreloadStability({
|
||||
plan: replayPlan, detail: replayDetail, links: replayLinks,
|
||||
task: { http: replayTaskResponse.http, code: codeOf(replayTaskResponse) },
|
||||
})
|
||||
const pushReplayDiff = diffPushPreloadStability(pushBaseline, pushReplaySnapshot)
|
||||
context.assert(MODULE, 'push exact replay 对计划版本历史关联任务零漂移', pushReplayResponse,
|
||||
pushBaseline === pushReplaySnapshot && codeOf(replayTaskResponse) === 'A0404',
|
||||
`baselineBytes=${pushBaseline.length}, replayBytes=${pushReplaySnapshot.length}`, OPTIONS)
|
||||
pushReplayDiff.length === 0 && codeOf(replayTaskResponse) === 'A0404',
|
||||
`changed=${pushReplayDiff.join('|') || 'none'}; replayTask=${responseEvidence(replayTaskResponse)}`, OPTIONS)
|
||||
const pushSameKeyDifferentBody = await request(context, 'POST', pushRequest.path,
|
||||
{ expectedVersion: Number(pushed.version) }, pushRequest.headers)
|
||||
{ expectedVersion: Number(postPushPlan.version) }, pushRequest.headers)
|
||||
context.addStep(MODULE, 'push 同 key 异参拒绝', pushSameKeyDifferentBody,
|
||||
failure(pushSameKeyDifferentBody), responseEvidence(pushSameKeyDifferentBody), OPTIONS)
|
||||
const pushNewKeyDuplicate = await request(context, 'POST', pushRequest.path, pushRequest.body,
|
||||
@@ -304,21 +313,22 @@ export async function run(context) {
|
||||
const rejectedLinks = context.apiSuccess(MODULE, '非法 push 后回查 bridge', rejectedLinksResponse, '', OPTIONS)
|
||||
const rejectedTaskResponse = await context.request('GET',
|
||||
`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const rejectedSnapshot = JSON.stringify({
|
||||
const rejectedSnapshot = projectPushPreloadStability({
|
||||
plan: rejectedPlan, detail: rejectedDetail, links: rejectedLinks,
|
||||
task: { http: rejectedTaskResponse.http, code: codeOf(rejectedTaskResponse) },
|
||||
})
|
||||
const rejectedDiff = diffPushPreloadStability(pushBaseline, rejectedSnapshot)
|
||||
context.assert(MODULE, '两类非法 push 后计划版本历史关联任务仍零漂移', rejectedTaskResponse,
|
||||
failure(pushSameKeyDifferentBody) && failure(pushNewKeyDuplicate)
|
||||
&& pushBaseline === rejectedSnapshot && codeOf(rejectedTaskResponse) === 'A0404',
|
||||
`baselineBytes=${pushBaseline.length}, rejectedBytes=${rejectedSnapshot.length}`, OPTIONS)
|
||||
if (pushBaseline !== rejectedSnapshot || codeOf(rejectedTaskResponse) !== 'A0404') return
|
||||
&& rejectedDiff.length === 0 && codeOf(rejectedTaskResponse) === 'A0404',
|
||||
`changed=${rejectedDiff.join('|') || 'none'}; sameKey=${responseEvidence(pushSameKeyDifferentBody)}; newKey=${responseEvidence(pushNewKeyDuplicate)}; task=${responseEvidence(rejectedTaskResponse)}`, OPTIONS)
|
||||
if (rejectedDiff.length !== 0 || codeOf(rejectedTaskResponse) !== 'A0404') return
|
||||
|
||||
const realContainerNo = `TST-${suffix}`.slice(0, 64)
|
||||
const transportResponse = await request(context, 'PATCH',
|
||||
`${FIRST_LEG}/loading-plans/${planId}/transport?warehouseId=${encodeURIComponent(warehouseId)}`, {
|
||||
containerNo: realContainerNo, voyageNo: `V-${suffix}`.slice(0, 64),
|
||||
expectedVersion: Number(pushed.version), operationReason: context.marker,
|
||||
expectedVersion: Number(postPushPlan.version), operationReason: context.marker,
|
||||
}, { 'Idempotency-Key': `${context.marker}-preload-bind-container-no`.slice(0, 128) })
|
||||
const transported = context.apiSuccess(MODULE, '预装 EDITING 清单绑定本轮真实柜号', transportResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '预装补号保持柜次和计划身份稳定', transportResponse,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
|
||||
|
||||
const MODULE = 'phase3-loading'
|
||||
const OPTIONS = { scenario: 'phase3-split-preload-loading', deploymentBlocker: true }
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
@@ -5,6 +7,9 @@ const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
||||
function responseEvidence(response) {
|
||||
return `HTTP=${response?.http ?? 'missing'}, code=${codeOf(response)}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`
|
||||
}
|
||||
function failure(response) {
|
||||
return [400, 401, 403, 404, 409, 503].includes(response?.http)
|
||||
|| (response?.http === 200 && codeOf(response) !== '200')
|
||||
@@ -205,6 +210,13 @@ export async function run(context) {
|
||||
&& 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 missingContainerTaskResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const missingContainerTaskAbsent = missingContainerTaskResponse.http === 200 && codeOf(missingContainerTaskResponse) === 'A0404'
|
||||
@@ -213,8 +225,8 @@ export async function run(context) {
|
||||
`HTTP=${missingContainerTaskResponse.http}, code=${codeOf(missingContainerTaskResponse)}, requestId=${missingContainerTaskResponse.requestId || 'missing'}, traceId=${missingContainerTaskResponse.traceId || 'missing'}`, OPTIONS)
|
||||
if (!missingContainerTaskAbsent) return
|
||||
|
||||
const replayBaseline = JSON.stringify({
|
||||
plan: pushedLoadingPlan, detail: pushedDetail, links: pushedLinks,
|
||||
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)
|
||||
@@ -228,16 +240,17 @@ export async function run(context) {
|
||||
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({
|
||||
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,
|
||||
replayBaseline === replaySnapshot && codeOf(replayTaskResponse) === 'A0404',
|
||||
`baselineBytes=${replayBaseline.length}, replayBytes=${replaySnapshot.length}`, OPTIONS)
|
||||
replayDiff.length === 0 && codeOf(replayTaskResponse) === 'A0404',
|
||||
`changed=${replayDiff.join('|') || 'none'}; replayTask=${responseEvidence(replayTaskResponse)}`, OPTIONS)
|
||||
|
||||
const sameKeyDifferentBody = await request(context, 'POST', pushRequest.path,
|
||||
{ expectedVersion: Number(pushedLoadingPlan.version) }, pushRequest.headers)
|
||||
{ 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,
|
||||
@@ -254,19 +267,20 @@ export async function run(context) {
|
||||
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({
|
||||
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)
|
||||
&& replayBaseline === rejectedSnapshot && codeOf(rejectedTaskResponse) === 'A0404',
|
||||
`baselineBytes=${replayBaseline.length}, rejectedBytes=${rejectedSnapshot.length}`, OPTIONS)
|
||||
if (replayBaseline !== rejectedSnapshot || codeOf(rejectedTaskResponse) !== 'A0404') return
|
||||
&& rejectedDiff.length === 0 && codeOf(rejectedTaskResponse) === 'A0404',
|
||||
`changed=${rejectedDiff.join('|') || 'none'}; sameKey=${responseEvidence(sameKeyDifferentBody)}; newKey=${responseEvidence(newKeyDuplicate)}; task=${responseEvidence(rejectedTaskResponse)}`, OPTIONS)
|
||||
if (rejectedDiff.length !== 0 || codeOf(rejectedTaskResponse) !== 'A0404') return
|
||||
|
||||
const transportResponse = await request(context, 'PATCH',
|
||||
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}/transport?warehouseId=${encodeURIComponent(warehouseId)}`, {
|
||||
containerNo, voyageNo: `P3V-${suffix}`, expectedVersion: Number(pushedLoadingPlan.version),
|
||||
containerNo, voyageNo: `P3V-${suffix}`, expectedVersion: Number(postPushPlan.version),
|
||||
operationReason: `${context.marker}-bind-container-no`,
|
||||
}, { 'Idempotency-Key': `${context.marker}-preload-transport`.slice(0, 128) })
|
||||
const transported = context.apiSuccess(MODULE, '预装 EDITING 清单绑定真实柜号', transportResponse, '', OPTIONS)
|
||||
|
||||
@@ -5,6 +5,7 @@ import test from 'node:test'
|
||||
import { selectCustomsFlow } from './phase3-customs.mjs'
|
||||
import { buildFinalizeResource } from './phase3-loading-finalize.mjs'
|
||||
import { selectContainerLoadingFixture, selectPlatformChannelIsolationPair } from './phase3-container-loading.mjs'
|
||||
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
|
||||
import { AcceptanceRun, normalizeModuleOrder, parseArguments } from '../admin-test-env-acceptance.mjs'
|
||||
|
||||
function source(name) {
|
||||
@@ -132,9 +133,53 @@ test('push replay and both conflict modes preserve the complete pre-PDA snapshot
|
||||
assert.match(value, /非法 push 后回查柜次计划[\s\S]*非法 push 后回查预装历史[\s\S]*非法 push 后回查 bridge/)
|
||||
assert.match(value, /两类非法 push 后计划版本历史关联(?:和)?任务仍?零漂移/)
|
||||
assert.match(value, /codeOf\(rejectedTaskResponse\) === 'A0404'/)
|
||||
assert.match(value, /projectPushPreloadStability/)
|
||||
assert.match(value, /diffPushPreloadStability/)
|
||||
assert.match(value, /首次 push 后读取事务完成态柜次计划/)
|
||||
assert.match(value, /事务完成态计划版本与 bridge 一致且保持 EDITING/)
|
||||
assert.match(value, /plan: postPushPlan, detail:/)
|
||||
assert.match(value, /expectedVersion: Number\(postPushPlan\.version\)/)
|
||||
assert.match(value, /changed=\$\{(?:pushReplayDiff|replayDiff|rejectedDiff)\.join/)
|
||||
assert.doesNotMatch(value, /baselineBytes=|replayBytes=|rejectedBytes=/)
|
||||
}
|
||||
})
|
||||
|
||||
test('push stability projection sorts unordered facts and reports persisted field drift', () => {
|
||||
const baseline = projectPushPreloadStability({
|
||||
plan: { id: '10', bookingId: '20', status: 'EDITING', version: 2,
|
||||
items: [{ id: '2', inboundId: '42', packageCount: 1, version: 1 },
|
||||
{ id: '1', inboundId: '41', packageCount: 1, version: 1 }] },
|
||||
detail: { summary: { planId: '10', containerBookingId: '20', status: 'EDITING', version: 2,
|
||||
readiness: { preloadPushed: true, preloadLinkCount: 2, confirmationStatus: 'PENDING',
|
||||
blockingReasons: ['CONTAINER_NO_MISSING', 'OCEAN_BOOKING_NOT_LINKED'] } },
|
||||
items: [{ itemId: '2', inboundId: '42', packageCount: 1 }, { itemId: '1', inboundId: '41', packageCount: 1 }],
|
||||
histories: [{ historyId: '2', actionType: 'PUSH' }, { historyId: '1', actionType: 'CREATE' }] },
|
||||
links: [{ id: '2', inboundId: '42', loadingPlanId: '10', allocatedPackageCount: 1 },
|
||||
{ id: '1', inboundId: '41', loadingPlanId: '10', allocatedPackageCount: 1 }],
|
||||
task: { http: 200, code: 'A0404' },
|
||||
})
|
||||
const reordered = projectPushPreloadStability({
|
||||
plan: { id: '10', bookingId: '20', status: 'EDITING', version: 2,
|
||||
items: [{ id: '1', inboundId: '41', packageCount: 1, version: 1 },
|
||||
{ id: '2', inboundId: '42', packageCount: 1, version: 1 }], updatedAt: 'ignored' },
|
||||
detail: { summary: { planId: '10', containerBookingId: '20', status: 'EDITING', version: 2,
|
||||
readiness: { preloadPushed: true, preloadLinkCount: 2, confirmationStatus: 'PENDING',
|
||||
blockingReasons: ['OCEAN_BOOKING_NOT_LINKED', 'CONTAINER_NO_MISSING'] } },
|
||||
items: [{ itemId: '1', inboundId: '41', packageCount: 1 }, { itemId: '2', inboundId: '42', packageCount: 1 }],
|
||||
histories: [{ historyId: '1', actionType: 'CREATE' }, { historyId: '2', actionType: 'PUSH' }] },
|
||||
links: [{ id: '1', inboundId: '41', loadingPlanId: '10', allocatedPackageCount: 1 },
|
||||
{ id: '2', inboundId: '42', loadingPlanId: '10', allocatedPackageCount: 1 }],
|
||||
task: { http: 200, code: 'A0404' },
|
||||
})
|
||||
assert.deepEqual(diffPushPreloadStability(baseline, reordered), [])
|
||||
const drifted = structuredClone(reordered)
|
||||
drifted.plan.version = 3
|
||||
drifted.links[0].preloadVersion = 2
|
||||
assert.deepEqual(diffPushPreloadStability(baseline, drifted), [
|
||||
'links[0].preloadVersion:null->2', 'plan.version:2->3',
|
||||
])
|
||||
})
|
||||
|
||||
test('push orchestration is transactional and a bridge failure cannot reach PDA creation', () => {
|
||||
const service = source('../../firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java')
|
||||
const serviceTest = source('../../firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java')
|
||||
@@ -212,7 +257,7 @@ test('container loading arranges a simulation before linking a confirmed real bo
|
||||
}
|
||||
assert.doesNotMatch(value, /loading-booking-(?:submit|confirm)|ocean-bookings\/.*\/(?:submit|confirm)/)
|
||||
assert.match(value, /containerAllocations\.length === 0/)
|
||||
assert.match(value, /expectedVersion: Number\(pushed\.version\), operationReason: context\.marker/)
|
||||
assert.match(value, /expectedVersion: Number\(postPushPlan\.version\), operationReason: context\.marker/)
|
||||
assert.match(value, /function responseEvidence\(response\)[\s\S]*HTTP=\$\{response\?\.http \?\? 'missing'\}[\s\S]*code=\$\{codeOf\(response\)\}[\s\S]*requestId=\$\{response\?\.requestId \|\| 'missing'\}[\s\S]*traceId=\$\{response\?\.traceId \|\| 'missing'\}/)
|
||||
assert.match(value, /emptySimulation = context\.apiSuccess\(MODULE, '空模拟柜首次关联真实订舱', emptyLink,\s+responseEvidence\(emptyLink\), OPTIONS\)\s+if \(!emptySimulation\) return\s+const emptyUnlink = await request/)
|
||||
assert.match(value, /emptySimulation = context\.apiSuccess\(MODULE, '空模拟柜允许解除关联', emptyUnlink,\s+responseEvidence\(emptyUnlink\), OPTIONS\)\s+if \(!emptySimulation\) return\s+const emptyRelink = await request/)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
function id(value) { return value == null ? null : String(value) }
|
||||
function number(value) { return value == null || Number.isNaN(Number(value)) ? null : Number(value) }
|
||||
function text(value) { return value == null ? null : String(value) }
|
||||
function bool(value) { return value === true }
|
||||
|
||||
function sorted(values, key) {
|
||||
return [...(Array.isArray(values) ? values : [])]
|
||||
.sort((left, right) => key(left).localeCompare(key(right)))
|
||||
}
|
||||
|
||||
function planItem(item) {
|
||||
return {
|
||||
id: id(item?.id), loadingPlanId: id(item?.loadingPlanId), inboundId: id(item?.inboundId),
|
||||
inboundDetailId: id(item?.inboundDetailId), accountId: id(item?.accountId), orderId: id(item?.orderId),
|
||||
packageCount: number(item?.packageCount), loadSequence: number(item?.loadSequence),
|
||||
frozenAt: text(item?.frozenAt), version: number(item?.version), deleted: bool(item?.deleted),
|
||||
}
|
||||
}
|
||||
|
||||
function detailItem(item) {
|
||||
return {
|
||||
itemId: id(item?.itemId), accountId: id(item?.accountId), inboundId: id(item?.inboundId),
|
||||
inboundDetailId: id(item?.inboundDetailId), packageCount: number(item?.packageCount),
|
||||
palletCount: number(item?.palletCount), loadSequence: number(item?.loadSequence),
|
||||
frozenAt: text(item?.frozenAt),
|
||||
}
|
||||
}
|
||||
|
||||
function readiness(value) {
|
||||
return {
|
||||
preloadPushed: bool(value?.preloadPushed), containerNoReady: bool(value?.containerNoReady),
|
||||
preloadLinkCount: number(value?.preloadLinkCount), confirmedPreloadCount: number(value?.confirmedPreloadCount),
|
||||
oceanBookingId: id(value?.oceanBookingId), oceanBookingStatus: text(value?.oceanBookingStatus),
|
||||
pdaTaskId: id(value?.pdaTaskId), pdaTaskStatus: text(value?.pdaTaskStatus),
|
||||
confirmationStatus: text(value?.confirmationStatus), canConfirm: bool(value?.canConfirm),
|
||||
readyForPda: bool(value?.readyForPda),
|
||||
blockingReasons: sorted(value?.blockingReasons, (item) => text(item) || '').map(text),
|
||||
}
|
||||
}
|
||||
|
||||
function history(value) {
|
||||
return {
|
||||
historyId: id(value?.historyId), itemId: id(value?.itemId), actionType: text(value?.actionType),
|
||||
fromStatus: text(value?.fromStatus), toStatus: text(value?.toStatus),
|
||||
beforeSnapshot: text(value?.beforeSnapshot), afterSnapshot: text(value?.afterSnapshot),
|
||||
operationReason: text(value?.operationReason), occurredAt: text(value?.occurredAt),
|
||||
}
|
||||
}
|
||||
|
||||
function link(value) {
|
||||
return {
|
||||
id: id(value?.id), tenantId: id(value?.tenantId), inboundId: id(value?.inboundId),
|
||||
preloadPlanId: id(value?.preloadPlanId), preloadStatus: text(value?.preloadStatus),
|
||||
preloadVersion: number(value?.preloadVersion), loadingPlanId: id(value?.loadingPlanId),
|
||||
containerBookingId: id(value?.containerBookingId), containerNo: text(value?.containerNo),
|
||||
loadingPlanStatus: text(value?.loadingPlanStatus), loadingPlanVersion: number(value?.loadingPlanVersion),
|
||||
allocatedPackageCount: number(value?.allocatedPackageCount), linkedAt: text(value?.linkedAt),
|
||||
}
|
||||
}
|
||||
|
||||
export function projectPushPreloadStability({ plan, detail, links, task }) {
|
||||
const summary = detail?.summary || {}
|
||||
return {
|
||||
plan: {
|
||||
id: id(plan?.id), bookingId: id(plan?.bookingId), status: text(plan?.status),
|
||||
containerNo: text(plan?.containerNo), voyageNo: text(plan?.voyageNo), version: number(plan?.version),
|
||||
items: sorted(plan?.items, (item) => id(item?.id) || '').map(planItem),
|
||||
},
|
||||
detail: {
|
||||
summary: {
|
||||
planId: id(summary?.planId), containerBookingId: id(summary?.containerBookingId),
|
||||
containerNo: text(summary?.containerNo), cabinetNo: text(summary?.cabinetNo),
|
||||
status: text(summary?.status), version: number(summary?.version),
|
||||
skuCount: number(summary?.skuCount), totalPackageCount: number(summary?.totalPackageCount),
|
||||
readiness: readiness(summary?.readiness),
|
||||
},
|
||||
items: sorted(detail?.items, (item) => id(item?.itemId) || '').map(detailItem),
|
||||
histories: sorted(detail?.histories,
|
||||
(item) => `${id(item?.historyId) || ''}:${text(item?.occurredAt) || ''}`).map(history),
|
||||
},
|
||||
links: sorted(links, (item) => id(item?.id) || '').map(link),
|
||||
task: { http: number(task?.http), code: text(task?.code) },
|
||||
}
|
||||
}
|
||||
|
||||
function display(value) {
|
||||
if (value === undefined) return 'missing'
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
export function diffPushPreloadStability(before, after) {
|
||||
const changes = []
|
||||
function visit(left, right, path) {
|
||||
if (Object.is(left, right)) return
|
||||
if (Array.isArray(left) && Array.isArray(right)) {
|
||||
const length = Math.max(left.length, right.length)
|
||||
for (let index = 0; index < length; index += 1) visit(left[index], right[index], `${path}[${index}]`)
|
||||
return
|
||||
}
|
||||
if (left && right && typeof left === 'object' && typeof right === 'object') {
|
||||
const keys = [...new Set([...Object.keys(left), ...Object.keys(right)])].sort()
|
||||
for (const key of keys) visit(left[key], right[key], path ? `${path}.${key}` : key)
|
||||
return
|
||||
}
|
||||
changes.push(`${path}:${display(left)}->${display(right)}`)
|
||||
}
|
||||
visit(before, after, '')
|
||||
return changes
|
||||
}
|
||||
Reference in New Issue
Block a user