Files
baiyue-firstleg/scripts/local-return-relabel-outbound.mjs
2026-09-22 08:49:21 +08:00

73 lines
5.8 KiB
JavaScript

import { readFile, writeFile, mkdir } from 'node:fs/promises'
import { dirname } from 'node:path'
import { AcceptanceRun, parseArguments, buildAcceptanceSummary } from './admin-test-env-acceptance.mjs'
import { resolvePickerAccess, createPicker, disablePicker } from './admin-test-env-modules/phase4-overseas-pda.mjs'
const options = parseArguments(process.argv.slice(2))
for (const url of [options.gateway, options.overseasGateway]) {
if (!['127.0.0.1', 'localhost'].includes(new URL(url).hostname)) throw new Error('Local only')
}
if (!process.env.LOCAL_RETURN_SOURCE) throw new Error('LOCAL_RETURN_SOURCE required')
const previous = JSON.parse(await readFile(process.env.LOCAL_RETURN_SOURCE, 'utf8'))
const f = previous.facts
if (!f.relabelCompleted || !f.returnNo || !f.relabelNo || !f.customerId || !f.warehouseId) throw new Error('Completed relabel evidence required')
const c = new AcceptanceRun(options)
const output = options.output || `reports/local-return-relabel-outbound-${c.marker}.json`
let picker, error = null
const report = { source: process.env.LOCAL_RETURN_SOURCE, facts: f, scope: 'PLATFORM_VERIFIED_OUTBOUND_ONLY', merchantInventoryRead: 'NOT_VERIFIED_SEE_SOURCE_REPORT' }
function check(name, ok) {
c.assert('local-outbound', name, null, Boolean(ok), '', { deploymentBlocker: true })
if (!ok) throw new Error(name)
}
async function api(region, token, method, path, body) {
const r = region === 'cn' ? await c.request(method, path, body, token) : await c.requestOverseas(method, path, body, {}, token)
const ok = r.http === 200 && String(r.body?.code) === '200'
c.addStep('local-outbound', `${method} ${path}`, r, ok, '', { deploymentBlocker: true })
if (!ok) throw new Error(`${path}: ${r.body?.code} ${r.body?.message}`)
return r.body.data
}
async function save() {
await mkdir(dirname(output), { recursive: true })
await writeFile(output, JSON.stringify({ ...report, error, summary: buildAcceptanceSummary(c.steps, [], []), steps: c.steps }, null, 2))
}
try {
const l = await api('cn', '', 'POST', '/api/auth/login', { loginId: options.username, password: options.password })
c.token = l.accessToken; c.overseasToken = l.overseasAccessToken
const inv = '/api/platform/v1/last-leg/return-inventory/cartons'
const root = '/api/platform/v1/last-leg/outbound-orders'
const candidates = await api('os', c.overseasToken, 'GET', `${root}/return/candidates?customerId=${f.customerId}&warehouseId=${f.warehouseId}`)
const matches = []
for (const row of candidates.filter(x => x.returnNo === f.returnNo)) {
const d = await api('os', c.overseasToken, 'GET', `${inv}/${encodeURIComponent(row.cartonMark)}`)
if (d.relabelNo === f.relabelNo) matches.push(d)
}
check('Exactly one matching relabelled return carton', matches.length === 1)
const d = matches[0]
check('Customer warehouse quantity and completed relabel match', d.customerId === f.customerId && d.overseasWarehouseId === f.warehouseId && d.sourceReturnNo === f.returnNo && d.status === 'AVAILABLE' && d.relabelStatus === 'COMPLETED' && Number(d.totalQuantity) === 1)
const access = await resolvePickerAccess(c)
check('PDA role', access)
picker = await createPicker(c, 94, f.warehouseId, access)
check('PDA ready', picker?.token)
const body = { recipient: 'LOCAL ACCEPTANCE', overseasWarehouseId: f.warehouseId, returnNo: f.returnNo, cartonMarks: [d.cartonMark], remark: f.marker, idempotencyKey: `${f.marker}-OUTBOUND` }
const task = await api('os', c.overseasToken, 'POST', `${root}/return?customerId=${f.customerId}`, body)
report.outboundNo = task.outboundNo; report.taskNo = task.taskNo
await save()
const replay = await api('os', c.overseasToken, 'POST', `${root}/return?customerId=${f.customerId}`, body)
check('Outbound idempotent', replay.outboundNo === task.outboundNo && replay.taskNo === task.taskNo)
const path = `/api/pda/v1/last-leg/outbound-tasks/${encodeURIComponent(task.taskNo)}`
const detail = await api('os', picker.token, 'GET', path)
const frozen = await api('os', c.overseasToken, 'GET', `${root}/${task.outboundNo}?customerId=${f.customerId}`)
check('Frozen outbound contains only matching carton', detail.items.length === 1 && Number(detail.items[0].packageCount) === 1 && frozen.pallets.length === 1 && frozen.pallets[0].cartonMarks === d.cartonMark && String(frozen.pallets[0].id) === String(detail.items[0].outboundPalletItemId))
await api('os', picker.token, 'POST', `${path}/pick`, { outboundPalletItemId: detail.items[0].outboundPalletItemId, inventoryPositionId: null, packages: 1, cartonMarks: [d.cartonMark], idempotencyKey: `${f.marker}-PICK` })
const done = await api('os', picker.token, 'POST', `${path}/complete`, { idempotencyKey: `${f.marker}-OUTBOUND-COMPLETE` })
const final = await api('os', c.overseasToken, 'GET', `${root}/${task.outboundNo}?customerId=${f.customerId}`)
const stock = await api('os', c.overseasToken, 'GET', `${inv}/${encodeURIComponent(d.cartonMark)}`)
check('Outbound completed and stock OUTBOUNDED', done.status === 'COMPLETED' && final.outbound.status === 'OUTBOUNDED' && stock.status === 'OUTBOUNDED' && stock.relabelNo === f.relabelNo && stock.sourceReturnNo === f.returnNo && Number(stock.totalQuantity) === 1)
report.finalOutboundStatus = final.outbound.status; report.finalInventoryStatus = stock.status; report.fulfillmentPassed = true
} catch (e) { error = e.message; process.exitCode = 1 }
finally {
if (picker?.membershipId) { try { await disablePicker(c, picker, 94) } catch (e) { error = `${error || ''} cleanup: ${e.message}`; process.exitCode = 1 } }
await save()
console.log(JSON.stringify({ output, error, outboundNo: report.outboundNo, finalOutboundStatus: report.finalOutboundStatus, finalInventoryStatus: report.finalInventoryStatus, summary: buildAcceptanceSummary(c.steps, [], []), failures: c.steps.filter(x => !x.passed) }, null, 2))
}