From 3755496e5cdc0ee641a8c0ef3d562e7a27620b25 Mon Sep 17 00:00:00 2001 From: xiefuzheng713-alt <266900198+xiefuzheng713-alt@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:18:03 +0800 Subject: [PATCH] Add revenue commitment true-up guard --- revenue-commitment-trueup-guard/README.md | 39 +++ .../commitment-trueup-guard-demo.mp4 | Bin 0 -> 39953 bytes .../artifacts/demo-output.json | 233 ++++++++++++++++++ .../artifacts/reviewer-report.md | 31 +++ .../artifacts/trueup-risk-map.svg | 40 +++ revenue-commitment-trueup-guard/demo.js | 103 ++++++++ .../fixtures/contracts.json | 196 +++++++++++++++ revenue-commitment-trueup-guard/guard.js | 201 +++++++++++++++ revenue-commitment-trueup-guard/test.js | 49 ++++ 9 files changed, 892 insertions(+) create mode 100644 revenue-commitment-trueup-guard/README.md create mode 100644 revenue-commitment-trueup-guard/artifacts/commitment-trueup-guard-demo.mp4 create mode 100644 revenue-commitment-trueup-guard/artifacts/demo-output.json create mode 100644 revenue-commitment-trueup-guard/artifacts/reviewer-report.md create mode 100644 revenue-commitment-trueup-guard/artifacts/trueup-risk-map.svg create mode 100644 revenue-commitment-trueup-guard/demo.js create mode 100644 revenue-commitment-trueup-guard/fixtures/contracts.json create mode 100644 revenue-commitment-trueup-guard/guard.js create mode 100644 revenue-commitment-trueup-guard/test.js 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 0000000000000000000000000000000000000000..8820a69c5945caf2fd60b074a4cd293299853b29 GIT binary patch literal 39953 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 literal 0 HcmV?d00001 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");