fix(billing): ignore inactive prices in checkout, plan matching, and invoicing - #1811
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
🚧 Files skipped from review as they are similar to previous changes (6)
📝 WalkthroughSummary by CodeRabbit
WalkthroughChangesBilling 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
Estimated code review effort: 3 (Moderate) | ~25 minutes 🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
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. Comment |
Coverage Report for CI Build 30356841188Coverage decreased (-0.02%) to 46.925%Details
Uncovered Changes
Coverage Regressions4 previously-covered lines in 2 files lost coverage.
Coverage Stats
💛 - Coveralls |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
billing/product/service_test.go (1)
442-460: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftAdd 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
📒 Files selected for processing (5)
billing/checkout/service.gobilling/invoice/service.gobilling/plan/service.gobilling/product/product.gobilling/product/service_test.go
There was a problem hiding this comment.
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 winApply 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 forplanObj.Intervalcan therefore returnErrPerSeatLimitReachedand 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; retainhasBillableProductfor 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
📒 Files selected for processing (7)
billing/checkout/service.gobilling/plan/plan.gobilling/plan/plan_test.gobilling/plan/service.gobilling/product/product.gobilling/product/service_test.gobilling/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
5625d6b to
9b6911a
Compare
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
Price.IsActive()helper andPriceStateActive/PriceStateInactiveconstants. 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.ChangePlanskips inactive prices when building the next subscription phase.hasMatchingPriceno 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.Tests
Price.IsActive()andPlan.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
UpdateProductretire a price.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.