Skip to content

feat(billing): Billing engine v2 date services - #6267

Merged
brunomiguelpinto merged 2 commits into
mainfrom
engine-dates
Sep 8, 2026
Merged

brunomiguelpinto merged 2 commits into
mainfrom
engine-dates

Conversation

@brunomiguelpinto

@brunomiguelpinto brunomiguelpinto commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Contract billing needs consistent dates across rate changes, phase overrides and long billing histories. This adds a rate-card schedule that produces billable segments and can resume near a requested date without walking every historical cycle.

Behaviour

  • CycleWalker owns traversal and resume. It stops at rate and phase transitions, recalculates the calendar when the interval changes, and reuses calendars between calls. Walking the complete history remains supported.
  • Schedule splits cycles at rate changes and assigns each priced segment its own billing date: start for advance, end for arrears. Unpriced windows still consume cycle indices.
  • Proration uses customer-local calendar days and the containing interval. Schedule windows have exclusive ends; BillingSegment.inclusive_end and .exclusive_end convert stored boundaries.
  • BuildScheduleService reads ContractRateCard, resolves ordered rates and card or plan phases, and resumes from that attachment's latest persisted cycle. The last cycle is intentionally returned again, so the writer must deduplicate persisted segments. Separate attachments do not share billing history.
  • Card effective and end dates use the customer's timezone. An inclusive card end date covers the whole final day; an earlier contract end or explicit termination instant takes precedence.
  • Contract materialization seeds the initial billing clock from the schedule, including backdated contracts and phase interval overrides.

Public schedule API

Method Purpose
segments_due_by(timestamp) Return priced segments due by that instant.
next_billing_at(after:) Find the next billing instant strictly after the given time.
billing_at_covering(timestamp) Find the billing date of the segment being served, or the next priced segment.
consumed_ratio(segment:, at:) Measure consumption against the original billed segment.

Integration with main

This PR is based directly on main and contains only the date engine and its contract integration. It uses the existing contract models and billing-segment table. Existing migrations, Ruby 4.0.6 and dependencies are unchanged.

A separate migration removes the unused pricing_unit_id and rate_card_rate_id indexes from billing_segments, with concurrent operations and a reversible rollback.

The invoice writer and processor are outside this PR.

Validation

  • 422 examples passed with Ruby 4.0.6 in a temporary API container and isolated database: 113 integration/model examples and 309 date/walker/resume examples.
  • Coverage includes month ends, leap years, DST, rate and phase transitions, unpriced periods, termination, persisted history, contract materialization and equivalence between full and resumed walks.
  • RuboCop: 32 files, no offenses.
  • Migration up/down/up verified with an existing billing segment; both indexes were restored on rollback and the row was unchanged.

@brunomiguelpinto brunomiguelpinto changed the title feat: cleaned the pr feat(billing): add the product-catalog date engine Sep 1, 2026
Comment thread app/services/billing/schedules/build_service.rb Outdated
Comment thread app/services/billing/schedules/build_service.rb Outdated
Comment thread app/services/billing/calendar.rb Outdated
Comment thread app/services/billing/schedule.rb Outdated
Comment thread app/services/billing/schedule.rb Outdated
Comment thread app/services/billing/segments.rb Outdated
Comment thread app/services/billing/segments.rb Outdated
Comment thread app/services/billing/segments.rb Outdated
Comment thread db/migrate/20260901122946_create_billing_segments.rb Outdated
Comment thread app/services/billing/segments.rb
Comment thread app/services/subscriptions/product_catalog/materialize_service.rb Outdated
Comment thread app/services/subscriptions/product_catalog/materialize_service.rb Outdated
Comment thread app/services/subscriptions/product_catalog/materialize_service.rb Outdated
Comment thread app/services/billing/interval.rb
Comment thread app/services/billing/calendar.rb Outdated
Comment thread app/services/billing/calendar.rb Outdated
Comment thread app/services/billing/calendar.rb Outdated
Comment thread app/services/billing/calendar.rb Outdated
Comment thread app/services/billing/calendar.rb Outdated
Comment thread app/services/billing/calendar.rb Outdated
Comment thread app/services/billing/schedule.rb Outdated
Comment thread app/services/billing/schedule.rb Outdated
Comment thread app/services/billing/segments.rb Outdated

@brunomiguelpinto brunomiguelpinto left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Read it end to end. The pure layer is the strongest part — Interval, Calendar and Segments have no dependencies, and having the QA scenarios pinned as value tables means an expectation can be checked against the plan rather than argued about. Three things I'd want addressed before this lands, one of them structural.

Comment thread app/services/subscriptions/product_catalog/materialize_service.rb Outdated
Comment thread app/services/subscriptions/product_catalog/materialize_service.rb Outdated
Comment thread app/models/billing_segment.rb
Comment thread app/models/billing_segment.rb
Comment thread app/models/billing_segment.rb Outdated
Comment thread app/models/billing_segment.rb
Comment thread db/migrate/20260901122946_create_billing_segments.rb Outdated
Comment thread db/migrate/20260901122946_create_billing_segments.rb Outdated
Comment thread app/models/billing_segment.rb
Comment thread app/services/billing/rate_cards/schedule.rb Outdated
Comment thread app/services/billing/rate_cards/schedule.rb Outdated
Comment thread app/services/billing/rate_cards/schedule.rb Outdated
Comment thread app/services/billing/rate_cards/schedule.rb Outdated
Comment thread app/services/billing/rate_cards/schedule.rb Outdated
Comment thread app/services/billing/rate_cards/schedule.rb Outdated
Comment thread app/services/billing/rate_cards/schedule.rb Outdated
Comment thread app/services/billing/rate_cards/build_schedule_service.rb Outdated
Comment thread app/services/billing/rate_cards/schedule.rb Outdated
Comment thread app/services/billing/rate_cards/schedule.rb Outdated
Comment thread app/services/billing/rate_cards/schedule.rb Outdated
Comment thread app/services/billing/rate_cards/schedule.rb Outdated
Comment thread app/services/billing/rate_cards/schedule.rb Outdated
Comment thread app/services/billing/rate_cards/schedule.rb Outdated
Comment thread app/services/billing/rate_cards/schedule.rb Outdated

@annvelents annvelents 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.

LGTM! 👏 ⭐ 🦸 🪨 🐐 🤘 🎊 🥇

@brunomiguelpinto
brunomiguelpinto changed the base branch from products-and-plans-subscription-runtime to main September 8, 2026 16:36
@brunomiguelpinto
brunomiguelpinto changed the base branch from main to products-and-plans-subscription-runtime September 8, 2026 16:55
@brunomiguelpinto brunomiguelpinto changed the title feat(billing): the v2 rate-card date engine feat(billing): add resumable rate-card schedules Sep 8, 2026
@brunomiguelpinto
brunomiguelpinto changed the base branch from products-and-plans-subscription-runtime to main September 8, 2026 17:01
## Context

Contract billing needs consistent dates across rate changes, phase
interval overrides and long billing histories.

## Description

Add schedules for due segments, future billing dates and consumption
ratios. Separate cycle traversal from segment construction, with
resume support and reusable calendars.

Build schedules from contract rate cards and resume from their latest
persisted cycle. Interpret card dates in the customer timezone and
respect both card and contract termination boundaries.

Seed the initial contract billing clock from the schedule. Add helpers
for converting exclusive schedule ends to inclusive stored ends.

Cover full and resumed walks, rate and phase transitions, proration,
termination, persisted history and contract materialization.
## Context

Billing segments already exist on main. Their pricing-unit and base-rate
indexes are not used by the current billing queries.

## Description

Remove the two indexes with a new concurrent migration and restore them
on rollback. Update the schema and model annotations to match.
@brunomiguelpinto brunomiguelpinto changed the title feat(billing): add resumable rate-card schedules feat(billing): add resumable contract schedules Sep 8, 2026
@brunomiguelpinto
brunomiguelpinto marked this pull request as ready for review September 8, 2026 17:24
@brunomiguelpinto
brunomiguelpinto merged commit 88adb6c into main Sep 8, 2026
12 checks passed
@brunomiguelpinto
brunomiguelpinto deleted the engine-dates branch September 8, 2026 17:25
@brunomiguelpinto brunomiguelpinto changed the title feat(billing): add resumable contract schedules feat(billing): Billing engine v2 date services Sep 10, 2026
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