Skip to content

feat: skaffold usagebased charge#3952

Open
turip wants to merge 5 commits intomainfrom
feat/usage-based-charges
Open

feat: skaffold usagebased charge#3952
turip wants to merge 5 commits intomainfrom
feat/usage-based-charges

Conversation

@turip
Copy link
Member

@turip turip commented Mar 16, 2026

Overview

Create database tables and basic infrastructure for usage based charges. The testcase only validates that we are passing a simple create /get cycle.

Notes for reviewer

Summary by CodeRabbit

  • New Features
    • Usage-based charging is now supported end-to-end: create, retrieve, validate, and generate invoice/gathering lines alongside flat-fee and credit purchases.
    • Invoice lifecycle hooks and settlement modes integrated for usage-based charges.
  • Tests
    • New tests cover usage-based creation, retrieval, invoicing, and partial credit realizations.

@turip turip requested a review from a team as a code owner March 16, 2026 15:48
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 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 end-to-end usage-based billing: new domain models, adapter and service layers, DB mappers, wiring into existing charges service, and test scaffolding—enabling creation, retrieval, and transformation of usage-based charges and intents.

Changes

Cohort / File(s) Summary
Core Charge Type Dispatch
openmeter/billing/charges/charge.go
Adds usageBased fields to Charge and ChargeIntent; extends generics NewCharge/NewChargeIntent; adds converters (AsUsageBased*), validation and GetChargeID support, and UsageBased slice in ChargeIntentsByType.
Service Wiring
openmeter/billing/charges/service/service.go
Wires UsageBasedService into service Config and service struct; validates and assigns the new dependency.
Create & Get Flows
openmeter/billing/charges/service/create.go, openmeter/billing/charges/service/get.go
Removes prior rejection of usage-based types; adds create/get paths that call the usage-based service/adapter, map results back, and merge with existing flat-fee/credit flows.
Usage-Based Domain Models
openmeter/billing/charges/usagebased/charge.go
New domain types: Charge, Intent, State, RealizationRun* with comprehensive validation, GetChargeID, and error attribute helpers.
Adapter Interface
openmeter/billing/charges/usagebased/adapter.go
Defines usagebased.Adapter interface with CreateCharges, UpdateCharge, GetByIDs and embeds entutils.TxCreator.
Adapter Impl & Mapper
openmeter/billing/charges/usagebased/adapter/...
Implements adapter: transaction helpers (Tx, WithTx, Self), CreateCharges, UpdateCharge, GetByIDs, and DB→model mappers with optional expansions and error context.
Usage-Based Service Surface
openmeter/billing/charges/usagebased/service.go
Adds usagebased.Service, UsageBasedService, InvoiceLifecycleHooks, DTOs (CreateInput, GetByIDsInput, ChargeWithGatheringLine) and validation logic.
Usage-Based Service Impl
openmeter/billing/charges/usagebased/service/...
Service constructor, Create() implementation producing ChargeWithGatheringLine (handles settlement modes, gathering line construction), and GetByIDs() delegating to adapter inside transactions.
Test & Handler Stubs
openmeter/billing/charges/usagebased/handler.go, openmeter/billing/charges/service/handlers_test.go, openmeter/billing/charges/service/base_test.go, openmeter/billing/charges/service/invoicable_test.go
Adds empty Handler interface, test helper usageBasedTestHandler, updates BaseSuite to support usage-based intents, and adds usage-based invoicing test(s).

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant Service as UsageBasedService
    participant Adapter
    participant MetaAdapter
    participant DB as EntDB
    participant Gathering as GatheringLine

    Caller->>Service: Create(ctx, CreateInput{Intents})
    Service->>Service: Validate input
    Service->>Adapter: CreateCharges(ctx, input)
    Adapter->>MetaAdapter: Create charge metas
    MetaAdapter->>DB: Persist meta records
    DB-->>MetaAdapter: Meta IDs
    Adapter->>DB: Bulk create ChargeUsageBased rows
    DB-->>Adapter: Created charge rows
    Adapter-->>Service: []usagebased.Charge
    Service->>Gathering: Transform charges → GatheringLine (per settlement mode)
    Gathering-->>Service: ChargeWithGatheringLine
    Service-->>Caller: []ChargeWithGatheringLine
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

release-note/feature

Suggested reviewers

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

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: skaffold usagebased charge' is vague and uses imprecise terminology ('skaffold') that doesn't clearly convey the actual changes, which involve comprehensive integration of usage-based charge support across the billing API and services. Consider a more descriptive title like 'feat: add usage-based charge support' or 'feat: integrate usage-based billing charges' that clearly indicates the feature being added.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/usage-based-charges
📝 Coding Plan
  • Generate coding plan for human review comments

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 force-pushed the feat/usage-based-charges branch from 78b8394 to 3533b26 Compare March 16, 2026 15:53
@turip turip added release-note/misc Miscellaneous changes area/billing labels Mar 16, 2026
@turip
Copy link
Member Author

turip commented Mar 16, 2026

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

✅ Actions performed

Full review triggered.

Copy link
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: 5

🤖 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/charge.go`:
- Around line 96-102: The AsUsageBasedCharge method lacks a nil-check before
dereferencing c.usageBased; update AsUsageBasedCharge to first verify c.t ==
meta.ChargeTypeUsageBased (already present) and then check if c.usageBased ==
nil, returning an appropriate error like "usage based charge is nil" if so,
otherwise return *c.usageBased; mirror the pattern used in AsFlatFeeCharge to
avoid a potential nil pointer dereference.

In `@openmeter/billing/charges/usagebased/adapter/charge.go`:
- Around line 98-145: The code incorrectly pairs DB entities to input metas by
slice index; change the logic in the entutils.TransactingRepo callback so you
first build a map metaByID from input.Charges keyed by meta.ID, then iterate the
returned entities and for each entity use entity.ID to look up the corresponding
meta (use MapUsageBasedChargeFromDB(entity, metaByID[entity.ID],
input.Expands)); collect mapped charges, group by ID as before, and finally use
the existing final loop to restore caller order and report missing IDs. Ensure
you handle missing metas when mapping entities and propagate errors from
MapUsageBasedChargeFromDB.

In `@openmeter/billing/charges/usagebased/charge.go`:
- Around line 149-151: The struct field RealizationRun.Type currently uses the
JSON tag `status` causing incorrect serialization; update the tag on the Type
field in the RealizationRun definition from `json:"status"` to `json:"type"` so
API payloads and round-trips expose the run kind under the correct key.
- Around line 80-103: Add a pre-check in Intent.Validate to reject non-flat
prices with an empty FeatureKey: before calling i.Price.Validate(), check the
Price for non-flat (e.g., not a flat/one-time price) and if its FeatureKey is
empty append an error like "price: feature key required for non-flat prices";
reference the Price struct/field (i.Price.FeatureKey) and the Intent.Validate
method so this mirrors the rule enforced by billing.GatheringLineBase.Validate
and prevents persisting charges that will later fail gathering line validation.

In `@openmeter/billing/charges/usagebased/service/create.go`:
- Around line 34-43: The code is returning invoice-managed gathering lines for
non-credit usage-based charges (via gatheringLineFromUsageBasedCharge) which
causes those charges to enter invoice allocation before the usage-based
lifecycle is wired; change the behavior to keep GatheringLineToCreate nil until
the usage-based lifecycle/hooks are implemented or explicitly enabled: in the
function that maps charges (where ChargeWithGatheringLine is constructed) return
ChargeWithGatheringLine{Charge: charge} with no GatheringLineToCreate for
non-credit-only charges (or gate creation behind a clear boolean/feature flag on
the charges service), and apply the same fix to the other places in this file
that currently call gatheringLineFromUsageBasedCharge so no invoice-managed
lines are emitted prematurely.
🪄 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: 37179185-314b-41cd-9c7a-79598494205b

📥 Commits

Reviewing files that changed from the base of the PR and between c23b3d5 and 3533b26.

⛔ Files ignored due to path filters (54)
  • openmeter/ent/db/charge.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge/charge.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased/chargeusagebased.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations/chargeusagebasedruncreditallocations.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage/chargeusagebasedruninvoicedusage.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment/chargeusagebasedrunpayment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment_update.go is excluded by !**/ent/db/**
  • 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_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/client.go is excluded by !**/ent/db/**
  • openmeter/ent/db/ent.go is excluded by !**/ent/db/**
  • openmeter/ent/db/entmixinaccessor.go is excluded by !**/ent/db/**
  • openmeter/ent/db/expose.go is excluded by !**/ent/db/**
  • openmeter/ent/db/hook/hook.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/**
  • openmeter/ent/db/paginate.go is excluded by !**/ent/db/**
  • openmeter/ent/db/predicate/predicate.go is excluded by !**/ent/db/**
  • openmeter/ent/db/runtime.go is excluded by !**/ent/db/**
  • openmeter/ent/db/setorclear.go is excluded by !**/ent/db/**
  • openmeter/ent/db/tx.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (20)
  • openmeter/billing/charges/charge.go
  • openmeter/billing/charges/service/create.go
  • openmeter/billing/charges/service/get.go
  • openmeter/billing/charges/service/handlers_test.go
  • openmeter/billing/charges/service/service.go
  • openmeter/billing/charges/service/service_test.go
  • openmeter/billing/charges/usagebased/adapter.go
  • openmeter/billing/charges/usagebased/adapter/adapter.go
  • openmeter/billing/charges/usagebased/adapter/charge.go
  • openmeter/billing/charges/usagebased/adapter/mapper.go
  • openmeter/billing/charges/usagebased/charge.go
  • openmeter/billing/charges/usagebased/handler.go
  • openmeter/billing/charges/usagebased/service.go
  • openmeter/billing/charges/usagebased/service/create.go
  • openmeter/billing/charges/usagebased/service/get.go
  • openmeter/billing/charges/usagebased/service/service.go
  • openmeter/ent/schema/charges.go
  • openmeter/ent/schema/chargesusagebased.go
  • tools/migrate/migrations/20260316153605_charge_usage_based.down.sql
  • tools/migrate/migrations/20260316153605_charge_usage_based.up.sql

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

✅ Actions performed

Full review triggered.

Copy link
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

♻️ Duplicate comments (4)
openmeter/billing/charges/usagebased/charge.go (2)

149-151: ⚠️ Potential issue | 🟠 Major

RealizationRun.Type is exposed under the wrong JSON field.

Type is currently tagged as status, so serialized payloads won’t round-trip correctly.

💡 Suggested fix
-	Type       RealizationRunType    `json:"status"`
+	Type       RealizationRunType    `json:"type"`
🤖 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 149 - 151, The
RealizationRun struct exposes the Type field under the wrong JSON tag; update
the struct field declaration for RealizationRun.Type in charge.go to use the
correct JSON tag (json:"type") instead of json:"status" so
serialized/deserialized payloads round-trip correctly; ensure the change is
applied to the Type field of the RealizationRun type and run tests or
serialization checks to confirm no other code relies on the old "status" tag.

80-103: ⚠️ Potential issue | 🟠 Major

Please fail fast when non-flat prices are missing FeatureKey.

Right now this can pass Intent.Validate() and fail later downstream when building/validating invoice/gathering lines.

💡 Suggested fix
 func (i Intent) Validate() error {
 	var errs []error
@@
 	if i.InvoiceAt.IsZero() {
 		errs = append(errs, fmt.Errorf("invoice at is required"))
 	}
 
+	if i.Price.Type() != productcatalog.FlatPriceType && i.FeatureKey == "" {
+		errs = append(errs, fmt.Errorf("feature key is required for non-flat prices"))
+	}
+
 	if err := i.Price.Validate(); err != nil {
 		errs = append(errs, fmt.Errorf("price: %w", err))
 	}
🤖 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 80 - 103, The
Intent.Validate() flow must fail fast when a non-flat price is missing
FeatureKey: add an explicit check in Intent.Validate (before calling
i.Price.Validate or before returning) that detects a non-flat price (e.g., using
Price.Type or a helper like Price.IsFlat()) and returns an immediate error if
Price.FeatureKey is empty (e.g., "price: non-flat price requires feature key");
update the validation path around the existing Intent.Validate(),
Price.Validate(), and FeatureKey references so the missing FeatureKey is caught
early rather than downstream.
openmeter/billing/charges/charge.go (1)

96-102: ⚠️ Potential issue | 🔴 Critical

AsUsageBasedCharge() can panic on nil pointer.

It checks type, but not c.usageBased before dereferencing.

💡 Suggested fix
 func (c Charge) AsUsageBasedCharge() (usagebased.Charge, error) {
 	if c.t != meta.ChargeTypeUsageBased {
 		return usagebased.Charge{}, fmt.Errorf("charge is not a usage based charge")
 	}
 
+	if c.usageBased == nil {
+		return usagebased.Charge{}, fmt.Errorf("usage based charge is nil")
+	}
+
 	return *c.usageBased, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/charge.go` around lines 96 - 102,
AsUsageBasedCharge currently checks c.t against meta.ChargeTypeUsageBased but
dereferences c.usageBased without nil-check, which can panic; update
AsUsageBasedCharge to also validate c.usageBased != nil (in addition to c.t ==
meta.ChargeTypeUsageBased) and return a descriptive error (e.g., "usageBased
data is nil for usage-based charge") instead of dereferencing when nil so the
function safely returns (usagebased.Charge{}, error).
openmeter/billing/charges/usagebased/adapter/charge.go (1)

122-129: ⚠️ Potential issue | 🔴 Critical

Index-based pairing assumes query returns results in input order — this is a bug.

The IDIn(...) query has no ordering guarantee, but line 124 pairs entities[idx] with input.Charges[idx]. If the DB returns rows in a different order (or fewer rows), you'll map the wrong meta to the wrong entity.

The fix from the previous review is still needed — build a metaByID map first, then look up by entity.ID:

+	metaByID := make(map[string]meta.Charge, len(input.Charges))
+	for _, chargeMeta := range input.Charges {
+		metaByID[chargeMeta.ID] = chargeMeta
+	}
+
 	entitiesMapped := make([]usagebased.Charge, 0, len(entities))
-	for idx, entity := range entities {
-		charge, err := MapUsageBasedChargeFromDB(entity, input.Charges[idx], input.Expands)
+	for _, entity := range entities {
+		chargeMeta, ok := metaByID[entity.ID]
+		if !ok {
+			return nil, fmt.Errorf("charge meta not found for usage based charge [id=%s]", entity.ID)
+		}
+		charge, err := MapUsageBasedChargeFromDB(entity, chargeMeta, input.Expands)
 		if err != nil {
 			return nil, err
 		}
 		entitiesMapped = append(entitiesMapped, charge)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/usagebased/adapter/charge.go` around lines 122 -
129, The loop currently pairs DB rows to input charges by index (entities[idx]
with input.Charges[idx]) which is incorrect because IDIn queries are unordered;
instead build a map from input charge ID to its metadata (e.g., metaByID from
input.Charges keyed by charge.ID) and then iterate entities, looking up the
matching meta via entity.ID and passing that to MapUsageBasedChargeFromDB;
ensure you handle missing meta (returning an error) and update the code that
appends to entitiesMapped to use the looked-up meta rather than
input.Charges[idx].
🧹 Nitpick comments (1)
openmeter/billing/charges/usagebased/adapter/mapper.go (1)

68-93: Edge loading error handling is strict but potentially confusing.

The pattern here returns an error when edges aren't loaded (lines 69-71, 78-80, 87-89), but then proceeds to use those edges anyway if they are loaded. This works, but the control flow is a bit subtle:

  • CreditAllocationsOrErr() returns an error if not loaded
  • You check for NotLoadedError and return early
  • But if there's no error, you proceed

This is correct, but the variable naming (err) being reused without checking the actual error value in the happy path might confuse future readers. Consider adding a brief comment or restructuring slightly.

That said, this matches similar patterns elsewhere, so it's fine as-is if that's the convention.

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

In `@openmeter/billing/charges/usagebased/adapter/mapper.go` around lines 68 - 93,
The edge-loading checks reuse the same err variable for multiple EdgeOrErr calls
(CreditAllocationsOrErr, InvoicedUsageOrErr, PaymentOrErr), which makes the
control flow subtle; rename each error variable (e.g., errAlloc, errInvoice,
errPayment) or explicitly check err != nil before using lo.ErrorsAs for each
call and add a one-line comment above each block explaining "return on
NotLoadedError for this edge"; update the blocks that set run.CreditsAllocated,
run.InvoiceUsage, and run.Payment to reference the new error variables so the
intent is explicit and future readers won't be confused.
🤖 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/service_test.go`:
- Around line 781-864: The test named TestUsageBasedPartialCreditRealizations
only creates and fetches a usage-based charge but does not exercise partial
credit realizations; either rename it to TestUsageBasedCreateAndGet or extend it
to follow the full lifecycle used by TestFlatFeePartialCreditRealizations: after
creating the charge with Charges.Create and verifying the gathering invoice via
BillingService.ListGatheringInvoices and mustGetChargeByID, create an invoice
for the servicePeriod, apply partial credit realizations to that invoice,
approve the payment (manual approval flow already configured), settle the
invoice and assert settlement/realization state and amounts (mirror the
assertions and helper calls used in TestFlatFeePartialCreditRealizations).
Ensure you update the test function name
(TestUsageBasedPartialCreditRealizations) or add the additional
invoice/realization/assert calls around usageBasedChargeID and related helpers
accordingly.

In `@openmeter/billing/charges/usagebased/service/create.go`:
- Line 47: Rename the parameter in function gatheringLineFromUsageBasedCharge
from flatFee to charge (its type is usagebased.Charge) and update all internal
references to use charge instead of flatFee, including uses in the return type
handling for usagebased.ChargeWithGatheringLine and any variable accesses or
field lookups that currently reference flatFee.

---

Duplicate comments:
In `@openmeter/billing/charges/charge.go`:
- Around line 96-102: AsUsageBasedCharge currently checks c.t against
meta.ChargeTypeUsageBased but dereferences c.usageBased without nil-check, which
can panic; update AsUsageBasedCharge to also validate c.usageBased != nil (in
addition to c.t == meta.ChargeTypeUsageBased) and return a descriptive error
(e.g., "usageBased data is nil for usage-based charge") instead of dereferencing
when nil so the function safely returns (usagebased.Charge{}, error).

In `@openmeter/billing/charges/usagebased/adapter/charge.go`:
- Around line 122-129: The loop currently pairs DB rows to input charges by
index (entities[idx] with input.Charges[idx]) which is incorrect because IDIn
queries are unordered; instead build a map from input charge ID to its metadata
(e.g., metaByID from input.Charges keyed by charge.ID) and then iterate
entities, looking up the matching meta via entity.ID and passing that to
MapUsageBasedChargeFromDB; ensure you handle missing meta (returning an error)
and update the code that appends to entitiesMapped to use the looked-up meta
rather than input.Charges[idx].

In `@openmeter/billing/charges/usagebased/charge.go`:
- Around line 149-151: The RealizationRun struct exposes the Type field under
the wrong JSON tag; update the struct field declaration for RealizationRun.Type
in charge.go to use the correct JSON tag (json:"type") instead of json:"status"
so serialized/deserialized payloads round-trip correctly; ensure the change is
applied to the Type field of the RealizationRun type and run tests or
serialization checks to confirm no other code relies on the old "status" tag.
- Around line 80-103: The Intent.Validate() flow must fail fast when a non-flat
price is missing FeatureKey: add an explicit check in Intent.Validate (before
calling i.Price.Validate or before returning) that detects a non-flat price
(e.g., using Price.Type or a helper like Price.IsFlat()) and returns an
immediate error if Price.FeatureKey is empty (e.g., "price: non-flat price
requires feature key"); update the validation path around the existing
Intent.Validate(), Price.Validate(), and FeatureKey references so the missing
FeatureKey is caught early rather than downstream.

---

Nitpick comments:
In `@openmeter/billing/charges/usagebased/adapter/mapper.go`:
- Around line 68-93: The edge-loading checks reuse the same err variable for
multiple EdgeOrErr calls (CreditAllocationsOrErr, InvoicedUsageOrErr,
PaymentOrErr), which makes the control flow subtle; rename each error variable
(e.g., errAlloc, errInvoice, errPayment) or explicitly check err != nil before
using lo.ErrorsAs for each call and add a one-line comment above each block
explaining "return on NotLoadedError for this edge"; update the blocks that set
run.CreditsAllocated, run.InvoiceUsage, and run.Payment to reference the new
error variables so the intent is explicit and future readers won't be confused.
🪄 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: 5c18e408-9069-4d0a-8102-e6152b9a6dd8

📥 Commits

Reviewing files that changed from the base of the PR and between c23b3d5 and 3533b26.

⛔ Files ignored due to path filters (54)
  • openmeter/ent/db/charge.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge/charge.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased/chargeusagebased.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations/chargeusagebasedruncreditallocations.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage/chargeusagebasedruninvoicedusage.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment/chargeusagebasedrunpayment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment_update.go is excluded by !**/ent/db/**
  • 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_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/client.go is excluded by !**/ent/db/**
  • openmeter/ent/db/ent.go is excluded by !**/ent/db/**
  • openmeter/ent/db/entmixinaccessor.go is excluded by !**/ent/db/**
  • openmeter/ent/db/expose.go is excluded by !**/ent/db/**
  • openmeter/ent/db/hook/hook.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/**
  • openmeter/ent/db/paginate.go is excluded by !**/ent/db/**
  • openmeter/ent/db/predicate/predicate.go is excluded by !**/ent/db/**
  • openmeter/ent/db/runtime.go is excluded by !**/ent/db/**
  • openmeter/ent/db/setorclear.go is excluded by !**/ent/db/**
  • openmeter/ent/db/tx.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (20)
  • openmeter/billing/charges/charge.go
  • openmeter/billing/charges/service/create.go
  • openmeter/billing/charges/service/get.go
  • openmeter/billing/charges/service/handlers_test.go
  • openmeter/billing/charges/service/service.go
  • openmeter/billing/charges/service/service_test.go
  • openmeter/billing/charges/usagebased/adapter.go
  • openmeter/billing/charges/usagebased/adapter/adapter.go
  • openmeter/billing/charges/usagebased/adapter/charge.go
  • openmeter/billing/charges/usagebased/adapter/mapper.go
  • openmeter/billing/charges/usagebased/charge.go
  • openmeter/billing/charges/usagebased/handler.go
  • openmeter/billing/charges/usagebased/service.go
  • openmeter/billing/charges/usagebased/service/create.go
  • openmeter/billing/charges/usagebased/service/get.go
  • openmeter/billing/charges/usagebased/service/service.go
  • openmeter/ent/schema/charges.go
  • openmeter/ent/schema/chargesusagebased.go
  • tools/migrate/migrations/20260316153605_charge_usage_based.down.sql
  • tools/migrate/migrations/20260316153605_charge_usage_based.up.sql

Copy link
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

♻️ Duplicate comments (1)
openmeter/billing/charges/usagebased/adapter/charge.go (1)

122-149: ⚠️ Potential issue | 🔴 Critical

Map DB rows to metas by entity.ID, not slice position.

Line 124 assumes query.All(ctx) result order matches input.Charges. With IDIn(...) and no explicit ordering, that is unsafe and can bind the wrong meta.Charge to a row.

💡 Proposed fix
-		entitiesMapped := make([]usagebased.Charge, 0, len(entities))
-		for idx, entity := range entities {
-			charge, err := MapUsageBasedChargeFromDB(entity, input.Charges[idx], input.Expands)
+		metaByID := make(map[string]meta.Charge, len(input.Charges))
+		for _, chargeMeta := range input.Charges {
+			metaByID[chargeMeta.ID] = chargeMeta
+		}
+
+		entitiesMapped := make([]usagebased.Charge, 0, len(entities))
+		for _, entity := range entities {
+			chargeMeta, ok := metaByID[entity.ID]
+			if !ok {
+				return nil, fmt.Errorf("charge meta not found for usage based charge [id=%s]", entity.ID)
+			}
+
+			charge, err := MapUsageBasedChargeFromDB(entity, chargeMeta, input.Expands)
 			if err != nil {
 				return nil, err
 			}
 			entitiesMapped = append(entitiesMapped, charge)
 		}

Use this to verify the positional mapping assumption and missing explicit ordering:

#!/bin/bash
set -euo pipefail

file="openmeter/billing/charges/usagebased/adapter/charge.go"

echo "=== GetByIDs block ==="
sed -n '93,156p' "$file"

echo
echo "=== Positional mapping callsite ==="
rg -nP 'MapUsageBasedChargeFromDB\(entity,\s*input\.Charges\[idx\]' "$file"

echo
echo "=== Query section (check for explicit ordering) ==="
rg -n -C2 'ChargeUsageBased\.Query|IDIn\(|All\(ctx\)|Order\(' "$file"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/usagebased/adapter/charge.go` around lines 122 -
149, The current loop calls MapUsageBasedChargeFromDB(entity,
input.Charges[idx], ...) assuming entities are in same order as input.Charges;
instead build a map of input charges by ID and, when iterating the DB rows
(entities), look up the corresponding input charge by entity.ID before calling
MapUsageBasedChargeFromDB; update the code that creates entitiesMapped (and
subsequently entitiesByID) to use that lookup, return an error if a DB row has
no matching input charge, and keep the final verification that the number of
mapped charges equals len(input.Charges). Ensure you touch the
MapUsageBasedChargeFromDB call site, the entitiesMapped construction, and the
lookup logic using entity.ID rather than the slice index.
🤖 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/service_test.go`:
- Around line 863-864: The assertion s.NotEmpty(usageBasedChargeID) is outside
the s.Run subtest and can panic if the subtest calls FailNow; move the
s.NotEmpty(usageBasedChargeID) check inside the s.Run closure (alongside the
existing s.NoError checks) or remove it entirely — alternatively rely on the
existing mustGetChargeByID/GetChargeID validation at line 860–865; update the
test so any assertion that depends on the subtest's success (usageBasedChargeID)
executes inside the subtest to avoid the "subtest may have called FailNow on a
parent test" panic.

In `@openmeter/billing/charges/usagebased/charge.go`:
- Around line 178-180: The error message for the meter value check is
misleading: the code only rejects negative values via r.MeterValue.IsNegative(),
so update the error text appended to errs in that conditional to reflect the
actual rule (e.g., use "meter value must be non-negative" or "meter value must
not be negative") so the message matches the check in the function where
r.MeterValue.IsNegative() is used.

---

Duplicate comments:
In `@openmeter/billing/charges/usagebased/adapter/charge.go`:
- Around line 122-149: The current loop calls MapUsageBasedChargeFromDB(entity,
input.Charges[idx], ...) assuming entities are in same order as input.Charges;
instead build a map of input charges by ID and, when iterating the DB rows
(entities), look up the corresponding input charge by entity.ID before calling
MapUsageBasedChargeFromDB; update the code that creates entitiesMapped (and
subsequently entitiesByID) to use that lookup, return an error if a DB row has
no matching input charge, and keep the final verification that the number of
mapped charges equals len(input.Charges). Ensure you touch the
MapUsageBasedChargeFromDB call site, the entitiesMapped construction, and the
lookup logic using entity.ID rather than the slice index.
🪄 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: 5ed6fb95-49a8-4cb2-9dcc-410193b76573

📥 Commits

Reviewing files that changed from the base of the PR and between 3533b26 and dc63d6d.

📒 Files selected for processing (5)
  • openmeter/billing/charges/charge.go
  • openmeter/billing/charges/service/service_test.go
  • openmeter/billing/charges/usagebased/adapter/charge.go
  • openmeter/billing/charges/usagebased/charge.go
  • test/billing/suite.go

Copy link
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

♻️ Duplicate comments (1)
openmeter/billing/charges/usagebased/adapter/charge.go (1)

122-153: ⚠️ Potential issue | 🔴 Critical

Map DB rows to meta by entity.ID, not by idx.

query.All(ctx) with IDIn(...) has no guaranteed positional match to input.Charges, so input.Charges[idx] can pair wrong meta with a row. Also, missing-ID details in errs are currently hidden by the earlier length check.

💡 Suggested fix
-		entitiesMapped := make([]usagebased.Charge, 0, len(entities))
-		for idx, entity := range entities {
-			charge, err := MapUsageBasedChargeFromDB(entity, input.Charges[idx], input.Expands)
+		metaByID := make(map[string]meta.Charge, len(input.Charges))
+		for _, chargeMeta := range input.Charges {
+			metaByID[chargeMeta.ID] = chargeMeta
+		}
+
+		entitiesMapped := make([]usagebased.Charge, 0, len(entities))
+		for _, entity := range entities {
+			chargeMeta, ok := metaByID[entity.ID]
+			if !ok {
+				return nil, fmt.Errorf("charge meta not found for usage based charge [id=%s]", entity.ID)
+			}
+
+			charge, err := MapUsageBasedChargeFromDB(entity, chargeMeta, input.Expands)
 			if err != nil {
 				return nil, err
 			}
 			entitiesMapped = append(entitiesMapped, charge)
 		}
@@
-		if len(out) != len(input.Charges) {
-			return nil, fmt.Errorf("expected to fetch %d charges, got %d", len(input.Charges), len(out))
-		}
-
 		if len(errs) > 0 {
 			return nil, errors.Join(errs...)
 		}
#!/bin/bash
# Verify index-based mapping and lack of explicit ordering in GetByIDs
rg -n -C3 'MapUsageBasedChargeFromDB\(entity, input\.Charges\[idx\], input\.Expands\)' openmeter/billing/charges/usagebased/adapter/charge.go
rg -n -C3 'IDIn\(' openmeter/billing/charges/usagebased/adapter/charge.go
rg -n -C3 '\.Order\(' openmeter/billing/charges/usagebased/adapter/charge.go
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/usagebased/adapter/charge.go` around lines 122 -
153, The loop currently pairs DB rows to input meta by index
(MapUsageBasedChargeFromDB(entity, input.Charges[idx], ...)), which is unsafe
because query.All(ctx) results are not guaranteed to preserve order; instead
build a map of input charges by ID (e.g., inputByID from input.Charges keyed by
charge.ID) and for each DB entity lookup the corresponding input meta by
entity.ID and call MapUsageBasedChargeFromDB(entity, inputMeta, input.Expands);
then reconstruct output in the original input order by iterating input.Charges
and pulling mapped results from entitiesByID (or a map of mapped results keyed
by ID), include missing-ID details in errs (use the charge.ID) and return
errors.Join(errs...) if any remain (remove any length-check that hides these
errs).
🧹 Nitpick comments (2)
openmeter/billing/charges/service/get.go (1)

97-113: Skip usage-based fetch when there are no usage-based metas.

This branch currently calls s.usageBasedService.GetByIDs even when chargesByType[meta.ChargeTypeUsageBased] is empty, which adds avoidable DB work in a hot retrieval path.

♻️ Suggested guard
-		// Let's fetch usage based charges
-		usageBasedCharges, err := s.usageBasedService.GetByIDs(ctx, usagebased.GetByIDsInput{
-			Namespace: input.Namespace,
-			Charges: lo.Map(chargesByType[meta.ChargeTypeUsageBased], func(chargeMeta charges.WithIndex[meta.Charge], _ int) meta.Charge {
-				return chargeMeta.Value
-			}),
-			Expands: input.Expands,
-		})
-		if err != nil {
-			return nil, err
-		}
-
-		for i, usageBasedCharge := range usageBasedCharges {
-			targetIndex := chargesByType[meta.ChargeTypeUsageBased][i].Index
-			out[targetIndex] = charges.NewCharge(usageBasedCharge)
-			nrFetched++
-		}
+		// Let's fetch usage based charges
+		usageBasedMetas := chargesByType[meta.ChargeTypeUsageBased]
+		if len(usageBasedMetas) > 0 {
+			usageBasedCharges, err := s.usageBasedService.GetByIDs(ctx, usagebased.GetByIDsInput{
+				Namespace: input.Namespace,
+				Charges: lo.Map(usageBasedMetas, func(chargeMeta charges.WithIndex[meta.Charge], _ int) meta.Charge {
+					return chargeMeta.Value
+				}),
+				Expands: input.Expands,
+			})
+			if err != nil {
+				return nil, err
+			}
+
+			for i, usageBasedCharge := range usageBasedCharges {
+				targetIndex := usageBasedMetas[i].Index
+				out[targetIndex] = charges.NewCharge(usageBasedCharge)
+				nrFetched++
+			}
+		}

As per coding guidelines, "Performance should be a priority in critical code paths... database operations ... should be vetted for potential performance bottlenecks."

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

In `@openmeter/billing/charges/service/get.go` around lines 97 - 113, The code
always calls s.usageBasedService.GetByIDs even when there are no usage-based
metas; add a guard that checks if len(chargesByType[meta.ChargeTypeUsageBased])
== 0 and skip the GetByIDs call and subsequent loop when empty to avoid
unnecessary DB work; keep the existing logic inside the block (constructing
GetByIDsInput, handling err, iterating usageBasedCharges and assigning
out[targetIndex]) but wrap it in an if len(...) > 0 branch so
s.usageBasedService.GetByIDs, usageBasedCharges and the for loop only run when
there are usage-based charge metas.
openmeter/billing/charges/service/create.go (1)

99-99: Comment text is a bit stale now.

This now creates gathering lines for flat-fee and usage-based charges, so the comment could be updated to avoid confusion later.

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

In `@openmeter/billing/charges/service/create.go` at line 99, The inline comment
"// Let's generate the gathering lines for the flat fees" is stale; update it to
accurately describe what the following code does by replacing it with a concise
comment stating that it generates gathering lines for both flat-fee and
usage-based charges (or for all charge types) so future readers of the code
around the gathering line generation in create.go understand that it handles
usage-based charges as well.
🤖 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/base_test.go`:
- Around line 183-190: BaseSuite.SetupSuite is creating the service via
service.New(Config{...}) but no UsageBasedService is provided, which now fails
config validation; update the test suite wiring in BaseSuite.SetupSuite to
include a mock or real UsageBasedService implementation in the Config passed to
service.New (refer to the Config type and the UsageBasedService interface) so
service.New succeeds during test setup and the new usage-based intent tests
don't fail during initialization.

In `@openmeter/billing/charges/service/invoicable_test.go`:
- Around line 263-346: The test TestUsageBasedPartialCreditRealizations
currently only creates a usage-based charge and verifies gathering lines (via
Charges.Create, billing.BillingService.ListGatheringInvoices, and
mustGetChargeByID) but does not exercise invoicing or credit realization logic;
either rename the test to reflect "create/get and gathering line linkage" (e.g.,
TestUsageBasedCreateAndGatheringLine) or extend it to advance the clock, run
invoicing (invoke the service that generates invoices for
servicePeriod.From..To), apply/realize credits, then assert invoice totals and
credit realizations against usageBasedChargeID and the generated invoices;
update assertions to check realized credits/totals on the produced invoices and
any related credit records.

---

Duplicate comments:
In `@openmeter/billing/charges/usagebased/adapter/charge.go`:
- Around line 122-153: The loop currently pairs DB rows to input meta by index
(MapUsageBasedChargeFromDB(entity, input.Charges[idx], ...)), which is unsafe
because query.All(ctx) results are not guaranteed to preserve order; instead
build a map of input charges by ID (e.g., inputByID from input.Charges keyed by
charge.ID) and for each DB entity lookup the corresponding input meta by
entity.ID and call MapUsageBasedChargeFromDB(entity, inputMeta, input.Expands);
then reconstruct output in the original input order by iterating input.Charges
and pulling mapped results from entitiesByID (or a map of mapped results keyed
by ID), include missing-ID details in errs (use the charge.ID) and return
errors.Join(errs...) if any remain (remove any length-check that hides these
errs).

---

Nitpick comments:
In `@openmeter/billing/charges/service/create.go`:
- Line 99: The inline comment "// Let's generate the gathering lines for the
flat fees" is stale; update it to accurately describe what the following code
does by replacing it with a concise comment stating that it generates gathering
lines for both flat-fee and usage-based charges (or for all charge types) so
future readers of the code around the gathering line generation in create.go
understand that it handles usage-based charges as well.

In `@openmeter/billing/charges/service/get.go`:
- Around line 97-113: The code always calls s.usageBasedService.GetByIDs even
when there are no usage-based metas; add a guard that checks if
len(chargesByType[meta.ChargeTypeUsageBased]) == 0 and skip the GetByIDs call
and subsequent loop when empty to avoid unnecessary DB work; keep the existing
logic inside the block (constructing GetByIDsInput, handling err, iterating
usageBasedCharges and assigning out[targetIndex]) but wrap it in an if len(...)
> 0 branch so s.usageBasedService.GetByIDs, usageBasedCharges and the for loop
only run when there are usage-based charge metas.
🪄 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: 9e76768f-6861-421d-b431-0f9f7f78ea63

📥 Commits

Reviewing files that changed from the base of the PR and between 91086aa and 14bf466.

⛔ Files ignored due to path filters (55)
  • openmeter/ent/db/charge.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge/charge.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/charge_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased/chargeusagebased.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations/chargeusagebasedruncreditallocations.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage/chargeusagebasedruninvoicedusage.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruninvoicedusage_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment/chargeusagebasedrunpayment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment_update.go is excluded by !**/ent/db/**
  • 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_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/client.go is excluded by !**/ent/db/**
  • openmeter/ent/db/cursor.go is excluded by !**/ent/db/**
  • openmeter/ent/db/ent.go is excluded by !**/ent/db/**
  • openmeter/ent/db/entmixinaccessor.go is excluded by !**/ent/db/**
  • openmeter/ent/db/expose.go is excluded by !**/ent/db/**
  • openmeter/ent/db/hook/hook.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/**
  • openmeter/ent/db/paginate.go is excluded by !**/ent/db/**
  • openmeter/ent/db/predicate/predicate.go is excluded by !**/ent/db/**
  • openmeter/ent/db/runtime.go is excluded by !**/ent/db/**
  • openmeter/ent/db/setorclear.go is excluded by !**/ent/db/**
  • openmeter/ent/db/tx.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (24)
  • openmeter/billing/charges/charge.go
  • openmeter/billing/charges/service/base_test.go
  • openmeter/billing/charges/service/create.go
  • openmeter/billing/charges/service/get.go
  • openmeter/billing/charges/service/handlers_test.go
  • openmeter/billing/charges/service/invoicable_test.go
  • openmeter/billing/charges/service/service.go
  • openmeter/billing/charges/usagebased/adapter.go
  • openmeter/billing/charges/usagebased/adapter/adapter.go
  • openmeter/billing/charges/usagebased/adapter/charge.go
  • openmeter/billing/charges/usagebased/adapter/mapper.go
  • openmeter/billing/charges/usagebased/charge.go
  • openmeter/billing/charges/usagebased/handler.go
  • openmeter/billing/charges/usagebased/service.go
  • openmeter/billing/charges/usagebased/service/create.go
  • openmeter/billing/charges/usagebased/service/get.go
  • openmeter/billing/charges/usagebased/service/service.go
  • openmeter/ent/schema/charges.go
  • openmeter/ent/schema/chargesusagebased.go
  • test/app/custominvoicing/invocing_test.go
  • test/billing/subscription_test.go
  • test/billing/suite.go
  • tools/migrate/migrations/20260317164048_charge_usage_based.down.sql
  • tools/migrate/migrations/20260317164048_charge_usage_based.up.sql
🚧 Files skipped from review as they are similar to previous changes (5)
  • openmeter/billing/charges/usagebased/handler.go
  • openmeter/billing/charges/usagebased/service/service.go
  • openmeter/billing/charges/usagebased/service/get.go
  • openmeter/billing/charges/usagebased/adapter/adapter.go
  • openmeter/billing/charges/usagebased/adapter.go

Copy link
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.

♻️ Duplicate comments (1)
openmeter/billing/charges/service/base_test.go (1)

46-46: ⚠️ Potential issue | 🟡 Minor

Please reset UsageBasedTestHandler in teardown.

You initialize UsageBasedTestHandler in setup, but TeardownTest doesn’t reset it. That can leak state across tests.

Suggested fix
 func (s *BaseSuite) TeardownTest() {
 	s.FlatFeeTestHandler.Reset()
 	s.CreditPurchaseTestHandler.Reset()
+	s.UsageBasedTestHandler.Reset()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/base_test.go` at line 46, TeardownTest
currently doesn’t clear the test state set in setup: reset the
UsageBasedTestHandler field in TeardownTest to avoid leaking state between tests
by assigning nil (or a fresh zero value) to s.UsageBasedTestHandler; locate
where setup calls newUsageBasedTestHandler() and add the reset in the
TeardownTest method to mirror initialization and ensure tests are isolated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@openmeter/billing/charges/service/base_test.go`:
- Line 46: TeardownTest currently doesn’t clear the test state set in setup:
reset the UsageBasedTestHandler field in TeardownTest to avoid leaking state
between tests by assigning nil (or a fresh zero value) to
s.UsageBasedTestHandler; locate where setup calls newUsageBasedTestHandler() and
add the reset in the TeardownTest method to mirror initialization and ensure
tests are isolated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f5490fe3-08d9-450f-9037-58177f0c6812

📥 Commits

Reviewing files that changed from the base of the PR and between 14bf466 and 4fb60c8.

📒 Files selected for processing (2)
  • openmeter/billing/charges/service/base_test.go
  • openmeter/billing/charges/service/invoicable_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • openmeter/billing/charges/service/invoicable_test.go

@turip turip enabled auto-merge (squash) March 17, 2026 17:20
@turip turip requested a review from tothandras March 17, 2026 18:26
@turip turip requested a review from GAlexIHU March 17, 2026 18:26
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.

1 participant