Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions subscription-quota-rollover-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Subscription Quota Rollover Guard

This slice adds a dependency-free Revenue Infrastructure guard for subscription AI-compute or token quota rollover. It audits synthetic quota ledgers before invoice release so SCIBASE can avoid double billing, expired quota application, and unsupported carry-forward during plan changes.

The scope is intentionally distinct from existing same-issue submissions for proration, renewal notice, entitlement downgrade, usage replay/idempotency, prepaid credit breakage, committed drawdown, quote approval, invoice delivery, tax, collections, analytics-seat leakage, and payment authorization freshness.

## What It Checks

- Carry-forward units exceeding the plan rollover cap.
- Posted rollover totals that do not match prior-cycle unused quota.
- Duplicate rollover lot identifiers.
- Expired rollover lots applied to the current invoice.
- Applied units that do not reconcile to lot-level records.
- Plan downgrade carry limits.
- Unapproved negative quota adjustments.
- Overage line items that fail to subtract applied rollover.
- Missing finance hold when blockers exist.

## Reviewer Output

Running the demo creates:

- `reports/quota-rollover-report.json`
- `reports/quota-rollover-report.md`
- `reports/summary.svg`
- `reports/demo-script.txt`
- `reports/demo.gif`
- `reports/demo.mp4`

All inputs are synthetic. The guard uses no credentials, private customer data, payment processors, live billing systems, analytics APIs, or external services.

## Commands

```bash
npm test
npm run demo
npm run demo:video
```
25 changes: 25 additions & 0 deletions subscription-quota-rollover-guard/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { mkdir, writeFile } from "node:fs/promises";
import { auditQuotaRollover, buildFinanceMarkdown, buildSummarySvg } from "./index.js";
import { riskyQuotaLedger } from "./sample-data.js";

const reportsDir = new URL("./reports/", import.meta.url);
await mkdir(reportsDir, { recursive: true });

const report = auditQuotaRollover(riskyQuotaLedger);
await writeFile(new URL("quota-rollover-report.json", reportsDir), `${JSON.stringify(report, null, 2)}\n`);
await writeFile(new URL("quota-rollover-report.md", reportsDir), buildFinanceMarkdown(report));
await writeFile(new URL("summary.svg", reportsDir), buildSummarySvg(report));
await writeFile(
new URL("demo-script.txt", reportsDir),
[
"Demo: Subscription Quota Rollover Guard",
`Account: ${report.accountId}`,
`Billing cycle: ${report.billingCycle}`,
`Decision: ${report.decision}`,
`Risk score: ${report.riskScore}/100`,
`Blockers: ${report.summary.blockCount}`,
"Finance action: hold invoice until duplicate/expired rollover, downgrade caps, and overage units reconcile.",
].join("\n"),
);

console.log(JSON.stringify(report.summary, null, 2));
45 changes: 45 additions & 0 deletions subscription-quota-rollover-guard/demo_video.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from pathlib import Path

import imageio.v3 as iio
import numpy as np
from PIL import Image, ImageDraw, ImageFont


ROOT = Path(__file__).resolve().parent
REPORTS = ROOT / "reports"
REPORTS.mkdir(exist_ok=True)


def font(size):
for name in ("arial.ttf", "segoeui.ttf"):
try:
return ImageFont.truetype(name, size)
except OSError:
pass
return ImageFont.load_default()


slides = [
("Subscription Quota Rollover Guard", "Synthetic revenue control for SCIBASE #20"),
("Decision", "hold-invoice · risk score 100/100"),
("Detected", "expired lots · duplicate carry-forward · downgrade cap breach"),
("Finance Action", "Hold invoice and reconcile overage before release"),
]

frames = []
for title, subtitle in slides:
image = Image.new("RGB", (960, 540), "#111827")
draw = ImageDraw.Draw(image)
draw.rectangle((48, 58, 912, 482), outline="#374151", width=3)
draw.text((82, 132), title, fill="#f9fafb", font=font(42))
draw.text((82, 214), subtitle, fill="#d1d5db", font=font(24))
draw.rectangle((82, 340, 690, 380), fill="#dc2626")
draw.text((82, 410), "Synthetic data only. No payment processor or live billing calls.", fill="#9ca3af", font=font(20))
frames.extend([image] * 14)

gif_path = REPORTS / "demo.gif"
frames[0].save(gif_path, save_all=True, append_images=frames[1:], duration=120, loop=0)
mp4_path = REPORTS / "demo.mp4"
iio.imwrite(mp4_path, [np.asarray(frame) for frame in frames], fps=8, codec="libx264")
print(f"wrote {gif_path}")
print(f"wrote {mp4_path}")
254 changes: 254 additions & 0 deletions subscription-quota-rollover-guard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
const SEVERITY_ORDER = { block: 3, warn: 2, info: 1 };

function finding(code, severity, message, evidence, remediation) {
return { code, severity, message, evidence, remediation };
}

function cycleToNumber(cycle) {
const match = String(cycle || "").match(/^(\d{4})-(\d{2})$/);
if (!match) return Number.NaN;
return Number(match[1]) * 12 + Number(match[2]);
}

function sum(values) {
return values.reduce((total, value) => total + Number(value || 0), 0);
}

export function auditQuotaRollover(ledger) {
if (!ledger || typeof ledger !== "object") {
throw new TypeError("quota ledger must be an object");
}

const issues = [];
const plan = ledger.plan || {};
const prior = ledger.priorCycle || {};
const current = ledger.currentCycle || {};
const lots = ledger.rolloverLots || [];
const adjustments = ledger.adjustments || [];
const invoice = ledger.invoicePreview || {};

if (prior.carryForwardUnits > plan.rolloverCapUnits) {
issues.push(
finding(
"ROLLOVER_CAP_EXCEEDED",
"block",
"Prior-cycle unused quota exceeds the contractual rollover cap.",
{
carryForwardUnits: prior.carryForwardUnits,
rolloverCapUnits: plan.rolloverCapUnits,
},
"Clamp carry-forward units to the plan cap and regenerate the invoice preview before release.",
),
);
}

if (prior.postedRolloverUnits !== prior.carryForwardUnits) {
issues.push(
finding(
"POSTED_ROLLOVER_MISMATCH",
"block",
"Posted rollover units do not match the calculated prior-cycle carry-forward units.",
{
postedRolloverUnits: prior.postedRolloverUnits,
carryForwardUnits: prior.carryForwardUnits,
},
"Reconcile the quota ledger before overage or credit lines are posted.",
),
);
}

const seenLots = new Set();
for (const lot of lots) {
if (seenLots.has(lot.lotId)) {
issues.push(
finding(
"DUPLICATE_ROLLOVER_LOT",
"block",
"The same rollover lot appears more than once and could be carried forward twice.",
{ lotId: lot.lotId },
"Deduplicate rollover lots by immutable lot id before invoice generation.",
),
);
}
seenLots.add(lot.lotId);

if (cycleToNumber(lot.expiresAfterCycle) < cycleToNumber(ledger.billingCycle) && lot.appliedUnits > 0) {
issues.push(
finding(
"EXPIRED_ROLLOVER_APPLIED",
"block",
"Expired quota was applied to the current invoice.",
{
lotId: lot.lotId,
expiresAfterCycle: lot.expiresAfterCycle,
billingCycle: ledger.billingCycle,
appliedUnits: lot.appliedUnits,
},
"Remove expired lots from available balance and route the invoice to finance review.",
),
);
}
}

const totalAppliedFromLots = sum(lots.map((lot) => lot.appliedUnits));
if (totalAppliedFromLots !== current.rolloverAppliedUnits) {
issues.push(
finding(
"ROLLOVER_APPLICATION_MISMATCH",
"block",
"Applied rollover units do not equal the sum of applied rollover lots.",
{
totalAppliedFromLots,
rolloverAppliedUnits: current.rolloverAppliedUnits,
},
"Regenerate applied quota from lot-level records instead of invoice-line totals.",
),
);
}

if (current.planChangedFrom !== current.planChangedTo && current.rolloverAppliedUnits > plan.downgradeCarryLimitUnits) {
issues.push(
finding(
"DOWNGRADE_CARRY_LIMIT_EXCEEDED",
"block",
"A downgraded account is applying more carried quota than the downgrade policy allows.",
{
planChangedFrom: current.planChangedFrom,
planChangedTo: current.planChangedTo,
rolloverAppliedUnits: current.rolloverAppliedUnits,
downgradeCarryLimitUnits: plan.downgradeCarryLimitUnits,
},
"Apply the downgrade carry limit or hold the invoice for contract review.",
),
);
}

const unapprovedNegativeAdjustments = adjustments.filter((adjustment) => adjustment.units < 0 && !adjustment.approvedByFinance);
if (unapprovedNegativeAdjustments.length > 0) {
issues.push(
finding(
"UNAPPROVED_NEGATIVE_ADJUSTMENT",
"warn",
"Negative quota adjustments exist without finance approval.",
{ adjustmentIds: unapprovedNegativeAdjustments.map((adjustment) => adjustment.adjustmentId) },
"Require finance approval before negative units affect quota or invoice totals.",
),
);
}

const expectedOverageUnits = Math.max(0, current.usageUnits - current.includedUnits - current.rolloverAppliedUnits);
if (invoice.lineItems?.some((line) => line.code === "ai-compute-overage") && current.invoiceOverageUnits !== expectedOverageUnits) {
issues.push(
finding(
"OVERAGE_DOUBLE_BILLING_RISK",
"block",
"Invoice overage units do not subtract applied rollover correctly.",
{
usageUnits: current.usageUnits,
includedUnits: current.includedUnits,
rolloverAppliedUnits: current.rolloverAppliedUnits,
invoiceOverageUnits: current.invoiceOverageUnits,
expectedOverageUnits,
},
"Recompute overage after rollover application to prevent double billing.",
),
);
}

if (issues.some((issue) => issue.severity === "block") && invoice.reviewerHold !== true) {
issues.push(
finding(
"MISSING_FINANCE_HOLD",
"block",
"The invoice preview is not on hold despite quota rollover blockers.",
{ reviewerHold: invoice.reviewerHold },
"Place the invoice on finance hold until quota carry-forward and overage lines reconcile.",
),
);
}

const blockCount = issues.filter((issue) => issue.severity === "block").length;
const warnCount = issues.filter((issue) => issue.severity === "warn").length;
return {
accountId: ledger.accountId,
billingCycle: ledger.billingCycle,
decision: blockCount > 0 ? "hold-invoice" : warnCount > 0 ? "finance-review" : "ready-to-invoice",
riskScore: Math.min(100, blockCount * 24 + warnCount * 9),
summary: {
blockCount,
warnCount,
findingCount: issues.length,
appliedRolloverUnits: current.rolloverAppliedUnits || 0,
expectedOverageUnits,
},
findings: issues.sort((a, b) => SEVERITY_ORDER[b.severity] - SEVERITY_ORDER[a.severity] || a.code.localeCompare(b.code)),
};
}

export function buildFinanceMarkdown(report) {
const lines = [
`# Subscription Quota Rollover Guard: ${report.accountId}`,
"",
`Billing cycle: **${report.billingCycle}**`,
`Decision: **${report.decision}**`,
`Risk score: **${report.riskScore}/100**`,
"",
`Findings: ${report.summary.blockCount} blockers, ${report.summary.warnCount} warnings.`,
`Applied rollover units: ${report.summary.appliedRolloverUnits}`,
`Expected overage units: ${report.summary.expectedOverageUnits}`,
"",
];

for (const item of report.findings) {
lines.push(`## ${item.severity.toUpperCase()}: ${item.code}`);
lines.push(item.message);
lines.push("");
lines.push(`Evidence: \`${JSON.stringify(item.evidence)}\``);
lines.push("");
lines.push(`Remediation: ${item.remediation}`);
lines.push("");
}

if (report.findings.length === 0) {
lines.push("No rollover blockers or warnings were detected in the synthetic ledger.");
lines.push("");
}

return lines.join("\n");
}

export function buildSummarySvg(report) {
const color = report.decision === "hold-invoice" ? "#dc2626" : report.decision === "finance-review" ? "#d97706" : "#16a34a";
return `<svg xmlns="http://www.w3.org/2000/svg" width="920" height="420" viewBox="0 0 920 420" role="img" aria-label="Subscription quota rollover guard summary">
<rect width="920" height="420" fill="#111827"/>
<text x="52" y="68" fill="#f9fafb" font-family="Arial, sans-serif" font-size="30" font-weight="700">Subscription Quota Rollover Guard</text>
<text x="52" y="108" fill="#d1d5db" font-family="Arial, sans-serif" font-size="17">${escapeXml(report.accountId)} · ${escapeXml(report.billingCycle)}</text>
<rect x="52" y="144" width="760" height="34" rx="5" fill="#1f2937" stroke="#374151"/>
<rect x="52" y="144" width="${Math.min(760, report.riskScore * 7.6)}" height="34" rx="5" fill="${color}"/>
<text x="835" y="169" fill="#f9fafb" font-family="Arial, sans-serif" font-size="20" text-anchor="end">${report.riskScore}/100</text>
<g transform="translate(52 230)">
<rect width="230" height="110" rx="8" fill="#1f2937"/>
<text x="24" y="44" fill="#fecaca" font-family="Arial, sans-serif" font-size="30" font-weight="700">${report.summary.blockCount}</text>
<text x="24" y="78" fill="#d1d5db" font-family="Arial, sans-serif" font-size="16">invoice blockers</text>
</g>
<g transform="translate(318 230)">
<rect width="230" height="110" rx="8" fill="#1f2937"/>
<text x="24" y="44" fill="#fde68a" font-family="Arial, sans-serif" font-size="30" font-weight="700">${report.summary.appliedRolloverUnits}</text>
<text x="24" y="78" fill="#d1d5db" font-family="Arial, sans-serif" font-size="16">applied rollover units</text>
</g>
<g transform="translate(584 230)">
<rect width="230" height="110" rx="8" fill="#1f2937"/>
<text x="24" y="44" fill="#bfdbfe" font-family="Arial, sans-serif" font-size="30" font-weight="700">${report.summary.expectedOverageUnits}</text>
<text x="24" y="78" fill="#d1d5db" font-family="Arial, sans-serif" font-size="16">expected overage units</text>
</g>
</svg>
`;
}

function escapeXml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
12 changes: 12 additions & 0 deletions subscription-quota-rollover-guard/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "subscription-quota-rollover-guard",
"version": "1.0.0",
"description": "Synthetic quota rollover audit guard for SCIBASE revenue infrastructure.",
"type": "module",
"private": true,
"scripts": {
"test": "node test.js",
"demo": "node demo.js",
"demo:video": "python demo_video.py"
}
}
7 changes: 7 additions & 0 deletions subscription-quota-rollover-guard/reports/demo-script.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Demo: Subscription Quota Rollover Guard
Account: LAB-QUOTA-1042
Billing cycle: 2026-06
Decision: hold-invoice
Risk score: 100/100
Blockers: 6
Finance action: hold invoice until duplicate/expired rollover, downgrade caps, and overage units reconcile.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading