125 lines
7.3 KiB
JavaScript
125 lines
7.3 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 } 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')
|
|
|
|
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, /query\(`\$\{RELABEL\}\/inventory-candidates`, \{/)
|
|
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, /no outbound, dispatch or delivery action is invoked/)
|
|
})
|
|
|
|
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: [{customerId:'123',overseasWarehouseId:'42',availablePackages:'4'}], ledger:[],
|
|
source:{orderId:'9',overseasWarehouseId:'42',inboundCode:'IP',availablePackages:'4'},detail:null }
|
|
const proof = {sourceOrderId:'9',customerId:'123',warehouseId:'42',inboundCode:'IP',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')) {
|
|
const changed=(state==='PENDING_REVIEW' && fault==='pending deducts') || (state==='APPROVED' && fault==='approve deducts') || (conflictSeen && fault==='create conflict mutates')
|
|
data={total:'1',records:[{...baseline.balances[0],availablePackages:changed?'3':'4'}]}
|
|
} 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/')))
|
|
})
|
|
}
|