import { readFileSync, existsSync } from 'node:fs' import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { createHash } from 'node:crypto' import { AcceptanceRun, parseArguments, validateExecutionOptions } from './admin-test-env-acceptance.mjs' import { installRetainedJournal, retainedApi, saveRetainedReport } from './mixed-container-outbound-test-env.mjs' const MODULE = 'overseas-relabel-producer' const DISPATCH = '/api/platform/v1/last-leg/dispatches' const id = value => { if (typeof value === 'number' && !Number.isSafeInteger(value)) throw Error('Relabel ID lost precision') return value == null ? '' : String(value) } const exactVersion = value => { if ((typeof value === 'number' && !Number.isSafeInteger(value)) || !/^(0|[1-9]\d*)$/.test(String(value))) throw Error('Authoritative relabel version is missing or imprecise') return value } const quantity = value => /^(0|[1-9]\d*)$/.test(String(value)) && Number.isSafeInteger(Number(value)) ? Number(value) : null const sorted = values => [...new Set(values)].sort() const same = (a, b) => JSON.stringify(a) === JSON.stringify(b) const canonical = value => JSON.stringify(normalize(value)) function normalize(value) { if (Array.isArray(value)) return value.map(normalize) return value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(key => [key, normalize(value[key])])) : value } function requireFact(condition, message) { if (!condition) throw Error(message) } function producerResponsePresent(path, data) { if (data == null || typeof data !== 'object') return false if (path === DISPATCH) return data.dispatchNo && data.action === 'CREATE' && data.recordVersion != null && data.result?.dispatchNo === data.dispatchNo if (path.endsWith('/photos/files')) return /^[1-9]\d*$/.test(id(data.fileId)) if (path.endsWith('/pallets/resolve')) return data.palletCode && /^[1-9]\d*$/.test(id(data.palletId)) && data.totalPackages != null if (path.endsWith('/relabel-pallet-label')) return data.palletCode && /^[1-9]\d*$/.test(id(data.palletId)) if (path.includes('/relabel-tasks/')) return data.relabelNo && data.version != null && Array.isArray(data.items) if (path.includes('/relabel-applications')) return data.application?.relabelNo && Array.isArray(data.items) return true } export function installRelabelJournal(context, report, save) { installRetainedJournal(context, report, save) const request = context.request.bind(context), upload = context.requestMultipart.bind(context) const verify = async (method, path, send) => { if (method !== 'GET' && report.requests.some(row => ['SENDING', 'UNKNOWN'].includes(row.status))) throw Error('Unknown producer write outcome: stop with original commands') const response = await send() if (method !== 'GET' && response.http === 200 && String(response.body?.code) === '200' && !producerResponsePresent(path, response.body?.data)) { const entry = report.requests.at(-1) if (entry) entry.status = 'UNKNOWN' await context.checkpoint(); throw Error('Unknown producer write result: native business response missing') } return response } context.request = (method, path, body, token, headers) => verify(method, path, () => request(method, path, body, token, headers)) context.requestMultipart = (method, path, file, token, headers) => verify(method, path, () => upload(method, path, file, token, headers)) return context } function validateLoadingEvidence(value, marker, dispatchNo, targets) { const physicalIds = sorted(targets.map(row => id(row.palletId))) requireFact(value?.environment === 'TEST' && value.marker === marker && typeof value.dispatchNo === 'string' && value.dispatchNo.trim() && value.dispatchNo === dispatchNo && Array.isArray(value.pallets) && same(sorted(value.pallets.map(row => id(row.warehousePalletId))), physicalIds) && value.pallets.length === physicalIds.length, 'Loading evidence belongs to another marker/dispatch/physical pallet') for (const row of value.pallets) { requireFact(Number.isSafeInteger(row.palletSpaces) && row.palletSpaces > 0 && Number.isSafeInteger(row.deliveryIndex) && row.deliveryIndex > 0, 'Actual pallet spaces and physical stop index are required') for (const field of ['palletSpaces', 'weightKg', 'volumeCbm']) { requireFact(/^(0|[1-9]\d*)(?:\.\d+)?$/.test(String(row[field])) && Number(row[field]) > 0 && row.evidence?.some(e => e.actual === true && e.field === field && e.sourceType && e.sourceBusinessNo && Number.isFinite(Date.parse(e.observedAt)) && Number(e.value) === Number(row[field])), 'Actual physical loading field evidence is missing') } } return value } export function selectExplicitRelabelSource(candidates, config) { requireFact(config.sourceBalanceId && config.sourcePositionId, 'Explicit source balance and position are required; no historical inventory fallback') const matches = candidates.filter(row => id(row.inventoryBalanceId) === id(config.sourceBalanceId) && id(row.overseasWarehouseId) === id(config.warehouseId) && (!config.inboundCode || row.inboundCode === config.inboundCode) && row.positions?.some(position => id(position.positionId) === id(config.sourcePositionId))) requireFact(matches.length === 1, 'Explicit relabel source is absent or ambiguous in this customer warehouse') const source = matches[0], positions = source.positions.filter(row => id(row.positionId) === id(config.sourcePositionId)) requireFact(positions.length === 1 && String(source.inboundOrderNo || '').startsWith('DS-') && source.destinationFbaWarehouseCode && quantity(source.availablePackages) >= 5 && quantity(positions[0].availablePackages) >= 5, 'Explicit source requires original DS inventory and five real available cartons') return { source, sourcePosition: positions[0] } } function scenarioCargo(scenario, rows, customerId, warehouseId) { const targets = sorted(scenario.targets.map(target => id(target.palletId))) const expected = scenario.items.reduce((sum, item) => sum + Number(item.notifiedPackageCount), 0) requireFact(rows.length === targets.length && same(sorted(rows.map(row => id(row.selection?.warehousePalletId))), targets) && rows.reduce((sum, row) => sum + Number(row.availablePackages), 0) === expected, 'Native relabel candidates do not exactly cover original targets and carton quantity') const cargo = rows.map(row => { const selection = row.selection requireFact(selection?.sourceType === 'RELABEL' && selection.relabelNo === scenario.relabelNo && selection.planPalletId == null && selection.returnReceiptId == null && /^[1-9]\d*$/.test(id(selection.warehousePalletId)) && row.sourceKey === `RELABEL:${scenario.relabelNo}:${id(selection.warehousePalletId)}` && /^[a-f\d]{64}$/i.test(row.sourceVersion || '') && row.sourceVersion === selection.sourceVersion && id(row.customerId) === id(customerId) && id(row.warehouseId) === id(warehouseId) && quantity(row.availablePackages) > 0 && row.destination?.type === 'FBA' && row.destination.warehouseCode === scenario.destinationFbaWarehouseCode && Array.isArray(row.blockReasons) && !row.blockReasons.length && Array.isArray(row.appointmentBatches) && !row.appointmentBatches.length, 'Incomplete native RELABEL selector, warehouse/customer/destination or source version') requireFact(!Object.hasOwn(row, 'cartonMarks') && !Object.hasOwn(row, 'relabelCartonMarks'), 'Native candidates must not expose legacy carton mark projections') return { sourceType: 'RELABEL', planPalletId: null, returnReceiptId: null, relabelNo: scenario.relabelNo, warehousePalletId: id(selection.warehousePalletId), sourceVersion: selection.sourceVersion } }) return cargo } /** The complete server group is the selection unit, including the whole application closure. */ export function nativeRelabelCreateBody(marker, scenarios, rows, customerId, warehouseId) { requireFact(Array.isArray(scenarios) && scenarios.length > 0 && rows.length > 0 && new Set(scenarios.map(s => s.relabelNo)).size === scenarios.length, 'Owned relabel scenarios are missing or duplicate') const group = rows[0].selectionGroup requireFact(typeof group?.groupKey === 'string' && group.groupKey.trim() && typeof group.groupVersion === 'string' && group.groupVersion.trim() && group.complete === true && Array.isArray(group.blockReasons) && !group.blockReasons.length && Array.isArray(group.sources) && group.sources.length && Array.isArray(group.pallets) && group.pallets.length, 'Authoritative complete selectionGroup and groupVersion are required') requireFact(rows.every(row => canonical(row.selectionGroup) === canonical(group)), 'Selection group version or membership drift') requireFact(new Set(rows.map(row => row.sourceKey)).size === rows.length && group.sources.length === rows.length && new Set(group.sources.map(source => source.sourceKey)).size === group.sources.length && same(sorted(group.sources.map(source => source.sourceKey)), sorted(rows.map(row => row.sourceKey))), 'Selection group logical sources are incomplete or duplicate') const cargo = scenarios.flatMap(scenario => scenarioCargo(scenario, rows.filter(row => row.selection?.relabelNo === scenario.relabelNo), customerId, warehouseId)) requireFact(cargo.length === rows.length, 'Selection group contains an unknown relabel business member') for (const source of group.sources) { const row = rows.find(value => value.sourceKey === source.sourceKey), scenario = scenarios.find(value => value.relabelNo === source.sourceBusinessNo) requireFact(scenario?.relabelVerified === true && row && source.sourceBusinessType && id(source.customerId) === id(customerId) && source.sourceBusinessNo === row.selection.relabelNo && canonical(source.selection) === canonical(row.selection) && quantity(source.packages) > 0 && quantity(source.packages) === quantity(row.availablePackages), 'Selection group business, selector version or source quantity drift') } requireFact(new Set(group.pallets.map(p => id(p.warehousePalletId))).size === group.pallets.length && same(sorted(group.pallets.map(p => id(p.warehousePalletId))), sorted(cargo.map(c => id(c.warehousePalletId)))), 'Selection group physical pallet closure is incomplete or duplicate') for (const pallet of group.pallets) { const sources = group.sources.filter(source => id(source.selection.warehousePalletId) === id(pallet.warehousePalletId)) requireFact(/^[1-9]\d*$/.test(id(pallet.warehousePalletId)) && pallet.palletNo && sources.length && Array.isArray(pallet.sourceKeys) && pallet.sourceKeys.length === sources.length && same(sorted(pallet.sourceKeys), sorted(sources.map(source => source.sourceKey))) && quantity(pallet.packageCount) === sources.reduce((sum, source) => sum + quantity(source.packages), 0) && rows.filter(row => id(row.selection.warehousePalletId) === id(pallet.warehousePalletId)).every(row => row.palletNo === pallet.palletNo), 'Selection group physical source contributions or carton quantity drift') } return { idempotencyKey: createHash('sha256').update(`${marker}\n${group.groupKey}\n${group.groupVersion}\nCREATE`).digest('hex'), warehouseId: id(warehouseId), vehicle: null, driver: null, plannedDepartureAt: null, estimatedArrivalAt: null, remark: `${marker}-${scenarios.map(s => s.name).join('+')}`, cargo, batchAllocations: [] } } export function nativeRelabelGroups(scenarios, rows, customerId, warehouseId) { const groups = new Map() for (const scenario of scenarios) { requireFact(scenario.relabelVerified === true && scenario.relabelCartonMarks?.length === scenario.expectedCartons && new Set(scenario.relabelCartonMarks).size === scenario.relabelCartonMarks.length && scenario.relabelCartonMarks.every(mark => typeof mark === 'string' && mark.trim() && !scenario.sourceCartonMarks.includes(mark)), 'Original/new relabel carton mark evidence is incomplete or overlaps') const candidates = rows.filter(row => row.selection?.sourceType === 'RELABEL' && row.selection.relabelNo === scenario.relabelNo) scenarioCargo(scenario, candidates, customerId, warehouseId) requireFact(candidates.every(row => row.selectionGroup?.groupKey) && new Set(candidates.map(row => row.selectionGroup.groupKey)).size === 1, 'Whole relabel application must belong to one authoritative complete selection group') const key = candidates[0].selectionGroup.groupKey if (!groups.has(key)) groups.set(key, { scenarios: [], selectionGroup: candidates[0].selectionGroup }) groups.get(key).scenarios.push(scenario) scenario.dispatchCandidates = candidates } const physicalOwners = new Map(), result = [] for (const entry of groups.values()) { const keys = entry.selectionGroup.sources?.map(source => source.sourceKey) || [] entry.candidates = rows.filter(row => keys.includes(row.sourceKey)) entry.createBody = nativeRelabelCreateBody('PREFLIGHT', entry.scenarios, entry.candidates, customerId, warehouseId) for (const pallet of entry.selectionGroup.pallets) { const key = id(pallet.warehousePalletId) requireFact(!physicalOwners.has(key), 'One physical pallet cannot be split across selection groups') physicalOwners.set(key, entry.selectionGroup.groupKey) } result.push(entry) } return result } // Compare physical values once, retaining per-scenario raw receipts without adding whole-pallet weights. export function relabelGroupMeasurements(scenarios, group) { const values = new Map(), fields = ['palletCode', 'packages', 'stockVersion', 'lengthCm', 'widthCm', 'heightCm', 'grossWeightKg', 'volumeCbm'] for (const scenario of scenarios) for (const measured of scenario.physicalMeasurements || []) { const palletId = id(measured.palletId), pallet = group.pallets.find(p => id(p.warehousePalletId) === palletId) if (!pallet) continue requireFact(measured.palletCode === pallet.palletNo && quantity(measured.packages) === quantity(pallet.packageCount), 'Physical measurement does not cover the complete shared pallet') const old = values.get(palletId) if (old) requireFact(fields.every(field => canonical(old[field]) === canonical(measured[field])), 'Shared physical measurement versions or values conflict') else values.set(palletId, structuredClone(measured)) } return [...values.values()] } function groupLoadingEvidence(config, report, scenarios, group, dispatchNo) { const targets = group.pallets.map(p => ({ palletId: id(p.warehousePalletId) })), inputs = scenarios.map(s => config.loadingEvidenceByScenario?.[s.name]).filter(Boolean) if (!inputs.length) return undefined const validated = inputs.map(value => validateLoadingEvidence(value, report.marker, dispatchNo || value.dispatchNo, targets)) const physical = value => [...value.pallets].sort((a, b) => id(a.warehousePalletId).localeCompare(id(b.warehousePalletId))).map(p => ({ warehousePalletId: id(p.warehousePalletId), palletSpaces: p.palletSpaces, weightKg: String(Number(p.weightKg)), volumeCbm: String(Number(p.volumeCbm)), deliveryIndex: p.deliveryIndex })) requireFact(validated.every(value => value.dispatchNo === validated[0].dispatchNo && canonical(physical(value)) === canonical(physical(validated[0]))), 'Shared scenario loading receipts must name the same dispatch and identical physical measurements') return structuredClone(validated[0]) } /** Read the complete candidate universe and validate every owned group before the first CREATE. */ export async function publishNativeRelabelDispatches(context, report, config) { const scenarios = report.scenarios.filter(row => row.operationType === 'CARTON_AND_SKU') report.blockers ||= [] const block = (scenario, code, evidence) => { scenario.dispatchStatus = 'BLOCKED'; scenario.nativeDispatchBlocker = { code, ...evidence } report.blockers.push({ scenario: scenario.name, code, ...evidence }) context.addCoverageGap?.(MODULE, scenario.name, 'Native relabel dispatch', code, 'physical-source-blocked', { deploymentBlocker: true }) context.addStep?.(MODULE, `${scenario.name} native dispatch blocked`, null, false, code, { deploymentBlocker: true }) } const page = await retainedApi(context, 'GET', `${DISPATCH}/candidates?${new URLSearchParams({ warehouseId: id(config.warehouseId), sourceStage: 'RELABEL' })}`) requireFact(page?.complete === true && Array.isArray(page.candidates), 'Native candidate list is incomplete') for (const scenario of scenarios) { scenario.sourceAfter = Number((await retainedApi(context, 'POST', '/api/pda/v1/last-leg/pallet-operations/pallets/resolve', { code: report.sourcePosition.palletNo }, config.pdaToken)).totalPackages) scenario.targetPallets = scenario.targets.map(target => ({ palletId: id(target.palletId), palletCode: target.palletCode })) scenario.physicalMeasurements = await retainedApi(context, 'POST', '/api/pda/v1/last-leg/pallet-operations/relabel-pallet-measurements', { relabelNo: scenario.relabelNo }, config.pdaToken) await context.checkpoint() } let groups try { groups = nativeRelabelGroups(scenarios, page.candidates, config.customerId, config.warehouseId) for (const group of groups) { group.physicalMeasurements = relabelGroupMeasurements(group.scenarios, group.selectionGroup) groupLoadingEvidence(config, report, group.scenarios, group.selectionGroup) } } catch (error) { report.nativeSelectionFailure = { message: error.message, candidates: page.candidates } for (const scenario of scenarios) block(scenario, 'NATIVE_SELECTION_GROUP_NOT_VERIFIED', { reason: error.message }) await context.checkpoint(); throw error } for (const group of groups) { const members = group.scenarios try { const createBody = nativeRelabelCreateBody(report.marker, members, group.candidates, config.customerId, config.warehouseId) const native = { marker: report.marker, customerId: id(config.customerId), warehouseId: id(config.warehouseId), createBody, physicalMeasurements: group.physicalMeasurements } for (const scenario of members) { scenario.nativeDispatch = native; scenario.nativeCreateBody = createBody; scenario.selectionGroup = group.selectionGroup scenario.sourceContributions = group.selectionGroup.sources.filter(source => source.sourceBusinessNo === scenario.relabelNo) } await context.checkpoint() const createResult = await retainedApi(context, 'POST', DISPATCH, createBody) Object.assign(native, { createResult, dispatchNo: createResult?.dispatchNo }); await context.checkpoint() requireFact(createResult?.action === 'CREATE' && createResult.dispatchNo && createResult.status === 'ACTIVE' && createResult.stage === 'BEFORE_LOADING', 'Native CREATE did not return original active vehicle identity') exactVersion(createResult.recordVersion) const replay = await retainedApi(context, 'POST', DISPATCH, createBody) native.createReplay = replay; await context.checkpoint() requireFact(replay.replayed === true && replay.dispatchNo === createResult.dispatchNo && String(replay.recordVersion) === String(createResult.recordVersion) && same(replay.result, createResult.result), 'Native CREATE replay changed original result') const detail = await retainedApi(context, 'GET', `${DISPATCH}/${encodeURIComponent(createResult.dispatchNo)}`) const sourceKeys = group.candidates.map(row => row.sourceKey) requireFact(detail?.dispatchNo === createResult.dispatchNo && detail.status === 'ACTIVE' && detail.stage === 'BEFORE_LOADING' && quantity(detail.counts?.packageCount) === group.selectionGroup.sources.reduce((sum, source) => sum + quantity(source.packages), 0) && quantity(detail.counts?.palletCount) === group.selectionGroup.pallets.length && id(exactVersion(detail.recordVersion)) === id(createResult.recordVersion) && detail.orderDetails?.length >= sourceKeys.length && same(sorted(detail.orderDetails?.map(row => row.sourceKey) || []), sorted(sourceKeys)) && detail.orderDetails.every(row => group.selectionGroup.sources.some(source => source.sourceKey === row.sourceKey && id(source.customerId) === id(row.customerId) && source.sourceBusinessNo === row.sourceBusinessNo && source.sourceBusinessType === row.sourceBusinessType)), 'Native Detail source/business/customer/carton identity drift') native.detail = detail const measurements = groupLoadingEvidence(config, report, members, group.selectionGroup, createResult.dispatchNo) if (measurements) native.loadingMeasurements = measurements for (const scenario of members) { scenario.dispatchNo = createResult.dispatchNo; scenario.dispatchStatus = detail.status if (!measurements) block(scenario, 'ACTUAL_LOADING_MEASUREMENT_REQUIRED', { fields: ['palletSpaces', 'weightKg', 'volumeCbm', 'deliveryIndex'], dispatchNo: createResult.dispatchNo, evidenceKind: 'PHYSICAL_MEASUREMENTS_NOT_FABRICATED' }) } await context.checkpoint() } catch (error) { if (report.requests?.some(row => ['SENDING', 'UNKNOWN'].includes(row.status))) throw error for (const scenario of members) block(scenario, 'NATIVE_DISPATCH_NOT_VERIFIED', { candidates: scenario.dispatchCandidates, sourceBeforePackages: scenario.sourceBefore, sourceAfterPackages: scenario.sourceAfter, reason: error.message, evidenceKind: 'RETAINED_ORIGINAL_REQUEST_AND_RESPONSE' }) await context.checkpoint() } } return scenarios } async function poll(action, predicate, attempts = 40) { for (let index = 0; index < attempts; index += 1) { const value = await action() if (predicate(value)) return value await new Promise((resolve) => setTimeout(resolve, 500)) } throw new Error('Polling timed out') } function assert(condition, message) { if (!condition) throw new Error(message) } export async function runRelabelProducer(context, config, report) { const { customerId, warehouseId, merchantToken, pdaToken, chargeAmount } = config const platformToken = context.token, marker = report.marker const key = value => `${marker}-${value}`.slice(0, 200) const api = (token, method, path, body) => retainedApi(context, method, path, body, token) const candidates = await api(merchantToken, 'GET', `/api/merchant/v1/last-leg/relabel-applications/inventory-candidates?operationWarehouseId=${warehouseId}`) const { source, sourcePosition } = selectExplicitRelabelSource(candidates, config) report.source = source; report.sourcePosition = sourcePosition await context.checkpoint() async function resolvePallet(palletCode) { return api(pdaToken, 'POST', '/api/pda/v1/last-leg/pallet-operations/pallets/resolve', { code: palletCode }) } async function createAndConfirm(name, cartonCount, operationType = 'CARTON_AND_SKU', prepare = null) { const suffix = `${name}-${marker}` const items = Array.from({ length: cartonCount }, (_, index) => ({ sourceInventoryBalanceId: id(source.inventoryBalanceId), sourceInventoryPositionId: id(sourcePosition.positionId), sourceInboundOrderId: id(source.inboundOrderId), skuCode: source.skuCode, productNameZh: source.productNameZh || source.skuCode, totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: `SHIP-${suffix}-${index + 1}`, newSkuCode: `${source.skuCode}-NEW-${suffix}-${index + 1}`.slice(0, 128), labelRequirement: marker, cartonMark: `CM-${suffix}-${index + 1}`, fbaCartonMark: operationType === 'CARTON_AND_SKU' ? `FBA-${suffix}-${index + 1}` : null, destinationFbaWarehouseCode: source.destinationFbaWarehouseCode, operationType, skuItems: [{ skuCode: source.skuCode, newSkuCode: `${source.skuCode}-NEW-${suffix}-${index + 1}`.slice(0, 128), quantity: 1, remark: marker }], })) const scenario = { name, operationType, expectedCartons: cartonCount, items: [], targets: [], requestItems: items, sourceCartonMarks: items.map(item => item.cartonMark), relabelVerified: false } report.scenarios.push(scenario) const created = await api(platformToken, 'POST', `/api/platform/v1/last-leg/relabel-applications?customerId=${customerId}`, { customerOrderNo: `TAIL-${suffix}`.slice(0, 128), operationWarehouseId: warehouseId, labelDeadline: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 19), contactName: '尾程验收', contactPhone: '13800000006', ...(chargeAmount > 0 ? { chargeAmount } : {}), remark: `${marker}-${name}`, idempotencyKey: key(`${name}-CREATE`), deliveryMethod: operationType === 'SKU_LABEL' ? null : 'TRUCK', printShippingLabel: false, items, }) const relabelNo = created.application.relabelNo scenario.relabelNo = relabelNo; scenario.createResult = created assert(created.application.overseasWarehouseId === warehouseId, 'Created relabel belongs to a different warehouse') await context.checkpoint() if (chargeAmount > 0) await poll(() => api(platformToken, 'GET', `/api/platform/v1/last-leg/relabel-applications/${relabelNo}`), (detail) => detail.application.walletStatus === 'FROZEN') let detail = await api(pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/claim`, { idempotencyKey: key(`${name}-CLAIM`) }) for (const [index, item] of detail.items.entries()) { if (operationType === 'CARTON_AND_SKU') { const cartonBody = { cartonMark: item.cartonMark, expectedVersion: exactVersion(detail.version), idempotencyKey: key(`${name}-CARTON-${index}`) } detail = await api(pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/cartons/scan`, cartonBody) const cartonReplay = await api(pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/cartons/scan`, cartonBody) assert(String(cartonReplay.version) === String(detail.version) && same(cartonReplay.items, detail.items), 'Original carton scan replay changed quantities/version') const stageItem = detail.items.find((row) => id(row.itemId) === id(item.itemId)) assert(Number(stageItem?.cartonProcessedPackages) === 1 && Number(stageItem?.skuProcessedPackages) === 0, `${relabelNo} carton stage incorrectly locked or completed SKU stage`) } const scanBody = { itemId: id(item.itemId), oldLabel: item.cartonMark, newLabel: null, skuVerifications: (item.skuItems || []).map((sku) => ({ skuCode: sku.skuCode, newSkuCode: sku.newSkuCode, quantity: Number(sku.quantity), })), expectedVersion: exactVersion(detail.version), idempotencyKey: key(`${name}-SCAN-${index}`), } detail = await api(pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/units`, scanBody) const scanReplay = await api(pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/units`, scanBody) assert(String(scanReplay.version) === String(detail.version) && same(scanReplay.items, detail.items), 'Original SKU verification replay changed quantities/version') } const sourceBefore = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages) Object.assign(scenario, { items: detail.items, sourceBefore, sourceCartonMarks: items.map(item => item.cartonMark), requestItems: items }) if (prepare) await prepare(scenario) // Valid synthetic PNG for API validation; no physical warehouse photograph is claimed. const photoResponse = await context.requestMultipart('POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/photos/files`, { name: `${marker}-${name}.png`, type: 'image/png', content: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aWQAAAABJRU5ErkJggg==', 'base64'), }, pdaToken) const photo = context.apiSuccess('overseas-relabel-producer', 'Upload synthetic relabel PNG', photoResponse) assert(photoResponse.http === 200 && String(photoResponse.body?.code) === '200' && photo?.fileId, 'Synthetic photo upload failed') scenario.photo = { fileId: id(photo.fileId), evidence: 'SYNTHETIC_PNG_NOT_PHYSICAL_WORK' } detail = await api(pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/complete`, { photoFileIds: [id(photo.fileId)], expectedVersion: exactVersion(detail.version), idempotencyKey: key(`${name}-COMPLETE`), remark: marker, }) assert(detail.applicationStatus === 'CUSTOMER_CONFIRM_PENDING', `${relabelNo} did not reach customer confirmation`) const confirmed = await api(merchantToken, 'POST', `/api/merchant/v1/last-leg/relabel-applications/${relabelNo}/confirm-completion`, { idempotencyKey: key(`${name}-CONFIRM`), remark: marker, }) const expectedStatus = operationType === 'SKU_LABEL' ? 'COMPLETED' : 'READY_FOR_OUTBOUND' assert(confirmed.application.status === expectedStatus, `${relabelNo} did not reach ${expectedStatus}`) assert(confirmed.items.every((item) => item.destinationFbaWarehouseCode === source.destinationFbaWarehouseCode), `${relabelNo} did not preserve the destination warehouse`) Object.assign(scenario, { items: confirmed.items, confirmedResult: confirmed, relabelVerified: true, relabelCartonMarks: operationType === 'CARTON_AND_SKU' ? confirmed.items.map(item => item.fbaCartonMark) : [] }) scenario.sourceAfterRelabel = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages) await context.checkpoint() return scenario } async function printPallet(relabelNo, suffix) { return api(pdaToken, 'POST', '/api/pda/v1/last-leg/pallet-operations/relabel-pallet-label', { relabelNo, idempotencyKey: key(`PRINT-${suffix}`), }) } async function moveCarton(scenario, item, target, index) { const sourcePallet = await resolvePallet(sourcePosition.palletNo) const targetPallet = await resolvePallet(target.palletCode) const result = await api(pdaToken, 'POST', '/api/pda/v1/last-leg/pallet-operations/relabel-disassemble', { idempotencyKey: key(`${scenario.name}-MOVE-${index}`), relabelNo: scenario.relabelNo, source: { palletCode: sourcePallet.palletCode, expectedVersion: exactVersion(sourcePallet.palletVersion) }, target: { palletCode: targetPallet.palletCode, expectedVersion: exactVersion(targetPallet.palletVersion) }, cartonMarks: [item.cartonMark], reason: `${marker}-${scenario.name}`, }) assert(result.operationType === 'RELABEL_PALLET_DISASSEMBLE', 'Unexpected pallet operation type') return result } const skuOnly = await createAndConfirm('SKU-ONLY', 1, 'SKU_LABEL') skuOnly.sourceAfter = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages) assert(skuOnly.sourceAfter === skuOnly.sourceBefore, 'SKU-only relabel must not split or regroup pallets') skuOnly.targetPallets = [] skuOnly.destinationFbaWarehouseCode = source.destinationFbaWarehouseCode let sharedTarget const fresh = await createAndConfirm('NEW-PALLET', 1, 'CARTON_AND_SKU', async scenario => { sharedTarget = await printPallet(scenario.relabelNo, 'SHARED') const printReplay = await printPallet(scenario.relabelNo, 'SHARED') assert(id(sharedTarget.palletId) === id(printReplay.palletId), 'Pallet print replay was not idempotent') await moveCarton(scenario, scenario.items[0], sharedTarget, 0) scenario.targets = [sharedTarget] }) const sharedBefore = Number((await resolvePallet(sharedTarget.palletCode)).totalPackages) const merged = await createAndConfirm('MERGE-EXISTING', 1, 'CARTON_AND_SKU', async scenario => { await moveCarton(scenario, scenario.items[0], sharedTarget, 0) scenario.targets = [sharedTarget] }) const sharedAfter = Number((await resolvePallet(sharedTarget.palletCode)).totalPackages) assert(sharedAfter === sharedBefore + 1, 'Existing target pallet carton count did not increase') merged.targets = [sharedTarget] report.sharedMerge = { palletId: id(sharedTarget.palletId), palletCode: sharedTarget.palletCode, beforePackages: sharedBefore, afterPackages: sharedAfter, sourceScenarios: [{ name: fresh.name, cartons: fresh.expectedCartons }, { name: merged.name, cartons: merged.expectedCartons }] } const split = await createAndConfirm('SPLIT-TWO-TARGETS', 2, 'CARTON_AND_SKU', async scenario => { const splitA = await printPallet(scenario.relabelNo, 'SPLIT-A') const splitB = await printPallet(scenario.relabelNo, 'SPLIT-B') await moveCarton(scenario, scenario.items[0], splitA, 0) await moveCarton(scenario, scenario.items[1], splitB, 1) scenario.targets = [splitA, splitB] }) for (const scenario of [fresh, merged, split]) scenario.destinationFbaWarehouseCode = source.destinationFbaWarehouseCode const ledger = await api(platformToken, 'GET', `/api/platform/v1/last-leg/inventory/ledger?customerId=${customerId}&page=1&size=100&businessType=RELABEL_PALLET_DISASSEMBLE`) const ledgerRows = ledger.records.filter(row => String(row.remark || '').includes(marker)) assert(ledgerRows.some(row => Number(row.deltaPackages) < 0), 'Missing negative relabel pallet ledger') assert(ledgerRows.some(row => Number(row.deltaPackages) > 0), 'Missing positive relabel pallet ledger') assert(ledgerRows.every(row => String(row.remark).startsWith('换标拆托')), 'Unexpected relabel ledger reason') report.ledger = { rows: ledgerRows.length, deltas: ledgerRows.map(row => Number(row.deltaPackages)), records: ledgerRows } report.relabelScenariosVerified = report.scenarios.length === 4 && report.scenarios.every(scenario => scenario.relabelVerified) await publishNativeRelabelDispatches(context, report, config) report.passed = report.relabelScenariosVerified && !report.blockers.length report.finalStatus = report.passed ? 'RELABEL_AND_NATIVE_DISPATCH_VERIFIED' : 'BLOCKED_NATIVE_DISPATCH_OR_PHYSICAL_LOADING_EVIDENCE' await context.checkpoint(); return report } export async function mainRelabelProducer(env = process.env) { const options = parseArguments([]); options.modules = [] const output = resolve(env.OVERSEAS_ACCEPTANCE_OUTPUT || 'reports/overseas-relabel-real-acceptance.json') options.output = output; validateExecutionOptions(options) requireFact(options.environment === 'test', 'Relabel producer only supports explicit TEST') const customerId = id(env.OVERSEAS_TEST_CUSTOMER_ID), warehouseId = id(env.OVERSEAS_TEST_WAREHOUSE_ID) const sourceBalanceId = id(env.OVERSEAS_RELABEL_SOURCE_BALANCE_ID), sourcePositionId = id(env.OVERSEAS_RELABEL_SOURCE_POSITION_ID) requireFact(/^[1-9]\d*$/.test(customerId) && /^[1-9]\d*$/.test(warehouseId) && /^[1-9]\d*$/.test(sourceBalanceId) && /^[1-9]\d*$/.test(sourcePositionId), 'Explicit customer, warehouse, original source balance and position IDs are required before HTTP') requireFact(!existsSync(output), 'Existing producer checkpoint must be reconciled with its original commands; do not overwrite it or regenerate a marker') const chargeAmount = Number(env.OVERSEAS_RELABEL_CHARGE_AMOUNT ?? '0.01') requireFact(Number.isFinite(chargeAmount) && chargeAmount >= 0, 'Invalid charge amount') const context = new AcceptanceRun(options), marker = env.OVERSEAS_RELABEL_MARKER || context.marker requireFact(/^[A-Za-z0-9_-]+$/.test(marker), 'Invalid explicit producer marker') const report = { marker, environment: 'TEST', startedAt: new Date().toISOString(), scope: 'RELABEL_PRODUCER_AND_NATIVE_DISPATCH_ONLY', chargeAmount, photoEvidence: 'SYNTHETIC_PNG_NOT_PHYSICAL_WORK', scenarios: [], blockers: [], passed: false, requests: [] } const raw = async (method, path, body, token) => { const response = await context.request(method, path, body, token) requireFact(response.http === 200 && String(response.body?.code) === '200', 'Unified authentication failed') return response.body.data } try { // Session tickets and tokens remain in memory and never enter the business journal. const platform = await raw('POST', '/api/auth/login', { loginId: options.username, password: options.password }, '') context.token = platform.accessToken requireFact(context.token && options.pdaUsername && options.pdaPassword, 'Unified platform and explicit PDA credentials required') const pda = await raw('POST', '/api/auth/login', { loginId: options.pdaUsername, password: options.pdaPassword }, '') let merchant if (options.merchantUsername && options.merchantPassword) merchant = await raw('POST', '/api/auth/login', { loginId: options.merchantUsername, password: options.merchantPassword }, '') else { const ticket = await raw('POST', `/api/platform/v1/iam/tenants/${customerId}/impersonation`, { reason: `${marker} TEST relabel producer` }, context.token) merchant = await raw('POST', '/api/auth/impersonations/exchange', { code: ticket.code }, '') } requireFact(pda.accessToken && merchant.accessToken, 'Unified PDA/customer accessToken required') const profile = await raw('GET', '/api/merchant/v1/iam/tenant/profile', undefined, merchant.accessToken) requireFact(id(profile.id) === customerId, 'Merchant session does not own the explicit customer') installRelabelJournal(context, report, () => saveRetainedReport(output, report)); context.marker = marker const loadingEvidenceByScenario = env.OVERSEAS_RELABEL_LOADING_EVIDENCE ? JSON.parse(readFileSync(resolve(env.OVERSEAS_RELABEL_LOADING_EVIDENCE), 'utf8')) : undefined await runRelabelProducer(context, { customerId, warehouseId, sourceBalanceId, sourcePositionId, inboundCode: env.OVERSEAS_RELABEL_SOURCE_INBOUND_CODE, merchantToken: merchant.accessToken, pdaToken: pda.accessToken, chargeAmount, loadingEvidenceByScenario }, report) } catch (error) { report.passed = false; report.finalStatus = report.requests.some(row => ['SENDING', 'UNKNOWN'].includes(row.status)) ? 'UNKNOWN_WRITE_STOPPED' : 'BLOCKED' report.failedStage = report.scenarios.at(-1)?.name || 'CUSTOMER_OR_EXPLICIT_SOURCE'; process.exitCode = 1 } finally { report.checks = context.steps; report.finishedAt = new Date().toISOString() if (!report.passed || context.steps.some(step => step.passed === false)) process.exitCode = 1 saveRetainedReport(output, report) console.log(JSON.stringify({ marker, finalStatus: report.finalStatus, passed: report.passed, relabelScenariosVerified: report.relabelScenariosVerified, scenarios: report.scenarios.map(scenario => ({ name: scenario.name, relabelNo: scenario.relabelNo, dispatchNo: scenario.dispatchNo, dispatchStatus: scenario.dispatchStatus })), report: output })) } return report } if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await mainRelabelProducer()