From fde79da369da28d84a0c33846f0f33951662041d Mon Sep 17 00:00:00 2001 From: telemarkdigital-publisher <307230483+telemarkdigital-publisher@users.noreply.github.com> Date: Sun, 16 Aug 2026 17:03:18 +0200 Subject: [PATCH] Add revenue entitlement delivery guard --- PR_BODY_REVENUE_ENTITLEMENT_DELIVERY_20.md | 36 ++ revenue-entitlement-delivery-guard/README.md | 42 +++ .../sample_entitlement_delivery_packets.json | 166 +++++++++ .../package.json | 13 + .../reports/demo.mp4 | Bin 0 -> 51493 bytes .../reports/reviewer-packet.md | 63 ++++ .../reports/summary.json | 287 ++++++++++++++++ .../reports/summary.svg | 17 + .../scripts/demo.js | 77 +++++ .../scripts/render-demo-video.js | 60 ++++ .../src/index.js | 315 ++++++++++++++++++ ...revenue-entitlement-delivery-guard.test.js | 67 ++++ 12 files changed, 1143 insertions(+) create mode 100644 PR_BODY_REVENUE_ENTITLEMENT_DELIVERY_20.md create mode 100644 revenue-entitlement-delivery-guard/README.md create mode 100644 revenue-entitlement-delivery-guard/data/sample_entitlement_delivery_packets.json create mode 100644 revenue-entitlement-delivery-guard/package.json create mode 100644 revenue-entitlement-delivery-guard/reports/demo.mp4 create mode 100644 revenue-entitlement-delivery-guard/reports/reviewer-packet.md create mode 100644 revenue-entitlement-delivery-guard/reports/summary.json create mode 100644 revenue-entitlement-delivery-guard/reports/summary.svg create mode 100644 revenue-entitlement-delivery-guard/scripts/demo.js create mode 100644 revenue-entitlement-delivery-guard/scripts/render-demo-video.js create mode 100644 revenue-entitlement-delivery-guard/src/index.js create mode 100644 revenue-entitlement-delivery-guard/test/revenue-entitlement-delivery-guard.test.js diff --git a/PR_BODY_REVENUE_ENTITLEMENT_DELIVERY_20.md b/PR_BODY_REVENUE_ENTITLEMENT_DELIVERY_20.md new file mode 100644 index 00000000..085cc952 --- /dev/null +++ b/PR_BODY_REVENUE_ENTITLEMENT_DELIVERY_20.md @@ -0,0 +1,36 @@ +/claim #20 + +## Summary + +Adds `revenue-entitlement-delivery-guard/`, a focused Revenue Infrastructure guard for contract-to-entitlement delivery evidence. + +This is distinct from existing #20 slices for broad billing modules, subscriptions, renewal notices, dunning holds, tax/FX, quota rollover, metering, payment disputes, grant budgets, privacy-safe licensing, analytics-seat rosters, data-product freshness, and revenue-recognition close controls. It focuses only on whether signed customer contract lines have been provisioned as seats, quotas, data feeds, support tiers, and delivery evidence before billing, renewal, expansion, or revenue-recognition workflows proceed. + +## What is included + +- Dependency-free Node evaluator. +- Synthetic contract and entitlement-delivery packets. +- Deterministic `RELEASE_REVENUE`, `REVIEW_REVENUE`, and `HOLD_REVENUE` decisions. +- Checks for contract term dates, delivery evidence hash, customer acceptance, account-owner acknowledgement, under-delivered seats/quotas/feeds/support, over-provisioned entitlements, unpurchased data feeds, and missing revenue holds. +- Reviewer artifacts: + - `reports/summary.json` + - `reports/reviewer-packet.md` + - `reports/summary.svg` + - `reports/demo.mp4` + +## Validation + +Locally verified: + +```text +npm.cmd test +npm.cmd run demo +npm.cmd run check +npm.cmd run video +git diff --check +confirmed reports/demo.mp4 was generated as a non-empty local artifact +``` + +The MP4 demo is an FFmpeg-generated synthetic slate only. It is not a screen recording and contains no desktop capture, credentials, private files, or personal information. + +AI-assisted with OpenAI Codex; I reviewed and locally verified the implementation before submitting. diff --git a/revenue-entitlement-delivery-guard/README.md b/revenue-entitlement-delivery-guard/README.md new file mode 100644 index 00000000..b460daea --- /dev/null +++ b/revenue-entitlement-delivery-guard/README.md @@ -0,0 +1,42 @@ +# Revenue Entitlement Delivery Guard + +This contribution adds a focused guard for SCIBASE's Revenue Infrastructure layer. It checks whether a signed subscription, compute, API, or data-licensing contract has actually been provisioned before billing, renewal, expansion, or revenue-recognition workflows treat the account as delivered. + +The slice is intentionally narrow. It does not replace pricing, checkout, metering, tax, FX, dunning, subscription-renewal notices, quota rollover, dispute evidence, grant budgets, privacy-safe licensing, or revenue-recognition close controls. It covers the delivery-evidence boundary between commercial contract lines and platform entitlements. + +## What It Checks + +- Contract term dates align with active billing. +- Delivery evidence hash and customer acceptance are present. +- Purchased seat counts match provisioned user entitlements. +- Purchased API, compute, and storage quotas match provisioned limits. +- Purchased analytics or data feeds are enabled, and unpurchased feeds are not accidentally exposed. +- Support tier, SLA, and account-owner acknowledgement match the contract. +- Revenue recognition is held when delivery findings are unresolved. +- Deterministic reviewer packets explain `RELEASE_REVENUE`, `REVIEW_REVENUE`, or `HOLD_REVENUE` decisions. + +## Running Locally + +```bash +npm test +npm run demo +npm run check +npm run video +``` + +The demo uses only synthetic contract packets in `data/sample_entitlement_delivery_packets.json`. + +Generated files: + +- `reports/summary.json` +- `reports/reviewer-packet.md` +- `reports/summary.svg` +- `reports/demo.mp4` + +The MP4 is generated from an FFmpeg color source and drawtext slate. It is not a screen recording and contains no desktop capture or private information. + +## Why This Matters + +Revenue infrastructure is not only about collecting money. Institutional customers need the access, quotas, support, and licensed feeds they bought, while SCIBASE needs a clean record that delivery happened before finance systems recognize the account as complete. + +This guard gives finance and platform reviewers a deterministic packet to catch under-delivery, accidental over-entitlement, missing evidence, and unsafe revenue-recognition states before invoices, renewals, and reporting workflows move forward. diff --git a/revenue-entitlement-delivery-guard/data/sample_entitlement_delivery_packets.json b/revenue-entitlement-delivery-guard/data/sample_entitlement_delivery_packets.json new file mode 100644 index 00000000..bd376caa --- /dev/null +++ b/revenue-entitlement-delivery-guard/data/sample_entitlement_delivery_packets.json @@ -0,0 +1,166 @@ +[ + { + "id": "revenue-release-001", + "title": "Institutional license delivered cleanly", + "contract": { + "accountId": "inst-north-001", + "termStart": "2026-08-01", + "termEnd": "2027-07-31", + "billingActive": true, + "revenueRecognitionReady": true, + "revenueHold": false, + "currency": "USD", + "lines": [ + { + "sku": "institutional-license", + "amount": 48000, + "entitlements": { + "seats": 40, + "apiCalls": 100000, + "computeHours": 400, + "storageGb": 2000, + "dataFeeds": ["analytics-digest"], + "supportTier": "enterprise" + } + } + ] + }, + "delivery": { + "deliveryDate": "2026-08-05", + "evidenceHash": "sha256:ae541f458d2f6ca52b7dd8ce2a894020358d8a91f1d3b71d0fbd5f62b715db77", + "acceptedByCustomer": true, + "ownerAcknowledged": true, + "entitlements": { + "seats": 40, + "apiCalls": 100000, + "computeHours": 400, + "storageGb": 2000, + "dataFeeds": ["analytics-digest"], + "supportTier": "enterprise" + } + } + }, + { + "id": "revenue-hold-002", + "title": "Active billing before customer access is delivered", + "contract": { + "accountId": "lab-east-002", + "termStart": "2026-08-01", + "termEnd": "2027-07-31", + "billingActive": true, + "revenueRecognitionReady": true, + "revenueHold": false, + "currency": "USD", + "lines": [ + { + "sku": "lab-pro", + "amount": 12000, + "entitlements": { + "seats": 25, + "apiCalls": 50000, + "computeHours": 250, + "storageGb": 1000, + "dataFeeds": ["usage-metadata"], + "supportTier": "professional" + } + } + ] + }, + "delivery": { + "deliveryDate": "2026-08-07", + "evidenceHash": "", + "acceptedByCustomer": false, + "ownerAcknowledged": false, + "entitlements": { + "seats": 12, + "apiCalls": 0, + "computeHours": 100, + "storageGb": 1000, + "dataFeeds": [], + "supportTier": "standard" + } + } + }, + { + "id": "revenue-review-003", + "title": "Expansion account over-provisioned before true-up", + "contract": { + "accountId": "r-and-d-west-003", + "termStart": "2026-07-15", + "termEnd": "2027-07-14", + "billingActive": true, + "revenueRecognitionReady": false, + "revenueHold": true, + "currency": "USD", + "lines": [ + { + "sku": "analytics-api", + "amount": 18000, + "entitlements": { + "seats": 10, + "apiCalls": 250000, + "computeHours": 0, + "storageGb": 200, + "dataFeeds": ["analytics-digest"], + "supportTier": "professional" + } + } + ] + }, + "delivery": { + "deliveryDate": "2026-08-01", + "evidenceHash": "sha256:b467e61f098e041c5a67663a8efef30d11016e7fb4a9de264aa9fce7e4ba0f14", + "acceptedByCustomer": true, + "ownerAcknowledged": true, + "entitlements": { + "seats": 15, + "apiCalls": 300000, + "computeHours": 0, + "storageGb": 200, + "dataFeeds": ["analytics-digest", "licensed-benchmark-feed"], + "supportTier": "professional" + } + } + }, + { + "id": "revenue-hold-004", + "title": "Expired data license still marked revenue-ready", + "contract": { + "accountId": "consortium-south-004", + "termStart": "2025-06-01", + "termEnd": "2026-05-31", + "billingActive": true, + "revenueRecognitionReady": true, + "revenueHold": false, + "currency": "USD", + "lines": [ + { + "sku": "data-license", + "amount": 36000, + "entitlements": { + "seats": 5, + "apiCalls": 10000, + "computeHours": 0, + "storageGb": 100, + "dataFeeds": ["licensed-benchmark-feed"], + "supportTier": "standard" + } + } + ] + }, + "delivery": { + "deliveryDate": "2026-08-15", + "evidenceHash": "sha256:c1375f1fb07f24f37a637f080e24c2d30500fc40edfc989850224123ca2ed33f", + "acceptedByCustomer": true, + "ownerAcknowledged": true, + "entitlements": { + "seats": 5, + "apiCalls": 10000, + "computeHours": 0, + "storageGb": 100, + "dataFeeds": ["licensed-benchmark-feed"], + "supportTier": "standard" + } + } + } +] diff --git a/revenue-entitlement-delivery-guard/package.json b/revenue-entitlement-delivery-guard/package.json new file mode 100644 index 00000000..0536e592 --- /dev/null +++ b/revenue-entitlement-delivery-guard/package.json @@ -0,0 +1,13 @@ +{ + "name": "revenue-entitlement-delivery-guard", + "version": "1.0.0", + "description": "Dependency-free guard for revenue contract-to-entitlement delivery evidence.", + "main": "src/index.js", + "scripts": { + "test": "node --test", + "demo": "node scripts/demo.js", + "video": "node scripts/render-demo-video.js", + "check": "node --check src/index.js && node --check scripts/demo.js && node --check scripts/render-demo-video.js" + }, + "license": "MIT" +} diff --git a/revenue-entitlement-delivery-guard/reports/demo.mp4 b/revenue-entitlement-delivery-guard/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..0553a57af2a3767edd8362ee97b6de5a55d01781 GIT binary patch literal 51493 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 pBm{ + + Revenue Entitlement Delivery Guard + Synthetic contract-to-provisioning audit before billing, renewal, or revenue recognition. + Decision distribution + HOLD + + 2 + REVIEW + + 1 + RELEASE + + 1 + Checks delivery evidence, acceptance, seats, quotas, feeds, support tier, contract term, and revenue holds. + Synthetic generated artifact only. No desktop capture or private data. + diff --git a/revenue-entitlement-delivery-guard/scripts/demo.js b/revenue-entitlement-delivery-guard/scripts/demo.js new file mode 100644 index 00000000..dcb94f9f --- /dev/null +++ b/revenue-entitlement-delivery-guard/scripts/demo.js @@ -0,0 +1,77 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { analyzeEntitlementDeliveryPackets } = require("../src"); + +const root = path.resolve(__dirname, ".."); +const dataPath = path.join(root, "data", "sample_entitlement_delivery_packets.json"); +const outDir = path.join(root, "reports"); +const packets = JSON.parse(fs.readFileSync(dataPath, "utf8")); +const report = analyzeEntitlementDeliveryPackets(packets); + +fs.mkdirSync(outDir, { recursive: true }); +fs.writeFileSync(path.join(outDir, "summary.json"), `${JSON.stringify(report, null, 2)}\n`); + +const markdown = [ + "# Revenue Entitlement Delivery Guard Report", + "", + `Generated: ${report.generatedAt}`, + `Contract packets analyzed: ${report.totalPackets}`, + `Decision counts: RELEASE_REVENUE ${report.counts.RELEASE_REVENUE}, REVIEW_REVENUE ${report.counts.REVIEW_REVENUE}, HOLD_REVENUE ${report.counts.HOLD_REVENUE}`, + "", + "## Findings", + "", + ...report.results.flatMap((item) => [ + `### ${item.id}: ${item.title}`, + "", + `Decision: ${item.decision}`, + `Account: ${item.accountId || "not declared"}`, + "", + item.findings.length === 0 + ? "- No entitlement delivery risk detected." + : item.findings.map((finding) => `- ${finding.severity.toUpperCase()} ${finding.code}: ${finding.message}`).join("\n"), + "", + "Recommended actions:", + ...item.recommendedActions.map((action) => `- ${action}`), + "" + ]) +].join("\n"); + +fs.writeFileSync(path.join(outDir, "reviewer-packet.md"), markdown); + +const total = Math.max(1, report.totalPackets); +const maxBar = 620; +const holdWidth = Math.round((report.counts.HOLD_REVENUE / total) * maxBar); +const reviewWidth = Math.round((report.counts.REVIEW_REVENUE / total) * maxBar); +const releaseWidth = Math.round((report.counts.RELEASE_REVENUE / total) * maxBar); +const svg = ` + + Revenue Entitlement Delivery Guard + Synthetic contract-to-provisioning audit before billing, renewal, or revenue recognition. + Decision distribution + HOLD + + ${report.counts.HOLD_REVENUE} + REVIEW + + ${report.counts.REVIEW_REVENUE} + RELEASE + + ${report.counts.RELEASE_REVENUE} + Checks delivery evidence, acceptance, seats, quotas, feeds, support tier, contract term, and revenue holds. + Synthetic generated artifact only. No desktop capture or private data. + +`; + +fs.writeFileSync(path.join(outDir, "summary.svg"), svg); + +console.log(JSON.stringify({ + packets: report.totalPackets, + counts: report.counts, + outputs: [ + path.join(outDir, "summary.json"), + path.join(outDir, "reviewer-packet.md"), + path.join(outDir, "summary.svg") + ] +}, null, 2)); diff --git a/revenue-entitlement-delivery-guard/scripts/render-demo-video.js b/revenue-entitlement-delivery-guard/scripts/render-demo-video.js new file mode 100644 index 00000000..e17b621d --- /dev/null +++ b/revenue-entitlement-delivery-guard/scripts/render-demo-video.js @@ -0,0 +1,60 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +const root = path.resolve(__dirname, ".."); +const ffmpeg = path.resolve( + root, + "..", + "..", + "tool_downloads", + "video_tools", + "node_modules", + "ffmpeg-static", + "ffmpeg.exe" +); +const outDir = path.join(root, "reports"); +const out = path.join(outDir, "demo.mp4"); +const font = "C\\:/Windows/Fonts/arial.ttf"; + +fs.mkdirSync(outDir, { recursive: true }); + +const draw = [ + `drawtext=fontfile=${font}:text='Revenue Entitlement Delivery Guard':x=64:y=56:fontsize=42:fontcolor=black`, + `drawtext=fontfile=${font}:text='Contract to provisioning audit before recognition or renewal':x=64:y=122:fontsize=25:fontcolor=0x424b3e`, + `drawtext=fontfile=${font}:text='Synthetic contract packets analyzed 4':x=64:y=214:fontsize=34:fontcolor=black`, + `drawtext=fontfile=${font}:text='HOLD 2 REVIEW 1 RELEASE 1':x=64:y=274:fontsize=40:fontcolor=0x2f714e`, + `drawtext=fontfile=${font}:text='Detects under delivery over provisioning missing acceptance':x=64:y=366:fontsize=26:fontcolor=0x424b3e`, + `drawtext=fontfile=${font}:text='Places revenue workflows on hold until entitlements match':x=64:y=428:fontsize=26:fontcolor=0x424b3e`, + `drawtext=fontfile=${font}:text='Synthetic generated slate only no desktop capture or private data':x=64:y=492:fontsize=26:fontcolor=0x424b3e` +].join(","); + +if (!fs.existsSync(ffmpeg)) { + throw new Error(`ffmpeg binary not found at ${ffmpeg}`); +} + +const result = spawnSync(ffmpeg, [ + "-y", + "-f", + "lavfi", + "-i", + "color=c=0xfbfaf6:s=1280x720:d=8:r=30", + "-vf", + draw, + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + out +], { encoding: "utf8" }); + +if (result.status !== 0) { + process.stderr.write(result.stderr || result.stdout); + process.exit(result.status || 1); +} + +console.log(JSON.stringify({ out, bytes: fs.statSync(out).size }, null, 2)); diff --git a/revenue-entitlement-delivery-guard/src/index.js b/revenue-entitlement-delivery-guard/src/index.js new file mode 100644 index 00000000..a7508761 --- /dev/null +++ b/revenue-entitlement-delivery-guard/src/index.js @@ -0,0 +1,315 @@ +"use strict"; + +const QUOTA_FIELDS = ["seats", "apiCalls", "computeHours", "storageGb"]; +const SUPPORT_RANK = { none: 0, standard: 1, professional: 2, enterprise: 3 }; + +function list(value) { + return Array.isArray(value) ? value : []; +} + +function normalizeText(value) { + return String(value || "").trim().toLowerCase(); +} + +function finding(code, severity, message, evidence, remediation) { + return { code, severity, message, evidence, remediation }; +} + +function normalizePacket(packet) { + return { + id: packet.id, + title: packet.title || "Untitled revenue entitlement packet", + contract: packet.contract || {}, + delivery: packet.delivery || {} + }; +} + +function contractEntitlements(packet) { + const totals = { + seats: 0, + apiCalls: 0, + computeHours: 0, + storageGb: 0, + dataFeeds: new Set(), + supportTier: "none" + }; + + for (const line of list(packet.contract.lines)) { + const entitlements = line.entitlements || {}; + for (const field of QUOTA_FIELDS) { + totals[field] += Number(entitlements[field] || 0); + } + for (const feed of list(entitlements.dataFeeds)) { + totals.dataFeeds.add(normalizeText(feed)); + } + totals.supportTier = higherSupportTier(totals.supportTier, entitlements.supportTier); + } + + return { + ...totals, + dataFeeds: [...totals.dataFeeds].sort() + }; +} + +function deliveryEntitlements(packet) { + const entitlements = packet.delivery.entitlements || {}; + return { + seats: Number(entitlements.seats || 0), + apiCalls: Number(entitlements.apiCalls || 0), + computeHours: Number(entitlements.computeHours || 0), + storageGb: Number(entitlements.storageGb || 0), + dataFeeds: list(entitlements.dataFeeds).map(normalizeText).filter(Boolean).sort(), + supportTier: normalizeText(entitlements.supportTier || "none") + }; +} + +function higherSupportTier(left, right) { + const a = normalizeText(left || "none"); + const b = normalizeText(right || "none"); + return (SUPPORT_RANK[b] || 0) > (SUPPORT_RANK[a] || 0) ? b : a; +} + +function hasEvidenceHash(packet) { + return /^sha256:[a-f0-9]{64}$/i.test(String(packet.delivery.evidenceHash || "")); +} + +function missingFeeds(expected, actual) { + const present = new Set(actual); + return expected.filter((feed) => !present.has(feed)); +} + +function extraFeeds(expected, actual) { + const allowed = new Set(expected); + return actual.filter((feed) => !allowed.has(feed)); +} + +function termStatus(packet) { + const start = String(packet.contract.termStart || ""); + const end = String(packet.contract.termEnd || ""); + const delivered = String(packet.delivery.deliveryDate || ""); + if (!start || !end || !delivered) return "missing"; + if (delivered < start) return "before_term"; + if (delivered > end) return "after_term"; + return "inside_term"; +} + +function compareQuotas(expected, actual) { + return QUOTA_FIELDS.flatMap((field) => { + if (actual[field] < expected[field]) { + return [{ + field, + direction: "under", + expected: expected[field], + actual: actual[field], + delta: expected[field] - actual[field] + }]; + } + if (actual[field] > expected[field]) { + return [{ + field, + direction: "over", + expected: expected[field], + actual: actual[field], + delta: actual[field] - expected[field] + }]; + } + return []; + }); +} + +function analyzePacket(input) { + const packet = normalizePacket(input); + const expected = contractEntitlements(packet); + const actual = deliveryEntitlements(packet); + const findings = []; + const quotaDeltas = compareQuotas(expected, actual); + const absentFeeds = missingFeeds(expected.dataFeeds, actual.dataFeeds); + const unpurchasedFeeds = extraFeeds(expected.dataFeeds, actual.dataFeeds); + const status = termStatus(packet); + + if (!hasEvidenceHash(packet)) { + findings.push(finding( + "DELIVERY_EVIDENCE_HASH_MISSING", + "major", + "The entitlement delivery packet lacks a stable evidence hash.", + { evidenceHash: packet.delivery.evidenceHash || null }, + "Attach a sha256 evidence hash for the provisioning export, acceptance record, or delivery audit packet." + )); + } + + if (packet.delivery.acceptedByCustomer !== true) { + findings.push(finding( + "CUSTOMER_ACCEPTANCE_MISSING", + "major", + "The customer has not accepted or acknowledged entitlement delivery.", + { acceptedByCustomer: packet.delivery.acceptedByCustomer || false }, + "Capture customer or authorized account-owner acceptance before marking delivery complete." + )); + } + + if (packet.delivery.ownerAcknowledged !== true) { + findings.push(finding( + "ACCOUNT_OWNER_ACK_MISSING", + "major", + "The internal account owner has not acknowledged the provisioned entitlements.", + { ownerAcknowledged: packet.delivery.ownerAcknowledged || false }, + "Require account-owner acknowledgement before renewal, expansion, or recognition workflows proceed." + )); + } + + const underProvisioned = quotaDeltas.filter((item) => item.direction === "under"); + const overProvisioned = quotaDeltas.filter((item) => item.direction === "over"); + + if (underProvisioned.length > 0) { + findings.push(finding( + "ENTITLEMENT_UNDER_DELIVERED", + "critical", + "One or more purchased entitlements are under-provisioned.", + { deltas: underProvisioned }, + "Provision the purchased seats, quotas, storage, and compute before billing or revenue recognition proceeds." + )); + } + + if (overProvisioned.length > 0) { + findings.push(finding( + "ENTITLEMENT_OVER_PROVISIONED", + "major", + "One or more entitlements exceed the signed contract and need true-up or correction.", + { deltas: overProvisioned }, + "Either amend the contract with a true-up order or reduce the provisioned entitlement to the purchased limit." + )); + } + + if (absentFeeds.length > 0) { + findings.push(finding( + "LICENSED_FEED_NOT_ENABLED", + "critical", + "A purchased analytics or data feed is not enabled for the customer.", + { missingFeeds: absentFeeds }, + "Enable the purchased feed or hold revenue workflows until delivery is complete." + )); + } + + if (unpurchasedFeeds.length > 0) { + findings.push(finding( + "UNPURCHASED_FEED_ENABLED", + "major", + "An analytics or data feed is enabled without a matching contract line.", + { extraFeeds: unpurchasedFeeds }, + "Disable the unpurchased feed or attach a signed amendment before renewal or usage reporting." + )); + } + + const expectedRank = SUPPORT_RANK[expected.supportTier] || 0; + const actualRank = SUPPORT_RANK[actual.supportTier] || 0; + if (actualRank < expectedRank) { + findings.push(finding( + "SUPPORT_TIER_UNDER_DELIVERED", + "critical", + "The provisioned support tier is below the signed contract tier.", + { expected: expected.supportTier, actual: actual.supportTier }, + "Upgrade the support entitlement or hold billing and recognition until support delivery matches the contract." + )); + } else if (actualRank > expectedRank) { + findings.push(finding( + "SUPPORT_TIER_OVER_PROVISIONED", + "major", + "The provisioned support tier exceeds the signed contract tier.", + { expected: expected.supportTier, actual: actual.supportTier }, + "Confirm a commercial amendment or reduce the support tier before true-up reporting." + )); + } + + if (status !== "inside_term") { + findings.push(finding( + "DELIVERY_DATE_OUTSIDE_CONTRACT_TERM", + status === "missing" ? "major" : "critical", + "The delivery date is missing or outside the active contract term.", + { + termStart: packet.contract.termStart || null, + termEnd: packet.contract.termEnd || null, + deliveryDate: packet.delivery.deliveryDate || null, + status + }, + "Align delivery with the active contract term or execute a renewal/amendment before revenue workflows proceed." + )); + } + + if ( + findings.length > 0 && + packet.contract.revenueRecognitionReady === true && + packet.contract.revenueHold !== true + ) { + findings.push(finding( + "REVENUE_RECOGNITION_NOT_HELD_FOR_DELIVERY_RISK", + "critical", + "Revenue recognition remains ready while entitlement-delivery findings are unresolved.", + { + revenueRecognitionReady: packet.contract.revenueRecognitionReady, + revenueHold: packet.contract.revenueHold || false + }, + "Set a revenue hold until delivery evidence, quota parity, support tier, and term checks pass." + )); + } + + const decision = decisionFor(findings); + return { + id: packet.id, + title: packet.title, + accountId: packet.contract.accountId || null, + decision, + expected, + actual, + findings, + recommendedActions: buildRecommendedActions(decision, findings) + }; +} + +function decisionFor(findings) { + if (findings.some((item) => item.severity === "critical")) return "HOLD_REVENUE"; + if (findings.some((item) => item.severity === "major")) return "REVIEW_REVENUE"; + return "RELEASE_REVENUE"; +} + +function buildRecommendedActions(decision, findings) { + if (decision === "RELEASE_REVENUE") { + return ["Allow billing, renewal, expansion, and revenue-recognition workflows to proceed with the attached delivery evidence."]; + } + + const actions = [ + "Attach entitlement findings to the revenue operations review packet.", + "Reconcile contract lines against provisioned seats, quotas, feeds, and support tier.", + "Regenerate delivery evidence after provisioning or commercial amendments are complete." + ]; + + if (findings.some((item) => item.code === "REVENUE_RECOGNITION_NOT_HELD_FOR_DELIVERY_RISK")) { + actions.unshift("Place revenue recognition and renewal automation on hold."); + } + + return actions; +} + +function analyzeEntitlementDeliveryPackets(packets) { + const results = list(packets).map(analyzePacket); + const counts = results.reduce((acc, item) => { + acc[item.decision] = (acc[item.decision] || 0) + 1; + return acc; + }, { RELEASE_REVENUE: 0, REVIEW_REVENUE: 0, HOLD_REVENUE: 0 }); + + return { + generatedAt: new Date().toISOString(), + totalPackets: results.length, + counts, + results + }; +} + +module.exports = { + analyzePacket, + analyzeEntitlementDeliveryPackets, + contractEntitlements, + deliveryEntitlements, + compareQuotas, + hasEvidenceHash, + termStatus +}; diff --git a/revenue-entitlement-delivery-guard/test/revenue-entitlement-delivery-guard.test.js b/revenue-entitlement-delivery-guard/test/revenue-entitlement-delivery-guard.test.js new file mode 100644 index 00000000..b68f1e73 --- /dev/null +++ b/revenue-entitlement-delivery-guard/test/revenue-entitlement-delivery-guard.test.js @@ -0,0 +1,67 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const test = require("node:test"); +const { + analyzePacket, + analyzeEntitlementDeliveryPackets, + contractEntitlements, + deliveryEntitlements, + compareQuotas, + hasEvidenceHash, + termStatus +} = require("../src"); +const samples = require("../data/sample_entitlement_delivery_packets.json"); + +test("releases a contract when delivery matches every purchased entitlement", () => { + const packet = samples.find((item) => item.id === "revenue-release-001"); + const result = analyzePacket(packet); + assert.equal(hasEvidenceHash(packet), true); + assert.equal(result.decision, "RELEASE_REVENUE"); + assert.equal(result.findings.length, 0); +}); + +test("holds active billing when delivery evidence and quotas are missing", () => { + const result = analyzePacket(samples.find((item) => item.id === "revenue-hold-002")); + assert.equal(result.decision, "HOLD_REVENUE"); + assert.ok(result.findings.some((item) => item.code === "DELIVERY_EVIDENCE_HASH_MISSING")); + assert.ok(result.findings.some((item) => item.code === "ENTITLEMENT_UNDER_DELIVERED")); + assert.ok(result.findings.some((item) => item.code === "LICENSED_FEED_NOT_ENABLED")); + assert.ok(result.findings.some((item) => item.code === "REVENUE_RECOGNITION_NOT_HELD_FOR_DELIVERY_RISK")); +}); + +test("routes over-provisioned expansions for review when revenue is held", () => { + const result = analyzePacket(samples.find((item) => item.id === "revenue-review-003")); + assert.equal(result.decision, "REVIEW_REVENUE"); + assert.ok(result.findings.some((item) => item.code === "ENTITLEMENT_OVER_PROVISIONED")); + assert.ok(result.findings.some((item) => item.code === "UNPURCHASED_FEED_ENABLED")); +}); + +test("detects delivery outside the active contract term", () => { + const packet = samples.find((item) => item.id === "revenue-hold-004"); + const result = analyzePacket(packet); + assert.equal(termStatus(packet), "after_term"); + assert.equal(result.decision, "HOLD_REVENUE"); + assert.ok(result.findings.some((item) => item.code === "DELIVERY_DATE_OUTSIDE_CONTRACT_TERM")); +}); + +test("compares purchased and provisioned quotas", () => { + const packet = samples.find((item) => item.id === "revenue-hold-002"); + const expected = contractEntitlements(packet); + const actual = deliveryEntitlements(packet); + const deltas = compareQuotas(expected, actual); + assert.ok(deltas.some((item) => item.field === "seats" && item.direction === "under" && item.delta === 13)); + assert.ok(deltas.some((item) => item.field === "apiCalls" && item.direction === "under" && item.delta === 50000)); +}); + +test("normalizes purchased data feeds and highest support tier", () => { + const expected = contractEntitlements(samples.find((item) => item.id === "revenue-release-001")); + assert.deepEqual(expected.dataFeeds, ["analytics-digest"]); + assert.equal(expected.supportTier, "enterprise"); +}); + +test("aggregates entitlement delivery decisions", () => { + const report = analyzeEntitlementDeliveryPackets(samples); + assert.equal(report.totalPackets, 4); + assert.deepEqual(report.counts, { RELEASE_REVENUE: 1, REVIEW_REVENUE: 1, HOLD_REVENUE: 2 }); +});