Files
baiyue-firstleg/scripts/admin-test-env-modules/phase4-overseas-return-receipt-contract.test.mjs
T
2026-09-20 19:33:51 +08:00

180 lines
11 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { validationFailure, inventorySnapshot } from './phase4-overseas-return-receipt.mjs'
import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
import { completeReturnRelabel, outboundRelabelledReturn } from './phase4-overseas-return-followups.mjs'
const moduleUrl = new URL('./phase4-overseas-return-receipt.mjs', import.meta.url)
const acceptanceUrl = new URL('../admin-test-env-acceptance.mjs', import.meta.url)
test('relabelled outbound refuses another relabel application before creating an outbound', async () => {
const calls = []
const context = { marker: 'fixture', overseasToken: 'admin', context: { merchantQualificationFixture: { tenantId: '123' } },
addStep() {}, assert() {},
async requestOverseas(method, path) {
calls.push({ method, path })
return { http: 200, body: { code: '200', data: path.includes('/candidates')
? [{ returnNo: 'RT-1', cartonMark: 'BOX' }] : { relabelNo: 'RL-OTHER' } } }
} }
await assert.rejects(outboundRelabelledReturn(context, 'merchant', 'pda', 'RL-1', 'RT-1', '42'), /只出库本轮/)
assert.ok(calls.every(call => call.method === 'GET'))
})
test('return outbound binds platform frozen carton to PDA item without requiring ordinary inventory positions', async () => {
const context = { marker: 'fixture', overseasToken: 'admin', context: { merchantQualificationFixture: { tenantId: '123' } },
createdResources: { phase4OverseasReturnReceipt: {} }, addStep() {},
assert(_m, name, _r, passed) { assert.ok(passed, name) },
async requestOverseas(method, path, body) {
let data
if (path.includes('/candidates')) data = [{ returnNo: 'RT-1', cartonMark: 'BOX' }]
else if (path.includes('/return-inventory/cartons/')) data = { cartonMark: 'BOX', customerId: '123',
overseasWarehouseId: '42', sourceReturnNo: 'RT-1', relabelNo: 'RL-1', relabelStatus: 'COMPLETED',
status: this.done ? 'OUTBOUNDED' : 'AVAILABLE', totalQuantity: 1, items: [{ skuCode: 'SKU', quantity: 1 }] }
else if (path.includes('/outbound-orders/return?')) data = { taskNo: 'TASK', outboundNo: 'OUT' }
else if (path.endsWith('/complete')) { this.done = true; data = { status: 'COMPLETED' } }
else if (path.endsWith('/pick')) {
assert.deepEqual(body.cartonMarks, ['BOX']); assert.equal(body.outboundPalletItemId, '9')
data = { status: 'IN_PROGRESS' }
} else if (path.includes('/outbound-orders/OUT?')) data = { outbound: { outboundNo: 'OUT', status: this.done ? 'OUTBOUNDED' : 'WAIT_PICK' }, pallets: [{ id: '9', cartonMarks: 'BOX' }] }
else data = { task: { outboundNo: 'OUT' }, items: [{ outboundPalletItemId: '9', packageCount: 1, positions: [] }] }
return { http: 200, body: { code: '200', data } }
} }
await outboundRelabelledReturn(context, 'merchant', 'pda', 'RL-1', 'RT-1', '42')
assert.equal(context.createdResources.phase4OverseasReturnReceipt.relabelOutboundNo, 'OUT')
})
test('return relabel lifecycle requires review, quantities, binding, photo and customer confirmation', async () => {
const calls = []
const task = { relabelNo: 'RL-1', version: 1, items: [{ itemId: '1', cartonMark: 'BOX', operationType: 'RETURN_BULK_SKU_LABEL', expectedPackages: 1 }] }
const context = { marker: 'fixture', overseasToken: 'admin', createdResources: { phase4OverseasReturnReceipt: {} },
addStep() {}, assert(_m, name, _r, passed) { assert.ok(passed, name) },
async requestOverseas(method, path, body, _headers, token) {
calls.push({ method, path, body, token })
let data = task
if (path.endsWith('/status')) data = { application: { relabelNo: 'RL-1', status: 'PENDING_PROCESSING' } }
if (path.endsWith('/relabel-sources')) data = [{ positionId: '12', positionVersion: 0, palletCode: 'OLD', cartonMarks: ['BOX'] }]
if (path.endsWith('/relabel-pallet-label')) data = { palletCode: 'NEW' }
if (path.endsWith('/resolve')) {
assert.equal(path, '/api/pda/v1/last-leg/pallet-operations/pallets/resolve')
data = { palletCode: body.code, palletVersion: 1 }
}
if (path.endsWith('/relabel-disassemble')) {
assert.deepEqual(body.actualSource, { positionId: '12', expectedPositionVersion: 0, packages: 1 })
data = { status: 'COMPLETED', operationId: '9', replayed: true }
}
if (path.endsWith('/RL-1/complete')) data = { applicationStatus: 'CUSTOMER_CONFIRM_PENDING' }
if (path.endsWith('/confirm-completion')) data = { application: { relabelNo: 'RL-1', status: 'COMPLETED' } }
if (path.endsWith('/return-orders/RT-1')) data = { order: { status: 'COMPLETED', relabelNo: 'RL-1', relabelStatus: 'COMPLETED' } }
return { http: 200, body: { code: '200', data } }
},
async requestOverseasMultipart(method, path, form, token) {
assert.equal(token, 'pda'); assert.ok(form instanceof FormData)
calls.push({ method, path })
return { http: 200, body: { code: '200', data: { fileId: '7' } } }
} }
await completeReturnRelabel(context, 'merchant', 'pda', 'RL-1', 'RT-1')
assert.equal(context.createdResources.phase4OverseasReturnReceipt.relabelCompleted, true)
const index = suffix => calls.findIndex(c => c.path.endsWith(suffix))
assert.ok(index('/status') < index('/claim'))
assert.ok(index('/return-skus/complete') < index('/relabel-disassemble'))
assert.ok(index('/relabel-disassemble') < index('/photos/files'))
assert.ok(index('/photos/files') < index('/RL-1/complete'))
assert.ok(index('/RL-1/complete') < index('/confirm-completion'))
assert.equal(calls.find(c => c.path.endsWith('/confirm-completion')).token, 'merchant')
})
test('full acceptance registers return receipt as a configured overseas module with an isolated merchant fixture', async () => {
const source = await readFile(acceptanceUrl, 'utf8')
assert.match(source, /CONFIGURED_MODULES[^\n]+phase4-overseas-return-receipt/)
assert.match(source, /\['phase4-overseas-return-receipt', \['merchant-qualification-fixture'\]\]/)
assert.match(source, /requiresMarkerOwnedMerchantFixture[\s\S]+phase4-overseas-return-receipt/)
})
test('return receipt acceptance covers both lookup paths, partial/final receipt and normal empty results', async () => {
const source = await readFile(moduleUrl, 'utf8')
assert.match(source, /PDA_RECEIPTS = '\/api\/pda\/v1\/last-leg\/return-receipts'/)
assert.match(source, /\$\{PDA_RECEIPTS\}\/start/)
assert.match(source, /\$\{PDA_RECEIPTS\}\/sku-candidates/)
assert.match(source, /PDA首次单SKU签收甲3件/)
assert.match(source, /PDA按单SKU完成乙2件签收/)
assert.match(source, /palletCode: cartonA/)
assert.match(source, /最后一批缺少打托照片拒绝且库存不变/)
assert.match(source, /photos\/files/)
assert.match(source, /requestOverseasMultipart\('POST',[^\n]+photoForm, picker\.token\)/)
assert.doesNotMatch(source, /photoForm, \{\}, picker\.token/)
assert.match(source, /不存在SKU不是业务错误/)
assert.match(source, /已完成退货单不再进入待签收流程/)
})
test('return receipt acceptance proves idempotency, over-receipt isolation and exact inventory', async () => {
const source = await readFile(moduleUrl, 'utf8')
assert.match(source, /预报幂等重放复用同一退货单/)
assert.match(source, /签收重放不重复创建签收单、箱或计费批次/)
assert.match(source, /超收被拒绝且不得生成费用批次/)
assert.match(source, /订单5件核平且恰有两个唯一仓储计费批次/)
assert.match(source, /两个实收箱逐一引用对应计费批次且数量守恒/)
})
test('return receipt acceptance includes fee-batch evidence and return packing lifecycle', async () => {
const source = await readFile(moduleUrl, 'utf8')
assert.match(source, /billingExpectation: 'One PENDING_PRICING storage billing batch per successful non-replayed receipt'/)
assert.match(source, /receiptBatchNos/)
assert.match(source, /sourceBatchNo/)
assert.match(source, /PDA查询完成退货单的可合箱库存/)
assert.match(source, /PDA将同客户退货库存合为新箱/)
assert.match(source, /合箱重放复用同一新箱且不重复扣减/)
})
test('HTTP acceptance uses injected clients and never bypasses APIs with database access', async () => {
const source = await readFile(moduleUrl, 'utf8')
assert.doesNotMatch(source, /process\.env|https?:\/\/|\bfetch\s*\(|\bmysql\b|\bSELECT\s|\bINSERT\s|\bUPDATE\s/i)
})
test('receipt scope stops before relabel and outbound; followups are explicit and dependency ordered', async () => {
const source = await readFile(moduleUrl, 'utf8')
assert.doesNotMatch(source, /relabel-candidates|relabel-applications|outbound-orders|outbound-tasks/)
const ordered = normalizeModuleOrder(['phase4-overseas-return-followups'])
assert.ok(ordered.indexOf('phase4-overseas-return-receipt') < ordered.indexOf('phase4-overseas-return-followups'))
assert.ok(!normalizeModuleOrder(['phase4-overseas-return-receipt']).includes('phase4-overseas-return-followups'))
})
test('return followups use SKU quantities, explicit FBA destination and current file and candidate contracts', async () => {
const source = await readFile(new URL('./phase4-overseas-return-followups.mjs', import.meta.url), 'utf8')
assert.match(source, /items: \[\{ skuCode: sku, quantity: 1 \}\]/)
assert.match(source, /destinationFbaWarehouseId: id\(destination.id\)/)
assert.match(source, /destinationFbaWarehouseCode: destination.warehouseCode/)
assert.match(source, /status === 'PENDING_REVIEW'/)
assert.match(source, /skuCodes\?\.includes\?\.\(sku\)/)
assert.match(source, /cartonTotalFileId/)
assert.match(source, /customerId, warehouseId: warehouse.id/)
assert.doesNotMatch(source, /form, \{\}, merchantToken/)
})
test('validation predicate rejects outages, permission failures, success and missing diagnostics', () => {
const response = { http: 200, body: { code: 'A0422' }, requestId: 'request', traceId: 'trace' }
assert.equal(validationFailure(response), true)
for (const code of ['200', 'A0403', 'A0409', 'S0500', 'S0503']) {
assert.equal(validationFailure({ ...response, body: { code } }), false)
}
assert.equal(validationFailure({ ...response, traceId: '' }), false)
})
test('inventory snapshot ignores ordering but detects quantity, status and source drift', () => {
const a = { cartonMark: 'A', totalQuantity: 2, status: 'AVAILABLE', sourceBatchNo: 'B1', sourceReturnNo: 'R1' }
const b = { ...a, cartonMark: 'B', totalQuantity: 3 }
const baseline = inventorySnapshot([a, b])
assert.equal(inventorySnapshot({ records: [b, a] }), baseline)
for (const change of [{ totalQuantity: 1 }, { status: 'MERGED' }, { sourceBatchNo: 'B2' }, { sourceReturnNo: 'R2' }]) {
assert.notEqual(inventorySnapshot([{ ...a, ...change }, b]), baseline)
}
})
test('extended HTTP coverage includes negative cases and persisted no-mutation verification', async () => {
const source = await readFile(moduleUrl, 'utf8')
for (const label of ['重复预报SKU', '负实收数量', '未预报SKU', '部分签收退货单禁止合箱',
'超可用库存', '失败与幂等重放不改变箱数数量状态及来源', '跨页箱号无重复无遗漏', '合箱及异常后客户SKU仍为甲3乙2']) {
assert.ok(source.includes(label), label)
}
})