Skip to content

fix(billing): ignore inactive prices in checkout, plan matching, and invoicing - #1811

Merged
rohilsurana merged 8 commits into
mainfrom
fix/billing-ignore-inactive-prices
Jul 29, 2026
Merged

fix(billing): ignore inactive prices in checkout, plan matching, and invoicing#1811
rohilsurana merged 8 commits into
mainfrom
fix/billing-ignore-inactive-prices

Conversation

@rohilsurana

@rohilsurana rohilsurana commented Jul 28, 2026

Copy link
Copy Markdown
Member

What

Make every purchase and billing path ignore prices that are marked inactive. An inactive price is a retired one, and it must never be used for a new checkout, subscription, plan change, plan match, or invoice.

Why

A price can be retired by marking it inactive rather than deleted, since provider (Stripe) prices are immutable and cannot be removed. But nothing in the code filtered by state, so a retired price would still flow into checkout line items, plan changes, and plan matching. Stripe rejects an archived price in a new checkout, so a product with a retired price could fail to sell or bill the wrong amount. This change makes the system safe to hold inactive prices, which is a prerequisite for managing a product's prices over time.

Changes

  • New Price.IsActive() helper and PriceStateActive / PriceStateInactive constants. Only the active state (or an empty state, which an in-memory price carries before it is stored) counts as usable; any other value fails closed.
  • Checkout: plan checkout, one-time product checkout, and subscription sync skip inactive prices when building line items.
  • Subscription: ChangePlan skips inactive prices when building the next subscription phase.
  • Plan matching: hasMatchingPrice no longer counts an inactive price, and its error now says "no active prices".
  • Plan.IsFree() ignores retired prices, so retiring a paid price can make a plan free.
  • Credit overdraft invoice: the rate comes from the first active price, and the product must have at least one active price.
  • Empty guards: if a plan or product has billable products but no active price to bill, the flow fails with a clear error instead of sending an empty request to Stripe. A credit-only plan still passes, since it has nothing to bill by design.

Tests

Price.IsActive() and Plan.IsFree() are unit-tested, including the retired-price and unknown-state cases. The call-site changes apply those helpers as guards, and the existing billing test suites pass.

Notes

  • Behavior is unchanged today, since nothing sets a price inactive yet. This is groundwork for a follow-up that lets UpdateProduct retire a price.
  • Two items are intentionally out of this PR and must land before any price is actually retired: the web SDK still reads prices[0] and would show a retired rate to users, and the plan boot loader matches a price by name including retired ones. Both belong with the retire feature.

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Jul 28, 2026 2:27pm

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 43e974ba-8dbf-4d1f-8b89-375535f7211f

📥 Commits

Reviewing files that changed from the base of the PR and between 5625d6b and 9b6911a.

📒 Files selected for processing (6)
  • billing/checkout/service.go
  • billing/invoice/service.go
  • billing/plan/plan.go
  • billing/plan/plan_test.go
  • billing/product/product.go
  • billing/subscription/service.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • billing/plan/plan_test.go
  • billing/plan/plan.go
  • billing/subscription/service.go
  • billing/invoice/service.go
  • billing/checkout/service.go
  • billing/product/product.go

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Checkout and subscription creation now ignore retired/inactive prices, and fail if no active, billable line items can be produced for the plan interval.
    • Next-phase subscription scheduling now skips inactive plan prices and errors if billable products yield zero upcoming items.
    • Credit overdraft setup now selects the first active price and fails when none exists.
    • Free-plan detection now ignores inactive/retired paid prices.
    • Plan validation now requires matching active prices for the plan interval.
  • Tests
    • Added unit tests for price activity detection and free-plan logic.

Walkthrough

Changes

Billing now models price lifecycle states and applies active-price filtering across plan validation, checkout, subscription changes, and invoice initialization. Empty billable results now produce explicit errors.

Price lifecycle handling

Layer / File(s) Summary
Price state contract
billing/product/product.go, billing/product/service_test.go
Defines active and inactive price states, treats empty state as active, and tests the behavior.
Plan price validation and free-status handling
billing/plan/plan.go, billing/plan/plan_test.go, billing/plan/service.go
Ignores inactive prices for free-status checks and requires active interval-matching prices during plan upserts.
Checkout price filtering
billing/checkout/service.go
Skips inactive prices in checkout and direct plan application, and errors when billable items cannot be produced.
Invoice and subscription price selection
billing/invoice/service.go, billing/subscription/service.go
Selects active invoice prices and rejects subscription phases with no active billable items.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coveralls

coveralls commented Jul 28, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 30356841188

Coverage decreased (-0.02%) to 46.925%

Details

  • Coverage decreased (-0.02%) from the base build.
  • Patch coverage: 41 uncovered changes across 4 files (6 of 47 lines covered, 12.77%).
  • 4 coverage regressions across 2 files.

Uncovered Changes

File Changed Covered %
billing/checkout/service.go 22 0 0.0%
billing/invoice/service.go 9 0 0.0%
billing/subscription/service.go 8 0 0.0%
billing/plan/service.go 2 0 0.0%
Total (6 files) 47 6 12.77%

Coverage Regressions

4 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
billing/checkout/service.go 2 2.81%
billing/subscription/service.go 2 42.27%

Coverage Stats

Coverage Status
Relevant Lines: 39054
Covered Lines: 18326
Line Coverage: 46.92%
Coverage Strength: 14.35 hits per line

💛 - Coveralls

@rohilsurana
rohilsurana marked this pull request as ready for review July 28, 2026 08:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
billing/product/service_test.go (1)

442-460: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add regression coverage for the billing call sites.

This test covers IsActive() only. Add focused tests confirming inactive prices are excluded from checkout and plan matching, and that invoice initialization selects the first active price or errors when none exists.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e310b6b-7b7f-464c-ac7c-3886d0a413e5

📥 Commits

Reviewing files that changed from the base of the PR and between ab8b990 and b7e0166.

📒 Files selected for processing (5)
  • billing/checkout/service.go
  • billing/invoice/service.go
  • billing/plan/service.go
  • billing/product/product.go
  • billing/product/service_test.go

Comment thread billing/checkout/service.go
Comment thread billing/plan/service.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
billing/subscription/service.go (1)

651-669: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply per-seat limits only after filtering unusable prices.

The existing per-seat check on Lines [659]-[664] runs before the new IsActive() filter. A per-seat product with no active price for planObj.Interval can therefore return ErrPerSeatLimitReached and block the change even when another product has a valid active price. Determine whether the product has a usable active price before enforcing its seat limit; retain hasBillableProduct for the final empty-phase validation.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1eb8638f-d159-4b7a-a053-be050f144744

📥 Commits

Reviewing files that changed from the base of the PR and between b7e0166 and a382995.

📒 Files selected for processing (7)
  • billing/checkout/service.go
  • billing/plan/plan.go
  • billing/plan/plan_test.go
  • billing/plan/service.go
  • billing/product/product.go
  • billing/product/service_test.go
  • billing/subscription/service.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • billing/plan/service.go
  • billing/product/product.go
  • billing/product/service_test.go
  • billing/checkout/service.go

Comment thread billing/product/product.go
@rohilsurana
rohilsurana merged commit 1b1f3a3 into main Jul 29, 2026
8 checks passed
@rohilsurana
rohilsurana deleted the fix/billing-ignore-inactive-prices branch July 29, 2026 06:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants