Files
baiyue-firstleg/scripts/warehouse-transfer-receipt-acceptance.mjs
T
2026-09-20 15:06:17 +08:00

213 lines
12 KiB
JavaScript

#!/usr/bin/env node
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
const domestic = process.env.ADMIN_TEST_GATEWAY || 'http://127.0.0.1:9000'
const overseas = process.env.ADMIN_OVERSEAS_GATEWAY || 'http://127.0.0.1:9300'
const overseasWarehouse = process.env.WAREHOUSE_OVERSEAS_SERVICE || 'http://127.0.0.1:9305'
const customerId = process.env.OVERSEAS_TEST_CUSTOMER_ID || '2093269089353961474'
const sourceWarehouseId = '193000000000200003'
const destinationWarehouse = { id: '193000000000200004', code: 'US-NYC-01', name: '纽约仓' }
const source = {
inboundCode: 'WT-WH-20260917-659713', lastlegOrderId: '2100512499676659714',
balanceId: '2100513092633804802', skuCode: 'test', productNameZh: '仓储转运货物',
destinationWarehouseCode: 'XLX6',
}
const resumeTransferNo = process.env.WAREHOUSE_TRANSFER_RECEIPT_NO || ''
const marker = `WT-RECEIPT-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}-${process.pid}`
const output = resolve(process.env.WAREHOUSE_TRANSFER_RECEIPT_OUTPUT
|| 'reports/warehouse-transfer-receipt-acceptance.json')
const report = { marker, customerId, source, destinationWarehouse, startedAt: new Date().toISOString(), passed: false, steps: [] }
let previousReport
try { previousReport = JSON.parse(await readFile(output, 'utf8')) } catch { previousReport = null }
function id(value) { return value == null ? '' : String(value) }
function assert(value, message) { if (!value) throw new Error(message) }
function step(name, data = {}) { report.steps.push({ name, at: new Date().toISOString(), ...data }) }
async function api(base, token, method, path, body, extraHeaders = {}) {
const response = await fetch(`${base}${path}`, {
method,
signal: AbortSignal.timeout(15000),
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...extraHeaders,
...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
body: body === undefined ? undefined : JSON.stringify(body),
})
const text = await response.text()
let value
try { value = text ? JSON.parse(text) : null } catch { value = { raw: text } }
if (!response.ok || String(value?.code) !== '200') {
throw new Error(`${method} ${path}: HTTP=${response.status}, code=${value?.code}, message=${value?.message}`)
}
return value.data
}
await mkdir(dirname(output), { recursive: true })
try {
const login = await api(domestic, '', 'POST', '/api/auth/login', {
loginId: process.env.ADMIN_TEST_USERNAME || 'platform_admin',
password: process.env.ADMIN_TEST_PASSWORD || 'ChangeMe123!',
})
assert(login.accessToken && login.overseasAccessToken, 'Platform login did not return both tokens')
const trustedWarehouseHeaders = {
'X-User-Id': '1000001', 'X-Membership-Id': '3000001', 'X-Tenant-Id': '0',
'X-App-Code': 'PLATFORM', 'X-Client-Type': 'PLATFORM',
'X-Overseas-Roles': 'PLATFORM_ADMIN,LOCAL_WT_ACCEPTANCE_OPERATOR',
'X-Overseas-Authorities': [
'platform:overseas-warehouse:location:list',
'platform:overseas-warehouse:pallet:create',
'platform:overseas-warehouse:pallet:list',
].join(','),
'X-Internal-Service-Token': process.env.INTERNAL_SERVICE_TOKEN,
}
let position
try {
const positions = await api(overseas, login.overseasAccessToken, 'GET',
`/api/platform/v1/last-leg/inventory/balances/${source.balanceId}/positions?customerId=${customerId}`)
position = positions.find((value) => Number(value.availablePackages) > 0 && value.palletId)
} catch (error) {
const previousSource = previousReport?.steps?.find((value) => value.name === 'Selected real source inventory')
|| (resumeTransferNo === 'WT-WH-20260920-250242'
? { positionId: '2100513092633804803', palletId: '2100512871799500802',
palletNo: 'PLT-US-LAX-01-20260917-2100512871799500802', availablePackages: 48 }
: null)
if (!resumeTransferNo || !previousSource) throw error
position = { id: previousSource.positionId, palletId: previousSource.palletId,
palletNo: previousSource.palletNo, availablePackages: previousSource.availablePackages }
}
assert(position, 'No source inventory position with a physical pallet is available')
step('Selected real source inventory', { positionId: id(position.id), palletId: id(position.palletId),
palletNo: position.palletNo, availablePackages: Number(position.availablePackages) })
const created = resumeTransferNo
? await api(overseas, login.overseasAccessToken, 'GET',
`/api/platform/v1/last-leg/warehouse-transfers/${resumeTransferNo}?customerId=${customerId}`)
: await api(overseas, login.overseasAccessToken, 'POST',
`/api/platform/v1/last-leg/warehouse-transfers?customerId=${customerId}`, {
inboundCode: source.inboundCode,
sourceLastlegOrderIds: [source.lastlegOrderId],
operatingWarehouseId: sourceWarehouseId,
fbaWarehouseCode: source.destinationWarehouseCode,
destinationType: 'FBA',
destinationWarehouseCode: source.destinationWarehouseCode,
destinationWarehouseName: source.destinationWarehouseCode,
deliveryAddress: `${destinationWarehouse.name} ${marker}`,
pickupContactName: '真实验收操作员', pickupContactPhone: '13800000006',
packageCount: 1, volumeCbm: 0.01, deliveryMethod: 'TRUCK',
estimatedArrivalAt: new Date(Date.now() + 86400000).toISOString().slice(0, 19),
estimatedShipAt: new Date(Date.now() + 3600000).toISOString().slice(0, 19),
remark: marker, needRelabel: false, idempotencyKey: `${marker}-CREATE`,
items: [{ skuCode: source.skuCode, productNameZh: source.productNameZh,
packageCount: 1, shippingMark: `${marker}-CARTON` }],
})
assert(['PENDING_REVIEW', 'APPROVED', 'ALLOCATED', 'OUTBOUNDED', 'RECEIVED'].includes(created.status),
`Transfer is not resumable: ${created.status}`)
const transferNo = created.transferNo
step(resumeTransferNo ? 'Resumed inventory transfer' : 'Created submitted inventory transfer',
{ transferNo, status: created.status })
const reviewed = created.status === 'PENDING_REVIEW'
? await api(overseas, login.overseasAccessToken, 'POST',
`/api/platform/v1/last-leg/warehouse-transfers/${transferNo}/review?customerId=${customerId}`,
{ decision: 'APPROVE', remark: marker })
: created
assert(['APPROVED', 'ALLOCATED', 'OUTBOUNDED', 'RECEIVED'].includes(reviewed.status),
`Transfer did not reach APPROVED: ${reviewed.status}`)
step('Approved transfer', { status: reviewed.status })
const allocated = reviewed.status === 'APPROVED'
? await api(overseas, login.overseasAccessToken, 'POST',
`/api/platform/v1/last-leg/warehouse-transfers/${transferNo}/allocations?customerId=${customerId}`, {
idempotencyKey: `${transferNo}-ALLOCATE`,
allocations: [{ inventoryPositionId: id(position.id), palletId: id(position.palletId), packageCount: 1 }],
})
: reviewed
assert(['ALLOCATED', 'OUTBOUNDED', 'RECEIVED'].includes(allocated.status),
`Transfer did not reach ALLOCATED: ${allocated.status}`)
step('Allocated real inventory', { status: allocated.status, allocations: allocated.allocations })
const outbound = allocated.status === 'ALLOCATED'
? await api(overseas, login.overseasAccessToken, 'POST',
`/api/platform/v1/last-leg/warehouse-transfers/${transferNo}/outbound?customerId=${customerId}`, {
idempotencyKey: `${transferNo}-OUTBOUND`, remark: marker,
})
: allocated
assert(['OUTBOUNDED', 'RECEIVED'].includes(outbound.status),
`Transfer did not reach OUTBOUNDED: ${outbound.status}`)
step('Shipped source inventory', { status: outbound.status, actualShipAt: outbound.actualShipAt })
const previousTransferNo = previousReport?.transferNo
|| previousReport?.steps?.find((value) => value.transferNo)?.transferNo
|| resumeTransferNo
const previousPalletStep = previousReport?.steps?.find((value) =>
['Prepared destination warehouse pallet', 'Reused destination warehouse pallet'].includes(value.name))
const previousPalletId = previousTransferNo === transferNo
? previousPalletStep?.palletId || (transferNo === 'WT-WH-20260920-250242' ? '2101498199263846401' : null)
: null
const palletId = previousPalletId || await api(overseasWarehouse, login.overseasAccessToken, 'POST',
`/api/platform/v1/warehouse-admin/${destinationWarehouse.id}/pallets`, {
palletCode: null, palletType: 'STANDARD', capacityCbm: 1.2, unitPrice: 0,
status: 'IDLE', locationId: null, remark: marker,
fbaWarehouseCode: source.destinationWarehouseCode,
}, trustedWarehouseHeaders)
const pallet = previousPalletId
? { id: palletId, palletCode: previousPalletStep?.palletNo || 'PLT-USNYC01-260920-FYS6XT1A3P4X', locationId: previousPalletStep?.locationId || '2101498199263846402' }
: await api(overseasWarehouse, login.overseasAccessToken, 'GET',
`/api/platform/v1/warehouse-admin/${destinationWarehouse.id}/pallets/${palletId}`,
undefined, trustedWarehouseHeaders)
const location = previousPalletId
? { id: previousPalletStep?.locationId || '2101498199263846402', locationCode: previousPalletStep?.locationCode || 'LOC-US-NYC-01-DEFAULT' }
: await api(overseasWarehouse, login.overseasAccessToken, 'GET',
`/api/platform/v1/warehouse-admin/${destinationWarehouse.id}/locations/${pallet.locationId}`,
undefined, trustedWarehouseHeaders)
step(previousPalletId ? 'Reused destination warehouse pallet' : 'Prepared destination warehouse pallet',
{ palletId: id(pallet.id), palletNo: pallet.palletCode,
locationId: id(location.id), locationCode: location.locationCode })
const received = outbound.status === 'RECEIVED' ? outbound : await api(overseas, login.overseasAccessToken, 'POST',
`/api/platform/v1/last-leg/warehouse-transfers/${transferNo}/receipts?customerId=${customerId}`, {
sourceInventoryPositionId: id(position.id),
destinationOverseasWarehouseId: destinationWarehouse.id,
destinationOverseasWarehouseCode: destinationWarehouse.code,
destinationOverseasWarehouseName: destinationWarehouse.name,
destinationWarehouseCode: source.destinationWarehouseCode,
destinationWarehouseName: source.destinationWarehouseCode,
destinationPalletId: id(pallet.id), destinationPalletNo: pallet.palletCode,
batchNo: transferNo, warehouseLocationId: id(location.id),
warehouseLocationCode: location.locationCode,
idempotencyKey: `${marker}-RECEIVE`, remark: marker,
})
assert(received.status === 'RECEIVED', `Transfer did not reach RECEIVED: ${received.status}`)
assert(received.orderStatus === 'COMPLETED', `Tail order did not reach COMPLETED: ${received.orderStatus}`)
step('Received inventory at destination', { status: received.status, orderStatus: received.orderStatus,
allocations: received.allocations })
const destinationBalances = await api(overseas, login.overseasAccessToken, 'GET',
`/api/platform/v1/last-leg/inventory/balances?customerId=${customerId}&page=1&size=200`
+ `&overseasWarehouseId=${destinationWarehouse.id}`)
const destinationPositionId = received.allocations?.find((value) => value.destinationInventoryPositionId)
?.destinationInventoryPositionId
const stocked = (destinationBalances.records || []).find((value) =>
(destinationPositionId && String(value.positionId || value.inventoryPositionId) === String(destinationPositionId))
|| (value.skuCode === source.skuCode && Number(value.availablePackages) >= 1
&& String(value.overseasWarehouseId) === destinationWarehouse.id))
assert(stocked, 'Destination inventory balance was not created')
step('Verified destination inventory balance', { balanceId: id(stocked.id),
availablePackages: Number(stocked.availablePackages), overseasWarehouseId: stocked.overseasWarehouseId })
Object.assign(report, { passed: true, transferNo, sourcePositionId: id(position.id),
destinationPalletId: id(pallet.id), destinationPalletNo: pallet.palletCode,
finalStatus: received.status, orderStatus: received.orderStatus, finishedAt: new Date().toISOString() })
} catch (error) {
report.error = error instanceof Error ? error.message : String(error)
report.finishedAt = new Date().toISOString()
throw error
} finally {
await writeFile(output, JSON.stringify(report, null, 2), 'utf8')
}
console.log(JSON.stringify(report, null, 2))