Files
baiyue-firstleg/scripts/admin-test-env-modules/phase5-root-tax-customs-contract.test.mjs
T

70 lines
4.9 KiB
JavaScript
Raw Normal View History

import test from 'node:test'
import assert from 'node:assert/strict'
import {createHash} from 'node:crypto'
import {readFileSync} from 'node:fs'
import {prepareTaxCustoms,verifyTaxDocumentsAfterCompletion,taxDocumentMatches} from './phase5-root-tax-customs.mjs'
const loading={tenantId:'90',orderId:'100',inboundId:'200',loadingPlanId:'300',containerBookingId:'400',inboundCode:'IN-TAX',containerNo:'TAX-BOX'}
function fixture(){
const calls=[],files={},evidence=[],view={version:0,status:'READY_TO_START',loadingPlanId:'300',containerBookingId:'400',
inbounds:[{inboundId:'200',taxMode:'TAX_REFUND'}],documents:[]}
const wrap=data=>({http:200,body:{code:'200',data:structuredClone(data)}})
const context={marker:'OFFLINE-TAX',token:'platform',createdResources:{},context:{f40TaxMerchantToken:'customer',f40TaxEvidence:evidence},
assert:(_m,name,_r,passed)=>{assert.equal(Boolean(passed),true,name)},apiSuccess:(_m,_n,r)=>r.body?.data,
request:async(method,path,body,token)=>{
calls.push({method,path,body,token})
if(path==='/api/merchant/v1/orders/100')return wrap({summary:{orderId:'100',mainStatus:'COMPLETED',taxMode:'TAX_REFUND'}})
if(method==='GET')return wrap(view)
if(path.endsWith('/start')){view.status='DECLARING';view.documents[0].frozen=true}
if(path.endsWith('/receipt/confirm'))view.documents[0].receiptStatus='CONFIRMED'
if(path.endsWith('/declaration-batches'))return wrap({id:'500',version:0,status:'READY'})
if(path.endsWith('/500/submit'))return wrap({version:1,status:'SUBMITTED'})
if(path.endsWith('/500/results'))return wrap({resultType:'RELEASED',resultStatus:'RESOLVED'})
view.version++;return wrap(view)
},
requestMultipart:async(_method,path,file,token)=>{
calls.push({method:'UPLOAD',path,token})
const kind=path.includes('/receipt/')?'receipt':path.includes('/final/')?'final':'source';files[kind]=file.content
const metadata={fileHash:createHash('sha256').update(file.content).digest('hex'),version:1}
if(kind==='source')view.documents.push({documentId:'600',tenantId:'90',inboundIds:['200'],source:metadata,frozen:false})
else view.documents[0][kind==='final'?'finalDocument':kind]=metadata
if(kind==='final')view.status='COMPLETED'
return wrap(view.documents[0])
},
async requestBinary(_method,path){assert.equal(this.token,'customer');const kind=path.includes('/receipt/')?'receipt':path.includes('/final/')?'final':'source';return {http:200,bytes:files[kind]}},
}
return {context,calls,files,evidence,view}
}
test('tax F40 uses actual upload confirm declaration release and final before post-COMPLETED customer downloads',async()=>{
const f=fixture();const proof=await prepareTaxCustoms(f.context,loading)
assert.equal(proof.status,'CUSTOMS_COMPLETED')
await verifyTaxDocumentsAfterCompletion(f.context,f.evidence,'customer')
assert.equal(proof.status,'ORDER_COMPLETED_CUSTOMS_READ_DOWNLOAD_VERIFIED')
assert.deepEqual(proof.downloads.map(x=>x.kind),['source','receipt','final'])
const paths=f.calls.filter(c=>c.method!=='GET').map(c=>c.path)
let last=-1
for(const suffix of ['/documents/upload?inboundIds=200','/start','/send-to-broker','/receipt/upload?expectedVersion=0',
'/receipt/confirm','/submit-to-broker','/declaration-batches','/500/submit','/500/results','/final/upload?expectedVersion=0']){
const index=paths.findIndex((p,i)=>i>last&&p.endsWith(suffix));assert.ok(index>last,suffix);last=index
}
assert.equal(f.calls.find(c=>c.path.endsWith('/receipt/confirm')).token,'customer')
assert.equal(JSON.stringify(proof).includes('customer'),true) // customerId evidence, never session token field
assert.equal(Object.hasOwn(proof,'token'),false)
assert.doesNotMatch(JSON.stringify(proof),/"(?:bytes|password|accessToken|refreshToken|authorization)"\s*:/i)
})
test('customer tax evidence rejects changed customer, source, plan, file bytes and still-uncompleted order',async()=>{
const f=fixture(),proof=await prepareTaxCustoms(f.context,loading)
for(const mutate of [v=>v.status='DECLARING',v=>v.loadingPlanId='999',v=>v.documents[0].tenantId='91',
v=>v.documents[0].inboundIds=['201'],v=>v.documents[0].finalDocument.fileHash='changed']){
const v=structuredClone(f.view);mutate(v);assert.equal(taxDocumentMatches(v,proof),false)
}
f.files.final=new TextEncoder().encode('incorrect bytes')
await assert.rejects(verifyTaxDocumentsAfterCompletion(f.context,[proof],'customer'),/final字节/)
})
test('buy remains the default; tax path is explicit and final verification occurs after Outbox proof',()=>{
const source=readFileSync(new URL('./phase5-root-fulfillment.mjs',import.meta.url),'utf8')
assert.match(source,/f40TaxMode\|\|'BUY_DECLARATION'/)
assert.match(source,/f40TaxMode==='TAX_REFUND'\) await prepareTaxCustoms/)
assert.match(source,/else await stage\(context,'phase3-customs'\)/)
assert.ok(source.indexOf('await verifyTaxDocumentsAfterCompletion')>source.indexOf('proof.outboxProof ='))
})