贴标申请,仓储转运
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { mkdir, writeFile, readFile } from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
const base = process.env.ADMIN_OVERSEAS_GATEWAY || 'http://127.0.0.1:9300'
|
||||
if (!['localhost', '127.0.0.1'].includes(new URL(base).hostname)) throw new Error('Local acceptance only')
|
||||
const merchant = process.env.MERCHANT_OVERSEAS_ACCESS_TOKEN, pda = process.env.PDA_OVERSEAS_ACCESS_TOKEN, admin = process.env.ADMIN_OVERSEAS_ACCESS_TOKEN
|
||||
const warehouse = process.env.OVERSEAS_TEST_WAREHOUSE_ID
|
||||
if (!merchant || !pda || !admin || !warehouse) throw new Error('Explicit session and warehouse required')
|
||||
const marker = `CLOSE-${Date.now()}`
|
||||
const report = { marker, passed: false, scope: 'REAL_HTTP_SYNTHETIC_WAREHOUSE_EVIDENCE_NO_FINANCE', scenarios: [] }
|
||||
const key = () => `${marker}-${crypto.randomUUID()}`
|
||||
const root = '/api/merchant/v1/last-leg/relabel-applications'
|
||||
const work = '/api/pda/v1/last-leg/relabel-tasks'
|
||||
const pallets = '/api/pda/v1/last-leg/pallet-operations'
|
||||
const assert = (condition, message) => { if (!condition) throw new Error(message) }
|
||||
async function raw(token, method, path, body) {
|
||||
const response = await fetch(base + path, { method, headers: { Authorization: `Bearer ${token}`, ...(body && !(body instanceof FormData) ? { 'Content-Type': 'application/json' } : {}) }, body: body instanceof FormData ? body : body ? JSON.stringify(body) : undefined })
|
||||
const result = await response.json()
|
||||
return { ...result, http: response.status }
|
||||
}
|
||||
async function api(token, method, path, body) {
|
||||
const result = await raw(token, method, path, body)
|
||||
assert(result.http === 200 && String(result.code) === '200', `${path}: ${result.code} ${result.message}`)
|
||||
return result.data
|
||||
}
|
||||
async function reject(token, path, body, message) {
|
||||
const result = await raw(token, 'POST', path, body)
|
||||
assert(String(result.code) !== '200' && message.test(result.message || ''), `Expected rejection at ${path}, got ${result.code} ${result.message}`)
|
||||
}
|
||||
const resolve = code => api(pda, 'POST', `${pallets}/pallets/resolve`, { code })
|
||||
async function move(no, mark, from, to) {
|
||||
const source = await resolve(from), target = await resolve(to)
|
||||
const request = { relabelNo: no, source: { palletCode: from, expectedVersion: source.palletVersion }, target: { palletCode: to, expectedVersion: target.palletVersion }, cartonMarks: [mark], reason: marker, idempotencyKey: key() }
|
||||
const result = await api(pda, 'POST', `${pallets}/relabel-disassemble`, request)
|
||||
const replay = await api(pda, 'POST', `${pallets}/relabel-disassemble`, request)
|
||||
assert(replay.replayed === true, 'Move replay was not idempotent')
|
||||
const after = await resolve(to)
|
||||
assert(Number(after.totalPackages) === Number(target.totalPackages) + 1 && after.status === 'OCCUPIED', 'Target stock/status did not update exactly once')
|
||||
assert(Number((await resolve(from)).totalPackages) === Number(source.totalPackages) - 1, 'Source stock was not reduced once')
|
||||
return result
|
||||
}
|
||||
try {
|
||||
for (const deliveryMethod of ['TRUCK_PICKUP', 'EXPRESS', 'EXPRESS_PICKUP']) {
|
||||
const candidates = await api(merchant, 'GET', `${root}/inventory-candidates?operationWarehouseId=${warehouse}`)
|
||||
const candidate = candidates.find(value => value.destinationFbaWarehouseCode && value.positions?.some(pos => Number(pos.availablePackages) > 0 && pos.palletNo !== 'UNBOUND'))
|
||||
assert(candidate, 'No available inventory for another scenario')
|
||||
const position = candidate.positions.find(pos => Number(pos.availablePackages) > 0 && pos.palletNo !== 'UNBOUND')
|
||||
const mark = `${marker}-${deliveryMethod}`
|
||||
const request = { customerOrderNo: mark, operationWarehouseId: warehouse, labelDeadline: new Date(Date.now() + 86400000).toISOString().slice(0, 19), contactName: '流程验收', contactPhone: '13800000000', deliveryMethod, printShippingLabel: false, idempotencyKey: key(), items: [{ sourceInventoryBalanceId: candidate.inventoryBalanceId, sourceInventoryPositionId: position.positionId, sourceInboundOrderId: candidate.inboundOrderId, skuCode: candidate.skuCode || 'TEST', productNameZh: '验收测试货物', totalPackageCount: 1, notifiedPackageCount: 1, cartonMark: mark, fbaCartonMark: `FBA-${mark}`, destinationFbaWarehouseCode: candidate.destinationFbaWarehouseCode, operationType: 'CARTON_LABEL', skuItems: [] }] }
|
||||
request.items[0].shipmentNo = `SHIP-${mark}`
|
||||
const created = await api(merchant, 'POST', root, request)
|
||||
const no = created.application.relabelNo
|
||||
const scenario = { deliveryMethod, relabelNo: no, originalPallet: position.palletNo, checks: [] }
|
||||
report.scenarios.push(scenario)
|
||||
await reject(merchant, root, { ...request, idempotencyKey: key() }, /已用于换标单/)
|
||||
scenario.checks.push('DUPLICATE_APPLICATION_REJECTED')
|
||||
// Read an actual Print-generated test document from File, then upload it through the merchant API.
|
||||
const pdfReport = JSON.parse(await readFile('reports/relabel-closure-20260916/pdf.json', 'utf8'))
|
||||
const doc = pdfReport.files[0]
|
||||
const pdfResponse = await fetch(`http://127.0.0.1:9206/internal/file/v1/business-files/${process.env.OVERSEAS_TEST_CUSTOMER_ID}/${doc.category}/${pdfReport.businessId}/${doc.fileId}/content`, { headers: { 'X-Internal-Service-Token': process.env.INTERNAL_SERVICE_TOKEN } })
|
||||
const pdf = Buffer.from(await pdfResponse.arrayBuffer())
|
||||
assert(pdfResponse.ok && pdf.subarray(0, 5).toString() === '%PDF-', 'Test PDF not available')
|
||||
for (const scope of ['CARTON_TOTAL', 'SKU_TOTAL', 'ITEM']) {
|
||||
const form = new FormData(); form.append('file', new Blob([pdf], { type: 'application/pdf' }), `${marker}.pdf`)
|
||||
await api(merchant, 'POST', `${root}/${no}/files?scope=${scope}${scope === 'ITEM' ? `&itemId=${created.items[0].id}` : ''}`, form)
|
||||
}
|
||||
let task = await api(pda, 'POST', `${work}/${no}/claim`, { idempotencyKey: key() })
|
||||
const documents = await api(pda, 'GET', `${work}/${no}/documents`)
|
||||
assert(documents.length >= 3, 'PDA is missing uploaded label documents')
|
||||
for (const file of documents) {
|
||||
const response = await fetch(`${base}${work}/${no}/documents/${file.fileId}/download?scope=${file.scope}${file.itemId ? `&itemId=${file.itemId}` : ''}`, { headers: { Authorization: `Bearer ${pda}` } })
|
||||
const downloaded = Buffer.from(await response.arrayBuffer())
|
||||
assert(response.ok && createHash('sha256').update(downloaded).digest('hex') === createHash('sha256').update(pdf).digest('hex'), 'PDA document mismatch')
|
||||
}
|
||||
scenario.checks.push('MERCHANT_UPLOAD_PDA_DOWNLOAD_THREE_SCOPES')
|
||||
task = await api(pda, 'POST', `${work}/${no}/cartons/scan`, { cartonMark: mark, expectedVersion: task.version, idempotencyKey: key() })
|
||||
const photoForm = new FormData(); photoForm.append('file', new Blob([Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aWQAAAABJRU5ErkJggg==', 'base64')], { type: 'image/png' }), 'synthetic.png')
|
||||
const photo = await api(pda, 'POST', `${work}/${no}/photos/files`, photoForm)
|
||||
const complete = () => ({ photoFileIds: [photo.fileId], expectedVersion: task.version, remark: marker, idempotencyKey: key() })
|
||||
await reject(pda, `${work}/${no}/complete`, complete(), /托盘绑定/)
|
||||
scenario.checks.push('UNBOUND_COMPLETION_REJECTED')
|
||||
const first = await api(pda, 'POST', `${pallets}/relabel-pallet-label`, { relabelNo: no, idempotencyKey: key() })
|
||||
await move(no, mark, position.palletNo, first.palletCode)
|
||||
const second = await api(pda, 'POST', `${pallets}/relabel-pallet-label`, { relabelNo: no, idempotencyKey: key() })
|
||||
await move(no, mark, first.palletCode, second.palletCode)
|
||||
scenario.targetPallet = second.palletCode
|
||||
scenario.checks.push('REBIND_STOCK_CONSERVED_AND_REPLAY_SAFE')
|
||||
task = await api(pda, 'POST', `${work}/${no}/complete`, complete())
|
||||
if (deliveryMethod === 'TRUCK_PICKUP') {
|
||||
task = await api(admin, 'POST', `/api/platform/v1/last-leg/relabel-applications/${no}/rework`, { decision: 'REWORK', expectedVersion: task.version, idempotencyKey: key(), remark: '验收退回:请核对并重新贴标' })
|
||||
assert(task.reviewRemark?.includes('验收退回'), 'Rework remark missing on PDA')
|
||||
task = await api(pda, 'POST', `${work}/${no}/cartons/scan`, { cartonMark: mark, expectedVersion: task.version, idempotencyKey: key() })
|
||||
await reject(pda, `${work}/${no}/complete`, complete(), /托盘绑定/)
|
||||
await move(no, mark, second.palletCode, first.palletCode)
|
||||
scenario.targetPallet = first.palletCode
|
||||
task = await api(pda, 'POST', `${work}/${no}/complete`, complete())
|
||||
scenario.checks.push('REWORK_RESCAN_REBIND_AND_RESUBMIT')
|
||||
}
|
||||
const confirmed = await api(merchant, 'POST', `${root}/${no}/confirm-completion`, { idempotencyKey: key(), remark: marker })
|
||||
scenario.outboundNo = confirmed.application.outboundOrderNo
|
||||
assert(scenario.outboundNo, 'Customer confirmation did not create direct outbound')
|
||||
await reject(pda, `${pallets}/relabel-disassemble`, { relabelNo: no, source: { palletCode: scenario.targetPallet, expectedVersion: 0 }, target: { palletCode: position.palletNo, expectedVersion: 0 }, cartonMarks: [mark], idempotencyKey: key() }, /排车或出库/)
|
||||
const resolved = await api(pda, 'GET', `/api/pda/v1/last-leg/outbound-tasks/resolve/${scenario.outboundNo}`)
|
||||
const picked = await api(pda, 'POST', `/api/pda/v1/last-leg/outbound-tasks/${resolved.taskNo}/pallets/${scenario.targetPallet}/pick`, { idempotencyKey: key() })
|
||||
await api(pda, 'POST', `/api/pda/v1/last-leg/outbound-tasks/${resolved.taskNo}/complete`, { idempotencyKey: key() })
|
||||
const outbound = await api(admin, 'GET', `/api/platform/v1/last-leg/outbound-orders/${scenario.outboundNo}?page=1&size=20`)
|
||||
assert(outbound.outbound.status === 'OUTBOUNDED', 'Direct outbound not completed')
|
||||
scenario.outboundStatus = outbound.outbound.status
|
||||
scenario.checks.push('DIRECT_OUTBOUND_COMPLETED', 'OUTBOUND_REBIND_REJECTED')
|
||||
}
|
||||
report.passed = true
|
||||
} catch (error) { report.error = error.message; process.exitCode = 1 }
|
||||
finally {
|
||||
await mkdir('reports/relabel-closure-20260916', { recursive: true })
|
||||
await writeFile('reports/relabel-closure-20260916/extra.json', JSON.stringify(report, null, 2))
|
||||
console.log(JSON.stringify(report, null, 2))
|
||||
}
|
||||
Reference in New Issue
Block a user