84 lines
6.5 KiB
JavaScript
84 lines
6.5 KiB
JavaScript
import { mkdir, writeFile } from 'node:fs/promises'
|
|
|
|
const BASE = '/api/platform/v1/last-leg'
|
|
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)
|
|
|
|
/** 原转运换标确认后出库,独立核验新SKU出库流水及重放不重复扣库。 */
|
|
export async function outboundWarehouseTransfer(context, { proof, merchantToken, checkpoint }) {
|
|
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 !== 'ALLOCATED' || proof.relabelFulfillment?.execution?.status !== 'HTTP_VERIFIED_READY_FOR_TRANSFER_OUTBOUND'
|
|
|| proof.outboundAllocations?.length !== 1 || !merchantToken || proof.outbound) throw Error('转运出库缺少本轮原单换标完成证据,或已执行')
|
|
const allocation = proof.outboundAllocations[0]
|
|
if (!validId(allocation.inventoryPositionId) || !validId(allocation.palletId) || allocation.packageCount !== 1
|
|
|| allocation.status !== 'RESERVED' || allocation.inboundCode !== proof.inboundCode || !allocation.skuCode || !allocation.palletNo) throw Error('转运实际出库预留无效')
|
|
const evidence = proof.outbound = { status: 'RUNNING', requests: [] }
|
|
let initialized = false
|
|
const directory = new URL('../../reports/warehouse-transfer-outbound-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-outbound', 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
|
|
&& id(detail.operatingWarehouseId) === proof.warehouseId && detail.packageCount === 1
|
|
&& detail.allocations?.length === 1 && id(detail.allocations[0].inventoryPositionId) === id(allocation.inventoryPositionId)
|
|
&& id(detail.allocations[0].palletId) === id(allocation.palletId) && detail.allocations[0].skuCode === allocation.skuCode
|
|
&& detail.allocations[0].packageCount === 1 && detail.allocations[0].inboundCode === proof.inboundCode
|
|
&& detail.allocations[0].status === (status === 'ALLOCATED' ? 'RESERVED' : 'SHIPPED')
|
|
const ledger = async () => {
|
|
const page = await request('GET', `${BASE}/inventory/ledger?${new URLSearchParams({ customerId: proof.customerId,
|
|
overseasWarehouseId: proof.warehouseId, businessOrderNo: proof.transferNo, businessType: 'SHIP', page: '1', size: '200' })}`)
|
|
check('转运出库流水完整且无重复记录', Array.isArray(page?.records) && Number(page.total) === page.records.length
|
|
&& new Set(page.records.map(row => id(row.id))).size === page.records.length)
|
|
return page.records
|
|
}
|
|
try {
|
|
const original = await request('GET', base + scope)
|
|
check('出库前仍为原转运实际新SKU预留', same(original, 'ALLOCATED') && Boolean(original.fbaWarehouseCode))
|
|
check('原转运出库前没有出库流水', (await ledger()).length === 0)
|
|
const body = { idempotencyKey: `${context.marker}-TRANSFER-OUTBOUND`, remark: context.marker }
|
|
const shipped = await request('POST', `${base}/outbound${scope}`, body)
|
|
check('原转运新SKU预留出库完成', same(shipped, 'OUTBOUNDED') && Boolean(shipped.actualShipAt))
|
|
const movements = await ledger()
|
|
check('原转运唯一SHIP流水按实际新SKU托盘扣除一箱', movements.length === 1 && movements.every(row => validId(row.id)
|
|
&& row.businessType === 'SHIP' && row.businessOrderNo === proof.transferNo && row.inboundCode === proof.inboundCode
|
|
&& id(row.customerId) === proof.customerId && id(row.overseasWarehouseId) === proof.warehouseId
|
|
&& row.palletNo === allocation.palletNo
|
|
&& row.skuCode === allocation.skuCode && Number(row.deltaPackages) === -1))
|
|
const replay = await request('POST', `${base}/outbound${scope}`, body)
|
|
check('出库重放保持原单和流水不变', stable(replay) === stable(shipped) && 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, 'OUTBOUNDED') && owned.actualShipAt === shipped.actualShipAt)
|
|
const relabel = await request('GET', `${BASE}/relabel-applications/${encodeURIComponent(proof.relabelNo)}`)
|
|
check('原关联换标同步出库且未提前完成', relabel?.application?.relabelNo === proof.relabelNo
|
|
&& relabel.application.customerOrderNo === proof.transferNo && relabel.application.fulfillmentStatus === 'OUTBOUNDED'
|
|
&& relabel.application.status === 'READY_FOR_OUTBOUND')
|
|
Object.assign(proof, { status: 'OUTBOUNDED' }); evidence.shipLedger = movements; evidence.status = 'OUTBOUNDED_HTTP_VERIFIED'
|
|
await persist(); return proof
|
|
} catch (error) { evidence.status = 'FAILED'; evidence.failure = error.message; await persist(); throw error }
|
|
}
|