Skip to content

feat: implement realtime expand#4155

Merged
turip merged 7 commits intomainfrom
feat/realtime-expand
Apr 16, 2026
Merged

feat: implement realtime expand#4155
turip merged 7 commits intomainfrom
feat/realtime-expand

Conversation

@turip
Copy link
Copy Markdown
Member

@turip turip commented Apr 16, 2026

Overview

Add support for getting the total usage for the charge. The total usage includes the already booked items.

Notes for reviewer

Summary by CodeRabbit

  • New Features

    • Added realtime_usage expansion so charge responses can include current usage totals.
  • Improvements

    • Charges responses now populate realtime totals when realtime_usage is requested.
    • List behavior updated to allow combining include/exclude status filters when valid.
  • Bug Fixes

    • Validation now rejects requests that set both inclusion and exclusion status filters simultaneously.
  • Tests

    • Added end-to-end test verifying realtime usage expansion across multiple usage-based charges.

@turip turip requested a review from a team as a code owner April 16, 2026 13:15
@turip turip added release-note/feature Release note: Exciting New Features area/billing labels Apr 16, 2026
@turip turip requested a review from mark-vass-konghq April 16, 2026 13:16
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds realtime-usage expansion for usage-based charges: new expand constant, model Expands with realtime totals, handler wiring to request/convert realtime expands, service logic to compute and attach realtime totals concurrently, adapter query fix for status filters, and an integration test validating totals.

Changes

Cohort / File(s) Summary
API Handlers
api/v3/handlers/customers/charges/list.go, api/v3/handlers/customers/charges/convert.go
List handler now computes expands to include ExpandRealtimeUsage when requested; converter conditionally sets BillingChargeTotals.Realtime from charge.Expands.RealtimeUsage.
Expand Enum & Input Validation
openmeter/billing/charges/meta/charge.go, openmeter/billing/charges/service.go
Added ExpandRealtimeUsage to expand values; ListChargesInput.Validate() now rejects setting both StatusIn and StatusNotIn.
Adapter Query
openmeter/billing/charges/adapter/search.go
ListCharges no longer skips StatusNotIn when StatusIn is set; both inclusion and exclusion filters can be combined.
Usage-based Charge Model
openmeter/billing/charges/usagebased/charge.go
Added Expands field with RealtimeUsage *totals.Totals, Expands.Validate(), and Charge.GetCustomerID(); integrated expands validation into Charge.Validate().
Service Expansion Logic
openmeter/billing/charges/usagebased/service/get.go
Added expandChargesUsage to dedupe customers/meters, resolve meters/overrides, and compute per-charge realtime rating totals concurrently with bounded parallelism and error aggregation; GetByIDs/GetByID invoke expansion when requested.
Tests
test/credits/rating_test.go
New integration test verifying realtime_usage expansion for multiple usage-based charges using streamed events and asserting expected realtime totals.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Handler as API Handler
    participant Service as Charges Service
    participant Adapter as DB Adapter
    participant Rater as Rating Engine

    Client->>Handler: ListCharges (expand=realizations,realtime_usage)
    Handler->>Service: GetByIDs with ExpandRealtimeUsage
    Service->>Adapter: GetByIDs (base charges)
    Adapter-->>Service: charges

    alt ExpandRealtimeUsage requested
        Service->>Service: Deduplicate customers & meters
        Service->>Adapter: ResolveFeatureMeters / FetchCustomerOverrides
        Adapter-->>Service: meters & overrides
        loop per-charge (concurrent, bounded)
            Service->>Rater: Compute rating for charge
            Rater-->>Service: totals or error
        end
        Service->>Service: Aggregate totals, attach to charge.Expands.RealtimeUsage
    end

    Service-->>Handler: charges with Expands.RealtimeUsage
    Handler->>Handler: convertUsageBasedChargeTotals (set API Realtime)
    Handler-->>Client: Response with realtime totals
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • tothandras
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: implement realtime expand' clearly and accurately describes the main change—adding support for retrieving realtime usage via an expand option, which is the central feature across all modified files.

✏️ 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 feat/realtime-expand

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.

GAlexIHU
GAlexIHU previously approved these changes Apr 16, 2026
Copy link
Copy Markdown
Contributor

@GAlexIHU GAlexIHU left a comment

Choose a reason for hiding this comment

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

why do we need to load live usage for list of charges? (i suppose old line feature-parity but still, these are super expensive)

Comment thread openmeter/billing/charges/usagebased/service/get.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.

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/usagebased/charge.go (1)

78-85: ⚠️ Potential issue | 🟡 Minor

Validate Expands from Charge.Validate().

Right now Lines 78-85 only validate ChargeBase, so invalid Expands.RealtimeUsage values bypass charge-level validation even though Expands.Validate() already exists.

Suggested fix
 func (c Charge) Validate() error {
 	var errs []error
 
 	if err := c.ChargeBase.Validate(); err != nil {
 		errs = append(errs, fmt.Errorf("charge base: %w", err))
 	}
+
+	if err := c.Expands.Validate(); err != nil {
+		errs = append(errs, fmt.Errorf("expands: %w", err))
+	}
 
 	return models.NewNillableGenericValidationError(errors.Join(errs...))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/usagebased/charge.go` around lines 78 - 85,
Charge.Validate currently only validates ChargeBase and misses validating the
Expands field; update Charge.Validate (the method on type Charge) to also call
c.Expands.Validate(), capture and wrap any returned error (e.g.,
fmt.Errorf("expands: %w", err)) and append it to errs before returning
models.NewNillableGenericValidationError(errors.Join(errs...)) so invalid
Expands.RealtimeUsage values are caught by charge-level validation.
🧹 Nitpick comments (1)
test/credits/rating_test.go (1)

28-121: Please cover the single-charge expand path too.

This is a nice end-to-end check for the list flow. I’d still add a small GetByID case as well, since openmeter/billing/charges/usagebased/service/get.go uses a separate GetCurrentTotals branch for ExpandRealtimeUsage, and this test won’t catch drift there.

As per coding guidelines "Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests."

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

In `@test/credits/rating_test.go` around lines 28 - 121, Add a small GetByID path
to the existing test: after creating charges and adding streaming events, call
s.Charges.GetByID (or the service method that maps to the get.go flow) for one
of the created charge IDs with Expands including meta.ExpandRealtimeUsage and
meta.ExpandRealizations, then assert the returned charge.AsUsageBasedCharge()
has the same RealtimeUsage.Total and Realizations.Sum().Total expectations as
the list result; this ensures the separate GetCurrentTotals branch used by
ExpandRealtimeUsage in get.go is exercised.
🤖 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/usagebased/service/get.go`:
- Around line 104-146: The code calls clock.Now() inside each worker when
building the GetRatingForUsageInput, causing inconsistent StoredAtOffset across
charges; capture a single timestamp once before the for loop (e.g., storedAt :=
clock.Now()) and pass that storedAt value into s.rater.GetRatingForUsage's
StoredAtOffset for every charge so all ratings use the same cutoff; update
references in the loop where StoredAtOffset is set and keep existing
semaphore/waitgroup logic intact.
- Around line 126-149: The code sends the same GetRatingForUsage error twice via
errCh because there's both an explicit errCh <- fmt.Errorf(...) and a deferred
sender that sends err when non-nil; in the wg.Go closure change the explicit
send into setting the local err variable and returning (e.g., err =
fmt.Errorf("rating charge %s: %w", charge.ID, err); return) so the existing
deferred func that does if err != nil { errCh <- err } handles the send exactly
once; reference the wg.Go closure, the local err variable, errCh, and
s.rater.GetRatingForUsage to locate and update the logic.

---

Outside diff comments:
In `@openmeter/billing/charges/usagebased/charge.go`:
- Around line 78-85: Charge.Validate currently only validates ChargeBase and
misses validating the Expands field; update Charge.Validate (the method on type
Charge) to also call c.Expands.Validate(), capture and wrap any returned error
(e.g., fmt.Errorf("expands: %w", err)) and append it to errs before returning
models.NewNillableGenericValidationError(errors.Join(errs...)) so invalid
Expands.RealtimeUsage values are caught by charge-level validation.

---

Nitpick comments:
In `@test/credits/rating_test.go`:
- Around line 28-121: Add a small GetByID path to the existing test: after
creating charges and adding streaming events, call s.Charges.GetByID (or the
service method that maps to the get.go flow) for one of the created charge IDs
with Expands including meta.ExpandRealtimeUsage and meta.ExpandRealizations,
then assert the returned charge.AsUsageBasedCharge() has the same
RealtimeUsage.Total and Realizations.Sum().Total expectations as the list
result; this ensures the separate GetCurrentTotals branch used by
ExpandRealtimeUsage in get.go is exercised.
🪄 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: 45c1a518-4ae1-4362-a7d5-70aae544c96a

📥 Commits

Reviewing files that changed from the base of the PR and between 7b40d30 and 984739e.

📒 Files selected for processing (8)
  • api/v3/handlers/customers/charges/convert.go
  • api/v3/handlers/customers/charges/list.go
  • openmeter/billing/charges/adapter/search.go
  • openmeter/billing/charges/meta/charge.go
  • openmeter/billing/charges/service.go
  • openmeter/billing/charges/usagebased/charge.go
  • openmeter/billing/charges/usagebased/service/get.go
  • test/credits/rating_test.go

Comment thread openmeter/billing/charges/usagebased/service/get.go
Comment thread openmeter/billing/charges/usagebased/service/get.go
@turip
Copy link
Copy Markdown
Member Author

turip commented Apr 16, 2026

why do we need to load live usage for list of charges? (i suppose old line feature-parity but still, these are super expensive)

This is for upcoming charges, we are only loading this if the realtime expand is set (and we should not).

This is feature parity with the gathering invoice.

Once we have periodic realizations we can make it faster by relying on the previous snapshots for some meter types such as count/sum.

@turip turip force-pushed the feat/realtime-expand branch from 0856ae8 to 33564fd Compare April 16, 2026 13:35
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

🤖 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/usagebased/service/get.go`:
- Around line 116-127: After successfully acquiring the semaphore with
sem.Acquire in the loop, ensure you release that slot if ResolveFeatureMeter
fails; currently you break after sending to errCh without releasing and leak the
slot. Modify the block after sem.Acquire so that when
charge.ResolveFeatureMeter(featureMeters) returns an error you call
sem.Release(ctx, 1) (or otherwise free the acquired slot) before sending the
error to errCh and breaking; reference the sem.Acquire/sem.Release calls and the
ResolveFeatureMeter invocation to locate where to add the release.
- Around line 129-155: The anonymous goroutine in wg.Go declares an outer err
but then uses := when calling s.rater.GetRatingForUsage, which shadows err so
the deferred error handler never sees failures; fix by avoiding shadowing—either
predeclare ratingResult (e.g. var ratingResult type) and use = when calling
s.rater.GetRatingForUsage, or replace := with = so the call assigns to the
existing err; keep the deferred panic-to-err and errCh send as-is and ensure you
still call ratingResults.Store(charge.GetChargeID(), ratingResult) only on
success.
🪄 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: 56b54aa2-88c4-49d8-b974-30d0e7d7c1ad

📥 Commits

Reviewing files that changed from the base of the PR and between 69eaaa7 and c83449c.

📒 Files selected for processing (2)
  • openmeter/billing/charges/usagebased/charge.go
  • openmeter/billing/charges/usagebased/service/get.go

Comment thread openmeter/billing/charges/usagebased/service/get.go Outdated
Comment thread openmeter/billing/charges/usagebased/service/get.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/usagebased/service/get.go (1)

182-185: Small nit: error message is slightly misleading.

The error on line 184 says "not found" but really means the type assertion failed. Not a big deal, just could be clearer if you ever need to debug this path.

Suggested fix
 		ratingResult, ok := ratingResultAny.(usagebasedrating.GetRatingForUsageResult)
 		if !ok {
-			return charge, fmt.Errorf("rating result not found for charge %s", charge.ID)
+			return charge, fmt.Errorf("rating result type assertion failed for charge %s", charge.ID)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/usagebased/service/get.go` around lines 182 - 185,
The error message is misleading because the failure is a type assertion on
ratingResultAny to usagebasedrating.GetRatingForUsageResult (variables
ratingResultAny and ratingResult) rather than a "not found" condition; update
the fmt.Errorf call in the type-assertion branch to clearly state the type
assertion/unexpected type and include the actual dynamic type (using %T) and the
charge.ID to aid debugging (e.g., "unexpected rating result type %T for charge
%s"), keeping the same early return path.
🤖 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/get.go`:
- Around line 182-185: The error message is misleading because the failure is a
type assertion on ratingResultAny to usagebasedrating.GetRatingForUsageResult
(variables ratingResultAny and ratingResult) rather than a "not found"
condition; update the fmt.Errorf call in the type-assertion branch to clearly
state the type assertion/unexpected type and include the actual dynamic type
(using %T) and the charge.ID to aid debugging (e.g., "unexpected rating result
type %T for charge %s"), keeping the same early return path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c79ae56b-d69f-4fbb-b3a9-e5896bbd3aa0

📥 Commits

Reviewing files that changed from the base of the PR and between c83449c and 33564fd.

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

@turip turip enabled auto-merge (squash) April 16, 2026 14:00
@turip turip merged commit 1e60d8e into main Apr 16, 2026
23 of 24 checks passed
@turip turip deleted the feat/realtime-expand branch April 16, 2026 14:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/billing release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants