2026-10-03 09:02:25 +08:00
|
|
|
import { createHash } from 'node:crypto'
|
|
|
|
|
import { isDeepStrictEqual as equal } from 'node:util'
|
|
|
|
|
|
|
|
|
|
export const feeVoidScopeKeys=['marker','customerId','sourceFeeId','sourceVersion','sourceType']
|
|
|
|
|
export const feeSourceTypes=['OVERSEAS_INBOUND_FEE','OVERSEAS_PROCUREMENT_FEE','OVERSEAS_TRUCK_FEE','OVERSEAS_LOOSEBOARD_FEE',
|
|
|
|
|
'OVERSEAS_WAREHOUSE_FEE','OVERSEAS_CONTAINER_FEE','OVERSEAS_ORDER_FEE','OVERSEAS_DELIVERY_FEE','OVERSEAS_RELABEL_FEE']
|
|
|
|
|
const hash=value=>'sha256:'+createHash('sha256').update(value,'utf8').digest('hex')
|
|
|
|
|
const without=(row,keys)=>Object.fromEntries(Object.entries(row).filter(([key])=>!keys.includes(key)))
|
|
|
|
|
const date=value=>Date.parse(String(value).replace(' ','T').replace(/Z$/,'')+'Z')
|
|
|
|
|
|
|
|
|
|
export function assertTestFeeVoidFaultProof(proof,scope) {
|
|
|
|
|
const need=(passed,code)=>{if(!passed)throw Error('Invalid fee void fault proof: '+code)}
|
|
|
|
|
need(proof?.readOnly===true && feeVoidScopeKeys.every(key=>typeof scope[key]==='string'&&proof[key]===scope[key])
|
|
|
|
|
&& Object.keys(scope).length===feeVoidScopeKeys.length && feeSourceTypes.includes(scope.sourceType)
|
|
|
|
|
&& /^[A-Za-z0-9_-]{1,96}$/.test(scope.marker) && ['customerId','sourceFeeId','sourceVersion'].every(key=>/^[1-9][0-9]{0,19}$/.test(scope[key]))
|
|
|
|
|
&& Number(scope.sourceVersion)<=2147483647, 'SCOPE')
|
|
|
|
|
const event=(row,action)=>{
|
|
|
|
|
need(row && /^[1-9][0-9]{0,19}$/.test(row.entryId) && /^[0-9a-fA-F-]{36}$/.test(row.eventId), 'EVENT_IDENTITY')
|
|
|
|
|
const env=row.envelope,p=env?.payload
|
|
|
|
|
need(row.eventType==='BusinessFeeSourceChanged' && row.customerId===scope.customerId && row.aggregateId===scope.sourceFeeId
|
|
|
|
|
&& row.aggregateType===scope.sourceType && row.eventKey===`BusinessFeeSourceChanged:LASTLEG:${scope.sourceType}:${scope.sourceFeeId}:${scope.sourceVersion}:${action}`
|
|
|
|
|
&& env?.eventId===row.eventId && env.eventType===row.eventType && env.tenantId===scope.customerId && Number(env.eventVersion)===1
|
|
|
|
|
&& env.aggregateType===scope.sourceType && env.aggregateId===scope.sourceFeeId && String(env.aggregateVersion)===scope.sourceVersion
|
|
|
|
|
&& p?.action===action && p.sourceSystem==='LASTLEG' && p.customerId===scope.customerId && p.sourceFeeId===scope.sourceFeeId
|
|
|
|
|
&& String(p.sourceVersion)===scope.sourceVersion && p.sourceType===scope.sourceType && p.direction==='RECEIVABLE'
|
|
|
|
|
&& p.currency==='CNY' && p.reviewStatus==='APPROVED' && p.publicationStatus==='PUBLISHED'
|
|
|
|
|
&& p.sourceStatus===(action==='VOIDED'?'VOIDED':'ACTIVE'), 'EVENT_SCOPE')
|
|
|
|
|
// Runtime preserves exact decimal lexemes and applies Java BigDecimal canonicalization.
|
|
|
|
|
let parsed,canonical,unsigned
|
|
|
|
|
try {parsed=JSON.parse(row.envelopeJson);canonical=JSON.parse(row.payloadCanonicalJson);unsigned=JSON.parse(row.sourceCanonicalJson)}
|
|
|
|
|
catch {need(false,'CANONICAL_JSON')}
|
2026-10-03 09:10:34 +08:00
|
|
|
need(equal(parsed,env) && equal(canonical,p)
|
|
|
|
|
&& hash(row.payloadCanonicalJson)===row.payloadHash && env.payloadHash===row.payloadHash, 'PAYLOAD_HASH')
|
|
|
|
|
need(equal(unsigned,{...without(p,['action','sourceHash']),sourceStatus:'ACTIVE'})
|
|
|
|
|
&& hash(row.sourceCanonicalJson)===p.sourceHash, 'SOURCE_HASH')
|
2026-10-03 09:02:25 +08:00
|
|
|
return p
|
|
|
|
|
}
|
|
|
|
|
const row=proof.producer,payload=event(row,'VOIDED'),prior=proof.prerequisite,publication=prior?.publication
|
|
|
|
|
const original=event(publication,scope.sourceVersion==='1'?'PUBLISHED':'ADJUSTED')
|
|
|
|
|
need(prior.ready===true && publication.publishStatus==='PUBLISHED' && equal(publication,proof.publicationNow)
|
|
|
|
|
&& publication.eventId!==row.eventId && publication.entryId!==row.entryId
|
|
|
|
|
&& publication.sourceCanonicalJson===row.sourceCanonicalJson && original.sourceHash===payload.sourceHash
|
|
|
|
|
&& prior.attempts?.some(a=>a.entryId===publication.entryId&&a.eventId===publication.eventId&&['APPLIED','DUPLICATE'].includes(a.outcome)), 'PUBLICATION_PREREQUISITE')
|
|
|
|
|
const before=proof.beforeRetry,after=proof.afterRetry
|
|
|
|
|
need(before?.publishStatus==='DEAD' && before.failedAttempts===12 && after?.publishStatus==='PUBLISHED' && row.publishStatus==='PUBLISHED'
|
|
|
|
|
&& [before,after].every(value=>value.entryId===row.entryId && value.eventId===row.eventId && value.payloadHash===row.payloadHash
|
|
|
|
|
&& equal(value.envelope,row.envelope) && value.envelopeJson===row.envelopeJson
|
|
|
|
|
&& value.payloadCanonicalJson===row.payloadCanonicalJson && value.sourceCanonicalJson===row.sourceCanonicalJson), 'ORIGINAL_RETRY')
|
|
|
|
|
const attempts=proof.attempts,records=proof.proxyRecords?.filter(value=>value.matched===true)
|
|
|
|
|
need(attempts?.length===13 && new Set(attempts.map(a=>a.attemptId)).size===13 && attempts.every((a,i)=>a.entryId===row.entryId
|
|
|
|
|
&& a.eventId===row.eventId && a.serviceName==='lastleg' && a.outcome===(i<11?'RETRY':i===11?'DEAD':'DUPLICATE')), 'ATTEMPTS')
|
|
|
|
|
need(records?.length===13 && records.every((a,i)=>a.eventId===row.eventId && a.payloadHash===row.payloadHash && a.attempt===i+1
|
|
|
|
|
&& a.outcome===(i===0?'APPLIED':'DUPLICATE') && (i<12?a.errorCode==='ACK_LOSS_INJECTED':a.errorCode===null)), 'REAL_ACK_LOSS')
|
|
|
|
|
const audit=proof.retryAudit
|
|
|
|
|
need(proof.retryAuditCount===1 && audit?.requestId===proof.retryRequestId && audit.entryId===row.entryId && audit.eventId===row.eventId
|
|
|
|
|
&& audit.payloadHash===row.payloadHash && audit.previousFailures===12, 'RETRY_AUDIT')
|
|
|
|
|
const initial=prior.business,dead=before.fee,done=after.fee
|
|
|
|
|
need(initial && dead && done && equal(dead.finance,done.finance), 'FINANCE_CHANGED_AFTER_DUPLICATE')
|
|
|
|
|
const selected=(facts,local)=>{
|
|
|
|
|
const rows=local?facts.lastleg:facts.finance?.source
|
|
|
|
|
need(Array.isArray(rows)&&rows.length>0&&new Set(rows.map(a=>a.id)).size===rows.length, 'SOURCE_ROWS')
|
|
|
|
|
need(rows.every(a=>a.customerId===scope.customerId && a.sourceType===scope.sourceType && a.sourceFeeId===scope.sourceFeeId), 'SOURCE_SCOPE')
|
|
|
|
|
const result=rows.filter(a=>String(local?a.sourceVersion:a.sourceFeeVersion)===scope.sourceVersion)
|
|
|
|
|
need(result.length===1, 'UNIQUE_SOURCE_VERSION')
|
|
|
|
|
need(Math.max(...rows.map(a=>Number(local?a.sourceVersion:a.sourceFeeVersion)))===Number(scope.sourceVersion), 'LATEST_VERSION')
|
|
|
|
|
return result[0]
|
|
|
|
|
}
|
|
|
|
|
const l0=selected(initial,true),ld=selected(dead,true),la=selected(done,true)
|
|
|
|
|
const f0=selected(initial,false),fd=selected(dead,false)
|
|
|
|
|
need(l0.sourceStatus==='ACTIVE' && ld.sourceStatus==='ACTIVE' && la.sourceStatus==='VOIDED'
|
|
|
|
|
&& [l0,ld,la].every(a=>a.reviewStatus==='APPROVED'&&a.publicationStatus==='PUBLISHED'&&a.direction==='RECEIVABLE'), 'LASTLEG_ACK_BOUNDARY')
|
|
|
|
|
need(equal(without(l0,['reviewReason','updatedBy','updatedAt']),without(ld,['reviewReason','updatedBy','updatedAt']))
|
|
|
|
|
&& equal(without(ld,['sourceStatus','updatedAt']),without(la,['sourceStatus','updatedAt']))
|
|
|
|
|
&& typeof ld.reviewReason==='string' && ld.reviewReason.trim() && date(la.updatedAt)>=date(ld.updatedAt), 'LASTLEG_UNEXPECTED_CHANGE')
|
|
|
|
|
need(equal(initial.lastleg.filter(a=>a.id!==l0.id),dead.lastleg.filter(a=>a.id!==ld.id))
|
|
|
|
|
&& equal(dead.lastleg.filter(a=>a.id!==ld.id),done.lastleg.filter(a=>a.id!==la.id)), 'OTHER_LOCAL_VERSIONS_CHANGED')
|
|
|
|
|
need(f0.status==='AVAILABLE' && fd.status==='VOIDED' && Number(fd.operationVersion)===Number(f0.operationVersion)+1
|
|
|
|
|
&& equal(without(f0,['status','operationVersion','updatedAt']),without(fd,['status','operationVersion','updatedAt']))
|
|
|
|
|
&& f0.sourceHash===payload.sourceHash && f0.direction==='RECEIVABLE' && f0.currencyCode==='CNY'
|
|
|
|
|
&& f0.amount===l0.amount && f0.walletHoldId===null && equal(f0.sourceSnapshot,original), 'FINANCE_VOID_FACT')
|
|
|
|
|
need(equal(initial.finance.source.filter(a=>a.id!==f0.id),dead.finance.source.filter(a=>a.id!==fd.id)), 'OTHER_FINANCE_VERSIONS_CHANGED')
|
|
|
|
|
for(const facts of [initial,dead,done]) for(const key of ['bill','settlement','cost']) need(Array.isArray(facts.finance[key])&&facts.finance[key].length===0,'FINANCIAL_LINKS')
|
|
|
|
|
const checkInbox=(rows,producer,resource)=>{
|
|
|
|
|
need(rows?.length===1 && rows[0].sourceSystem==='LASTLEG' && rows[0].eventType==='BusinessFeeSourceChanged'
|
|
|
|
|
&& rows[0].eventId===producer.eventId && rows[0].payloadHash===producer.payloadHash && rows[0].status==='PROCESSED'
|
|
|
|
|
&& rows[0].resultResourceId===resource.id && rows[0].processedAt && rows[0].errorCode===null
|
|
|
|
|
&& equal(rows[0].envelope,producer.envelope), 'FINANCE_INBOX')
|
|
|
|
|
}
|
|
|
|
|
checkInbox(initial.finance.inbox,publication,f0); checkInbox(dead.finance.inbox,row,fd)
|
|
|
|
|
need(proof.consumer?.count===1 && equal(without(proof.consumer,['count']),dead.finance.inbox[0]), 'CONSUMER_IDENTITY')
|
|
|
|
|
return proof
|
|
|
|
|
}
|