Skip to content

feat(catalog): wire metered billing through ProcessService - #6396

Open
tiagolupepic wants to merge 10 commits into
mainfrom
feat/process-metered-items-billing-segments
Open

tiagolupepic wants to merge 10 commits into
mainfrom
feat/process-metered-items-billing-segments

Conversation

@tiagolupepic

Copy link
Copy Markdown
Contributor

This commit introduces the metered items compute Fee in ProcessService

  • Remove fixed-only filter from billing_segments scope to include metered
  • Partition segments by product_type using PRODUCT_TYPES constants
  • Add attach_fixed_fees and attach_metered_fees methods
  • Call ChargeService for metered segments with pre-loaded filter targets
  • Add specs for count/sum aggregations, graduated pricing, minimum amounts
  • Add mixed fixed+metered segments spec

Extend Fees::ChargeService so a metered product's BillingSegment can be
priced into product-backed fees, without going through a legacy Charge.

- MeteredItem gains a from_billing_segment source and drives buckets
  through source.pricing_buckets, so filter expansion, true-up target and
  filter association are resolved polymorphically by the source rather
  than assuming charge-shaped data.
- Segment-backed fees persist fee_type: :product with the product as
  invoiceable and set product_filter_id (never charge_filter_id). Legacy
  charge filters are not expanded, adjusted-fee lookup is skipped, and no
  charge-shaped CachedAggregation is written.
- Segment items use a nil cache path instead of building a charge cache
  middleware from fake boundaries.
- Fees::CreateTrueUpService supports segment-backed true-ups, reading the
  prorated minimum, proration ratio and pricing-unit conversion from the
  segment snapshot while leaving charge-backed true-ups unchanged.
- Add fees.product_filter_id (nullable, indexed, FK validated in a paired
  migration) and the discarded-aware Fee#product_filter association.

Billing-state idempotency is owned by BillingSegment persistence, so
fee-level already_billed? is bypassed for segments. Store/enrichment and
cache identity by product_id remain out of scope and are tracked by TODOs.

Covered by charge_service_billing_segment_spec and create_true_up_service_spec.
- Remove fixed-only filter from billing_segments scope to include metered
- Partition segments by product_type using PRODUCT_TYPES constants
- Add attach_fixed_fees and attach_metered_fees methods
- Call ChargeService for metered segments with pre-loaded filter targets
- Add specs for count/sum aggregations, graduated pricing, minimum amounts
- Add mixed fixed+metered segments spec
…o feat/process-metered-items-billing-segments
@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

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

HOLD: Consolidated metered segments from different contracts can be billed with zero usage.

  • build_invoice groups segments across contracts but resolves event filters using only the first contract. The resolver queries that contract's external ID, so ChargeService can bypass aggregation for another contract's segment. Resolve filters in each segment's contract context and add a regression spec with two consolidated metered contracts that both have usage.

…ices

Previously, build_invoice grouped segments across contracts but resolved
event filters using only the first contract's external_id. This caused
ChargeService to bypass aggregation for another contract's segment.

Changes:
- Remove contract parameter from BillingPeriodFilterService.for_billing_segments!
- Remove contract dependency from BillingSegmentsResolver
- Memoize event_store by contract.id in BillingSegmentsResolver
- Aggregate event combinations across all contracts in filter_targets
- Add regression spec with two consolidated metered contracts

The BillingSegmentsResolver now:
1. Extracts unique contracts from the billing segments
2. Creates a separate event store for each contract (memoized by id)
3. Queries distinct_codes_and_property_combinations for each contract
4. Merges the results before matching against product filters
## Context

Consolidated billing segments can belong to different contracts with different metric codes, event boundaries, and filter keys. Recurring metrics also need their historical event pass to retain ingestion timestamps.

## Description

Scope billing-segment event resolution by contract and preserve recurring history handling. Add regression coverage for consolidated contracts with different billing periods and product filters.
Billing segments from different contracts can reference the same product while using different event boundaries and filter sets. Product-only target keys merge their filter results.

Qualify billing-segment target keys with the contract and product identities, use the key when attaching filtered aggregations, and add regression coverage for the shared-product scenario.
@tiagolupepic

tiagolupepic commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

build_invoice groups segments across contracts but resolves event filters using only the first contract. The resolver queries that contract's external ID, so ChargeService can bypass aggregation for another contract's segment. Resolve filters in each segment's contract context and add a regression spec with two consolidated metered contracts that both have usage.

Refactored the event filters to use the contract scope.

  • Removed the contract from_billing_segments!.
  • BillingSegmentsResolver is responsible to group by contract and define the boundaries (from_datetime / to_datetime) per contract
  • Changed the queries of codes per contract (recurrence and not recurrence)
  • Changed the target_key to use contract id scope. During the billing, the customer can have two different contracts (pricing) to the same product X. Using the previous target_key might create collisions because the timestamps will be put in the the same bucket and ChargeService will receive wrong data to filter

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

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

HOLD — metered segment processing now accepts configurations that the contract-backed charge path cannot execute.

  • A valid recurring weighted_sum_agg reaches WeightedSumService#latest_value, which calls billing_context.previous_subscription_id?; Billing::Context raises NotImplementedError for contracts, rolling back the invoice group.
  • Advance rate cards using latest_agg, max_agg, or weighted_sum_agg are also accepted by current validations, but AggregationFactory explicitly raises during finalization. Restrict unsupported configurations or implement contract-compatible behavior, and cover these cases in the process-service specs.

@tiagolupepic

Copy link
Copy Markdown
Contributor Author

A valid recurring weighted_sum_agg reaches WeightedSumService#latest_value, which calls billing_context.previous_subscription_id?; Billing::Context raises NotImplementedError for contracts, rolling back the invoice group.
Advance rate cards using latest_agg, max_agg, or weighted_sum_agg are also accepted by current validations, but AggregationFactory explicitly raises during finalization. Restrict unsupported configurations or implement contract-compatible behavior, and cover these cases in the process-service specs.

Both options are going to be addressed soon in another ticket. It's part of Billing::Context

Base automatically changed from feat/update-charge-service-metered-billing-segment to main September 14, 2026 15:44
@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

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

HOLD — the broadened pending scope processes valid metered configurations that this contract-backed fee path cannot safely finalize.

  • ProcessService#pending_segments now includes advance metered cards, which are valid and due at the period start; it aggregates before usage exists and marks the segment done, while no contract pay-in-advance event path bills later usage. Exclude these cards or implement advance handling, with a regression spec.
  • A valid recurring weighted-sum product reaches Billing::Context#previous_subscription_id? through WeightedSumService and raises NotImplementedError for the contract context. Support contract-backed recurring state or keep this configuration out of processing, and cover it.
  • BillingSegmentsResolver flattens combinations from every contract and then matches each combination against every same-code target, so filter buckets and last-seen timestamps leak across contracts. Preserve contract identity during matching and strengthen the same-product multi-contract spec to assert per-contract values.

Consolidated invoices can contain the same product under multiple contracts. Event combinations and their ingestion timestamps are contract-specific.

Match each contract's event combinations only against its own billing segment targets and add regression coverage for filter buckets and last-seen timestamps.
@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

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

HOLD: Metered segment processing now incorrectly finalizes supported advance-billed cards at the segment start.

  • pending_segments admits every metered segment, then compute_metered_fees uses the period-finalization path. Advance metered cards are modeled as per-event billing (and may be non-invoiceable/regrouped), but their segments are due at started_at; this can create a periodic invoice before usage exists and mark the segment done. Restrict this path to arrears metered cards until complete advance behavior exists, and add an advance-card regression spec.

## Context

Advance metered segments are billed per event and are not ready for periodic invoice finalization. Processing them through the metered period-finalization path can create invoices before usage exists.

## Description

Exclude advance metered segments from pending billing-segment processing while retaining arrears metered and fixed segments. Add regression coverage to ensure advance segments remain pending without an invoice.
@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

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

HOLD — periodic processing now admits every arrears metered segment, but segment-backed recurring state is not implemented.

  • Recurring weighted-sum products can raise when their aggregator calls the subscription-only previous_subscription_id? on a contract context, while recurring custom products restart without prior-period state because segment-backed cached aggregation persistence is explicitly skipped. Either support product/contract-keyed recurring state or exclude these configurations, and add periodic-processing regression coverage.

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.

1 participant