70 lines
4.9 KiB
JavaScript
70 lines
4.9 KiB
JavaScript
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 ='))
|
||
|
|
})
|