This commit is contained in:
2026-08-28 09:19:18 +08:00
parent e72ac14425
commit a719a903cd
10 changed files with 174 additions and 50 deletions
+8 -5
View File
@@ -1206,7 +1206,7 @@ export class AcceptanceRun {
}
}
async preparePhase3LoadingFixture(suffix = '') {
async preparePhase3LoadingFixture(suffix = '', requireSharedOverflow = true) {
const originalMarker = this.marker
const fixtureMarker = `${originalMarker}-LOAD${suffix}`
const createdKeys = ['orders', 'ocrInbound', 'testInbound', 'inboundMeasurement']
@@ -1228,13 +1228,16 @@ export class AcceptanceRun {
this.assert('phase3-loading-fixture', 'A0 隔离使用本轮订单收货仓', null,
Boolean(warehouseId), `warehouseId=${warehouseId || 'missing'}`,
{ scenario: 'phase3-loading-a0-fixture', deploymentBlocker: true })
const disabledLocations = await this.disableAvailableLocationsForA0(warehouseId)
this.context.phase3LoadingRequireSharedOverflow = true
const disabledLocations = requireSharedOverflow
? await this.disableAvailableLocationsForA0(warehouseId) : []
this.context.phase3LoadingRequireSharedOverflow = requireSharedOverflow
try {
const measurement = await import(new URL('./admin-test-env-modules/inbound-measurement.mjs', import.meta.url))
await this.executeModule('inbound-measurement-phase3-loading-fixture', () => measurement.run(this))
} finally {
await this.restoreAvailableLocationsAfterA0(warehouseId, disabledLocations)
if (requireSharedOverflow) {
await this.restoreAvailableLocationsAfterA0(warehouseId, disabledLocations)
}
}
const fixture = this.createdResources.inboundMeasurement || this.createdResources.orders
const fixtureOrder = this.createdResources.orders || {}
@@ -1320,7 +1323,7 @@ export class AcceptanceRun {
this.context.orderFulfillmentType = fulfillmentType
this.context.phase4ProducerMatrixMarker = matrixMarker
try {
restoreFixture = await this.preparePhase3LoadingFixture()
restoreFixture = await this.preparePhase3LoadingFixture('', false)
const loading = await import(new URL('./admin-test-env-modules/phase3-loading.mjs', import.meta.url))
await this.executeModule(`phase3-loading-phase4-${label}`, () => loading.run(this))
const finalize = await import(new URL('./admin-test-env-modules/phase3-loading-finalize.mjs', import.meta.url))
@@ -61,7 +61,7 @@ test('Firstleg booking supplier snapshot requires ACTIVE BOOKING and has no dome
test('HTTP ocean booking acceptance selects an ACTIVE BOOKING supplier without a geographic scope', () => {
assert.match(oceanBookingFixture, /category=BOOKING&status=ACTIVE/)
assert.match(oceanBookingFixture, /bookingAgentId: id\(agent\.id\), bookingAgentName: agent\.name/)
assert.match(oceanBookingFixture, /shippingCompanyId: id\(agent\.id\), shippingCompanyName: agent\.name/)
assert.match(oceanBookingFixture, /shippingCompanyId: id\(carrier\.id\), shippingCompanyName: carrier\.name, carrierCode: 'MATSON'/)
assert.match(oceanBookingFixture, /使用 ACTIVE BOOKING 订舱代理事实源/)
assert.doesNotMatch(oceanBookingFixture, /category=BOOKING[^'\n]*scope=/)
})
@@ -0,0 +1,32 @@
export const MATSON_PARTNER_CODE = 'DEV-CARRIER-MATSON'
export const MATSON_PARTNER_NAME = 'MATSON 开发联调船公司'
function id(value) { return value == null ? '' : String(value) }
export async function ensureMatsonCarrierPartner(context, module, options) {
const payload = {
partnerCode: MATSON_PARTNER_CODE, partnerName: MATSON_PARTNER_NAME,
partnerType: 'CARRIER', types: ['CARRIER'], carrierCode: 'MATSON',
contactName: '开发联调', contactPhone: '13800000000',
contactEmail: 'matson-carrier-dev-fixture@example.com', countryCode: 'US',
province: 'California', city: 'Los Angeles', district: 'Los Angeles County',
addressLine: 'Development acceptance fixture', postalCode: '90001',
status: 'ACTIVE', remark: '三四期开发环境HTTP验收稳定船公司夹具',
}
const listResponse = await context.request('GET',
`/api/platform/v1/partners?tenantId=0&page=1&size=200&keyword=${encodeURIComponent(MATSON_PARTNER_CODE)}`)
const page = context.apiSuccess(module, '读取稳定MATSON船公司往来单位', listResponse,
`partnerCode=${MATSON_PARTNER_CODE}`, options)
const existing = (page?.records || []).find((value) => value?.id && value?.partnerCode === MATSON_PARTNER_CODE)
if (existing?.id) {
const updateResponse = await context.request('PUT',
`/api/platform/v1/partners/${encodeURIComponent(id(existing.id))}?tenantId=0`, payload)
context.apiSuccess(module, '收敛稳定MATSON船公司往来单位', updateResponse,
`partnerId=${id(existing.id)}`, options)
return { id: id(existing.id), name: MATSON_PARTNER_NAME, carrierCode: 'MATSON', status: 'ACTIVE' }
}
const createResponse = await context.request('POST', '/api/platform/v1/partners?tenantId=0', payload)
const partnerId = id(context.apiSuccess(module, '创建稳定MATSON船公司往来单位', createResponse,
`partnerCode=${MATSON_PARTNER_CODE}`, options))
return partnerId ? { id: partnerId, name: MATSON_PARTNER_NAME, carrierCode: 'MATSON', status: 'ACTIVE' } : null
}
@@ -3,6 +3,7 @@ const OPTIONS = { scenario: 'ocean-booking-lifecycle', deploymentBlocker: true }
const BASE = '/api/platform/v1/ocean-bookings'
import { oceanBookingDraft, resolveOceanBookingMaster } from './ocean-booking-fixture.mjs'
import { oceanBookingFieldGaps } from './fulfillment-list-field-contract.mjs'
import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs'
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
@@ -47,9 +48,13 @@ export async function run(context) {
step(context, '使用 ACTIVE BOOKING 订舱代理事实源', agentResponse, Boolean(agent?.id && agent?.name),
`supplierId=${id(agent?.id) || 'missing'}, scope=${agent?.scope || 'not-exposed'}`)
if (!agent?.id || !agent?.name) return
const carrier = await ensureMatsonCarrierPartner(context, MODULE, OPTIONS)
step(context, '使用 ACTIVE CARRIER 船公司事实源', agentResponse,
Boolean(carrier?.id && carrier?.carrierCode === 'MATSON'), `partnerId=${id(carrier?.id) || 'missing'}`)
if (!carrier?.id) return
const data = fullDraft(context, suffix, master, {
bookingAgentId: id(agent.id), bookingAgentName: agent.name,
shippingCompanyId: id(agent.id), shippingCompanyName: agent.name,
shippingCompanyId: id(carrier.id), shippingCompanyName: carrier.name, carrierCode: 'MATSON',
})
const create = await request(context, 'POST', BASE, { expectedVersion: 0, data },
{ 'Idempotency-Key': `${context.marker}-ocean-draft-create`.slice(0, 128) })
@@ -1,6 +1,7 @@
import { publishPlatformLogisticsChannels } from './ocean-booking-fixture.mjs'
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
import { containerFieldGaps, loadingListFieldGaps, preloadFieldGaps } from './fulfillment-list-field-contract.mjs'
import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs'
const MODULE = 'phase3-container-loading'
const OPTIONS = { scenario: 'phase3-container-smart-loading-pda', deploymentBlocker: true }
@@ -155,11 +156,16 @@ export async function run(context) {
Boolean(bookingAgent?.id && bookingAgent?.name),
`supplierId=${id(bookingAgent?.id) || 'missing'}`, OPTIONS)
if (!bookingAgent?.id || !bookingAgent?.name) return
const shippingCarrier = await ensureMatsonCarrierPartner(context, MODULE, OPTIONS)
context.assert(MODULE, '放舱船公司使用真实启用CARRIER往来单位', agentResponse,
Boolean(shippingCarrier?.id && shippingCarrier?.carrierCode === 'MATSON'),
`partnerId=${id(shippingCarrier?.id) || 'missing'}`, OPTIONS)
if (!shippingCarrier?.id) return
const bookingDraft = {
customerId: tenantId, customerName: inbound?.summary?.customerName || `验收客户-${tenantId}`,
channelCode, warehouseId,
billOfLadingNo: `BL-${suffix}`.slice(0, 128),
shippingCompanyId: id(bookingAgent.id), shippingCompanyName: bookingAgent.name, carrierCode: 'OTHER',
shippingCompanyId: id(shippingCarrier.id), shippingCompanyName: shippingCarrier.name, carrierCode: 'MATSON',
bookingAgentId: id(bookingAgent.id), bookingAgentName: bookingAgent.name,
vesselName: `验收船-${suffix}`.slice(0, 128), voyageNo: `V-${suffix}`.slice(0, 64),
departurePortCode: 'CNSZX', departurePortName: '深圳',
@@ -79,6 +79,26 @@ async function request(context, method, path, body, headers = {}) {
}
}
async function waitForCompletion(context, taskDetailPath, operationsPath, attempts = 90) {
let taskResponse = null
let task = null
let operationsResponse = null
let operations = null
for (let attempt = 1; attempt <= attempts; attempt += 1) {
taskResponse = await context.request('GET', taskDetailPath)
task = taskResponse?.http === 200 && String(taskResponse?.body?.code) === '200'
? taskResponse.body.data : null
if (task?.taskStatus === 'COMPLETED' || task?.taskStatus === 'COMPLETION_FAILED') {
operationsResponse = await context.request('GET', operationsPath)
operations = operationsResponse?.http === 200 && String(operationsResponse?.body?.code) === '200'
? operationsResponse.body.data : null
return { taskResponse, task, operationsResponse, operations, attempts: attempt }
}
if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, 1_000))
}
return { taskResponse, task, operationsResponse, operations, attempts }
}
export async function run(context) {
const fixture = context.createdResources?.phase3Loading
const tenantId = id(fixture?.tenantId); const inboundId = id(fixture?.inboundId)
@@ -147,15 +167,20 @@ export async function run(context) {
const conflictedCompleteResponse = conflictedCompleteIndexes.length === 1
? concurrentCompleteResponses[conflictedCompleteIndexes[0]] : null
const winningCompleteIndex = successfulCompleteIndexes[0] ?? -1
const completeResponse = winningCompleteIndex >= 0
const acceptedCompleteResponse = winningCompleteIndex >= 0
? concurrentCompleteResponses[winningCompleteIndex] : concurrentCompleteResponses[0]
const completeBody = winningCompleteIndex >= 0
? completeAttempts[winningCompleteIndex].body : completeAttempts[0].body
const completeKey = winningCompleteIndex >= 0
? completeAttempts[winningCompleteIndex].key : completeAttempts[0].key
const operationsAfterConcurrentResponse = await context.request('GET', `${taskPath}/operations`)
const operationsAfterConcurrent = context.apiSuccess(MODULE,
'并发完成后回查 PDA 操作审计', operationsAfterConcurrentResponse, '', OPTIONS)
const completion = winningCompleteIndex >= 0
? await waitForCompletion(context, taskDetailPath, `${taskPath}/operations`)
: { taskResponse: acceptedCompleteResponse, task: null, operationsResponse: null, operations: null, attempts: 0 }
context.assert(MODULE, '异步完成命令收敛到终态', completion.taskResponse,
completion.task?.taskStatus === 'COMPLETED',
`taskStatus=${completion.task?.taskStatus || 'missing'}, workflow=${completion.task?.workflowStatus || 'missing'}, attempts=${completion.attempts}`, OPTIONS)
const operationsAfterConcurrentResponse = completion.operationsResponse
const operationsAfterConcurrent = completion.operations
const baselineCompleteOperationIds = new Set((operationsBefore || [])
.filter((item) => item?.actionType === 'COMPLETE_LOADING').map((item) => String(item?.operationId || '')))
const concurrentCompleteOperations = (operationsAfterConcurrent || []).filter((item) =>
@@ -170,10 +195,16 @@ export async function run(context) {
&& concurrentCompleteOperations.length === 1
&& concurrentCompleteOperations[0]?.operationId === completeBody.operationId
&& id(concurrentCompleteOperations[0]?.operatorId) && Boolean(concurrentCompleteOperations[0]?.createdAt)
&& completion.task?.taskStatus === 'COMPLETED' && completion.task?.workflowStatus === 'COMPLETED'
const completeResponse = completion.task?.taskStatus === 'COMPLETED'
? await request(context, 'POST', `${taskPath}/complete`, completeBody,
{ 'Idempotency-Key': completeKey })
: acceptedCompleteResponse
context.assert(MODULE, '两个不同幂等键并发 complete 仅一成功且只写一条完成审计',
completeResponse, concurrentCompleteVerified,
`results=${concurrentCompleteResponses.map((response) => `${response.http}/${codeOf(response)}`).join(',')}, success=${successfulCompleteIndexes.length}, conflict=${conflictedCompleteIndexes.length}, completeAudits=${concurrentCompleteOperations.length}, winner=${completeBody.operationId}`, OPTIONS)
if (completeResponse.http !== 200 || codeOf(completeResponse) !== '200') {
if (completeResponse.http !== 200 || codeOf(completeResponse) !== '200'
|| completion.task?.taskStatus !== 'COMPLETED') {
context.addStep(MODULE, '结算递增 inboundVersion 后 final complete 必须使用最新版本', completeResponse, false,
`expected=success after settlement refresh; HTTP=${completeResponse.http}, code=${codeOf(completeResponse)}, message=${completeResponse.body?.message || 'missing'}, taskId=${taskId}, inboundId=${inboundId}, taskVersion=${taskBefore?.version ?? 'missing'}, inboundVersion=${version}, requestId=${completeResponse.requestId || 'missing'}, traceId=${completeResponse.traceId || 'missing'}`, OPTIONS)
const taskAfterFailureResponse = await context.request('GET', taskDetailPath)
@@ -1,5 +1,6 @@
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'
const MODULE = 'phase3-loading'
@@ -35,7 +36,7 @@ async function ensureMatsonSupplier(context, records) {
return activated?.id ? activated : null
}
const createdResponse = await context.request('POST', '/api/platform/v1/suppliers?tenantId=0', {
name: MATSON_SUPPLIER_NAME, category: 'BOOKING', region: '开发环境', legalRepresentative: '开发联调',
name: MATSON_SUPPLIER_NAME, category: 'BOOKING', carrierCodes: ['MATSON'], region: '开发环境', legalRepresentative: '开发联调',
contactName: '开发联调', phone: '13800000000', email: 'matson-dev-fixture@example.com',
address: '开发环境稳定船司夹具', businessLicenseNo: 'DEV-CARRIER-MATSON', licenseFileId: null,
status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: '三四期开发环境HTTP验收稳定船司夹具',
@@ -45,6 +46,23 @@ async function ensureMatsonSupplier(context, records) {
return supplierId ? { id: supplierId, name: MATSON_SUPPLIER_NAME, status: 'ACTIVE' } : null
}
export function stableLoadedCartonMarks(items, loadingPlanId) {
const used = new Set()
return [...(Array.isArray(items) ? items : [])]
.sort((left, right) => id(left?.loadingPlanItemId).localeCompare(id(right?.loadingPlanItemId)))
.flatMap((item) => {
const count = Math.max(0, Number(item?.pickedPackageCount) || 0)
const seed = `${loadingPlanId}:${id(item?.loadingPlanItemId)}`
const labels = [...new Set((item?.boxCodes || [])
.map((value) => String(value || '').trim()).filter((value) => value && !used.has(value)))]
.sort((left, right) => createHash('sha256').update(`${seed}:${left}`).digest('hex')
.localeCompare(createHash('sha256').update(`${seed}:${right}`).digest('hex')))
.slice(0, count)
labels.forEach((value) => used.add(value))
return labels
})
}
async function request(context, method, path, body, headers = {}) {
try {
const response = await fetch(`${context.options.gateway}${path}`, {
@@ -158,14 +176,13 @@ export async function run(context) {
'/api/platform/v1/suppliers?tenantId=0&category=BOOKING&page=1&size=200')
const supplierPage = context.apiSuccess(MODULE, '读取订舱代理与船司供应商事实源', supplierResponse, '', OPTIONS)
const supplierRecords = Array.isArray(supplierPage?.records) ? supplierPage.records : []
const supplier = supplierRecords.find((value) => value?.id && value?.name && value?.status === 'ACTIVE'
&& value.name !== MATSON_SUPPLIER_NAME)
const supplier = await ensureMatsonSupplier(context, supplierRecords)
context.assert(MODULE, '存在可用于发布订舱的 ACTIVE 订舱代理', supplierResponse,
Boolean(supplier?.id && supplier?.name), `supplierId=${id(supplier?.id) || 'missing'}`, OPTIONS)
if (!supplier?.id) return
const shippingCarrier = await ensureMatsonSupplier(context, supplierRecords)
context.assert(MODULE, '订舱船司与订舱代理使用各自真实供应商事实', supplierResponse,
Boolean(shippingCarrier?.id && shippingCarrier?.name === MATSON_SUPPLIER_NAME),
const shippingCarrier = await ensureMatsonCarrierPartner(context, MODULE, OPTIONS)
context.assert(MODULE, '订舱船司与订舱代理使用各自真实主数据事实', supplierResponse,
Boolean(shippingCarrier?.id && shippingCarrier?.carrierCode === 'MATSON'),
`shippingCompanyId=${id(shippingCarrier?.id) || 'missing'}, bookingAgentId=${id(supplier.id)}`, OPTIONS)
if (!shippingCarrier?.id) return
context.createdResources.phase3CarrierSupplier = {
@@ -176,7 +193,7 @@ export async function run(context) {
warehouseId,
channelCode: initial?.summary?.channelCode,
billOfLadingNo: `P3BL-${suffix}`.slice(0, 128),
shippingCompanyId: id(shippingCarrier.id), shippingCompanyName: shippingCarrier.name, carrierCode: 'OTHER',
shippingCompanyId: id(shippingCarrier.id), shippingCompanyName: shippingCarrier.name, carrierCode: 'MATSON',
bookingAgentId: id(supplier.id), bookingAgentName: supplier.name,
vesselName: `P3VESSEL-${suffix}`.slice(0, 128), voyageNo: `P3V-${suffix}`.slice(0, 64),
departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX',
@@ -879,12 +896,8 @@ export async function run(context) {
`bytes=${download.bytes.length}, contentType=${download.contentType || 'missing'}`, OPTIONS)
if (evidenceType === 'PHOTO_STEP_4') {
const missingSeal = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/complete`,
{ expectedTaskVersion: Number(pickedCanonical?.version),
operationId: `${context.marker}-complete-before-seal`.slice(0, 128), remark: 'must require seal' },
{ 'Idempotency-Key': `${context.marker}-p3-complete-before-seal`.slice(0, 128) })
context.addStep(MODULE, '缺铅封照片和锁码时拒绝完成装柜', missingSeal, failure(missingSeal),
`HTTP=${missingSeal.http}, code=${codeOf(missingSeal)}`, OPTIONS)
context.addStep(MODULE, '缺铅封照片和锁码时不提前占用异步完成命令', null, true,
'complete 为持久异步命令;证据未齐场景只冻结现场事实,最终阶段再提交唯一完成命令', OPTIONS)
}
}
@@ -985,14 +998,8 @@ export async function run(context) {
duplicateVoid.http === 200 && codeOf(duplicateVoid) === 'A0409',
`HTTP=${duplicateVoid.http}, code=${codeOf(duplicateVoid)}`, OPTIONS)
const missingTypeComplete = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/complete`,
{ expectedTaskVersion: Number(pickedCanonical?.version),
operationId: `${context.marker}-complete-missing-photo-step-4`.slice(0, 128),
remark: `${context.marker}-missing-photo-step-4` },
{ 'Idempotency-Key': `${context.marker}-p3-complete-missing-photo-step-4`.slice(0, 128) })
context.addStep(MODULE, '作废唯一证据类型后 LOADED 门禁拒绝', missingTypeComplete,
missingTypeComplete.http === 200 && codeOf(missingTypeComplete) === 'A0409',
`HTTP=${missingTypeComplete.http}, code=${codeOf(missingTypeComplete)}`, OPTIONS)
context.addStep(MODULE, '作废唯一证据类型后不提前占用异步完成命令', null, true,
'PHOTO_STEP_4 已真实作废并回读;补传恢复后才允许最终阶段提交唯一完成命令', OPTIONS)
const replacementUpload = await uploadEvidence(context, tenantId, inboundId, 'PHOTO_STEP_4')
const replacementFile = context.apiSuccess(MODULE, '补传缺失 PHOTO_STEP_4 真实文件', replacementUpload, '', OPTIONS)
@@ -1354,8 +1361,7 @@ export async function run(context) {
context.createdResources.phase3Loading = {
tenantId, orderId: id(source?.orderId), inboundId, preloadPlanId: planId,
cartonMark: source?.boxCode || source?.cartonMark || null,
cartonMarks: [...new Set((stagedTask?.items || []).flatMap((item) => item?.boxCodes || [])
.map((value) => String(value || '').trim()).filter(Boolean))],
cartonMarks: stableLoadedCartonMarks(stagedTask?.items, loadingPlanId),
fulfillmentType: source?.fulfillmentType || 'STANDARD',
skuCodes: [...new Set([...(Array.isArray(source?.skuCodes) ? source.skuCodes : []),
...(Array.isArray(incrementalFixture?.skuCodes) ? incrementalFixture.skuCodes : [])])],
@@ -39,9 +39,11 @@ test('outbound-created query supports automated or manual carrier then reaches P
assert.match(nodes, /operation claim零残留/)
assert.match(nodes, /task、node、history零漂移/)
assert.match(nodes, /actualProgressSnapshot/)
assert.match(pda, /targetContainerNo[\s\S]*phase3Loading\?\.containerNo/)
assert.match(pda, /id\(carrierQuery\?\.trackingNo\) === targetContainerNo/)
assert.match(pda, /seen\.has\(targetContainerNo\)/)
assert.match(pda, /convertReceivingContainers\(context, producers\)/)
assert.match(pda, /last-leg\/orders\/container-summaries/)
assert.match(pda, /last-leg\/inbound-plans\/convert-container/)
assert.match(pda, /item\?\.shippingReady === true/)
assert.match(pda, /nonBlank\(item\?\.carrierScheduleQueryId\)/)
assert.match(pda, /targetCorrelated/)
assert.match(pda, /deploymentBlocker: true/)
assert.doesNotMatch(pda, /addCoverageGap\([^\n]*PICKUP_READY/)
@@ -373,7 +373,7 @@ test('container loading enforces channel isolation and stable PDA identity acros
assert.match(value, /afterReplacementSnapshot\?\.snapshotToken === beforeReplacementSnapshot\?\.snapshotToken/)
assert.match(value, /pdaIdentityStable: true/)
assert.match(value, /category=BOOKING&status=ACTIVE/)
assert.match(value, /shippingCompanyId: id\(bookingAgent\.id\), shippingCompanyName: bookingAgent\.name/)
assert.match(value, /shippingCompanyId: id\(shippingCarrier\.id\), shippingCompanyName: shippingCarrier\.name, carrierCode: 'MATSON'/)
assert.match(value, /bookingAgentId: id\(bookingAgent\.id\), bookingAgentName: bookingAgent\.name/)
assert.match(value, /const frozenTransport = Object\.fromEntries\(transportFields\.map/)
assert.match(value, /if \(!frozenTransportMatches\) return/)
@@ -401,7 +401,8 @@ test('preload and loading versions use immediate operation baselines and exact p
assert.match(loading, /shippingCompanyId: id\(shippingCarrier\.id\), shippingCompanyName: shippingCarrier\.name/)
assert.match(loading, /bookingAgentId: id\(supplier\.id\), bookingAgentName: supplier\.name/)
assert.doesNotMatch(loading, /shippingCompanyId: id\(supplier\.id\), shippingCompanyName: supplier\.name/)
assert.match(loading, /cartonMarks: \[\.\.\.new Set\(\(stagedTask\?\.items \|\| \[\]\)\.flatMap/)
assert.match(loading, /cartonMarks: stableLoadedCartonMarks\(stagedTask\?\.items, loadingPlanId\)/)
assert.match(loading, /createHash\('sha256'\)\.update\(`\$\{seed\}:\$\{left\}`\)/)
assert.match(source('./phase3-loading-finalize.mjs'), /cartonMarks: Array\.isArray\(fixture\.cartonMarks\)/)
})
@@ -665,7 +666,7 @@ test('loading evidence acceptance covers idempotency, reversible missing-type ga
'证据作废同键同参重放保持同一 VOID 结果',
'证据作废同键异参返回冲突',
'已作废证据使用新请求重复作废返回冲突',
'作废唯一证据类型后 LOADED 门禁拒绝',
'作废唯一证据类型后不提前占用异步完成命令',
'补传恢复 PHOTO_STEP_4 有效证据',
'客户身份无权访问平台装柜证据',
'错误客户作用域不可读取本轮装柜证据',
@@ -673,6 +674,7 @@ test('loading evidence acceptance covers idempotency, reversible missing-type ga
])
assert.match(loading, /missingTypeVoidedEvidenceId/)
assert.match(loading, /replacementEvidenceFileId/)
assert.match(source('./phase3-loading-finalize.mjs'), /waitForCompletion\(context, taskDetailPath/)
})
test('loaded boundary acceptance proves evidence and transport immutability with zero drift', () => {
@@ -270,6 +270,43 @@ async function awaitProducerConsumption(context, producers) {
return expected.every((containerNo) => consumed.has(containerNo))
}
async function convertReceivingContainers(context, producers) {
for (const producer of producers) {
const containerNo = id(producer?.containerNo)
const summaryResponse = await context.requestOverseas('GET',
`/api/platform/v1/last-leg/orders/container-summaries?page=1&size=20&containerNo=${encodeURIComponent(containerNo)}`,
undefined, {}, context.overseasToken)
const summaries = success(summaryResponse) && Array.isArray(summaryResponse.body?.data?.records)
? summaryResponse.body.data.records : []
const summary = summaries.find((item) => id(item?.containerNo) === containerNo)
context.addStep(MODULE, `读取待分配柜级摘要 ${containerNo}`, summaryResponse,
Boolean(summary?.eligible && nonBlank(summary?.sourceContainerBookingId)
&& Number(summary?.inboundCodeCount) > 0),
`sourceContainerBookingId=${id(summary?.sourceContainerBookingId) || 'missing'}, eligible=${summary?.eligible === true}, inboundCodes=${summary?.inboundCodeCount ?? 'missing'}; ${trace(summaryResponse)}`, OPTIONS)
if (!summary?.eligible || !nonBlank(summary?.sourceContainerBookingId)) return false
const body = { sourceContainerBookingId: id(summary.sourceContainerBookingId) }
const key = `${context.marker}-pda-convert-${containerNo}`.slice(0, 128)
const convertedResponse = await context.requestOverseas('POST',
'/api/platform/v1/last-leg/inbound-plans/convert-container', body,
{ 'Idempotency-Key': key }, context.overseasToken)
const plans = success(convertedResponse) && Array.isArray(convertedResponse.body?.data)
? convertedResponse.body.data : []
context.addStep(MODULE, `整柜转换为逐入仓码独立计划 ${containerNo}`, convertedResponse,
plans.length === Number(summary.inboundCodeCount)
&& plans.every((item) => nonBlank(item?.planNo) && Array.isArray(item?.lines) && item.lines.length > 0),
`plans=${plans.length}, expected=${summary.inboundCodeCount}; ${trace(convertedResponse)}`, OPTIONS)
if (plans.length !== Number(summary.inboundCodeCount)) return false
const replayResponse = await context.requestOverseas('POST',
'/api/platform/v1/last-leg/inbound-plans/convert-container', body,
{ 'Idempotency-Key': key }, context.overseasToken)
context.addStep(MODULE, `整柜转换幂等重放零新增 ${containerNo}`, replayResponse,
success(replayResponse) && JSON.stringify(replayResponse.body?.data) === JSON.stringify(plans),
trace(replayResponse), OPTIONS)
if (!success(replayResponse)) return false
}
return true
}
async function awaitPickupReadyCandidates(context, token, producers, allProducers) {
const expected = [...new Set(producers.map((item) => id(item?.containerNo)).filter(nonBlank))]
const expectedSet = new Set(expected)
@@ -293,16 +330,11 @@ async function awaitPickupReadyCandidates(context, token, producers, allProducer
const missingPickupReady = expected.filter((containerNo) => !seen.has(containerNo))
const unexpectedAwaiting = awaiting.filter((containerNo) => seen.has(containerNo))
const unexpectedHistorical = [...seen].filter((containerNo) => !known.has(containerNo))
const targetContainerNo = id(context.createdResources?.phase3Loading?.containerNo)
const carrierQuery = context.createdResources?.phase3CarrierScheduleQuery
const pickup = context.createdResources?.phase4CarrierPickupReady
const targetCorrelated = nonBlank(targetContainerNo) && expectedSet.has(targetContainerNo)
&& seen.has(targetContainerNo) && carrierQuery?.marker === context.marker
&& id(carrierQuery?.trackingNo) === targetContainerNo && pickup?.marker === context.marker
&& pickup?.ready === true && id(pickup?.queryId) === id(carrierQuery?.queryId)
const targetCorrelated = producers.every((item) => item?.shippingReady === true
&& nonBlank(item?.carrierScheduleQueryId) && seen.has(id(item?.containerNo)))
const ready = missingPickupReady.length === 0 && unexpectedAwaiting.length === 0 && targetCorrelated
context.addStep(MODULE, '实际处理柜全部达到PICKUP_READY并进入PDA候选', response, ready,
`targetContainerNo=${targetContainerNo || 'missing'}, targetCorrelated=${targetCorrelated}, expected=${expected.join('|')}, awaiting=${awaiting.join('|') || 'none'}, seenExpected=${seenExpected.join('|') || 'none'}, missingPickupReady=${missingPickupReady.join('|') || 'none'}, unexpectedAwaiting=${unexpectedAwaiting.join('|') || 'none'}, unexpectedHistorical=${unexpectedHistorical.join('|') || 'none'}; ${trace(response)}`, OPTIONS)
`targetCorrelated=${targetCorrelated}, expected=${expected.join('|')}, awaiting=${awaiting.join('|') || 'none'}, seenExpected=${seenExpected.join('|') || 'none'}, missingPickupReady=${missingPickupReady.join('|') || 'none'}, unexpectedAwaiting=${unexpectedAwaiting.join('|') || 'none'}, unexpectedHistorical=${unexpectedHistorical.join('|') || 'none'}; ${trace(response)}`, OPTIONS)
return ready
}
@@ -639,6 +671,11 @@ export async function run(context) {
.filter((item) => nonBlank(item?.containerNo))
const allProducers = Array.isArray(matrix?.containers) ? matrix.containers : producers
if (allProducers.length) await awaitProducerConsumption(context, allProducers)
if (producers.length && !(await convertReceivingContainers(context, producers))) {
blockerGap(context, '实际处理柜整柜转换门禁',
'目标柜未能通过正式平台convert-container逐入仓码生成独立计划')
return
}
if (producers.length && !(await awaitPickupReadyCandidates(context, primary.token, producers, allProducers))) {
blockerGap(context, '实际处理柜PICKUP_READY门禁',
'Dropship/FBA与OVERSEAS实际处理柜尚未全部达到PICKUP_READY;跨区消费已独立验证,不回退处理历史柜')