diff --git a/revenue-refund-liability-reserve-guard/README.md b/revenue-refund-liability-reserve-guard/README.md new file mode 100644 index 00000000..885235bd --- /dev/null +++ b/revenue-refund-liability-reserve-guard/README.md @@ -0,0 +1,49 @@ +# Revenue Refund Liability Reserve Guard + +This module adds a focused refund liability reserve guard for the Revenue +Infrastructure bounty. It evaluates whether subscription, institutional +invoice, and compute-credit refund requests are financially safe before cash +leaves the platform or entitlements are reversed. + +## Scope + +The guard checks synthetic refund packets for: + +- annual subscription refund eligibility and proration +- institutional invoice reversal evidence and approval status +- unused AI compute credit refund treatment +- non-refundable processor fees and tax handling +- entitlement rollback readiness before cash release +- refund evidence completeness +- reserve sufficiency by revenue stream and currency +- reviewer actions for hold, approve, or manual finance review decisions + +This is not a generic quote approval, billing readiness, payment rail, +dunning, dispute, tax exemption, proration, prepaid credit breakage, or +collections module. It focuses specifically on refund liability exposure and +reserve sufficiency before money is returned. + +## Issue #20 Requirement Mapping + +- Tiered subscription billing: evaluates subscription refunds, annual-plan + proration, cancellation terms, and entitlement rollback readiness. +- AI compute billing: checks unused compute-credit refund requests against + consumed usage, restricted grants, and available reserve coverage. +- Institutional invoicing: validates invoice reversal approvals, PO evidence, + and tax/processor fee treatment. +- Revenue operations: emits deterministic finance review packets, reserve + impact, audit digests, and curator actions for revenue teams. + +## Validation + +```bash +node revenue-refund-liability-reserve-guard/test.js +node revenue-refund-liability-reserve-guard/demo.js +node revenue-refund-liability-reserve-guard/render-video.js +node --check revenue-refund-liability-reserve-guard/index.js +node --check revenue-refund-liability-reserve-guard/sample-data.js +node --check revenue-refund-liability-reserve-guard/test.js +node --check revenue-refund-liability-reserve-guard/demo.js +node --check revenue-refund-liability-reserve-guard/render-video.js +git diff --check +``` diff --git a/revenue-refund-liability-reserve-guard/demo.js b/revenue-refund-liability-reserve-guard/demo.js new file mode 100644 index 00000000..ecde0e08 --- /dev/null +++ b/revenue-refund-liability-reserve-guard/demo.js @@ -0,0 +1,27 @@ +const fs = require("fs"); +const path = require("path"); +const packets = require("./sample-data"); +const { + evaluateRefundBatch, + renderMarkdownReport, + renderSvgReport, +} = require("./index"); + +const reportDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportDir, { recursive: true }); + +const report = evaluateRefundBatch(packets); +const jsonPath = path.join(reportDir, "refund-liability-review.json"); +const markdownPath = path.join(reportDir, "refund-liability-review.md"); +const svgPath = path.join(reportDir, "refund-liability-review.svg"); + +fs.writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`); +fs.writeFileSync(markdownPath, renderMarkdownReport(report)); +fs.writeFileSync(svgPath, renderSvgReport(report)); + +console.log(`wrote ${path.relative(process.cwd(), jsonPath)}`); +console.log(`wrote ${path.relative(process.cwd(), markdownPath)}`); +console.log(`wrote ${path.relative(process.cwd(), svgPath)}`); +console.log( + `summary: ${report.summary.hold} held refunds, ${report.summary.refundableAmount} refundable` +); diff --git a/revenue-refund-liability-reserve-guard/index.js b/revenue-refund-liability-reserve-guard/index.js new file mode 100644 index 00000000..1efce99a --- /dev/null +++ b/revenue-refund-liability-reserve-guard/index.js @@ -0,0 +1,269 @@ +const crypto = require("crypto"); + +function money(value) { + return Math.round(Number(value || 0) * 100) / 100; +} + +function requiredEvidenceFor(packet) { + const evidence = ["contract-term", "entitlement-rollback"]; + if (packet.revenueStream === "institutional_invoice") { + evidence.push("finance-approval"); + if (packet.requiresPOEvidence) { + evidence.push("po-or-invoice-evidence"); + } + } + if (packet.revenueStream === "ai_compute") { + evidence.push("credit-ledger", "usage-meter-snapshot"); + if (packet.restrictedGrantFunded) { + evidence.push("grant-refund-approval"); + } + } + if (packet.taxCollected > 0) { + evidence.push("tax-refund-treatment"); + } + return evidence; +} + +function missingEvidence(packet) { + const present = new Set(packet.evidence || []); + return requiredEvidenceFor(packet).filter((item) => !present.has(item)); +} + +function refundableAmount(packet) { + const serviceRefund = Math.min(packet.requestedRefund, packet.unusedServiceValue); + const taxComponent = packet.taxRefundable ? packet.taxCollected : 0; + return money(Math.max(0, serviceRefund + taxComponent - packet.processorFee)); +} + +function reserveImpact(packet) { + return money(refundableAmount(packet)); +} + +function evaluateRefund(packet) { + const blockers = []; + const warnings = []; + const actions = []; + const missing = missingEvidence(packet); + const refundable = refundableAmount(packet); + const overRequest = money(Math.max(0, packet.requestedRefund - packet.unusedServiceValue)); + const reserveAfter = money(packet.reserveAvailable - reserveImpact(packet)); + + if (missing.length > 0) { + blockers.push(`missing evidence: ${missing.join(", ")}`); + actions.push("hold refund until evidence packet is complete"); + } + + if (packet.approvalStatus !== "approved") { + blockers.push(`approval status is ${packet.approvalStatus}`); + actions.push("route to finance approver before cash release"); + } + + if (!packet.entitlementsRolledBack) { + blockers.push("entitlements are not rolled back"); + actions.push("rollback entitlements before refund release"); + } + + if (overRequest > 0) { + warnings.push(`requested refund exceeds unused service value by ${overRequest} ${packet.currency}`); + actions.push("cap refundable service amount at unused value"); + } + + if (packet.processorFee > 0) { + warnings.push(`processor fee ${packet.processorFee} ${packet.currency} is non-refundable`); + } + + if (packet.taxCollected > 0 && !packet.taxRefundable) { + warnings.push(`tax collected ${packet.taxCollected} ${packet.currency} requires non-refund treatment`); + } + + if (packet.restrictedGrantFunded && !(packet.evidence || []).includes("grant-refund-approval")) { + blockers.push("restricted grant-funded compute lacks refund approval"); + actions.push("hold refund for grant compliance review"); + } + + if (reserveAfter < 0) { + blockers.push(`refund reserve short by ${money(Math.abs(reserveAfter))} ${packet.currency}`); + actions.push("top up refund reserve before release"); + } else if (reserveAfter < refundable * 0.25) { + warnings.push(`reserve after refund is low at ${reserveAfter} ${packet.currency}`); + actions.push("notify finance about low reserve coverage"); + } + + if (blockers.length === 0) { + actions.push("approve refund for payment queue"); + actions.push("record reserve draw and revenue reversal"); + } + + const decision = + blockers.length > 0 ? "hold" : warnings.length > 0 ? "approve_with_adjustments" : "approve"; + + const result = { + refundId: packet.id, + customer: packet.customer, + revenueStream: packet.revenueStream, + currency: packet.currency, + requestedRefund: money(packet.requestedRefund), + refundableAmount: refundable, + reserveAvailable: money(packet.reserveAvailable), + reserveAfter, + decision, + blockers, + warnings, + actions: [...new Set(actions)], + }; + + return { + ...result, + auditDigest: digest(result), + }; +} + +function evaluateRefundBatch(packets) { + const decisions = packets.map(evaluateRefund); + const summary = decisions.reduce( + (acc, item) => { + acc.total += 1; + acc[item.decision] += 1; + acc.requestedRefund = money(acc.requestedRefund + item.requestedRefund); + acc.refundableAmount = money(acc.refundableAmount + item.refundableAmount); + acc.reserveDraw = money(acc.reserveDraw + Math.max(0, item.refundableAmount)); + if (item.decision === "hold") { + acc.heldRefunds = money(acc.heldRefunds + item.requestedRefund); + } + return acc; + }, + { + total: 0, + approve: 0, + approve_with_adjustments: 0, + hold: 0, + requestedRefund: 0, + refundableAmount: 0, + reserveDraw: 0, + heldRefunds: 0, + } + ); + + const report = { + generatedAt: new Date("2026-05-31T00:00:00Z").toISOString(), + summary, + decisions, + }; + + return { + ...report, + auditDigest: digest(report), + }; +} + +function renderMarkdownReport(report) { + const lines = [ + "# Revenue Refund Liability Reserve Report", + "", + `Generated: ${report.generatedAt}`, + `Audit digest: ${report.auditDigest}`, + "", + "## Summary", + "", + `- Refund packets: ${report.summary.total}`, + `- Approve: ${report.summary.approve}`, + `- Approve with adjustments: ${report.summary.approve_with_adjustments}`, + `- Hold: ${report.summary.hold}`, + `- Requested refund: ${report.summary.requestedRefund}`, + `- Refundable amount: ${report.summary.refundableAmount}`, + `- Reserve draw: ${report.summary.reserveDraw}`, + `- Held refunds: ${report.summary.heldRefunds}`, + "", + "## Decisions", + "", + ]; + + for (const item of report.decisions) { + lines.push(`### ${item.refundId}`); + lines.push(""); + lines.push(`- Customer: ${item.customer}`); + lines.push(`- Stream: ${item.revenueStream}`); + lines.push(`- Decision: ${item.decision}`); + lines.push(`- Requested refund: ${item.requestedRefund} ${item.currency}`); + lines.push(`- Refundable amount: ${item.refundableAmount} ${item.currency}`); + lines.push(`- Reserve after: ${item.reserveAfter} ${item.currency}`); + lines.push(`- Blockers: ${item.blockers.join("; ") || "none"}`); + lines.push(`- Warnings: ${item.warnings.join("; ") || "none"}`); + lines.push(`- Actions: ${item.actions.join("; ")}`); + lines.push(`- Digest: ${item.auditDigest}`); + lines.push(""); + } + + while (lines[lines.length - 1] === "") { + lines.pop(); + } + + return `${lines.join("\n")}\n`; +} + +function escapeXml(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function renderSvgReport(report) { + const cards = [ + ["Packets", report.summary.total, "#355c7d"], + ["Approved", report.summary.approve, "#2a9d8f"], + ["Adjusted", report.summary.approve_with_adjustments, "#e9c46a"], + ["Held", report.summary.hold, "#e76f51"], + ["Refundable", `$${report.summary.refundableAmount}`, "#457b9d"], + ["Held value", `$${report.summary.heldRefunds}`, "#b56576"], + ]; + + const cardSvg = cards + .map(([label, value, color], index) => { + const x = 72 + (index % 3) * 380; + const y = 188 + Math.floor(index / 3) * 150; + return [ + ``, + ``, + `${escapeXml(label)}`, + `${escapeXml(value)}`, + ].join("\n"); + }) + .join("\n"); + + const queueRows = report.decisions + .filter((item) => item.decision !== "approve") + .slice(0, 3) + .map((item, index) => { + const y = 545 + index * 32; + return `${escapeXml(item.refundId)} - ${escapeXml(item.decision)} - ${escapeXml(item.actions[0])}`; + }) + .join("\n"); + + return ` + + + Revenue Refund Liability Reserve Guard + Refund exposure and reserve sufficiency review + ${cardSvg} + Finance queue + ${queueRows} + Audit digest ${escapeXml(report.auditDigest.slice(0, 32))} +`; +} + +function digest(value) { + return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); +} + +module.exports = { + evaluateRefund, + evaluateRefundBatch, + refundableAmount, + reserveImpact, + requiredEvidenceFor, + renderMarkdownReport, + renderSvgReport, + digest, +}; diff --git a/revenue-refund-liability-reserve-guard/render-video.js b/revenue-refund-liability-reserve-guard/render-video.js new file mode 100644 index 00000000..40b0a9df --- /dev/null +++ b/revenue-refund-liability-reserve-guard/render-video.js @@ -0,0 +1,50 @@ +const fs = require("fs"); +const path = require("path"); +const { execFileSync } = require("child_process"); + +const reportDir = path.join(__dirname, "reports"); +const jsonPath = path.join(reportDir, "refund-liability-review.json"); +const svgPath = path.join(reportDir, "refund-liability-review.svg"); +const framePath = path.join(reportDir, "refund-liability-review.png"); +const outputPath = path.join(reportDir, "demo.mp4"); + +if (!fs.existsSync(jsonPath) || !fs.existsSync(svgPath)) { + require("./demo"); +} + +const report = JSON.parse(fs.readFileSync(jsonPath, "utf8")); + +execFileSync( + "rsvg-convert", + ["--width", "1280", "--height", "720", "--output", framePath, svgPath], + { stdio: "inherit" } +); + +execFileSync( + "ffmpeg", + [ + "-y", + "-loop", + "1", + "-framerate", + "25", + "-i", + framePath, + "-t", + "4", + "-pix_fmt", + "yuv420p", + "-vf", + "scale=1280:720", + "-movflags", + "+faststart", + outputPath, + ], + { stdio: "inherit" } +); + +fs.unlinkSync(framePath); + +console.log( + `wrote ${path.relative(process.cwd(), outputPath)} for ${report.summary.total} refund packets` +); diff --git a/revenue-refund-liability-reserve-guard/reports/demo.mp4 b/revenue-refund-liability-reserve-guard/reports/demo.mp4 new file mode 100644 index 00000000..879da938 Binary files /dev/null and b/revenue-refund-liability-reserve-guard/reports/demo.mp4 differ diff --git a/revenue-refund-liability-reserve-guard/reports/refund-liability-review.json b/revenue-refund-liability-reserve-guard/reports/refund-liability-review.json new file mode 100644 index 00000000..c7d896ce --- /dev/null +++ b/revenue-refund-liability-reserve-guard/reports/refund-liability-review.json @@ -0,0 +1,109 @@ +{ + "generatedAt": "2026-05-31T00:00:00.000Z", + "summary": { + "total": 4, + "approve": 0, + "approve_with_adjustments": 2, + "hold": 2, + "requestedRefund": 11100, + "refundableAmount": 9498, + "reserveDraw": 9498, + "heldRefunds": 7600 + }, + "decisions": [ + { + "refundId": "refund:annual-pro-cancel-clean", + "customer": "Northbridge Lab", + "revenueStream": "subscription", + "currency": "USD", + "requestedRefund": 900, + "refundableAmount": 938, + "reserveAvailable": 1200, + "reserveAfter": 262, + "decision": "approve_with_adjustments", + "blockers": [], + "warnings": [ + "processor fee 34 USD is non-refundable" + ], + "actions": [ + "approve refund for payment queue", + "record reserve draw and revenue reversal" + ], + "auditDigest": "39dbf4dd893713fc64b55d5d5ef6de4669a088120c01b5074ec3ed851de81155" + }, + { + "refundId": "refund:institutional-invoice-missing-po", + "customer": "East Valley University", + "revenueStream": "institutional_invoice", + "currency": "USD", + "requestedRefund": 7000, + "refundableAmount": 6200, + "reserveAvailable": 5000, + "reserveAfter": -1200, + "decision": "hold", + "blockers": [ + "missing evidence: entitlement-rollback, finance-approval, po-or-invoice-evidence", + "approval status is pending", + "entitlements are not rolled back", + "refund reserve short by 1200 USD" + ], + "warnings": [ + "requested refund exceeds unused service value by 800 USD" + ], + "actions": [ + "hold refund until evidence packet is complete", + "route to finance approver before cash release", + "rollback entitlements before refund release", + "cap refundable service amount at unused value", + "top up refund reserve before release" + ], + "auditDigest": "7705b511278e5888ce93be1264ae43da26dd26d8c441be69fbef7d6e50c351d1" + }, + { + "refundId": "refund:compute-credit-over-request", + "customer": "Helix AI Core", + "revenueStream": "ai_compute", + "currency": "USD", + "requestedRefund": 2600, + "refundableAmount": 1815, + "reserveAvailable": 2400, + "reserveAfter": 585, + "decision": "approve_with_adjustments", + "blockers": [], + "warnings": [ + "requested refund exceeds unused service value by 700 USD", + "processor fee 85 USD is non-refundable" + ], + "actions": [ + "cap refundable service amount at unused value", + "approve refund for payment queue", + "record reserve draw and revenue reversal" + ], + "auditDigest": "f8c6f3e25b3e20063c04d6ff23ce2c88a6a2975967f406763ccd19049e41aa49" + }, + { + "refundId": "refund:restricted-grant-compute", + "customer": "Civic Bio Grant", + "revenueStream": "ai_compute", + "currency": "USD", + "requestedRefund": 600, + "refundableAmount": 545, + "reserveAvailable": 1000, + "reserveAfter": 455, + "decision": "hold", + "blockers": [ + "missing evidence: grant-refund-approval", + "restricted grant-funded compute lacks refund approval" + ], + "warnings": [ + "processor fee 55 USD is non-refundable" + ], + "actions": [ + "hold refund until evidence packet is complete", + "hold refund for grant compliance review" + ], + "auditDigest": "adda46759e57cf7997019364e7c8467be44025f767d05debfcf68a612fa8798d" + } + ], + "auditDigest": "6c51ddbe808a1a8cacb81a52cfd1e832b78f82bbfc31c0e4df4bb16a373022af" +} diff --git a/revenue-refund-liability-reserve-guard/reports/refund-liability-review.md b/revenue-refund-liability-reserve-guard/reports/refund-liability-review.md new file mode 100644 index 00000000..2255e9de --- /dev/null +++ b/revenue-refund-liability-reserve-guard/reports/refund-liability-review.md @@ -0,0 +1,69 @@ +# Revenue Refund Liability Reserve Report + +Generated: 2026-05-31T00:00:00.000Z +Audit digest: 6c51ddbe808a1a8cacb81a52cfd1e832b78f82bbfc31c0e4df4bb16a373022af + +## Summary + +- Refund packets: 4 +- Approve: 0 +- Approve with adjustments: 2 +- Hold: 2 +- Requested refund: 11100 +- Refundable amount: 9498 +- Reserve draw: 9498 +- Held refunds: 7600 + +## Decisions + +### refund:annual-pro-cancel-clean + +- Customer: Northbridge Lab +- Stream: subscription +- Decision: approve_with_adjustments +- Requested refund: 900 USD +- Refundable amount: 938 USD +- Reserve after: 262 USD +- Blockers: none +- Warnings: processor fee 34 USD is non-refundable +- Actions: approve refund for payment queue; record reserve draw and revenue reversal +- Digest: 39dbf4dd893713fc64b55d5d5ef6de4669a088120c01b5074ec3ed851de81155 + +### refund:institutional-invoice-missing-po + +- Customer: East Valley University +- Stream: institutional_invoice +- Decision: hold +- Requested refund: 7000 USD +- Refundable amount: 6200 USD +- Reserve after: -1200 USD +- Blockers: missing evidence: entitlement-rollback, finance-approval, po-or-invoice-evidence; approval status is pending; entitlements are not rolled back; refund reserve short by 1200 USD +- Warnings: requested refund exceeds unused service value by 800 USD +- Actions: hold refund until evidence packet is complete; route to finance approver before cash release; rollback entitlements before refund release; cap refundable service amount at unused value; top up refund reserve before release +- Digest: 7705b511278e5888ce93be1264ae43da26dd26d8c441be69fbef7d6e50c351d1 + +### refund:compute-credit-over-request + +- Customer: Helix AI Core +- Stream: ai_compute +- Decision: approve_with_adjustments +- Requested refund: 2600 USD +- Refundable amount: 1815 USD +- Reserve after: 585 USD +- Blockers: none +- Warnings: requested refund exceeds unused service value by 700 USD; processor fee 85 USD is non-refundable +- Actions: cap refundable service amount at unused value; approve refund for payment queue; record reserve draw and revenue reversal +- Digest: f8c6f3e25b3e20063c04d6ff23ce2c88a6a2975967f406763ccd19049e41aa49 + +### refund:restricted-grant-compute + +- Customer: Civic Bio Grant +- Stream: ai_compute +- Decision: hold +- Requested refund: 600 USD +- Refundable amount: 545 USD +- Reserve after: 455 USD +- Blockers: missing evidence: grant-refund-approval; restricted grant-funded compute lacks refund approval +- Warnings: processor fee 55 USD is non-refundable +- Actions: hold refund until evidence packet is complete; hold refund for grant compliance review +- Digest: adda46759e57cf7997019364e7c8467be44025f767d05debfcf68a612fa8798d diff --git a/revenue-refund-liability-reserve-guard/reports/refund-liability-review.svg b/revenue-refund-liability-reserve-guard/reports/refund-liability-review.svg new file mode 100644 index 00000000..baba5bb9 --- /dev/null +++ b/revenue-refund-liability-reserve-guard/reports/refund-liability-review.svg @@ -0,0 +1,35 @@ + + + + Revenue Refund Liability Reserve Guard + Refund exposure and reserve sufficiency review + + +Packets +4 + + +Approved +0 + + +Adjusted +2 + + +Held +2 + + +Refundable +$9498 + + +Held value +$7600 + Finance queue + refund:annual-pro-cancel-clean - approve_with_adjustments - approve refund for payment queue +refund:institutional-invoice-missing-po - hold - hold refund until evidence packet is complete +refund:compute-credit-over-request - approve_with_adjustments - cap refundable service amount at unused value + Audit digest 6c51ddbe808a1a8cacb81a52cfd1e832 + \ No newline at end of file diff --git a/revenue-refund-liability-reserve-guard/sample-data.js b/revenue-refund-liability-reserve-guard/sample-data.js new file mode 100644 index 00000000..3bb58bee --- /dev/null +++ b/revenue-refund-liability-reserve-guard/sample-data.js @@ -0,0 +1,75 @@ +const refundPackets = [ + { + id: "refund:annual-pro-cancel-clean", + customer: "Northbridge Lab", + revenueStream: "subscription", + currency: "USD", + originalCharge: 2400, + requestedRefund: 900, + unusedServiceValue: 960, + processorFee: 34, + taxCollected: 72, + taxRefundable: true, + cancellationReason: "lab merged into institutional plan", + approvalStatus: "approved", + evidence: ["contract-term", "cancellation-ticket", "entitlement-rollback", "tax-refund-treatment"], + entitlementsRolledBack: true, + reserveAvailable: 1200, + }, + { + id: "refund:institutional-invoice-missing-po", + customer: "East Valley University", + revenueStream: "institutional_invoice", + currency: "USD", + originalCharge: 18000, + requestedRefund: 7000, + unusedServiceValue: 6200, + processorFee: 0, + taxCollected: 0, + taxRefundable: false, + cancellationReason: "department budget reallocation", + approvalStatus: "pending", + evidence: ["contract-term"], + entitlementsRolledBack: false, + reserveAvailable: 5000, + requiresPOEvidence: true, + }, + { + id: "refund:compute-credit-over-request", + customer: "Helix AI Core", + revenueStream: "ai_compute", + currency: "USD", + originalCharge: 5000, + requestedRefund: 2600, + unusedServiceValue: 1900, + processorFee: 85, + taxCollected: 0, + taxRefundable: false, + cancellationReason: "unused top-up credits", + approvalStatus: "approved", + evidence: ["contract-term", "credit-ledger", "usage-meter-snapshot", "entitlement-rollback"], + entitlementsRolledBack: true, + reserveAvailable: 2400, + restrictedGrantFunded: false, + }, + { + id: "refund:restricted-grant-compute", + customer: "Civic Bio Grant", + revenueStream: "ai_compute", + currency: "USD", + originalCharge: 3200, + requestedRefund: 600, + unusedServiceValue: 650, + processorFee: 55, + taxCollected: 0, + taxRefundable: false, + cancellationReason: "grant-funded compute pack closed", + approvalStatus: "approved", + evidence: ["contract-term", "entitlement-rollback", "credit-ledger", "usage-meter-snapshot", "grant-restriction"], + entitlementsRolledBack: true, + reserveAvailable: 1000, + restrictedGrantFunded: true, + }, +]; + +module.exports = refundPackets; diff --git a/revenue-refund-liability-reserve-guard/test.js b/revenue-refund-liability-reserve-guard/test.js new file mode 100644 index 00000000..7c6a05bb --- /dev/null +++ b/revenue-refund-liability-reserve-guard/test.js @@ -0,0 +1,66 @@ +const assert = require("assert"); +const packets = require("./sample-data"); +const { + evaluateRefund, + evaluateRefundBatch, + refundableAmount, + requiredEvidenceFor, +} = require("./index"); + +const report = evaluateRefundBatch(packets); + +function decision(id) { + return report.decisions.find((item) => item.refundId === id); +} + +assert.strictEqual(report.summary.total, 4); +assert.strictEqual(report.summary.approve, 0); +assert.strictEqual(report.summary.approve_with_adjustments, 2); +assert.strictEqual(report.summary.hold, 2); +assert.strictEqual(report.summary.requestedRefund, 11100); +assert.strictEqual(report.summary.heldRefunds, 7600); + +const clean = decision("refund:annual-pro-cancel-clean"); +assert.strictEqual(clean.decision, "approve_with_adjustments"); +assert.strictEqual(clean.refundableAmount, 938); +assert( + clean.warnings.some((item) => item.includes("processor fee")), + "clean annual refund should surface non-refundable processor fee" +); + +const invoice = decision("refund:institutional-invoice-missing-po"); +assert.strictEqual(invoice.decision, "hold"); +assert( + invoice.blockers.some((item) => item.includes("po-or-invoice-evidence")), + "institutional refund should require PO or invoice evidence" +); +assert( + invoice.blockers.some((item) => item.includes("approval status")), + "institutional refund should require approved finance status" +); +assert( + invoice.blockers.some((item) => item.includes("reserve short")), + "institutional refund should be held if reserve coverage is short" +); + +const compute = decision("refund:compute-credit-over-request"); +assert.strictEqual(compute.decision, "approve_with_adjustments"); +assert.strictEqual(compute.refundableAmount, 1815); +assert( + compute.warnings.some((item) => item.includes("exceeds unused service value")), + "compute over-request should be capped by unused service value" +); + +const restricted = decision("refund:restricted-grant-compute"); +assert.strictEqual(restricted.decision, "hold"); +assert( + restricted.blockers.some((item) => item.includes("grant-refund-approval")), + "restricted grant funded compute refund needs grant approval evidence" +); + +assert.strictEqual(refundableAmount(packets[0]), 938); +assert(requiredEvidenceFor(packets[1]).includes("po-or-invoice-evidence")); +assert.match(report.auditDigest, /^[a-f0-9]{64}$/); +assert.match(evaluateRefund(packets[0]).auditDigest, /^[a-f0-9]{64}$/); + +console.log("revenue refund liability reserve guard tests passed");