const MODULE = 'phase4-overseas-pda' const SCENARIO = 'phase4-overseas-pda-container-receiving' const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true } const OBSERVATION_OPTIONS = { scenario: SCENARIO, deploymentBlocker: false } const PHOTO_ROLES = ['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN'] const PNG = Uint8Array.from(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')) 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 } 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) } function conflict(response) { return (response?.http === 409 || (response?.http === 200 && ['409', 'A0409'].includes(code(response)))) && traced(response) } function a0409Message(response, pattern) { return response?.http === 200 && code(response) === 'A0409' && traced(response) && pattern.test(String(response?.body?.message || '')) } function a0404Message(response, pattern) { return response?.http === 200 && code(response) === 'A0404' && traced(response) && pattern.test(String(response?.body?.message || '')) } function cartonResolveRejected(response) { return response?.http === 200 && ['A0400', 'A0404', 'A0409'].includes(code(response)) && traced(response) } function notFound(response) { return (response?.http === 404 || (response?.http === 200 && ['404', 'A0404'].includes(code(response)))) && traced(response) } function forbidden(response) { return [401, 403].includes(response?.http) || (response?.http === 200 && ['A0401', 'A0403'].includes(code(response))) } function trace(response) { return `HTTP=${response?.http ?? 'missing'}, code=${response?.body?.code ?? 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}` } function key(context, action, suffix = '') { return `${context.marker}-${context.pdaContainerSuffix || ''}-${action}-${suffix}`.replace(/[^A-Za-z0-9:_-]/g, '').slice(0, 120) } function markerSuffix(context) { return `${context.marker}${context.pdaContainerSuffix || ''}`.replace(/[^A-Za-z0-9]/g, '').slice(-16).toUpperCase() } function records(data) { return Array.isArray(data?.records) ? data.records : Array.isArray(data) ? data : [] } function receivingSnapshot(data) { return JSON.stringify({ containerNo: data?.containerNo, status: data?.status, version: data?.version, totalPackages: data?.totalPackages, receivedPackages: data?.receivedPackages, pendingPackages: data?.pendingPackages, inbounds: (Array.isArray(data?.inbounds) ? data.inbounds : []).map((item) => ({ inboundCode: item?.inboundCode, destinationType: item?.destinationType, totalPackages: item?.totalPackages, receivedPackages: item?.receivedPackages, pendingPackages: item?.pendingPackages, })).sort((left, right) => `${left.inboundCode}:${left.destinationType}`.localeCompare(`${right.inboundCode}:${right.destinationType}`)), }) } export function expandOverseasCartonCandidates(inbounds, producerCartonMarks = []) { const marks = (Array.isArray(producerCartonMarks) ? producerCartonMarks : [producerCartonMarks]) .map(id).filter(nonBlank) return inbounds.filter((item) => item?.destinationType === 'OVERSEAS').flatMap((inbound) => { const patterns = marks.map((cartonMark) => ({ cartonMark, match: /^(.*)#(\d+)-(\d+)$/.exec(cartonMark) })) .filter(({ match }) => match && match[1] === inbound.inboundCode) const totals = [...new Set(patterns.map(({ match }) => Number(match[2])) .filter((total) => Number.isSafeInteger(total) && total > 0 && total <= 2000))] if (totals.length !== 1) return [] return Array.from({ length: totals[0] }, (_, index) => `${inbound.inboundCode}#${totals[0]}-${index + 1}`) }) } function blockerGap(context, name, reason, category = 'fixture-unavailable', response = null) { context.addCoverageGap(MODULE, SCENARIO, name, reason, category, { deploymentBlocker: true }) context.assert(MODULE, `${name}覆盖前置`, response, false, reason, OPTIONS) } async function pda(context, token, method, name, path, body = undefined) { const response = await context.requestOverseas(method, path, body, {}, token) context.addStep(MODULE, name, response, success(response), trace(response), OPTIONS) return { response, data: success(response) ? response.body?.data : null } } async function expected(context, token, method, name, path, body, predicate) { const response = await context.requestOverseas(method, path, body, {}, token) context.addStep(MODULE, name, response, predicate(response), trace(response), OPTIONS) return response } async function captureCompletedOrder(context, containerNo, expectedCustomerId) { const response = await context.requestOverseas('GET', `/api/platform/v1/last-leg/orders?page=1&size=200&containerNo=${encodeURIComponent(containerNo)}`) const candidates = records(response?.body?.data) .filter((item) => id(item?.containerNo) === id(containerNo)) const exactCustomer = expectedCustomerId == null ? candidates : candidates.filter((item) => id(item?.customerId) === id(expectedCustomerId)) const order = exactCustomer[0] || candidates[0] const mapped = success(response) && longId(order?.id) && nonBlank(id(order?.customerId)) context.assert(MODULE, '完成柜回查平台尾程订单并冻结跨区身份', response, mapped, `containerNo=${containerNo}, orderId=${id(order?.id) || 'missing'}, customerId=${id(order?.customerId) || 'missing'}; ${trace(response)}`, OPTIONS) if (mapped) { context.createdResources.phase4OverseasPda.orderId = id(order.id) context.createdResources.phase4OverseasPda.customerId = id(order.customerId) const detailPath = `/api/platform/v1/last-leg/orders/${encodeURIComponent(id(order.id))}?customerId=${encodeURIComponent(id(order.customerId))}` const detail = await context.requestOverseas('GET', detailPath) context.assert(MODULE, '完成柜回查尾程订单货物明细供后续PDA作业', detail, success(detail) && id(detail.body?.data?.order?.id) === id(order.id) && Array.isArray(detail.body?.data?.cargo) && detail.body.data.cargo.length > 0, `orderId=${id(order.id)}, cargo=${detail.body?.data?.cargo?.length ?? 'missing'}; ${trace(detail)}`, OPTIONS) return success(detail) ? detail.body?.data : null } return null } async function loginMerchantForNegativeProbe(context) { const fixture = context.context?.merchantQualificationFixture if (!fixture?.username || !fixture?.password) return null const response = await context.request('POST', '/api/auth/login', { loginId: fixture.username, password: fixture.password }, '') const data = success(response) ? response.body?.data : null context.addStep(MODULE, '本轮客户登录取得海外令牌用于PDA越权反证', response, success(response) && nonBlank(data?.overseasAccessToken), `overseasTokenPresent=${nonBlank(data?.overseasAccessToken)}; ${trace(response)}`, OPTIONS) return nonBlank(data?.overseasAccessToken) ? data.overseasAccessToken : null } export async function resolvePickerAccess(context) { const rolesResponse = await context.request('GET', '/api/platform/v1/iam/roles') const roles = context.apiSuccess(MODULE, '查询海外PDA仓库操作员角色', rolesResponse, '', OPTIONS) const role = (Array.isArray(roles) ? roles : []).find((item) => item?.roleCode === 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' && item?.status === 'ACTIVE') const permissionsResponse = await context.request('GET', '/api/platform/v1/iam/permissions') const permissions = context.apiSuccess(MODULE, '查询海外PDA权限资源', permissionsResponse, '', OPTIONS) const operate = (Array.isArray(permissions) ? permissions : []).find((item) => item?.permissionCode === 'lastleg:pda:operate') const assigned = Boolean(role && operate && (role.permissionIds || []).map(id).includes(id(operate.id))) context.assert(MODULE, 'OVERSEAS_WAREHOUSE_PDA_OPERATOR精确具备海外PDA权限', rolesResponse, assigned, `roleId=${id(role?.id) || 'missing'}, permissionId=${id(operate?.id) || 'missing'}`, OPTIONS) if (!assigned) { blockerGap(context, '海外PDA仓库操作员权限', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR未获得lastleg:pda:operate;必须先执行最新国内统一增量SQL并部署IAM') return null } return { role, operate } } export async function createPicker(context, index, overseasWarehouseId, access) { const role = access.role const suffix = markerSuffix(context) const username = `pda_overseas_${suffix.toLowerCase()}_${index}`.slice(0, 64) const password = `Pda${suffix.replace(/[^A-Za-z0-9]/g, '')}A${index}9` const phone = await context.findAvailablePhone(MODULE) const payload = { username, displayName: `海外PDA验收操作员${index}-${suffix}`, phone, email: `${username}@example.com`, tenantId: '0', roleIds: [id(role.id)], overseasWarehouseId, password, status: 'ACTIVE' } const createdResponse = await context.request('POST', '/api/platform/v1/iam/users', payload) const created = context.apiSuccess(MODULE, `创建海外PDA仓库操作员${index}`, createdResponse, '', OPTIONS) const membershipId = id(created?.membershipId) context.assert(MODULE, `海外PDA操作员${index}冻结平台范围与角色`, createdResponse, longId(membershipId) && id(created?.tenantId) === '0' && id(created?.overseasWarehouseId) === id(overseasWarehouseId) && (created?.roleIds || []).map(id).includes(id(role.id)), `membershipId=${membershipId || 'missing'}, tenantId=${id(created?.tenantId) || 'missing'}, overseasWarehouseId=${id(created?.overseasWarehouseId) || 'missing'}`, OPTIONS) if (!longId(membershipId)) return null const login = await context.request('POST', '/api/auth/login', { loginId: username, password }, '') const token = success(login) ? login.body?.data?.overseasAccessToken : null context.addStep(MODULE, `海外PDA操作员${index}登录取得双区域海外令牌`, login, nonBlank(token), `overseasTokenPresent=${nonBlank(token)}; ${trace(login)}`, OPTIONS) if (!nonBlank(token)) return { membershipId, username, token: null } let projectionResponse = null let projectionReady = false let projectionAttempt = 0 for (; projectionAttempt < 30; projectionAttempt += 1) { projectionResponse = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/current', undefined, {}, token) projectionReady = success(projectionResponse) || notFound(projectionResponse) if (projectionReady || !forbidden(projectionResponse)) break await new Promise((resolve) => setTimeout(resolve, 500)) } context.addStep(MODULE, `海外PDA操作员${index}安全投影已收敛`, projectionResponse, projectionReady, `attempts=${projectionAttempt + 1}; ${trace(projectionResponse)}`, OPTIONS) return { membershipId, username, token: projectionReady ? token : null } } export async function disablePicker(context, picker, index) { if (!longId(picker?.membershipId)) return const response = await context.request('PUT', `/api/platform/v1/iam/users/${picker.membershipId}/status`, { status: 'DISABLED' }) const disabled = success(response) && response.body?.data?.status === 'DISABLED' context.addStep(MODULE, `finally停用海外PDA仓库操作员${index}`, response, disabled, trace(response), OPTIONS) } function rememberGeneratedPlans(context, plans) { if (!Array.isArray(plans)) return for (const plan of plans) { const inboundCode = id(plan?.lines?.[0]?.inboundCode) if (!nonBlank(inboundCode)) continue const generated = Array.isArray(plan?.pallets) ? plan.pallets : [] context.createdResources.phase4OverseasPda.generatedPalletsByInbound[inboundCode] = generated.map((pallet) => ({ warehouseId: id(plan?.overseasWarehouseId), locationId: id(pallet?.warehouseLocationId), palletId: id(pallet?.warehousePalletId), palletCode: id(pallet?.palletNo), unitPrice: Number(pallet?.unitPrice || 0), retained: true, })) } } async function resolveGeneratedPallet(context, inbound) { const candidates = context.createdResources.phase4OverseasPda.generatedPalletsByInbound[inbound.inboundCode] || [] const pallet = candidates[0] if (!pallet || !longId(pallet.palletId) || !nonBlank(pallet.palletCode)) return null const warehouseId = id(inbound?.overseasWarehouseId || pallet.warehouseId) const listed = await context.requestOverseas('GET', `/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets?page=1&size=10&keyword=${encodeURIComponent(pallet.palletCode)}`) const stored = records(listed?.body?.data).find((row) => id(row?.id) === pallet.palletId && row?.palletCode === pallet.palletCode) const locations = stored && longId(stored?.locationId) ? await context.requestOverseas('GET', `/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/locations?page=1&size=200`) : null const location = records(locations?.body?.data).find((row) => id(row?.id) === id(stored?.locationId)) const resolved = success(listed) && success(locations) && stored && longId(stored?.locationId) && nonBlank(location?.locationCode) context.addStep(MODULE, `复用入仓码${inbound.inboundCode}自动计划托盘`, listed, resolved, `palletCode=${pallet.palletCode}, palletId=${pallet.palletId}, locationId=${id(stored?.locationId) || 'missing'}, locationCode=${location?.locationCode || 'missing'}; ${trace(listed)}`, OPTIONS) if (!resolved) return null return { ...pallet, warehouseId, locationId: id(stored.locationId), locationCode: location.locationCode, unitPrice: Number(stored.unitPrice || pallet.unitPrice || 0) } } async function verifyWarehouseQueries(context, token, inboundCode, pallet, destinationType) { const inventory = await pda(context, token, 'POST', '完成入库后按入仓码现场查询库存', '/api/pda/v1/last-leg/inventory/resolve', { code: inboundCode }) context.assert(MODULE, '现场库存查询只返回Token绑定仓且包含库存位置', inventory.response, success(inventory.response) && inventory.data?.queryCode === inboundCode && Array.isArray(inventory.data?.items) && inventory.data.items.length > 0 && inventory.data.items.every((item) => id(item?.palletId) === pallet.palletId && id(item?.locationId) === pallet.locationId && Number(item?.availablePackages) > 0), `items=${inventory.data?.items?.length ?? 'missing'}; ${trace(inventory.response)}`, OPTIONS) const inventoryItems = Array.isArray(inventory.data?.items) ? inventory.data.items : [] context.assert(MODULE, `${destinationType}实收形成服务端SKU库存账`, inventory.response, success(inventory.response) && inventoryItems.length > 0 && inventoryItems.every((item) => nonBlank(item?.skuCode) && Number(item?.availablePackages) > 0), `skuCodes=${[...new Set(inventoryItems.map((item) => item?.skuCode).filter(nonBlank))].join('|') || 'missing'}, availablePackages=${inventoryItems.reduce((total, item) => total + Number(item?.availablePackages || 0), 0)}; ${trace(inventory.response)}`, OPTIONS) const location = await pda(context, token, 'POST', '完成入库后扫码查询库位', '/api/pda/v1/last-leg/locations/resolve', { code: pallet.locationCode }) context.assert(MODULE, '库位查询返回当前仓真实占用汇总', location.response, success(location.response) && id(location.data?.locationId) === pallet.locationId && location.data?.locationCode === pallet.locationCode && Number(location.data?.occupiedPackages) > 0 && Number(location.data?.inventoryPositionCount) > 0, `locationId=${id(location.data?.locationId) || 'missing'}; ${trace(location.response)}`, OPTIONS) const resolvedPallet = await pda(context, token, 'POST', '完成入库后扫码查询托盘内容', '/api/pda/v1/last-leg/pallets/resolve', { code: pallet.palletCode }) context.assert(MODULE, '托盘查询返回当前位置与库存明细', resolvedPallet.response, success(resolvedPallet.response) && id(resolvedPallet.data?.palletId) === pallet.palletId && id(resolvedPallet.data?.locationId) === pallet.locationId && Array.isArray(resolvedPallet.data?.inventory) && resolvedPallet.data.inventory.length > 0 && Number(resolvedPallet.data?.occupiedPackages) > 0, `palletId=${id(resolvedPallet.data?.palletId) || 'missing'}; ${trace(resolvedPallet.response)}`, OPTIONS) const history = await pda(context, token, 'GET', '查询当前仓管PDA作业历史', '/api/pda/v1/last-leg/operations?page=1&size=50') const historyRows = records(history.data) context.assert(MODULE, '作业历史包含本轮柜号收货动作且分页稳定', history.response, success(history.response) && historyRows.length > 0 && historyRows.some((item) => String(item?.operationType || '').startsWith('RECEIVING_')) && Number(history.data?.page) === 1 && Number(history.data?.size) === 50, `records=${historyRows.length}; ${trace(history.response)}`, OPTIONS) return success(inventory.response) && success(location.response) && success(resolvedPallet.response) ? { inboundCode, pallet: resolvedPallet.data, location: location.data, inventory: inventory.data?.items || [] } : null } async function uploadPhotos(context, token) { const malicious = await context.requestOverseasMultipart('POST', '/api/pda/v1/last-leg/receiving-containers/current/unload-photos/files', { name: `${context.marker}-forged.png`, type: 'image/png', content: Uint8Array.from(Buffer.from('')), }, token) context.addStep(MODULE, '伪造图片内容安全扫描返回A0400', malicious, malicious?.http === 200 && code(malicious) === 'A0400' && traced(malicious), trace(malicious), OPTIONS) const photos = [] for (const role of PHOTO_ROLES) { const response = await context.requestOverseasMultipart('POST', '/api/pda/v1/last-leg/receiving-containers/current/unload-photos/files', { name: `${context.marker}-${role.toLowerCase()}.png`, type: 'image/png', content: PNG, }, token) const fileId = id(response?.body?.data?.fileId) context.addStep(MODULE, `上传${role}真实签收照片`, response, success(response) && longId(fileId), `role=${role}, fileId=${fileId || 'missing'}; ${trace(response)}`, OPTIONS) if (!longId(fileId)) return null photos.push({ role, fileId }) } return photos } async function awaitProducerConsumption(context, producers) { const expected = [...new Set(producers.map((item) => id(item?.containerNo)).filter(nonBlank))] if (!expected.length) return true const consumed = new Set() const lastResponses = new Map() for (let attempt = 1; attempt <= 90; attempt += 1) { const missing = expected.filter((containerNo) => !consumed.has(containerNo)) const results = await Promise.all(missing.map(async (containerNo) => { const response = await context.requestOverseas('GET', `/api/platform/v1/last-leg/orders?page=1&size=200&containerNo=${encodeURIComponent(containerNo)}`) return { containerNo, response } })) for (const { containerNo, response } of results) { lastResponses.set(containerNo, response) const rows = records(response?.body?.data) if (success(response) && rows.some((item) => id(item?.containerNo) === containerNo)) { consumed.add(containerNo) } } if (expected.every((containerNo) => consumed.has(containerNo))) break await new Promise((resolve) => setTimeout(resolve, 1000)) } for (const containerNo of expected) { const response = lastResponses.get(containerNo) || null context.addStep(MODULE, `跨区广播消费观察 ${containerNo}`, response, true, `containerNo=${containerNo}, consumed=${consumed.has(containerNo)}; ${trace(response)}`, OBSERVATION_OPTIONS) } return expected.every((containerNo) => consumed.has(containerNo)) } async function convertReceivingContainers(context, producers) { for (const producer of producers) { const containerNo = id(producer?.containerNo) const summaryResponse = await context.requestOverseas('GET', `/api/platform/v1/last-leg/orders/container-summaries?page=1&size=20&containerNo=${encodeURIComponent(containerNo)}`, undefined, {}, context.overseasToken) const summaries = success(summaryResponse) && Array.isArray(summaryResponse.body?.data?.records) ? summaryResponse.body.data.records : [] const summary = summaries.find((item) => id(item?.containerNo) === containerNo) const totalOrders = Number(summary?.totalOrderCount) const inboundCodes = Number(summary?.inboundCodeCount) const existingPlans = Number(summary?.planCount) const pendingPlans = Number(summary?.pendingPlanCount) const convertible = nonBlank(summary?.sourceContainerBookingId) && totalOrders > 0 && inboundCodes === totalOrders && Number(summary?.missingInboundCodeCount) === 0 && ((existingPlans === 0 && pendingPlans === totalOrders) || (summary?.allPlansReady === true && existingPlans === totalOrders && pendingPlans === 0)) context.addStep(MODULE, `读取待分配柜级摘要 ${containerNo}`, summaryResponse, convertible, `sourceContainerBookingId=${id(summary?.sourceContainerBookingId) || 'missing'}, allPlansReady=${summary?.allPlansReady === true}, orders=${totalOrders}, inboundCodes=${inboundCodes}, plans=${existingPlans}, pending=${pendingPlans}; ${trace(summaryResponse)}`, OPTIONS) if (!convertible) return false const bookingId = id(summary.sourceContainerBookingId) const previewPath = `/api/platform/v1/last-leg/inbound-plans/container-preview?sourceContainerBookingId=${encodeURIComponent(bookingId)}` const previewResponse = await context.requestOverseas('GET', previewPath, undefined, {}, context.overseasToken) const preview = success(previewResponse) ? previewResponse.body?.data : null context.addStep(MODULE, `读取PDA前柜级计划与自动托盘预览 ${containerNo}`, previewResponse, preview?.ready === true && Number(preview?.totalOrderCount) === totalOrders && Number(preview?.pendingOrderCount) === pendingPlans, `orders=${preview?.totalOrderCount ?? 'missing'}, pendingOrders=${preview?.pendingOrderCount ?? 'missing'}, pendingPallets=${preview?.pendingPalletCount ?? 'missing'}; ${trace(previewResponse)}`, OPTIONS) if (preview?.ready !== true) return false if (pendingPlans > 0) { const body = { sourceContainerBookingId: bookingId, snapshotVersion: Number(preview.snapshotVersion), snapshotHash: preview.snapshotHash } const key = `${context.marker}-pda-convert-${containerNo}`.slice(0, 128) const convertedResponse = await context.requestOverseas('POST', '/api/platform/v1/last-leg/inbound-plans/convert-container', body, { 'Idempotency-Key': key }, context.overseasToken) const plans = success(convertedResponse) && Array.isArray(convertedResponse.body?.data) ? convertedResponse.body.data : [] const convertedInboundCodes = new Set(plans .flatMap((item) => Array.isArray(item?.lines) ? item.lines : []) .map((line) => id(line?.inboundCode)).filter(nonBlank)) context.addStep(MODULE, `整柜转换为逐入仓码独立计划及实体托盘 ${containerNo}`, convertedResponse, plans.length === inboundCodes && plans.every((item) => Array.isArray(item?.lines) && item.lines.length > 0 && Array.isArray(item?.pallets) && item.pallets.length > 0 && item.pallets.every((pallet) => nonBlank(pallet?.palletNo) && longId(id(pallet?.warehousePalletId)))) && convertedInboundCodes.size === inboundCodes, `plans=${plans.length}, inboundCodes=${convertedInboundCodes.size}, expected=${inboundCodes}; ${trace(convertedResponse)}`, OPTIONS) if (plans.length !== inboundCodes) return false const actualPalletCodes = plans.flatMap((item) => Array.isArray(item?.pallets) ? item.pallets : []) .map((pallet) => id(pallet?.palletNo)).filter(nonBlank).sort() const expectedPalletCodes = (producer?.palletLabelManifest?.groups || []) .flatMap((group) => group?.labels || []).map((label) => id(label?.palletCode)).filter(nonBlank).sort() if (producer?.destinationType === 'FBA' && producer?.fulfillmentType !== 'ONE_CLICK_DROPSHIP') { context.assert(MODULE, `头程 FBA 散货托盘码跨区原样用于实体托盘 ${containerNo}`, convertedResponse, expectedPalletCodes.length > 0 && JSON.stringify(actualPalletCodes) === JSON.stringify(expectedPalletCodes), `source=${expectedPalletCodes.join(',') || 'missing'}, overseas=${actualPalletCodes.join(',') || 'missing'}`, OPTIONS) } if (producer?.destinationType === 'OVERSEAS' && producer?.fulfillmentType !== 'ONE_CLICK_DROPSHIP') { context.assert(MODULE, `标准海外仓订单由海外仓生成本地实体托盘码 ${containerNo}`, convertedResponse, expectedPalletCodes.length === 0 && actualPalletCodes.length > 0 && actualPalletCodes.every((code) => code.startsWith('PLT-')), `sourceCount=${expectedPalletCodes.length}, overseas=${actualPalletCodes.join(',') || 'missing'}`, OPTIONS) } rememberGeneratedPlans(context, plans) const replayResponse = await context.requestOverseas('POST', '/api/platform/v1/last-leg/inbound-plans/convert-container', body, { 'Idempotency-Key': key }, context.overseasToken) context.addStep(MODULE, `整柜转换幂等重放零新增 ${containerNo}`, replayResponse, success(replayResponse) && JSON.stringify(replayResponse.body?.data) === JSON.stringify(plans), trace(replayResponse), OPTIONS) if (!success(replayResponse)) return false } else { const retainedPlans = context.createdResources?.phase4TailOrderControls?.generatedPlans rememberGeneratedPlans(context, retainedPlans) context.addStep(MODULE, `复用尾程控制已生成的整柜计划与实体托盘 ${containerNo}`, previewResponse, summary?.allPlansReady === true && existingPlans === totalOrders && Number(preview?.existingPlannedPalletCount) > 0, `plans=${existingPlans}, plannedPallets=${preview?.existingPlannedPalletCount ?? 'missing'}; ${trace(previewResponse)}`, OPTIONS) } const progressResponse = await context.requestOverseas('GET', `/api/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(summary.sourceContainerBookingId)}`, undefined, {}, context.overseasToken) const progress = success(progressResponse) ? progressResponse.body?.data?.summary : null context.addStep(MODULE, `PDA前整柜计划完整度闭环 ${containerNo}`, progressResponse, progress?.allPlansReady === true && Number(progress?.planCount) === totalOrders && Number(progress?.pendingPlanCount) === 0, `allPlansReady=${progress?.allPlansReady === true}, plans=${progress?.planCount ?? 'missing'}, pending=${progress?.pendingPlanCount ?? 'missing'}, plannedArrivalAt=${progress?.plannedArrivalAt || 'missing'}; ${trace(progressResponse)}`, OPTIONS) if (progress?.allPlansReady !== true) return false } return true } async function awaitPickupReadyCandidates(context, token, producers, allProducers) { const expected = [...new Set(producers.map((item) => id(item?.containerNo)).filter(nonBlank))] const expectedSet = new Set(expected) const awaiting = [...new Set(allProducers .filter((item) => item?.shippingReady === false && !expectedSet.has(id(item?.containerNo))) .map((item) => id(item?.containerNo)).filter(nonBlank))] const known = new Set(allProducers.map((item) => id(item?.containerNo)).filter(nonBlank)) if (!expected.length) return false let response = null let candidatesByContainer = new Map() for (let attempt = 1; attempt <= 30; attempt += 1) { response = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/candidates', undefined, {}, token) const candidateRows = success(response) && Array.isArray(response.body?.data) ? response.body.data : [] candidatesByContainer = new Map(candidateRows .map((item) => [id(item?.containerNo), item]).filter(([containerNo]) => nonBlank(containerNo))) if (expected.every((containerNo) => candidatesByContainer.get(containerNo)?.operable === true) && awaiting.every((containerNo) => candidatesByContainer.get(containerNo)?.operable !== true)) break await new Promise((resolve) => setTimeout(resolve, 1000)) } const seenExpected = expected.filter((containerNo) => candidatesByContainer.has(containerNo)) const missingPickupReady = expected.filter((containerNo) => candidatesByContainer.get(containerNo)?.operable !== true) const unexpectedAwaiting = awaiting.filter((containerNo) => candidatesByContainer.get(containerNo)?.operable === true) const unexpectedHistorical = [...candidatesByContainer.keys()].filter((containerNo) => !known.has(containerNo)) const targetCorrelated = producers.every((item) => nonBlank(item?.resourceKey) && item?.shippingReady !== false && candidatesByContainer.get(id(item?.containerNo))?.operable === true) const ready = missingPickupReady.length === 0 && unexpectedAwaiting.length === 0 && targetCorrelated context.addStep(MODULE, '实际处理柜全部达到PICKUP_READY并进入PDA候选', response, ready, `targetCorrelated=${targetCorrelated}, expected=${expected.join('|')}, awaiting=${awaiting.join('|') || 'none'}, seenExpected=${seenExpected.join('|') || 'none'}, missingPickupReady=${missingPickupReady.join('|') || 'none'}, unexpectedAwaiting=${unexpectedAwaiting.join('|') || 'none'}, unexpectedHistorical=${unexpectedHistorical.join('|') || 'none'}; ${trace(response)}`, OPTIONS) return ready } async function resolveCartonFixtures(context, token, inbounds, producerCartonMarks = []) { const manifestCartons = (Array.isArray(producerCartonMarks) ? producerCartonMarks : [producerCartonMarks]) .map(id).filter(nonBlank) const fbaCartons = inbounds.filter((item) => item.destinationType === 'FBA') .map((item, index) => `${item.inboundCode}#PDA-CONTRACT-${index + 1}`) const overseasCartons = expandOverseasCartonCandidates(inbounds, manifestCartons) const cartonCandidates = [...new Set([...fbaCartons, ...overseasCartons])] if (!cartonCandidates.length) { blockerGap(context, '统一箱码解析链', 'FBA缺少当前柜入仓码或OVERSEAS缺少真实manifest箱码') return null } const cartonFixtures = [] for (const [index, cartonMark] of cartonCandidates.entries()) { const response = await context.requestOverseas('POST', '/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark }, {}, token) if (overseasCartons.includes(cartonMark) && a0404Message(response, /^Carton mark was not found$/)) { context.addStep(MODULE, `排除未实装箱码候选${index + 1}`, response, true, `cartonMark=${cartonMark}; ${trace(response)}`, OPTIONS) continue } const resolved = response.body?.data const matches = inbounds.filter((item) => item.inboundCode === resolved?.inboundCode) const destinations = new Set(matches.map((item) => item?.destinationType).filter(nonBlank)) const unique = success(response) && nonBlank(resolved?.inboundCode) && !Array.isArray(resolved?.inboundCode) && matches.length === 1 && destinations.size === 1 && ['FBA', 'OVERSEAS'].includes(resolved?.destinationType) && destinations.has(resolved?.destinationType) context.addStep(MODULE, `统一解析${resolved?.destinationType || 'UNKNOWN'}箱码${index + 1}并返回唯一入仓码`, response, unique, `cartonMark=${cartonMark}, inboundCode=${resolved?.inboundCode || 'missing'}, matches=${matches.length}; ${trace(response)}`, OPTIONS) if (!unique) { blockerGap(context, `箱码${cartonMark}唯一归属`, `FBA箱码与OVERSEAS真实manifest箱码必须HTTP 200/code 200并返回当前柜唯一入仓码;inboundCode=${resolved?.inboundCode || 'missing'}, matches=${matches.length}`, 'fixture-mismatch', response) return null } cartonFixtures.push({ cartonMark, inbound: matches[0], resolved }) } return cartonFixtures } async function probeCartonResolveNegatives(context, token, current) { const fba = current.inbounds.find((item) => item.destinationType === 'FBA') await expected(context, token, 'POST', '无#箱码被服务端拒绝', '/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark: fba?.inboundCode || `NO-HASH-${markerSuffix(context)}` }, cartonResolveRejected) await expected(context, token, 'POST', '空#前缀箱码被服务端拒绝', '/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark: `#${markerSuffix(context)}` }, cartonResolveRejected) await expected(context, token, 'POST', '未命中入仓码前缀被服务端拒绝', '/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark: `UNMATCHED-${markerSuffix(context)}#ANY-SUFFIX` }, cartonResolveRejected) const allProducers = context.createdResources?.phase4InboundProducerFixtures?.containers || [] const crossContainerMark = allProducers .filter((item) => id(item?.containerNo) !== id(current.containerNo)) .flatMap((item) => Array.isArray(item?.cartonMarks) ? item.cartonMarks : [item?.cartonMark]) .map(id).find(nonBlank) if (crossContainerMark) { await expected(context, token, 'POST', '其他柜箱码前缀被当前锁定柜拒绝', '/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark: crossContainerMark }, cartonResolveRejected) } else { blockerGap(context, '箱码跨柜负向', '缺少另一真实柜的冻结箱码,不能伪造跨柜归属反证') } const overseas = current.inbounds.find((item) => item.destinationType === 'OVERSEAS') if (overseas) { const total = Math.max(1, Number(overseas.totalPackages || 1)) await expected(context, token, 'POST', 'OVERSEAS缺少真实manifest行时不得按标准箱码推导', '/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark: `${overseas.inboundCode}#${total + 1}-1` }, (response) => a0404Message(response, /^Carton mark was not found$/)) } } async function receiveFba(context, token, inbound, pallet, cartonFixtures, index) { const resolvedCartons = cartonFixtures.filter((item) => item.inbound.inboundCode === inbound.inboundCode && item.resolved.destinationType === 'FBA') if (!resolvedCartons.length) { blockerGap(context, `FBA入仓码${inbound.inboundCode}箱码解析`, '没有冻结箱码以200/code 200唯一解析到当前FBA入仓码') return false } const packageCount = Number(inbound.pendingPackages || 0) if (packageCount <= 0) return true const body = { inboundCode: inbound.inboundCode, palletCode: pallet.palletCode, packageCount, idempotencyKey: key(context, `fba-bulk-${index}`) } const first = await pda(context, token, 'POST', `FBA入仓码${inbound.inboundCode}按箱数实收`, '/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', body) if (!success(first.response)) return false const replay = await pda(context, token, 'POST', `FBA入仓码${inbound.inboundCode}同键同载荷重放`, '/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', body) context.assert(MODULE, `FBA入仓码${inbound.inboundCode}幂等结果一致`, replay.response, success(replay.response) && JSON.stringify(replay.data) === JSON.stringify(first.data), trace(replay.response), OPTIONS) await expected(context, token, 'POST', `FBA入仓码${inbound.inboundCode}同键异载荷冲突`, '/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', { ...body, packageCount: packageCount + 1 }, conflict) await expected(context, token, 'POST', `FBA入仓码${inbound.inboundCode}超收被拒绝`, '/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', { ...body, packageCount: Number(inbound.totalPackages) + 1, idempotencyKey: key(context, `fba-over-${index}`) }, conflict) return true } async function receiveOverseas(context, token, inbounds, pallets, cartonFixtures) { const expectedPending = inbounds.reduce((total, inbound) => total + Number(inbound.pendingPackages || 0), 0) const overseasFixtures = cartonFixtures.filter((item) => item.resolved.destinationType === 'OVERSEAS' && inbounds.some((inbound) => inbound.inboundCode === item.inbound.inboundCode)) .map((item) => ({ cartonMark: item.cartonMark, pallet: pallets.get(item.inbound.inboundCode) })) const fixturesConserved = overseasFixtures.length === expectedPending && overseasFixtures.every((item) => item.pallet) context.assert(MODULE, 'OVERSEAS实装箱唛与待收箱数守恒', null, fixturesConserved, `resolvedFixtures=${overseasFixtures.length}, pendingPackages=${expectedPending}`, OPTIONS) if (!fixturesConserved) { blockerGap(context, 'OVERSEAS逐箱冻结事实', `统一解析结果未完整覆盖本柜真实冻结箱码;resolvedFixtures=${overseasFixtures.length}, pendingPackages=${expectedPending}`, 'fixture-mismatch') return false } for (const [index, fixture] of overseasFixtures.entries()) { const { cartonMark, pallet } = fixture const body = { cartonMark, palletCode: pallet.palletCode, idempotencyKey: key(context, `overseas-carton-${index}`) } const first = await pda(context, token, 'POST', `OVERSEAS逐箱实收${index + 1}`, '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', body) if (!success(first.response)) return false const replay = await pda(context, token, 'POST', `OVERSEAS逐箱${index + 1}同键同载荷重放`, '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', body) context.assert(MODULE, `OVERSEAS逐箱${index + 1}幂等结果一致`, replay.response, success(replay.response) && JSON.stringify(replay.data) === JSON.stringify(first.data), trace(replay.response), OPTIONS) await expected(context, token, 'POST', `OVERSEAS逐箱${index + 1}同键异载荷冲突`, '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', { ...body, palletCode: `${pallet.palletCode}-MUTATED` }, conflict) await expected(context, token, 'POST', `OVERSEAS重复箱新键被拒绝${index + 1}`, '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', { ...body, idempotencyKey: key(context, `overseas-duplicate-${index}`) }, conflict) } return true } async function runReceiving(context, token, secondaryToken, merchantToken, preferredProducer = null, runAuthorizationProbes = true) { if (runAuthorizationProbes && context.overseasToken) { const denied = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/candidates', undefined, {}, context.overseasToken) context.addStep(MODULE, '平台管理员海外令牌不能冒充仓库PDA操作员', denied, forbidden(denied) && traced(denied), trace(denied), OPTIONS) } if (runAuthorizationProbes && merchantToken) { const denied = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/candidates', undefined, {}, merchantToken) context.addStep(MODULE, '客户海外令牌不能冒充仓库PDA操作员', denied, forbidden(denied) && traced(denied), trace(denied), OPTIONS) } const before = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/current', undefined, {}, token) const noCurrent = notFound(before) context.addStep(MODULE, '无当前柜返回404属于正常空态', before, noCurrent || success(before), `${noCurrent ? 'empty=true' : `resumeStatus=${before?.body?.data?.status || 'missing'}`}; ${trace(before)}`, OPTIONS) if (success(before)) { blockerGap(context, '可重复的完整柜号验收', `账号已有未完成柜 ${before.body?.data?.containerNo || 'unknown'},本轮不覆盖或篡改遗留现场会话`, 'dirty-fixture', before) return } const candidates = await pda(context, token, 'GET', '查询当前客户待收柜号', '/api/pda/v1/last-leg/receiving-containers/candidates') if (!Array.isArray(candidates.data) || candidates.data.length === 0) { blockerGap(context, 'PDA柜号候选', `当前客户没有由三期装柜完成事件生成的待收入仓计划;${trace(candidates.response)}`, 'producer-chain', candidates.response) return } const preferredContainer = id(preferredProducer?.containerNo) || context.createdResources?.phase3Loading?.containerNo const candidate = preferredContainer ? candidates.data.find((item) => item.containerNo === preferredContainer) : candidates.data[0] if (!candidate) { blockerGap(context, `本轮柜号${preferredContainer || 'missing'}候选`, '三期装柜完成生产者已落地,但海外候选尚未出现该柜;禁止回退处理历史或其他柜', 'producer-chain', candidates.response) return } const containerNo = id(candidate?.containerNo) context.assert(MODULE, '柜号候选来自本轮生产链且包含进度', candidates.response, nonBlank(containerNo) && Number(candidate?.totalPackages) > 0 && Number(candidate?.receivedPackages) >= 0, `containerNo=${containerNo || 'missing'}, total=${candidate?.totalPackages ?? 'missing'}; ${trace(candidates.response)}`, OPTIONS) if (!containerNo) return context.createdResources.phase4OverseasPda.containerNo = containerNo const detail = await pda(context, token, 'GET', '查询柜号完整入仓码详情', `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}`) 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) if (!success(prepared.response)) return const prepareReplay = await pda(context, token, 'POST', '选择柜号同键重放', `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare`, 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`, { 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) else blockerGap(context, '跨柜锁409', '本轮首柜作业前只有一个可收柜,无法在不伪造柜数据的前提下验证跨柜锁') } const photos = await uploadPhotos(context, token) if (!photos) return const signBody = { photos, idempotencyKey: key(context, 'sign') } const signed = await pda(context, token, 'POST', '绑定车牌封签开柜三角色照片并签收', '/api/pda/v1/last-leg/receiving-containers/current/sign', signBody) if (!success(signed.response)) return const signReplay = await pda(context, token, 'POST', '签收同键同载荷重放', '/api/pda/v1/last-leg/receiving-containers/current/sign', signBody) context.assert(MODULE, '三角色照片各一次且签收幂等', signReplay.response, success(signReplay.response) && signReplay.data?.status === 'SIGNED' && PHOTO_ROLES.every((role) => signReplay.data?.photos?.filter((item) => item.role === role).length === 1), trace(signReplay.response), OPTIONS) await expected(context, token, 'POST', '签收同键异载荷冲突', '/api/pda/v1/last-leg/receiving-containers/current/sign', { ...signBody, photos: [...photos].reverse() }, conflict) const current = await pda(context, token, 'GET', '签收后按账号恢复当前柜', '/api/pda/v1/last-leg/receiving-containers/current') if (!success(current.response)) return const beforeResolve = receivingSnapshot(current.data) await probeCartonResolveNegatives(context, token, current.data) const producerCartonMarks = preferredProducer?.cartonMarks?.length ? preferredProducer.cartonMarks : [preferredProducer?.cartonMark] const cartonFixtures = await resolveCartonFixtures(context, token, current.data.inbounds, producerCartonMarks) if (!cartonFixtures) return const afterResolve = await pda(context, token, 'GET', '箱码解析正负向后回查当前柜零写入', '/api/pda/v1/last-leg/receiving-containers/current') context.assert(MODULE, 'FBA与OVERSEAS箱码resolve均为纯查询且零状态写入', afterResolve.response, success(afterResolve.response) && receivingSnapshot(afterResolve.data) === beforeResolve, `before=${beforeResolve}, after=${receivingSnapshot(afterResolve.data)}; ${trace(afterResolve.response)}`, OPTIONS) if (!success(afterResolve.response) || receivingSnapshot(afterResolve.data) !== beforeResolve) return const pallets = new Map() for (const [index, inbound] of current.data.inbounds.entries()) { const pallet = await resolveGeneratedPallet(context, inbound) if (!pallet) { blockerGap(context, `${inbound.destinationType}入仓码${inbound.inboundCode}自动计划托盘`, `按柜确认没有为该入仓码留下可解析的实体托盘;禁止在PDA收货时临时新建脱离计划的托盘`, 'warehouse-resource') continue } pallets.set(inbound.inboundCode, pallet) context.createdResources.phase4OverseasPda.palletIds.push(pallet.palletId) const resolved = await pda(context, token, 'POST', `解析${inbound.destinationType}实物托盘`, '/api/pda/v1/last-leg/receiving-containers/current/pallets/resolve', { palletCode: pallet.palletCode, inboundCode: inbound.inboundCode }) context.assert(MODULE, `${inbound.destinationType}托盘目的模式匹配`, resolved.response, success(resolved.response) && resolved.data?.compatible === true && resolved.data?.destinationType === inbound.destinationType, trace(resolved.response), OPTIONS) } const fbaInbounds = current.data.inbounds.filter((item) => item.destinationType === 'FBA') const overseasInbounds = current.data.inbounds.filter((item) => item.destinationType === 'OVERSEAS') if (fbaInbounds.length && fbaInbounds.every((item) => pallets.has(item.inboundCode))) { const firstFba = fbaInbounds[0] const fbaCarton = cartonFixtures.find((item) => item.resolved.destinationType === 'FBA' && item.inbound.inboundCode === firstFba.inboundCode) if (!fbaCarton) { blockerGap(context, `FBA入仓码${firstFba.inboundCode}箱码反证`, '没有冻结FBA箱码可用于验证carton收货精确A0409') return } await expected(context, token, 'POST', 'FBA模式硬拒绝逐箱收货', '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', { cartonMark: fbaCarton?.cartonMark, palletCode: pallets.get(firstFba.inboundCode).palletCode, idempotencyKey: key(context, 'fba-carton-forbidden'), }, (response) => a0409Message(response, /^FBA destination must use bulk count$/)) for (const [index, inbound] of fbaInbounds.entries()) { await receiveFba(context, token, inbound, pallets.get(inbound.inboundCode), cartonFixtures, index) } } else if (preferredProducer?.destinationType !== 'OVERSEAS') { blockerGap(context, 'FBA按箱数链', '当前FBA柜没有带真实托盘的 FBA 入仓码') } if (overseasInbounds.length && overseasInbounds.every((item) => pallets.has(item.inboundCode))) { const firstOverseas = overseasInbounds[0] const pallet = pallets.get(firstOverseas.inboundCode) await expected(context, token, 'POST', 'OVERSEAS模式硬拒绝批量收货', '/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', { inboundCode: firstOverseas.inboundCode, palletCode: pallet.palletCode, packageCount: 1, idempotencyKey: key(context, 'overseas-bulk-forbidden'), }, (response) => a0409Message(response, /^OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden$/)) await receiveOverseas(context, token, overseasInbounds, pallets, cartonFixtures) } else if (preferredProducer?.destinationType !== 'FBA') { blockerGap(context, 'OVERSEAS逐箱链', '当前OVERSEAS柜没有带真实冻结箱唛与托盘的 OVERSEAS 入仓码') } const refreshed = await pda(context, token, 'GET', '回查柜内逐入仓码实收进度', '/api/pda/v1/last-leg/receiving-containers/current') if (!success(refreshed.response) || Number(refreshed.data?.pendingPackages) !== 0) { blockerGap(context, '完成当前柜', `柜内实收尚未精确核平;pendingPackages=${refreshed.data?.pendingPackages ?? 'missing'}`, 'receiving-incomplete', refreshed.response) return } const completeBody = { idempotencyKey: key(context, 'complete') } const completed = await pda(context, token, 'POST', '完成当前柜并写入库存与卸柜事件', '/api/pda/v1/last-leg/receiving-containers/current/complete', completeBody) if (!success(completed.response)) return const completeReplay = await pda(context, token, 'POST', '完成当前柜同键同载荷重放', '/api/pda/v1/last-leg/receiving-containers/current/complete', completeBody) context.assert(MODULE, '完成幂等重放仍返回COMPLETED', completeReplay.response, success(completeReplay.response) && completeReplay.data?.status === 'COMPLETED', trace(completeReplay.response), OPTIONS) const after = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/current', undefined, {}, token) context.addStep(MODULE, '完成后current返回404正常空态', after, notFound(after), trace(after), OPTIONS) context.createdResources.phase4OverseasPda.finalStatus = completed.data?.status || null 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) for (const inbound of current.data.inbounds.filter((item) => pallets.has(item.inboundCode))) { const warehouseFacts = await verifyWarehouseQueries(context, token, inbound.inboundCode, pallets.get(inbound.inboundCode), inbound.destinationType) if (warehouseFacts && context.context?.phase4OverseasPdaOperations) { context.context.phase4OverseasPdaOperations.completedFixtures.push({ containerNo, order, destinationType: inbound.destinationType, fbaWarehouseCode: inbound.destinationType === 'FBA' ? inbound.fbaWarehouseCode : null, ...warehouseFacts, }) } } } } export async function run(context) { context.createdResources.phase4OverseasPda = { marker: context.marker, containerNo: null, customerId: null, orderId: null, palletIds: [], locationIds: [], finalStatus: null, pickerMembershipIds: [], completedContainers: [], completedContainerFacts: [], overseasWarehouseId: null, overseasWarehouseName: null, generatedPalletsByInbound: {}, cleanup: 'Shared warehouse, pallets and locations used by completed inventory are retained as acceptance evidence', } const merchantToken = await loginMerchantForNegativeProbe(context) const pickerAccess = await resolvePickerAccess(context) if (!pickerAccess) return const sharedWarehouse = await resolveSharedWarehouse(context) if (!sharedWarehouse) { blockerGap(context, '海外PDA共享实际收货仓', '国内Warehouse主数据没有ACTIVE且启用RECEIVING的OVERSEAS固定作业仓') return } context.createdResources.phase4OverseasPda.overseasWarehouseId = sharedWarehouse.id context.createdResources.phase4OverseasPda.overseasWarehouseName = sharedWarehouse.name const primary = await createPicker(context, 1, sharedWarehouse.id, pickerAccess) const secondary = primary?.token ? await createPicker(context, 2, sharedWarehouse.id, pickerAccess) : null for (const picker of [primary, secondary]) if (picker?.membershipId) { context.createdResources.phase4OverseasPda.pickerMembershipIds.push(picker.membershipId) } let handoffReady = false const cleanup = async () => { await disablePicker(context, secondary, 2) await disablePicker(context, primary, 1) } context.context.phase4OverseasPdaOperations = { marker: context.marker, primary: primary ? { membershipId: primary.membershipId, username: primary.username, token: primary.token } : null, secondary: secondary ? { membershipId: secondary.membershipId, username: secondary.username, token: secondary.token } : null, overseasWarehouseId: sharedWarehouse.id, completedFixtures: [], cleanup, } try { if (!primary?.token || !secondary?.token) { blockerGap(context, '海外PDA双操作员账号', '未能创建并登录两个OVERSEAS_WAREHOUSE_PDA_OPERATOR平台账号,无法验证全局一柜锁') return } const matrix = context.createdResources?.phase4InboundProducerFixtures const dropship = matrix?.containers?.find((item) => item?.fulfillmentType === 'ONE_CLICK_DROPSHIP') const producers = [dropship || matrix?.byDestinationType?.FBA?.[0], matrix?.byDestinationType?.OVERSEAS?.[0]] .filter((item) => nonBlank(item?.containerNo)) const allProducers = Array.isArray(matrix?.containers) ? matrix.containers : producers if (allProducers.length) await awaitProducerConsumption(context, allProducers) if (producers.length && !(await convertReceivingContainers(context, producers))) { blockerGap(context, '实际处理柜整柜转换门禁', '目标柜未能通过正式柜级预览与确认形成逐入仓码计划及实体托盘') return } if (producers.length && !(await awaitPickupReadyCandidates(context, primary.token, producers, allProducers))) { blockerGap(context, '实际处理柜PICKUP_READY门禁', 'Dropship/FBA与OVERSEAS实际处理柜尚未全部达到PICKUP_READY;跨区消费已独立验证,不回退处理历史柜') return } for (const [index, producer] of (producers.length ? producers : [null]).entries()) { context.pdaContainerSuffix = producer?.containerNo || `CONTAINER-${index + 1}` context.createdResources.phase4OverseasPda.finalStatus = null await runReceiving(context, primary.token, secondary.token, index === 0 ? merchantToken : null, producer, index === 0) if (context.createdResources.phase4OverseasPda.finalStatus === 'COMPLETED') { context.createdResources.phase4OverseasPda.completedContainers.push(producer?.containerNo || null) } } handoffReady = context.context.phase4OverseasPdaOperations.completedFixtures.length > 0 } finally { delete context.pdaContainerSuffix if (!handoffReady) { try { await cleanup() } finally { delete context.context.phase4OverseasPdaOperations } } } } async function resolveSharedWarehouse(context) { const path = '/api/platform/v1/warehouse-admin?tenantId=0&page=1&size=200&capabilityCode=RECEIVING&warehouseType=OVERSEAS&status=ACTIVE' const response = await context.request('GET', path) const rows = records(response?.body?.data) const fixedIds = new Set(['193000000000200003', '193000000000200004']) const warehouse = success(response) ? rows.find((item) => fixedIds.has(id(item?.id))) : null context.addStep(MODULE, '读取国内Warehouse权威海外PDA实际收货仓', response, Boolean(warehouse) && warehouse?.warehouseType === 'OVERSEAS' && warehouse?.status === 'ACTIVE', `warehouseId=${id(warehouse?.id) || 'missing'}, warehouseName=${warehouse?.warehouseName || warehouse?.name || 'missing'}; ${trace(response)}`, OPTIONS) return warehouse ? { id: id(warehouse.id), name: warehouse.warehouseName || warehouse.name } : null }