104 lines
6.3 KiB
JavaScript
104 lines
6.3 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { verifyCargoDiscardFulfillment } from './cargo-discard-fulfillment.mjs'
|
|
|
|
function harness(fault = '') {
|
|
const calls = [], persisted = []
|
|
let discarded = false, confirmed = false, repeats = 0, invalidPhotoRejected = false
|
|
const proof = { approvedNo: 'CD202610220001', customerId: '101', warehouseId: '201', inboundCode: 'RK-TEST', skuCode: 'SKU-TEST',
|
|
status: 'APPROVAL_HTTP_VERIFIED_CARTON_SQL_PENDING' }
|
|
const task = { discardNo: proof.approvedNo, customerId: '101', overseasWarehouseId: '201', palletNo: 'PALLET-TEST',
|
|
status: 'PENDING', skus: [{ itemId: '301', skuCode: proof.skuCode, quantityUnit: '箱', plannedPackages: 1 }], cartonMarks: ['BOX-TEST'] }
|
|
const context = { marker: 'DISCARD-TEST', context: { merchantQualificationFixture: { tenantId: '101' } },
|
|
createdResources: { phase4OverseasClientServices: { marker: 'DISCARD-TEST' } },
|
|
assert(_module, name, _response, passed) { assert.ok(passed, name) },
|
|
async request(method, path, body, token) {
|
|
calls.push({ method, path, body, token })
|
|
assert.equal(persisted.at(-1)?.requests.at(-1)?.status, 'SENDING')
|
|
let data
|
|
if (method === 'GET') data = [{ ...task, customerId: fault === 'wrong customer' ? '999' : '101' }]
|
|
else if (path.endsWith('/resolve')) data = task
|
|
else if (path.endsWith('/confirm-completion')) { confirmed = true; data = {} }
|
|
else if (path.endsWith('/confirm')) {
|
|
if (body.photoFileIds.length === 2) {
|
|
invalidPhotoRejected = true
|
|
return { http: 200, requestId: 'request-test', traceId: fault === 'photo missing trace' ? null : 'trace-test',
|
|
body: { code: fault === 'photo accepted' ? '200' : fault === 'photo wrong code' ? 'A0400' : 'A0409', data: null } }
|
|
}
|
|
if (fault === 'unknown confirm') throw new Error('network unknown')
|
|
if (discarded) repeats++
|
|
discarded = true
|
|
data = { ...task, status: 'COMPLETED', skus: [{ ...task.skus[0], actualPackages: 1 }] }
|
|
} else throw new Error(`unexpected ${path}`)
|
|
return { http: 200, body: { code: '200', data } }
|
|
},
|
|
async requestMultipart(method, path, form, token) {
|
|
calls.push({ method, path, token })
|
|
assert.ok(form.get('file'))
|
|
assert.equal(persisted.at(-1)?.requests.at(-1)?.status, 'SENDING')
|
|
return { http: 200, body: { code: '200', data: { fileId: '401', palletNo: task.palletNo } } }
|
|
},
|
|
}
|
|
const inputs = { proof, merchantToken: 'client-secret', pdaToken: 'pda-secret',
|
|
async checkpoint() {
|
|
if (fault === 'disk') throw new Error('disk full')
|
|
persisted.push(structuredClone(proof.fulfillment))
|
|
},
|
|
async snapshot() {
|
|
const quantity = discarded ? fault === 'no deduction' ? 4 : fault === 'replay deduction' && repeats ? 2 : 3 : 4
|
|
const ledger = discarded ? [{ businessOrderNo: proof.approvedNo, businessType: 'DISCARD', deltaPackages: -1,
|
|
id: '501', customerId: proof.customerId, overseasWarehouseId: proof.warehouseId, palletNo: task.palletNo,
|
|
beforePackages: 4, afterPackages: 3,
|
|
skuCode: proof.skuCode, inboundCode: proof.inboundCode }] : []
|
|
if (discarded && fault === 'duplicate ledger') ledger.push({ ...ledger[0] })
|
|
if (discarded && fault === 'ledger customer') ledger[0].customerId = '999'
|
|
if (discarded && fault === 'ledger warehouse') ledger[0].overseasWarehouseId = '999'
|
|
if (discarded && fault === 'ledger pallet') ledger[0].palletNo = 'OTHER-PALLET'
|
|
if (discarded && fault === 'ledger identity') delete ledger[0].id
|
|
if (discarded && fault === 'ledger arithmetic') ledger[0].afterPackages = 4
|
|
return { balances: [{ inboundCode: proof.inboundCode, skuCode: proof.skuCode, totalPackages: quantity, availablePackages: quantity }],
|
|
ledger, detail: { summary: { discardNo: proof.approvedNo, customerId: proof.customerId, overseasWarehouseId: proof.warehouseId,
|
|
status: confirmed ? 'COMPLETED' : discarded ? 'PDA_COMPLETED' : 'APPROVED', totalPackages: 1,
|
|
actualPackages: discarded || invalidPhotoRejected && fault === 'photo drift' ? 1 : null, customerConfirmedAt: confirmed ? '2026-10-22T12:00:00' : null } } }
|
|
} }
|
|
return { context, inputs, calls, persisted }
|
|
}
|
|
|
|
test('discard executes same application through PDA and customer completion with zero-drift replays', async () => {
|
|
const h = harness()
|
|
const result = await verifyCargoDiscardFulfillment(h.context, h.inputs)
|
|
assert.equal(result.status, 'HTTP_VERIFIED')
|
|
assert.equal(h.inputs.proof.status, 'COMPLETED_HTTP_VERIFIED_CARTON_SQL_PENDING')
|
|
assert.equal(h.calls.filter(call => call.path.endsWith('/confirm')).length, 4)
|
|
assert.equal(h.calls.filter(call => call.path.endsWith('/confirm-completion')).length, 2)
|
|
assert.equal(h.calls.filter(call => call.path.endsWith('/photos/files')).length, 1)
|
|
assert.ok(!JSON.stringify(h.persisted).includes('client-secret'))
|
|
assert.ok(!JSON.stringify(h.persisted).includes('pda-secret'))
|
|
await assert.rejects(verifyCargoDiscardFulfillment(h.context, h.inputs), /已批准申请|不自动重跑/)
|
|
})
|
|
|
|
for (const fault of ['wrong customer', 'no deduction', 'duplicate ledger', 'replay deduction', 'unknown confirm', 'disk',
|
|
'ledger customer', 'ledger warehouse', 'ledger pallet', 'ledger identity', 'ledger arithmetic',
|
|
'photo accepted', 'photo wrong code', 'photo missing trace', 'photo drift']) {
|
|
test(`discard stops on ${fault}`, async () => {
|
|
const h = harness(fault)
|
|
await assert.rejects(verifyCargoDiscardFulfillment(h.context, h.inputs))
|
|
assert.equal(h.inputs.proof.fulfillment.status, 'FAILED')
|
|
assert.equal(h.calls.some(call => call.path.endsWith('/confirm-completion')), false)
|
|
if (fault === 'wrong customer') assert.equal(h.calls.length, 1)
|
|
if (fault === 'disk') assert.equal(h.calls.length, 0)
|
|
if (fault === 'unknown confirm') {
|
|
assert.equal(h.calls.filter(call => call.path.endsWith('/confirm')).length, 2)
|
|
assert.equal(h.inputs.proof.fulfillment.requests.at(-1).status, 'UNKNOWN_RESULT')
|
|
}
|
|
})
|
|
}
|
|
|
|
test('foreign batch or customer fails before HTTP or checkpoint', async () => {
|
|
const h = harness()
|
|
h.inputs.proof.customerId = '999'
|
|
await assert.rejects(verifyCargoDiscardFulfillment(h.context, h.inputs), /本轮已批准申请/)
|
|
assert.equal(h.calls.length, 0)
|
|
assert.equal(h.persisted.length, 0)
|
|
})
|