Skip to content

Handle multiple Stripe subscription items - #690

Merged
epompeii merged 3 commits into
develfrom
u/ep/stripe-default
Mar 9, 2026
Merged

Handle multiple Stripe subscription items#690
epompeii merged 3 commits into
develfrom
u/ep/stripe-default

Conversation

@epompeii

@epompeii epompeii commented Mar 8, 2026

Copy link
Copy Markdown
Member

Last year, Stripe deprecated the old usage-based billing. As we look to add bare metal runners, we will need to add a new usage-based billing product. Instead of managing both the old and new usage-based systems, move the current metrics based billing over to the new metered system.

The actual transition will take place in #689
However, in order to have a smooth cutover, we will need to have multiple Stripe subscription items active at once. The current default keys will be used for now and then cutover to the new metrics keys.

@github-actions

github-actions Bot commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

PR: #690
Base: devel
Head: u/ep/stripe-default
Commit: a0d64715b5e4c39d3589079c50b2136bf50db399


Here's my review of the PR:


PR Review: Metered Billing Migration — Subscription Item Filtering

Commits: default_price_ids, claude_1, claude_2

Summary

Adds a filter_subscription_items method to handle Stripe subscriptions that temporarily have multiple subscription items during a metered billing migration. It filters down to items matching known "default" price IDs before enforcing the existing exactly-one-item invariant.

Positives

  • Well-documented migration context in comments — clear why this exists and when it becomes a no-op
  • Good test coverage: 7 unit tests covering single match, no match, empty input, all match, multiple known IDs, None price, and integration with get_subscription_item
  • Clean separation — filtering is a pure function, easy to reason about and test
  • Error variant NoMatchingSubscriptionItem includes the total count for debuggability

Issues

1. default_price_ids() is called on every request, allocating a new HashSet each time

In get_plan_status and record_usage, self.products.default_price_ids() allocates a HashSet and two Vecs on every call. Since the product configuration is immutable after initialization, consider computing and caching this set once in the Products constructor.

2. Potential for silent misconfiguration if DEFAULT_PRICE_NAME key is missing

In Product::default_price_ids() (products.rs:72-79), if neither self.metered nor self.licensed contains the "default" key, the method returns an empty Vec. This propagates up to Products::default_price_ids() which could return an empty HashSet, causing filter_subscription_items to always return NoMatchingSubscriptionItem. This is technically caught at runtime, but a misconfiguration would surface as a confusing billing error rather than a clear startup failure. Consider validating that each product has a "default" price during Product::new().

3. Minor: commit messages are non-descriptive

The commit messages claude_1 and claude_2 don't describe the changes. Per project conventions, these should be squashed or reworded before merge (e.g., "Filter subscription items by default price IDs during metered billing migration").

Style / CLAUDE.md Compliance

  • Uses thiserror for the new error variant — correct per project standards
  • No #[allow(...)] used — good
  • Tests use strong types (SubscriptionId, PriceId) parsed from strings rather than raw strings — compliant
  • HashSet import is correctly placed in both production and test modules

Suggestion

Once the migration is complete and old subscription items are cleaned up in Stripe, this filtering code should be removed to reduce unnecessary complexity. Consider adding a // TODO(migration): remove after metered billing migration marker to make this easy to find later.


Model: claude-opus-4-6

@github-actions

github-actions Bot commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchu/ep/stripe-default
Testbedubuntu-22.04
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
microseconds (µs)
(Result Δ%)
Upper Boundary
microseconds (µs)
(Limit %)
Adapter::Json📈 view plot
🚷 view threshold
3.77 µs
(+12.50%)Baseline: 3.35 µs
4.56 µs
(82.77%)
Adapter::Magic (JSON)📈 view plot
🚷 view threshold
3.71 µs
(+10.83%)Baseline: 3.35 µs
4.49 µs
(82.78%)
Adapter::Magic (Rust)📈 view plot
🚷 view threshold
26.15 µs
(+2.50%)Baseline: 25.51 µs
31.08 µs
(84.13%)
Adapter::Rust📈 view plot
🚷 view threshold
2.86 µs
(+0.93%)Baseline: 2.83 µs
3.38 µs
(84.68%)
Adapter::RustBench📈 view plot
🚷 view threshold
2.85 µs
(+0.54%)Baseline: 2.83 µs
3.36 µs
(84.79%)
head_version_insert/batch/10📈 view plot
🚷 view threshold
105.47 µs
(+6.99%)Baseline: 98.58 µs
123.15 µs
(85.65%)
head_version_insert/batch/100📈 view plot
🚷 view threshold
244.38 µs
(+3.67%)Baseline: 235.74 µs
272.10 µs
(89.81%)
head_version_insert/batch/255📈 view plot
🚷 view threshold
468.20 µs
(+1.92%)Baseline: 459.37 µs
492.77 µs
(95.01%)
head_version_insert/batch/50📈 view plot
🚷 view threshold
165.68 µs
(+4.28%)Baseline: 158.87 µs
185.39 µs
(89.37%)
threshold_query/join/10📈 view plot
🚷 view threshold
147.84 µs
(+3.81%)Baseline: 142.42 µs
172.56 µs
(85.67%)
threshold_query/join/20📈 view plot
🚷 view threshold
162.94 µs
(+3.97%)Baseline: 156.72 µs
188.50 µs
(86.44%)
threshold_query/join/5📈 view plot
🚷 view threshold
140.55 µs
(+4.29%)Baseline: 134.77 µs
162.98 µs
(86.24%)
threshold_query/join/50📈 view plot
🚷 view threshold
203.91 µs
(+3.35%)Baseline: 197.31 µs
229.99 µs
(88.66%)
🐰 View full continuous benchmarking report in Bencher

@epompeii
epompeii force-pushed the u/ep/stripe-default branch from 2219248 to a0d6471 Compare March 8, 2026 17:17
@epompeii
epompeii merged commit d02970f into devel Mar 9, 2026
93 of 97 checks passed
@epompeii
epompeii deleted the u/ep/stripe-default branch March 9, 2026 04:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant