fix: align HTTP acceptance fixtures and pallet permissions
This commit is contained in:
@@ -37,6 +37,11 @@ export async function run(context) {
|
||||
|
||||
## 可用上下文
|
||||
|
||||
- 一件代发任务 `Task.pallets` 只列实际参与绑箱的托盘;未使用的预生成托盘通过 `/api/platform/v1/first-leg/pda-dropship/inbound-orders/{inboundId}/pallets?accountId=...` 读取,客户范围取本轮任务。脚本同时核对 `generatedPalletCount/palletSetupReady`,不因空的任务托盘列表新增资源。
|
||||
- `phase3-container-documents` 从本轮货柜详情读取渠道:海铁验证 DO 上传被明确拒绝且聚合中没有 DO,其他渠道继续验证 DO 上传、重放及零漂移;缺渠道不猜测适用性。
|
||||
- `phase4-overseas-pda-operations` 用三个独立逐箱任务分别验证正常换标、跳过和异常,异常不得重复使用前两箱。前一步失败时保留失败并停止该链,不继续使用空响应版本。
|
||||
- 海外 PDA 按真实柜号去重执行签收,缺少一件代发生产者不得用 FBA 生产者替代;原生产矩阵失败仍保留。P4 解绑样本保留为 `UNBOUND`,P5 只从本轮已完成同柜、同目的仓的入仓码中选择三箱实物托盘库存,排除 `UNBOUND` 和重复仓位,不借用历史订单。
|
||||
|
||||
货柜查询使用 `/api/platform/v1/containers/domestic` 列表和 `/domestic/{id}` 详情,响应为 `WorkbenchItem`(`container`、`workbenchStage`、`statusSummary`、`allowedActions`),不接受旧扁平实体。默认 `phase3-container-loading` 同时验证页面的 ACTIVE、状态、关键字组合查询及详情结构;`container-detail-route-contract.test.mjs` 离线核对 Admin、Controller 和人工分段/同柜脚本读路径,并反证旧 DTO。定向真实 HTTP 使用总入口 `--modules phase3-container-loading`,仍自动展开本轮前置并创建测试资源;不借历史柜。
|
||||
|
||||
一件代发的 `inbound-measurement` 前置在测量草稿录入预计托数;审核通过时系统一次性预生成全部 DP 托盘码,允许空托先打印。PDA 既支持先清点箱再扫预生成托盘码批量归托,也支持先扫托盘后逐箱直接归托;预计托数不得超过实测箱数。验收逐托封托并确认全部箱数已绑定、全部预生成托盘已封托后任务才完成,默认一件代发后续链复用该前置。人工验收造数可传 `--prepare-dropship-manual-count true`,脚本会停在任务未领取、箱未清点、托盘未绑定的状态。
|
||||
|
||||
@@ -123,7 +123,7 @@ test('confirmed package count reconciles an uncertain transport result before re
|
||||
})
|
||||
|
||||
async function exerciseSystemPalletScenario(fault = '') {
|
||||
const task = { taskId: '81', inboundId: '82', warehouseId: '83', taskStatus: 'CLAIMED',
|
||||
const task = { taskId: '81', inboundId: '82', warehouseId: '83', tenantId: '85', generatedPalletCount: 1, palletSetupReady: true, taskStatus: 'CLAIMED',
|
||||
totalCartons: 2, countedCartons: 0, boundCartons: 0, unboundCartons: 0, pallets: [{ palletId: '84', palletNo: 'DP-260909-0001', cartonCount: 0, status: 'OPEN', version: 1 }] }
|
||||
const seenCounts = new Map()
|
||||
const seenCreates = new Set()
|
||||
@@ -138,7 +138,9 @@ async function exerciseSystemPalletScenario(fault = '') {
|
||||
assert(_module, name, _response, condition) { checks.push({ name, passed: Boolean(condition) }) },
|
||||
async request(method, path, body) {
|
||||
if (path.includes('/pending?')) return response([task])
|
||||
if (path.endsWith('/claim') || (method === 'GET' && path.includes('/tasks/81?'))) return response(task)
|
||||
if (path.endsWith('/claim')) return response({ ...task, pallets: [] })
|
||||
if (path === '/api/platform/v1/first-leg/pda-dropship/inbound-orders/82/pallets?accountId=85') return response(task.pallets)
|
||||
if (method === 'GET' && path.includes('/tasks/81?')) return response(task)
|
||||
if (path.includes('/cartons/resolve?')) return response({ boxCode: new URL(path, 'http://fixture').searchParams.get('boxCode'), canCount: true, skuLines: [{ inboundDetailId: '91', skuCode: 'SKU-1', expectedQuantity: 10 }] })
|
||||
if (path.endsWith('/cartons/count')) {
|
||||
if (seenCounts.has(body.operationId)) return seenCounts.get(body.operationId)
|
||||
|
||||
@@ -112,12 +112,20 @@ export async function completeDropshipPalletization(context, { inboundId, inboun
|
||||
const totalCartons = Number(measurement?.actualPackageCount)
|
||||
const expectedPalletCount = Number(measurement?.expectedPalletCount)
|
||||
const taskPath = `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}?warehouseId=${warehouseId}`
|
||||
const generatedPallets = Array.isArray(claimed?.pallets) ? claimed.pallets : []
|
||||
context.assert(MODULE, '审核通过时已按预计托数预生成全部DP托盘码', claimResponse,
|
||||
generatedPallets.length === expectedPalletCount && generatedPallets.every((item) =>
|
||||
item?.status === 'OPEN' && Number(item?.cartonCount) === 0 && /^DP-\d{6}-\d{4}$/.test(item?.palletNo || '')),
|
||||
if (!claimed?.tenantId || claimed.taskStatus !== 'CLAIMED') return null
|
||||
// Task.pallets contains pallets with actual bound cartons, not unused pre-generated pallets.
|
||||
const palletResponse = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/pda-dropship/inbound-orders/${inboundId}/pallets?accountId=${asId(claimed.tenantId)}`)
|
||||
const palletData = context.apiSuccess(MODULE, '读取本轮入仓单预生成托盘', palletResponse, '', OPTIONS)
|
||||
const generatedPallets = Array.isArray(palletData) ? palletData : []
|
||||
const palletSetupValid = Number(claimed.generatedPalletCount) === expectedPalletCount
|
||||
&& claimed.palletSetupReady === true && generatedPallets.length === expectedPalletCount
|
||||
&& generatedPallets.every((item) =>
|
||||
item?.status === 'OPEN' && Number(item?.cartonCount) === 0 && /^DP-\d{6}-\d{4}$/.test(item?.palletNo || ''))
|
||||
context.assert(MODULE, '审核通过时已按预计托数预生成全部DP托盘码', palletResponse,
|
||||
palletSetupValid,
|
||||
`expected=${expectedPalletCount}, generated=${generatedPallets.length}`, OPTIONS)
|
||||
if (!generatedPallets.length) return null
|
||||
if (!palletSetupValid) return null
|
||||
|
||||
let countedCartons = 0
|
||||
const sealedCartonMarks = []
|
||||
@@ -189,13 +197,13 @@ export async function completeDropshipPalletization(context, { inboundId, inboun
|
||||
context.assert(MODULE, '待组托箱绑定到OPEN托盘且不再游离', bindResponse,
|
||||
Number(bound?.unboundCartons) === 0 && Number(pallet?.cartonCount) === 1
|
||||
&& pallet?.status === 'OPEN' && pallet?.palletNo === firstPallet.palletNo
|
||||
&& bound?.pallets?.length === expectedPalletCount,
|
||||
&& bound?.pallets?.length === 1,
|
||||
`unbound=${bound?.unboundCartons ?? 'missing'}, palletStatus=${pallet?.status ?? 'missing'}`, OPTIONS)
|
||||
const replayBindResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/pallets/bind-pending`, bindPayload)
|
||||
const replayBind = context.apiSuccess(MODULE, '预生成托盘绑定同键重放', replayBindResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '预生成托盘绑定重放不新增托盘且计数稳定', replayBindResponse,
|
||||
replayBind?.pallets?.length === expectedPalletCount
|
||||
replayBind?.pallets?.length === bound?.pallets?.length
|
||||
&& Number(replayBind?.boundCartons) === Number(bound?.boundCartons)
|
||||
&& Number(replayBind?.unboundCartons) === 0, '', OPTIONS)
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,15 @@ import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import { DOCUMENT_TYPES } from './phase3-container-documents.mjs'
|
||||
import { DOCUMENT_TYPES, applicableDocumentTypes } from './phase3-container-documents.mjs'
|
||||
|
||||
test('rail excludes DO but truck still requires it; missing channel fails closed', () => {
|
||||
for (const code of ['EXX_RAIL_NY', 'NYEXX', 'OA_CHICAGO_RAIL', 'CPHT']) {
|
||||
assert.deepEqual(applicableDocumentTypes(code), ['DESTINATION_CLEARANCE'])
|
||||
}
|
||||
assert.deepEqual(applicableDocumentTypes('EXX_TRUCK'), DOCUMENT_TYPES)
|
||||
assert.throws(() => applicableDocumentTypes(null), /权威渠道/)
|
||||
})
|
||||
import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
|
||||
|
||||
const source = readFileSync(new URL('./phase3-container-documents.mjs', import.meta.url), 'utf8')
|
||||
|
||||
@@ -6,6 +6,13 @@ export const DOCUMENT_TYPES = Object.freeze([
|
||||
'DESTINATION_CLEARANCE', 'DELIVERY_ORDER',
|
||||
])
|
||||
|
||||
export function applicableDocumentTypes(channelCode) {
|
||||
if (!String(channelCode || '').trim()) throw new Error('货柜缺少权威渠道,不能判断DO适用性')
|
||||
const seaRail = ['EXX_RAIL_NY', 'NYEXX', 'OA_CHICAGO_RAIL', 'CPHT']
|
||||
.includes(String(channelCode).trim().toUpperCase())
|
||||
return seaRail ? ['DESTINATION_CLEARANCE'] : [...DOCUMENT_TYPES]
|
||||
}
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
||||
function success(response) { return response?.http === 200 && codeOf(response) === '200' }
|
||||
@@ -115,6 +122,13 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
|
||||
const detailResponse = await context.request('GET', `${CONTAINERS}/domestic/${containerId}`)
|
||||
const detail = context.apiSuccess(MODULE, '读取本轮货柜权威渠道', detailResponse, '', OPTIONS)
|
||||
if (id(detail?.container?.id) !== containerId || !detail?.container?.channelCode) {
|
||||
context.addStep(MODULE, '货柜渠道适用性前置', detailResponse, false, '缺少同柜权威渠道', OPTIONS)
|
||||
return
|
||||
}
|
||||
const expectedTypes = applicableDocumentTypes(detail.container.channelCode)
|
||||
const specs = [
|
||||
{
|
||||
type: 'DESTINATION_CLEARANCE',
|
||||
@@ -122,7 +136,7 @@ export async function run(context) {
|
||||
readPath: `${CONTAINERS}/${containerId}/clearance`,
|
||||
completePath: `${CONTAINERS}/${containerId}/clearance/complete`,
|
||||
},
|
||||
...['DELIVERY_ORDER'].map((type) => ({
|
||||
...expectedTypes.filter((type) => type === 'DELIVERY_ORDER').map((type) => ({
|
||||
type,
|
||||
uploadPath: `${CONTAINERS}/${containerId}/documents/${type}/upload`,
|
||||
readPath: `${CONTAINERS}/${containerId}/documents/${type}`,
|
||||
@@ -131,17 +145,28 @@ export async function run(context) {
|
||||
|
||||
const documents = []
|
||||
for (const spec of specs) documents.push(await uploadAndVerify(context, spec))
|
||||
let applicabilityPassed = true
|
||||
if (!expectedTypes.includes('DELIVERY_ORDER')) {
|
||||
const rejected = await upload(context, `${CONTAINERS}/${containerId}/documents/DELIVERY_ORDER/upload`,
|
||||
'DELIVERY_ORDER', 0, `${context.marker} 海铁DO负向验收`, key(context, 'rail-do-rejected'))
|
||||
applicabilityPassed = conflict(rejected) && traced(rejected)
|
||||
&& rejected.body?.message === '海铁渠道不适用DO资料'
|
||||
context.addStep(MODULE, '海铁DO上传按渠道规则拒绝', rejected, applicabilityPassed,
|
||||
`code=${codeOf(rejected)}, requestId=${rejected.requestId}, traceId=${rejected.traceId}`, OPTIONS)
|
||||
}
|
||||
const overviewResponse = await context.request('GET', `${CONTAINERS}/${containerId}/overview`)
|
||||
const overview = context.apiSuccess(MODULE, '回查货柜作业资料聚合', overviewResponse, '', OPTIONS)
|
||||
const storedTypes = new Set((overview?.documents || []).map((item) => item?.documentType))
|
||||
const typesValid = expectedTypes.every((type) => storedTypes.has(type))
|
||||
&& (expectedTypes.includes('DELIVERY_ORDER') || !storedTypes.has('DELIVERY_ORDER'))
|
||||
context.assert(MODULE, '同一LOADED柜次聚合返回作业资料且身份未漂移', overviewResponse,
|
||||
id(overview?.container?.id) === containerId && documents.every(Boolean)
|
||||
&& DOCUMENT_TYPES.every((type) => storedTypes.has(type)),
|
||||
&& typesValid && applicabilityPassed,
|
||||
`containerBookingId=${containerId}, documentTypes=${[...storedTypes].join('|') || 'none'}`, OPTIONS)
|
||||
if (documents.every(Boolean) && DOCUMENT_TYPES.every((type) => storedTypes.has(type))) {
|
||||
if (id(overview?.container?.id) === containerId && documents.every(Boolean) && typesValid && applicabilityPassed) {
|
||||
context.createdResources.phase3ContainerDocuments = {
|
||||
marker: context.marker, containerBookingId: containerId, loadingPlanId: planId,
|
||||
warehouseId, finalStatus: 'COMPLETED', documentTypes: [...DOCUMENT_TYPES],
|
||||
warehouseId, finalStatus: 'COMPLETED', documentTypes: expectedTypes,
|
||||
documentIds: Object.fromEntries(documents.map((item) => [item.documentType, id(item.id)])),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@ import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { expandOverseasCartonCandidates, resolveCartonFixtures, run } from './phase4-overseas-pda.mjs'
|
||||
import { expandOverseasCartonCandidates, resolveCartonFixtures, run, uniqueContainerProducer } from './phase4-overseas-pda.mjs'
|
||||
|
||||
test('one physical container is received once even when matrix roles overlap', () => {
|
||||
const fba = { containerNo: 'TEST-C1', fulfillmentType: 'STANDARD' }
|
||||
const overseas = { containerNo: 'TEST-C2' }
|
||||
assert.deepEqual([null, fba, { ...fba }, overseas].filter(uniqueContainerProducer), [fba, overseas])
|
||||
assert.doesNotMatch(source, /dropship \|\| matrix/)
|
||||
})
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const source = fs.readFileSync(path.join(here, 'phase4-overseas-pda.mjs'), 'utf8')
|
||||
|
||||
@@ -5,7 +5,26 @@ import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
|
||||
import { run } from './phase4-overseas-pda-operations.mjs'
|
||||
import { run, remainingRelabelItem, selectPhysicalRelabelSources } from './phase4-overseas-pda-operations.mjs'
|
||||
|
||||
test('relabel uses three physical cartons across current inbounds, never UNBOUND or duplicate positions', () => {
|
||||
const a = { inventoryBalanceId: 'A', inboundOrderId: '1', positions: [
|
||||
{ positionId: '10', palletNo: 'REAL-A', availablePackages: 2 },
|
||||
{ positionId: '11', palletNo: 'UNBOUND', availablePackages: 1 }] }
|
||||
const b = { inventoryBalanceId: 'B', inboundOrderId: '2', positions: [
|
||||
{ positionId: '20', palletNo: 'REAL-B', availablePackages: 1 }] }
|
||||
const selected = selectPhysicalRelabelSources([b, a, a], 'A')
|
||||
assert.deepEqual(selected.map(line => line.position.palletNo), ['REAL-A', 'REAL-A', 'REAL-B'])
|
||||
assert.equal(selectPhysicalRelabelSources([a, a], 'A').length, 2)
|
||||
})
|
||||
|
||||
test('exception uses the untouched third carton, independent of response ordering', () => {
|
||||
const third = { itemId: '3', expectedPackages: 1 }
|
||||
const detail = { items: [third, { itemId: '2' }, { itemId: '1' }] }
|
||||
assert.equal(remainingRelabelItem(detail, [1, '2']), third)
|
||||
assert.equal(remainingRelabelItem(detail, ['1', '2', '3']), null)
|
||||
assert.equal(remainingRelabelItem(null, []), null)
|
||||
})
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const source = fs.readFileSync(path.join(here, 'phase4-overseas-pda-operations.mjs'), 'utf8')
|
||||
|
||||
@@ -87,7 +87,7 @@ async function expectConflict(context, token, method, name, path, body) {
|
||||
}
|
||||
|
||||
async function resolvePallet(context, token, palletCode, name = '扫码解析托盘') {
|
||||
return request(context, token, 'POST', name,
|
||||
return request(context, token, 'POST', `${name}(${palletCode})`,
|
||||
'/api/pda/v1/last-leg/pallet-operations/pallets/resolve', { code: palletCode })
|
||||
}
|
||||
|
||||
@@ -103,6 +103,11 @@ async function resolvePublicLocation(context, fixture) {
|
||||
? { locationId: id(location.id), locationCode: location.locationCode } : null
|
||||
}
|
||||
|
||||
export function remainingRelabelItem(detail, usedItemIds) {
|
||||
const used = new Set(usedItemIds.map(id))
|
||||
return (detail?.items || []).find((item) => id(item?.itemId) && !used.has(id(item.itemId))) || null
|
||||
}
|
||||
|
||||
async function resolveReusableAuxiliaryPallet(context, token, fixture) {
|
||||
for (const candidate of fixture.auxiliaryPallets || []) {
|
||||
if (!nonBlank(candidate?.palletCode) || id(candidate?.palletId) === id(fixture.pallet?.palletId)) continue
|
||||
@@ -328,6 +333,27 @@ async function verifyPalletOperations(context, token, fixture) {
|
||||
return success(unbound.response)
|
||||
}
|
||||
|
||||
export function selectPhysicalRelabelSources(candidates, preferredBalanceId) {
|
||||
const ordered = [...candidates].sort((a, b) =>
|
||||
Number(id(b.inventoryBalanceId) === id(preferredBalanceId)) - Number(id(a.inventoryBalanceId) === id(preferredBalanceId)))
|
||||
const sourceLines = []
|
||||
const seen = new Set()
|
||||
for (const source of ordered) {
|
||||
if (!id(source?.inventoryBalanceId) || !id(source?.inboundOrderId)) continue
|
||||
for (const position of source.positions || []) {
|
||||
const positionId = id(position?.positionId)
|
||||
if (!positionId || seen.has(positionId) || !nonBlank(position?.palletNo)
|
||||
|| position.palletNo.toUpperCase() === 'UNBOUND') continue
|
||||
seen.add(positionId)
|
||||
const selected = Math.min(3 - sourceLines.length, Number(position.availablePackages))
|
||||
if (!Number.isInteger(selected) || selected <= 0) continue
|
||||
for (let index = 0; index < selected; index++) sourceLines.push({ source, position, packageCount: 1 })
|
||||
if (sourceLines.length === 3) return sourceLines
|
||||
}
|
||||
}
|
||||
return sourceLines
|
||||
}
|
||||
|
||||
async function verifyRelabel(context, token, merchantToken, fixture) {
|
||||
const customerId = id(fixture.order?.order?.customerId || context.createdResources?.phase4OverseasPda?.customerId)
|
||||
const customerOrderNo = fixture.order?.order?.orderNo || fixture.order?.order?.sourceOrderNo
|
||||
@@ -338,33 +364,27 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
|
||||
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 orderedCandidates = [...inventoryCandidates].sort((left, right) => {
|
||||
const leftPreferred = id(left?.inventoryBalanceId) === id(inventory?.balanceId) ? 1 : 0
|
||||
const rightPreferred = id(right?.inventoryBalanceId) === id(inventory?.balanceId) ? 1 : 0
|
||||
return rightPreferred - leftPreferred
|
||||
})
|
||||
const sourceLines = []
|
||||
let requiredPackages = 3
|
||||
for (const source of orderedCandidates) {
|
||||
if (!id(source?.inventoryBalanceId) || !id(source?.inboundOrderId)) continue
|
||||
for (const position of source?.positions || []) {
|
||||
const available = Number(position?.availablePackages)
|
||||
if (!id(position?.positionId) || available <= 0) continue
|
||||
const selected = Math.min(requiredPackages, available)
|
||||
for (let packageIndex = 0; packageIndex < selected; packageIndex += 1) {
|
||||
sourceLines.push({ source, position, packageCount: 1 })
|
||||
}
|
||||
requiredPackages -= selected
|
||||
if (requiredPackages === 0) break
|
||||
}
|
||||
if (requiredPackages === 0) break
|
||||
// P4 intentionally leaves one carton UNBOUND. P5 requires physical source pallets,
|
||||
// and may combine only the current completed container's same-destination inbounds.
|
||||
const inboundCodes = [...new Set((context.context.phase4OverseasPdaOperations?.completedFixtures || [])
|
||||
.filter((item) => item.containerNo === fixture.containerNo && item.destinationType === fixture.destinationType
|
||||
&& item.fbaWarehouseCode === fixture.fbaWarehouseCode)
|
||||
.map((item) => item.inboundCode).filter(nonBlank))]
|
||||
const inventoryCandidates = []
|
||||
let inventoryCandidatesResponse = null
|
||||
for (const inboundCode of inboundCodes) {
|
||||
inventoryCandidatesResponse = await context.requestOverseas('GET', query(
|
||||
'/api/merchant/v1/last-leg/relabel-applications/inventory-candidates', {
|
||||
operationWarehouseId: fixture.overseasWarehouseId, inboundCode,
|
||||
}), undefined, {}, merchantToken)
|
||||
context.addStep(MODULE, 'P5读取本轮同柜同目的仓换标候选', inventoryCandidatesResponse,
|
||||
success(inventoryCandidatesResponse), trace(inventoryCandidatesResponse), OPTIONS)
|
||||
if (!success(inventoryCandidatesResponse)) return false
|
||||
inventoryCandidates.push(...records(inventoryCandidatesResponse.body?.data).filter((item) =>
|
||||
item.inboundCode === inboundCode && id(item.overseasWarehouseId) === id(fixture.overseasWarehouseId)))
|
||||
}
|
||||
const sourceLines = selectPhysicalRelabelSources(inventoryCandidates, inventory.balanceId)
|
||||
const requiredPackages = 3 - sourceLines.length
|
||||
context.addStep(MODULE, 'P5客户库存候选返回换标权威来源', inventoryCandidatesResponse,
|
||||
requiredPackages === 0,
|
||||
`sourceLines=${sourceLines.length}, selectedPackages=${3 - requiredPackages}; ${trace(inventoryCandidatesResponse)}`, OPTIONS)
|
||||
@@ -452,8 +472,13 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
|
||||
await expectConflict(context, token, 'POST', 'P5正常换标旧版本A0409', unitPath,
|
||||
{ ...scanBody, oldLabel: `${context.marker}-OLD-STALE`, idempotencyKey: key(context, 'RELABEL-STALE') })
|
||||
detail = scanned.data
|
||||
if (!detail) return false
|
||||
const skipItem = Array.isArray(detail?.items)
|
||||
? detail.items.find((candidate) => id(candidate?.itemId) && id(candidate.itemId) !== id(item.itemId)) : null
|
||||
if (!skipItem) {
|
||||
blocker(context, 'P5跳过换标明细', '缺少独立的第二箱换标任务', scanned.response)
|
||||
return false
|
||||
}
|
||||
const skipBody = { itemId: id(skipItem?.itemId), oldLabel: skipItem?.cartonMark,
|
||||
reason: `${context.marker}-skip`, expectedVersion: Number(detail.version),
|
||||
idempotencyKey: key(context, 'RELABEL-SKIP') }
|
||||
@@ -462,7 +487,12 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
|
||||
context.assert(MODULE, 'P5跳过换标重放不重复计数', skipReplay.response,
|
||||
Number(skipReplay.data?.skippedPackages) === 1, trace(skipReplay.response), OPTIONS)
|
||||
detail = skipped.data
|
||||
const exceptionItem = item
|
||||
if (!detail) return false
|
||||
const exceptionItem = remainingRelabelItem(detail, [item.itemId, skipItem.itemId])
|
||||
if (!exceptionItem) {
|
||||
blocker(context, 'P5异常换标明细', '缺少未被正常换标或跳过的第三箱任务', skipped.response)
|
||||
return false
|
||||
}
|
||||
const exceptionBody = { itemId: id(exceptionItem?.itemId), oldLabel: null,
|
||||
exceptionCode: 'DAMAGED', reason: `${context.marker}-damaged`, expectedVersion: Number(detail.version),
|
||||
idempotencyKey: key(context, 'RELABEL-EXCEPTION') }
|
||||
@@ -473,6 +503,7 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
|
||||
context.assert(MODULE, 'P5换标异常重放不重复计数', exceptionReplay.response,
|
||||
Number(exceptionReplay.data?.exceptionPackages) === 1, trace(exceptionReplay.response), OPTIONS)
|
||||
detail = exception.data
|
||||
if (!detail) return false
|
||||
|
||||
const photo = await context.requestOverseasMultipart('POST',
|
||||
`${pdaBase}/${encodeURIComponent(relabelNo)}/photos/files`, {
|
||||
|
||||
@@ -10,6 +10,10 @@ const PNG = Uint8Array.from(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function longId(value) { return /^\d+$/.test(id(value)) && id(value) !== '0' }
|
||||
function nonBlank(value) { return typeof value === 'string' && value.trim().length > 0 }
|
||||
export function uniqueContainerProducer(item, index, all) {
|
||||
return nonBlank(item?.containerNo)
|
||||
&& all.findIndex((candidate) => candidate?.containerNo === item.containerNo) === index
|
||||
}
|
||||
function success(response) { return response?.http === 200 && String(response?.body?.code) === '200' }
|
||||
function code(response) { return String(response?.body?.code ?? '') }
|
||||
function traced(response) { return nonBlank(response?.requestId) && nonBlank(response?.traceId) }
|
||||
@@ -897,9 +901,9 @@ export async function run(context) {
|
||||
const standardFba = matrix?.byDestinationType?.FBA?.find((item) => item?.fulfillmentType !== 'ONE_CLICK_DROPSHIP')
|
||||
// The sealed dropship pallet proves the indivisible handover. A separate standard FBA
|
||||
// fixture supplies multiple cartons for reversible split/merge/unbind and relabel checks.
|
||||
const producers = [dropship || matrix?.byDestinationType?.FBA?.[0], standardFba,
|
||||
const producers = [dropship, standardFba,
|
||||
matrix?.byDestinationType?.OVERSEAS?.[0]]
|
||||
.filter((item) => nonBlank(item?.containerNo))
|
||||
.filter(uniqueContainerProducer)
|
||||
const allProducers = Array.isArray(matrix?.containers) ? matrix.containers : producers
|
||||
if (allProducers.length) await awaitProducerConsumption(context, allProducers)
|
||||
if (producers.length && !(await convertReceivingContainers(context, producers))) {
|
||||
|
||||
Reference in New Issue
Block a user