Files
baiyue-firstleg/scripts/admin-test-env-modules/warehouse-transfer-allocation.mjs

83 lines
7.0 KiB
JavaScript

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 }
}