test(acceptance): reconcile dropship runtime facts

This commit is contained in:
2026-09-09 02:40:42 +08:00
parent 4ccdae0665
commit 41225248d0
14 changed files with 108 additions and 41 deletions
@@ -6,17 +6,17 @@ const read = (name) => readFileSync(new URL(name, import.meta.url), 'utf8')
test('domestic container fixtures use the current workbench detail route', () => { test('domestic container fixtures use the current workbench detail route', () => {
const containerLoading = read('./phase3-container-loading.mjs') const containerLoading = read('./phase3-container-loading.mjs')
assert.ok(containerLoading.includes("context.request('GET', `${CONTAINERS}/domestic/${containerId}`)")) assert.ok(containerLoading.includes("context.request('GET', `${DOMESTIC_CONTAINERS}/${containerId}`)"))
assert.ok(!containerLoading.includes("context.request('GET', `${CONTAINERS}/${containerId}`)")) assert.ok(!containerLoading.includes("context.request('GET', `${CONTAINERS}/${containerId}`)"))
assert.ok(containerLoading.includes('const containerDetail = containerWorkbench?.container')) assert.ok(containerLoading.includes("const containerDetail = containerRecord(context.apiSuccess(MODULE, '回查货柜详情'"))
assert.ok(containerLoading.includes('const simulationBeforeLink = simulationWorkbench?.container')) assert.ok(containerLoading.includes("const simulationBeforeLink = containerRecord(context.apiSuccess(MODULE, '预装确认前读取模拟柜最新版本'"))
assert.ok(containerLoading.includes('`${CONTAINERS}/domestic?scope=ACTIVE&page=1&size=50&keyword=')) assert.ok(containerLoading.includes('`${DOMESTIC_CONTAINERS}?page=1&size=50&scope=ACTIVE&keyword='))
assert.ok(containerLoading.includes('records(activeWorkbench).map((item) => item?.container)')) assert.ok(containerLoading.includes('records(activeContainers).map(containerRecord)'))
const loading = read('./phase3-loading.mjs') const loading = read('./phase3-loading.mjs')
assert.ok(loading.includes("context.request('GET', `/api/platform/v1/containers/domestic/${containerBookingId}`)")) assert.ok(loading.includes("context.request('GET', `/api/platform/v1/containers/domestic/${containerBookingId}`)"))
assert.ok(!loading.includes("context.request('GET', `/api/platform/v1/containers/${containerBookingId}`)")) assert.ok(!loading.includes("context.request('GET', `/api/platform/v1/containers/${containerBookingId}`)"))
assert.ok(loading.includes('const latestContainer = latestWorkbench?.container')) assert.ok(loading.includes("const latestContainer = containerRecord(context.apiSuccess(MODULE, '合并前读取模拟柜最新版本'"))
const crossRegion = read('./phase4-cross-region-events.mjs') const crossRegion = read('./phase4-cross-region-events.mjs')
const fixtureId = 'encodeURIComponent(id(phase3.containerBookingId))' const fixtureId = 'encodeURIComponent(id(phase3.containerBookingId))'
@@ -36,7 +36,7 @@ test('port link mutates the simulation through the frozen four-field request', (
}) })
test('HTTP fixture keeps simulation and plan identities and creates allocation at link', () => { test('HTTP fixture keeps simulation and plan identities and creates allocation at link', () => {
assert.ok(fixture.includes("context.request('GET', `${CONTAINERS}/domestic/${containerId}`)")) assert.ok(fixture.includes("context.request('GET', `${DOMESTIC_CONTAINERS}/${containerId}`)"))
assert.ok(!fixture.includes("context.request('GET', `${CONTAINERS}/${containerId}`)")) assert.ok(!fixture.includes("context.request('GET', `${CONTAINERS}/${containerId}`)"))
assert.match(fixture, /关联前后内部柜次、计划和排货明细身份稳定/) assert.match(fixture, /关联前后内部柜次、计划和排货明细身份稳定/)
assert.match(fixture, /订舱 allocation 原子创建并指向稳定模拟柜身份/) assert.match(fixture, /订舱 allocation 原子创建并指向稳定模拟柜身份/)
@@ -110,3 +110,10 @@ test('dropship measurement completes the formal palletization workflow before sm
assert.match(source, /dropship \? \['WAIT_PALLETIZE'\] : \['WAIT_SPLIT'\]/) assert.match(source, /dropship \? \['WAIT_PALLETIZE'\] : \['WAIT_SPLIT'\]/)
assert.match(source, /一件代发合托完成后进入智能分柜门禁/) assert.match(source, /一件代发合托完成后进入智能分柜门禁/)
}) })
test('confirmed package count reconciles an uncertain transport result before retrying', () => {
assert.match(source, /async function updateConfirmedPackageCount/)
assert.match(source, /recoveredFromTransportFailure: true/)
assert.match(source, /Number\(detail\?\.version\) > Number\(body\.measurementVersion\)/)
assert.match(source, /await updateConfirmedPackageCount\(context, measurementPath/)
})
@@ -536,6 +536,29 @@ async function requestWithHeaders(context, method, path, body, extraHeaders) {
} }
} }
async function updateConfirmedPackageCount(context, measurementPath, itemId, body) {
const updatePath = measurementPath.replace(/\?.*$/, '')
+ `/items/${encodeURIComponent(itemId)}/confirmed-package-count?${measurementPath.split('?')[1] || ''}`
let lastResponse = null
for (let attempt = 1; attempt <= 2; attempt += 1) {
lastResponse = await context.request('PUT', updatePath, body)
if (lastResponse?.http !== 0) return lastResponse
const recovered = await context.request('GET', measurementPath)
const detail = recovered?.http === 200 && String(recovered?.body?.code) === '200'
? recovered.body?.data : null
const item = detail?.items?.find((candidate) => asId(candidate?.id) === asId(itemId))
if (Number(item?.confirmedPackageCount) === Number(body.confirmedPackageCount)
&& Number(detail?.version) > Number(body.measurementVersion)) {
return {
...recovered, method: 'GET', path: measurementPath, recoveredFromTransportFailure: true,
body: { ...recovered.body, data: { ...item, measurementVersion: Number(detail.version) } },
}
}
if (attempt < 2) await new Promise((resolve) => setTimeout(resolve, 500))
}
return lastResponse
}
function finishScenario(context, start) { function finishScenario(context, start) {
const steps = context.steps.slice(start) const steps = context.steps.slice(start)
context.scenarios.push({ context.scenarios.push({
@@ -651,11 +674,10 @@ export async function run(context) {
&& closeTo(firstItem?.heightCm, firstDimensions.height), && closeTo(firstItem?.heightCm, firstDimensions.height),
`itemFound=${Boolean(firstItem)}, imageFileId=${asId(firstItem?.intelligentImageFileId) || 'missing'}`, OPTIONS) `itemFound=${Boolean(firstItem)}, imageFileId=${asId(firstItem?.intelligentImageFileId) || 'missing'}`, OPTIONS)
const countResponse = await context.request('PUT', const countResponse = await updateConfirmedPackageCount(context, measurementPath, firstScan.itemId,
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/items/${firstScan.itemId}/confirmed-package-count?${query(tenantId)}`,
{ measurementVersion: Number(firstScan.measurementVersion), confirmedPackageCount: originalPackageCount }) { measurementVersion: Number(firstScan.measurementVersion), confirmedPackageCount: originalPackageCount })
const countResult = context.apiSuccess(MODULE, '人工确认当前测量代表箱数', countResponse, const countResult = context.apiSuccess(MODULE, '人工确认当前测量代表箱数', countResponse,
`confirmedPackageCount=${originalPackageCount}`, OPTIONS) `confirmedPackageCount=${originalPackageCount}, transportRecovered=${countResponse?.recoveredFromTransportFailure === true}`, OPTIONS)
context.assert(MODULE, '人工箱数更新且测量版本递增', countResponse, context.assert(MODULE, '人工箱数更新且测量版本递增', countResponse,
Number(countResult?.confirmedPackageCount) === originalPackageCount Number(countResult?.confirmedPackageCount) === originalPackageCount
&& Number(countResult?.measurementVersion) === Number(firstScan.measurementVersion) + 1, && Number(countResult?.measurementVersion) === Number(firstScan.measurementVersion) + 1,
@@ -722,8 +744,7 @@ export async function run(context) {
`detailCount=${incrementalDetailCount}, packageCounts=${detailPackageCounts.join('+')}, frozen=${originalPackageCount}`, OPTIONS) `detailCount=${incrementalDetailCount}, packageCounts=${detailPackageCounts.join('+')}, frozen=${originalPackageCount}`, OPTIONS)
if (expectedPackages === originalPackageCount) { if (expectedPackages === originalPackageCount) {
const firstDetailPackageCount = detailPackageCounts[0] const firstDetailPackageCount = detailPackageCounts[0]
const resetFirstCountResponse = await context.request('PUT', const resetFirstCountResponse = await updateConfirmedPackageCount(context, measurementPath, firstScan.itemId,
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/items/${firstScan.itemId}/confirmed-package-count?${query(tenantId)}`,
{ measurementVersion: Number(measured?.version), confirmedPackageCount: firstDetailPackageCount }) { measurementVersion: Number(measured?.version), confirmedPackageCount: firstDetailPackageCount })
const resetFirstCount = context.apiSuccess(MODULE, const resetFirstCount = context.apiSuccess(MODULE,
'增量 fixture 将首条货物确认箱数恢复为自身箱数', resetFirstCountResponse, '增量 fixture 将首条货物确认箱数恢复为自身箱数', resetFirstCountResponse,
@@ -751,8 +772,7 @@ export async function run(context) {
&& asId(scanned?.itemId) !== asId(firstScan.itemId), && asId(scanned?.itemId) !== asId(firstScan.itemId),
`itemId=${asId(scanned?.itemId) || 'missing'}, inboundDetailId=${asId(scanned?.inboundDetailId) || 'pending-query'}`, OPTIONS) `itemId=${asId(scanned?.itemId) || 'missing'}, inboundDetailId=${asId(scanned?.inboundDetailId) || 'pending-query'}`, OPTIONS)
if (!scanned?.itemId) continue if (!scanned?.itemId) continue
const countDetailResponse = await context.request('PUT', const countDetailResponse = await updateConfirmedPackageCount(context, measurementPath, scanned.itemId,
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/items/${scanned.itemId}/confirmed-package-count?${query(tenantId)}`,
{ measurementVersion: Number(scanned.measurementVersion), confirmedPackageCount: detailPackageCount }) { measurementVersion: Number(scanned.measurementVersion), confirmedPackageCount: detailPackageCount })
const countDetail = context.apiSuccess(MODULE, const countDetail = context.apiSuccess(MODULE,
`增量第${detailIndex + 1}条货物确认自身箱数`, countDetailResponse, `增量第${detailIndex + 1}条货物确认自身箱数`, countDetailResponse,
@@ -71,7 +71,7 @@ test('records LOADED and clears the reason only after a real LOADED reread', ()
test('container loading follows push, claim, real pick, completed sequence', () => { test('container loading follows push, claim, real pick, completed sequence', () => {
const value = source('./phase3-container-loading.mjs') const value = source('./phase3-container-loading.mjs')
assert.match(value, /const containerDetail = containerWorkbench\?\.container/) assert.match(value, /const containerDetail = containerRecord\(context\.apiSuccess/)
assert.match(value, /\['EDITING', 'WAIT_LOADING'\]\.includes\(containerDetail\?\.planStatus\)/) assert.match(value, /\['EDITING', 'WAIT_LOADING'\]\.includes\(containerDetail\?\.planStatus\)/)
assertOrder(value, [ assertOrder(value, [
"pushed?.status === 'EDITING'", "pushed?.status === 'EDITING'",
@@ -183,7 +183,7 @@ test('PDA receiving lifecycle is ordered from candidate claim through photos, mo
'/current/receipts/bulk', '/current/receipts/carton', '/current/receipts/bulk', '/current/receipts/carton',
"'FBA模式硬拒绝逐箱收货'", "'OVERSEAS模式硬拒绝批量收货'", "'FBA模式硬拒绝逐箱收货'", "'OVERSEAS模式硬拒绝批量收货'",
'PHOTO_ROLES.every', "finalStatus === 'COMPLETED'", 'completedContainers.push', 'PHOTO_ROLES.every', "finalStatus === 'COMPLETED'", 'completedContainers.push',
'completedContainerFacts.push', 'expectedPalletCost', 'unitPrice', 'completedContainerFacts.push', 'expectedPalletCost', 'physicalPallets', 'resolvedPallet.data?.unitPrice',
]) assert.ok(source.includes(token), token) ]) assert.ok(source.includes(token), token)
}) })
@@ -28,6 +28,7 @@ test('PDA operations run immediately after receiving and all later consumers dep
test('receiving hands off only transient credentials and completed warehouse facts', () => { test('receiving hands off only transient credentials and completed warehouse facts', () => {
assert.match(receivingSource, /context\.context\.phase4OverseasPdaOperations = \{/) assert.match(receivingSource, /context\.context\.phase4OverseasPdaOperations = \{/)
assert.match(receivingSource, /completedFixtures: \[\]/) assert.match(receivingSource, /completedFixtures: \[\]/)
assert.match(receivingSource, /merchantToken,/)
assert.match(receivingSource, /if \(!handoffReady\) \{[\s\S]*await cleanup\(\)/) assert.match(receivingSource, /if \(!handoffReady\) \{[\s\S]*await cleanup\(\)/)
assert.doesNotMatch(receivingSource, /createdResources\.phase4OverseasPdaOperations/) assert.doesNotMatch(receivingSource, /createdResources\.phase4OverseasPdaOperations/)
assert.doesNotMatch(receivingSource, /primary: primary \? \{[^}]*password/) assert.doesNotMatch(receivingSource, /primary: primary \? \{[^}]*password/)
@@ -47,6 +48,8 @@ test('P2 to P5 cover all frozen endpoints, replays and conflict boundaries witho
for (const marker of ['按原数计数', '同键重放', '同键异载荷A0409', '旧版本A0409', for (const marker of ['按原数计数', '同键重放', '同键异载荷A0409', '旧版本A0409',
'SUBMITTED', '移回原库位', 'WAIT_REVIEW', 'REWORK', 'APPROVE', 'COMPLETED', 'SUBMITTED', '移回原库位', 'WAIT_REVIEW', 'REWORK', 'APPROVE', 'COMPLETED',
'chargeAmount: 0.01', 'cartonMark:', 'fbaCartonMark:', 'chargeAmount: 0.01', 'cartonMark:', 'fbaCartonMark:',
'inventory-candidates', 'sourceInventoryBalanceId:', 'sourceInventoryPositionId:',
'sourceInboundOrderId:', 'operationWarehouseId:',
'P5换标费用冻结后开放PDA作业', 'P5换标完成后钱包结算收敛']) { 'P5换标费用冻结后开放PDA作业', 'P5换标完成后钱包结算收敛']) {
assert.ok(source.includes(marker), marker) assert.ok(source.includes(marker), marker)
} }
@@ -78,6 +81,7 @@ test('fixture failure still disables both picker accounts and removes all transi
marker: 'PDAOPS-CONTRACT', marker: 'PDAOPS-CONTRACT',
primary: { membershipId: '1', username: 'primary', token: 'primary-secret-token' }, primary: { membershipId: '1', username: 'primary', token: 'primary-secret-token' },
secondary: { membershipId: '2', username: 'secondary', token: 'secondary-secret-token' }, secondary: { membershipId: '2', username: 'secondary', token: 'secondary-secret-token' },
merchantToken: 'merchant-secret-token',
completedFixtures: [], completedFixtures: [],
async cleanup() { cleanups.push('secondary', 'primary') }, async cleanup() { cleanups.push('secondary', 'primary') },
} }
@@ -95,6 +99,7 @@ test('fixture failure still disables both picker accounts and removes all transi
assert.deepEqual(cleanups, ['secondary', 'primary']) assert.deepEqual(cleanups, ['secondary', 'primary'])
assert.equal(handoff.primary.token, undefined) assert.equal(handoff.primary.token, undefined)
assert.equal(handoff.secondary.token, undefined) assert.equal(handoff.secondary.token, undefined)
assert.equal(handoff.merchantToken, undefined)
assert.equal(context.context.phase4OverseasPdaOperations, undefined) assert.equal(context.context.phase4OverseasPdaOperations, undefined)
assert.ok(gaps.some((gap) => gap[2] === 'P2-P5本轮PDA收货交接')) assert.ok(gaps.some((gap) => gap[2] === 'P2-P5本轮PDA收货交接'))
assert.ok(steps.some((step) => !step.passed && step.options?.deploymentBlocker)) assert.ok(steps.some((step) => !step.passed && step.options?.deploymentBlocker))
@@ -328,29 +328,51 @@ async function verifyPalletOperations(context, token, fixture) {
return success(unbound.response) return success(unbound.response)
} }
async function verifyRelabel(context, token, fixture) { async function verifyRelabel(context, token, merchantToken, fixture) {
const customerId = id(fixture.order?.order?.customerId || context.createdResources?.phase4OverseasPda?.customerId) const customerId = id(fixture.order?.order?.customerId || context.createdResources?.phase4OverseasPda?.customerId)
const customerOrderNo = fixture.order?.order?.orderNo || fixture.order?.order?.sourceOrderNo const customerOrderNo = fixture.order?.order?.orderNo || fixture.order?.order?.sourceOrderNo
|| `PDA-${context.marker}` || `PDA-${context.marker}`
const inventory = fixture.inventory.find((item) => nonBlank(item?.skuCode)) const inventory = fixture.inventory.find((item) => nonBlank(item?.skuCode))
if (!customerId || !inventory?.skuCode || !fixture.overseasWarehouseId) { if (!customerId || !inventory?.skuCode || !fixture.overseasWarehouseId || !nonBlank(merchantToken)) {
blocker(context, 'P5本轮库存换标申请', '缺少本轮完成库存的客户、SKU或海外仓事实') blocker(context, 'P5本轮库存换标申请', '缺少本轮完成库存的客户、SKU、海外仓或客户令牌事实')
return false return false
} }
if (!await fundRelabelWallet(context, customerId)) return false if (!await fundRelabelWallet(context, customerId)) return false
const inventoryCandidatesResponse = await context.requestOverseas('GET', query(
'/api/merchant/v1/last-leg/relabel-applications/inventory-candidates', {
operationWarehouseId: fixture.overseasWarehouseId, inboundCode: fixture.inboundCode,
}), undefined, {}, merchantToken)
const inventoryCandidates = success(inventoryCandidatesResponse)
&& Array.isArray(inventoryCandidatesResponse.body?.data) ? inventoryCandidatesResponse.body.data : []
const source = inventoryCandidates.find((item) => id(item?.inventoryBalanceId) === id(inventory?.balanceId))
|| inventoryCandidates.find((item) => item?.skuCode === inventory.skuCode
&& item?.inboundCode === fixture.inboundCode)
const sourcePosition = source?.positions?.find((item) => id(item?.positionId) === id(inventory?.positionId)
&& Number(item?.availablePackages) >= 3)
|| source?.positions?.find((item) => Number(item?.availablePackages) >= 3)
context.addStep(MODULE, 'P5客户库存候选返回换标权威来源', inventoryCandidatesResponse,
Boolean(source) && Boolean(sourcePosition) && id(source?.inboundOrderId) !== '',
`balanceId=${id(source?.inventoryBalanceId) || 'missing'}, positionId=${id(sourcePosition?.positionId) || 'missing'}, inboundOrderId=${id(source?.inboundOrderId) || 'missing'}; ${trace(inventoryCandidatesResponse)}`, OPTIONS)
if (!source || !sourcePosition || !id(source.inboundOrderId)) {
blocker(context, 'P5本轮库存换标来源', '客户库存候选未返回本轮库存的余额、至少三箱位置和来源入库单', inventoryCandidatesResponse)
return false
}
const relabelBase = '/api/platform/v1/last-leg/relabel-applications' const relabelBase = '/api/platform/v1/last-leg/relabel-applications'
const pdaBase = '/api/pda/v1/last-leg/relabel-tasks' const pdaBase = '/api/pda/v1/last-leg/relabel-tasks'
const newSku = `${inventory.skuCode}-NEW-${String(context.marker).slice(-8)}`.slice(0, 128) const newSku = `${inventory.skuCode}-NEW-${String(context.marker).slice(-8)}`.slice(0, 128)
const createBody = { const createBody = {
customerOrderNo: String(customerOrderNo).slice(0, 128), overseasWarehouseId: id(fixture.overseasWarehouseId), customerOrderNo: String(customerOrderNo).slice(0, 128), operationWarehouseId: id(fixture.overseasWarehouseId),
labelDeadline: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 19), labelDeadline: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 19),
contactName: 'PDA换标验收', contactPhone: '13800000006', chargeAmount: 0.01, remark: context.marker, contactName: 'PDA换标验收', contactPhone: '13800000006', chargeAmount: 0.01, remark: context.marker,
idempotencyKey: key(context, 'RELABEL-CREATE'), idempotencyKey: key(context, 'RELABEL-CREATE'),
items: [{ skuCode: inventory.skuCode, productNameZh: inventory.productName || '本轮库存商品', items: [{ sourceInventoryBalanceId: id(source.inventoryBalanceId),
sourceInventoryPositionId: id(sourcePosition.positionId), sourceInboundOrderId: id(source.inboundOrderId),
skuCode: source.skuCode, productNameZh: source.productNameZh || inventory.productName || '本轮库存商品',
totalPackageCount: 3, notifiedPackageCount: 3, shipmentNo: `SHIP-${context.marker}`.slice(0, 128), totalPackageCount: 3, notifiedPackageCount: 3, shipmentNo: `SHIP-${context.marker}`.slice(0, 128),
newSkuCode: newSku, labelRequirement: context.marker, newSkuCode: newSku, labelRequirement: context.marker,
cartonMark: `CM-${String(context.marker).slice(-32)}`, cartonMark: `CM-${String(context.marker).slice(-32)}`,
fbaCartonMark: `FBA-${String(context.marker).slice(-32)}` }], fbaCartonMark: `FBA-${String(context.marker).slice(-32)}`,
destinationFbaWarehouseCode: source.destinationFbaWarehouseCode }],
} }
const createdResponse = await context.requestOverseas('POST', const createdResponse = await context.requestOverseas('POST',
query(relabelBase, { customerId }), createBody) query(relabelBase, { customerId }), createBody)
@@ -469,6 +491,7 @@ async function verifyRelabel(context, token, fixture) {
export async function run(context) { export async function run(context) {
const handoff = context.context?.phase4OverseasPdaOperations const handoff = context.context?.phase4OverseasPdaOperations
const token = handoff?.primary?.token const token = handoff?.primary?.token
const merchantToken = handoff?.merchantToken
try { try {
if (handoff?.marker !== context.marker || !nonBlank(token) if (handoff?.marker !== context.marker || !nonBlank(token)
|| !Array.isArray(handoff.completedFixtures) || handoff.completedFixtures.length === 0) { || !Array.isArray(handoff.completedFixtures) || handoff.completedFixtures.length === 0) {
@@ -488,13 +511,14 @@ export async function run(context) {
await verifyStocktake(context, token, fixture) await verifyStocktake(context, token, fixture)
await verifyInventoryMove(context, token, fixture) await verifyInventoryMove(context, token, fixture)
await verifyPalletOperations(context, token, fixture) await verifyPalletOperations(context, token, fixture)
await verifyRelabel(context, token, fixture) await verifyRelabel(context, token, merchantToken, fixture)
} finally { } finally {
try { try {
if (typeof handoff?.cleanup === 'function') await handoff.cleanup() if (typeof handoff?.cleanup === 'function') await handoff.cleanup()
} finally { } finally {
if (handoff?.primary) delete handoff.primary.token if (handoff?.primary) delete handoff.primary.token
if (handoff?.secondary) delete handoff.secondary.token if (handoff?.secondary) delete handoff.secondary.token
if (handoff) delete handoff.merchantToken
delete context.context.phase4OverseasPdaOperations delete context.context.phase4OverseasPdaOperations
} }
} }
@@ -212,6 +212,8 @@ async function verifyWarehouseQueries(context, token, inboundCode, pallet, desti
&& Number(resolvedPallet.data?.occupiedPackages) > 0, && Number(resolvedPallet.data?.occupiedPackages) > 0,
`palletId=${id(resolvedPallet.data?.palletId) || 'missing'}; ${trace(resolvedPallet.response)}`, OPTIONS) `palletId=${id(resolvedPallet.data?.palletId) || 'missing'}; ${trace(resolvedPallet.response)}`, OPTIONS)
if (Number(resolvedPallet.data?.unitPrice) > 0) pallet.unitPrice = Number(resolvedPallet.data.unitPrice)
const locationCode = resolvedPallet.data?.locationCode || pallet.locationCode const locationCode = resolvedPallet.data?.locationCode || pallet.locationCode
const location = await pda(context, token, 'POST', '完成入库后扫码查询库位', const location = await pda(context, token, 'POST', '完成入库后扫码查询库位',
'/api/pda/v1/last-leg/locations/resolve', { code: locationCode }) '/api/pda/v1/last-leg/locations/resolve', { code: locationCode })
@@ -693,12 +695,6 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
context.addStep(MODULE, '完成后current返回404正常空态', after, notFound(after), trace(after), OPTIONS) context.addStep(MODULE, '完成后current返回404正常空态', after, notFound(after), trace(after), OPTIONS)
context.createdResources.phase4OverseasPda.finalStatus = completed.data?.status || null context.createdResources.phase4OverseasPda.finalStatus = completed.data?.status || null
if (context.createdResources.phase4OverseasPda.finalStatus === 'COMPLETED') { if (context.createdResources.phase4OverseasPda.finalStatus === 'COMPLETED') {
context.createdResources.phase4OverseasPda.completedContainerFacts.push({
containerNo,
containerBookingId: id(preferredProducer?.containerBookingId),
expectedPalletCost: [...pallets.values()].reduce((sum, item) => sum + Number(item.unitPrice || 0), 0),
palletIds: [...pallets.values()].map((item) => item.palletId),
})
const order = await captureCompletedOrder(context, containerNo, preferredProducer?.tenantId) const order = await captureCompletedOrder(context, containerNo, preferredProducer?.tenantId)
for (const inbound of current.data.inbounds.filter((item) => pallets.has(item.inboundCode))) { for (const inbound of current.data.inbounds.filter((item) => pallets.has(item.inboundCode))) {
const warehouseFacts = await verifyWarehouseQueries(context, token, inbound.inboundCode, const warehouseFacts = await verifyWarehouseQueries(context, token, inbound.inboundCode,
@@ -711,6 +707,14 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
}) })
} }
} }
const physicalPallets = [...new Map([...pallets.values()]
.map((item) => [id(item.palletId), item])).values()]
context.createdResources.phase4OverseasPda.completedContainerFacts.push({
containerNo,
containerBookingId: id(preferredProducer?.containerBookingId),
expectedPalletCost: physicalPallets.reduce((sum, item) => sum + Number(item.unitPrice || 0), 0),
palletIds: physicalPallets.map((item) => item.palletId),
})
} }
} }
@@ -747,6 +751,7 @@ export async function run(context) {
primary: primary ? { membershipId: primary.membershipId, username: primary.username, token: primary.token } : null, primary: primary ? { membershipId: primary.membershipId, username: primary.username, token: primary.token } : null,
secondary: secondary ? { membershipId: secondary.membershipId, username: secondary.username, token: secondary.token } : null, secondary: secondary ? { membershipId: secondary.membershipId, username: secondary.username, token: secondary.token } : null,
overseasWarehouseId: sharedWarehouse.id, overseasWarehouseId: sharedWarehouse.id,
merchantToken,
completedFixtures: [], completedFixtures: [],
cleanup, cleanup,
} }
@@ -75,7 +75,7 @@ test('HTTP scenario freezes normal, invalid, replay, concurrency, partial and PD
test('container API uses one stable long string identity', () => { test('container API uses one stable long string identity', () => {
const endpoint = openapi.slice(openapi.indexOf(` ${CONTAINER_CONVERT_ENDPOINT}:`), const endpoint = openapi.slice(openapi.indexOf(` ${CONTAINER_CONVERT_ENDPOINT}:`),
openapi.indexOf(' /api/platform/v1/last-leg/dispatches:', openapi.indexOf(` ${CONTAINER_CONVERT_ENDPOINT}:`))) openapi.indexOf(' /api/platform/v1/last-leg/dispatches:', openapi.indexOf(` ${CONTAINER_CONVERT_ENDPOINT}:`)))
assert.match(endpoint, /required: \[sourceContainerBookingId\]/) assert.match(endpoint, /required: \[sourceContainerBookingId, snapshotVersion, snapshotHash\]/)
assert.match(endpoint, /sourceContainerBookingId:\s*\{ type: string/) assert.match(endpoint, /sourceContainerBookingId:\s*\{ type: string/)
assert.match(controller, /convertContainer/) assert.match(controller, /convertContainer/)
assert.match(adminApi, /sourceContainerBookingId: string/) assert.match(adminApi, /sourceContainerBookingId: string/)
+3 -3
View File
@@ -11,9 +11,9 @@
"services": 15 "services": 15
}, },
"openapi": { "openapi": {
"documents": 39, "documents": 43,
"coveredServices": 8, "coveredServices": 10,
"codeFirstServices": 7 "codeFirstServices": 5
}, },
"reports": { "reports": {
"jacocoPresent": 21, "jacocoPresent": 21,
+3 -3
View File
@@ -65,9 +65,9 @@ test('repository freezes the current topology and non-blocking aggregate summary
const summary = await buildQualitySummary(repositoryRoot) const summary = await buildQualitySummary(repositoryRoot)
const baseline = JSON.parse(await readFile(path.join(repositoryRoot, 'scripts', 'java-quality-baseline.json'), 'utf8')) const baseline = JSON.parse(await readFile(path.join(repositoryRoot, 'scripts', 'java-quality-baseline.json'), 'utf8'))
assert.deepEqual(summary.topology, { modules: 22, codeModules: 21, services: 15 }) assert.deepEqual(summary.topology, { modules: 22, codeModules: 21, services: 15 })
assert.equal(summary.openapi.documents, 40) assert.equal(summary.openapi.documents, 43)
assert.equal(summary.openapi.coveredServices.length, 8) assert.equal(summary.openapi.coveredServices.length, 10)
assert.equal(summary.openapi.codeFirstServices.length, 7) assert.equal(summary.openapi.codeFirstServices.length, 5)
assert.equal(baseline.mode, 'report-only') assert.equal(baseline.mode, 'report-only')
assert.deepEqual(baseline.topology, summary.topology) assert.deepEqual(baseline.topology, summary.topology)
assert.equal(baseline.modules.length, 21) assert.equal(baseline.modules.length, 21)
@@ -80,6 +80,9 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([
'firstleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '价目文件上传无幂等键,读取可重复'), 'firstleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '价目文件上传无幂等键,读取可重复'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/PriceBookWarehouseClient.java', feign('firstleg/src/main/java/com/baiyue/firstleg/client/PriceBookWarehouseClient.java',
'firstleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取仓库/渠道主数据'), 'firstleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取仓库/渠道主数据'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/PrintableOrderInboundClient.java',
'firstleg-service', 'order-service', ['REQUEST'], 'WRITE', 'BUSINESS_KEY',
'首次打印按客户与订单预留稳定入仓码,重复调用复用已冻结快照'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/PrintPdfClient.java', feign('firstleg/src/main/java/com/baiyue/firstleg/client/PrintPdfClient.java',
'firstleg-service', 'print-service', ['REQUEST'], 'READ', 'SAFE_READ', '无状态 PDF 渲染,POST 为计算型读取'), 'firstleg-service', 'print-service', ['REQUEST'], 'READ', 'SAFE_READ', '无状态 PDF 渲染,POST 为计算型读取'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/TruckAppointmentFileClient.java', feign('firstleg/src/main/java/com/baiyue/firstleg/client/TruckAppointmentFileClient.java',
@@ -154,6 +157,8 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([
'gateway-overseas', 'auth-service', ['INFRASTRUCTURE'], 'READ', 'SAFE_READ', 'POST PDA 会话校验,失败关闭'), 'gateway-overseas', 'auth-service', ['INFRASTRUCTURE'], 'READ', 'SAFE_READ', 'POST PDA 会话校验,失败关闭'),
http('lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/gateway/HttpCustomerVisitFileGateway.java', http('lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/gateway/HttpCustomerVisitFileGateway.java',
'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '回访附件上传无幂等键,归属校验可重复'), 'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '回访附件上传无幂等键,归属校验可重复'),
http('lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/gateway/HttpExceptionEvidenceFileGateway.java',
'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '异常处理证据上传无幂等键,归属校验可重复'),
http('lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpUnloadPhotoFileGateway.java', http('lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpUnloadPhotoFileGateway.java',
'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '上传无幂等键,元数据校验可重复'), 'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '上传无幂等键,元数据校验可重复'),
http('lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java', http('lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java',
@@ -98,15 +98,15 @@ async function discoverFeignEdges() {
return clients.sort((left, right) => left.path.localeCompare(right.path)) return clients.sort((left, right) => left.path.localeCompare(right.path))
} }
test('all 55 Feign clients have reviewed context, access and idempotency metadata', async () => { test('all 56 Feign clients have reviewed context, access and idempotency metadata', async () => {
const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'FEIGN') const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'FEIGN')
.map(({ path, source, target }) => ({ path, source, target })) .map(({ path, source, target }) => ({ path, source, target }))
.sort((left, right) => left.path.localeCompare(right.path)) .sort((left, right) => left.path.localeCompare(right.path))
assert.equal(reviewed.length, 55) assert.equal(reviewed.length, 56)
assert.deepEqual(await discoverFeignEdges(), reviewed) assert.deepEqual(await discoverFeignEdges(), reviewed)
}) })
test('all 19 raw HTTP transports have reviewed context, access and idempotency metadata', async () => { test('all 20 raw HTTP transports have reviewed context, access and idempotency metadata', async () => {
const discovered = [] const discovered = []
for (const module of ['common-audit', 'event-bridge-service', 'gateway-overseas', 'lastleg-service', 'integration-service']) { for (const module of ['common-audit', 'event-bridge-service', 'gateway-overseas', 'lastleg-service', 'integration-service']) {
for (const file of await javaFiles(path.join(root, module, 'src', 'main', 'java'))) { for (const file of await javaFiles(path.join(root, module, 'src', 'main', 'java'))) {
@@ -118,7 +118,7 @@ test('all 19 raw HTTP transports have reviewed context, access and idempotency m
} }
const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'HTTP') const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'HTTP')
.map(client => client.path).sort() .map(client => client.path).sort()
assert.equal(reviewed.length, 19) assert.equal(reviewed.length, 20)
assert.deepEqual(discovered.sort(), reviewed) assert.deepEqual(discovered.sort(), reviewed)
}) })
@@ -226,7 +226,8 @@ test('Order and Firstleg remain a bilateral REQUEST SCC until loading-complete t
assert.deepEqual(firstlegToOrder.map(client => client.path).sort(), [ assert.deepEqual(firstlegToOrder.map(client => client.path).sort(), [
'firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java', 'firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java',
'firstleg/src/main/java/com/baiyue/firstleg/client/OrderFulfillmentSplitClient.java', 'firstleg/src/main/java/com/baiyue/firstleg/client/OrderFulfillmentSplitClient.java',
'firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java' 'firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java',
'firstleg/src/main/java/com/baiyue/firstleg/client/PrintableOrderInboundClient.java'
]) ])
assert.ok(stronglyConnectedComponents(graphEdges('REQUEST')).some(component => assert.ok(stronglyConnectedComponents(graphEdges('REQUEST')).some(component =>
component.includes('firstleg-service') && component.includes('order-service'))) component.includes('firstleg-service') && component.includes('order-service')))