Skip to content

feat(products): attach rate cards to subscriptions - #5871

Open
rsempe wants to merge 2 commits into
mainfrom
products-and-plans-subscription-runtime
Open

feat(products): attach rate cards to subscriptions#5871
rsempe wants to merge 2 commits into
mainfrom
products-and-plans-subscription-runtime

Conversation

@rsempe

@rsempe rsempe commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Context

Runtime counterpart of the plan-side attachments: when a customer subscribes, the plan's rate cards are materialized onto the subscription so the billing engine has per-subscriber rows to scan, with their own anchor and clock.

Description

  • Materialize a plan's rate cards onto the subscription at creation, copying phases and overrides.
  • Support attaching rate cards directly to a subscription for sales-led (plan-less) deals.
  • Version units on an active subscription: the change requires apply_units (now | next_billing_period), closes the current row and opens a successor, so historical periods keep the units they billed with.
  • Add phase-aware rate resolution helpers.
  • Serve subscriptions on the v2 API with a catalog-shaped payload; period information lives on each attachment rather than on the plan.
  • Seed subscriptions on the example catalog.

@rsempe
rsempe force-pushed the products-and-plans-subscription-runtime branch 3 times, most recently from 42c28c5 to 41c1197 Compare July 7, 2026 12:49
@rsempe rsempe changed the title feat(products): subscription runtime — snapshots, billing, direct attach feat(products): attach rate cards to subscriptions Jul 7, 2026
@rsempe rsempe changed the title feat(products): attach rate cards to subscriptions feat(products): Attach rate cards to subscriptions Jul 7, 2026
@rsempe
rsempe force-pushed the products-and-plans-subscription-runtime branch from 41c1197 to 1d8e8ca Compare July 8, 2026 13:57
@rsempe rsempe changed the title feat(products): Attach rate cards to subscriptions feat(products): attach rate cards to subscriptions Jul 8, 2026
@rsempe
rsempe force-pushed the products-and-plans-subscription-runtime branch 3 times, most recently from aafc370 to 8b8b0d9 Compare July 8, 2026 14:29
@rsempe
rsempe force-pushed the products-and-plans-subscription-runtime branch from 8b8b0d9 to 04b2bc0 Compare July 9, 2026 08:57
@rsempe
rsempe force-pushed the products-and-plans-subscription-runtime branch from 04b2bc0 to b4fdc0f Compare July 9, 2026 10:02
@rsempe
rsempe force-pushed the products-and-plans-subscription-runtime branch from b4fdc0f to 882e320 Compare July 9, 2026 10:18
@rsempe
rsempe force-pushed the products-and-plans-subscription-runtime branch from 882e320 to be11831 Compare July 9, 2026 10:30
@rsempe
rsempe force-pushed the products-and-plans-subscription-runtime branch from be11831 to c2df6e0 Compare July 9, 2026 10:32
@rsempe
rsempe force-pushed the products-and-plans-subscription-runtime branch from a43b497 to 1cb1984 Compare July 10, 2026 09:34
@rsempe
rsempe force-pushed the products-and-plans-subscription-runtime branch from 1cb1984 to 4750454 Compare July 10, 2026 10:02
@rsempe
rsempe force-pushed the products-and-plans-subscription-runtime branch from 4750454 to c50ad2c Compare July 10, 2026 14:22
@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the default-on catalog workflow is not billable, and several new subscription-card paths break on realistic inputs.

  • Organizations::CreateService enables product_catalog for every new organization, but catalog plans have no interval and periodic billing still selects only plans.interval; no billing/fee path consumes the new card, rate, or phase resolvers. Keep rollout opt-in or integrate catalog selection/pricing, with an end-to-end billing spec.
  • Both nested card and phase lookups use active_at(Time.current). Materialization starts a future pending subscription's cards at subscription_at, so its authoring endpoints return 404 until activation. Resolve scheduled cards for pending subscriptions and cover a genuinely future subscription in request specs.
  • SubscriptionRateCard omits the sibling model's non-negative numeric units validation, while active updates call BigDecimal directly; null/non-numeric input can raise and negative pending values persist. Validate units and cover invalid/null updates.
  • Superseding a scheduled units version discards its phases and entry but not their cloned RateOverride records. Discard those overrides explicitly and test superseding a version that has an override.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new subscription API has backward-compatibility and time-versioning defects.

  • GET /api/v2/subscriptions[/:id] now hits the product-catalog guard before the existing v1 fallback, so legacy organizations receive 403 feature_unavailable where these v2 paths previously worked. Preserve the fallback for organizations without the feature and add regression coverage.
  • Future/scheduled cards are listed but cannot be addressed: materialization starts a future subscription's cards at subscription_at, while card and phase member lookups require active_at(Time.current). Scheduled unit updates have the same mismatch. Define consistent natural-key lookup semantics and test future pending subscriptions and scheduled successors.
  • Validate units before persistence/comparison. Invalid active updates can raise from BigDecimal, while pending create/update lacks the non-negative numericality validation used by PlanRateCard; add service/model coverage for malformed and negative values.
  • Serialize subscription-card creation around the slice check, mirroring PlanRateCards::CreateService; otherwise concurrent requests using different cards for the same product/filter can both pass and violate the one-card-per-slice invariant.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new subscription-rate-card paths can create ignored configuration, invalid quantities, and orphaned pricing records.

  • Reject direct attachments to legacy-plan subscriptions, matching PlanRateCards::CreateService, and cover that case; the new service currently accepts configuration the legacy billing engine ignores.
  • Validate SubscriptionRateCard#units consistently with PlanRateCard and handle invalid active-update input without BigDecimal raising; the new create/update APIs currently accept negative or malformed quantities.
  • When superseding a scheduled version, discard its soft-deletable RateOverride records before discarding its phases, as the destroy/replace services already do, and add coverage for the cleanup.
  • Make RateOverride#prorated? inherit the rate card's non-overridable proration setting and test calculator behavior; hardcoding false makes a phase override silently disable proration.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new subscription rate-card lifecycle has blocking gaps.

  • Materialize the target plan's rate cards on every catalog plan-change path: active upgrades/downgrades currently create replacements with no entries, while changing a future pending subscription leaves the old plan's entries attached. Add coverage for all three paths.
  • Make member and rate-phase lookups handle future-start pending entries consistently with the index; active_at(Time.current) makes a newly created or materialized scheduled entry return 404 from show/update/destroy.
  • Validate subscription-level units consistently with PlanRateCard and test invalid/negative values; pending writes can persist bad quantities, while active updates call BigDecimal directly and can raise instead of returning a validation error.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new subscription rate-card paths can create or expose incorrect billing state.

  • RateOverride#prorated? hard-codes false although proration is a structural rate-card setting, so phase overrides on prorated cards select the wrong charge-model behavior. Inherit the parent card's setting and cover it.
  • Direct attachment defaults started_at and next_billing_at to Time.current; a normal future pending subscription therefore gets a rate card active before the subscription. Default from the subscription start and add that case.
  • Subscription-level units lack the sibling PlanRateCard validation: negative values can persist, while malformed active updates raise from BigDecimal instead of returning a validation error. Validate and test both create/update inputs.
  • Scope list/show/update/delete to the same selected subscription record. The index currently filters only by external ID and can return cards from multiple lifecycle records sharing that ID.
  • Product-catalog materialization runs only for standalone creation. Upgrade/downgrade paths use sibling services, still compare nil legacy plan amounts, and do not materialize a newly created subscription; explicitly support or reject these transitions with coverage.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new default and subscription-card lifecycle have blocking correctness gaps.

  • Do not unconditionally enable product_catalog for every new organization yet: catalog plans have no legacy interval, while the current billing selector/date services only handle interval-based plans, so these new organizations cannot be periodically billed and their normal legacy plan writes are rejected.
  • Validate subscription-card units like plan-card units and cover invalid/negative create and update inputs. Pending writes currently accept invalid values through decimal casting, while active updates call BigDecimal directly and can raise instead of returning a validation error.
  • Scope the v2 subscription serializer and count to non-superseded card versions. After a units version becomes effective, show still returns the ended historical row and inflates applied_rate_cards_count, unlike the dedicated list query's current_and_scheduled scope.
  • When superseding a scheduled version, discard its copied rate overrides as well as its phases; discard_version currently leaves orphaned active RateOverride rows. Add a regression assertion for that cleanup.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new subscription-rate-card surface has correctness gaps in supported flows.

  • Future pending subscriptions are not authorable: materialization sets card started_at to the future subscription_at, but member and phase lookups use active_at(Time.current), so a listed scheduled card returns 404 from show/update/delete/phase endpoints. Select the pending subscription consistently, allow its current/scheduled card, and cover this path.
  • Validate units before casting or versioning. SubscriptionRateCard lacks the sibling PlanRateCard non-negative numericality validation, so pending writes can coerce malformed values while active updates raise from BigDecimal(...); add invalid-value coverage.
  • Superseding a scheduled units version discards its phases but leaves their copied RateOverride rows kept. Discard those overrides as DestroyService does and test the replacement path.
  • RateOverride#prorated? always returns false even though proration is a structural rate-card field that overrides explicitly inherit. Resolve it from the parent rate card and cover a prorated card instead of asserting the incorrect constant value.
  • Defaulting every new organization to product-catalog billing exposes an unsafe sibling path: changing an existing subscription to another catalog plan calls yearly_amount_cents with nil interval/amount and raises. Handle or explicitly reject catalog plan changes and cover the new-organization lifecycle.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new default billing path and subscription-rate-card updates have blocking correctness gaps.

  • Organizations::CreateService enables product_catalog for every new organization, but no invoice/fee path consumes SubscriptionRateCard or RateCardRate; existing billing still reads legacy charges, which catalog plans reject. Keep the flag opt-in until billing is wired and covered, or include that integration here.
  • SubscriptionRateCards::UpdateService#units_changed? raises on null/non-numeric input, while create/pending updates can persist invalid or negative units because SubscriptionRateCard lacks the units validation present on PlanRateCard. Validate units and cover invalid POST/PUT input.
  • RateOverride#prorated? always returns false despite proration being inherited from its rate card, and ChargeModels::Factory still rejects both new rate adapter classes. Wire calculator compatibility end-to-end and test a prorated phase override.
  • Superseding a scheduled units version discards its phases but not their copied RateOverride records. Mirror the override cleanup used by destroy/replace and add coverage.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — subscription rate-card lifecycle and scoping are inconsistent across the new API.

  • Member and nested-phase lookups use active_at(Time.current), so a valid future entry cannot be read, edited, deleted, or have phases authored before it starts. Support the pending/scheduled entry path and cover it with request specs.
  • The index resolves one subscription but queries by external ID, merging cards from active, pending, and historical subscription rows that share that ID. Filter by the resolved subscription ID and test this versioned-subscription case.
  • The v2 subscription serializer counts and returns all kept card versions, including superseded rows after a units change. Scope both fields consistently to current and scheduled entries and test an ended predecessor.
  • Unlike the sibling plan endpoint, the subscription rate-phase controller drops rate_override: null, preventing clients from clearing an override. Preserve explicit null and add the matching request spec.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new subscription rate-card API has correctness gaps in version and override handling.

  • Validate units like PlanRateCard and safely handle nil/non-numeric input; pending writes currently accept invalid/negative values, while active writes can raise from BigDecimal.
  • Scope the nested index to the specific subscription selected by find_subscription, and serialize/count only current_and_scheduled cards; reused external IDs can mix subscriptions, and ended unit-history rows currently leak into v2 subscription responses.
  • Mirror the plan phase endpoint's explicit-null handling so an override can be cleared, and discard copied overrides when superseding a scheduled version. Add regression coverage for these paths.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — Defaulting new organizations to the product catalog exposes subscriptions that never enter periodic billing: catalog plans have no interval, the billing scheduler only selects legacy plan intervals, and no billing path consumes or advances applied rate cards' next_billing_at. Implement catalog billing first or keep the flag opt-in.

  • Scope nested applied-card APIs to the one selected subscription and a consistent current/scheduled lifecycle: future pending cards are listed but member/rate-phase routes return 404, while reused external IDs can mix cards from several subscriptions.
  • Serialize the intended live versions instead of the raw association; ended unit-history rows currently inflate applied_rate_cards_count and duplicate cards in subscription responses.
  • When superseding a scheduled version, discard its rate overrides as well as its phases; the current cleanup leaves active orphan overrides.
  • Mirror sibling input guarantees: preserve explicit rate_override: null in the subscription phase controller and validate subscription-card units like plan-card units so invalid active values cannot raise from BigDecimal.
  • Preserve the rate card's structural proration setting for phase overrides; hard-coding RateOverride#prorated? to false contradicts the inherited-field contract and changes pricing.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the nested subscription rate-card lifecycle and versioning paths have correctness gaps.

  • Scope list/show/update/delete to the same resolved subscription ID; filtering the index by external ID returns cards from both past and pending subscriptions that share that ID.
  • Make future pending cards manageable: materialized cards start in the future but active_at(Time.current) hides them, while directly attached cards default to starting now, before the subscription.
  • Mirror the plan phase endpoint's explicit-null handling so rate_override: null actually clears the override, and cover it with a request spec.
  • Validate units as a non-negative number before assignment/comparison; negative values are accepted and malformed active updates can raise from BigDecimal instead of returning 422.
  • Serialize/count only current and scheduled applied cards in the v2 subscription shape; ended unit-history rows currently appear as duplicate applied cards.
  • When superseding a scheduled version, discard its rate overrides as well as its phases and entry; otherwise soft-deleted phases leave kept orphan overrides.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new subscription rate-card lifecycle has blocking validation and temporal-record inconsistencies.

  • Validate units like PlanRateCard and handle malformed/null active-update input before BigDecimal; the current path accepts negative values and can raise ArgumentError instead of returning 422.
  • Make future-dated cards addressable while a subscription is pending; both card and phase lookups use active_at(Time.current), so materialized or explicitly future-started entries return 404 during their only editable window.
  • Scope V2::SubscriptionSerializer to non-superseded card versions; its count and embedded collection currently expose every ended history row after unit versioning.
  • Preserve explicit rate_override: null in the subscription phase controller, matching the plan phase controller, so an existing override can be cleared; add the missing request coverage.
  • When superseding a scheduled version, discard its phase overrides as well as its phases and entry; discard_version currently leaves orphaned kept RateOverride rows.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new subscription-level catalog API exposes unreachable and invalid states.

  • Future-start rate cards (including cards materialized for future subscriptions) are excluded by active_at(Time.current), so show/update/delete and phase operations return 404 during the only editable window. Select the intended pending/current entry and cover future-start operations.
  • SubscriptionRateCard lacks the non-negative units validation used by PlanRateCard; create/pending update can persist bad quantities, while active update raises from BigDecimal for null or non-numeric input. Validate units and return normal validation errors with coverage.
  • V2::SubscriptionSerializer reads every kept rate-card version, leaking ended history into the count and embedded collection after unit changes. Scope it to the intended current/scheduled public set and test a versioned subscription response.
  • Subscription phase strong params drop explicit rate_override: null, unlike the sibling plan endpoint, so an override cannot be cleared. Preserve null and add request coverage.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the subscription rate-card lifecycle has correctness gaps that can create invalid billing state.

  • Reject direct attachments to legacy-plan subscriptions, mirroring PlanRateCards::CreateService; these entries are otherwise accepted even though legacy billing ignores them. Add coverage for this case.
  • Default a direct attachment's started_at/next_billing_at to the pending subscription's start, not Time.current; a future subscription currently gets a billing clock before activation. Add a future-subscription regression test.
  • When superseding a scheduled units version, discard its rate overrides as well as its phases; discard_version currently leaves live orphaned overrides. Cover rescheduling a version that has an override.
  • Validate malformed units before BigDecimal conversion; an active update such as units: "invalid" currently raises ArgumentError and returns a 500 instead of a validation response.
  • Make RateOverride#prorated? inherit the parent rate card's structural proration setting, as RateCardRate does; always returning false contradicts the override contract and selects the wrong calculator. Replace the test that asserts the incorrect behavior.
  • Scope the v2 subscription index/show to product-catalog plans, as the v2 plans surface does; catalog-enabled organizations can retain legacy subscriptions, which are currently returned in a shape that omits their billing fields. Add mixed-plan request coverage.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the subscription-level catalog surface has correctness gaps against its plan-side and soft-deletion patterns.

  • Lock the subscription around the slice check and create; concurrent requests can currently attach two cards for the same product/filter slice and double-price it.
  • Validate subscription-card units like plan-card units, and handle nil/non-numeric active updates without BigDecimal raising; cover invalid create and update inputs.
  • Preserve an explicit rate_override: null in subscription rate-phase params, matching the plan-side controller, and add request coverage for clearing an override.
  • Reject an explicitly empty rate_phases list instead of silently creating the default phase, matching plan-card creation, and add coverage.
  • When superseding a scheduled version, discard its duplicated rate overrides as well as its phases; otherwise kept orphan overrides accumulate.
  • Scope v2 subscription counts and nested applied cards to current/scheduled versions so closed unit-history rows are not returned after an update; cover the versioned response.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — The new subscription-rate-card lifecycle has blocking gaps around future entries, quantity validation, and version cleanup.

  • Member and rate-phase lookups use active_at(Time.current), so a supported future started_at entry appears in the index but cannot be shown, updated, deleted, or phase-managed while its subscription is pending; resolve pending/current-scheduled entries consistently and add request coverage.
  • Creation defaults started_at to Time.current, unlike plan materialization, which makes a card on a future pending subscription start before the subscription; derive the default from the subscription and test that case.
  • SubscriptionRateCard has no plan-equivalent non-negative units validation, and active updates call BigDecimal directly, so invalid or null input can be accepted on pending entries or raise on active ones; validate the API input and cover invalid values.
  • Subscription phase updates do not mirror the plan endpoint's update params: explicit rate_override: null is dropped instead of clearing the override, while position is permitted and then silently ignored.
  • Superseding a scheduled units version discards its phases but leaves their copied RateOverride rows kept; discard overrides with the version and cover the cleanup.
  • The pending/slice checks and create are not protected by a subscription lock, so concurrent requests can attach two different cards for the same product/filter slice or race activation; make the check-and-write atomic as in the plan-side flow.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new subscription catalog API has several correctness gaps:

  • Validate units consistently with PlanRateCard: active updates call BigDecimal directly and 500 on null/non-numeric input, while negative values have no validation.
  • Scope nested applied-rate-card lists to the resolved subscription ID; filtering by external ID combines active, pending, and historical subscriptions that may legally share that ID.
  • Mirror the plan-level phase API: reject an explicitly empty phase sequence and preserve rate_override: null on update so an override can be cleared.
  • Restrict v2 subscription/card endpoints to product-catalog plans; an enabled organization can still have legacy subscriptions, which these endpoints currently expose and mutate.
  • Keep materialized card started_at/next_billing_at values synchronized when a pending subscription is rescheduled or activates at a different time.
  • Do not enable product_catalog for every new organization until an end-to-end billing path consumes these new card/rate/phase records, or add coverage proving those subscriptions invoice correctly.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — valid subscription-rate-card states are not consistently addressable, and several established sibling invariants are missing.

  • Scope the nested index to the subscription selected by find_subscription (external IDs are reused), and make member/rate-phase lookups address future-started cards on pending subscriptions; add request regressions for both cases.
  • Match the plan-rate-card phase API: reject an explicitly empty rate_phases sequence instead of creating a default, and preserve rate_override: null on update so an override can be cleared; cover both behaviors.
  • Validate subscription-card units like plan-card units and return validation errors for malformed active-unit updates instead of letting BigDecimal raise; cover negative and nonnumeric values.
  • When superseding a scheduled version, discard its rate overrides as well as its phases and card so soft-deleted versions do not leave live orphan overrides.
  • Remove the new list N+1s: preload card serializer dependencies and use the loaded association size for applied_rate_cards_count, following the existing plan endpoints.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the subscription-level card lifecycle has correctness gaps in its primary pending and versioned flows.

  • Resolve cards from the selected subscription without active_at(Time.current): materialized cards on a genuinely future pending subscription have a future started_at, so show/update/delete and every rate-phase route return 404 during the only editable window; direct attachments should also default to the subscription start instead of starting before it.
  • Scope the list and V2 serializer to the selected subscription and intended versions: filtering only by external ID mixes past and pending subscriptions that share that ID, while the raw association exposes ended history and inflates applied_rate_cards_count; add reuse and unit-version request coverage.
  • Validate units like PlanRateCard and handle nil/malformed values before BigDecimal; the current model accepts negative quantities and active updates can raise instead of returning a validation error.
  • Perform the pending-state and pricing-slice checks plus creation under a subscription lock, mirroring PlanRateCards::CreateService, so concurrent requests cannot attach two cards for the same product/filter slice; also reject attachments to legacy plans whose billing engine ignores them.
  • Mirror the plan rate-phase update parameter handling so explicit rate_override: null clears an override, and discard copied overrides when a scheduled unit version is superseded.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new subscription rate-card API breaks important pending and catalog-subscription paths.

  • Member and phase lookups use active_at(Time.current) and do not consistently prefer the pending subscription, so cards materialized with a future subscription's started_at cannot be shown, updated, deleted, or phase-edited during their intended authoring window. Select the pending/scheduled entry and add a future-subscription request spec.
  • SubscriptionRateCard lacks the non-negative units validation used by PlanRateCard, while active updates call BigDecimal without handling invalid or null input. Validate units and cover invalid, null, and negative values across create and update paths.
  • Catalog cards are materialized only in the brand-new-subscription branch. Catalog plan changes still enter legacy yearly_amount_cents comparisons that raise for nil catalog amounts/intervals, and plan/start-date changes do not rebuild or reschedule existing cards. Support or explicitly reject these sibling flows and test them.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — future entries are unreachable, and subscription-level pricing diverges from established plan-side behavior.

  • Resolve nested applied-rate-card and phase routes against the same selected pending/current subscription and its current-or-scheduled entries; cards created with a future started_at currently return 404 before they can be edited.
  • Enforce the same non-negative numeric units validation as PlanRateCard; the new subscription create/update paths currently accept invalid quantities or can raise while parsing them.
  • Preserve the parent rate card's proration for RateOverride; hardcoding prorated? to false makes override phases use non-prorated pricing on prorated cards.
  • Mirror the plan-side phase update params so rate_override: null reaches RatePhases::UpdateService and clears the override instead of being dropped.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new subscription rate-card API has correctness gaps in version and authoring behavior.

  • Scope the nested index to the exact subscription selected by find_subscription; filtering by external ID currently returns cards from every historical/pending subscription that reuses that ID.
  • Default a newly attached card on a future pending subscription to the subscription start, not Time.current, so its pricing cannot become active before the subscription.
  • Keep request semantics consistent with the plan-rate-card endpoints: reject an explicit empty rate_phases list and preserve rate_override: null on phase updates so overrides can be cleared.
  • Exclude ended historical card versions from the V2 subscription card list/count, matching SubscriptionRateCardsQuery.current_and_scheduled.
  • When superseding a scheduled units version, discard its rate overrides as well as its phases/card, and add regression coverage for these edge paths.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — the new catalog subscription surface accepts legacy subscriptions and leaves key mutation invariants race-prone.

  • Scope v2 subscription list/show/nested lookups to product-catalog plans and reject legacy plans in SubscriptionRateCards::CreateService; the current success specs use legacy plans even though that billing path ignores these cards.
  • Serialize card create/update/destroy and subscription-level phase mutations on the subscription row. The unlocked slice check, unit versioning, and pending-status guards can race into duplicate pricing slices, unique-index 500s, or edits concurrent with activation; add concurrency coverage.
  • Mirror the sibling eager-loading patterns: card collections currently query subscription, rate card, and phase count per row (count instead of size), while phase collections query each override separately.

rsempe added 2 commits August 26, 2026 23:44
## Context

The subscription runtime PR grew past reviewable size, so it is split
in two. This first half covers the subscription itself; the follow-up
keeps the rate cards attached directly to subscriptions and their
endpoints.

## Description

- Materialize a plan's rate cards onto the subscription at creation,
  inheriting the subscription billing anchor and validating
  billing_anchor_date.
- Add phase-aware rate resolution helpers on the catalog models.
- Serve product-catalog subscriptions on the v2 API (index and show)
  with a v2 shape that drops the plan-interval fields and only counts
  current and scheduled rate cards.
- Guard sibling v1 flows: reject plan changes involving catalog plans
  and resync card dates when a pending subscription moves.
- Default new organizations to the product catalog.
Second half of the subscription runtime split: the base PR carries
subscription materialization and the v2 subscription endpoints; this
one carries the rate cards attached directly to a subscription.

- Attach, list, update and detach rate cards on a pending
  subscription over /api/v2, addressed by external id and card code,
  with per-card phase sequence endpoints at plan parity.
- Version the entry when units change on an active subscription:
  the current row closes and a successor opens now or at the next
  billing period, carrying phases and overrides.
- Enforce the authoring rules: pending-only edits, one card per
  pricing slice, currency match, anchor and units validation, and
  strict override inputs.
- Seed a directly-attached negotiated card in the demo catalog.
@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — this enables a catalog runtime that the current billing paths do not price.

  • Remove/gate the automatic product_catalog flag for new organizations, or implement and test end-to-end rate-card billing first: catalog plans reject legacy charges/fixed charges, while invoice and usage calculation still read only those legacy associations.
  • Eager-load subscription, rate_card, and rate_phases in the applied-rate-card index; the serializer currently triggers per-card association queries.
  • Protect the direct-attachment slice check and create with a subscription lock, matching the plan-level sibling, so concurrent requests cannot attach two cards for the same product/filter slice.

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