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 } 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 ['', '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, /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, /不自动删除业务数据/) 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: [{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/'))) }) }