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
46 changes: 46 additions & 0 deletions revenue-usage-reconciliation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Revenue Usage Reconciliation

This module adds a focused revenue-ops validation layer for SCIBASE revenue infrastructure.
It reconciles plan entitlements, compute usage, top-ups, invoice totals, and anonymized analytics licensing exports so reviewers can catch undercharges, overcharges, refund risk, and unsafe licensing exports before closing a billing cycle.

It is intentionally dependency-free and can be run with stock Node.js.

## What It Covers

- Tiered plan quotas and included subscription charges.
- AI compute usage meters by capability.
- Top-up credit reconciliation before overage calculation.
- Invoice comparison against expected charges.
- Undercharge and overcharge anomaly detection.
- Anonymized licensing export safety checks.
- Revenue health reports with high-risk account lists and audit hashes.
- Entitlement regression matrix for month-over-month review.

## Demo

```bash
npm run demo
```

The demo prints a report with one clean lab account, one undercharged institutional account, and one unsafe licensing export.
Text-only demo evidence is included in `docs/demo-transcript.md` for reviewers who prefer not to inspect the GIF.

## Verification

```bash
npm run check
npm test
npm run demo
```

## Files

- `src/reconciliation.js` - core reconciliation, anomaly, health report, and regression logic.
- `test/reconciliation.test.js` - focused tests for clean billing, undercharges, licensing risk, aggregation, and regressions.
- `scripts/demo.js` - CLI demo with sample subscription, usage, invoice, and licensing data.
- `docs/issue-20-requirement-map.md` - mapping from issue requirements to implementation evidence.
- `docs/demo-transcript.md` - text-only reviewer evidence for the demo scenario.

## AI-Assisted Disclosure

This contribution was produced with AI assistance and manually verified before submission.
24 changes: 24 additions & 0 deletions revenue-usage-reconciliation/docs/demo-transcript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Revenue Usage Reconciliation Demo Transcript

This transcript gives reviewers a text-only demo artifact for issue #20 in addition to the GIF.

## Command

```bash
npm run demo
```

## Expected Highlights

- Report status is `needs-revenue-ops-review`.
- Total expected revenue is `313000` cents.
- Total actual invoice revenue is `309800` cents.
- Delta is `-3200` cents.
- Two anomalies are reported.
- `institute-undercharged` is flagged for a medium-severity undercharge.
- `agency-export-risk` is flagged for a high-severity unsafe licensing export.
- The regression matrix keeps `lab-clean` clean and marks new risky accounts as review-required.

## Reviewer Value

The demo proves this module can catch revenue leakage, refund-risk style billing mismatches, and unsafe analytics licensing exports before a billing cycle is closed.
Binary file added revenue-usage-reconciliation/docs/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions revenue-usage-reconciliation/docs/issue-20-requirement-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Issue #20 Requirement Map

This module is a focused revenue operations slice for the SCIBASE revenue infrastructure bounty. It is not another payment provider mock; it verifies whether usage, entitlements, invoices, top-ups, and analytics licensing exports reconcile cleanly before the month is closed.

| Issue #20 requirement | Implementation evidence |
| --- | --- |
| Tiered subscription billing | `normalizeAccount` models plan name, included quota, included price, and overage rate. |
| Volume/top-up support | `expectedChargeCents` includes top-up credits before calculating overage units. |
| AI compute usage billing | `summarizeUsage` aggregates billable and non-billable usage by capability. |
| Transparent quotas and usage meters | `reconcileAccount` reports included units, billable units, overage units, expected charge, actual charge, and delta. |
| Institutional invoicing | Invoice totals are compared against expected entitlement and usage charges. |
| Licensing APIs and analytics | `inspectLicensingExports` checks that analytics exports are anonymized and do not expose private fields. |
| Revenue health reporting | `buildRevenueHealthReport` summarizes total expected/actual revenue, anomaly counts, high-risk accounts, and an audit hash. |
| Regression evidence | `buildEntitlementRegressionMatrix` flags accounts that moved from clean to review-required between billing runs. |

## Reviewer Notes

- Dependency-free Node.js implementation for easy review.
- Designed as a revenue-ops validation layer that can sit beside existing billing or entitlement engines.
- Demo output includes one clean lab account, one undercharged institution, and one unsafe analytics export.
13 changes: 13 additions & 0 deletions revenue-usage-reconciliation/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "revenue-usage-reconciliation",
"version": "1.0.0",
"description": "Usage anomaly reconciliation and entitlement regression reporting for SCIBASE revenue infrastructure.",
"main": "src/reconciliation.js",
"type": "commonjs",
"scripts": {
"check": "node --check src/reconciliation.js && node --check scripts/demo.js && node --check test/reconciliation.test.js",
"demo": "node scripts/demo.js",
"test": "node test/reconciliation.test.js"
},
"license": "Apache-2.0"
}
69 changes: 69 additions & 0 deletions revenue-usage-reconciliation/scripts/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use strict";

const {
buildRevenueHealthReport,
buildEntitlementRegressionMatrix
} = require("../src/reconciliation");

const previousAccounts = [
{
id: "lab-clean",
plan: { name: "Lab Pro", monthlyQuotaUnits: 1000, includedCents: 9900, overageCentsPerUnit: 12 },
topUps: [{ id: "top-1", units: 200 }],
usageEvents: [{ capability: "ai-review", units: 900 }],
invoices: [{ id: "inv-1", amountCents: 9900 }],
licensingExports: [{ id: "lic-1", dataset: "topic-trends", anonymized: true, fields: ["topic", "score"] }]
}
];

const currentAccounts = [
{
id: "lab-clean",
plan: { name: "Lab Pro", monthlyQuotaUnits: 1000, includedCents: 9900, overageCentsPerUnit: 12 },
topUps: [{ id: "top-1", units: 200 }],
usageEvents: [{ capability: "ai-review", units: 900 }],
invoices: [{ id: "inv-1", amountCents: 9900 }],
licensingExports: [{ id: "lic-1", dataset: "topic-trends", anonymized: true, fields: ["topic", "score"] }]
},
{
id: "institute-undercharged",
plan: { name: "Institution", monthlyQuotaUnits: 5000, includedCents: 49900, overageCentsPerUnit: 8 },
topUps: [],
usageEvents: [
{ capability: "reproducibility-run", units: 5400 },
{ capability: "literature-scan", units: 200, billable: false }
],
invoices: [{ id: "inv-2", amountCents: 49900 }],
licensingExports: [{ id: "lic-2", dataset: "reuse-map", anonymized: true, fields: ["topic", "method", "score"] }]
},
{
id: "agency-export-risk",
plan: { name: "Analytics License", monthlyQuotaUnits: 0, includedCents: 250000, overageCentsPerUnit: 0 },
topUps: [],
usageEvents: [],
invoices: [{ id: "inv-3", amountCents: 250000 }],
licensingExports: [{ id: "lic-3", dataset: "grant-impact", anonymized: false, fields: ["topic", "orcid", "raw_email"] }]
}
];

const previous = buildRevenueHealthReport(previousAccounts);
const current = buildRevenueHealthReport(currentAccounts);
const regression = buildEntitlementRegressionMatrix(previous, current);

console.log(JSON.stringify({
report: {
status: current.status,
totals: current.totals,
highRiskAccounts: current.highRiskAccounts,
auditHash: current.auditHash
},
regression,
anomalies: current.reconciliations.flatMap((account) =>
account.anomalies.map((anomaly) => ({
accountId: account.accountId,
type: anomaly.type,
severity: anomaly.severity,
amountCents: anomaly.amountCents || 0
}))
)
}, null, 2));
185 changes: 185 additions & 0 deletions revenue-usage-reconciliation/src/reconciliation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"use strict";

const crypto = require("crypto");

function money(cents) {
return Math.round(cents);
}

function hashPayload(value) {
return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
}

function normalizeAccount(account) {
if (!account || !account.id || !account.plan) {
throw new Error("account requires id and plan");
}

return {
id: account.id,
plan: {
name: account.plan.name,
monthlyQuotaUnits: account.plan.monthlyQuotaUnits || 0,
includedCents: money(account.plan.includedCents || 0),
overageCentsPerUnit: money(account.plan.overageCentsPerUnit || 0)
},
topUps: Array.isArray(account.topUps) ? account.topUps : [],
invoices: Array.isArray(account.invoices) ? account.invoices : [],
usageEvents: Array.isArray(account.usageEvents) ? account.usageEvents : [],
licensingExports: Array.isArray(account.licensingExports) ? account.licensingExports : []
};
}

function summarizeUsage(usageEvents) {
return usageEvents.reduce((summary, event) => {
const units = Number(event.units || 0);
summary.totalUnits += units;
summary.byCapability[event.capability] = (summary.byCapability[event.capability] || 0) + units;
if (event.billable === false) {
summary.nonBillableUnits += units;
}
return summary;
}, {
totalUnits: 0,
nonBillableUnits: 0,
byCapability: {}
});
}

function summarizeCredits(topUps) {
return topUps.reduce((total, topUp) => total + Number(topUp.units || 0), 0);
}

function expectedChargeCents(account, usageSummary) {
const includedUnits = account.plan.monthlyQuotaUnits + summarizeCredits(account.topUps);
const billableUnits = Math.max(0, usageSummary.totalUnits - usageSummary.nonBillableUnits);
const overageUnits = Math.max(0, billableUnits - includedUnits);
return {
includedUnits,
billableUnits,
overageUnits,
expectedCents: money(account.plan.includedCents + (overageUnits * account.plan.overageCentsPerUnit))
};
}

function actualInvoiceCents(invoices) {
return money(invoices.reduce((total, invoice) => total + Number(invoice.amountCents || 0), 0));
}

function inspectLicensingExports(exports) {
return exports.map((item) => {
const fields = Array.isArray(item.fields) ? item.fields : [];
const privateFields = fields.filter((field) => /email|name|orcid|private|raw/i.test(field));
return {
id: item.id,
dataset: item.dataset,
anonymized: item.anonymized === true,
privateFields,
safeForLicensing: item.anonymized === true && privateFields.length === 0
};
});
}

function reconcileAccount(accountInput) {
const account = normalizeAccount(accountInput);
const usageSummary = summarizeUsage(account.usageEvents);
const expected = expectedChargeCents(account, usageSummary);
const actualCents = actualInvoiceCents(account.invoices);
const deltaCents = money(actualCents - expected.expectedCents);
const licensing = inspectLicensingExports(account.licensingExports);
const anomalies = [];

if (deltaCents < 0) {
anomalies.push({
type: "undercharge",
severity: Math.abs(deltaCents) > 5000 ? "high" : "medium",
amountCents: Math.abs(deltaCents),
message: "Invoice total is lower than expected entitlement and overage charge."
});
}

if (deltaCents > 0) {
anomalies.push({
type: "overcharge",
severity: deltaCents > 5000 ? "high" : "medium",
amountCents: deltaCents,
message: "Invoice total is higher than expected entitlement and overage charge."
});
}

for (const exportCheck of licensing) {
if (!exportCheck.safeForLicensing) {
anomalies.push({
type: "licensing-export-risk",
severity: "high",
exportId: exportCheck.id,
message: "Licensing export is not safely anonymized for institutional analytics."
});
}
}

return {
accountId: account.id,
planName: account.plan.name,
usageSummary,
expected,
actualCents,
deltaCents,
licensing,
anomalies
};
}

function buildRevenueHealthReport(accounts) {
const reconciliations = accounts.map(reconcileAccount);
const totals = reconciliations.reduce((summary, item) => {
summary.expectedCents += item.expected.expectedCents;
summary.actualCents += item.actualCents;
summary.deltaCents += item.deltaCents;
summary.anomalyCount += item.anomalies.length;
return summary;
}, {
expectedCents: 0,
actualCents: 0,
deltaCents: 0,
anomalyCount: 0
});

const highRiskAccounts = reconciliations
.filter((item) => item.anomalies.some((anomaly) => anomaly.severity === "high"))
.map((item) => item.accountId);

return {
status: totals.anomalyCount === 0 ? "ready-for-close" : "needs-revenue-ops-review",
totals,
highRiskAccounts,
reconciliations,
auditHash: hashPayload(reconciliations)
};
}

function buildEntitlementRegressionMatrix(previousReport, currentReport) {
const previousByAccount = new Map(previousReport.reconciliations.map((item) => [item.accountId, item]));
return currentReport.reconciliations.map((current) => {
const previous = previousByAccount.get(current.accountId);
const previousStatus = previous
? (previous.anomalies.length === 0 ? "clean" : "review")
: "new-account";
const currentStatus = current.anomalies.length === 0 ? "clean" : "review";
return {
accountId: current.accountId,
previousStatus,
currentStatus,
regressed: previousStatus === "clean" && currentStatus === "review",
deltaCents: current.deltaCents - (previous ? previous.deltaCents : 0)
};
});
}

module.exports = {
normalizeAccount,
summarizeUsage,
reconcileAccount,
buildRevenueHealthReport,
buildEntitlementRegressionMatrix
};
Loading