This commit is contained in:
2026-09-20 19:45:24 +08:00
parent 80778e017e
commit 93b58fdd6d
4 changed files with 28 additions and 8 deletions
@@ -6,6 +6,14 @@ import { fileURLToPath } from 'node:url'
import { expandOverseasCartonCandidates, resolveCartonFixtures, run, uniqueContainerProducer } from './phase4-overseas-pda.mjs'
test('receiving selects one current booking and supplies mandatory booking identity', () => {
assert.match(source, /id\(item.sourceContainerBookingId\) === preferredBookingId/)
assert.match(source, /matchingCandidates.length === 1/)
assert.match(source, /if \(!longId\(sourceContainerBookingId\)\)/)
assert.match(source, /sourceContainerBookingId=\$\{encodeURIComponent\(sourceContainerBookingId\)\}/)
assert.equal((source.match(/\/prepare\$\{bookingQuery\}/g) || []).length, 3)
})
test('one physical container is received once even when matrix roles overlap', () => {
const fba = { containerNo: 'TEST-C1', fulfillmentType: 'STANDARD' }
const overseas = { containerNo: 'TEST-C2' }
@@ -692,9 +692,10 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
}
const preferredContainer = id(preferredProducer?.containerNo)
|| context.createdResources?.phase3Loading?.containerNo
const candidate = preferredContainer
? candidates.data.find((item) => item.containerNo === preferredContainer)
: candidates.data[0]
const preferredBookingId = id(preferredProducer?.containerBookingId)
const matchingCandidates = candidates.data.filter(item => item.containerNo === preferredContainer
&& (!preferredBookingId || id(item.sourceContainerBookingId) === preferredBookingId))
const candidate = matchingCandidates.length === 1 ? matchingCandidates[0] : null
if (!candidate) {
blockerGap(context, `本轮柜号${preferredContainer || 'missing'}候选`,
'三期装柜完成生产者已落地,但海外候选尚未出现该柜;禁止回退处理历史或其他柜', 'producer-chain', candidates.response)
@@ -705,27 +706,33 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
nonBlank(containerNo) && Number(candidate?.totalPackages) > 0 && Number(candidate?.receivedPackages) >= 0,
`containerNo=${containerNo || 'missing'}, total=${candidate?.totalPackages ?? 'missing'}; ${trace(candidates.response)}`, OPTIONS)
if (!containerNo) return
const sourceContainerBookingId = id(candidate.sourceContainerBookingId)
if (!longId(sourceContainerBookingId)) {
blockerGap(context, '候选来源柜次身份', '候选未返回有效sourceContainerBookingId,不得仅按可复用柜号选柜')
return
}
const bookingQuery = `?sourceContainerBookingId=${encodeURIComponent(sourceContainerBookingId)}`
context.createdResources.phase4OverseasPda.containerNo = containerNo
const detail = await pda(context, token, 'GET', '查询柜号完整入仓码详情', `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}`)
const detail = await pda(context, token, 'GET', '查询柜号完整入仓码详情', `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}${bookingQuery}`)
if (!Array.isArray(detail.data?.inbounds) || detail.data.inbounds.length === 0) return
const prepareBody = { idempotencyKey: key(context, 'prepare') }
const prepared = await pda(context, token, 'POST', '选择柜号并建立WAIT_SIGN会话',
`/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare`, prepareBody)
`/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare${bookingQuery}`, prepareBody)
if (!success(prepared.response)) return
const prepareReplay = await pda(context, token, 'POST', '选择柜号同键重放',
`/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare`, prepareBody)
`/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare${bookingQuery}`, prepareBody)
context.assert(MODULE, '选择柜号幂等结果一致', prepareReplay.response,
success(prepareReplay.response) && prepareReplay.data?.containerNo === containerNo && prepareReplay.data?.status === 'WAIT_SIGN', trace(prepareReplay.response), OPTIONS)
if (secondaryToken) {
await expected(context, secondaryToken, 'POST', '第二仓库操作员抢占同柜返回409',
`/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare`,
`/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare${bookingQuery}`,
{ idempotencyKey: key(context, 'secondary-prepare') }, conflict)
}
if (runAuthorizationProbes) {
const other = candidates.data.find((item) => item.containerNo !== containerNo
&& item.operable === true && item.allPlansReady === true && Number(item.totalPackages) > 0)
if (other) await expected(context, token, 'GET', '当前账号跨柜切换锁返回409',
`/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(other.containerNo)}`, undefined, conflict)
`/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(other.containerNo)}?sourceContainerBookingId=${encodeURIComponent(other.sourceContainerBookingId)}`, undefined, conflict)
else blockerGap(context, '跨柜锁409', '本轮首柜作业前只有一个可收柜,无法在不伪造柜数据的前提下验证跨柜锁')
}
const photos = await uploadPhotos(context, token)
@@ -7,6 +7,10 @@ import { activeCustomerIdentity, inspectOceanBookingIdentity, minimalPdfBytes,
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
test('self-contained truck booking bill has its own full run namespace', () => {
assert.match(source, /billOfLadingNo: `BL-TRUCK-\$\{context.marker\}`/)
})
test('PDF fixtures contain actual PDF magic bytes', () => {
const content = Buffer.from(minimalPdfBytes('TRUCK')).toString('ascii')
assert.equal(content.slice(0, 5), '%PDF-')
@@ -143,6 +143,7 @@ async function fixture(context) {
const carrier = await ensureMatsonCarrierPartner(context, MODULE, OPTIONS)
if (!bookingAgent || !truckSupplier || !carrier?.id) return null
const draft = oceanBookingDraft(context, suffix, master, {
billOfLadingNo: `BL-TRUCK-${context.marker}`.slice(0, 64),
bookingAgentId: bookingAgent.supplierId, bookingAgentName: bookingAgent.supplierName,
shippingCompanyId: id(carrier.id), shippingCompanyName: carrier.name, carrierCode: 'MATSON',
containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', quantity: 2, capacityCbm: 68, maxWeightKg: 19500 }],