From 2f08e29584936b8dcd245e48fca9e428c549b96b Mon Sep 17 00:00:00 2001 From: cw <88217123+2bf@users.noreply.github.com> Date: Tue, 19 May 2026 18:28:48 -0700 Subject: [PATCH] Add revenue dispute guard --- revenue-dispute-guard/README.md | 43 ++++++ revenue-dispute-guard/demo.js | 60 ++++++++ revenue-dispute-guard/demo.mp4 | Bin 0 -> 51221 bytes revenue-dispute-guard/demo.svg | 22 +++ revenue-dispute-guard/index.js | 249 ++++++++++++++++++++++++++++++++ revenue-dispute-guard/test.js | 131 +++++++++++++++++ 6 files changed, 505 insertions(+) create mode 100644 revenue-dispute-guard/README.md create mode 100644 revenue-dispute-guard/demo.js create mode 100644 revenue-dispute-guard/demo.mp4 create mode 100644 revenue-dispute-guard/demo.svg create mode 100644 revenue-dispute-guard/index.js create mode 100644 revenue-dispute-guard/test.js diff --git a/revenue-dispute-guard/README.md b/revenue-dispute-guard/README.md new file mode 100644 index 0000000..929d403 --- /dev/null +++ b/revenue-dispute-guard/README.md @@ -0,0 +1,43 @@ +# Revenue Dispute Guard + +Self-contained milestone for SCIBASE.AI issue #20, Revenue Infrastructure. + +This module evaluates payment disputes, failed payment recovery, AI compute +overages, top-up velocity, and licensing export readiness before revenue is +recognized or entitlements are extended. It is synthetic-data-only and has no +network calls, credentials, or payment-provider dependencies. + +## What It Covers + +- Chargeback and open-dispute risk scoring for subscription accounts. +- AI compute overage and rapid top-up abuse detection. +- Entitlement hold decisions for past-due or disputed accounts. +- Invoice reserve recommendations and revenue adjustment packets. +- Dunning, payment-method, procurement, and account-review actions. +- Privacy-safe licensing export gates with consent and aggregation checks. +- Stable audit digests for finance review packets. + +## Files + +- `index.js` - revenue dispute and adjustment guard engine. +- `demo.js` - terminal demo for finance guard packets. +- `test.js` - dependency-free regression tests. +- `demo.mp4` - short demo artifact for bounty review. + +## Run + +```sh +node revenue-dispute-guard/test.js +node revenue-dispute-guard/demo.js +``` + +## Requirement Map + +| Issue #20 Requirement | Implementation | +| --- | --- | +| Tiered subscription billing | Account packets include plan, recurring revenue, invoice status, entitlement decisions, and reserve actions. | +| Usage-based AI compute billing | Compute spend, budget, and top-up velocity feed risk scores and usage hold recommendations. | +| Payment integrations and invoicing | Payment failures, chargebacks, dunning actions, and institutional invoice reserves are modeled without live provider calls. | +| Data licensing APIs and analytics | Licensing exports are gated by anonymization, consent coverage, and aggregation thresholds. | +| Predictable recurring revenue | Portfolio packets surface held ARR/MRR, reserve totals, risk tiers, and audit digests for finance review. | +| Compliance and auditability | Every account packet includes evidence factors, recovery actions, revenue adjustments, and stable audit digests. | diff --git a/revenue-dispute-guard/demo.js b/revenue-dispute-guard/demo.js new file mode 100644 index 0000000..3bf81f3 --- /dev/null +++ b/revenue-dispute-guard/demo.js @@ -0,0 +1,60 @@ +const { buildPortfolioGuard } = require("./index") + +const accounts = [ + { + id: "lab-healthy", + plan: "lab", + monthlyRecurringRevenueUsd: 1200, + computeBudgetUsd: 800, + computeSpendUsd: 620, + licensingExports: [ + { + id: "reuse-trends-q2", + valueUsd: 2200, + anonymized: true, + consentCoverage: 0.99, + aggregationThreshold: 40, + }, + ], + }, + { + id: "institute-disputed", + plan: "institution", + monthlyRecurringRevenueUsd: 9000, + openDisputes: 2, + chargebacksLostLast180d: 2, + averageDisputeValueUsd: 4200, + openInvoiceBalanceUsd: 18000, + invoiceDaysPastDue: 45, + paymentFailuresLast30d: 4, + computeBudgetUsd: 12000, + computeSpendUsd: 24000, + topupPurchasesLast24h: 4, + topupValueLast24hUsd: 6000, + licensingExports: [ + { + id: "national-reuse-api", + valueUsd: 12500, + anonymized: true, + consentCoverage: 0.91, + aggregationThreshold: 8, + }, + ], + }, + { + id: "startup-topups", + plan: "pro", + monthlyRecurringRevenueUsd: 300, + paymentFailuresLast30d: 1, + computeBudgetUsd: 400, + computeSpendUsd: 980, + topupPurchasesLast24h: 5, + topupValueLast24hUsd: 700, + licensingExports: [], + }, +] + +const packet = buildPortfolioGuard(accounts, { generatedFor: "monthly_close_packet" }) + +console.log("Revenue dispute guard") +console.log(JSON.stringify(packet, null, 2)) diff --git a/revenue-dispute-guard/demo.mp4 b/revenue-dispute-guard/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..0d15a89cbdda6e84245266800e79b09ef59b1be9 GIT binary patch literal 51221 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 + + + Revenue Dispute Guard + Payment disputes, failed recovery, AI compute overages, and licensing export holds + + Critical Hold + 2 open disputes + 41 days past due + $17k compute spend + + Reserve Packet + Dispute evidence + Deferred revenue + License export hold + + Audit Digest + Risk factors + Recovery actions + Stable close packet + Result: hold risky entitlements, reserve exposed revenue, and keep privacy-risky licensing exports out of recognized revenue. + diff --git a/revenue-dispute-guard/index.js b/revenue-dispute-guard/index.js new file mode 100644 index 0000000..6e3788c --- /dev/null +++ b/revenue-dispute-guard/index.js @@ -0,0 +1,249 @@ +const crypto = require("node:crypto") + +const RISK_THRESHOLDS = [ + { tier: "critical", minScore: 0.8 }, + { tier: "high", minScore: 0.6 }, + { tier: "medium", minScore: 0.35 }, + { tier: "low", minScore: 0 }, +] + +function stableJson(value) { + if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]` + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`) + .join(",")}}` + } + return JSON.stringify(value) +} + +function digest(value) { + return crypto.createHash("sha256").update(stableJson(value)).digest("hex") +} + +function clamp(value, min = 0, max = 1) { + return Math.min(Math.max(value, min), max) +} + +function roundCurrency(value) { + return Number(value.toFixed(2)) +} + +function sum(values) { + return values.reduce((total, value) => total + value, 0) +} + +function riskTier(score) { + return RISK_THRESHOLDS.find((threshold) => score >= threshold.minScore).tier +} + +function accountRiskFactors(account) { + const computeBudget = account.computeBudgetUsd || 0 + const computeSpend = account.computeSpendUsd || 0 + const computeOverageRatio = computeBudget > 0 ? Math.max(computeSpend - computeBudget, 0) / computeBudget : 0 + const topupVelocityRatio = + (account.monthlyRecurringRevenueUsd || 0) > 0 + ? (account.topupValueLast24hUsd || 0) / account.monthlyRecurringRevenueUsd + : 0 + + return [ + { + id: "lost_chargebacks", + label: "Lost chargebacks in the last 180 days", + value: account.chargebacksLostLast180d || 0, + score: clamp((account.chargebacksLostLast180d || 0) / 3), + weight: 0.2, + }, + { + id: "open_disputes", + label: "Open disputes", + value: account.openDisputes || 0, + score: clamp((account.openDisputes || 0) / 2), + weight: 0.18, + }, + { + id: "payment_failures", + label: "Failed payments in the last 30 days", + value: account.paymentFailuresLast30d || 0, + score: clamp((account.paymentFailuresLast30d || 0) / 4), + weight: 0.16, + }, + { + id: "past_due", + label: "Invoice days past due", + value: account.invoiceDaysPastDue || 0, + score: clamp((account.invoiceDaysPastDue || 0) / 45), + weight: 0.14, + }, + { + id: "compute_overage", + label: "AI compute overage ratio", + value: Number(computeOverageRatio.toFixed(3)), + score: clamp(computeOverageRatio / 0.75), + weight: 0.14, + }, + { + id: "topup_velocity", + label: "Top-up value in last 24h vs MRR", + value: Number(topupVelocityRatio.toFixed(3)), + score: clamp(topupVelocityRatio / 0.5), + weight: 0.1, + }, + { + id: "licensing_export_risk", + label: "Highest licensing export privacy risk", + value: Math.max(...(account.licensingExports || []).map(licensingExportRiskScore), 0), + score: Math.max(...(account.licensingExports || []).map(licensingExportRiskScore), 0), + weight: 0.08, + }, + ] +} + +function scoreAccountRisk(account) { + const factors = accountRiskFactors(account) + return Number(sum(factors.map((factor) => factor.score * factor.weight)).toFixed(4)) +} + +function licensingExportRiskScore(exportItem) { + const consentRisk = clamp((0.95 - (exportItem.consentCoverage || 0)) / 0.95) + const aggregationRisk = clamp((10 - (exportItem.aggregationThreshold || 0)) / 10) + const anonymizationRisk = exportItem.anonymized ? 0 : 1 + return Number((consentRisk * 0.35 + aggregationRisk * 0.3 + anonymizationRisk * 0.35).toFixed(4)) +} + +function reviewLicensingExports(account) { + return (account.licensingExports || []).map((exportItem) => { + const blockers = [] + if (!exportItem.anonymized) blockers.push("not_anonymized") + if ((exportItem.consentCoverage || 0) < 0.95) blockers.push("consent_below_threshold") + if ((exportItem.aggregationThreshold || 0) < 10) blockers.push("aggregation_threshold_too_low") + + return { + id: exportItem.id, + valueUsd: exportItem.valueUsd || 0, + decision: blockers.length > 0 ? "hold" : "approve", + blockers, + riskScore: licensingExportRiskScore(exportItem), + } + }) +} + +function entitlementDecision(account, score) { + if ((account.openDisputes || 0) > 0 && score >= 0.6) return "hold_paid_entitlements" + if ((account.invoiceDaysPastDue || 0) >= 30) return "hold_new_usage" + if ((account.computeSpendUsd || 0) > (account.computeBudgetUsd || 0) * 1.5) return "review_compute_usage" + if ((account.paymentFailuresLast30d || 0) >= 2) return "require_payment_method_update" + return "allow" +} + +function reserveRecommendation(account, licensingReviews) { + const disputedExposure = (account.openDisputes || 0) * (account.averageDisputeValueUsd || 0) + const pastDueReserve = + (account.invoiceDaysPastDue || 0) > 0 ? (account.openInvoiceBalanceUsd || 0) * 0.5 : 0 + const chargebackReserve = (account.chargebacksLostLast180d || 0) * (account.averageDisputeValueUsd || 0) + const licensingReserve = sum( + licensingReviews + .filter((review) => review.decision === "hold") + .map((review) => review.valueUsd * Math.max(review.riskScore, 0.25)), + ) + + return roundCurrency(disputedExposure + pastDueReserve + chargebackReserve + licensingReserve) +} + +function recoveryActions(account, licensingReviews, score) { + const actions = [] + if ((account.paymentFailuresLast30d || 0) > 0) actions.push("Send payment-method refresh notice.") + if ((account.invoiceDaysPastDue || 0) >= 15) actions.push("Start institutional dunning sequence.") + if ((account.openDisputes || 0) > 0) actions.push("Prepare dispute evidence packet before recognizing revenue.") + if ((account.computeSpendUsd || 0) > (account.computeBudgetUsd || 0)) actions.push("Throttle AI compute overages until top-up clears.") + if ((account.topupPurchasesLast24h || 0) >= 3) actions.push("Review rapid top-up pattern for abuse or card testing.") + if (licensingReviews.some((review) => review.decision === "hold")) { + actions.push("Hold analytics licensing export until privacy blockers clear.") + } + if (score >= 0.8) actions.push("Escalate account to finance risk review.") + return actions +} + +function revenueAdjustments(account, reserveUsd, licensingReviews) { + const adjustments = [] + if (reserveUsd > 0) { + adjustments.push({ + type: "reserve", + amountUsd: reserveUsd, + reason: "Dispute, past-due, chargeback, or licensing hold exposure.", + }) + } + if ((account.invoiceDaysPastDue || 0) >= 30) { + adjustments.push({ + type: "defer_revenue", + amountUsd: roundCurrency((account.openInvoiceBalanceUsd || 0) * 0.5), + reason: "Invoice is 30+ days past due.", + }) + } + for (const review of licensingReviews.filter((item) => item.decision === "hold")) { + adjustments.push({ + type: "hold_license_revenue", + amountUsd: review.valueUsd, + reason: `Licensing export ${review.id} has privacy blockers.`, + }) + } + return adjustments +} + +function buildAccountGuard(account) { + const score = scoreAccountRisk(account) + const licensingReviews = reviewLicensingExports(account) + const reserveUsd = reserveRecommendation(account, licensingReviews) + const packet = { + accountId: account.id, + plan: account.plan, + monthlyRecurringRevenueUsd: account.monthlyRecurringRevenueUsd || 0, + riskScore: score, + riskTier: riskTier(score), + entitlementDecision: entitlementDecision(account, score), + riskFactors: accountRiskFactors(account).map((factor) => ({ + id: factor.id, + label: factor.label, + value: factor.value, + score: factor.score, + weight: factor.weight, + })), + licensingReviews, + reserveUsd, + revenueAdjustments: revenueAdjustments(account, reserveUsd, licensingReviews), + recoveryActions: recoveryActions(account, licensingReviews, score), + } + + return { + ...packet, + auditDigest: digest(packet), + } +} + +function buildPortfolioGuard(accounts, options = {}) { + const accountPackets = accounts.map(buildAccountGuard).sort((a, b) => b.riskScore - a.riskScore) + const packet = { + generatedFor: options.generatedFor || "finance_review", + accountCount: accountPackets.length, + heldAccounts: accountPackets.filter((account) => account.entitlementDecision !== "allow").length, + totalMonthlyRecurringRevenueUsd: roundCurrency(sum(accountPackets.map((account) => account.monthlyRecurringRevenueUsd))), + totalReserveUsd: roundCurrency(sum(accountPackets.map((account) => account.reserveUsd))), + criticalAccounts: accountPackets.filter((account) => account.riskTier === "critical").map((account) => account.accountId), + accounts: accountPackets, + } + + return { + ...packet, + auditDigest: digest(accountPackets.map((account) => account.auditDigest)), + } +} + +module.exports = { + buildAccountGuard, + buildPortfolioGuard, + digest, + licensingExportRiskScore, + scoreAccountRisk, + stableJson, +} diff --git a/revenue-dispute-guard/test.js b/revenue-dispute-guard/test.js new file mode 100644 index 0000000..e4cec42 --- /dev/null +++ b/revenue-dispute-guard/test.js @@ -0,0 +1,131 @@ +const assert = require("node:assert/strict") +const { + buildAccountGuard, + buildPortfolioGuard, + digest, + licensingExportRiskScore, + scoreAccountRisk, +} = require("./index") + +function healthyAccount() { + return { + id: "healthy", + plan: "lab", + monthlyRecurringRevenueUsd: 1000, + computeBudgetUsd: 800, + computeSpendUsd: 500, + licensingExports: [ + { + id: "safe-export", + valueUsd: 1200, + anonymized: true, + consentCoverage: 0.99, + aggregationThreshold: 30, + }, + ], + } +} + +function disputedAccount() { + return { + id: "disputed", + plan: "institution", + monthlyRecurringRevenueUsd: 5000, + openDisputes: 2, + chargebacksLostLast180d: 2, + averageDisputeValueUsd: 2500, + paymentFailuresLast30d: 4, + invoiceDaysPastDue: 45, + openInvoiceBalanceUsd: 10000, + computeBudgetUsd: 2000, + computeSpendUsd: 4100, + topupPurchasesLast24h: 4, + topupValueLast24hUsd: 4000, + licensingExports: [ + { + id: "unsafe-export", + valueUsd: 3000, + anonymized: false, + consentCoverage: 0.8, + aggregationThreshold: 4, + }, + ], + } +} + +function testLowRiskAccountAllowsEntitlements() { + const guard = buildAccountGuard(healthyAccount()) + + assert.equal(guard.riskTier, "low") + assert.equal(guard.entitlementDecision, "allow") + assert.equal(guard.reserveUsd, 0) + assert.equal(guard.licensingReviews[0].decision, "approve") +} + +function testDisputedAccountIsHeldAndReserved() { + const guard = buildAccountGuard(disputedAccount()) + + assert.equal(guard.riskTier, "critical") + assert.equal(guard.entitlementDecision, "hold_paid_entitlements") + assert.ok(guard.reserveUsd > 12000) + assert.ok(guard.recoveryActions.some((action) => action.includes("dispute evidence"))) +} + +function testTopupVelocityAddsAbuseReviewAction() { + const guard = buildAccountGuard({ + ...healthyAccount(), + id: "rapid-topup", + monthlyRecurringRevenueUsd: 500, + topupPurchasesLast24h: 5, + topupValueLast24hUsd: 1000, + }) + + assert.ok(guard.riskFactors.find((factor) => factor.id === "topup_velocity").score > 0) + assert.ok(guard.recoveryActions.some((action) => action.includes("rapid top-up"))) +} + +function testLicensingExportRiskBlocksUnsafeExports() { + const risk = licensingExportRiskScore({ + anonymized: false, + consentCoverage: 0.8, + aggregationThreshold: 3, + }) + const guard = buildAccountGuard(disputedAccount()) + + assert.ok(risk > 0.6) + assert.equal(guard.licensingReviews[0].decision, "hold") + assert.ok(guard.licensingReviews[0].blockers.includes("not_anonymized")) +} + +function testPortfolioTotalsAndSorting() { + const portfolio = buildPortfolioGuard([healthyAccount(), disputedAccount()]) + + assert.equal(portfolio.accountCount, 2) + assert.equal(portfolio.heldAccounts, 1) + assert.equal(portfolio.accounts[0].accountId, "disputed") + assert.ok(portfolio.totalReserveUsd > 12000) +} + +function testRiskAndDigestAreDeterministic() { + const first = scoreAccountRisk(disputedAccount()) + const second = scoreAccountRisk({ ...disputedAccount() }) + + assert.equal(first, second) + assert.equal(digest({ b: 2, a: 1 }), digest({ a: 1, b: 2 })) +} + +const tests = [ + testLowRiskAccountAllowsEntitlements, + testDisputedAccountIsHeldAndReserved, + testTopupVelocityAddsAbuseReviewAction, + testLicensingExportRiskBlocksUnsafeExports, + testPortfolioTotalsAndSorting, + testRiskAndDigestAreDeterministic, +] + +for (const test of tests) { + test() + console.log(`ok - ${test.name}`) +} + +console.log(`${tests.length} tests passed`)