2026-09-11 16:02:10 +08:00
|
|
|
import test from 'node:test'
|
|
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { readFile } from 'node:fs/promises'
|
2026-09-15 16:10:55 +08:00
|
|
|
import { validationFailure, inventorySnapshot } from './phase4-overseas-return-receipt.mjs'
|
|
|
|
|
import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
|
2026-09-11 16:02:10 +08:00
|
|
|
|
|
|
|
|
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('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首次分批签收2件/)
|
|
|
|
|
assert.match(source, /PDA按SKU完成剩余3件签收/)
|
|
|
|
|
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)
|
|
|
|
|
})
|
2026-09-15 16:10:55 +08:00
|
|
|
|
|
|
|
|
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('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)
|
|
|
|
|
}
|
|
|
|
|
})
|