优化
This commit is contained in:
@@ -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 : [])])],
|
||||
|
||||
+5
-3
@@ -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;跨区消费已独立验证,不回退处理历史柜')
|
||||
|
||||
Reference in New Issue
Block a user