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

126 lines
9.9 KiB
JavaScript

import { mkdir, writeFile } from 'node:fs/promises'
const BASE = '/api/platform/v1/last-leg'
const RESOURCES = '/api/platform/v1/overseas-warehouse-resources'
const id = value => String(value ?? '')
const validId = value => /^[1-9]\d*$/.test(id(value))
const stable = value => JSON.stringify(value, (_, item) => item && typeof item === 'object' && !Array.isArray(item)
? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item)
/** 转运收货复用其他海外仓的空闲匹配托盘,并验证原单入库及关联换标结算。 */
export async function receiveWarehouseTransfer(context, { proof, merchantToken, checkpoint, delay = () => new Promise(resolve => setTimeout(resolve, 1000)) }) {
if (context.createdResources?.phase4OverseasClientServices?.transferFulfillment !== proof
|| proof?.marker !== context.marker || !/^[A-Za-z0-9_-]{1,100}$/.test(context.marker)
|| proof.customerId !== id(context.context?.merchantQualificationFixture?.tenantId)
|| proof.status !== 'OUTBOUNDED' || proof.outbound?.status !== 'OUTBOUNDED_HTTP_VERIFIED'
|| proof.outboundAllocations?.length !== 1 || !merchantToken || proof.receipt) throw Error('转运收货缺少本轮已出库原单,或已执行')
const allocation = proof.outboundAllocations[0]
const evidence = proof.receipt = { status: 'RUNNING', requests: [] }
let initialized = false
const directory = new URL('../../reports/warehouse-transfer-receipt-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, ok) => {
context.assert('phase4-overseas-client-services', name, null, Boolean(ok), `transferNo=${proof.transferNo}`,
{ scenario: 'warehouse-transfer-receipt', deploymentBlocker: true })
if (!ok) throw Error(name)
}
const request = async (method, path, body, token = context.token) => {
const entry = { method, path, body, status: 'SENDING' }; evidence.requests.push(entry); await persist()
let response
try { response = await context.request(method, path, body, token) }
catch (error) { entry.status = 'UNKNOWN_RESULT'; throw error }
Object.assign(entry, { http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId,
status: !response?.http || response.http >= 500 ? 'UNKNOWN_RESULT' : 'RESPONDED' }); await persist()
check(`转运收货 ${method} ${path}`, response?.http === 200 && id(response?.body?.code) === '200')
return response.body.data
}
const base = `${BASE}/warehouse-transfers/${encodeURIComponent(proof.transferNo)}`
const scope = `?customerId=${encodeURIComponent(proof.customerId)}`
const same = (detail, status) => detail?.transferNo === proof.transferNo && detail.status === status
&& detail.relabelNo === proof.relabelNo && id(detail.relabelApplicationId) === proof.relabelApplicationId
&& detail.allocations?.length === 1 && id(detail.allocations[0].inventoryPositionId) === id(allocation.inventoryPositionId)
&& detail.allocations[0].packageCount === 1 && detail.allocations[0].skuCode === allocation.skuCode
&& detail.allocations[0].status === (status === 'OUTBOUNDED' ? 'SHIPPED' : 'RECEIVED')
try {
const original = await request('GET', base + scope)
check('原转运出库来源不变', same(original, 'OUTBOUNDED') && Boolean(original.fbaWarehouseCode))
let target
// 完整翻页寻找既有空闲资源,不创建公共托盘,也不借用已被其他业务占用的托盘。
for (let page = 1; page <= 100 && !target; page++) {
const pallets = await request('GET', `${RESOURCES}/pallets?status=IDLE&page=${page}&size=200`)
check('目标托盘分页有效', Array.isArray(pallets?.records) && Number.isSafeInteger(Number(pallets.total)) && Number(pallets.total) >= 0)
for (const candidate of pallets.records) {
if (candidate.status !== 'IDLE' || id(candidate.customerId) !== '0' || !validId(candidate.warehouseId)
|| id(candidate.warehouseId) === proof.warehouseId || candidate.fbaWarehouseCode !== original.fbaWarehouseCode
|| !validId(candidate.id) || !validId(candidate.locationId)) continue
const resource = `${RESOURCES}/${candidate.warehouseId}`
const pallet = await request('GET', `${resource}/pallets/${candidate.id}`)
if (pallet?.status !== 'IDLE' || id(pallet.id) !== id(candidate.id) || id(pallet.customerId) !== '0'
|| id(pallet.warehouseId) !== id(candidate.warehouseId) || id(pallet.locationId) !== id(candidate.locationId)
|| pallet.fbaWarehouseCode !== original.fbaWarehouseCode || !pallet.palletCode) continue
const location = await request('GET', `${resource}/locations/${pallet.locationId}`)
if (id(location?.customerId) !== '0' || id(location?.warehouseId) !== id(pallet.warehouseId)
|| id(location?.id) !== id(pallet.locationId) || !location.locationCode
|| !(location.locationType === 'VIRTUAL' ? location.status === 'ACTIVE' : location.status === 'IDLE')) continue
const master = await request('GET', `/api/platform/v1/warehouse-admin/${pallet.warehouseId}?tenantId=0`)
const warehouse = master?.warehouse
if (id(warehouse?.id) !== id(pallet.warehouseId) || id(warehouse.tenantId) !== '0'
|| warehouse.warehouseType !== 'OVERSEAS' || warehouse.status !== 'ACTIVE') continue
target = { pallet, location, warehouse }; break
}
if (page * 200 >= Number(pallets.total)) break
}
check('必须存在其他海外仓同目的仓的空闲托盘与可预留库位', Boolean(target))
evidence.target = target
const { pallet, location, warehouse } = target
const ledger = async () => {
const data = await request('GET', `${BASE}/inventory/ledger?${new URLSearchParams({ customerId: proof.customerId,
overseasWarehouseId: id(warehouse.id), businessOrderNo: proof.transferNo, businessType: 'STOCK_IN', page: '1', size: '200' })}`)
check('目标收货流水完整无重复', Array.isArray(data?.records) && Number(data.total) === data.records.length
&& new Set(data.records.map(row => id(row.id))).size === data.records.length)
return data.records
}
check('目标仓未重复接收原转运单', (await ledger()).length === 0)
const body = evidence.command = { sourceInventoryPositionId: id(allocation.inventoryPositionId),
destinationOverseasWarehouseId: id(warehouse.id), destinationOverseasWarehouseCode: warehouse.warehouseCode,
destinationOverseasWarehouseName: warehouse.warehouseName, destinationWarehouseCode: original.fbaWarehouseCode,
destinationPalletId: id(pallet.id), destinationPalletNo: pallet.palletCode,
warehouseLocationId: id(location.id), warehouseLocationCode: location.locationCode,
batchNo: proof.transferNo, idempotencyKey: `${context.marker}-TRANSFER-RECEIVE`, remark: context.marker }
const received = await request('POST', `${base}/receipts${scope}`, body)
check('原转运收货完成且生成目标库存位置', same(received, 'RECEIVED') && validId(received.allocations[0].destinationInventoryPositionId))
const committed = received.receiptCommands?.[0]
check('收货命令完成且权威目标绑定保持一致', received.receiptCommands?.length === 1 && committed?.status === 'COMPLETED'
&& id(committed.sourceInventoryPositionId) === id(allocation.inventoryPositionId)
&& committed.request?.idempotencyKey === body.idempotencyKey && committed.request?.batchNo === body.batchNo
&& id(committed.target?.warehouseId) === id(warehouse.id) && id(committed.target?.palletId) === id(pallet.id)
&& committed.target?.palletNo === pallet.palletCode && id(committed.target?.locationId) === id(location.id)
&& committed.target?.locationCode === location.locationCode && committed.target?.fbaCode === original.fbaWarehouseCode)
const movements = await ledger()
check('目标仓唯一原单新SKU入库一箱', movements.length === 1 && movements.every(row => validId(row.id)
&& id(row.customerId) === proof.customerId && id(row.overseasWarehouseId) === id(warehouse.id)
&& row.businessOrderNo === proof.transferNo && row.businessType === 'STOCK_IN' && row.inboundCode === proof.inboundCode
&& row.skuCode === allocation.skuCode && row.palletNo === pallet.palletCode && row.warehouseLocationCode === location.locationCode
&& Number(row.deltaPackages) === 1))
const replay = await request('POST', `${base}/receipts${scope}`, body)
check('收货重放不重复入库', stable(replay) === stable(received) && stable(await ledger()) === stable(movements))
const owned = await request('GET', `/api/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(proof.transferNo)}`, undefined, merchantToken)
check('客户原转运收货事实一致', same(owned, 'RECEIVED') && stable(owned.allocations) === stable(received.allocations))
let relabel
for (let attempt = 0; attempt < 31; attempt++) {
relabel = await request('GET', `${BASE}/relabel-applications/${encodeURIComponent(proof.relabelNo)}`)
check('原换标随转运收货完成且身份保持', relabel?.application?.relabelNo === proof.relabelNo
&& relabel.application.customerOrderNo === proof.transferNo && relabel.application.status === 'COMPLETED'
&& relabel.application.fulfillmentStatus === 'COMPLETED')
if (relabel.application.walletStatus === 'SETTLED') break
if (attempt < 30) await delay()
}
check('原转运关联换标费用完成结算', relabel.application.walletStatus === 'SETTLED')
evidence.stockInLedger = movements; evidence.status = 'RECEIVED_AND_SETTLED_HTTP_VERIFIED'; proof.status = 'RECEIVED'
await persist(); return proof
} catch (error) { evidence.status = 'FAILED'; evidence.failure = error.message; await persist(); throw error }
}