// WorthBits synthetic demo v1. Offline, no network, no model and no user-data collection. import fs from 'node:fs'; import {fileURLToPath} from 'node:url'; const codes={duplicate_billing:'同一请求超过一条有效扣费',output_missing:'缺少约定的图片成果',charged_without_output:'按演示约定无成果不应扣费',unit_unknown:'单位不一致且没有换算规则',quantity_mismatch:'同单位用量与扣费不一致',time_unknown:'时间缺少明确时区或无法解析',time_mismatch:'扣费记录的请求时刻不一致',request_mismatch:'扣费未关联本请求',billing_missing:'缺少扣费记录',refund_unconfirmed:'退款通知不能证明到账',refund_unlinked:'退款未关联本例扣费',feedback_unlinked:'反馈不能关联本请求'}; const instant=s=>typeof s==='string'&&/(Z|[+-]\d{2}:\d{2})$/.test(s)&&Number.isFinite(Date.parse(s))?Date.parse(s):null; export function reconcile(data){ if(data.synthetic!==true||data.schemaVersion!==1)throw Error('Only synthetic v1 demo data is supported'); const keys=['oneBillPerRequest','billOnlyDeliveredImage','sameUnitQuantityMustMatch','compareExplicitTimezoneInstants','noUnitConversionInThisDemo','refundNeedsSettledStatusAndTime','feedbackNeedsRequestId']; if(keys.some(k=>data.rules?.[k]!==true)||Object.keys(data.rules).length!==keys.length)throw Error('Unsupported rule change; revise the checker explicitly'); if(new Set(data.cases.map(c=>c.id)).size!==data.cases.length)throw Error('Duplicate case IDs'); return {synthetic:true,version:1,boundary:'只检验给定合成记录是否符合演示约定;匹配不证明真实计费正确、客户认可或退款到账。',results:data.cases.map(c=>{ const flags=new Set(),r=c.request,b=c.bills;if(!r?.id||r.expectedOutput!=='image'||!Array.isArray(b))throw Error('Missing request or bills'); for(const usage of [r.usage,...b])if(!Number.isFinite(usage.quantity)||usage.quantity<0||!usage.unit)throw Error('Invalid usage value'); if(b.length===0)flags.add('billing_missing');if(b.length>1)flags.add('duplicate_billing'); if(r.actualOutput!=='image'){flags.add('output_missing');if(b.some(x=>x.quantity>0))flags.add('charged_without_output');} for(const x of b){if(x.requestId!==r.id)flags.add('request_mismatch');if(x.unit!==r.usage.unit)flags.add('unit_unknown');else if(x.quantity!==r.usage.quantity)flags.add('quantity_mismatch');const t=instant(r.at),u=instant(x.requestAt);if(t===null||u===null)flags.add('time_unknown');else if(t!==u)flags.add('time_mismatch');} for(const x of c.refunds||[]){if(!b.some(y=>y.id===x.billId))flags.add('refund_unlinked');if(x.status!=='settled'||instant(x.settledAt)===null)flags.add('refund_unconfirmed');} for(const x of c.feedback||[])if(x.requestId!==r.id)flags.add('feedback_unlinked'); return {id:c.id,title:c.title,status:flags.size?'needs_review':'matched_under_demo_rules',flags:[...flags],notes:[...flags].map(x=>codes[x])}; })}; } if(process.argv[1]&&fileURLToPath(import.meta.url)===process.argv[1]){ const input=process.argv[2]||new URL('./worthbits-api-reconciliation-v1.json',import.meta.url); console.log(JSON.stringify(reconcile(JSON.parse(fs.readFileSync(input,'utf8'))),null,2)); }