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

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.outboundAt))
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.outboundAt === shipped.outboundAt)
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 }
}