diff --git a/revenue-commitment-trueup-guard/README.md b/revenue-commitment-trueup-guard/README.md new file mode 100644 index 00000000..71815f5e --- /dev/null +++ b/revenue-commitment-trueup-guard/README.md @@ -0,0 +1,39 @@ +# Revenue Commitment True-Up Guard + +This module adds a synthetic, dependency-free release guard for enterprise annual +minimum commitments before a revenue true-up invoice is released. + +It focuses on the Revenue Infrastructure issue slice that sits after usage +metering and before invoice release: + +- annual or multi-month minimum commitment drawdown +- true-up amount reconciliation +- duplicate true-up window detection +- unsigned or out-of-period amendment impact +- SLA credit evidence and cap checks +- overage evidence before billing above the commitment + +No credentials, payment processors, real customers, private billing data, +external APIs, or payout systems are used. All scenarios are synthetic fixtures. + +## Run + +```bash +node revenue-commitment-trueup-guard/test.js +node revenue-commitment-trueup-guard/demo.js +``` + +## Reviewer Artifacts + +Running `demo.js` writes: + +- `artifacts/demo-output.json` +- `artifacts/reviewer-report.md` +- `artifacts/trueup-risk-map.svg` +- `artifacts/commitment-trueup-guard-demo.mp4` + +## Actions + +- `RELEASE_INVOICE`: commitment, true-up, credits, and evidence are aligned. +- `REVIEW_BEFORE_RELEASE`: non-blocking finance review is needed. +- `HOLD_INVOICE`: invoice should not be released until a blocker is resolved. diff --git a/revenue-commitment-trueup-guard/artifacts/commitment-trueup-guard-demo.mp4 b/revenue-commitment-trueup-guard/artifacts/commitment-trueup-guard-demo.mp4 new file mode 100644 index 00000000..8820a69c Binary files /dev/null and b/revenue-commitment-trueup-guard/artifacts/commitment-trueup-guard-demo.mp4 differ diff --git a/revenue-commitment-trueup-guard/artifacts/demo-output.json b/revenue-commitment-trueup-guard/artifacts/demo-output.json new file mode 100644 index 00000000..b7751913 --- /dev/null +++ b/revenue-commitment-trueup-guard/artifacts/demo-output.json @@ -0,0 +1,233 @@ +{ + "generatedAt": "2026-06-04T07:17:36.824Z", + "portfolio": "synthetic-enterprise-commitments", + "summary": { + "contracts": 6, + "invoices": 7, + "release": 2, + "review": 1, + "hold": 4, + "totalVarianceCents": 420000, + "topFindings": { + "TRUEUP_VARIANCE": 4, + "DUPLICATE_TRUEUP_WINDOW": 1, + "MISSING_BASE_EVIDENCE": 1, + "FUTURE_AMENDMENT_APPLIED_EARLY": 1, + "MISSING_SLA_TICKET": 2, + "SLA_CREDIT_EXCEEDS_CAP": 1 + } + }, + "evaluations": [ + { + "contractId": "ENT-CLEAN-2026", + "customerSegment": "Institutional license", + "currency": "USD", + "action": "RELEASE_INVOICE", + "invoices": [ + { + "contractId": "ENT-CLEAN-2026", + "invoiceId": "INV-CLEAN-Q1", + "trueUpWindow": "2026-Q1", + "customerSegment": "Institutional license", + "effectiveCommitmentCents": 1200000, + "meteredUsageCents": 1475000, + "proposedTrueUpCents": 225000, + "expectedTrueUpCents": 225000, + "varianceCents": 0, + "findings": [], + "action": "RELEASE_INVOICE", + "riskScore": 0 + } + ] + }, + { + "contractId": "ENT-UNDERTRUE-2026", + "customerSegment": "Lab group account", + "currency": "USD", + "action": "HOLD_INVOICE", + "invoices": [ + { + "contractId": "ENT-UNDERTRUE-2026", + "invoiceId": "INV-UNDER-Q1", + "trueUpWindow": "2026-Q1", + "customerSegment": "Lab group account", + "effectiveCommitmentCents": 900000, + "meteredUsageCents": 620000, + "proposedTrueUpCents": 150000, + "expectedTrueUpCents": 280000, + "varianceCents": -130000, + "findings": [ + { + "severity": "hold", + "code": "TRUEUP_VARIANCE", + "message": "Proposed true-up $1500.00 differs from expected $2800.00", + "amountCents": -130000 + } + ], + "action": "HOLD_INVOICE", + "riskScore": 40 + } + ] + }, + { + "contractId": "ENT-DUPLICATE-2026", + "customerSegment": "Enterprise license", + "currency": "USD", + "action": "HOLD_INVOICE", + "invoices": [ + { + "contractId": "ENT-DUPLICATE-2026", + "invoiceId": "INV-DUP-Q1-A", + "trueUpWindow": "2026-Q1", + "customerSegment": "Enterprise license", + "effectiveCommitmentCents": 1000000, + "meteredUsageCents": 1080000, + "proposedTrueUpCents": 80000, + "expectedTrueUpCents": 80000, + "varianceCents": 0, + "findings": [], + "action": "RELEASE_INVOICE", + "riskScore": 0 + }, + { + "contractId": "ENT-DUPLICATE-2026", + "invoiceId": "INV-DUP-Q1-B", + "trueUpWindow": "2026-Q1", + "customerSegment": "Enterprise license", + "effectiveCommitmentCents": 1000000, + "meteredUsageCents": 1015000, + "proposedTrueUpCents": 15000, + "expectedTrueUpCents": 15000, + "varianceCents": 0, + "findings": [ + { + "severity": "hold", + "code": "DUPLICATE_TRUEUP_WINDOW", + "message": "2026-Q1 is already represented by another invoice", + "amountCents": 0 + } + ], + "action": "HOLD_INVOICE", + "riskScore": 40 + } + ] + }, + { + "contractId": "ENT-AMENDMENT-2026", + "customerSegment": "Consortium account", + "currency": "USD", + "action": "HOLD_INVOICE", + "invoices": [ + { + "contractId": "ENT-AMENDMENT-2026", + "invoiceId": "INV-AMEND-Q1", + "trueUpWindow": "2026-Q1", + "customerSegment": "Consortium account", + "effectiveCommitmentCents": 800000, + "meteredUsageCents": 610000, + "proposedTrueUpCents": 0, + "expectedTrueUpCents": 190000, + "varianceCents": -190000, + "findings": [ + { + "severity": "hold", + "code": "MISSING_BASE_EVIDENCE", + "message": "signed-amendment evidence is required before release", + "amountCents": 0 + }, + { + "severity": "hold", + "code": "TRUEUP_VARIANCE", + "message": "Proposed true-up $0.00 differs from expected $1900.00", + "amountCents": -190000 + }, + { + "severity": "hold", + "code": "FUTURE_AMENDMENT_APPLIED_EARLY", + "message": "Invoice appears to use a future amendment before its effective date", + "amountCents": 0 + } + ], + "action": "HOLD_INVOICE", + "riskScore": 120 + } + ] + }, + { + "contractId": "ENT-SLA-2026", + "customerSegment": "Enterprise license", + "currency": "USD", + "action": "HOLD_INVOICE", + "invoices": [ + { + "contractId": "ENT-SLA-2026", + "invoiceId": "INV-SLA-Q1", + "trueUpWindow": "2026-Q1", + "customerSegment": "Enterprise license", + "effectiveCommitmentCents": 1100000, + "meteredUsageCents": 1280000, + "proposedTrueUpCents": 100000, + "expectedTrueUpCents": 20000, + "varianceCents": 80000, + "findings": [ + { + "severity": "hold", + "code": "TRUEUP_VARIANCE", + "message": "Proposed true-up $1000.00 differs from expected $200.00", + "amountCents": 80000 + }, + { + "severity": "review", + "code": "MISSING_SLA_TICKET", + "message": "SLA credit needs an incident or support ticket reference", + "amountCents": 0 + }, + { + "severity": "hold", + "code": "SLA_CREDIT_EXCEEDS_CAP", + "message": "SLA credit $1600.00 exceeds cap $1000.00", + "amountCents": 60000 + } + ], + "action": "HOLD_INVOICE", + "riskScore": 95 + } + ] + }, + { + "contractId": "ENT-REVIEW-2026", + "customerSegment": "Institutional license", + "currency": "USD", + "action": "REVIEW_BEFORE_RELEASE", + "invoices": [ + { + "contractId": "ENT-REVIEW-2026", + "invoiceId": "INV-REVIEW-Q1", + "trueUpWindow": "2026-Q1", + "customerSegment": "Institutional license", + "effectiveCommitmentCents": 1200000, + "meteredUsageCents": 1300000, + "proposedTrueUpCents": 100000, + "expectedTrueUpCents": 80000, + "varianceCents": 20000, + "findings": [ + { + "severity": "review", + "code": "TRUEUP_VARIANCE", + "message": "Proposed true-up $1000.00 differs from expected $800.00", + "amountCents": 20000 + }, + { + "severity": "review", + "code": "MISSING_SLA_TICKET", + "message": "SLA credit needs an incident or support ticket reference", + "amountCents": 0 + } + ], + "action": "REVIEW_BEFORE_RELEASE", + "riskScore": 30 + } + ] + } + ] +} diff --git a/revenue-commitment-trueup-guard/artifacts/reviewer-report.md b/revenue-commitment-trueup-guard/artifacts/reviewer-report.md new file mode 100644 index 00000000..9f6e825a --- /dev/null +++ b/revenue-commitment-trueup-guard/artifacts/reviewer-report.md @@ -0,0 +1,31 @@ +# Revenue Commitment True-Up Guard Report + +Generated: 2026-06-04T07:17:36.824Z +Portfolio: synthetic-enterprise-commitments + +## Summary + +- Contracts reviewed: 6 +- Invoices reviewed: 7 +- Release: 2 +- Review before release: 1 +- Hold invoice: 4 +- Absolute true-up variance: $4200.00 + +## Invoice Decisions + +| Contract | Invoice | Action | Expected true-up | Proposed true-up | Findings | +| --- | --- | --- | ---: | ---: | --- | +| ENT-CLEAN-2026 | INV-CLEAN-Q1 | RELEASE_INVOICE | $2250.00 | $2250.00 | none | +| ENT-UNDERTRUE-2026 | INV-UNDER-Q1 | HOLD_INVOICE | $2800.00 | $1500.00 | TRUEUP_VARIANCE | +| ENT-DUPLICATE-2026 | INV-DUP-Q1-A | RELEASE_INVOICE | $800.00 | $800.00 | none | +| ENT-DUPLICATE-2026 | INV-DUP-Q1-B | HOLD_INVOICE | $150.00 | $150.00 | DUPLICATE_TRUEUP_WINDOW | +| ENT-AMENDMENT-2026 | INV-AMEND-Q1 | HOLD_INVOICE | $1900.00 | $0.00 | MISSING_BASE_EVIDENCE, TRUEUP_VARIANCE, FUTURE_AMENDMENT_APPLIED_EARLY | +| ENT-SLA-2026 | INV-SLA-Q1 | HOLD_INVOICE | $200.00 | $1000.00 | TRUEUP_VARIANCE, MISSING_SLA_TICKET, SLA_CREDIT_EXCEEDS_CAP | +| ENT-REVIEW-2026 | INV-REVIEW-Q1 | REVIEW_BEFORE_RELEASE | $800.00 | $1000.00 | TRUEUP_VARIANCE, MISSING_SLA_TICKET | + +## Reviewer Notes + +- All data is synthetic and credential-free. +- `HOLD_INVOICE` means a finance or revenue-ops blocker should be resolved before release. +- `REVIEW_BEFORE_RELEASE` means release is possible after a documented finance check. diff --git a/revenue-commitment-trueup-guard/artifacts/trueup-risk-map.svg b/revenue-commitment-trueup-guard/artifacts/trueup-risk-map.svg new file mode 100644 index 00000000..e183028c --- /dev/null +++ b/revenue-commitment-trueup-guard/artifacts/trueup-risk-map.svg @@ -0,0 +1,40 @@ + + + Revenue Commitment True-Up Risk Map + Synthetic invoices scored before release + + INV-CLEAN-Q1 RELEASE_INVOICE + + 0 + clear + + INV-UNDER-Q1 HOLD_INVOICE + + 40 + TRUEUP_VARIANCE + + INV-DUP-Q1-A RELEASE_INVOICE + + 0 + clear + + INV-DUP-Q1-B HOLD_INVOICE + + 40 + DUPLICATE_TRUEUP_WINDOW + + INV-AMEND-Q1 HOLD_INVOICE + + 120 + MISSING_BASE_EVIDENCE, TRUEUP_VARIANCE, FUTURE_AMENDMENT_APPLIED_EARLY + + INV-SLA-Q1 HOLD_INVOICE + + 95 + TRUEUP_VARIANCE, MISSING_SLA_TICKET, SLA_CREDIT_EXCEEDS_CAP + + INV-REVIEW-Q1 REVIEW_BEFORE_RELEASE + + 30 + TRUEUP_VARIANCE, MISSING_SLA_TICKET + diff --git a/revenue-commitment-trueup-guard/demo.js b/revenue-commitment-trueup-guard/demo.js new file mode 100644 index 00000000..87184658 --- /dev/null +++ b/revenue-commitment-trueup-guard/demo.js @@ -0,0 +1,103 @@ +const fs = require("fs"); +const path = require("path"); +const { centsToDollars, evaluatePortfolio } = require("./guard"); +const portfolio = require("./fixtures/contracts.json"); + +const artifactsDir = path.join(__dirname, "artifacts"); +fs.mkdirSync(artifactsDir, { recursive: true }); + +const result = evaluatePortfolio(portfolio); + +function writeJson() { + fs.writeFileSync(path.join(artifactsDir, "demo-output.json"), `${JSON.stringify(result, null, 2)}\n`); +} + +function writeMarkdown() { + const lines = [ + "# Revenue Commitment True-Up Guard Report", + "", + `Generated: ${result.generatedAt}`, + `Portfolio: ${result.portfolio}`, + "", + "## Summary", + "", + `- Contracts reviewed: ${result.summary.contracts}`, + `- Invoices reviewed: ${result.summary.invoices}`, + `- Release: ${result.summary.release}`, + `- Review before release: ${result.summary.review}`, + `- Hold invoice: ${result.summary.hold}`, + `- Absolute true-up variance: ${centsToDollars(result.summary.totalVarianceCents)}`, + "", + "## Invoice Decisions", + "", + "| Contract | Invoice | Action | Expected true-up | Proposed true-up | Findings |", + "| --- | --- | --- | ---: | ---: | --- |", + ]; + + for (const contract of result.evaluations) { + for (const invoice of contract.invoices) { + const findings = invoice.findings.map((finding) => finding.code).join(", ") || "none"; + lines.push( + `| ${invoice.contractId} | ${invoice.invoiceId} | ${invoice.action} | ${centsToDollars( + invoice.expectedTrueUpCents + )} | ${centsToDollars(invoice.proposedTrueUpCents)} | ${findings} |` + ); + } + } + + lines.push( + "", + "## Reviewer Notes", + "", + "- All data is synthetic and credential-free.", + "- `HOLD_INVOICE` means a finance or revenue-ops blocker should be resolved before release.", + "- `REVIEW_BEFORE_RELEASE` means release is possible after a documented finance check." + ); + + fs.writeFileSync(path.join(artifactsDir, "reviewer-report.md"), `${lines.join("\n")}\n`); +} + +function escapeXml(value) { + return String(value).replace(/&/g, "&").replace(//g, ">"); +} + +function writeSvg() { + const invoices = result.evaluations.flatMap((contract) => contract.invoices); + const width = 920; + const rowHeight = 62; + const height = 110 + invoices.length * rowHeight; + const maxRisk = Math.max(1, ...invoices.map((invoice) => invoice.riskScore)); + + const rows = invoices + .map((invoice, index) => { + const y = 82 + index * rowHeight; + const barWidth = Math.max(4, Math.round((invoice.riskScore / maxRisk) * 420)); + const color = invoice.action === "HOLD_INVOICE" ? "#b91c1c" : invoice.action === "REVIEW_BEFORE_RELEASE" ? "#ca8a04" : "#15803d"; + const label = `${invoice.invoiceId} ${invoice.action}`; + return ` + ${escapeXml(label)} + + ${invoice.riskScore} + ${escapeXml( + invoice.findings.map((finding) => finding.code).join(", ") || "clear" + )}`; + }) + .join("\n"); + + const svg = ` + + Revenue Commitment True-Up Risk Map + Synthetic invoices scored before release +${rows} + +`; + + fs.writeFileSync(path.join(artifactsDir, "trueup-risk-map.svg"), svg); +} + +writeJson(); +writeMarkdown(); +writeSvg(); + +console.log(`Wrote reviewer artifacts to ${artifactsDir}`); +console.log(JSON.stringify(result.summary, null, 2)); diff --git a/revenue-commitment-trueup-guard/fixtures/contracts.json b/revenue-commitment-trueup-guard/fixtures/contracts.json new file mode 100644 index 00000000..1410905a --- /dev/null +++ b/revenue-commitment-trueup-guard/fixtures/contracts.json @@ -0,0 +1,196 @@ +{ + "generatedAt": "2026-06-04T07:15:00.000Z", + "portfolio": "synthetic-enterprise-commitments", + "contracts": [ + { + "contractId": "ENT-CLEAN-2026", + "customerSegment": "Institutional license", + "currency": "USD", + "periodStart": "2026-01-01", + "periodEnd": "2026-03-31", + "minimumCommitmentCents": 1200000, + "signedAmendments": [ + { + "amendmentId": "AMD-CLEAN-Q1", + "effectiveDate": "2026-01-01", + "signedAt": "2025-12-20", + "minimumCommitmentCents": 1200000 + } + ], + "invoices": [ + { + "invoiceId": "INV-CLEAN-Q1", + "trueUpWindow": "2026-Q1", + "periodStart": "2026-01-01", + "periodEnd": "2026-03-31", + "meteredUsageCents": 1475000, + "proposedTrueUpCents": 225000, + "slaCreditCents": 50000, + "slaCreditCapCents": 75000, + "evidence": [ + "usage-meter-export", + "signed-amendment", + "overage-approval", + "sla-credit-ticket" + ] + } + ] + }, + { + "contractId": "ENT-UNDERTRUE-2026", + "customerSegment": "Lab group account", + "currency": "USD", + "periodStart": "2026-01-01", + "periodEnd": "2026-03-31", + "minimumCommitmentCents": 900000, + "signedAmendments": [ + { + "amendmentId": "AMD-UNDER-Q1", + "effectiveDate": "2026-01-01", + "signedAt": "2025-12-15", + "minimumCommitmentCents": 900000 + } + ], + "invoices": [ + { + "invoiceId": "INV-UNDER-Q1", + "trueUpWindow": "2026-Q1", + "periodStart": "2026-01-01", + "periodEnd": "2026-03-31", + "meteredUsageCents": 620000, + "proposedTrueUpCents": 150000, + "slaCreditCents": 0, + "slaCreditCapCents": 50000, + "evidence": ["usage-meter-export", "signed-amendment"] + } + ] + }, + { + "contractId": "ENT-DUPLICATE-2026", + "customerSegment": "Enterprise license", + "currency": "USD", + "periodStart": "2026-01-01", + "periodEnd": "2026-03-31", + "minimumCommitmentCents": 1000000, + "signedAmendments": [ + { + "amendmentId": "AMD-DUP-Q1", + "effectiveDate": "2026-01-01", + "signedAt": "2025-12-11", + "minimumCommitmentCents": 1000000 + } + ], + "invoices": [ + { + "invoiceId": "INV-DUP-Q1-A", + "trueUpWindow": "2026-Q1", + "periodStart": "2026-01-01", + "periodEnd": "2026-03-31", + "meteredUsageCents": 1080000, + "proposedTrueUpCents": 80000, + "slaCreditCents": 0, + "slaCreditCapCents": 50000, + "evidence": ["usage-meter-export", "signed-amendment", "overage-approval"] + }, + { + "invoiceId": "INV-DUP-Q1-B", + "trueUpWindow": "2026-Q1", + "periodStart": "2026-01-01", + "periodEnd": "2026-03-31", + "meteredUsageCents": 1015000, + "proposedTrueUpCents": 15000, + "slaCreditCents": 0, + "slaCreditCapCents": 50000, + "evidence": ["usage-meter-export", "signed-amendment", "overage-approval"] + } + ] + }, + { + "contractId": "ENT-AMENDMENT-2026", + "customerSegment": "Consortium account", + "currency": "USD", + "periodStart": "2026-01-01", + "periodEnd": "2026-03-31", + "minimumCommitmentCents": 800000, + "signedAmendments": [ + { + "amendmentId": "AMD-FUTURE-Q2", + "effectiveDate": "2026-04-01", + "signedAt": "2026-03-15", + "minimumCommitmentCents": 600000 + } + ], + "invoices": [ + { + "invoiceId": "INV-AMEND-Q1", + "trueUpWindow": "2026-Q1", + "periodStart": "2026-01-01", + "periodEnd": "2026-03-31", + "meteredUsageCents": 610000, + "proposedTrueUpCents": 0, + "slaCreditCents": 0, + "slaCreditCapCents": 50000, + "evidence": ["usage-meter-export"] + } + ] + }, + { + "contractId": "ENT-SLA-2026", + "customerSegment": "Enterprise license", + "currency": "USD", + "periodStart": "2026-01-01", + "periodEnd": "2026-03-31", + "minimumCommitmentCents": 1100000, + "signedAmendments": [ + { + "amendmentId": "AMD-SLA-Q1", + "effectiveDate": "2026-01-01", + "signedAt": "2025-12-18", + "minimumCommitmentCents": 1100000 + } + ], + "invoices": [ + { + "invoiceId": "INV-SLA-Q1", + "trueUpWindow": "2026-Q1", + "periodStart": "2026-01-01", + "periodEnd": "2026-03-31", + "meteredUsageCents": 1280000, + "proposedTrueUpCents": 100000, + "slaCreditCents": 160000, + "slaCreditCapCents": 100000, + "evidence": ["usage-meter-export", "signed-amendment", "overage-approval"] + } + ] + }, + { + "contractId": "ENT-REVIEW-2026", + "customerSegment": "Institutional license", + "currency": "USD", + "periodStart": "2026-01-01", + "periodEnd": "2026-03-31", + "minimumCommitmentCents": 1200000, + "signedAmendments": [ + { + "amendmentId": "AMD-REVIEW-Q1", + "effectiveDate": "2026-01-01", + "signedAt": "2025-12-21", + "minimumCommitmentCents": 1200000 + } + ], + "invoices": [ + { + "invoiceId": "INV-REVIEW-Q1", + "trueUpWindow": "2026-Q1", + "periodStart": "2026-01-01", + "periodEnd": "2026-03-31", + "meteredUsageCents": 1300000, + "proposedTrueUpCents": 100000, + "slaCreditCents": 20000, + "slaCreditCapCents": 75000, + "evidence": ["usage-meter-export", "signed-amendment", "overage-approval"] + } + ] + } + ] +} diff --git a/revenue-commitment-trueup-guard/guard.js b/revenue-commitment-trueup-guard/guard.js new file mode 100644 index 00000000..0ad3c3fd --- /dev/null +++ b/revenue-commitment-trueup-guard/guard.js @@ -0,0 +1,201 @@ +const REQUIRED_EVIDENCE = { + base: ["usage-meter-export", "signed-amendment"], + overage: "overage-approval", + slaCredit: "sla-credit-ticket", +}; + +const ACTIONS = { + release: "RELEASE_INVOICE", + review: "REVIEW_BEFORE_RELEASE", + hold: "HOLD_INVOICE", +}; + +function centsToDollars(cents) { + return `$${(cents / 100).toFixed(2)}`; +} + +function parseDate(value, fieldName) { + const date = new Date(`${value}T00:00:00.000Z`); + if (Number.isNaN(date.getTime())) { + throw new Error(`Invalid ${fieldName}: ${value}`); + } + return date; +} + +function isDateWithin(value, start, end) { + const date = parseDate(value, "date"); + return date >= parseDate(start, "periodStart") && date <= parseDate(end, "periodEnd"); +} + +function getEffectiveCommitment(contract, invoice) { + const amendments = Array.isArray(contract.signedAmendments) ? contract.signedAmendments : []; + const eligible = amendments + .filter((amendment) => amendment.signedAt) + .filter((amendment) => isDateWithin(amendment.effectiveDate, invoice.periodStart, invoice.periodEnd)) + .sort((a, b) => parseDate(b.effectiveDate, "effectiveDate") - parseDate(a.effectiveDate, "effectiveDate")); + + return eligible[0]?.minimumCommitmentCents ?? contract.minimumCommitmentCents; +} + +function expectedTrueUpCents(invoice, effectiveCommitmentCents) { + const gross = invoice.meteredUsageCents - effectiveCommitmentCents; + const overage = gross > 0 ? gross : 0; + const floorCatchUp = gross < 0 ? Math.abs(gross) : 0; + return Math.max(0, overage + floorCatchUp - invoice.slaCreditCents); +} + +function addFinding(findings, severity, code, message, amountCents = 0) { + findings.push({ severity, code, message, amountCents }); +} + +function evaluateInvoice(contract, invoice, seenWindows) { + const findings = []; + const evidence = new Set(invoice.evidence || []); + const effectiveCommitmentCents = getEffectiveCommitment(contract, invoice); + const expectedTrueUp = expectedTrueUpCents(invoice, effectiveCommitmentCents); + const variance = invoice.proposedTrueUpCents - expectedTrueUp; + + for (const required of REQUIRED_EVIDENCE.base) { + if (!evidence.has(required)) { + addFinding(findings, "hold", "MISSING_BASE_EVIDENCE", `${required} evidence is required before release`); + } + } + + if (seenWindows.has(invoice.trueUpWindow)) { + addFinding(findings, "hold", "DUPLICATE_TRUEUP_WINDOW", `${invoice.trueUpWindow} is already represented by another invoice`); + } + seenWindows.add(invoice.trueUpWindow); + + if (variance !== 0) { + addFinding( + findings, + Math.abs(variance) >= 50000 ? "hold" : "review", + "TRUEUP_VARIANCE", + `Proposed true-up ${centsToDollars(invoice.proposedTrueUpCents)} differs from expected ${centsToDollars(expectedTrueUp)}`, + variance + ); + } + + if (invoice.meteredUsageCents > effectiveCommitmentCents && !evidence.has(REQUIRED_EVIDENCE.overage)) { + addFinding(findings, "hold", "MISSING_OVERAGE_APPROVAL", "Overage billing requires approval evidence"); + } + + if (invoice.slaCreditCents > 0 && !evidence.has(REQUIRED_EVIDENCE.slaCredit)) { + addFinding(findings, "review", "MISSING_SLA_TICKET", "SLA credit needs an incident or support ticket reference"); + } + + if (invoice.slaCreditCents > invoice.slaCreditCapCents) { + addFinding( + findings, + "hold", + "SLA_CREDIT_EXCEEDS_CAP", + `SLA credit ${centsToDollars(invoice.slaCreditCents)} exceeds cap ${centsToDollars(invoice.slaCreditCapCents)}`, + invoice.slaCreditCents - invoice.slaCreditCapCents + ); + } + + const hasOnlyFutureAmendment = (contract.signedAmendments || []).some( + (amendment) => + amendment.signedAt && + parseDate(amendment.effectiveDate, "effectiveDate") > parseDate(invoice.periodEnd, "periodEnd") && + amendment.minimumCommitmentCents !== contract.minimumCommitmentCents + ); + + if (hasOnlyFutureAmendment && invoice.proposedTrueUpCents < expectedTrueUp) { + addFinding(findings, "hold", "FUTURE_AMENDMENT_APPLIED_EARLY", "Invoice appears to use a future amendment before its effective date"); + } + + return { + contractId: contract.contractId, + invoiceId: invoice.invoiceId, + trueUpWindow: invoice.trueUpWindow, + customerSegment: contract.customerSegment, + effectiveCommitmentCents, + meteredUsageCents: invoice.meteredUsageCents, + proposedTrueUpCents: invoice.proposedTrueUpCents, + expectedTrueUpCents: expectedTrueUp, + varianceCents: variance, + findings, + }; +} + +function decideAction(findings) { + if (findings.some((finding) => finding.severity === "hold")) { + return ACTIONS.hold; + } + if (findings.some((finding) => finding.severity === "review")) { + return ACTIONS.review; + } + return ACTIONS.release; +} + +function riskScore(findings) { + return findings.reduce((score, finding) => score + (finding.severity === "hold" ? 40 : 15), 0); +} + +function evaluateContract(contract) { + const seenWindows = new Set(); + const invoices = contract.invoices.map((invoice) => { + const result = evaluateInvoice(contract, invoice, seenWindows); + return { + ...result, + action: decideAction(result.findings), + riskScore: riskScore(result.findings), + }; + }); + + const contractAction = decideAction(invoices.flatMap((invoice) => invoice.findings)); + + return { + contractId: contract.contractId, + customerSegment: contract.customerSegment, + currency: contract.currency, + action: contractAction, + invoices, + }; +} + +function summarize(evaluations) { + const summary = { + contracts: evaluations.length, + invoices: 0, + release: 0, + review: 0, + hold: 0, + totalVarianceCents: 0, + topFindings: {}, + }; + + for (const contract of evaluations) { + for (const invoice of contract.invoices) { + summary.invoices += 1; + summary.totalVarianceCents += Math.abs(invoice.varianceCents); + if (invoice.action === ACTIONS.release) summary.release += 1; + if (invoice.action === ACTIONS.review) summary.review += 1; + if (invoice.action === ACTIONS.hold) summary.hold += 1; + + for (const finding of invoice.findings) { + summary.topFindings[finding.code] = (summary.topFindings[finding.code] || 0) + 1; + } + } + } + + return summary; +} + +function evaluatePortfolio(portfolio) { + const evaluations = portfolio.contracts.map(evaluateContract); + return { + generatedAt: new Date().toISOString(), + portfolio: portfolio.portfolio, + summary: summarize(evaluations), + evaluations, + }; +} + +module.exports = { + ACTIONS, + centsToDollars, + evaluatePortfolio, + expectedTrueUpCents, +}; diff --git a/revenue-commitment-trueup-guard/test.js b/revenue-commitment-trueup-guard/test.js new file mode 100644 index 00000000..a6aea6df --- /dev/null +++ b/revenue-commitment-trueup-guard/test.js @@ -0,0 +1,49 @@ +const assert = require("assert"); +const { ACTIONS, evaluatePortfolio, expectedTrueUpCents } = require("./guard"); +const portfolio = require("./fixtures/contracts.json"); + +const result = evaluatePortfolio(portfolio); +const invoices = new Map( + result.evaluations.flatMap((contract) => contract.invoices.map((invoice) => [invoice.invoiceId, invoice])) +); + +assert.strictEqual(result.summary.contracts, 6); +assert.strictEqual(result.summary.invoices, 7); +assert.strictEqual(result.summary.release, 2); +assert.strictEqual(result.summary.review, 1); +assert.strictEqual(result.summary.hold, 4); + +assert.strictEqual(invoices.get("INV-CLEAN-Q1").action, ACTIONS.release); +assert.strictEqual(invoices.get("INV-CLEAN-Q1").expectedTrueUpCents, 225000); +assert.strictEqual(invoices.get("INV-CLEAN-Q1").proposedTrueUpCents, 225000); +assert.strictEqual(invoices.get("INV-CLEAN-Q1").findings.length, 0); + +assert.strictEqual(invoices.get("INV-UNDER-Q1").action, ACTIONS.hold); +assert.ok(invoices.get("INV-UNDER-Q1").findings.some((finding) => finding.code === "TRUEUP_VARIANCE")); + +assert.strictEqual(invoices.get("INV-DUP-Q1-B").action, ACTIONS.hold); +assert.ok(invoices.get("INV-DUP-Q1-B").findings.some((finding) => finding.code === "DUPLICATE_TRUEUP_WINDOW")); + +assert.strictEqual(invoices.get("INV-AMEND-Q1").action, ACTIONS.hold); +assert.ok(invoices.get("INV-AMEND-Q1").findings.some((finding) => finding.code === "FUTURE_AMENDMENT_APPLIED_EARLY")); + +assert.strictEqual(invoices.get("INV-SLA-Q1").action, ACTIONS.hold); +assert.ok(invoices.get("INV-SLA-Q1").findings.some((finding) => finding.code === "SLA_CREDIT_EXCEEDS_CAP")); +assert.ok(invoices.get("INV-SLA-Q1").findings.some((finding) => finding.code === "MISSING_SLA_TICKET")); + +assert.strictEqual(invoices.get("INV-REVIEW-Q1").action, ACTIONS.review); +assert.ok(invoices.get("INV-REVIEW-Q1").findings.some((finding) => finding.code === "TRUEUP_VARIANCE")); +assert.ok(invoices.get("INV-REVIEW-Q1").findings.some((finding) => finding.code === "MISSING_SLA_TICKET")); + +assert.strictEqual( + expectedTrueUpCents( + { + meteredUsageCents: 125000, + slaCreditCents: 25000, + }, + 100000 + ), + 0 +); + +console.log("revenue-commitment-trueup-guard tests passed");