This commit is contained in:
2026-08-12 19:04:45 +08:00
parent f2f959e8a5
commit 9d863500ed
4 changed files with 206 additions and 27 deletions
@@ -61,7 +61,47 @@ export async function run(context) {
`status=${inbound?.summary?.processStatus || 'missing'}, review=${inbound?.summary?.measureReviewStatus || 'missing'}, warehouseId=${warehouseId || 'missing'}`, OPTIONS)
if (!warehouseId) return
const candidateResponse = await context.request('GET', `${SMART}/candidates?warehouseId=${encodeURIComponent(warehouseId)}`)
const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-16)
const channelCode = id(inbound?.summary?.channelCode || source?.channelCode)
const channelName = inbound?.summary?.channelName || source?.channelName || channelCode
const warehouseName = inbound?.summary?.warehouseName || source?.warehouseName
const bookingDraft = {
customerId: tenantId, customerName: inbound?.summary?.customerName || `验收客户-${tenantId}`,
channelCode: channelCode || 'FBA', channelName, warehouseId, warehouseName,
billOfLadingNo: `BL-${suffix}`, shippingCompanyId: null, shippingCompanyName: `验收船公司-${suffix}`,
bookingAgentId: null, bookingAgentName: `验收代理-${suffix}`, vesselName: `验收船-${suffix}`,
voyageNo: `V-${suffix}`, departurePortCode: 'CNSZX', departurePortName: '深圳',
destinationPortCode: 'USLAX', destinationPortName: '洛杉矶', placeOfDelivery: 'LAX',
serviceTerm: 'CY/CY', freightTerm: 'PREPAID', estimatedSailingAt: '2026-09-01T08:00:00',
estimatedArrivalAt: '2026-09-20T08:00:00', remark: context.marker,
parties: [{ partyType: 'SHIPPER', partyName: `发货方-${suffix}` },
{ partyType: 'CONSIGNEE', partyName: `收货方-${suffix}` },
{ partyType: 'NOTIFY_PARTY', partyName: `通知方-${suffix}` }],
containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', capacityCbm: 100, maxWeightKg: 100000, quantity: 1 }],
}
context.assert(MODULE, '订舱渠道与仓库快照完整', inboundResponse, Boolean(channelCode && channelName && warehouseName),
`channelCode=${channelCode || 'missing'}, warehouseId=${warehouseId}, warehouseName=${warehouseName || 'missing'}`, OPTIONS)
if (!channelCode || !warehouseName) return
const bookingCreate = await request(context, 'POST', '/api/platform/v1/ocean-bookings',
{ expectedVersion: 0, data: bookingDraft }, { 'Idempotency-Key': `${context.marker}-loading-booking-create` })
const bookingCreated = context.apiSuccess(MODULE, '创建含渠道仓库快照的隔离订舱', bookingCreate, '', OPTIONS)
const bookingId = id(bookingCreated?.oceanBooking?.id)
if (!bookingId) return
const bookingVersion = Number(bookingCreated.oceanBooking.version)
const submit = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/submit`,
{ expectedVersion: bookingVersion, reason: context.marker }, { 'Idempotency-Key': `${context.marker}-loading-booking-submit` })
const submitted = context.apiSuccess(MODULE, '提交隔离订舱', submit, '', OPTIONS)
const confirm = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/confirm`,
{ expectedVersion: Number(submitted?.oceanBooking?.version), reason: context.marker }, { 'Idempotency-Key': `${context.marker}-loading-booking-confirm` })
const confirmedBooking = context.apiSuccess(MODULE, '确认订舱并自动生成真实柜次', confirm, '', OPTIONS)
const allocation = confirmedBooking?.containerAllocations?.[0]
const containerId = id(allocation?.containerBookingId)
context.assert(MODULE, '确认响应包含自动生成的真实柜次', confirm,
Boolean(containerId) && id(confirmedBooking?.oceanBooking?.id) === bookingId,
`bookingId=${bookingId}, containerBookingId=${containerId || 'missing'}`, OPTIONS)
if (!containerId) return
const candidateResponse = await context.request('GET', `${SMART}/candidates?bookingId=${encodeURIComponent(containerId)}&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)
@@ -72,37 +112,14 @@ export async function run(context) {
`candidateFound=${Boolean(candidate)}, available=${candidate?.availablePackageCount ?? 'missing'}`, OPTIONS)
if (!candidate?.inboundDetailId) return
const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-16)
const containerNo = `P3C-${suffix}`.slice(0, 64)
const cabinetNo = `P3I-${suffix}`.slice(0, 64)
const containerPayload = {
containerNo, cabinetNo, warehouseId, warehouseName: inbound?.summary?.warehouseName || null,
billOfLadingNo: `BL-${suffix}`, vesselName: `P3-VESSEL-${suffix}`.slice(0, 128), voyageNo: `V-${suffix}`.slice(0, 64),
containerType: '40HQ', shippingCompanyId: null, shippingCompanyName: `P3-SHIPPING-${suffix}`.slice(0, 128),
departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
destinationWarehouseCode: candidate.destinationWarehouseCode || null,
destinationWarehouseName: candidate.destinationWarehouseName || null,
capacityCbm: 100, maxWeightKg: 100000, terminalTruckingCompany: `P3-TRUCK-${suffix}`.slice(0, 128),
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 createContainerResponse = await request(context, 'POST', CONTAINERS, containerPayload)
const container = context.apiSuccess(MODULE, '创建 marker 隔离货柜', createContainerResponse,
`containerNo=${containerNo}`, OPTIONS)
const containerId = id(container?.id)
context.assert(MODULE, '货柜归属仓库、路线、版本和编号正确', createContainerResponse,
Boolean(containerId) && container?.containerNo === containerNo && container?.cabinetNo === cabinetNo
&& id(container?.warehouseId) === warehouseId && Number(container?.version) === 0,
`containerId=${containerId || 'missing'}, version=${container?.version ?? 'missing'}`, OPTIONS)
if (!containerId) return
const containerDetailResponse = await context.request('GET', `${CONTAINERS}/${containerId}`)
const containerDetail = context.apiSuccess(MODULE, '回查货柜详情', containerDetailResponse, '', OPTIONS)
context.assert(MODULE, '货柜详情保留候选货物路线约束', containerDetailResponse,
containerDetail?.destinationWarehouseCode === (candidate.destinationWarehouseCode || null)
&& containerDetail?.planStatus === 'WAIT_LOADING',
&& ['EDITING', 'WAIT_LOADING'].includes(containerDetail?.planStatus),
`planStatus=${containerDetail?.planStatus || 'missing'}`, OPTIONS)
const containerNo = id(containerDetail?.containerNo || containerDetail?.cabinetNo)
const planResponse = await request(context, 'POST', `${SMART}/plans`, { bookingId: containerId, remark: context.marker })
const plan = context.apiSuccess(MODULE, '创建智能装柜计划', planResponse, '', OPTIONS)
const planId = id(plan?.id)
@@ -68,7 +68,7 @@ test('records LOADED and clears the reason only after a real LOADED reread', ()
test('container loading follows push, claim, real pick, completed sequence', () => {
const value = source('./phase3-container-loading.mjs')
assert.match(value, /containerDetail\?\.planStatus === 'WAIT_LOADING'/)
assert.match(value, /\['EDITING', 'WAIT_LOADING'\]\.includes\(containerDetail\?\.planStatus\)/)
assertOrder(value, [
"pushed?.status === 'EDITING'",
"claimedPlan?.status === 'LOADING'",
@@ -124,6 +124,23 @@ test('PDA inventory snapshot gates batch and records the only safe mutation gap'
assert.match(convergence, /fixture\?\.inventoryMutationVerified !== true/)
})
test('container loading creates and confirms booking before scoped candidates', () => {
const value = source('./phase3-container-loading.mjs')
assertOrder(value, [
"const bookingCreate = await request",
"const submit = await request",
"const confirm = await request",
"const candidateResponse = await context.request('GET', `${SMART}/candidates?bookingId=",
"const planResponse = await request(context, 'POST', `${SMART}/plans`",
"const assignResponse = await request",
])
assert.match(value, /channelCode: channelCode/)
assert.match(value, /warehouseId, warehouseName/)
assert.match(value, /containerAllocations\?\.\[0\]/)
assert.match(value, /bookingId=\$\{encodeURIComponent\(containerId\)\}&warehouseId=/)
assert.doesNotMatch(value, /POST', CONTAINERS, containerPayload/)
})
test('PDA completion acceptance verifies immutable BATCH and COMPLETE_LOADING audit with operator metadata', () => {
const container = source('./phase3-container-loading.mjs')
const convergence = source('./phase3-pda-convergence.mjs')