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)) }