From 789fa822b5098fdd73069887553e3d60125e773f Mon Sep 17 00:00:00 2001 From: HunterCML <5335527+HunterCML@users.noreply.github.com> Date: Tue, 19 May 2026 20:25:43 -0500 Subject: [PATCH] Add SLA credit revenue guard --- sla-credit-revenue-guard/README.md | 28 ++ sla-credit-revenue-guard/acceptance-notes.md | 12 + sla-credit-revenue-guard/demo.js | 41 +++ sla-credit-revenue-guard/demo.mp4 | Bin 0 -> 47310 bytes sla-credit-revenue-guard/demo.svg | 23 ++ sla-credit-revenue-guard/index.js | 314 +++++++++++++++++++ sla-credit-revenue-guard/requirements-map.md | 18 ++ sla-credit-revenue-guard/test.js | 119 +++++++ 8 files changed, 555 insertions(+) create mode 100644 sla-credit-revenue-guard/README.md create mode 100644 sla-credit-revenue-guard/acceptance-notes.md create mode 100644 sla-credit-revenue-guard/demo.js create mode 100644 sla-credit-revenue-guard/demo.mp4 create mode 100644 sla-credit-revenue-guard/demo.svg create mode 100644 sla-credit-revenue-guard/index.js create mode 100644 sla-credit-revenue-guard/requirements-map.md create mode 100644 sla-credit-revenue-guard/test.js diff --git a/sla-credit-revenue-guard/README.md b/sla-credit-revenue-guard/README.md new file mode 100644 index 00000000..9d7d0b74 --- /dev/null +++ b/sla-credit-revenue-guard/README.md @@ -0,0 +1,28 @@ +# SLA Credit Revenue Guard + +This module adds a focused Revenue Infrastructure slice for issue #20. It evaluates institutional contracts, AI compute usage, service incidents, and anonymized licensing exports before invoice release. + +It covers: + +- tiered subscription plan rules for individual, lab, and institutional accounts +- AI compute usage metering, included quotas, and overage invoice lines +- SLA uptime measurement and capped service-credit calculations +- institutional invoice packets with approval and postmortem warnings +- licensing/API export readiness checks that block private content and weak aggregation +- stable audit digests for finance review and revenue operations traceability + +This is not another generic billing ledger, tax module, renewal true-up, margin guard, pricing experiment, or procurement workflow. The focus is the operational revenue moment when an outage or service-level breach must be converted into an auditable credit before billing AI compute and institutional licensing access. + +## Local Validation + +```sh +node sla-credit-revenue-guard/test.js +node sla-credit-revenue-guard/demo.js +``` + +## Demo Evidence + +- [demo.mp4](demo.mp4) shows the problem, implementation scope, invoice packet, and validation commands. +- [demo.svg](demo.svg) provides a static finance dashboard preview. +- [requirements-map.md](requirements-map.md) maps the implementation to issue #20. +- [acceptance-notes.md](acceptance-notes.md) lists reviewer checks. diff --git a/sla-credit-revenue-guard/acceptance-notes.md b/sla-credit-revenue-guard/acceptance-notes.md new file mode 100644 index 00000000..44e624cc --- /dev/null +++ b/sla-credit-revenue-guard/acceptance-notes.md @@ -0,0 +1,12 @@ +# Acceptance Notes + +Reviewer checks: + +1. Run `node sla-credit-revenue-guard/test.js`. +2. Run `node sla-credit-revenue-guard/demo.js`. +3. Confirm institutional usage over quota creates an AI compute overage line. +4. Confirm SLA incidents below the contracted uptime produce capped service credits. +5. Confirm private or under-aggregated licensing exports hold the invoice. +6. Confirm audit digests remain stable for the same input. + +The module is dependency-free and uses synthetic revenue events only, so it can be reviewed without payment credentials, cloud accounts, or private customer data. diff --git a/sla-credit-revenue-guard/demo.js b/sla-credit-revenue-guard/demo.js new file mode 100644 index 00000000..7ce40a57 --- /dev/null +++ b/sla-credit-revenue-guard/demo.js @@ -0,0 +1,41 @@ +"use strict"; + +const { evaluateRevenueGuard } = require("./index"); + +const result = evaluateRevenueGuard({ + period: { id: "2026-05", totalMinutes: 43_200 }, + contracts: [ + { + customerId: "midwest-university", + plan: "institutional", + monthlyBaseCents: 275000, + includedComputeUnits: 50000, + overageUnitCents: 7, + }, + ], + usageEvents: [ + { customerId: "midwest-university", computeUnits: 42_500 }, + { customerId: "midwest-university", computeUnits: 11_000 }, + ], + incidents: [ + { + id: "inc-ai-review-outage", + impactMinutes: 75, + impactedCustomerIds: ["midwest-university"], + postmortemReady: true, + }, + ], + licensingExports: [ + { + customerId: "midwest-university", + billableCents: 80000, + minimumAggregationCount: 100, + privateContentIncluded: false, + customerNoticeReady: true, + }, + ], +}); + +console.log("SLA credit revenue guard demo"); +console.log(JSON.stringify(result.dashboard, null, 2)); +console.log(JSON.stringify(result.packets[0], null, 2)); diff --git a/sla-credit-revenue-guard/demo.mp4 b/sla-credit-revenue-guard/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..ed84aaacdc4c7d296bb2d358511c063dc69df25f GIT binary patch literal 47310 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 literal 0 HcmV?d00001 diff --git a/sla-credit-revenue-guard/demo.svg b/sla-credit-revenue-guard/demo.svg new file mode 100644 index 00000000..86fa0c2b --- /dev/null +++ b/sla-credit-revenue-guard/demo.svg @@ -0,0 +1,23 @@ + + SLA Credit Revenue Guard demo dashboard + Static dashboard preview for institutional revenue invoice controls. + + + SLA Credit Revenue Guard + Issue #20 revenue infrastructure slice + + Invoice Ready + 1 + + Service Credits + $550 + + Compute Overage + 3500 + + Finance packet + - Base subscription fee plus AI compute overage line + - SLA uptime breach converted to a capped service credit + - Licensing export blocks private content and weak aggregation + Validation: node sla-credit-revenue-guard/test.js && node sla-credit-revenue-guard/demo.js + diff --git a/sla-credit-revenue-guard/index.js b/sla-credit-revenue-guard/index.js new file mode 100644 index 00000000..e9848f08 --- /dev/null +++ b/sla-credit-revenue-guard/index.js @@ -0,0 +1,314 @@ +"use strict"; + +const crypto = require("node:crypto"); + +const PLAN_RULES = { + individualPro: { + monthlyBaseCents: 4900, + includedComputeUnits: 250, + overageUnitCents: 18, + slaPercent: 0, + monthlyCreditCapPercent: 0, + approvalThresholdCents: 0, + }, + lab: { + monthlyBaseCents: 19900, + includedComputeUnits: 2500, + overageUnitCents: 12, + slaPercent: 99.5, + monthlyCreditCapPercent: 10, + approvalThresholdCents: 15000, + }, + institutional: { + monthlyBaseCents: 250000, + includedComputeUnits: 50000, + overageUnitCents: 7, + slaPercent: 99.9, + monthlyCreditCapPercent: 20, + approvalThresholdCents: 50000, + }, +}; + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function stableDigest(value) { + return crypto.createHash("sha256").update(stableStringify(value)).digest("hex"); +} + +function asArray(value) { + return Array.isArray(value) ? value : []; +} + +function cents(value) { + return Math.round(Number(value || 0)); +} + +function formatUsd(centsValue) { + return `$${(centsValue / 100).toFixed(2)}`; +} + +function normalizePlan(plan) { + const planKey = plan || "individualPro"; + if (!PLAN_RULES[planKey]) { + throw new Error(`Unknown plan: ${planKey}`); + } + return planKey; +} + +function getMonthMinutes(period) { + if (period && Number.isFinite(period.totalMinutes)) return period.totalMinutes; + return 30 * 24 * 60; +} + +function sumIncidentMinutes(incidents) { + return asArray(incidents).reduce((sum, incident) => sum + Number(incident.impactMinutes || 0), 0); +} + +function calculateUptimePercent(period, incidents) { + const totalMinutes = getMonthMinutes(period); + const incidentMinutes = sumIncidentMinutes(incidents); + return Math.max(0, ((totalMinutes - incidentMinutes) / totalMinutes) * 100); +} + +function calculateServiceCredit(contract, period, incidents) { + const plan = PLAN_RULES[normalizePlan(contract.plan)]; + if (!plan.slaPercent) { + return { + eligible: false, + uptimePercent: calculateUptimePercent(period, incidents), + creditCents: 0, + reason: "Plan does not include an SLA credit.", + }; + } + + const uptimePercent = calculateUptimePercent(period, incidents); + if (uptimePercent >= plan.slaPercent) { + return { + eligible: false, + uptimePercent, + creditCents: 0, + reason: "Measured uptime meets the contracted SLA.", + }; + } + + const baseAmount = cents(contract.monthlyBaseCents || plan.monthlyBaseCents); + const gap = plan.slaPercent - uptimePercent; + const rawCreditPercent = Math.min(plan.monthlyCreditCapPercent, Math.ceil(gap * 4)); + const creditCents = Math.round(baseAmount * (rawCreditPercent / 100)); + + return { + eligible: true, + uptimePercent, + creditCents, + creditPercent: rawCreditPercent, + reason: `Uptime ${uptimePercent.toFixed(3)}% is below ${plan.slaPercent}% SLA.`, + }; +} + +function meterCompute(contract, usageEvents) { + const plan = PLAN_RULES[normalizePlan(contract.plan)]; + const units = asArray(usageEvents).reduce((sum, event) => sum + Number(event.computeUnits || 0), 0); + const includedUnits = Number(contract.includedComputeUnits || plan.includedComputeUnits); + const overageUnits = Math.max(0, units - includedUnits); + const overageUnitCents = cents(contract.overageUnitCents || plan.overageUnitCents); + + return { + units, + includedUnits, + overageUnits, + overageUnitCents, + overageCents: overageUnits * overageUnitCents, + }; +} + +function validateLicensingExport(licensingExport) { + if (!licensingExport) { + return { + ready: false, + findings: [ + { + severity: "warning", + code: "missing-licensing-export", + message: "No anonymized licensing API export metadata was attached to the revenue packet.", + }, + ], + }; + } + + const findings = []; + if (licensingExport.privateContentIncluded) { + findings.push({ + severity: "blocker", + code: "private-content-in-export", + message: "Licensing export includes private project content and must not be billed or shipped.", + }); + } + if (Number(licensingExport.minimumAggregationCount || 0) < 25) { + findings.push({ + severity: "blocker", + code: "aggregation-threshold-too-low", + message: "Licensing export aggregation threshold is below the institutional minimum of 25.", + }); + } + if (!licensingExport.customerNoticeReady) { + findings.push({ + severity: "warning", + code: "customer-notice-missing", + message: "Customer notice for licensing/API analytics access is not ready.", + }); + } + + return { + ready: findings.every((finding) => finding.severity !== "blocker"), + findings, + }; +} + +function buildInvoicePacket(input) { + const contract = input.contract; + const plan = PLAN_RULES[normalizePlan(contract.plan)]; + const baseCents = cents(contract.monthlyBaseCents || plan.monthlyBaseCents); + const compute = meterCompute(contract, input.usageEvents); + const serviceCredit = calculateServiceCredit(contract, input.period, input.incidents); + const licensing = validateLicensingExport(input.licensingExport); + + const lines = [ + { + code: "subscription-base", + description: `${contract.plan} subscription base fee`, + amountCents: baseCents, + }, + ]; + + if (compute.overageCents > 0) { + lines.push({ + code: "ai-compute-overage", + description: `${compute.overageUnits} AI compute units over included quota`, + amountCents: compute.overageCents, + }); + } + + if (serviceCredit.creditCents > 0) { + lines.push({ + code: "sla-service-credit", + description: `Service credit for ${serviceCredit.reason}`, + amountCents: -serviceCredit.creditCents, + }); + } + + if (input.licensingExport?.billableCents) { + lines.push({ + code: "licensing-api-access", + description: "Anonymized analytics licensing/API access", + amountCents: cents(input.licensingExport.billableCents), + }); + } + + const findings = [...licensing.findings]; + const approvalThresholdCents = cents(contract.approvalThresholdCents || plan.approvalThresholdCents); + + if (serviceCredit.creditCents > approvalThresholdCents && approvalThresholdCents > 0) { + findings.push({ + severity: "warning", + code: "finance-approval-needed", + message: `${formatUsd(serviceCredit.creditCents)} SLA credit exceeds automatic approval threshold.`, + }); + } + + for (const incident of asArray(input.incidents)) { + if (!incident.postmortemReady) { + findings.push({ + severity: "warning", + code: "postmortem-missing", + message: `${incident.id} needs customer-ready incident evidence before sending the invoice adjustment.`, + }); + } + } + + const totalCents = lines.reduce((sum, line) => sum + line.amountCents, 0); + const blockers = findings.filter((finding) => finding.severity === "blocker"); + + const packet = { + customerId: contract.customerId, + plan: contract.plan, + period: input.period?.id || "current", + compute, + serviceCredit, + licensingReady: licensing.ready, + lines, + totalCents, + total: formatUsd(totalCents), + decision: blockers.length > 0 ? "hold" : "invoice-ready", + findings, + }; + + return { + ...packet, + auditDigest: stableDigest({ + customerId: packet.customerId, + period: packet.period, + lines: packet.lines, + totalCents: packet.totalCents, + findings: packet.findings.map((finding) => finding.code), + }), + }; +} + +function evaluateRevenueGuard(input) { + const packets = asArray(input.contracts).map((contract) => + buildInvoicePacket({ + contract, + period: input.period, + usageEvents: asArray(input.usageEvents).filter((event) => event.customerId === contract.customerId), + incidents: asArray(input.incidents).filter((incident) => + asArray(incident.impactedCustomerIds).includes(contract.customerId), + ), + licensingExport: asArray(input.licensingExports).find((exportRow) => exportRow.customerId === contract.customerId), + }), + ); + + const dashboard = { + invoiceReady: packets.filter((packet) => packet.decision === "invoice-ready").length, + held: packets.filter((packet) => packet.decision === "hold").length, + totalBilledCents: packets.reduce((sum, packet) => sum + packet.totalCents, 0), + totalCreditsCents: packets.reduce( + (sum, packet) => sum + Math.abs(packet.lines.filter((line) => line.amountCents < 0).reduce((lineSum, line) => lineSum + line.amountCents, 0)), + 0, + ), + blockerCount: packets.flatMap((packet) => packet.findings).filter((finding) => finding.severity === "blocker").length, + warningCount: packets.flatMap((packet) => packet.findings).filter((finding) => finding.severity === "warning").length, + }; + + return { + dashboard: { + ...dashboard, + totalBilled: formatUsd(dashboard.totalBilledCents), + totalCredits: formatUsd(dashboard.totalCreditsCents), + }, + packets, + auditRoot: stableDigest(packets.map((packet) => packet.auditDigest).sort()), + }; +} + +module.exports = { + PLAN_RULES, + buildInvoicePacket, + calculateServiceCredit, + calculateUptimePercent, + evaluateRevenueGuard, + formatUsd, + meterCompute, + stableDigest, + validateLicensingExport, +}; diff --git a/sla-credit-revenue-guard/requirements-map.md b/sla-credit-revenue-guard/requirements-map.md new file mode 100644 index 00000000..2c4f8305 --- /dev/null +++ b/sla-credit-revenue-guard/requirements-map.md @@ -0,0 +1,18 @@ +# Requirements Map + +| Issue #20 requirement | Implementation coverage | +| --- | --- | +| Tiered subscription billing | `PLAN_RULES` defines individual, lab, and institutional plan pricing, SLA, quotas, overages, caps, and approval thresholds. | +| Institutional licenses | Invoice packets are grouped by customer contract and include institutional SLA/approval behavior. | +| AI compute billing | `meterCompute()` turns usage events into quota and overage invoice lines. | +| Transparent quotas and usage meters | Each packet reports compute units, included units, overage units, unit price, and overage amount. | +| Institutional invoicing | `buildInvoicePacket()` creates base fee, overage, SLA credit, and licensing/API lines with a release decision. | +| Licensing APIs and analytics | `validateLicensingExport()` blocks private content, enforces aggregation thresholds, and tracks customer notice readiness. | +| Predictable recurring revenue controls | SLA credits are capped by plan and approval thresholds so finance can release or hold invoice adjustments. | +| Auditability | Every invoice packet and the dashboard root include stable SHA-256 digests. | + +## Non-goals + +- No live Stripe, PayPal, ERP, or bank integration. +- No real customer or private research data. +- No attempt to replace a full revenue-recognition close engine. diff --git a/sla-credit-revenue-guard/test.js b/sla-credit-revenue-guard/test.js new file mode 100644 index 00000000..0c46844b --- /dev/null +++ b/sla-credit-revenue-guard/test.js @@ -0,0 +1,119 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const { + buildInvoicePacket, + calculateServiceCredit, + calculateUptimePercent, + evaluateRevenueGuard, + formatUsd, + meterCompute, + stableDigest, + validateLicensingExport, +} = require("./index"); + +const period = { id: "2026-05", totalMinutes: 43_200 }; +const institutionalContract = { + customerId: "university-health", + plan: "institutional", + monthlyBaseCents: 300000, + includedComputeUnits: 50000, + overageUnitCents: 6, +}; + +const incidents = [ + { + id: "inc-2026-05-18", + impactMinutes: 90, + impactedCustomerIds: ["university-health"], + postmortemReady: true, + }, +]; + +const uptime = calculateUptimePercent(period, incidents); +assert.ok(uptime < 99.9); + +const credit = calculateServiceCredit(institutionalContract, period, incidents); +assert.equal(credit.eligible, true); +assert.ok(credit.creditCents > 0); +assert.ok(credit.reason.includes("below")); + +const compute = meterCompute(institutionalContract, [ + { customerId: "university-health", computeUnits: 49_000 }, + { customerId: "university-health", computeUnits: 3_000 }, +]); +assert.equal(compute.units, 52_000); +assert.equal(compute.overageUnits, 2_000); +assert.equal(compute.overageCents, 12000); + +const packet = buildInvoicePacket({ + contract: institutionalContract, + period, + incidents, + usageEvents: [ + { customerId: "university-health", computeUnits: 49_000 }, + { customerId: "university-health", computeUnits: 3_000 }, + ], + licensingExport: { + customerId: "university-health", + billableCents: 65000, + minimumAggregationCount: 50, + privateContentIncluded: false, + customerNoticeReady: true, + }, +}); + +assert.equal(packet.decision, "invoice-ready"); +assert.ok(packet.lines.some((line) => line.code === "sla-service-credit" && line.amountCents < 0)); +assert.ok(packet.lines.some((line) => line.code === "ai-compute-overage")); +assert.ok(packet.lines.some((line) => line.code === "licensing-api-access")); +assert.match(packet.auditDigest, /^[a-f0-9]{64}$/); + +const unsafeLicensing = validateLicensingExport({ + customerId: "agency-1", + minimumAggregationCount: 8, + privateContentIncluded: true, +}); +assert.equal(unsafeLicensing.ready, false); +assert.equal(unsafeLicensing.findings.filter((finding) => finding.severity === "blocker").length, 2); + +const guard = evaluateRevenueGuard({ + period, + contracts: [ + institutionalContract, + { + customerId: "solo-researcher", + plan: "individualPro", + monthlyBaseCents: 4900, + includedComputeUnits: 250, + }, + ], + usageEvents: [ + { customerId: "university-health", computeUnits: 52_000 }, + { customerId: "solo-researcher", computeUnits: 310 }, + ], + incidents, + licensingExports: [ + { + customerId: "university-health", + billableCents: 65000, + minimumAggregationCount: 50, + privateContentIncluded: false, + customerNoticeReady: true, + }, + { + customerId: "solo-researcher", + minimumAggregationCount: 4, + privateContentIncluded: true, + }, + ], +}); + +assert.equal(guard.dashboard.invoiceReady, 1); +assert.equal(guard.dashboard.held, 1); +assert.ok(guard.dashboard.blockerCount >= 2); +assert.ok(guard.dashboard.totalCreditsCents > 0); +assert.equal(formatUsd(12345), "$123.45"); +assert.equal(stableDigest({ b: 2, a: 1 }), stableDigest({ a: 1, b: 2 })); + +console.log("sla credit revenue guard tests passed");