Files
baiyue-firstleg/scripts/admin-test-env-modules/phase4-overseas-client-services-contract.test.mjs
T

201 lines
13 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { verifyDiscardApproval, submitClientTransfer, currentOrders, discardSnapshot } from './phase4-overseas-client-services.mjs'
import { parseArguments } from '../admin-test-env-acceptance.mjs'
const source = await readFile(fileURLToPath(new URL('./phase4-overseas-client-services.mjs', import.meta.url)), 'utf8')
for (const fault of ['', 'foreign inbound', 'mixed aggregate']) test(`discard resolves source balance rather than inventing inbound on aggregate: ${fault || 'native grouped list'}`, async () => {
const aggregate = { id: '901', customerId: '101', overseasWarehouseId: '201', skuCode: 'SKU', totalPackages: '5', availablePackages: '5',
holdPackages: '0', allocatedPackages: '0', pickedPackages: '0', inTransitPackages: '0', receivingPackages: '0', procurementPackages: '0' }
const proof = { customerId: '101', warehouseId: '201', skuCode: 'SKU', inboundCode: 'RK', sourceOrderId: '301' }
const calls = []
const context = { token: 'admin', assert(_m, name, _r, passed) { assert.ok(passed, name) },
apiSuccess(_m, _n, r) { assert.equal(r.body.code, '200'); return r.body.data },
async request(method, path) {
assert.equal(method, 'GET'); calls.push(path)
const pathname = new URL(path, 'https://offline.invalid').pathname
const data = pathname.endsWith('/balances') ? { total: '1', records: [aggregate] }
: pathname.endsWith('/ledger') ? { total: '0', records: [] }
: pathname.endsWith('/balances/901') ? { ...aggregate, inboundCode: fault === 'foreign inbound' ? 'OTHER' : 'RK', totalPackages: fault === 'mixed aggregate' ? '4' : '5' }
: [{ orderId: '301', inboundCode: 'RK' }]
return { http: 200, body: { code: '200', data } }
} }
if (fault) await assert.rejects(discardSnapshot(context, 'merchant', proof), /来源余额身份与聚合数量一致/)
else {
const result = await discardSnapshot(context, 'merchant', proof)
assert.equal(result.balances[0].inboundCode, 'RK')
assert.equal(aggregate.inboundCode, undefined)
assert.ok(calls.some(path => path.includes('/balances/901?customerId=101')))
}
})
for (const fault of ['', 'duplicate-page', 'missing-total']) test(`current customer orders preserve dropship and fully page before selection: ${fault || 'page two'}`, async () => {
const calls = [], row = { id: 'DROP', containerNo: 'CURRENT', orderType: 'DROPSHIP' }
const context = { createdResources: { phase4OverseasPda: { completedContainers: ['CURRENT'], orderId: 'DROP' } },
assert(_m, label, _r, passed) { assert.ok(passed, label) }, apiSuccess(_m, _n, response) { return response.body.data },
async request(method, path) {
calls.push(path); assert.equal(method, 'GET')
const url = new URL(path, 'https://offline.invalid'); assert.equal(url.searchParams.has('sailingStage'), false)
const data = url.pathname.endsWith('/DROP') ? { order: row, cargo: [] }
: { total: fault === 'missing-total' ? null : 201, records: url.searchParams.get('page') === '1' || fault === 'duplicate-page'
? Array.from({ length: 200 }, (_, n) => ({ id: `OTHER-${n}`, containerNo: 'HISTORY' })) : [row] }
return { http: 200, body: { code: '200', data } }
} }
if (fault) await assert.rejects(currentOrders(context, 'merchant'), /分页/)
else assert.deepEqual((await currentOrders(context, 'merchant')).map(item => item.row.id), ['DROP'])
assert.ok(calls.every(path => !path.endsWith('/OTHER')))
})
for (const fault of ['', 'network', 'server', 'rejected', 'wrong order', 'wrong status', 'throws']) {
test(`transfer submit sends once and stops on uncertain result: ${fault || 'success'}`, async () => {
const calls = []
const context = { marker: 'batch', createdResources: { phase4OverseasClientServices: {} },
apiSuccess(_m, _n, response) { return response?.body?.data },
async request(method, path, body, token) {
calls.push({ method, path, body, token })
if (fault === 'throws') throw new Error('connection closed')
return { http: fault === 'network' ? 0 : fault === 'server' ? 503 : 200,
requestId: 'request', traceId: 'trace', body: { code: fault === 'rejected' ? 'A0409' : '200',
data: { transferNo: fault === 'wrong order' ? 'OTHER' : 'WT-1',
status: fault === 'wrong status' ? 'DRAFT' : 'PENDING_REVIEW' } } }
} }
if (fault) await assert.rejects(submitClientTransfer(context, 'merchant', 'WT-1'))
else assert.equal((await submitClientTransfer(context, 'merchant', 'WT-1')).status, 'PENDING_REVIEW')
assert.equal(calls.length, 1)
assert.equal(calls[0].method, 'POST')
assert.equal(calls[0].path, '/api/merchant/v1/last-leg/warehouse-transfers/WT-1/submit')
const proof = context.createdResources.phase4OverseasClientServices.transferSubmission
assert.equal(proof.status, !fault ? 'PENDING_REVIEW' : ['network', 'server', 'throws'].includes(fault)
? 'UNKNOWN_RESULT' : fault === 'rejected' ? 'REJECTED' : 'UNEXPECTED_RESULT')
assert.equal(JSON.stringify(proof).includes('merchant'), false)
await assert.rejects(submitClientTransfer(context, 'merchant', 'WT-1'), /禁止重复提交/)
assert.equal(calls.length, 1)
})
}
test('discard proof follows reserved approval and keeps actual PDA confirmation separate', () => {
assert.match(source, /Approval retains the existing reservation without deducting stock/)
assert.match(source, /Actual PDA confirmation deducts the confirmed quantity/)
assert.doesNotMatch(source, /require approval-transaction deduction/)
assert.match(source, /completed-confirm recovery\/replay/)
})
test('client services module uses only stable merchant self-service paths', () => {
for (const path of [
'/api/merchant/v1/last-leg/orders',
'/api/merchant/v1/last-leg/relabel-applications',
'/api/merchant/v1/last-leg/warehouse-transfers',
'/api/merchant/v1/last-leg/cargo-discards',
'inventory-candidates',
]) assert.ok(source.includes(path), `missing ${path}`)
for (const forbidden of ['/api/platform/v1/last-leg/dispatches', '/outbound', '/delivery', '/dispatch', '/depart']) {
assert.equal(source.includes(forbidden), false, `must exclude ${forbidden}`)
}
})
test('module never borrows historical order or inventory candidates', () => {
assert.match(source, /fixture\?\.marker !== context\.marker/)
assert.match(source, /pda\?\.marker !== context\.marker/)
assert.match(source, /pda\?\.finalStatus !== 'COMPLETED'/)
assert.match(source, /completedContainers/)
assert.match(source, /markerOrderIds\.has\(id\(value\?\.orderId\)\)/)
assert.match(source, /禁止借历史一件代发库存/)
assert.doesNotMatch(source, /candidates\[0\]/)
})
test('relabel transfer and discard cover create replay conflict list detail and legal initial states', () => {
assert.match(source, /deliveryMethod: 'TRUCK', printShippingLabel: false/)
assert.match(source, /chargeAmount: 0\.01/)
assert.match(source, /cartonMark:/)
assert.match(source, /fbaCartonMark:/)
for (const field of [
'operationWarehouseId:', 'sourceInventoryBalanceId:', 'sourceInventoryPositionId:', 'sourceInboundOrderId:',
'destinationFbaWarehouseCode:',
]) assert.match(source, new RegExp(field))
assert.match(source, /await readCurrentBoxedInventory\(context, token,/)
assert.match(source, /cartonMark: carton.cartonMark/)
assert.match(source, /cartonIds: \[id\(carton.cartonId\)\]/)
for (const token of [
'客户创建贴标申请', '客户贴标申请幂等重放', '贴标同幂等键异载荷返回A0409',
'客户查询自身贴标申请列表', '客户查询自身贴标申请详情', 'PENDING_REVIEW',
'客户创建仓储转运申请', '客户仓储转运申请幂等重放', '转运同幂等键异载荷返回A0409',
'客户查询自身仓储转运列表', '客户查询自身仓储转运详情',
'客户创建货物废弃申请', '客户货物废弃申请幂等重放',
'客户查询自身货物废弃列表', '客户查询自身货物废弃详情', 'PENDING_REVIEW',
]) assert.ok(source.includes(token), `missing ${token}`)
})
test('customer isolation is exercised with merchant token, forged customerId and platform denial', () => {
assert.match(source, /merchantQualificationFixture/)
assert.match(source, /accessToken/)
assert.match(source, /customerId: 0/)
assert.match(source, /平台统一Token不能冒充客户访问/)
assert.match(source, /response\?\.http === 403.*codeOf\(response\) === 'A0403'/s)
})
test('module records marker-owned resources without forbidden fulfillment cleanup', () => {
assert.match(source, /createdResources\.phase4OverseasClientServices/)
assert.match(source, /relabelNo = relabelNo/)
assert.match(source, /transferNo = transferNo/)
assert.match(source, /discardNo = discardNo/)
assert.match(source, /不自动删除业务数据/)
assert.match(source, /transfer: transferDone \? context\.createdResources\.phase4OverseasClientServices\.transferFulfillment\?\.status \?\? null : null/)
assert.match(source, /await verifyCargoDiscardFulfillment\(context/)
assert.match(source, /snapshot: \(\) => discardSnapshot\(context, token, proof, proof.approvedNo\)/)
})
test('focused client services keeps the marker-owned merchant fixture when reusable credentials exist', () => {
const parsed = parseArguments([
'--modules', 'phase4-overseas-client-services',
'--merchant-username', 'existing-merchant',
'--merchant-password', 'existing-password',
])
const fixture = parsed.modules.indexOf('merchant-qualification-fixture')
const clientServices = parsed.modules.indexOf('phase4-overseas-client-services')
assert.ok(fixture >= 0 && fixture < clientServices)
})
test('discard approval retains reservation and keeps actual confirmation evidence pending', () => {
assert.match(source, /批准只保留锁箱且库存数量流水不变/)
assert.match(source, /废弃拒绝释放原箱且全部库存数量流水不变/)
assert.match(source, /APPROVAL_HTTP_VERIFIED_CARTON_SQL_PENDING/)
assert.doesNotMatch(source, /INSERT INTO|UPDATE ll_|fleet-trip|\/api\/pda\//)
})
for (const fault of ['', 'pending deducts', 'reject does not release', 'approve deducts', 'create conflict mutates']) {
test(`discard approval stock boundaries: ${fault || 'formal approval contract'}`, async () => {
let state = 'PENDING_REVIEW', approved = false, conflictSeen = false
const baseline = { balances: [{id:'777',customerId:'123',overseasWarehouseId:'42',skuCode:'SKU',inboundCode:'IP',totalPackages:'4',availablePackages:'4',
holdPackages:'0',allocatedPackages:'0',pickedPackages:'0',inTransitPackages:'0',receivingPackages:'0',procurementPackages:'0'}], ledger:[],
source:{orderId:'9',overseasWarehouseId:'42',inboundCode:'IP',availablePackages:'4'},detail:null }
const proof = {sourceOrderId:'9',customerId:'123',warehouseId:'42',inboundCode:'IP',skuCode:'SKU',snapshots:{before:baseline}}
const calls=[]
const context={marker:'fixture',token:'admin',addStep(){},
assert(_m,name,_r,passed){ assert.ok(passed,name); if(name==='重复批准不会提前扣库存或新增历史') throw new Error('CHECKPOINT_PASSED') },
apiSuccess(_m,_n,r){ assert.equal(r.body.code,'200');return r.body.data },
async request(method,raw,body){
calls.push({method,raw,body})
const path=new URL(raw,'https://offline.invalid').pathname
let data
if(method==='POST' && path.endsWith('/review')) {state=body.decision==='REJECT'?'REJECTED':'APPROVED';data={}}
else if(method==='POST' && path.endsWith('/cargo-discards')) {
if(body.remark.endsWith('-DIFFERENT')) {conflictSeen=true;return {http:200,body:{code:'A0409'},requestId:'req',traceId:'trace'}}
approved=true;state='PENDING_REVIEW';data={summary:{discardNo:'NEW'}}
} else if(path.endsWith('/balances') || path.endsWith('/balances/777')) {
const changed=(state==='PENDING_REVIEW' && fault==='pending deducts') || (state==='APPROVED' && fault==='approve deducts') || (conflictSeen && fault==='create conflict mutates')
const balance={...baseline.balances[0],availablePackages:changed?'3':'4'}
data=path.endsWith('/777')?balance:{total:'1',records:[balance]}
} else if(path.endsWith('/ledger')) data={total:'0',records:[]}
else if(path.endsWith('/inventory-candidates')) data=[{...baseline.source,availablePackages:state==='REJECTED' && fault!=='reject does not release'?'4':'3'}]
else data={summary:{discardNo:approved?'NEW':'OLD',status:state}}
return {http:200,body:{code:'200',data}}
}}
await assert.rejects(verifyDiscardApproval(context,'merchant',{idempotencyKey:'key',remark:'fixture'},'OLD',proof),
fault ? /仅锁定|释放原箱|只保留锁箱|异载荷拒绝后/ : /CHECKPOINT_PASSED/)
assert.ok(!calls.some(call=>call.raw.includes('/pda/')))
})
}