diff --git a/grandfathered-pricing-migration-guard/README.md b/grandfathered-pricing-migration-guard/README.md new file mode 100644 index 00000000..485ab04f --- /dev/null +++ b/grandfathered-pricing-migration-guard/README.md @@ -0,0 +1,36 @@ +# Grandfathered Pricing Migration Guard + +This self-contained Revenue Infrastructure slice validates whether a legacy-price customer can be migrated to a new price book before an invoice is released. + +The guard focuses on a gap that is separate from existing billing ledgers, usage metering, quota rollover, coupon checks, committed-usage true-ups, grant billing, FX settlement, tax handling, invoice delivery, and analytics-seat licensing work. + +## What it checks + +- active price locks and grandfathered contract windows +- required customer notice periods before a price increase +- explicit consent for material increases +- annual and volume discount compatibility +- included seat and compute regressions +- add-on parity during plan migration +- currency changes that need finance approval +- dispute, delinquency, and strategic-account review holds +- reviewer evidence such as order forms, runbooks, legal approval, and sales owner signoff + +## Run locally + +```sh +npm test +npm run demo +``` + +The demo writes reviewer artifacts under `artifacts/`: + +- `pricing-migration-results.json` +- `pricing-migration-report.md` +- `pricing-migration-summary.svg` +- `pricing-migration-demo.mp4` when `swift scripts/make-demo-video.swift artifacts/pricing-migration-demo.mp4` is run on macOS +- `demo-transcript.md` + +## Boundaries + +All data is synthetic. The module does not call payment processors, customer systems, pricing APIs, bank rails, external services, or private billing records. diff --git a/grandfathered-pricing-migration-guard/REQUIREMENT_MAP.md b/grandfathered-pricing-migration-guard/REQUIREMENT_MAP.md new file mode 100644 index 00000000..868d5f2a --- /dev/null +++ b/grandfathered-pricing-migration-guard/REQUIREMENT_MAP.md @@ -0,0 +1,17 @@ +# Requirement Map + +| Issue requirement | Implementation | +| --- | --- | +| Tiered subscription billing | Evaluates current and target plans, price books, billing cycles, included seats, and included compute. | +| Volume discounts and annual cycles | Blocks or reviews incompatible annual commitments, volume discounts, seat caps, and billing-cycle regressions. | +| Free trials, coupons, consortium pricing coexistence | Keeps this slice distinct by checking plan migration release readiness rather than coupon eligibility itself. | +| Institutional invoicing | Produces deterministic RELEASE, REVIEW, or HOLD decisions before legacy-price invoices can be released. | +| Usage-based pricing for AI compute | Detects target plan compute regressions against recent average compute usage. | +| Transparent quotas and usage meters | Includes included-seat and compute-hour deltas in every reviewer result. | +| High-margin, predictable recurring revenue | Prevents surprise migrations, missing consent, active price-lock violations, and add-on parity regressions before invoices ship. | +| Reviewer-ready evidence | Demo script generates JSON, Markdown, SVG, and transcript artifacts from synthetic account packets. | +| Safe contribution boundary | No payment processor calls, no live customers, no credentials, no external APIs, and no private billing records. | + +## Distinct slice statement + +This contribution is focused only on grandfathered-price and price-book migration release decisions. It intentionally does not implement generic subscriptions, payment collection, quota rollover, tax/VAT, dunning, FX settlement, grant compute budgets, committed-usage true-ups, analytics-seat licensing, invoice delivery, or coupon eligibility. diff --git a/grandfathered-pricing-migration-guard/artifacts/demo-transcript.md b/grandfathered-pricing-migration-guard/artifacts/demo-transcript.md new file mode 100644 index 00000000..f234d2c1 --- /dev/null +++ b/grandfathered-pricing-migration-guard/artifacts/demo-transcript.md @@ -0,0 +1,14 @@ +# Demo Transcript + +1. Load five synthetic legacy-price account packets. +2. Evaluate notice windows, price locks, consent thresholds, add-on parity, usage regressions, and reviewer evidence. +3. Emit deterministic invoice-release decisions: RELEASE_INVOICE, REVIEW_BEFORE_RELEASE, or HOLD_INVOICE. +4. Write JSON, Markdown, and SVG artifacts for reviewer replay. + +## Demo Output + +- Release: 1 +- Review: 1 +- Hold: 3 +- Held accounts: acct-lab-price-lock, acct-notice-gap, acct-currency-mismatch +- Review accounts: acct-grace-review diff --git a/grandfathered-pricing-migration-guard/artifacts/pricing-migration-demo.mp4 b/grandfathered-pricing-migration-guard/artifacts/pricing-migration-demo.mp4 new file mode 100644 index 00000000..135e04a6 Binary files /dev/null and b/grandfathered-pricing-migration-guard/artifacts/pricing-migration-demo.mp4 differ diff --git a/grandfathered-pricing-migration-guard/artifacts/pricing-migration-report.md b/grandfathered-pricing-migration-guard/artifacts/pricing-migration-report.md new file mode 100644 index 00000000..3934d672 --- /dev/null +++ b/grandfathered-pricing-migration-guard/artifacts/pricing-migration-report.md @@ -0,0 +1,51 @@ +# Grandfathered Pricing Migration Report + +As of: 2026-06-18 + +## Summary + +- Total accounts: 5 +- Release: 1 +- Review: 1 +- Hold: 3 +- Monthly revenue delta: USD 1,510.00 before currency-specific settlement review + +## Account Decisions + +| Account | Decision | Monthly delta | Primary reason | +| --- | --- | ---: | --- | +| Northbridge University Research Office | RELEASE_INVOICE | USD 390.00 | All release checks passed | +| Meridian Lab Collaborative | HOLD_INVOICE | USD 160.00 | ACTIVE_PRICE_LOCK: Price lock remains active until 2026-12-31. | +| Civic Methods Consortium | HOLD_INVOICE | USD 520.00 | ADD_ON_PARITY_GAP: Target plan is missing legacy add-ons: sso. | +| Helix Bioinformatics Studio | REVIEW_BEFORE_RELEASE | EUR 180.00 | INSIDE_LEGACY_GRACE_PERIOD: Grandfathering ended 29 days ago, inside the 45-day review grace period. | +| Pacific Applied Science Center | HOLD_INVOICE | HKD 260.00 | CURRENCY_CHANGE_WITHOUT_FINANCE_APPROVAL: Currency changes from USD to HKD. | + +## Release Actions + +### Northbridge University Research Office +- Release invoice under the target price book. + +### Meridian Lab Collaborative +- Keep the account on the legacy price book until the price lock expires or a new order form is signed. +- Request explicit customer approval before creating a new-price invoice. +- Hold invoice release until the notice window is satisfied. +- Collect explicit consent or signed renewal before migration. +- Attach legal approval before release. + +### Civic Methods Consortium +- Route through renewals review before release. +- Hold invoice release until the notice window is satisfied. +- Collect explicit consent or signed renewal before migration. +- Attach legal approval before release. +- Increase target seat allowance or collect admin confirmation for seat reduction. +- Confirm customer-facing compute entitlement changes before billing. +- Add equivalent add-ons or document an accepted replacement. +- Confirm volume discount recalculation with revenue operations. +- Attach named sales owner or renewal owner signoff. + +### Helix Bioinformatics Studio +- Route through renewals review before release. +- Attach legal approval before release. + +### Pacific Applied Science Center +- Add finance approval and settlement notes for the currency migration. diff --git a/grandfathered-pricing-migration-guard/artifacts/pricing-migration-results.json b/grandfathered-pricing-migration-guard/artifacts/pricing-migration-results.json new file mode 100644 index 00000000..9972fee7 --- /dev/null +++ b/grandfathered-pricing-migration-guard/artifacts/pricing-migration-results.json @@ -0,0 +1,326 @@ +{ + "asOf": "2026-06-18", + "policy": { + "noticeDays": 90, + "legacyGraceDays": 45, + "materialIncreasePercent": 15, + "maxComputeRegressionPercent": 20, + "defaultConsentThresholdPercent": 12 + }, + "summary": { + "totalAccounts": 5, + "release": 1, + "review": 1, + "hold": 3, + "totalMonthlyDeltaCents": 151000, + "heldAccountIds": [ + "acct-lab-price-lock", + "acct-notice-gap", + "acct-currency-mismatch" + ], + "reviewAccountIds": [ + "acct-grace-review" + ], + "topRisks": [ + { + "accountId": "acct-lab-price-lock", + "severity": "HOLD_INVOICE", + "code": "ACTIVE_PRICE_LOCK" + }, + { + "accountId": "acct-lab-price-lock", + "severity": "HOLD_INVOICE", + "code": "GRANDFATHERING_BLOCKS_AUTO_MIGRATION" + }, + { + "accountId": "acct-lab-price-lock", + "severity": "HOLD_INVOICE", + "code": "MISSING_MATERIAL_INCREASE_CONSENT" + }, + { + "accountId": "acct-lab-price-lock", + "severity": "HOLD_INVOICE", + "code": "NOTICE_WINDOW_NOT_MET" + }, + { + "accountId": "acct-notice-gap", + "severity": "HOLD_INVOICE", + "code": "ADD_ON_PARITY_GAP" + }, + { + "accountId": "acct-notice-gap", + "severity": "HOLD_INVOICE", + "code": "MISSING_MATERIAL_INCREASE_CONSENT" + }, + { + "accountId": "acct-notice-gap", + "severity": "HOLD_INVOICE", + "code": "NOTICE_WINDOW_NOT_MET" + }, + { + "accountId": "acct-notice-gap", + "severity": "HOLD_INVOICE", + "code": "SEAT_CAP_REGRESSION" + } + ] + }, + "results": [ + { + "accountId": "acct-univ-ready", + "accountName": "Northbridge University Research Office", + "segment": "institution", + "decision": "RELEASE_INVOICE", + "priceImpact": { + "currentMonthlyCents": 420000, + "targetMonthlyCents": 459000, + "deltaMonthlyCents": 39000, + "increasePercent": 9.29, + "displayDelta": "USD 390.00" + }, + "entitlementImpact": { + "seatDelta": 20, + "computeHourDelta": 200, + "missingAddOns": [] + }, + "evidence": { + "customerNoticeId": "notice-NBU-2026-pricebook", + "consentReceivedAt": "2026-04-02", + "financeApproval": "fin-2026-104", + "legalApproval": "legal-legacy-migration-7", + "migrationRunbook": "runbook-pricebook-2026-v3", + "salesOwner": "enterprise-renewals" + }, + "reasons": [], + "actions": [], + "riskScore": 0 + }, + { + "accountId": "acct-lab-price-lock", + "accountName": "Meridian Lab Collaborative", + "segment": "lab", + "decision": "HOLD_INVOICE", + "priceImpact": { + "currentMonthlyCents": 78000, + "targetMonthlyCents": 94000, + "deltaMonthlyCents": 16000, + "increasePercent": 20.51, + "displayDelta": "USD 160.00" + }, + "entitlementImpact": { + "seatDelta": 0, + "computeHourDelta": 0, + "missingAddOns": [] + }, + "evidence": { + "customerNoticeId": "notice-MLC-2026-pricebook", + "consentReceivedAt": null, + "financeApproval": "fin-2026-188", + "legalApproval": null, + "migrationRunbook": "runbook-pricebook-2026-v3", + "salesOwner": "lab-success" + }, + "reasons": [ + { + "severity": "HOLD_INVOICE", + "code": "ACTIVE_PRICE_LOCK", + "message": "Price lock remains active until 2026-12-31." + }, + { + "severity": "HOLD_INVOICE", + "code": "GRANDFATHERING_BLOCKS_AUTO_MIGRATION", + "message": "Contract is grandfathered until 2026-12-31 and does not allow automatic migration." + }, + { + "severity": "HOLD_INVOICE", + "code": "MISSING_MATERIAL_INCREASE_CONSENT", + "message": "Increase is 20.51% and requires consent above 10%." + }, + { + "severity": "HOLD_INVOICE", + "code": "NOTICE_WINDOW_NOT_MET", + "message": "Customer notice is 29 days old; policy requires 90 days." + }, + { + "severity": "REVIEW_BEFORE_RELEASE", + "code": "MATERIAL_INCREASE_NEEDS_LEGAL_REVIEW", + "message": "Increase is 20.51%, above the 15% materiality threshold." + } + ], + "actions": [ + "Keep the account on the legacy price book until the price lock expires or a new order form is signed.", + "Request explicit customer approval before creating a new-price invoice.", + "Hold invoice release until the notice window is satisfied.", + "Collect explicit consent or signed renewal before migration.", + "Attach legal approval before release." + ], + "riskScore": 9 + }, + { + "accountId": "acct-notice-gap", + "accountName": "Civic Methods Consortium", + "segment": "institution", + "decision": "HOLD_INVOICE", + "priceImpact": { + "currentMonthlyCents": 250000, + "targetMonthlyCents": 302000, + "deltaMonthlyCents": 52000, + "increasePercent": 20.8, + "displayDelta": "USD 520.00" + }, + "entitlementImpact": { + "seatDelta": -10, + "computeHourDelta": -80, + "missingAddOns": [ + "sso" + ] + }, + "evidence": { + "customerNoticeId": "notice-CMC-2026-pricebook", + "consentReceivedAt": null, + "financeApproval": null, + "legalApproval": null, + "migrationRunbook": "runbook-pricebook-2026-v3", + "salesOwner": null + }, + "reasons": [ + { + "severity": "HOLD_INVOICE", + "code": "ADD_ON_PARITY_GAP", + "message": "Target plan is missing legacy add-ons: sso." + }, + { + "severity": "HOLD_INVOICE", + "code": "MISSING_MATERIAL_INCREASE_CONSENT", + "message": "Increase is 20.8% and requires consent above 10%." + }, + { + "severity": "HOLD_INVOICE", + "code": "NOTICE_WINDOW_NOT_MET", + "message": "Customer notice is 21 days old; policy requires 90 days." + }, + { + "severity": "HOLD_INVOICE", + "code": "SEAT_CAP_REGRESSION", + "message": "Target plan includes 170 seats but current usage has 178." + }, + { + "severity": "REVIEW_BEFORE_RELEASE", + "code": "COMPUTE_ENTITLEMENT_REGRESSION", + "message": "Target compute allowance is 520 hours while recent usage is 581." + }, + { + "severity": "REVIEW_BEFORE_RELEASE", + "code": "INSIDE_LEGACY_GRACE_PERIOD", + "message": "Grandfathering ended 19 days ago, inside the 45-day review grace period." + }, + { + "severity": "REVIEW_BEFORE_RELEASE", + "code": "MATERIAL_INCREASE_NEEDS_LEGAL_REVIEW", + "message": "Increase is 20.8%, above the 15% materiality threshold." + }, + { + "severity": "REVIEW_BEFORE_RELEASE", + "code": "STRATEGIC_ACCOUNT_OWNER_MISSING", + "message": "Strategic account lacks a sales-owner signoff." + }, + { + "severity": "REVIEW_BEFORE_RELEASE", + "code": "VOLUME_DISCOUNT_SEAT_BASE_CHANGED", + "message": "Volume discount 11% is tied to 180 legacy seats." + } + ], + "actions": [ + "Route through renewals review before release.", + "Hold invoice release until the notice window is satisfied.", + "Collect explicit consent or signed renewal before migration.", + "Attach legal approval before release.", + "Increase target seat allowance or collect admin confirmation for seat reduction.", + "Confirm customer-facing compute entitlement changes before billing.", + "Add equivalent add-ons or document an accepted replacement.", + "Confirm volume discount recalculation with revenue operations.", + "Attach named sales owner or renewal owner signoff." + ], + "riskScore": 13 + }, + { + "accountId": "acct-grace-review", + "accountName": "Helix Bioinformatics Studio", + "segment": "lab", + "decision": "REVIEW_BEFORE_RELEASE", + "priceImpact": { + "currentMonthlyCents": 114000, + "targetMonthlyCents": 132000, + "deltaMonthlyCents": 18000, + "increasePercent": 15.79, + "displayDelta": "EUR 180.00" + }, + "entitlementImpact": { + "seatDelta": 10, + "computeHourDelta": 10, + "missingAddOns": [] + }, + "evidence": { + "customerNoticeId": "notice-HBS-2026-pricebook", + "consentReceivedAt": null, + "financeApproval": "fin-eu-2026-077", + "legalApproval": null, + "migrationRunbook": "runbook-pricebook-2026-v3", + "salesOwner": "eu-renewals" + }, + "reasons": [ + { + "severity": "REVIEW_BEFORE_RELEASE", + "code": "INSIDE_LEGACY_GRACE_PERIOD", + "message": "Grandfathering ended 29 days ago, inside the 45-day review grace period." + }, + { + "severity": "REVIEW_BEFORE_RELEASE", + "code": "MATERIAL_INCREASE_NEEDS_LEGAL_REVIEW", + "message": "Increase is 15.79%, above the 15% materiality threshold." + } + ], + "actions": [ + "Route through renewals review before release.", + "Attach legal approval before release." + ], + "riskScore": 2 + }, + { + "accountId": "acct-currency-mismatch", + "accountName": "Pacific Applied Science Center", + "segment": "institution", + "decision": "HOLD_INVOICE", + "priceImpact": { + "currentMonthlyCents": 360000, + "targetMonthlyCents": 386000, + "deltaMonthlyCents": 26000, + "increasePercent": 7.22, + "displayDelta": "HKD 260.00" + }, + "entitlementImpact": { + "seatDelta": 10, + "computeHourDelta": 40, + "missingAddOns": [] + }, + "evidence": { + "customerNoticeId": "notice-PASC-2026-pricebook", + "consentReceivedAt": "2026-04-18", + "financeApproval": null, + "legalApproval": "legal-apac-2026-02", + "migrationRunbook": "runbook-pricebook-2026-v3", + "salesOwner": "apac-renewals" + }, + "reasons": [ + { + "severity": "HOLD_INVOICE", + "code": "CURRENCY_CHANGE_WITHOUT_FINANCE_APPROVAL", + "message": "Currency changes from USD to HKD." + } + ], + "actions": [ + "Add finance approval and settlement notes for the currency migration." + ], + "riskScore": 2 + } + ] +} diff --git a/grandfathered-pricing-migration-guard/artifacts/pricing-migration-summary.svg b/grandfathered-pricing-migration-guard/artifacts/pricing-migration-summary.svg new file mode 100644 index 00000000..3132a530 --- /dev/null +++ b/grandfathered-pricing-migration-guard/artifacts/pricing-migration-summary.svg @@ -0,0 +1,12 @@ + + Grandfathered pricing migration guard summary + Portfolio release, review, and hold counts for synthetic price-book migrations. + + Grandfathered Pricing Migration Guard + Synthetic reviewer packet for Revenue Infrastructure issue #20 + + + + Release: 1 | Review: 1 | Hold: 3 + Northbridge University Research OfficeRELEASE_INVOICEMeridian Lab CollaborativeHOLD_INVOICECivic Methods ConsortiumHOLD_INVOICEHelix Bioinformatics StudioREVIEW_BEFORE_RELEASEPacific Applied Science CenterHOLD_INVOICE + diff --git a/grandfathered-pricing-migration-guard/examples/migration-packets.json b/grandfathered-pricing-migration-guard/examples/migration-packets.json new file mode 100644 index 00000000..211f6a82 --- /dev/null +++ b/grandfathered-pricing-migration-guard/examples/migration-packets.json @@ -0,0 +1,274 @@ +{ + "asOf": "2026-06-18", + "policy": { + "noticeDays": 90, + "legacyGraceDays": 45, + "materialIncreasePercent": 15, + "maxComputeRegressionPercent": 20, + "defaultConsentThresholdPercent": 12 + }, + "accounts": [ + { + "id": "acct-univ-ready", + "name": "Northbridge University Research Office", + "segment": "institution", + "currency": "USD", + "currentPlan": { + "planId": "institution-legacy-2024", + "priceBookId": "pb-legacy-2024", + "monthlyPriceCents": 420000, + "billingCycle": "annual", + "includedSeats": 240, + "includedComputeHours": 900, + "addOns": ["analytics-api", "priority-support"] + }, + "targetPlan": { + "planId": "institution-enterprise-2026", + "priceBookId": "pb-enterprise-2026", + "monthlyPriceCents": 459000, + "billingCycle": "annual", + "includedSeats": 260, + "includedComputeHours": 1100, + "addOns": ["analytics-api", "priority-support"] + }, + "contract": { + "grandfatheredUntil": "2026-04-30", + "priceLockUntil": "2026-03-31", + "allowsAutoMigration": true, + "requiresConsentAbovePct": 12, + "noticeSentAt": "2026-02-10", + "consentReceivedAt": "2026-04-02", + "signedOrderFormAt": "2026-01-18" + }, + "usage": { + "activeSeats": 221, + "averageComputeHours": 812 + }, + "discounts": { + "annualCommitment": true, + "volumePercent": 8 + }, + "evidence": { + "customerNoticeId": "notice-NBU-2026-pricebook", + "addOnParityReview": "passed", + "financeApproval": "fin-2026-104", + "legalApproval": "legal-legacy-migration-7", + "migrationRunbook": "runbook-pricebook-2026-v3", + "salesOwner": "enterprise-renewals" + }, + "flags": { + "billingDispute": false, + "delinquent": false, + "strategicAccount": true + } + }, + { + "id": "acct-lab-price-lock", + "name": "Meridian Lab Collaborative", + "segment": "lab", + "currency": "USD", + "currentPlan": { + "planId": "lab-growth-legacy", + "priceBookId": "pb-lab-2023", + "monthlyPriceCents": 78000, + "billingCycle": "monthly", + "includedSeats": 45, + "includedComputeHours": 180, + "addOns": ["private-workspaces"] + }, + "targetPlan": { + "planId": "lab-growth-2026", + "priceBookId": "pb-lab-2026", + "monthlyPriceCents": 94000, + "billingCycle": "monthly", + "includedSeats": 45, + "includedComputeHours": 180, + "addOns": ["private-workspaces"] + }, + "contract": { + "grandfatheredUntil": "2026-12-31", + "priceLockUntil": "2026-12-31", + "allowsAutoMigration": false, + "requiresConsentAbovePct": 10, + "noticeSentAt": "2026-05-20", + "signedOrderFormAt": "2025-12-19" + }, + "usage": { + "activeSeats": 39, + "averageComputeHours": 131 + }, + "discounts": { + "annualCommitment": false, + "volumePercent": 0 + }, + "evidence": { + "customerNoticeId": "notice-MLC-2026-pricebook", + "addOnParityReview": "passed", + "financeApproval": "fin-2026-188", + "migrationRunbook": "runbook-pricebook-2026-v3", + "salesOwner": "lab-success" + }, + "flags": { + "billingDispute": false, + "delinquent": false, + "strategicAccount": false + } + }, + { + "id": "acct-notice-gap", + "name": "Civic Methods Consortium", + "segment": "institution", + "currency": "USD", + "currentPlan": { + "planId": "consortium-legacy", + "priceBookId": "pb-public-sector-2024", + "monthlyPriceCents": 250000, + "billingCycle": "annual", + "includedSeats": 180, + "includedComputeHours": 600, + "addOns": ["analytics-api", "sso"] + }, + "targetPlan": { + "planId": "consortium-2026", + "priceBookId": "pb-public-sector-2026", + "monthlyPriceCents": 302000, + "billingCycle": "annual", + "includedSeats": 170, + "includedComputeHours": 520, + "addOns": ["analytics-api"] + }, + "contract": { + "grandfatheredUntil": "2026-05-30", + "priceLockUntil": "2026-05-30", + "allowsAutoMigration": true, + "requiresConsentAbovePct": 10, + "noticeSentAt": "2026-05-28", + "signedOrderFormAt": "2025-11-04" + }, + "usage": { + "activeSeats": 178, + "averageComputeHours": 581 + }, + "discounts": { + "annualCommitment": true, + "volumePercent": 11 + }, + "evidence": { + "customerNoticeId": "notice-CMC-2026-pricebook", + "addOnParityReview": "missing", + "migrationRunbook": "runbook-pricebook-2026-v3" + }, + "flags": { + "billingDispute": false, + "delinquent": false, + "strategicAccount": true + } + }, + { + "id": "acct-grace-review", + "name": "Helix Bioinformatics Studio", + "segment": "lab", + "currency": "EUR", + "currentPlan": { + "planId": "lab-pro-legacy-eu", + "priceBookId": "pb-eu-2024", + "monthlyPriceCents": 114000, + "billingCycle": "annual", + "includedSeats": 70, + "includedComputeHours": 350, + "addOns": ["priority-support"] + }, + "targetPlan": { + "planId": "lab-pro-eu-2026", + "priceBookId": "pb-eu-2026", + "monthlyPriceCents": 132000, + "billingCycle": "annual", + "includedSeats": 80, + "includedComputeHours": 360, + "addOns": ["priority-support"] + }, + "contract": { + "grandfatheredUntil": "2026-05-20", + "priceLockUntil": "2026-05-20", + "allowsAutoMigration": true, + "requiresConsentAbovePct": 20, + "noticeSentAt": "2026-02-15", + "signedOrderFormAt": "2025-10-10" + }, + "usage": { + "activeSeats": 61, + "averageComputeHours": 342 + }, + "discounts": { + "annualCommitment": true, + "volumePercent": 5 + }, + "evidence": { + "customerNoticeId": "notice-HBS-2026-pricebook", + "addOnParityReview": "passed", + "financeApproval": "fin-eu-2026-077", + "migrationRunbook": "runbook-pricebook-2026-v3", + "salesOwner": "eu-renewals" + }, + "flags": { + "billingDispute": false, + "delinquent": false, + "strategicAccount": false + } + }, + { + "id": "acct-currency-mismatch", + "name": "Pacific Applied Science Center", + "segment": "institution", + "currency": "USD", + "currentPlan": { + "planId": "institution-legacy-apac", + "priceBookId": "pb-apac-2024", + "monthlyPriceCents": 360000, + "billingCycle": "annual", + "includedSeats": 210, + "includedComputeHours": 760, + "addOns": ["analytics-api", "priority-support", "sso"] + }, + "targetPlan": { + "planId": "institution-apac-2026", + "priceBookId": "pb-apac-2026", + "monthlyPriceCents": 386000, + "billingCycle": "annual", + "currency": "HKD", + "includedSeats": 220, + "includedComputeHours": 800, + "addOns": ["analytics-api", "priority-support", "sso"] + }, + "contract": { + "grandfatheredUntil": "2026-03-31", + "priceLockUntil": "2026-03-31", + "allowsAutoMigration": true, + "requiresConsentAbovePct": 15, + "noticeSentAt": "2026-01-20", + "consentReceivedAt": "2026-04-18", + "signedOrderFormAt": "2026-01-05" + }, + "usage": { + "activeSeats": 201, + "averageComputeHours": 721 + }, + "discounts": { + "annualCommitment": true, + "volumePercent": 6 + }, + "evidence": { + "customerNoticeId": "notice-PASC-2026-pricebook", + "addOnParityReview": "passed", + "legalApproval": "legal-apac-2026-02", + "migrationRunbook": "runbook-pricebook-2026-v3", + "salesOwner": "apac-renewals" + }, + "flags": { + "billingDispute": false, + "delinquent": false, + "strategicAccount": true + } + } + ] +} diff --git a/grandfathered-pricing-migration-guard/package.json b/grandfathered-pricing-migration-guard/package.json new file mode 100644 index 00000000..bc915104 --- /dev/null +++ b/grandfathered-pricing-migration-guard/package.json @@ -0,0 +1,11 @@ +{ + "name": "grandfathered-pricing-migration-guard", + "version": "1.0.0", + "private": true, + "description": "Offline guard for legacy-price plan migrations before revenue invoices are released.", + "type": "commonjs", + "scripts": { + "test": "node --test", + "demo": "node scripts/demo.js" + } +} diff --git a/grandfathered-pricing-migration-guard/scripts/demo.js b/grandfathered-pricing-migration-guard/scripts/demo.js new file mode 100644 index 00000000..f95e4814 --- /dev/null +++ b/grandfathered-pricing-migration-guard/scripts/demo.js @@ -0,0 +1,55 @@ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const { + buildMarkdownReport, + buildSvgSummary, + evaluatePortfolio +} = require('../src'); + +const root = path.join(__dirname, '..'); +const examplesPath = path.join(root, 'examples', 'migration-packets.json'); +const artifactsDir = path.join(root, 'artifacts'); +const packet = JSON.parse(fs.readFileSync(examplesPath, 'utf8')); +const portfolio = evaluatePortfolio(packet); + +fs.mkdirSync(artifactsDir, { recursive: true }); +fs.writeFileSync( + path.join(artifactsDir, 'pricing-migration-results.json'), + `${JSON.stringify(portfolio, null, 2)}\n` +); +fs.writeFileSync( + path.join(artifactsDir, 'pricing-migration-report.md'), + buildMarkdownReport(portfolio) +); +fs.writeFileSync( + path.join(artifactsDir, 'pricing-migration-summary.svg'), + buildSvgSummary(portfolio) +); +fs.writeFileSync( + path.join(artifactsDir, 'demo-transcript.md'), + [ + '# Demo Transcript', + '', + '1. Load five synthetic legacy-price account packets.', + '2. Evaluate notice windows, price locks, consent thresholds, add-on parity, usage regressions, and reviewer evidence.', + '3. Emit deterministic invoice-release decisions: RELEASE_INVOICE, REVIEW_BEFORE_RELEASE, or HOLD_INVOICE.', + '4. Write JSON, Markdown, and SVG artifacts for reviewer replay.', + '', + '## Demo Output', + '', + `- Release: ${portfolio.summary.release}`, + `- Review: ${portfolio.summary.review}`, + `- Hold: ${portfolio.summary.hold}`, + `- Held accounts: ${portfolio.summary.heldAccountIds.join(', ') || 'none'}`, + `- Review accounts: ${portfolio.summary.reviewAccountIds.join(', ') || 'none'}` + ].join('\n') + '\n' +); + +console.log( + `Generated pricing migration artifacts for ${portfolio.summary.totalAccounts} synthetic accounts.` +); +console.log( + `Release=${portfolio.summary.release} Review=${portfolio.summary.review} Hold=${portfolio.summary.hold}` +); diff --git a/grandfathered-pricing-migration-guard/scripts/make-demo-video.swift b/grandfathered-pricing-migration-guard/scripts/make-demo-video.swift new file mode 100644 index 00000000..d0d5f2d1 --- /dev/null +++ b/grandfathered-pricing-migration-guard/scripts/make-demo-video.swift @@ -0,0 +1,183 @@ +import AVFoundation +import CoreGraphics +import CoreText +import Foundation + +let outputPath = CommandLine.arguments.dropFirst().first ?? "artifacts/pricing-migration-demo.mp4" +let outputURL = URL(fileURLWithPath: outputPath) +try? FileManager.default.removeItem(at: outputURL) +try FileManager.default.createDirectory( + at: outputURL.deletingLastPathComponent(), + withIntermediateDirectories: true +) + +let width = 1280 +let height = 720 +let fps: Int32 = 30 +let totalFrames = Int(fps) * 6 + +let writer = try AVAssetWriter(outputURL: outputURL, fileType: .mp4) +let input = AVAssetWriterInput( + mediaType: .video, + outputSettings: [ + AVVideoCodecKey: AVVideoCodecType.h264, + AVVideoWidthKey: width, + AVVideoHeightKey: height + ] +) +input.expectsMediaDataInRealTime = false + +let adaptor = AVAssetWriterInputPixelBufferAdaptor( + assetWriterInput: input, + sourcePixelBufferAttributes: [ + kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB, + kCVPixelBufferWidthKey as String: width, + kCVPixelBufferHeightKey as String: height + ] +) + +writer.add(input) +writer.startWriting() +writer.startSession(atSourceTime: .zero) + +func color(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat, _ alpha: CGFloat = 1) -> CGColor { + CGColor(red: red / 255, green: green / 255, blue: blue / 255, alpha: alpha) +} + +func drawText( + _ context: CGContext, + _ text: String, + x: CGFloat, + y: CGFloat, + size: CGFloat, + color textColor: CGColor, + weight: String = "Regular" +) { + let font = CTFontCreateWithName("Helvetica-\(weight)" as CFString, size, nil) + let attributes: [CFString: Any] = [ + kCTFontAttributeName: font, + kCTForegroundColorAttributeName: textColor + ] + let attributed = CFAttributedStringCreate(nil, text as CFString, attributes as CFDictionary)! + let line = CTLineCreateWithAttributedString(attributed) + context.textPosition = CGPoint(x: x, y: y) + CTLineDraw(line, context) +} + +func fillRounded(_ context: CGContext, rect: CGRect, radius: CGFloat, fill: CGColor) { + let path = CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil) + context.addPath(path) + context.setFillColor(fill) + context.fillPath() +} + +func renderFrame(_ buffer: CVPixelBuffer, frame: Int) { + CVPixelBufferLockBaseAddress(buffer, []) + defer { CVPixelBufferUnlockBaseAddress(buffer, []) } + + let context = CGContext( + data: CVPixelBufferGetBaseAddress(buffer), + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: CVPixelBufferGetBytesPerRow(buffer), + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue + )! + + context.setFillColor(color(248, 250, 252)) + context.fill(CGRect(x: 0, y: 0, width: width, height: height)) + context.translateBy(x: 0, y: CGFloat(height)) + context.scaleBy(x: 1, y: -1) + + let progress = min(1, CGFloat(frame) / CGFloat(totalFrames - 1)) + let reveal = min(1, progress * 1.35) + + drawText( + context, + "Grandfathered Pricing Migration Guard", + x: 72, + y: 92, + size: 44, + color: color(17, 24, 39), + weight: "Bold" + ) + drawText( + context, + "Revenue Infrastructure issue #20 - synthetic replay packet", + x: 74, + y: 132, + size: 22, + color: color(71, 85, 105) + ) + + fillRounded(context, rect: CGRect(x: 74, y: 178, width: 1132, height: 84), radius: 16, fill: color(255, 255, 255)) + drawText(context, "Portfolio result", x: 104, y: 214, size: 24, color: color(15, 23, 42), weight: "Bold") + drawText(context, "1 release | 1 review | 3 holds", x: 104, y: 246, size: 26, color: color(30, 41, 59)) + + let barY: CGFloat = 305 + let maxWidth: CGFloat = 1000 * reveal + fillRounded(context, rect: CGRect(x: 74, y: barY, width: maxWidth * 0.2, height: 36), radius: 8, fill: color(22, 163, 74)) + fillRounded(context, rect: CGRect(x: 74 + maxWidth * 0.2, y: barY, width: maxWidth * 0.2, height: 36), radius: 8, fill: color(202, 138, 4)) + fillRounded(context, rect: CGRect(x: 74 + maxWidth * 0.4, y: barY, width: maxWidth * 0.6, height: 36), radius: 8, fill: color(234, 88, 12)) + + let rows = [ + ("Northbridge University Research Office", "RELEASE_INVOICE", color(21, 128, 61)), + ("Helix Bioinformatics Studio", "REVIEW_BEFORE_RELEASE", color(161, 98, 7)), + ("Meridian Lab Collaborative", "HOLD_INVOICE", color(194, 65, 12)), + ("Civic Methods Consortium", "HOLD_INVOICE", color(194, 65, 12)), + ("Pacific Applied Science Center", "HOLD_INVOICE", color(194, 65, 12)) + ] + + for (index, row) in rows.enumerated() { + let y = CGFloat(390 + index * 44) + let rowReveal = progress > CGFloat(index) * 0.12 ? CGFloat(1) : CGFloat(0) + if rowReveal > 0 { + fillRounded(context, rect: CGRect(x: 74, y: y - 24, width: 1132, height: 34), radius: 8, fill: color(255, 255, 255)) + drawText(context, row.0, x: 98, y: y, size: 20, color: color(30, 41, 59)) + drawText(context, row.1, x: 820, y: y, size: 20, color: row.2, weight: "Bold") + } + } + + drawText( + context, + "Checks: notice window, price lock, consent threshold, add-on parity, usage regression, reviewer evidence.", + x: 74, + y: 650, + size: 19, + color: color(71, 85, 105) + ) +} + +for frame in 0.. 0 ? 100 : 0; + return ((toCents - fromCents) / fromCents) * 100; +} + +function roundPercent(value) { + return Number(value.toFixed(2)); +} + +function unique(values) { + return [...new Set(values.filter(Boolean))]; +} + +function classify(current, incoming) { + return SEVERITY_RANK[incoming] > SEVERITY_RANK[current] ? incoming : current; +} + +function formatMoney(cents, currency) { + const sign = cents < 0 ? '-' : ''; + const absolute = Math.abs(cents); + return `${sign}${currency} ${(absolute / 100).toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + })}`; +} + +function getTargetCurrency(account) { + return account.targetPlan.currency || account.currency; +} + +function findMissingAddOns(currentAddOns, targetAddOns) { + const target = new Set(targetAddOns || []); + return (currentAddOns || []).filter((addOn) => !target.has(addOn)); +} + +function evaluateAccount(account, options = {}) { + const policy = { ...DEFAULT_POLICY, ...(options.policy || {}) }; + const asOf = parseDate(options.asOf, 'asOf') || new Date(); + const current = account.currentPlan || {}; + const target = account.targetPlan || {}; + const contract = account.contract || {}; + const usage = account.usage || {}; + const discounts = account.discounts || {}; + const evidence = account.evidence || {}; + const flags = account.flags || {}; + + let decision = 'RELEASE_INVOICE'; + const reasons = []; + const actions = []; + + function add(severity, code, message, action) { + decision = classify(decision, severity); + reasons.push({ severity, code, message }); + if (action) actions.push(action); + } + + const currentPrice = Number(current.monthlyPriceCents || 0); + const targetPrice = Number(target.monthlyPriceCents || 0); + const priceDeltaCents = targetPrice - currentPrice; + const increasePercent = roundPercent(percentChange(currentPrice, targetPrice)); + const targetCurrency = getTargetCurrency(account); + + const priceLockUntil = parseDate(contract.priceLockUntil, 'contract.priceLockUntil'); + const grandfatheredUntil = parseDate(contract.grandfatheredUntil, 'contract.grandfatheredUntil'); + const noticeSentAt = parseDate(contract.noticeSentAt, 'contract.noticeSentAt'); + + if (priceDeltaCents > 0 && priceLockUntil && asOf <= priceLockUntil) { + add( + 'HOLD_INVOICE', + 'ACTIVE_PRICE_LOCK', + `Price lock remains active until ${contract.priceLockUntil}.`, + 'Keep the account on the legacy price book until the price lock expires or a new order form is signed.' + ); + } + + if (priceDeltaCents > 0 && grandfatheredUntil) { + if (asOf <= grandfatheredUntil && contract.allowsAutoMigration === false) { + add( + 'HOLD_INVOICE', + 'GRANDFATHERING_BLOCKS_AUTO_MIGRATION', + `Contract is grandfathered until ${contract.grandfatheredUntil} and does not allow automatic migration.`, + 'Request explicit customer approval before creating a new-price invoice.' + ); + } else if (asOf > grandfatheredUntil) { + const daysAfterGrandfathering = daysBetween(grandfatheredUntil, asOf); + if (daysAfterGrandfathering <= policy.legacyGraceDays) { + add( + 'REVIEW_BEFORE_RELEASE', + 'INSIDE_LEGACY_GRACE_PERIOD', + `Grandfathering ended ${daysAfterGrandfathering} days ago, inside the ${policy.legacyGraceDays}-day review grace period.`, + 'Route through renewals review before release.' + ); + } + } + } + + if (priceDeltaCents > 0) { + if (!noticeSentAt) { + add( + 'HOLD_INVOICE', + 'MISSING_CUSTOMER_NOTICE', + 'No customer price-change notice date is present.', + 'Send customer notice and restart the notice clock.' + ); + } else { + const noticeAgeDays = daysBetween(noticeSentAt, asOf); + if (noticeAgeDays < policy.noticeDays) { + add( + 'HOLD_INVOICE', + 'NOTICE_WINDOW_NOT_MET', + `Customer notice is ${noticeAgeDays} days old; policy requires ${policy.noticeDays} days.`, + 'Hold invoice release until the notice window is satisfied.' + ); + } + } + } + + const consentThreshold = Number( + contract.requiresConsentAbovePct ?? policy.defaultConsentThresholdPercent + ); + if (priceDeltaCents > 0 && increasePercent >= consentThreshold && !contract.consentReceivedAt) { + add( + 'HOLD_INVOICE', + 'MISSING_MATERIAL_INCREASE_CONSENT', + `Increase is ${increasePercent}% and requires consent above ${consentThreshold}%.`, + 'Collect explicit consent or signed renewal before migration.' + ); + } + + if (increasePercent >= policy.materialIncreasePercent && !evidence.legalApproval) { + add( + 'REVIEW_BEFORE_RELEASE', + 'MATERIAL_INCREASE_NEEDS_LEGAL_REVIEW', + `Increase is ${increasePercent}%, above the ${policy.materialIncreasePercent}% materiality threshold.`, + 'Attach legal approval before release.' + ); + } + + if (account.currency !== targetCurrency && !evidence.financeApproval) { + add( + 'HOLD_INVOICE', + 'CURRENCY_CHANGE_WITHOUT_FINANCE_APPROVAL', + `Currency changes from ${account.currency} to ${targetCurrency}.`, + 'Add finance approval and settlement notes for the currency migration.' + ); + } + + if (usage.activeSeats && target.includedSeats && target.includedSeats < usage.activeSeats) { + add( + 'HOLD_INVOICE', + 'SEAT_CAP_REGRESSION', + `Target plan includes ${target.includedSeats} seats but current usage has ${usage.activeSeats}.`, + 'Increase target seat allowance or collect admin confirmation for seat reduction.' + ); + } + + if ( + usage.averageComputeHours && + target.includedComputeHours && + current.includedComputeHours && + target.includedComputeHours < usage.averageComputeHours + ) { + const computeRegressionPercent = roundPercent( + ((current.includedComputeHours - target.includedComputeHours) / + current.includedComputeHours) * + 100 + ); + const severity = + computeRegressionPercent > policy.maxComputeRegressionPercent + ? 'HOLD_INVOICE' + : 'REVIEW_BEFORE_RELEASE'; + add( + severity, + 'COMPUTE_ENTITLEMENT_REGRESSION', + `Target compute allowance is ${target.includedComputeHours} hours while recent usage is ${usage.averageComputeHours}.`, + 'Confirm customer-facing compute entitlement changes before billing.' + ); + } + + const missingAddOns = findMissingAddOns(current.addOns, target.addOns); + if (missingAddOns.length > 0) { + add( + 'HOLD_INVOICE', + 'ADD_ON_PARITY_GAP', + `Target plan is missing legacy add-ons: ${missingAddOns.join(', ')}.`, + 'Add equivalent add-ons or document an accepted replacement.' + ); + } else if ((current.addOns || []).length > 0 && evidence.addOnParityReview !== 'passed') { + add( + 'REVIEW_BEFORE_RELEASE', + 'ADD_ON_PARITY_REVIEW_MISSING', + 'Legacy add-ons are present but no passed parity review is attached.', + 'Attach add-on parity review evidence.' + ); + } + + if (discounts.annualCommitment && target.billingCycle !== 'annual') { + add( + 'REVIEW_BEFORE_RELEASE', + 'ANNUAL_COMMITMENT_BILLING_CYCLE_CHANGE', + 'Account has an annual commitment but target plan is not annual.', + 'Review annual discount compatibility before release.' + ); + } + + if (discounts.volumePercent > 0 && target.includedSeats < current.includedSeats) { + add( + 'REVIEW_BEFORE_RELEASE', + 'VOLUME_DISCOUNT_SEAT_BASE_CHANGED', + `Volume discount ${discounts.volumePercent}% is tied to ${current.includedSeats} legacy seats.`, + 'Confirm volume discount recalculation with revenue operations.' + ); + } + + if (!contract.signedOrderFormAt && account.segment === 'institution') { + add( + 'REVIEW_BEFORE_RELEASE', + 'ORDER_FORM_EVIDENCE_MISSING', + 'Institutional account does not have signed order form evidence.', + 'Attach signed order form or renewal packet.' + ); + } + + if (!evidence.migrationRunbook) { + add( + 'REVIEW_BEFORE_RELEASE', + 'MIGRATION_RUNBOOK_MISSING', + 'No migration runbook is linked for reviewer replay.', + 'Attach the price-book migration runbook.' + ); + } + + if (flags.billingDispute) { + add( + 'HOLD_INVOICE', + 'ACTIVE_BILLING_DISPUTE', + 'Account has an active billing dispute.', + 'Resolve the dispute before releasing a migration invoice.' + ); + } + + if (flags.delinquent) { + add( + 'HOLD_INVOICE', + 'DELINQUENT_ACCOUNT', + 'Account is delinquent and should not be silently migrated.', + 'Move the account through collections or customer-success review.' + ); + } + + if (flags.strategicAccount && !evidence.salesOwner) { + add( + 'REVIEW_BEFORE_RELEASE', + 'STRATEGIC_ACCOUNT_OWNER_MISSING', + 'Strategic account lacks a sales-owner signoff.', + 'Attach named sales owner or renewal owner signoff.' + ); + } + + const sortedReasons = reasons.sort( + (a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity] || a.code.localeCompare(b.code) + ); + + return { + accountId: account.id, + accountName: account.name, + segment: account.segment, + decision, + priceImpact: { + currentMonthlyCents: currentPrice, + targetMonthlyCents: targetPrice, + deltaMonthlyCents: priceDeltaCents, + increasePercent, + displayDelta: formatMoney(priceDeltaCents, targetCurrency) + }, + entitlementImpact: { + seatDelta: Number(target.includedSeats || 0) - Number(current.includedSeats || 0), + computeHourDelta: + Number(target.includedComputeHours || 0) - Number(current.includedComputeHours || 0), + missingAddOns + }, + evidence: { + customerNoticeId: evidence.customerNoticeId || null, + consentReceivedAt: contract.consentReceivedAt || null, + financeApproval: evidence.financeApproval || null, + legalApproval: evidence.legalApproval || null, + migrationRunbook: evidence.migrationRunbook || null, + salesOwner: evidence.salesOwner || null + }, + reasons: sortedReasons, + actions: unique(actions), + riskScore: sortedReasons.reduce((total, reason) => total + SEVERITY_RANK[reason.severity], 0) + }; +} + +function evaluatePortfolio(packet, options = {}) { + const asOf = options.asOf || packet.asOf; + const policy = { ...DEFAULT_POLICY, ...(packet.policy || {}), ...(options.policy || {}) }; + const results = (packet.accounts || []).map((account) => + evaluateAccount(account, { asOf, policy }) + ); + return { + asOf, + policy, + summary: summarizeResults(results), + results + }; +} + +function summarizeResults(results) { + const summary = { + totalAccounts: results.length, + release: 0, + review: 0, + hold: 0, + totalMonthlyDeltaCents: 0, + heldAccountIds: [], + reviewAccountIds: [], + topRisks: [] + }; + + for (const result of results) { + summary.totalMonthlyDeltaCents += result.priceImpact.deltaMonthlyCents; + if (result.decision === 'RELEASE_INVOICE') summary.release += 1; + if (result.decision === 'REVIEW_BEFORE_RELEASE') { + summary.review += 1; + summary.reviewAccountIds.push(result.accountId); + } + if (result.decision === 'HOLD_INVOICE') { + summary.hold += 1; + summary.heldAccountIds.push(result.accountId); + } + } + + summary.topRisks = results + .flatMap((result) => + result.reasons.map((reason) => ({ + accountId: result.accountId, + severity: reason.severity, + code: reason.code + })) + ) + .sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]) + .slice(0, 8); + + return summary; +} + +function buildMarkdownReport(portfolio) { + const lines = [ + '# Grandfathered Pricing Migration Report', + '', + `As of: ${portfolio.asOf}`, + '', + '## Summary', + '', + `- Total accounts: ${portfolio.summary.totalAccounts}`, + `- Release: ${portfolio.summary.release}`, + `- Review: ${portfolio.summary.review}`, + `- Hold: ${portfolio.summary.hold}`, + `- Monthly revenue delta: ${formatMoney(portfolio.summary.totalMonthlyDeltaCents, 'USD')} before currency-specific settlement review`, + '', + '## Account Decisions', + '', + '| Account | Decision | Monthly delta | Primary reason |', + '| --- | --- | ---: | --- |' + ]; + + for (const result of portfolio.results) { + const primary = result.reasons[0] + ? `${result.reasons[0].code}: ${result.reasons[0].message}` + : 'All release checks passed'; + lines.push( + `| ${result.accountName} | ${result.decision} | ${result.priceImpact.displayDelta} | ${primary} |` + ); + } + + lines.push('', '## Release Actions', ''); + for (const result of portfolio.results) { + lines.push(`### ${result.accountName}`); + if (result.actions.length === 0) { + lines.push('- Release invoice under the target price book.'); + } else { + for (const action of result.actions) lines.push(`- ${action}`); + } + lines.push(''); + } + + while (lines[lines.length - 1] === '') lines.pop(); + return `${lines.join('\n')}\n`; +} + +function buildSvgSummary(portfolio) { + const width = 860; + const height = 360; + const releaseWidth = portfolio.summary.release * 90; + const reviewWidth = portfolio.summary.review * 90; + const holdWidth = portfolio.summary.hold * 90; + const escape = (value) => + String(value) + .replace(/&/g, '&') + .replace(//g, '>'); + + const rows = portfolio.results + .map((result, index) => { + const y = 176 + index * 30; + const color = + result.decision === 'HOLD_INVOICE' + ? '#c2410c' + : result.decision === 'REVIEW_BEFORE_RELEASE' + ? '#a16207' + : '#15803d'; + return `${escape(result.accountName)}${result.decision}`; + }) + .join(''); + + return ` + Grandfathered pricing migration guard summary + Portfolio release, review, and hold counts for synthetic price-book migrations. + + Grandfathered Pricing Migration Guard + Synthetic reviewer packet for Revenue Infrastructure issue #20 + + + + Release: ${portfolio.summary.release} | Review: ${portfolio.summary.review} | Hold: ${portfolio.summary.hold} + ${rows} + +`; +} + +module.exports = { + DEFAULT_POLICY, + buildMarkdownReport, + buildSvgSummary, + evaluateAccount, + evaluatePortfolio, + formatMoney, + summarizeResults +}; diff --git a/grandfathered-pricing-migration-guard/test/pricingMigrationGuard.test.js b/grandfathered-pricing-migration-guard/test/pricingMigrationGuard.test.js new file mode 100644 index 00000000..2dbf818a --- /dev/null +++ b/grandfathered-pricing-migration-guard/test/pricingMigrationGuard.test.js @@ -0,0 +1,96 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); +const { + buildMarkdownReport, + buildSvgSummary, + evaluateAccount, + evaluatePortfolio +} = require('../src'); + +const packet = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', 'examples', 'migration-packets.json'), 'utf8') +); + +function account(id) { + return packet.accounts.find((entry) => entry.id === id); +} + +test('releases a legacy account when notice, consent, entitlements, and evidence pass', () => { + const result = evaluateAccount(account('acct-univ-ready'), { + asOf: packet.asOf, + policy: packet.policy + }); + + assert.equal(result.decision, 'RELEASE_INVOICE'); + assert.equal(result.reasons.length, 0); + assert.equal(result.entitlementImpact.missingAddOns.length, 0); + assert.equal(result.priceImpact.increasePercent, 9.29); +}); + +test('holds migration invoices while a price lock blocks automatic migration', () => { + const result = evaluateAccount(account('acct-lab-price-lock'), { + asOf: packet.asOf, + policy: packet.policy + }); + + assert.equal(result.decision, 'HOLD_INVOICE'); + assert.ok(result.reasons.some((reason) => reason.code === 'ACTIVE_PRICE_LOCK')); + assert.ok( + result.reasons.some((reason) => reason.code === 'GRANDFATHERING_BLOCKS_AUTO_MIGRATION') + ); +}); + +test('holds accounts when notice windows, consent, and add-on parity are missing', () => { + const result = evaluateAccount(account('acct-notice-gap'), { + asOf: packet.asOf, + policy: packet.policy + }); + + assert.equal(result.decision, 'HOLD_INVOICE'); + assert.ok(result.reasons.some((reason) => reason.code === 'NOTICE_WINDOW_NOT_MET')); + assert.ok(result.reasons.some((reason) => reason.code === 'MISSING_MATERIAL_INCREASE_CONSENT')); + assert.ok(result.reasons.some((reason) => reason.code === 'ADD_ON_PARITY_GAP')); + assert.ok(result.reasons.some((reason) => reason.code === 'SEAT_CAP_REGRESSION')); +}); + +test('routes recently expired grandfathering windows to review before release', () => { + const result = evaluateAccount(account('acct-grace-review'), { + asOf: packet.asOf, + policy: packet.policy + }); + + assert.equal(result.decision, 'REVIEW_BEFORE_RELEASE'); + assert.ok(result.reasons.some((reason) => reason.code === 'INSIDE_LEGACY_GRACE_PERIOD')); + assert.ok( + result.reasons.some((reason) => reason.code === 'MATERIAL_INCREASE_NEEDS_LEGAL_REVIEW') + ); +}); + +test('portfolio summaries count release, review, and hold decisions deterministically', () => { + const portfolio = evaluatePortfolio(packet); + + assert.equal(portfolio.summary.totalAccounts, 5); + assert.equal(portfolio.summary.release, 1); + assert.equal(portfolio.summary.review, 1); + assert.equal(portfolio.summary.hold, 3); + assert.deepEqual(portfolio.summary.heldAccountIds, [ + 'acct-lab-price-lock', + 'acct-notice-gap', + 'acct-currency-mismatch' + ]); +}); + +test('review artifacts include account decisions and accessible SVG labels', () => { + const portfolio = evaluatePortfolio(packet); + const report = buildMarkdownReport(portfolio); + const svg = buildSvgSummary(portfolio); + + assert.match(report, /Grandfathered Pricing Migration Report/); + assert.match(report, /CURRENCY_CHANGE_WITHOUT_FINANCE_APPROVAL/); + assert.match(svg, /Grandfathered pricing migration guard summary/); + assert.match(svg, /HOLD_INVOICE/); +});