This commit is contained in:
2026-08-13 19:19:29 +08:00
parent cca1bc002e
commit 808fabaf41
11 changed files with 176 additions and 37 deletions
+4 -5
View File
@@ -521,10 +521,8 @@ export class AcceptanceRun {
async preparePhase3ContainerFixture() {
const originalMarker = this.marker
// Keep the shared orchestration marker stable. Downstream loading,
// overview and finance modules select fixtures by context.marker; using a
// private -CAB suffix here made the producer invisible to those modules.
const fixtureMarker = originalMarker
// Isolate order idempotency, then publish the fixture under the run marker.
const fixtureMarker = `${originalMarker}-CAB`
const createdKeys = ['orders', 'ocrInbound', 'testInbound', 'inboundMeasurement']
const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound']
const createdSnapshot = new Map(createdKeys.map((key) => [key, {
@@ -546,7 +544,8 @@ export class AcceptanceRun {
this.createdResources.orchestrationFixtures = {
...(this.createdResources.orchestrationFixtures || {}),
phase3Container: fixture ? {
marker: fixtureMarker,
marker: originalMarker,
sourceMarker: fixtureMarker,
tenantId: fixture.tenantId,
orderId: fixture.orderId || this.createdResources.orders?.orderId,
inboundId: fixture.inboundId,
@@ -70,15 +70,22 @@ export async function run(context) {
context.assert(MODULE, '订舱存在匹配本轮柜型的箱型需求', changed,
Boolean(requirement?.id), `requirementId=${id(requirement?.id) || 'missing'}`, OPTIONS)
if (!requirement?.id || id(containerFlow?.tenantId) !== id(changeConfirmed?.oceanBooking?.customerId)) return
const allocation = await request(context, 'POST', `${path}/allocations`, {
requirementId: id(requirement.id), containerBookingId: id(containerFlow.containerBookingId), expectedVersion: version,
}, { 'Idempotency-Key': `${context.marker}-ocean-container-allocation`.slice(0, 128) })
const allocated = context.apiSuccess(MODULE, '放舱前分配本轮柜次', allocation, '', OPTIONS)
version = Number(allocated?.oceanBooking?.version)
context.assert(MODULE, '订舱柜次分配持久化且版本递增', allocation,
allocated?.containerAllocations?.some?.((item) => id(item?.containerBookingId) === id(containerFlow.containerBookingId))
&& Number.isInteger(version),
`containerBookingId=${id(containerFlow.containerBookingId)}, version=${version}`, OPTIONS)
const existingAllocation = changeConfirmed?.containerAllocations?.find?.(
(item) => id(item?.containerBookingId) === id(containerFlow.containerBookingId))
if (existingAllocation) {
context.assert(MODULE, '确认订舱已自动关联本轮柜次', changed, true,
`containerBookingId=${id(containerFlow.containerBookingId)}`, OPTIONS)
} else {
const allocation = await request(context, 'POST', `${path}/allocations`, {
requirementId: id(requirement.id), containerBookingId: id(containerFlow.containerBookingId), expectedVersion: version,
}, { 'Idempotency-Key': `${context.marker}-ocean-container-allocation`.slice(0, 128) })
const allocated = context.apiSuccess(MODULE, '放舱前分配本轮柜次', allocation, '', OPTIONS)
version = Number(allocated?.oceanBooking?.version)
context.assert(MODULE, '订舱柜次分配持久化且版本递增', allocation,
allocated?.containerAllocations?.some?.((item) => id(item?.containerBookingId) === id(containerFlow.containerBookingId))
&& Number.isInteger(version),
`containerBookingId=${id(containerFlow.containerBookingId)}, version=${version}`, OPTIONS)
}
}
const released = await request(context, 'POST', `${path}/release`, {
expectedVersion: version, billOfLadingNo: `BL-${suffix}`, vesselName: `放舱船-${suffix}`,
@@ -145,7 +145,7 @@ export async function run(context) {
expectedVersion: Number(foreignPlan?.version), operationId: `${context.marker}-other-channel-assign`.slice(0, 128),
})
context.addStep(MODULE, '异渠道入库单拒绝装入当前柜次', foreignAssignResponse,
foreignAssignResponse.http === 200 && ['A0400', '400'].includes(codeOf(foreignAssignResponse)),
foreignAssignResponse.http === 200 && ['A0422', '422'].includes(codeOf(foreignAssignResponse)),
`HTTP=${foreignAssignResponse.http}, code=${codeOf(foreignAssignResponse)}`, OPTIONS)
const containerDetailResponse = await context.request('GET', `${CONTAINERS}/${containerId}`)
@@ -275,7 +275,7 @@ export async function run(context) {
version: Number(replaced?.version), reason: `${context.marker}-clear-container-no`,
})
context.addStep(MODULE, 'PDA 任务创建后清空真实柜号失败', clearContainerNoResponse,
clearContainerNoResponse.http === 200 && ['A0400', '400'].includes(codeOf(clearContainerNoResponse)),
clearContainerNoResponse.http === 200 && ['A0409', '409'].includes(codeOf(clearContainerNoResponse)),
`HTTP=${clearContainerNoResponse.http}, code=${codeOf(clearContainerNoResponse)}`, OPTIONS)
const replacedTaskResponse = await context.request('GET',
@@ -117,7 +117,38 @@ export async function run(context) {
const warehouseId = id(initial?.summary?.warehouseId)
const smartLoadingBase = '/api/platform/v1/first-leg/smart-loading'
const pdaBase = '/api/platform/v1/first-leg/pda-loading'
const candidateResponse = await context.request('GET', `${smartLoadingBase}/candidates?warehouseId=${encodeURIComponent(warehouseId)}`)
const bookingDraft = {
customerId: tenantId, customerName: initial?.summary?.customerName,
warehouseId, warehouseName: initial?.summary?.warehouseName,
channelCode: initial?.summary?.channelCode, channelName: initial?.summary?.channelName,
shippingCompanyName: `P3-SHIPPING-${suffix}`, bookingAgentName: `P3-AGENT-${suffix}`,
departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX',
destinationPortName: '洛杉矶', placeOfDelivery: 'LAX', serviceTerm: 'CY/CY', freightTerm: 'PREPAID',
remark: context.marker,
parties: [{ partyType: 'SHIPPER', partyName: `P3-SHIPPER-${suffix}` },
{ partyType: 'CONSIGNEE', partyName: `P3-CONSIGNEE-${suffix}` },
{ partyType: 'NOTIFY_PARTY', partyName: `P3-NOTIFY-${suffix}` }],
containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', capacityCbm: 100,
maxWeightKg: 100000, quantity: 1 }],
}
const bookingCreateResponse = await request(context, 'POST', '/api/platform/v1/ocean-bookings',
{ expectedVersion: 0, data: bookingDraft },
{ 'Idempotency-Key': `${context.marker}-p3-booking-create`.slice(0, 128) })
const bookingCreated = context.apiSuccess(MODULE, '创建带渠道仓库快照的三期订舱', bookingCreateResponse, '', OPTIONS)
const bookingId = id(bookingCreated?.oceanBooking?.id)
if (!bookingId) return
const bookingSubmitResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/submit`,
{ expectedVersion: Number(bookingCreated.oceanBooking.version), reason: context.marker },
{ 'Idempotency-Key': `${context.marker}-p3-booking-submit`.slice(0, 128) })
const bookingSubmitted = context.apiSuccess(MODULE, '提交三期订舱', bookingSubmitResponse, '', OPTIONS)
const bookingConfirmResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/confirm`,
{ expectedVersion: Number(bookingSubmitted?.oceanBooking?.version), reason: context.marker },
{ 'Idempotency-Key': `${context.marker}-p3-booking-confirm`.slice(0, 128) })
const bookingConfirmed = context.apiSuccess(MODULE, '确认订舱并自动生成三期柜次', bookingConfirmResponse, '', OPTIONS)
const containerBookingId = id(bookingConfirmed?.containerAllocations?.[0]?.containerBookingId)
if (!containerBookingId) return
const candidateResponse = await context.request('GET', `${smartLoadingBase}/candidates?bookingId=${encodeURIComponent(containerBookingId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const candidates = context.apiSuccess(MODULE, '查询本轮入库单柜次装柜候选', candidateResponse, '', OPTIONS)
const candidate = Array.isArray(candidates) ? candidates.find((item) => id(item.inboundId) === inboundId
&& Number(item.availablePackageCount) > 0) : null
@@ -128,21 +159,6 @@ export async function run(context) {
if (!candidate?.inboundDetailId || !warehouseId) return
const containerNo = `P3B-${suffix}`
const containerResponse = await request(context, 'POST', '/api/platform/v1/containers', {
containerNo, cabinetNo: `P3BI-${suffix}`, warehouseId,
warehouseName: initial?.summary?.warehouseName || null, billOfLadingNo: `P3BL-${suffix}`,
vesselName: `P3-BRIDGE-${suffix}`, voyageNo: `P3V-${suffix}`, containerType: '40HQ',
shippingCompanyName: `P3-SHIPPING-${suffix}`, departurePortCode: 'CNSZX', departurePortName: '深圳',
destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
destinationWarehouseCode: candidate.destinationWarehouseCode || null,
destinationWarehouseName: candidate.destinationWarehouseName || null,
capacityCbm: 100, maxWeightKg: 100000, loadingAt: '2026-09-02T08:00:00',
estimatedSailingAt: '2026-09-03T08:00:00', estimatedPortArrivalAt: '2026-09-20T08:00:00',
estimatedWarehouseArrivalAt: '2026-09-21T08:00:00',
})
const container = context.apiSuccess(MODULE, '创建与入库预装串联的柜次', containerResponse, '', OPTIONS)
const containerBookingId = id(container?.id)
if (!containerBookingId) return
const loadingPlanResponse = await request(context, 'POST', `${smartLoadingBase}/plans`, {
bookingId: containerBookingId, remark: context.marker,
@@ -203,6 +219,20 @@ export async function run(context) {
&& Number(pushedLoadingPlan?.version) === Number(link.loadingPlanVersion) + 1,
`status=${pushedLoadingPlan?.status || 'missing'}, version=${pushedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
const containerBeforeTransportResponse = await context.request('GET',
`/api/platform/v1/containers/${containerBookingId}`)
const containerBeforeTransport = context.apiSuccess(MODULE, '读取补真实柜号前柜次版本',
containerBeforeTransportResponse, '', OPTIONS)
const transportResponse = await request(context, 'PATCH',
`/api/platform/v1/containers/${containerBookingId}/transport`, {
containerNo, voyageNo: `P3V-${suffix}`, version: Number(containerBeforeTransport?.version),
reason: `${context.marker}-bind-container-no`,
})
const transported = context.apiSuccess(MODULE, '补真实柜号并开放三期 PDA 任务', transportResponse, '', OPTIONS)
context.assert(MODULE, '真实柜号绑定到稳定柜次且计划身份不变', transportResponse,
transported?.containerNo === containerNo && id(transported?.id) === containerBookingId,
`containerNo=${transported?.containerNo || 'missing'}, containerBookingId=${id(transported?.id) || 'missing'}`, OPTIONS)
const blockedBeforePreloadConfirm = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`,
{ expectedVersion: initialVersion, remark: 'must confirm preload first' },
{ 'Idempotency-Key': `${context.marker}-p3-complete-before-preload-confirm`.slice(0, 128) })
@@ -211,7 +241,7 @@ export async function run(context) {
`HTTP=${blockedBeforePreloadConfirm.http}, code=${codeOf(blockedBeforePreloadConfirm)}`, OPTIONS)
const canonicalTaskResponse = await context.request('GET',
`${pdaBase}/tasks?containerNo=${encodeURIComponent(containerNo)}&warehouseId=${encodeURIComponent(warehouseId)}`)
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const canonicalTask = context.apiSuccess(MODULE, '读取柜次级 PDA 唯一事实任务', canonicalTaskResponse, '', OPTIONS)
const canonicalTaskId = id(canonicalTask?.id)
const canonicalItem = canonicalTask?.items?.find?.((item) => id(item.inboundId) === inboundId)
@@ -298,9 +298,10 @@ test('phase3 loading provisions its own marker-isolated WAIT_SPLIT fixture', asy
assert.equal(run.marker.includes('-LOAD'), false)
})
test('container fixture keeps the run marker and does not publish incomplete WAIT_PRICING resources', () => {
test('container fixture isolates order idempotency while publishing the run marker', () => {
const entry = source('../admin-test-env-acceptance.mjs')
assert.match(entry, /const fixtureMarker = originalMarker/)
assert.match(entry, /const fixtureMarker = `\$\{originalMarker\}-CAB`/)
assert.match(entry, /marker: originalMarker,\s*\n\s*sourceMarker: fixtureMarker/)
assert.match(entry, /fixture\.finalStatus !== 'APPROVED'/)
assert.match(entry, /禁止回退使用 WAIT_PRICING 订单/)
})