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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions pricing-experiment-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Pricing Experiment Guard

This self-contained revenue infrastructure module checks whether pricing and packaging experiments are safe to launch. It is designed for finance, growth, and product teams that need audit-ready controls before changing SaaS prices, entitlements, or AI-compute bundles.

## What It Checks

- A measurable hypothesis and unchanged control cohort
- Gross-margin headroom against a finance-approved floor
- Entitlement removals without grandfathering
- Variant exposure limits and rollback readiness
- Invoice copy review, customer notice windows, and tax review
- Finance/legal approvals and revenue-recognition notes

## Run It

```bash
node pricing-experiment-guard/test.js
node pricing-experiment-guard/demo.js
```

The module uses only Node.js standard library APIs.

## Public API

```js
const { evaluatePricingExperiment } = require("./index.js");

const result = evaluatePricingExperiment(experiment);
console.log(result.summary.launchDecision);
console.log(result.findings);
console.log(result.financePacket.requiredActions);
```

The guard returns a launch decision of `approve`, `needs-remediation`, or `block`, plus finance-owner remediation steps and audit questions.

## Demo

The included `demo.gif` shows a risky enterprise AI-compute price lift being blocked because it lacks a hypothesis/control, breaches margin floors, removes entitlements, exceeds rollout limits, and has incomplete billing/audit controls.
Binary file added pricing-experiment-guard/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions pricing-experiment-guard/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const { evaluatePricingExperiment } = require("./index.js");

const experiment = {
title: "Enterprise AI Compute Price Lift",
hypothesis: "",
cohorts: [
{
name: "new-pricing",
exposurePercent: 42,
priceMultiplier: 1.35,
entitlementsRemoved: ["priority-support", "included-compute-pack"],
grandfathering: false
}
],
economics: {
grossMarginFloorPercent: 55,
projectedGrossMarginPercent: 41
},
rollout: {
maxExposurePercent: 25,
rollbackMetric: "",
rollbackThreshold: null,
owner: "revenue-ops"
},
billing: {
invoiceCopyReviewed: false,
customerNoticeDays: 3,
taxTreatmentReviewed: false
},
audit: {
approvalRecords: [],
revenueRecognitionNotes: ""
}
};

const result = evaluatePricingExperiment(experiment);

console.log(`Pricing experiment guard: ${result.summary.experimentTitle}`);
console.log(`Decision: ${result.summary.launchDecision}`);
console.log(`Finance readiness: ${result.summary.financeReadiness}`);
console.log(`Findings: ${result.summary.findingCount}`);
console.log("");

for (const finding of result.findings) {
console.log(`[${finding.severity.toUpperCase()}] ${finding.title}`);
for (const evidence of finding.evidence) {
console.log(` - ${evidence}`);
}
console.log(` Remediation: ${finding.remediation}`);
console.log("");
}

console.log("Finance actions:");
for (const action of result.financePacket.requiredActions) {
console.log(`- ${action}`);
}
253 changes: 253 additions & 0 deletions pricing-experiment-guard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
const crypto = require("node:crypto");

function evaluatePricingExperiment(experiment) {
const normalized = normalizeExperiment(experiment);
const findings = [
...findMissingHypothesisOrControl(normalized),
...findMarginFloorBreach(normalized),
...findEntitlementRegressions(normalized),
...findRollbackGaps(normalized),
...findBillingNoticeGaps(normalized),
...findAuditGaps(normalized)
];

const severityCounts = countSeverities(findings);
const summary = {
experimentTitle: normalized.title,
findingCount: findings.length,
severityCounts,
launchDecision: decideLaunch(severityCounts),
financeReadiness: scoreFinanceReadiness(findings)
};

return {
summary,
findings,
financePacket: buildFinancePacket(summary, findings)
};
}

function normalizeExperiment(experiment) {
if (!experiment || typeof experiment !== "object") {
throw new TypeError("evaluatePricingExperiment expects an experiment object");
}

return {
title: experiment.title || "Untitled pricing experiment",
hypothesis: experiment.hypothesis || "",
cohorts: Array.isArray(experiment.cohorts) ? experiment.cohorts : [],
economics: experiment.economics || {},
rollout: experiment.rollout || {},
billing: experiment.billing || {},
audit: experiment.audit || {}
};
}

function findMissingHypothesisOrControl(experiment) {
const evidence = [];
if (!experiment.hypothesis.trim()) evidence.push("experiment hypothesis is missing");
if (!experiment.cohorts.some((cohort) => Number(cohort.priceMultiplier) === 1)) {
evidence.push("control cohort with unchanged pricing is missing");
}

if (evidence.length === 0) return [];

return [
finding({
type: "missing_hypothesis_or_control",
severity: "critical",
title: "Pricing experiment lacks a measurable hypothesis or control cohort",
evidence,
remediation:
"Define the business hypothesis and add a control cohort before exposing customers to changed prices."
})
];
}

function findMarginFloorBreach(experiment) {
const floor = Number(experiment.economics.grossMarginFloorPercent);
const projected = Number(experiment.economics.projectedGrossMarginPercent);

if (!Number.isFinite(floor) || !Number.isFinite(projected) || projected >= floor) {
return [];
}

return [
finding({
type: "margin_floor_breach",
severity: "high",
title: "Projected gross margin falls below the approved floor",
evidence: [`projected margin ${projected}% is below the ${floor}% floor`],
remediation:
"Reprice the cohort, reduce compute cost, or lower exposure until projected gross margin clears the finance floor."
})
];
}

function findEntitlementRegressions(experiment) {
const regressions = [];

for (const cohort of experiment.cohorts) {
const removed = Array.isArray(cohort.entitlementsRemoved) ? cohort.entitlementsRemoved : [];
if (removed.length > 0 && !cohort.grandfathering) {
regressions.push(
`${cohort.name || "unnamed cohort"} removes ${removed.join(", ")} without grandfathering`
);
}
}

if (regressions.length === 0) return [];

return [
finding({
type: "entitlement_regression",
severity: "high",
title: "Pricing change removes customer entitlements without protection",
evidence: regressions,
remediation:
"Grandfather existing customers, add customer-facing entitlement notices, or remove entitlement regressions from the experiment."
})
];
}

function findRollbackGaps(experiment) {
const evidence = [];
const maxExposure = Number(experiment.rollout.maxExposurePercent);

for (const cohort of experiment.cohorts) {
const exposure = Number(cohort.exposurePercent);
const changesPrice = Number(cohort.priceMultiplier) !== 1;
if (changesPrice && Number.isFinite(exposure) && Number.isFinite(maxExposure) && exposure > maxExposure) {
evidence.push(
`${cohort.name || "unnamed cohort"} exposure ${exposure}% exceeds max ${maxExposure}%`
);
}
}

if (!experiment.rollout.rollbackMetric) evidence.push("rollback metric is missing");
if (!experiment.rollout.rollbackThreshold) evidence.push("rollback threshold is missing");
if (!experiment.rollout.owner) evidence.push("rollout owner is missing");

if (evidence.length === 0) return [];

return [
finding({
type: "rollback_gap",
severity: "medium",
title: "Rollout controls are not ready for a pricing experiment",
evidence,
remediation:
"Set cohort exposure limits, rollback metrics, rollback thresholds, and a named owner before launch."
})
];
}

function findBillingNoticeGaps(experiment) {
const evidence = [];
const noticeDays = Number(experiment.billing.customerNoticeDays);

if (!experiment.billing.invoiceCopyReviewed) evidence.push("invoice copy has not been reviewed");
if (!Number.isFinite(noticeDays) || noticeDays < 14) {
evidence.push(`customer notice window is ${Number.isFinite(noticeDays) ? noticeDays : "missing"} days`);
}
if (!experiment.billing.taxTreatmentReviewed) evidence.push("tax treatment review is missing");

if (evidence.length === 0) return [];

return [
finding({
type: "billing_notice_gap",
severity: "medium",
title: "Billing and customer notice controls are incomplete",
evidence,
remediation:
"Review invoice copy, confirm tax treatment, and provide an adequate customer notice window before billing changes go live."
})
];
}

function findAuditGaps(experiment) {
const evidence = [];
const approvals = Array.isArray(experiment.audit.approvalRecords)
? experiment.audit.approvalRecords
: [];

if (approvals.length === 0) evidence.push("finance/legal approval records are missing");
if (!experiment.audit.revenueRecognitionNotes) {
evidence.push("revenue recognition notes are missing");
}

if (evidence.length === 0) return [];

return [
finding({
type: "audit_packet_gap",
severity: "medium",
title: "Finance audit packet is incomplete",
evidence,
remediation:
"Attach finance/legal approvals and revenue-recognition notes before the experiment is approved."
})
];
}

function buildFinancePacket(summary, findings) {
if (findings.length === 0) {
return {
requiredActions: ["Proceed with monitored launch."],
ownerQuestions: [
"Are finance and support teams ready to watch refund, churn, and gross-margin triggers?"
],
auditSummary: `${summary.experimentTitle} is ready for monitored launch.`
};
}

return {
requiredActions: findings.map((finding) => finding.remediation),
ownerQuestions: [
"Is there a control cohort and a measurable revenue hypothesis?",
"Can finance confirm the projected margin stays above the approved floor?",
"Can billing roll back invoices and entitlements without customer harm?"
],
auditSummary: `${summary.experimentTitle} needs ${findings.length} remediation action(s) before launch.`
};
}

function finding(input) {
return {
id: `${input.type}-${hash(input.evidence.join("|")).slice(0, 8)}`,
type: input.type,
severity: input.severity,
title: input.title,
evidence: input.evidence,
remediation: input.remediation
};
}

function countSeverities(findings) {
const counts = { critical: 0, high: 0, medium: 0 };
for (const finding of findings) counts[finding.severity] += 1;
return counts;
}

function decideLaunch(counts) {
if (counts.critical > 0 || counts.high > 0) return "block";
if (counts.medium > 0) return "needs-remediation";
return "approve";
}

function scoreFinanceReadiness(findings) {
if (findings.some((finding) => finding.severity === "critical" || finding.severity === "high")) {
return "low";
}
if (findings.length > 0) return "medium";
return "high";
}

function hash(value) {
return crypto.createHash("sha256").update(String(value)).digest("hex");
}

module.exports = {
evaluatePricingExperiment
};
30 changes: 30 additions & 0 deletions pricing-experiment-guard/requirement-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Requirement Map

Issue: SCIBASE-AI/SCIBASE.AI#20, Revenue Infrastructure.

## Tiered Subscription Billing

- Validates pricing changes against unchanged control cohorts.
- Detects entitlement removals without grandfathering so plan changes do not silently break customer expectations.
- Checks invoice copy and customer notice readiness before billing changes go live.

## AI Compute Billing

- Enforces projected gross-margin floors for compute-heavy plan changes.
- Supports safe AI-compute bundle experiments through exposure limits and rollback triggers.

## Licensing APIs And Analytics

- Produces an audit packet that finance and analytics teams can attach to pricing-experiment reviews.
- Captures owner questions for margin, billing, and rollback readiness before revenue experiments affect customers.

## Safety And Scope

- Uses synthetic sample data only.
- Requires no payment provider, external service, credential, or network access.
- Keeps the implementation isolated under `pricing-experiment-guard/`.

## Verification

- `node pricing-experiment-guard/test.js`
- `node pricing-experiment-guard/demo.js`
Loading