Skip to content

chore: rework run snapshotting#4226

Merged
turip merged 4 commits intomainfrom
chore/rework-run-snapshotting
Apr 24, 2026
Merged

chore: rework run snapshotting#4226
turip merged 4 commits intomainfrom
chore/rework-run-snapshotting

Conversation

@turip
Copy link
Copy Markdown
Member

@turip turip commented Apr 24, 2026

Overview

This PR refactors usage-based realization run snapshot fields to make the persisted data model match the new run
semantics.

The run no longer stores the old asof, collection_end, and meter_value concepts. Instead it stores the
explicit query boundaries and the resulting quantity:

  • stored_at_lt
  • service_period_to
  • metered_quantity

It also aligns usage-based invoice collection timing with the charge state machine by setting
StandardLine.OverrideCollectionPeriodEnd from the run snapshot cutoff.

Field Renames And Semantics

Old field New field Meaning
asof stored_at_lt Stored-at upper bound used for meter queries. Events are included only if stored_at < stored_at_lt.
collection_end removed Replaced by explicit stored_at_lt plus invoice-line override_collection_period_end.
meter_value metered_quantity Metered quantity returned by the run’s meter query.
new service_period_to Event-time upper bound used for the run’s meter query.

Calculation Rules

Meter queries for detailed run calculation use:

intent.service_period.from <= event_time < service_period_to
stored_at < stored_at_lt

For partial invoice runs:

service_period_to = standard_line.period.to
stored_at_lt      = service_period_to

For final realization runs:

service_period_to = charge.intent.service_period.to
stored_at_lt      = charge.intent.service_period.to + billing_profile.workflow.collection.interval

The charge state machine waits for the internal ingestion buffer before processing the run:

run_snapshot_after = stored_at_lt + usagebased.InternalCollectionPeriod

Usage-based standard invoice lines are aligned to the same readiness point:

standard_line.override_collection_period_end = stored_at_lt + usagebased.InternalCollectionPeriod

This keeps the billing invoice state machine and usage-based charge state machine using the same collection
readiness timestamp.

Migration Backfill

The migration renames:

asof        -> stored_at_lt
meter_value -> metered_quantity

Historical rows used collection_end as the effective meter-query cutoff, so the migration backfills:

stored_at_lt = collection_end

For service_period_to:

if run.type == 'final_realization':
    service_period_to = parent_charge.service_period_to
else:
    service_period_to = collection_end

The non-final fallback is defensive. Persisted partial runs are not expected for current production data, but the
fallback keeps the migration safe for any existing non-final rows.

After backfill, collection_end is dropped.

Summary by CodeRabbit

  • Refactor

    • Standardized usage-based billing: switched to a stored-at lower-bound plus explicit service-period end and renamed meter values to metered quantity; includes database migration and updated collection-period semantics.
  • Documentation

    • Clarified PostgreSQL identifier-length (63 bytes) and recommended name-shortening mitigations for verbose schema/entity names.
  • Tests

    • Updated tests and fixtures to align with the new time/quantity semantics and exclusive/inclusive cutoff behavior.

@turip turip requested a review from a team as a code owner April 24, 2026 12:47
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

This PR refactors usage-based realization runs to replace offset-based timestamps with an explicit stored-at-lower-bound model. Core fields AsOf/CollectionEnd/MeterValue are renamed and propagated to models, adapters, services, schema, migrations, and tests as StoredAtLT/ServicePeriodTo/MeteredQuantity. Documentation updated for an Ent/Postgres identifier-length mitigation.

Changes

Cohort / File(s) Summary
Core Domain Models
openmeter/billing/charges/usagebased/realizationrun.go
Renamed fields: AsOfStoredAtLT, CollectionEndServicePeriodTo, MeterValueMeteredQuantity; updated JSON tags, validation, and normalization.
Adapters & Persistence
openmeter/billing/charges/usagebased/adapter/mapper.go, openmeter/billing/charges/usagebased/adapter/realizationrun.go, openmeter/ent/schema/chargesusagebased.go
DB mapping, sorting, ent schema, and adapter create/update now use stored_at_lt/service_period_to/metered_quantity.
Service Layer (runs & state machine)
openmeter/billing/charges/usagebased/service/run/create.go, .../statemachine.go, .../creditsonly.go, .../creditheninvoice.go, .../lineengine.go, .../currenttotals.go, .../get.go
Rewired control flow to compute/pass StoredAtLT and ServicePeriodTo instead of offsets; snapshotting, allocation times, and persistence updated; added getFinalRunStoredAtLT() and removed GetCollectionPeriodEnd().
Rating & Snapshotting
openmeter/billing/charges/usagebased/service/rating/details.go, .../totals.go, .../quantitysnapshot.go, .../service_test.go
Inputs/validation now require ServicePeriodTo and StoredAtLT (removed StoredAtOffset); meter queries use StoredAtLT as exclusive cutoff.
Run API & Adapter Methods
openmeter/billing/charges/usagebased/adapter/realizationrun.go, openmeter/billing/charges/usagebased/realizationrun.go
Create/Update methods set/conditionally update StoredAtLT/ServicePeriodTo/MeteredQuantity instead of old fields.
Migrations
tools/migrate/migrations/20260424091352_usagebased_run_data_structure.up.sql, ...down.sql
Up migration renames/backfills stored_at_lt/service_period_to/metered_quantity and removes collection_end; down migration reverts to previous columns.
Tests & Fixtures
openmeter/billing/charges/service/*.go, openmeter/billing/charges/usagebased/*_test.go, openmeter/ledger/chargeadapter/usagebased_test.go, openmeter/billing/worker/subscriptionsync/service/creditsonly_test.go, .agents/skills/charges/SKILL.md, .agents/skills/ent/SKILL.md
Updated assertions, fixtures, and test inputs to use StoredAtLT/ServicePeriodTo/MeteredQuantity. Docs updated with Postgres identifier-length mitigation for Ent-generated names.
Small mappings & callers
openmeter/billing/charges/usagebased/adapter/detailedline_test.go, openmeter/billing/charges/service/featureid_test.go, openmeter/billing/charges/service/invoicable_test.go, openmeter/billing/charges/service/truncation_test.go, openmeter/billing/charges/service/usagebased_test.go
Tests adjusted to assert on MeteredQuantity and StoredAtLT/ServicePeriodTo; timestamp/expectation updates.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Rating as Rating Service
    participant DB as RealizationRun Persistence
    participant SM as State Machine
    participant Credit as Credit Allocation

    Note over Client,Rating: OLD: offset-based snapshot
    Client->>Rating: GetDetailedLines(StoredAtOffset)
    Rating->>DB: snapshotQuantity using StoredAtOffset
    Client->>DB: Create(AsOf, CollectionEnd, MeterValue)
    DB-->>Client: persisted run
    Client->>SM: AdvanceAfterCollectionPeriodEnd()
    SM->>SM: compute snapshot-after from CollectionEnd + interval
    SM->>Credit: allocate using CollectionEnd-derived time

    Note over Client,Rating: NEW: stored-at-lower-bound
    Client->>Rating: GetDetailedLines(StoredAtLT, ServicePeriodTo)
    Rating->>DB: snapshotQuantity using StoredAtLT
    Client->>DB: Create(StoredAtLT, ServicePeriodTo, MeteredQuantity)
    DB-->>Client: persisted run
    Client->>SM: AdvanceAfterCollectionPeriodEnd()
    SM->>SM: compute snapshot-after from StoredAtLT + interval
    SM->>Credit: allocate using StoredAtLT-derived time
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • tothandras
  • GAlexIHU
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'chore: rework run snapshotting' directly reflects the main change: refactoring run snapshot fields from asof/collection_end/meter_value to stored_at_lt/service_period_to/metered_quantity across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/rework-run-snapshotting

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@turip turip added release-note/misc Miscellaneous changes area/billing labels Apr 24, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openmeter/billing/charges/service/invoicable_test.go (1)

467-636: ⚠️ Potential issue | 🔴 Critical

These assertions contradict each other — one of them can't pass.

Inside the #3.1 start final realization block, currentRun.StoredAtLT is asserted against two different instants within three lines:

  • Line 633: must equal firstStoredAtLT = 2026-02-01T11:59:00Z
  • Line 635: must equal expectedCollectionEnd = 2026-02-03T00:00:00Z

Those are two days apart, so whichever one the run actually has, the other assertion will fail. Looking at the runtime, getFinalRunStoredAtLT() (statemachine.go) computes ServicePeriod.To + collectionInterval (= expectedCollectionEnd), so line 633 seems to be a stale “offset-from-now” assertion that survived the rename.

The downstream expectation also looks wrong with the current code:

  • Line 636 expects MeteredQuantity == 3, i.e. only events 1 and 2 are rated. That only happens if the cutoff is < 2026-02-01T12:00:00Z (the storedAt of event 3). With StoredAtLT = 2026-02-03T00:00:00Z from getFinalRunStoredAtLT(), all three events qualify and the sum would be 6.
  • Line 647 asserts the startedCallbacks[0].Input.AllocateAt == firstStoredAtLT, but CreateRatedRun sets AllocateAt = in.ServicePeriodTo = NormalizeTimestamp(ServicePeriod.To) = 2026-02-01T00:00:00Z, which is neither of the declared expectations.

Either the test is reflecting the intended new semantic (compute storedAtLT as clock.Now() - InternalCollectionPeriod) and the code hasn't caught up in getFinalRunStoredAtLT/StartFinalRealizationRun, or the code is the source of truth and the test fixtures need to drop the old firstStoredAtOffset-style expectations. Either way, worth reconciling before this goes in — the same pattern repeats in TestUsageBasedCreditOnlyLifecycleVolumeTieredCorrection at lines 784-785, 874, 913-914.

Also applies to: 643-648

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/invoicable_test.go` around lines 467 - 636,
The assertions in the test use two contradictory expectations for StoredAtLT and
resulting metered totals; reconcile the test with the actual implementation:
update the assertions in invoicable_test.go (and the other locations called out)
to expect getFinalRunStoredAtLT()/StartFinalRealizationRun behavior (StoredAtLT
== expectedCollectionEnd, which will include all three events so MeteredQuantity
== 6) and assert AllocateAt equals the service period end normalized
(NormalizeTimestamp(ServicePeriod.To)), not firstStoredAtLT; also remove the
stale firstStoredAtLT equality checks and update the similar assertions in
TestUsageBasedCreditOnlyLifecycleVolumeTieredCorrection to match the new
semantics.
🧹 Nitpick comments (5)
openmeter/billing/charges/service/truncation_test.go (1)

167-171: Nit: duplicated assertion on finalRun.StoredAtLT.

expectedCollectionEnd and expectedStoredAtLT are parsed from the exact same timestamp and both are asserted against finalRun.StoredAtLT, so the second line is a redundant check. Either drop one, or give them distinct semantics (e.g., keep the expectedCollectionEnd variable only if you intend to later also assert against a separate ServicePeriodTo-style field on the run).

♻️ Suggested tidy-up
 	finalRun := finalCharge.Realizations[0]
-	expectedCollectionEnd := datetime.MustParseTimeInLocation(s.T(), "2026-02-01T00:01:00Z", time.UTC).AsTime()
-	expectedStoredAtLT := datetime.MustParseTimeInLocation(s.T(), "2026-02-01T00:01:00Z", time.UTC).AsTime()
+	expectedStoredAtLT := datetime.MustParseTimeInLocation(s.T(), "2026-02-01T00:01:00Z", time.UTC).AsTime()
 
-	s.True(expectedCollectionEnd.Equal(finalRun.StoredAtLT))
 	s.True(expectedStoredAtLT.Equal(finalRun.StoredAtLT))

Also worth asking: should this test additionally assert finalRun.ServicePeriodTo now that it's a first-class field on the run? That'd make the "truncation of persisted calculation timestamps" contract tighter.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/truncation_test.go` around lines 167 - 171,
The test contains a duplicated assertion: both expectedCollectionEnd and
expectedStoredAtLT are parsed identically and both asserted against
finalRun.StoredAtLT; remove the redundant assertion (drop one of the s.True(...)
lines) or give them distinct semantics by asserting expectedCollectionEnd
against finalRun.ServicePeriodTo and expectedStoredAtLT against
finalRun.StoredAtLT so each variable verifies a different field (use
finalRun.StoredAtLT and finalRun.ServicePeriodTo as the unique symbols to
update).
openmeter/billing/charges/usagebased/service/rating/totals.go (1)

41-42: Tiny doc-comment leftover from the old naming.

The method comment still says "requested stored-at offset," but the input is now a concrete timestamp (StoredAtLT), not an offset. Worth a quick tweak so the doc matches the field:

📝 Proposed wording fix
-// GetTotalsForUsage returns the rated totals for the charge at the requested stored-at offset.
+// GetTotalsForUsage returns the rated totals for the charge at the requested stored-at-lt cutoff.
 // It avoids generating detailed lines, so prefer it over GetDetailedLinesForUsage when only totals are needed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/usagebased/service/rating/totals.go` around lines
41 - 42, Update the doc comment for GetTotalsForUsage to reflect that it accepts
a concrete timestamp rather than an offset: replace the phrase "requested
stored-at offset" with wording that references the StoredAtLT timestamp (e.g.,
"requested stored-at timestamp (StoredAtLT)") so the comment matches the method
signature and the StoredAtLT field.
openmeter/billing/charges/usagebased/service/creditsonly.go (1)

185-185: Re-normalizing an already-normalized timestamp.

currentRun.StoredAtLT was written via meta.NormalizeTimestamp(...) (either here in StartFinalRealizationRun line 159 or in run/create.go), and NormalizeTimestamp is idempotent, so this extra call is a no-op. Dropping it makes the intent a bit clearer:

✨ Optional cleanup
-	storedAtLT := meta.NormalizeTimestamp(currentRun.StoredAtLT)
+	storedAtLT := currentRun.StoredAtLT

Not blocking — just tidying.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/usagebased/service/creditsonly.go` at line 185, The
line re-calls meta.NormalizeTimestamp on currentRun.StoredAtLT which is already
normalized (done in StartFinalRealizationRun or run/create.go), so remove the
redundant call: replace the assignment to storedAtLT in creditsonly.go to
directly use currentRun.StoredAtLT (or otherwise stop calling
meta.NormalizeTimestamp there) to clarify intent and avoid a no-op; update any
nearby comments to reflect that currentRun.StoredAtLT is already normalized.
openmeter/billing/charges/usagebased/realizationrun.go (2)

76-90: Tiny nit: group the temporal validations together.

Totally optional polish — since StoredAtLT and ServicePeriodTo are conceptually a pair (both timestamps, both required, same zero-check), reading the validator would flow a bit better if their checks sat next to each other instead of being separated by the MeteredQuantity and Totals checks. Same applies to RealizationRunBase.Validate() at lines 194–208. Happy to leave as-is if you prefer the current ordering.

✨ Suggested tidy-up
 	if r.StoredAtLT.IsZero() {
 		errs = append(errs, fmt.Errorf("stored at lt must be set"))
 	}
 
+	if r.ServicePeriodTo.IsZero() {
+		errs = append(errs, fmt.Errorf("service period to must be set"))
+	}
+
 	if r.MeteredQuantity.IsNegative() {
 		errs = append(errs, fmt.Errorf("metered quantity must be zero or positive"))
 	}
 
 	if err := r.Totals.Validate(); err != nil {
 		errs = append(errs, fmt.Errorf("totals: %w", err))
 	}
-
-	if r.ServicePeriodTo.IsZero() {
-		errs = append(errs, fmt.Errorf("service period to must be set"))
-	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/usagebased/realizationrun.go` around lines 76 - 90,
Move the timestamp zero-checks so they are adjacent: in
RealizationRun.Validate(), place the ServicePeriodTo.IsZero() check immediately
after the StoredAtLT.IsZero() check (so both temporal validations are grouped
together) and keep MeteredQuantity.IsNegative() and Totals.Validate() following
them; do the same tidy-up in RealizationRunBase.Validate() by grouping its
StoredAtLT and ServicePeriodTo zero-checks next to each other to improve logical
ordering and readability while leaving the same error messages and behavior
intact.

99-115: Consider adding a doc comment to UpdateRealizationRunInput clarifying that ServicePeriodTo is immutable.

The design looks solid — ServicePeriodTo is correctly set-once at creation via CreateRealizationRunInput and intentionally absent from updates. No caller tries to mutate it. That said, a one-liner doc comment on the struct explaining "service period is fixed at creation and cannot be updated" would help future maintainers understand the constraint at a glance, since it's not immediately obvious why the field was omitted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/usagebased/realizationrun.go` around lines 99 -
115, Add a one-line doc comment to the UpdateRealizationRunInput struct
explaining that the service period end (ServicePeriodTo) is intentionally
immutable and set during creation; specifically update the comment above type
UpdateRealizationRunInput to state that ServicePeriodTo is fixed at creation and
cannot be modified via UpdateRealizationRunInput so future maintainers
understand why that field is omitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openmeter/billing/charges/service/invoicable_test.go`:
- Line 634: Change the assertions that use s.NotNil on the value-typed timestamp
RealizationRunBase.StoredAtLT to check for non-zero time instead: replace
s.NotNil(currentRun.StoredAtLT) and s.NotNil(finalRun.StoredAtLT) (and the same
occurrences at the other reported locations) with assertions that call .IsZero()
and assert it is false (e.g., s.False(currentRun.StoredAtLT.IsZero())) so the
test actually verifies the timestamp is present.

In
`@tools/migrate/migrations/20260424091352_usagebased_run_data_structure.down.sql`:
- Around line 1-7: Add a one-line comment at the top of the down migration
explaining the intentional semantic shift performed in the up migration: note
that the up migration preserved the query cutoff by copying collection_end into
stored_at_lt (and thereby overwrote asof), so when rolling back the down
migration the restored asof values come from stored_at_lt and therefore
represent collection_end timestamps rather than original creation times;
reference the table "charge_usage_based_runs" and the columns "asof",
"stored_at_lt", and "collection_end" in the comment.

---

Outside diff comments:
In `@openmeter/billing/charges/service/invoicable_test.go`:
- Around line 467-636: The assertions in the test use two contradictory
expectations for StoredAtLT and resulting metered totals; reconcile the test
with the actual implementation: update the assertions in invoicable_test.go (and
the other locations called out) to expect
getFinalRunStoredAtLT()/StartFinalRealizationRun behavior (StoredAtLT ==
expectedCollectionEnd, which will include all three events so MeteredQuantity ==
6) and assert AllocateAt equals the service period end normalized
(NormalizeTimestamp(ServicePeriod.To)), not firstStoredAtLT; also remove the
stale firstStoredAtLT equality checks and update the similar assertions in
TestUsageBasedCreditOnlyLifecycleVolumeTieredCorrection to match the new
semantics.

---

Nitpick comments:
In `@openmeter/billing/charges/service/truncation_test.go`:
- Around line 167-171: The test contains a duplicated assertion: both
expectedCollectionEnd and expectedStoredAtLT are parsed identically and both
asserted against finalRun.StoredAtLT; remove the redundant assertion (drop one
of the s.True(...) lines) or give them distinct semantics by asserting
expectedCollectionEnd against finalRun.ServicePeriodTo and expectedStoredAtLT
against finalRun.StoredAtLT so each variable verifies a different field (use
finalRun.StoredAtLT and finalRun.ServicePeriodTo as the unique symbols to
update).

In `@openmeter/billing/charges/usagebased/realizationrun.go`:
- Around line 76-90: Move the timestamp zero-checks so they are adjacent: in
RealizationRun.Validate(), place the ServicePeriodTo.IsZero() check immediately
after the StoredAtLT.IsZero() check (so both temporal validations are grouped
together) and keep MeteredQuantity.IsNegative() and Totals.Validate() following
them; do the same tidy-up in RealizationRunBase.Validate() by grouping its
StoredAtLT and ServicePeriodTo zero-checks next to each other to improve logical
ordering and readability while leaving the same error messages and behavior
intact.
- Around line 99-115: Add a one-line doc comment to the
UpdateRealizationRunInput struct explaining that the service period end
(ServicePeriodTo) is intentionally immutable and set during creation;
specifically update the comment above type UpdateRealizationRunInput to state
that ServicePeriodTo is fixed at creation and cannot be modified via
UpdateRealizationRunInput so future maintainers understand why that field is
omitted.

In `@openmeter/billing/charges/usagebased/service/creditsonly.go`:
- Line 185: The line re-calls meta.NormalizeTimestamp on currentRun.StoredAtLT
which is already normalized (done in StartFinalRealizationRun or run/create.go),
so remove the redundant call: replace the assignment to storedAtLT in
creditsonly.go to directly use currentRun.StoredAtLT (or otherwise stop calling
meta.NormalizeTimestamp there) to clarify intent and avoid a no-op; update any
nearby comments to reflect that currentRun.StoredAtLT is already normalized.

In `@openmeter/billing/charges/usagebased/service/rating/totals.go`:
- Around line 41-42: Update the doc comment for GetTotalsForUsage to reflect
that it accepts a concrete timestamp rather than an offset: replace the phrase
"requested stored-at offset" with wording that references the StoredAtLT
timestamp (e.g., "requested stored-at timestamp (StoredAtLT)") so the comment
matches the method signature and the StoredAtLT field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d551c001-5d4a-494b-a124-f669ef96e08f

📥 Commits

Reviewing files that changed from the base of the PR and between 5f30643 and d98fd2e.

⛔ Files ignored due to path filters (8)
  • openmeter/ent/db/chargeusagebasedruns.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns/chargeusagebasedruns.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/migrate/schema.go is excluded by !**/ent/db/**
  • openmeter/ent/db/mutation.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (25)
  • .agents/skills/ent/SKILL.md
  • openmeter/billing/charges/service/featureid_test.go
  • openmeter/billing/charges/service/invoicable_test.go
  • openmeter/billing/charges/service/truncation_test.go
  • openmeter/billing/charges/service/usagebased_test.go
  • openmeter/billing/charges/usagebased/adapter/detailedline_test.go
  • openmeter/billing/charges/usagebased/adapter/mapper.go
  • openmeter/billing/charges/usagebased/adapter/realizationrun.go
  • openmeter/billing/charges/usagebased/realizationrun.go
  • openmeter/billing/charges/usagebased/service/creditheninvoice.go
  • openmeter/billing/charges/usagebased/service/creditheninvoice_test.go
  • openmeter/billing/charges/usagebased/service/creditsonly.go
  • openmeter/billing/charges/usagebased/service/currenttotals.go
  • openmeter/billing/charges/usagebased/service/get.go
  • openmeter/billing/charges/usagebased/service/lineengine.go
  • openmeter/billing/charges/usagebased/service/rating/details.go
  • openmeter/billing/charges/usagebased/service/rating/quantitysnapshot.go
  • openmeter/billing/charges/usagebased/service/rating/service_test.go
  • openmeter/billing/charges/usagebased/service/rating/totals.go
  • openmeter/billing/charges/usagebased/service/run/create.go
  • openmeter/billing/charges/usagebased/service/run/payment_test.go
  • openmeter/billing/charges/usagebased/service/statemachine.go
  • openmeter/ent/schema/chargesusagebased.go
  • tools/migrate/migrations/20260424091352_usagebased_run_data_structure.down.sql
  • tools/migrate/migrations/20260424091352_usagebased_run_data_structure.up.sql

Comment thread openmeter/billing/charges/service/invoicable_test.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
openmeter/billing/charges/usagebased/service/run/create.go (1)

196-201: Nit: StoredAtLT / MeteredQuantity in the update are redundant.

Inside this branch the only values that actually change post-allocation are runTotals.CreditsTotal and runTotals.Total. StoredAtLT and ratingResult.Quantity were already persisted a few lines above via createNewRealizationRun, so passing them again just causes a no-op write. If you wanted to keep the call lean, you could drop them and only ship Totals.

♻️ Optional trim
 		currentRunBase, err := s.adapter.UpdateRealizationRun(ctx, usagebased.UpdateRealizationRunInput{
-			ID:              currentRun.ID,
-			StoredAtLT:      mo.Some(in.StoredAtLT),
-			MeteredQuantity: mo.Some(ratingResult.Quantity),
-			Totals:          mo.Some(runTotals),
+			ID:     currentRun.ID,
+			Totals: mo.Some(runTotals),
 		})

Totally fine to defer — just flagging since the new shape makes the redundancy more visible than the old AsOf/MeterValue version did.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/usagebased/service/run/create.go` around lines 196
- 201, The UpdateRealizationRun call is sending redundant fields StoredAtLT and
MeteredQuantity (ratingResult.Quantity) which were already persisted by
createNewRealizationRun; modify the call to s.adapter.UpdateRealizationRun in
this branch to only include the fields that actually changed (e.g.,
Totals/runTotals with CreditsTotal and Total) and remove StoredAtLT and
MeteredQuantity from the UpdateRealizationRunInput to avoid a no-op write.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@openmeter/billing/charges/usagebased/service/run/create.go`:
- Around line 196-201: The UpdateRealizationRun call is sending redundant fields
StoredAtLT and MeteredQuantity (ratingResult.Quantity) which were already
persisted by createNewRealizationRun; modify the call to
s.adapter.UpdateRealizationRun in this branch to only include the fields that
actually changed (e.g., Totals/runTotals with CreditsTotal and Total) and remove
StoredAtLT and MeteredQuantity from the UpdateRealizationRunInput to avoid a
no-op write.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 11f010e2-1606-4dfa-b8db-2cb28e4e2a84

📥 Commits

Reviewing files that changed from the base of the PR and between d98fd2e and 700d0da.

📒 Files selected for processing (5)
  • openmeter/billing/charges/service/invoicable_test.go
  • openmeter/billing/charges/service/usagebased_test.go
  • openmeter/billing/charges/usagebased/service/run/create.go
  • openmeter/billing/worker/subscriptionsync/service/creditsonly_test.go
  • openmeter/ledger/chargeadapter/usagebased_test.go
✅ Files skipped from review due to trivial changes (1)
  • openmeter/billing/charges/service/usagebased_test.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
openmeter/billing/charges/service/invoicable_test.go (1)

467-470: Tiny nit: expectedCollectionEnd is a literal duplicate of finalStoredAtLT.

Both variables are initialized to the exact same 2026-02-03T00:00:00Z value and used together in triplets like s.True(finalStoredAtLT.Equal(x)) + s.True(expectedCollectionEnd.Equal(x.UTC())). Since stored_at_lt is the collection cutoff in the new model, keeping both names around mostly just adds noise. Same thing happens at lines 783–784.

Feel free to collapse them into one (and tweak the paired assertions accordingly) — purely a readability/maintenance nicety, nothing broken.

🧹 Suggested simplification
-	finalStoredAtLT := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:00:00Z", time.UTC).AsTime()
-	expectedCollectionEnd := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:00:00Z", time.UTC).AsTime()
+	finalStoredAtLT := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:00:00Z", time.UTC).AsTime()

And at the assertion sites (e.g., 632–634, 728–730, 911–912, 999–1000, 1813–1815), a single equality check on StoredAtLT is enough — once it equals a known non-zero timestamp, the IsZero() guard and the .UTC() re-check are redundant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/invoicable_test.go` around lines 467 - 470,
Remove the duplicated timestamp variable and use a single shared variable
(either finalStoredAtLT or expectedCollectionEnd) for the collection cutoff;
update the tests that compare stored_at_lt (examples: assertions around
StoredAtLT checks referenced in the diff such as the checks at 632–634, 728–730,
911–912, 999–1000, 1813–1815 and the similar pair at 783–784) to assert equality
against the single timestamp variable and drop the redundant IsZero() and .UTC()
re-checks so each stored_at_lt comparison is a single equality assertion to the
known non-zero timestamp.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@openmeter/billing/charges/service/invoicable_test.go`:
- Around line 467-470: Remove the duplicated timestamp variable and use a single
shared variable (either finalStoredAtLT or expectedCollectionEnd) for the
collection cutoff; update the tests that compare stored_at_lt (examples:
assertions around StoredAtLT checks referenced in the diff such as the checks at
632–634, 728–730, 911–912, 999–1000, 1813–1815 and the similar pair at 783–784)
to assert equality against the single timestamp variable and drop the redundant
IsZero() and .UTC() re-checks so each stored_at_lt comparison is a single
equality assertion to the known non-zero timestamp.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: abfed217-c37c-465a-b9ad-f49572bd3caa

📥 Commits

Reviewing files that changed from the base of the PR and between 700d0da and 9679c3a.

📒 Files selected for processing (1)
  • openmeter/billing/charges/service/invoicable_test.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.agents/skills/charges/SKILL.md:
- Around line 173-176: The doc list of timestamp surfaces is missing
usagebased.UpdateRealizationRunInput.ServicePeriodTo; update
.agents/skills/charges/SKILL.md to include
`usagebased.UpdateRealizationRunInput.ServicePeriodTo` alongside the existing
`StoredAtLT` entry so the checklist matches the guidance that references
SetServicePeriodTo(...), ensuring the UpdateRealizationRunInput timestamp
surfaces section lists both StoredAtLT and ServicePeriodTo.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f3af12cb-ad7a-45c5-91e7-3692099b8faf

📥 Commits

Reviewing files that changed from the base of the PR and between 9679c3a and 8d5b6ff.

📒 Files selected for processing (1)
  • .agents/skills/charges/SKILL.md

Comment thread .agents/skills/charges/SKILL.md
@turip turip merged commit 710826a into main Apr 24, 2026
30 of 31 checks passed
@turip turip deleted the chore/rework-run-snapshotting branch April 24, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants