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 00000000..ed84aaac Binary files /dev/null and b/sla-credit-revenue-guard/demo.mp4 differ 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");