375 lines
24 KiB
JavaScript
375 lines
24 KiB
JavaScript
import { palletReceiptBody } from './return-boundary-scenarios.mjs'
|
|
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, describeAcceptanceScope } from '../admin-test-env-acceptance.mjs'
|
|
import { completeReturnRelabel, outboundRelabelledReturn, verifyReturnOutbound, resolveReturnFollowupWarehouse, run as runFollowups } 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('both pallet receipt batches supply SKU box counts and measured pallet fields', () => {
|
|
for (const [sku, quantity, startedBy] of [['A', 3, 'RETURN_NO'], ['B', 2, 'SKU']]) {
|
|
const body = palletReceiptBody('RT-1', `P-${sku}`, sku, quantity, `KEY-${sku}`, startedBy)
|
|
assert.equal(body.unitType, 'PALLET')
|
|
assert.equal(body.unitCode, `P-${sku}`)
|
|
assert.equal(body.palletCode, body.unitCode)
|
|
assert.equal(body.startedBy, startedBy)
|
|
assert.deepEqual(body.items, [{ skuCode: sku, receivedQuantity: quantity, boxCount: 1 }])
|
|
for (const field of ['length', 'width', 'height', 'weight']) assert.ok(body[field] > 0)
|
|
assert.equal(body.totalBoxCount, undefined, 'server derives the total from SKU box counts')
|
|
}
|
|
})
|
|
|
|
test('relabelled outbound refuses another relabel application before creating an outbound', async () => {
|
|
const calls = []
|
|
const context = { marker: 'fixture', token: 'admin', context: { merchantQualificationFixture: { tenantId: '123' } },
|
|
addStep() {}, assert() {},
|
|
async request(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'))
|
|
})
|
|
|
|
function outboundHarness(fault = '', quantity = 5, multiSku = false, wire = true) {
|
|
const calls = []
|
|
let picked = false, completed = false, duplicate = false, drift = false, reserved = true
|
|
const context = { marker: 'fixture', token: 'admin', context: { merchantQualificationFixture: { tenantId: '123' } },
|
|
createdResources: { phase4OverseasReturnReceipt: {} }, addStep() {},
|
|
assert(_m, name, _r, passed) { assert.ok(passed, name) },
|
|
async request(method, rawPath, body, token) {
|
|
calls.push({ method, path: rawPath, body, token })
|
|
const url = new URL(rawPath, 'https://offline.invalid'), path = url.pathname
|
|
let data
|
|
if (path.endsWith('/pick')) {
|
|
if (!picked) {
|
|
assert.deepEqual(body.cartonMarks, ['BOX']); assert.equal(body.outboundPalletItemId, '9')
|
|
assert.equal(body.inventoryPositionId, null); assert.equal(body.packages, 1); picked = true
|
|
} else if (body.packages !== 1 || body.cartonMarks[0] !== 'BOX' || body.idempotencyKey.endsWith('AGAIN')) {
|
|
if (fault === 'rejected write') drift = true
|
|
return { http: 200, body: { code: fault === 'accept wrong payload' ? '200' : 'A0409' },
|
|
requestId: 'request', traceId: fault === 'missing diagnostics' ? '' : 'trace' }
|
|
} else if (fault === 'replay deducts') duplicate = true
|
|
data = { status: 'IN_PROGRESS' }
|
|
} else if (path.endsWith('/complete')) { completed = true; data = { status: 'COMPLETED' } }
|
|
else if (path.endsWith('/candidates')) { reserved = false; data = [{ returnNo: 'RT-1', cartonMark: 'BOX' }] }
|
|
else if (path.endsWith('/outbound-orders/return')) { reserved = true; data = { taskNo: 'TASK', outboundNo: 'OUT' } }
|
|
else if (path.endsWith('/return-inventory')) {
|
|
assert.equal(url.searchParams.get('customerId'), '123'); assert.equal(url.searchParams.get('overseasWarehouseId'), '42')
|
|
let deducted = picked ? (fault === 'package count deduction' ? 1 : quantity) : 0
|
|
if (duplicate || (completed && fault === 'complete deducts')) deducted += quantity
|
|
const stock = { customerId: fault === 'wrong scope' ? 'other' : '123', overseasWarehouseId: '42', skuCode: 'SKU',
|
|
totalQuantity: quantity - deducted, availableQuantity: reserved ? 0 : quantity - deducted,
|
|
cartonQuantity: picked && fault !== 'carton not deducted' ? 0 : 1, updatedAt: picked ? 'after' : 'before' }
|
|
const unrelated = { ...stock, skuCode: 'OTHER', totalQuantity: drift ? 8 : 9, availableQuantity: 9, cartonQuantity: 1 }
|
|
data = { total: fault === 'truncated page' ? 3 : 2, records: url.searchParams.get('page') === '1' ? [stock]
|
|
: url.searchParams.get('page') === '2' ? [unrelated] : [] }
|
|
} else if (path.endsWith('/movements')) {
|
|
const receipt = { customerId: '123', overseasWarehouseId: '42', skuCode: 'SKU', movementType: 'RECEIPT', referenceNo: 'RECEIPT', totalDelta: quantity, availableDelta: quantity, occurredAt: 'before' }
|
|
const movement = { ...receipt, movementType: 'OUTBOUND', referenceNo: 'OUT', totalDelta: -quantity,
|
|
availableDelta: fault === 'wrong movement quantity' ? -1 : -quantity, occurredAt: 'after' }
|
|
const rows = [receipt, ...(picked && fault !== 'missing movement' ? [movement] : [])]
|
|
if (picked && fault === 'duplicate movement') rows.push(movement)
|
|
data = { records: rows.reverse(), total: rows.length }
|
|
} else if (path.includes('/return-inventory/cartons/')) data = { cartonMark: 'BOX', customerId: '123',
|
|
overseasWarehouseId: '42', sourceReturnNo: 'RT-1', relabelNo: 'RL-1', relabelStatus: 'COMPLETED',
|
|
status: picked ? 'OUTBOUNDED' : reserved ? 'OUTBOUND_PENDING' : 'AVAILABLE', skuTypes: 1, totalQuantity: quantity,
|
|
receivedAt: picked && fault === 'history time drift' ? '2026-10-02T17:54:38.305' : '2026-10-02T17:52:56.794',
|
|
items: [{ skuCode: 'SKU', quantity: picked && fault === 'history erased' ? 0 : quantity }] }
|
|
else if (path.endsWith('/outbound-orders/OUT')) data = { outbound: { outboundNo: 'OUT', outboundType: 'RETURN_OUTBOUND', status: completed ? 'OUTBOUNDED' : 'WAIT_PICK' }, pallets: [{ id: '9', cartonMarks: 'BOX', pickedPackageCount: picked ? 1 : 0 }] }
|
|
else if (path.endsWith('/outbound-tasks/TASK')) data = { task: { taskNo: 'TASK', outboundNo: 'OUT', status: completed ? 'COMPLETED' : picked ? 'IN_PROGRESS' : 'PENDING' },
|
|
items: [{ outboundPalletItemId: '9', inboundCode: 'RT-1', packageCount: 1, pickedPackageCount: picked ? 1 : 0,
|
|
status: picked ? 'PICKED' : 'PENDING', positions: fault === 'missing positions' ? [] : [{ inventoryPositionId: null,
|
|
cartonMark: fault === 'wrong PDA carton' ? 'OTHER' : 'BOX', inboundCode: 'RT-1', availablePackages: picked ? 0 : 1, pickedPackages: picked ? 1 : 0 }] }] }
|
|
else throw new Error(`unexpected request: ${method} ${rawPath}`)
|
|
if (multiSku && path.endsWith('/return-inventory')) {
|
|
data.total += 1
|
|
if (url.searchParams.get('page') === '2') data.records.push({ customerId: '123', overseasWarehouseId: '42',
|
|
skuCode: 'SKU-B', totalQuantity: picked ? 0 : 2, availableQuantity: reserved ? 0 : picked ? 0 : 2, cartonQuantity: picked ? 0 : 1 })
|
|
}
|
|
if (multiSku && path.includes('/return-inventory/cartons/')) {
|
|
data.skuTypes = 2; data.totalQuantity += 2; data.items.push({ skuCode: 'SKU-B', quantity: 2 })
|
|
}
|
|
if (multiSku && picked && path.endsWith('/movements') && fault !== 'second SKU movement missing') {
|
|
data.records.push({ customerId: '123', overseasWarehouseId: '42', skuCode: 'SKU-B', movementType: 'OUTBOUND',
|
|
referenceNo: 'OUT', totalDelta: -2, availableDelta: -2, occurredAt: 'after' }); data.total += 1
|
|
}
|
|
if (wire) {
|
|
if ('total' in data) data.total = String(data.total)
|
|
if (path.endsWith('/return-inventory')) for (const row of data.records) {
|
|
for (const field of ['totalQuantity', 'availableQuantity', 'cartonQuantity']) row[field] = String(row[field])
|
|
}
|
|
if (path.endsWith('/movements')) for (const row of data.records) {
|
|
row.totalDelta = String(row.totalDelta); row.availableDelta = String(row.availableDelta)
|
|
}
|
|
if (path.includes('/return-inventory/cartons/')) {
|
|
data.totalQuantity = String(data.totalQuantity)
|
|
for (const row of data.items) row.quantity = String(row.quantity)
|
|
}
|
|
if (path.endsWith('/outbound-tasks/TASK')) for (const item of data.items) for (const position of item.positions) {
|
|
delete position.inventoryPositionId
|
|
position.availablePackages = String(position.availablePackages); position.pickedPackages = String(position.pickedPackages)
|
|
}
|
|
}
|
|
if (path.endsWith('/return-inventory') && fault && typeof fault === 'object' && data.records.length) {
|
|
data.records[0][fault.field] = fault.value
|
|
}
|
|
if (path.endsWith('/outbound-tasks/TASK') && fault === 'nonempty position') data.items[0].positions[0].inventoryPositionId = '9007199254740993'
|
|
if (path.endsWith('/return-inventory') && picked && fault === 'identifier drift') {
|
|
for (const row of data.records) row.customerId = 123
|
|
}
|
|
return { http: 200, body: { code: '200', data } }
|
|
} }
|
|
return { context, calls }
|
|
}
|
|
const outboundInput = { taskNo: 'TASK', outboundNo: 'OUT', returnNo: 'RT-1', warehouseId: '42', cartonMark: 'BOX', action: 'OUTBOUND' }
|
|
|
|
test('return outbound proves one box five units accounting history replay and completion using current DTOs', async () => {
|
|
const { context, calls } = outboundHarness()
|
|
await verifyReturnOutbound(context, 'merchant', 'pda', outboundInput)
|
|
assert.equal(calls.filter(c => c.path.endsWith('/pick')).length, 5)
|
|
assert.equal(calls.filter(c => c.path.endsWith('/complete')).length, 2)
|
|
assert.ok(calls.some(c => c.path.includes('page=2')))
|
|
assert.ok(calls.filter(c => c.method === 'POST').every(c => c.token === 'pda'))
|
|
})
|
|
|
|
for (const fault of ['package count deduction', 'carton not deducted', 'missing movement', 'duplicate movement',
|
|
'wrong movement quantity', 'history erased', 'replay deducts', 'rejected write', 'accept wrong payload',
|
|
'missing diagnostics', 'complete deducts', 'wrong scope', 'truncated page', 'missing positions', 'wrong PDA carton',
|
|
'history time drift', 'nonempty position', 'identifier drift']) {
|
|
test(`return outbound fails closed on ${fault}`, async () => {
|
|
const { context, calls } = outboundHarness(fault)
|
|
await assert.rejects(verifyReturnOutbound(context, 'merchant', 'pda', outboundInput))
|
|
if (['wrong scope', 'truncated page', 'missing positions', 'wrong PDA carton', 'nonempty position'].includes(fault)) assert.ok(calls.every(c => c.method === 'GET'))
|
|
if (fault !== 'complete deducts') assert.ok(!calls.some(c => c.path.endsWith('/complete')))
|
|
})
|
|
}
|
|
|
|
test('explicit null positions and numeric counters remain accepted', async () => {
|
|
await verifyReturnOutbound(outboundHarness('', 5, false, false).context, 'merchant', 'pda', outboundInput)
|
|
})
|
|
|
|
for (const field of ['totalQuantity', 'availableQuantity', 'cartonQuantity']) {
|
|
test(`stock ${field} rejects malformed or unsafe integer values before picking`, async () => {
|
|
for (const value of ['', ' ', '1.5', '1e2', '0x10', '-1', '01', '9007199254740993', null, undefined, true, 1.5, -1, NaN, Infinity, 9007199254740992]) {
|
|
const { context, calls } = outboundHarness({ field, value })
|
|
await assert.rejects(verifyReturnOutbound(context, 'merchant', 'pda', outboundInput), /必须为非负安全整数/)
|
|
assert.ok(calls.every(call => call.method === 'GET'))
|
|
}
|
|
})
|
|
}
|
|
|
|
test('mixed SKU carton verifies each SKU quantity and one carton deduction per SKU', async () => {
|
|
await verifyReturnOutbound(outboundHarness('', 5, true).context, 'merchant', 'pda', outboundInput)
|
|
await assert.rejects(verifyReturnOutbound(outboundHarness('second SKU movement missing', 5, true).context,
|
|
'merchant', 'pda', outboundInput), /每个出库SKU只有一条/)
|
|
})
|
|
|
|
test('relabelled outbound shares accounting and strict PDA scan checks', async () => {
|
|
const { context } = outboundHarness('', 1)
|
|
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', skuCode: 'SKU-1', operationType: 'RETURN_BULK_SKU_LABEL', expectedPackages: 1 }] }
|
|
const context = { marker: 'fixture', token: 'admin', createdResources: { phase4OverseasReturnReceipt: {} },
|
|
addStep() {}, assert(_m, name, _r, passed) { assert.ok(passed, name) },
|
|
async request(method,path,body,token,_headers) {
|
|
calls.push({ method, path, body, token })
|
|
let data = task
|
|
if (path.endsWith('/status')) data = { application: { relabelNo: 'RL-1', status: 'PENDING_PROCESSING' } }
|
|
if (path.endsWith('/return-skus/complete')) {
|
|
assert.equal(body.cartonMark, 'BOX')
|
|
assert.equal(body.skuCode, 'SKU-1')
|
|
}
|
|
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 requestMultipart(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')
|
|
})
|
|
|
|
for (const missing of ['cartonMark', 'skuCode', 'expectedPackages']) {
|
|
test(`return relabel blocks incomplete task before quantity write: ${missing}`, async () => {
|
|
const calls = []
|
|
const item = { itemId: '1', cartonMark: 'BOX', skuCode: 'SKU-1', expectedPackages: 1, operationType: 'RETURN_BULK_SKU_LABEL' }
|
|
delete item[missing]
|
|
const context = { marker: 'batch', token: 'admin', addStep() {}, assert() {},
|
|
async request(method, path) {
|
|
calls.push(path)
|
|
const data = path.endsWith('/status')
|
|
? { application: { relabelNo: 'RL-1', status: 'PENDING_PROCESSING' } }
|
|
: { relabelNo: 'RL-1', version: 1, items: [item] }
|
|
return { http: 200, body: { code: '200', data } }
|
|
} }
|
|
await assert.rejects(completeReturnRelabel(context, 'merchant', 'pda', 'RL-1', 'RT-1'), /真实箱唛和SKU/)
|
|
assert.equal(calls.length, 2)
|
|
assert.ok(calls.every(path => !path.endsWith('/return-skus/complete')))
|
|
})
|
|
}
|
|
|
|
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, /palletReceiptBody\(returnNo, palletCodeA, skuA, 3,/)
|
|
assert.match(source, /const cartonA = id\(first.data\?\.cartonMark\)/)
|
|
assert.match(source, /const cartonB = id\(second.data\?\.cartonMark\)/)
|
|
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-only scope remains focused while default full plan includes ordered return followups', 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'))
|
|
const defaults = describeAcceptanceScope().defaultModules
|
|
assert.ok(defaults.includes('phase4-overseas-return-followups'))
|
|
assert.ok(defaults.indexOf('phase4-overseas-return-receipt') < defaults.indexOf('phase4-overseas-return-followups'))
|
|
})
|
|
|
|
test('return followups read only the actual receipt warehouse and reject drift', async () => {
|
|
for (const patch of [{}, { id: '202' }, { status: 'INACTIVE' }, { warehouseType: 'DOMESTIC' }]) {
|
|
const calls = [], checks = []
|
|
const context = { createdResources: { phase4OverseasReturnReceipt: { warehouseId: '201' } },
|
|
addStep(module, name, response, passed) { checks.push(passed) },
|
|
async request(method, path) {
|
|
calls.push({ method, path })
|
|
return { http: 200, body: { code: '200', data: { warehouse: { id: '201', status: 'ACTIVE', warehouseType: 'OVERSEAS', ...patch } } } }
|
|
} }
|
|
const warehouse = await resolveReturnFollowupWarehouse(context)
|
|
assert.equal(Boolean(warehouse), Object.keys(patch).length === 0)
|
|
assert.equal(calls.length, 1)
|
|
assert.equal(calls[0].path, '/api/platform/v1/warehouse-admin/201?tenantId=0')
|
|
}
|
|
await assert.rejects(resolveReturnFollowupWarehouse({ createdResources: {} }), /实际收货仓/)
|
|
})
|
|
|
|
test('return followups stop before login when main chain failed or customer changed', async () => {
|
|
for (const patch of [{ mainChainStatus: 'FAILED' }, { customerId: '102' }, { marker: 'OLD' }]) {
|
|
const checks = []
|
|
await runFollowups({ marker: 'CURRENT', context: { merchantQualificationFixture: { tenantId: '101' } },
|
|
createdResources: { phase4OverseasReturnReceipt: { marker: 'CURRENT', returnNo: 'TH202610220001',
|
|
mainChainStatus: 'READY_FOR_FOLLOWUPS', customerId: '101', ...patch } },
|
|
addStep(module, name, response, passed) { checks.push(passed) },
|
|
request() { throw new Error('must not send HTTP') } })
|
|
assert.deepEqual(checks, [false])
|
|
}
|
|
})
|
|
|
|
test('fixed return main chain excludes customer provisioning and write transport retries', async () => {
|
|
const source = await readFile(moduleUrl, 'utf8')
|
|
assert.match(source, /context.options.returnMainChainOnly \|\| context.options.fixedCustomerId/)
|
|
assert.match(source, /addCoverageGap\(MODULE, 'return-isolation-boundaries'/)
|
|
assert.match(source, /method === 'GET' && attempt < 3/)
|
|
assert.match(source, /mainChainStatus = context.steps.slice\(start\).every/)
|
|
for (const url of [moduleUrl, new URL('./phase4-overseas-return-followups.mjs', import.meta.url)]) {
|
|
assert.match(await readFile(url, 'utf8'), /finally \{\s*const logout = await context.request\('POST', '\/api\/auth\/logout'/)
|
|
}
|
|
})
|
|
|
|
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)
|
|
}
|
|
})
|