83 lines
7.0 KiB
JavaScript
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 }
|
|
}
|