import { mkdir, writeFile } from 'node:fs/promises' const PLATFORM = '/api/platform/v1/last-leg' const MODULE = 'phase4-overseas-client-services' const id = value => String(value ?? '') const validId = value => /^[1-9]\d*$/.test(id(value)) /** 已有库存转运按本轮原入仓单分配真实仓位,保留关联换标申请供后续使用。 */ export async function allocateWarehouseTransfer(context, { proof, merchantToken, checkpoint }) { if (proof?.marker !== context.marker || !/^[A-Za-z0-9_-]{1,100}$/.test(context.marker) || context.createdResources?.phase4OverseasClientServices?.transferFulfillment !== proof || proof.customerId !== id(context.context?.merchantQualificationFixture?.tenantId) || !proof.transferNo || !proof.inboundCode || !proof.skuCode || !validId(proof.warehouseId) || proof.status !== 'APPROVED' || !merchantToken || proof.allocation) throw Error('转运库存分配缺少本轮原单和客户来源,或已执行') const evidence = proof.allocation = { status: 'RUNNING', requests: [] } let initialized = false const directory = new URL('../../reports/warehouse-transfer-checkpoints/', import.meta.url) const persist = checkpoint || (async () => { await mkdir(directory, { recursive: true }) await writeFile(new URL(`${context.marker}.json`, directory), JSON.stringify(proof, null, 2), { encoding: 'utf8', flag: initialized ? 'w' : 'wx' }) initialized = true }) const check = (name, passed) => { context.assert(MODULE, name, null, Boolean(passed), `transferNo=${proof.transferNo}`, { scenario: 'warehouse-transfer-allocation', deploymentBlocker: true }) if (!passed) throw Error(name) } const request = async (method, path, body, token) => { const log = { method, path, body, status: 'PREPARED' }; evidence.requests.push(log); await persist() let response try { log.status = 'SENDING'; response = await context.request(method, path, body, token) } catch (error) { log.status = 'UNKNOWN_RESULT'; throw error } const ok = response?.http === 200 && id(response?.body?.code) === '200' Object.assign(log, { status: ok ? 'SUCCEEDED' : !response?.http || response.http >= 500 ? 'UNKNOWN_RESULT' : 'REJECTED', http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId }) await persist() context.apiSuccess(MODULE, '转运原库存分配', response, JSON.stringify(log), { scenario: 'warehouse-transfer-allocation', deploymentBlocker: true }) if (!ok) throw Error('转运库存请求失败,保留原单且不自动重发') return response.body.data } const base = `${PLATFORM}/warehouse-transfers/${encodeURIComponent(proof.transferNo)}` const scope = `?customerId=${encodeURIComponent(proof.customerId)}` try { const transfer = await request('GET', base + scope) check('转运原单已通过且待分配换标库存', transfer?.transferNo === proof.transferNo && transfer.status === 'APPROVED' && id(transfer.operatingWarehouseId) === proof.warehouseId && transfer.needRelabel === true && transfer.packageCount === 1 && transfer.allocations?.length === 0 && transfer.sourceOrders?.some(row => row.inboundCode === proof.inboundCode)) const candidates = await request('GET', `/api/merchant/v1/last-leg/relabel-applications/inventory-candidates?${new URLSearchParams({ operationWarehouseId: proof.warehouseId, inboundCode: proof.inboundCode })}`, undefined, merchantToken) const eligible = (Array.isArray(candidates) ? candidates : []).filter(c => c.inboundCode === proof.inboundCode && c.skuCode === proof.skuCode && id(c.overseasWarehouseId) === proof.warehouseId && validId(c.inventoryBalanceId) && transfer.sourceOrders.some(s => id(s.inboundOrderId) === id(c.inboundOrderId))) let source, position for (const candidate of eligible) { const positions = await request('GET', `${PLATFORM}/inventory/balances/${encodeURIComponent(candidate.inventoryBalanceId)}/positions${scope}`) check('库存位置回查必须返回完整数组', Array.isArray(positions)) position = positions.find(p => id(p.customerId) === proof.customerId && id(p.overseasWarehouseId) === proof.warehouseId && (!proof.requiredSourcePositionId || id(p.id) === proof.requiredSourcePositionId) && p.inboundCode === proof.inboundCode && id(p.inboundOrderId) === id(candidate.inboundOrderId) && id(p.balanceId) === id(candidate.inventoryBalanceId) && validId(p.id) && validId(p.palletId) && Number(p.availablePackages) >= 1 && candidate.positions?.some(cp => id(cp.positionId) === id(p.id) && cp.palletNo === p.palletNo && Number(cp.availablePackages) >= 1)) if (position) { source = candidate; break } } check('只使用本轮入仓单同SKU实际可用托盘仓位', Boolean(source && position)) Object.assign(proof, { sourceBalanceId: id(source.inventoryBalanceId), sourcePositionId: id(position.id), sourcePalletId: id(position.palletId), sourcePalletNo: position.palletNo, sourceInboundOrderId: id(source.inboundOrderId) }) const body = evidence.request = { idempotencyKey: `${context.marker}-TRANSFER-ALLOCATE`, allocations: [{ inventoryPositionId: proof.sourcePositionId, palletId: proof.sourcePalletId, packageCount: 1 }] } const allocated = await request('POST', `${base}/allocations${scope}`, body) check('原库存分配进入待换标并产生唯一关联申请', allocated?.transferNo === proof.transferNo && allocated.status === 'WAIT_RELABEL' && typeof allocated.relabelNo === 'string' && allocated.relabelNo.length > 0 && validId(allocated.relabelApplicationId) && allocated.allocations?.length === 1 && id(allocated.allocations[0].inventoryPositionId) === proof.sourcePositionId && id(allocated.allocations[0].inboundOrderId) === proof.sourceInboundOrderId && allocated.allocations[0].inboundCode === proof.inboundCode && id(allocated.allocations[0].palletId) === proof.sourcePalletId && allocated.allocations[0].packageCount === 1) const replay = await request('POST', `${base}/allocations${scope}`, body) check('转运分配重放不生成第二申请或重复分配', JSON.stringify(replay) === JSON.stringify(allocated)) const merchant = await request('GET', `/api/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(proof.transferNo)}`, undefined, merchantToken) check('客户回查同单同换标申请及真实来源', merchant?.transferNo === proof.transferNo && merchant.status === 'WAIT_RELABEL' && merchant.relabelNo === allocated.relabelNo && id(merchant.relabelApplicationId) === id(allocated.relabelApplicationId) && JSON.stringify(merchant.allocations) === JSON.stringify(allocated.allocations)) Object.assign(proof, { status: 'WAIT_RELABEL', relabelNo: allocated.relabelNo, relabelApplicationId: id(allocated.relabelApplicationId) }) evidence.status = 'ALLOCATED_WAIT_RELABEL'; await persist(); return proof } catch (error) { evidence.status = 'FAILED'; evidence.failure = error.message; await persist(); throw error } }