Skip to content

feat(ledger): ubp, corrections and lineage tracking#4115

Merged
GAlexIHU merged 14 commits into
mainfrom
feat/ledger-ubp
Apr 9, 2026
Merged

feat(ledger): ubp, corrections and lineage tracking#4115
GAlexIHU merged 14 commits into
mainfrom
feat/ledger-ubp

Conversation

@GAlexIHU
Copy link
Copy Markdown
Contributor

@GAlexIHU GAlexIHU commented Apr 9, 2026

Summary

This PR refactors credit-only charge accrual/correction handling around a new shared ledger collector and adds lineage tracking for credit realizations.

What changed

  • introduced a dedicated ledger collector flow for usage-based and flat-fee credit-only charges, replacing the older charge-adapter-specific ledger orchestration
  • added ledger support for correction planning/execution, including mixed advance vs purchased-credit cases and backfilled-advance correction handling
  • added persisted credit realization lineage (credit_realization_lineage + segments) to track what each realization currently represents over time
  • implemented lineage lifecycle handling for:
    • initial allocation creation
    • purchase-time advance backfill
    • correction writeback
  • extracted lineage write orchestration into a dedicated charges/lineage service, and moved lineage triggering into the charge service/state-machine layer instead of hiding it inside adapters
  • kept read-side active-lineage expansion on charge fetches so correction flows can reason from current lineage state

Why

  • corrections now need to distinguish between:
    • real credit
    • uncovered advance
    • advance that was later backfilled by a credit purchase
  • lineage gives us a durable way to answer “what does this allocated value represent now?” so later corrections, refunds, and purchases unwind the right buckets

Test coverage

  • added lineage-focused service tests for:
    • initial lineage creation
    • transaction-scoped locking
    • correction writeback ordering
  • added end-to-end credit sanity coverage for:
    • partial backfill + correction + delete
    • settle-before-delete vs delete-before-settle
    • multi-charge / multi-purchase lifecycles
  • expanded balance assertions throughout the lifecycle sanities to verify intermediate states, not just terminal outcomes

Review Guidance

A good way to review this PR is in this order:

  1. openmeter/ledger/collector/...
  2. openmeter/billing/charges/models/creditrealization/... and openmeter/ent/schema/creditrealizationlineage.go
  3. openmeter/billing/charges/lineage/...
  4. openmeter/billing/charges/flatfee/..., .../usagebased/..., .../creditpurchase/...
  5. test/credits/... and openmeter/billing/charges/service/lineage_test.go

Business Behavior Gotchas

  • Released purchased credit goes back to FBO as available credit. It does not automatically re-cover other uncovered advance.
  • Backfill happens on credit purchase initiation, not on payment settlement.
  • Payment settlement clears the purchased-credit receivable, but does not itself drive lineage re-attribution.
  • cost_basis=nil receivable represents uncovered advance exposure; purchased cost_basis receivable represents the purchase-side payment obligation.
  • Correction writeback consumes active lineage in precedence order:
    • advance_backfilled
    • advance_uncovered
    • real_credit
    • Because of that ordering, a correction will release purchased backing before it reduces still-uncovered advance.
  • Different lifecycle orderings can produce different intermediate balances, even when the terminal state is the same.

Summary by CodeRabbit

  • New Features

    • Added credit realization lineage tracking system to audit and trace credit allocations throughout their lifecycle (real, advance uncovered, advance backfilled states).
    • Implemented ledger transaction correction framework enabling reversal and adjustment of previously committed transactions with proper unwinding of ledger impacts.
    • Added support for backfilling advance credits with actual transaction backing when subsequent purchases occur.
  • Tests

    • Added comprehensive lineage and correction lifecycle tests validating credit tracking and ledger unwinding.

@GAlexIHU GAlexIHU requested a review from a team as a code owner April 9, 2026 12:51
@GAlexIHU GAlexIHU added the release-note/ignore Ignore this change when generating release notes label Apr 9, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

This PR introduces credit realization lineage tracking for billing charges, adding persistence, querying, and lifecycle management of credit allocation segments. It implements a new lineage adapter and service layer, integrates a ledger collector for accrual handling, and extends transaction templates with correction operations to support charge reversals and adjustments.

Changes

Cohort / File(s) Summary
Lineage Domain & Models
openmeter/billing/charges/lineage/...
Introduced lineage service interface, adapter implementation (Ent-backed with transaction support), segment/lineage domain models, and helpers for segment sorting, validation, and origin-kind mapping. Defines active segment queries, creation, locking, and closing operations.
Credit Realization Models
openmeter/billing/charges/models/creditrealization/lineage.go, openmeter/billing/charges/models/creditrealization/lineage_specs.go
Added lineage origin kinds (real_credit, advance) and segment states (real_credit, advance_uncovered, advance_backfilled) with validation. Introduced InitialLineageSpecs to generate lineage segment specs from credit realizations.
Charge Service Wiring
app/common/charges.go, app/common/customerbalance.go
Added NewChargesCollectorService, NewChargesLineageAdapter, and NewChargesLineageService constructors. Updated flat-fee, usage-based, and credit-purchase service configs to require Lineage dependency and wired collector/lineage into handlers.
Flat-Fee Charge Integration
openmeter/billing/charges/flatfee/...
Added createCreditAllocations helper to create credit realizations with initial and correction lineage records. Updated credit allocation call sites to use service helper instead of adapter directly. Extended handlers to accept lineage service and use collector for accrual operations.
Usage-Based Charge Integration
openmeter/billing/charges/usagebased/...
Added createRunCreditRealizations helper for credit realization with lineage initialization/persistence. Updated credit-only handler to use collector service. Modified correction paths to load and inject active lineage segments.
Credit Purchase Integration
openmeter/billing/charges/creditpurchase/service/...
Wrapped external and invoice credit purchase initiation in transactions. Added lineage backfill calls when transaction group IDs are present. Extended Config to require Lineage service dependency.
Ledger Collector Service
openmeter/ledger/collector/...
Implemented Service interface with CollectToAccrued (FBO→accrued + advance receivable staging) and CorrectCollectedAccrued (correction planning/resolution with lineage-aware segment consumption). Handles mixed real-credit and advance-backed allocations.
Ledger Transaction Corrections
openmeter/ledger/transactions/correction.go, openmeter/ledger/transactions/accrual.go, openmeter/ledger/transactions/customer.go, openmeter/ledger/transactions/earnings.go, openmeter/ledger/transactions/fx.go
Added CorrectTransaction orchestrator and template-specific correct methods for reversing transfers, issues, and translations. Renamed TransferCustomerFBOBucketToAccruedTemplate to TransferCustomerFBOAdvanceToAccruedTemplate.
Ledger Primitives & Historical
openmeter/ledger/primitives.go, openmeter/ledger/historical/..., openmeter/ledger/transactions/...
Extended TransactionInput/Transaction interfaces with Annotations(). Added GetTransactionGroup to load committed groups. Updated transaction resolution to annotate outputs with template name and direction.
Ledger Routing & Transactions
openmeter/ledger/routingrules/routingrules.go, openmeter/ledger/transactions/accrual.go
Extended flow-direction validation to accept reversed-sign transfers. Changed cost-basis translation to accept either direction. Updated templates and helpers to support bidirectional accrual flows.
Charge Handlers & Adapters
openmeter/ledger/chargeadapter/flatfee.go, openmeter/ledger/chargeadapter/usagebased.go, openmeter/ledger/chargeadapter/helpers.go
Refactored flat-fee handler to use collector service instead of inline allocation logic. Introduced usage-based handler backed by collector. Added helper for retrieving sub-account settled balances.
Database Schema
openmeter/ent/schema/creditrealizationlineage.go, tools/migrate/migrations/20260409130630_*
Added Ent schemas for CreditRealizationLineage and CreditRealizationLineageSegment with immutable metadata, segment state tracking, and backing transaction group references. Created migration files for table/index setup.
Test Support & Fixtures
openmeter/billing/charges/service/base_test.go, openmeter/billing/charges/service/lineage_test.go, openmeter/billing/charges/testutils/service.go, openmeter/ledger/customerbalance/testenv_test.go, openmeter/ledger/transactions/testenv_test.go
Updated test environments to construct and wire lineage adapters/services. Added comprehensive end-to-end lineage tests covering segment consumption, backfill, and correction flows. Updated test helpers with template type signatures.
Integration & Sanity Tests
test/credits/sanity_test.go, test/credits/sanity_lifecycle_test.go, openmeter/ledger/chargeadapter/flatfee_test.go, openmeter/ledger/chargeadapter/usagebased_test.go, openmeter/ledger/chargeadapter/creditpurchase_test.go, openmeter/ledger/transactions/correction_test.go
Added comprehensive sanity tests for credit-only delete corrections, partial backfill scenarios, and multi-charge/multi-purchase lifecycles. Updated handler tests to use collector service. Added correction template tests.
Documentation
AGENTS.md
Added guidance for usage-based billing lifecycle testing via charges.Service APIs, lineage annotation/segment handling with StoredAt cutoff logic, and adapter-layer transaction wrapping requirements.

Sequence Diagrams

sequenceDiagram
    participant Client as Charges Service
    participant Collector as Ledger Collector
    participant Ledger as Ledger Repo
    participant Lineage as Lineage Service
    participant DB as Database

    Client->>Collector: CollectToAccrued(charge, amount)
    Collector->>Ledger: ResolveTransactions(FBO→Accrued template)
    Ledger-->>Collector: Transaction inputs
    Collector->>Ledger: CommitGroup(inputs)
    Ledger->>DB: Insert transactions
    Ledger-->>Collector: Committed group ID
    Collector->>Client: Create allocation inputs
    
    Client->>Lineage: CreateInitialLineages(allocations)
    Lineage->>DB: Insert lineage roots + segments
    Lineage-->>Client: ✓

    Client->>Collector: CorrectCollectedAccrued(corrections)
    Collector->>Lineage: LoadActiveSegments(realizations)
    Lineage->>DB: Query segments by realization
    Lineage-->>Collector: Active segments
    Collector->>Collector: Plan segment consumption
    Collector->>Ledger: CorrectTransaction(per-segment)
    Ledger->>DB: Insert corrections
    Ledger-->>Collector: Correction amounts
    
    Collector->>Lineage: CloseSegment / CreateSegment
    Lineage->>DB: Update segment state
    Lineage-->>Collector: ✓
    Collector-->>Client: Correction inputs
Loading
sequenceDiagram
    participant FlatFee as Flat-Fee Handler
    participant CreditAlloc as Credit Allocation Helper
    participant Adapter as Charge Adapter
    participant Lineage as Lineage Service
    participant Ledger as Ledger

    FlatFee->>CreditAlloc: createCreditAllocations(charge, allocations)
    CreditAlloc->>Adapter: CreateCreditAllocations(chargeID, allocations)
    Adapter-->>CreditAlloc: Realizations created
    
    CreditAlloc->>Lineage: CreateInitialLineages(namespace, realizations)
    Lineage-->>CreditAlloc: ✓
    
    CreditAlloc->>Lineage: PersistCorrectionLineageSegments(namespace, realizations)
    Lineage-->>CreditAlloc: ✓
    CreditAlloc-->>FlatFee: Return realizations
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

This PR introduces a substantial new subsystem for credit realization lineage with diverse changes across charges services, ledger adapters, transaction templates, and database schemas. The logic density is moderate-to-high in collector/correction paths, and the heterogeneity spans domain models, service wiring, template corrections, and migration scripts—requiring separate reasoning for each area.

Possibly related PRs

Suggested labels

release-note/feature, area/billing

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.70% 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 PR title 'feat(ledger): ubp, corrections and lineage tracking' clearly summarizes the main changes, covering the three core features introduced: usage-based platform ledger improvements, correction handling, and lineage tracking.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ledger-ubp

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

🧹 Nitpick comments (8)
openmeter/ledger/routingrules/routingrules.go (1)

380-385: Keep the first validation error when both directions fail.

Right now the helper always returns the second error if neither direction matches. That can hide the real failure mode — for example, a route-field mismatch in the intended direction gets rewritten into a known_cost_basis_required/unknown_cost_basis_required failure, which makes debugging pretty confusing.

♻️ Suggested tweak
 func requireKnownToUnknownCostBasisTranslationEitherDirection(leftEntries, rightEntries []EntryView, accountType ledger.AccountType, fields []RouteField) error {
-	if err := requireKnownToUnknownCostBasisTranslation(leftEntries, rightEntries, accountType, fields); err == nil {
-		return nil
-	}
-
-	return requireKnownToUnknownCostBasisTranslation(rightEntries, leftEntries, accountType, fields)
+	if err := requireKnownToUnknownCostBasisTranslation(leftEntries, rightEntries, accountType, fields); err != nil {
+		if reverseErr := requireKnownToUnknownCostBasisTranslation(rightEntries, leftEntries, accountType, fields); reverseErr == nil {
+			return nil
+		}
+		return err
+	}
+
+	return nil
 }

As per coding guidelines, "In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them."

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

In `@openmeter/ledger/routingrules/routingrules.go` around lines 380 - 385, The
helper requireKnownToUnknownCostBasisTranslationEitherDirection currently
discards the first error; change it to call
requireKnownToUnknownCostBasisTranslation for the left->right direction, store
that error (err1), and if err1 is nil return nil; otherwise call
requireKnownToUnknownCostBasisTranslation for right->left and if that returns
nil return nil, else return the first stored err1 so the original validation
failure is preserved; update the function body accordingly to keep the first
error when both directions fail.
openmeter/ledger/transactions/input.go (1)

74-100: Consider adding AsGroupInput override to prevent annotation loss via method promotion.

The wrapper doesn't implement AsGroupInput, so Go will promote the embedded method and call it on the wrapped input instead—dropping the annotations. While the codebase currently avoids this by using GroupInputs(...) helper instead, it's worth adding an override as defensive programming to guard against future calls like WithAnnotations(...).AsGroupInput(...).

💡 Suggested fix
 func (a *annotatedTransactionInput) Annotations() models.Annotations {
 	return a.annotations
 }
+
+func (a *annotatedTransactionInput) AsGroupInput(namespace string, annotations models.Annotations) ledger.TransactionGroupInput {
+	return &TransactionGroupInput{
+		namespace:    namespace,
+		transactions: []ledger.TransactionInput{a},
+		annotations:  annotations,
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/ledger/transactions/input.go` around lines 74 - 100, The wrapper
annotatedTransactionInput currently embeds ledger.TransactionInput but doesn't
override AsGroupInput, so calls to WithAnnotations(...).AsGroupInput() will be
forwarded to the embedded input and lose the annotations; implement an
AsGroupInput method on annotatedTransactionInput that calls the embedded
TransactionInput.AsGroupInput(), wraps each returned ledger.GroupInput (or its
inputs) with the same annotations (e.g., via existing WithAnnotations or
GroupInputs helper), and returns the annotated group input(s) to preserve
annotations when AsGroupInput is used.
tools/migrate/migrations/20260407102402_add_credit_realization_lineage.up.sql (1)

12-13: Redundant unique indexes on primary key columns.

The unique indexes creditrealizationlineage_id (line 13) and creditrealizationlineagesegment_id (line 33) are redundant since the PRIMARY KEY constraint already creates a unique index on the id columns. These extra indexes consume storage and add overhead to writes without providing any additional benefit.

🧹 Consider removing the redundant indexes
 -- create "credit_realization_lineages" table
 CREATE TABLE "credit_realization_lineages" (
   ...
   PRIMARY KEY ("id")
 );
--- create index "creditrealizationlineage_id" to table: "credit_realization_lineages"
-CREATE UNIQUE INDEX "creditrealizationlineage_id" ON "credit_realization_lineages" ("id");
 -- create index "creditrealizationlineage_namespace" to table: "credit_realization_lineages"
 -- create "credit_realization_lineage_segments" table
 CREATE TABLE "credit_realization_lineage_segments" (
   ...
   PRIMARY KEY ("id"),
   ...
 );
--- create index "creditrealizationlineagesegment_id" to table: "credit_realization_lineage_segments"
-CREATE UNIQUE INDEX "creditrealizationlineagesegment_id" ON "credit_realization_lineage_segments" ("id");
 -- create index "creditrealizationlineagesegment_lineage_id" to table: "credit_realization_lineage_segments"

Also applies to: 32-33

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

In
`@tools/migrate/migrations/20260407102402_add_credit_realization_lineage.up.sql`
around lines 12 - 13, Remove the redundant unique indexes that duplicate the
primary key uniqueness: delete the CREATE UNIQUE INDEX statements for
"creditrealizationlineage_id" on "credit_realization_lineages" ("id") and for
"creditrealizationlineagesegment_id" on "credit_realization_lineage_segments"
("id"); leave the primary key constraints intact and ensure no other code
depends on these specific index names before dropping them from the migration.
openmeter/ledger/collector/collect.go (1)

163-185: Consider logging when annotation derivation fails silently.

The fallback to original annotations when template name extraction or merge fails (lines 165-167, 180-182) is a reasonable defensive choice, but these silent fallbacks might make debugging harder if something goes wrong.

You might want to add debug-level logging here to help with future troubleshooting, though this is optional given the fallback behavior is safe.

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

In `@openmeter/ledger/collector/collect.go` around lines 163 - 185, In
creditRealizationAnnotationsForCollectedInput, add debug-level logging when
TransactionTemplateNameFromAnnotations(input.Annotations()) returns an error and
when input.Annotations().Merge(...) returns an error so failures aren't silent;
log the error value, the templateName failure context, and the input annotations
(or a short identifier) before returning the original annotations. Reference the
functions TransactionTemplateNameFromAnnotations, annotations.Merge, and
creditrealization.LineageAnnotations so the logs clearly indicate which step
failed.
openmeter/ledger/transactions/correction.go (1)

80-107: Comprehensive template dispatch.

The exhaustive switch covers all the transaction templates. The default case with a clear error for unknown templates is a nice fail-safe.

One thing to keep in mind: if new templates are added, they'll need to be registered here. Consider adding a comment near the template definitions to remind future contributors.

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

In `@openmeter/ledger/transactions/correction.go` around lines 80 - 107, The
switch in transactionTemplateByName enumerates all transaction template types
and requires manual updates when new templates are added; add a clear TODO
comment near the template type definitions (and/or above
transactionTemplateByName) stating that any new Template structs (e.g.,
IssueCustomerReceivableTemplate, FundCustomerReceivableTemplate,
ConvertCurrencyTemplate, etc.) must be registered in transactionTemplateByName
(or in a central registry if you later refactor) to prevent silent failures;
keep the comment concise and include an example and pointer to the function name
transactionTemplateByName for future contributors.
openmeter/billing/charges/lineage/service/service.go (1)

149-215: Consider clarifying the intentional leftover handling with a code comment.

The asymmetry with WritebackCorrectionLineageSegments is real: corrections error if they can't be fully applied, while backfills silently exit if there's leftover amount. This likely reflects different domain semantics (corrections are strict, backfills are best-effort coverage), but it's worth adding a brief inline comment explaining why leftover backfill amounts are acceptable—especially for financial operations where silent loss of amounts can raise eyebrows.

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

In `@openmeter/billing/charges/lineage/service/service.go` around lines 149 - 215,
Add a brief inline comment in BackfillAdvanceLineageSegments explaining why
leftover remaining amounts are intentionally ignored (best-effort backfill
semantics) and reference the asymmetry with WritebackCorrectionLineageSegments
so reviewers understand that corrections are strict while backfills may leave
unconsumed amounts; place the comment near the loop that iterates segments (for
_, segment := range segments) or right before the remaining check (if
!remaining.IsPositive()) to make the intent explicit.
test/credits/sanity_lifecycle_test.go (1)

386-388: Minor: defer clock.UnFreeze() inside a setup helper.

The defer clock.UnFreeze() on line 388 will execute when setupUsageBasedCreditOnlyLifecyclePartialBackfillCorrection returns, but the calling test continues to use the frozen clock. This works because the tests themselves also call clock.FreezeTime(), but it's a bit subtle.

Since TearDownTest already handles clock.UnFreeze(), you might consider removing this defer to avoid confusion. Not blocking though — the current behavior is correct.

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

In `@test/credits/sanity_lifecycle_test.go` around lines 386 - 388, Remove the
deferred UnFreeze call inside
setupUsageBasedCreditOnlyLifecyclePartialBackfillCorrection: the helper should
call clock.FreezeTime(createAt) but not defer clock.UnFreeze(), because
TearDownTest already handles clock.UnFreeze(); specifically remove the line that
defers clock.UnFreeze() so the frozen clock's lifecycle is managed only by the
global teardown and not by the setup helper.
openmeter/ledger/collector/correct.go (1)

352-389: Consider extracting the FBO entry detection logic.

The nested switch-case inside the loop (lines 377-384) is a bit dense. Since you're checking AccountType == CustomerFBO && Amount.IsNegative(), you might consider extracting this into a small helper or adding a brief comment clarifying what constitutes a "collected source" entry.

Not blocking, just a readability suggestion.

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

In `@openmeter/ledger/collector/correct.go` around lines 352 - 389, Extract the
FBO entry detection into a helper to improve readability: create a function
(e.g., isCollectedSourceEntry(entry ledger.Entry) bool) that encapsulates the
condition entry.PostingAddress().AccountType() == ledger.AccountTypeCustomerFBO
&& entry.Amount().IsNegative(), and replace the inline check inside
collectedSourcesForGroup with a call to that helper (or add a one-line
explanatory comment if you prefer). Update collectedSourcesForGroup to call
isCollectedSourceEntry for each entry before appending a collectedSource
(preserve use of collectedSource struct and advanceReceivableIssueTransaction
logic).
🤖 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/ledger/routingrules/routingrules_test.go`:
- Around line 38-80: Add two focused unit tests that assert same-account
translations are allowed: create
TestDefaultValidator_AllowsReceivableToReceivable_SameAccount and
TestDefaultValidator_AllowsAccruedToAccrued_SameAccount that use
routingrules.DefaultValidator.ValidateEntries with two EntryInput items created
via addressForRoute for ledger.AccountTypeCustomerReceivable (include
TransactionAuthorizationStatus open if required) and
ledger.AccountTypeCustomerAccrued respectively, mirror the pattern in
TestDefaultValidator_AllowsFBOToReceivableReverse/TestDefaultValidator_AllowsAccruedToFBO
(use +/-50 alpacadecimal amounts) and assert require.NoError on the validation
to cover the requireKnownToUnknownCostBasisTranslationEitherDirection regression
path.

---

Nitpick comments:
In `@openmeter/billing/charges/lineage/service/service.go`:
- Around line 149-215: Add a brief inline comment in
BackfillAdvanceLineageSegments explaining why leftover remaining amounts are
intentionally ignored (best-effort backfill semantics) and reference the
asymmetry with WritebackCorrectionLineageSegments so reviewers understand that
corrections are strict while backfills may leave unconsumed amounts; place the
comment near the loop that iterates segments (for _, segment := range segments)
or right before the remaining check (if !remaining.IsPositive()) to make the
intent explicit.

In `@openmeter/ledger/collector/collect.go`:
- Around line 163-185: In creditRealizationAnnotationsForCollectedInput, add
debug-level logging when
TransactionTemplateNameFromAnnotations(input.Annotations()) returns an error and
when input.Annotations().Merge(...) returns an error so failures aren't silent;
log the error value, the templateName failure context, and the input annotations
(or a short identifier) before returning the original annotations. Reference the
functions TransactionTemplateNameFromAnnotations, annotations.Merge, and
creditrealization.LineageAnnotations so the logs clearly indicate which step
failed.

In `@openmeter/ledger/collector/correct.go`:
- Around line 352-389: Extract the FBO entry detection into a helper to improve
readability: create a function (e.g., isCollectedSourceEntry(entry ledger.Entry)
bool) that encapsulates the condition entry.PostingAddress().AccountType() ==
ledger.AccountTypeCustomerFBO && entry.Amount().IsNegative(), and replace the
inline check inside collectedSourcesForGroup with a call to that helper (or add
a one-line explanatory comment if you prefer). Update collectedSourcesForGroup
to call isCollectedSourceEntry for each entry before appending a collectedSource
(preserve use of collectedSource struct and advanceReceivableIssueTransaction
logic).

In `@openmeter/ledger/routingrules/routingrules.go`:
- Around line 380-385: The helper
requireKnownToUnknownCostBasisTranslationEitherDirection currently discards the
first error; change it to call requireKnownToUnknownCostBasisTranslation for the
left->right direction, store that error (err1), and if err1 is nil return nil;
otherwise call requireKnownToUnknownCostBasisTranslation for right->left and if
that returns nil return nil, else return the first stored err1 so the original
validation failure is preserved; update the function body accordingly to keep
the first error when both directions fail.

In `@openmeter/ledger/transactions/correction.go`:
- Around line 80-107: The switch in transactionTemplateByName enumerates all
transaction template types and requires manual updates when new templates are
added; add a clear TODO comment near the template type definitions (and/or above
transactionTemplateByName) stating that any new Template structs (e.g.,
IssueCustomerReceivableTemplate, FundCustomerReceivableTemplate,
ConvertCurrencyTemplate, etc.) must be registered in transactionTemplateByName
(or in a central registry if you later refactor) to prevent silent failures;
keep the comment concise and include an example and pointer to the function name
transactionTemplateByName for future contributors.

In `@openmeter/ledger/transactions/input.go`:
- Around line 74-100: The wrapper annotatedTransactionInput currently embeds
ledger.TransactionInput but doesn't override AsGroupInput, so calls to
WithAnnotations(...).AsGroupInput() will be forwarded to the embedded input and
lose the annotations; implement an AsGroupInput method on
annotatedTransactionInput that calls the embedded
TransactionInput.AsGroupInput(), wraps each returned ledger.GroupInput (or its
inputs) with the same annotations (e.g., via existing WithAnnotations or
GroupInputs helper), and returns the annotated group input(s) to preserve
annotations when AsGroupInput is used.

In `@test/credits/sanity_lifecycle_test.go`:
- Around line 386-388: Remove the deferred UnFreeze call inside
setupUsageBasedCreditOnlyLifecyclePartialBackfillCorrection: the helper should
call clock.FreezeTime(createAt) but not defer clock.UnFreeze(), because
TearDownTest already handles clock.UnFreeze(); specifically remove the line that
defers clock.UnFreeze() so the frozen clock's lifecycle is managed only by the
global teardown and not by the setup helper.

In
`@tools/migrate/migrations/20260407102402_add_credit_realization_lineage.up.sql`:
- Around line 12-13: Remove the redundant unique indexes that duplicate the
primary key uniqueness: delete the CREATE UNIQUE INDEX statements for
"creditrealizationlineage_id" on "credit_realization_lineages" ("id") and for
"creditrealizationlineagesegment_id" on "credit_realization_lineage_segments"
("id"); leave the primary key constraints intact and ensure no other code
depends on these specific index names before dropping them from the migration.
🪄 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: 66112915-94a6-4687-8f28-ccf8e053b058

📥 Commits

Reviewing files that changed from the base of the PR and between 621e99a and 691df57.

⛔ Files ignored due to path filters (28)
  • openmeter/ent/db/client.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage/creditrealizationlineage.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment/creditrealizationlineagesegment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment_update.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 (67)
  • AGENTS.md
  • app/common/charges.go
  • app/common/customerbalance.go
  • openmeter/billing/charges/creditpurchase/service/external.go
  • openmeter/billing/charges/creditpurchase/service/invoice.go
  • openmeter/billing/charges/creditpurchase/service/service.go
  • openmeter/billing/charges/flatfee/adapter/charge.go
  • openmeter/billing/charges/flatfee/adapter/credits.go
  • openmeter/billing/charges/flatfee/service/creditsonly.go
  • openmeter/billing/charges/flatfee/service/invoice.go
  • openmeter/billing/charges/flatfee/service/lineage.go
  • openmeter/billing/charges/flatfee/service/service.go
  • openmeter/billing/charges/lineage/adapter/adapter.go
  • openmeter/billing/charges/lineage/adapter/lineage.go
  • openmeter/billing/charges/lineage/lineage.go
  • openmeter/billing/charges/lineage/service.go
  • openmeter/billing/charges/lineage/service/service.go
  • openmeter/billing/charges/models/creditrealization/lineage.go
  • openmeter/billing/charges/models/creditrealization/lineage_segment.go
  • openmeter/billing/charges/models/creditrealization/lineage_specs.go
  • openmeter/billing/charges/models/creditrealization/models.go
  • openmeter/billing/charges/service/base_test.go
  • openmeter/billing/charges/service/lineage_test.go
  • openmeter/billing/charges/testutils/service.go
  • openmeter/billing/charges/usagebased/adapter/charge.go
  • openmeter/billing/charges/usagebased/adapter/creditallocation.go
  • openmeter/billing/charges/usagebased/service/creditsonly.go
  • openmeter/billing/charges/usagebased/service/lineage.go
  • openmeter/billing/charges/usagebased/service/service.go
  • openmeter/ent/schema/creditrealizationlineage.go
  • openmeter/ledger/annotations.go
  • openmeter/ledger/chargeadapter/creditpurchase.go
  • openmeter/ledger/chargeadapter/creditpurchase_test.go
  • openmeter/ledger/chargeadapter/flatfee.go
  • openmeter/ledger/chargeadapter/flatfee_test.go
  • openmeter/ledger/chargeadapter/helpers.go
  • openmeter/ledger/chargeadapter/usagebased.go
  • openmeter/ledger/chargeadapter/usagebased_test.go
  • openmeter/ledger/collector/collect.go
  • openmeter/ledger/collector/correct.go
  • openmeter/ledger/collector/service.go
  • openmeter/ledger/customerbalance/testenv_test.go
  • openmeter/ledger/historical/adapter/ledger.go
  • openmeter/ledger/historical/ledger.go
  • openmeter/ledger/historical/repo.go
  • openmeter/ledger/historical/transaction.go
  • openmeter/ledger/primitives.go
  • openmeter/ledger/routingrules/routingrules.go
  • openmeter/ledger/routingrules/routingrules_test.go
  • openmeter/ledger/transactions/accrual.go
  • openmeter/ledger/transactions/accrual_test.go
  • openmeter/ledger/transactions/correction.go
  • openmeter/ledger/transactions/correction_test.go
  • openmeter/ledger/transactions/customer.go
  • openmeter/ledger/transactions/earnings.go
  • openmeter/ledger/transactions/earnings_test.go
  • openmeter/ledger/transactions/fx.go
  • openmeter/ledger/transactions/input.go
  • openmeter/ledger/transactions/resolve.go
  • openmeter/ledger/transactions/resolve_test.go
  • openmeter/ledger/transactions/template.go
  • openmeter/ledger/transactions/testenv_test.go
  • openmeter/ledger/transactions/testutils/anytransaction.go
  • test/credits/sanity_lifecycle_test.go
  • test/credits/sanity_test.go
  • tools/migrate/migrations/20260407102402_add_credit_realization_lineage.down.sql
  • tools/migrate/migrations/20260407102402_add_credit_realization_lineage.up.sql

Comment on lines +38 to +80
func TestDefaultValidator_AllowsAccruedToFBO(t *testing.T) {
validator := routingrules.DefaultValidator

err := validator.ValidateEntries([]ledger.EntryInput{
&transactionstestutils.AnyEntryInput{
Address: addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-accrued", ledger.Route{
Currency: currencyx.Code("USD"),
}),
AmountValue: alpacadecimal.NewFromInt(-50),
},
&transactionstestutils.AnyEntryInput{
Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo", ledger.Route{
Currency: currencyx.Code("USD"),
}),
AmountValue: alpacadecimal.NewFromInt(50),
},
})

require.NoError(t, err)
}

func TestDefaultValidator_AllowsFBOToReceivableReverse(t *testing.T) {
validator := routingrules.DefaultValidator
openStatus := ledger.TransactionAuthorizationStatusOpen

err := validator.ValidateEntries([]ledger.EntryInput{
&transactionstestutils.AnyEntryInput{
Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo", ledger.Route{
Currency: currencyx.Code("USD"),
}),
AmountValue: alpacadecimal.NewFromInt(-50),
},
&transactionstestutils.AnyEntryInput{
Address: addressForRoute(t, ledger.AccountTypeCustomerReceivable, "sub-rec-open", ledger.Route{
Currency: currencyx.Code("USD"),
TransactionAuthorizationStatus: &openStatus,
}),
AmountValue: alpacadecimal.NewFromInt(50),
},
})

require.NoError(t, err)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Please add a direct same-account regression for the new cost-basis translation paths.

These cases are nice coverage for the reversed cross-account flows, but the code change also relaxed CustomerReceivable -> CustomerReceivable and CustomerAccrued -> CustomerAccrued validation through requireKnownToUnknownCostBasisTranslationEitherDirection. A focused test for those same-account transitions would make this much safer, because the new helper could regress while these tests still stay green.

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 `@openmeter/ledger/routingrules/routingrules_test.go` around lines 38 - 80, Add
two focused unit tests that assert same-account translations are allowed: create
TestDefaultValidator_AllowsReceivableToReceivable_SameAccount and
TestDefaultValidator_AllowsAccruedToAccrued_SameAccount that use
routingrules.DefaultValidator.ValidateEntries with two EntryInput items created
via addressForRoute for ledger.AccountTypeCustomerReceivable (include
TransactionAuthorizationStatus open if required) and
ledger.AccountTypeCustomerAccrued respectively, mirror the pattern in
TestDefaultValidator_AllowsFBOToReceivableReverse/TestDefaultValidator_AllowsAccruedToFBO
(use +/-50 alpacadecimal amounts) and assert require.NoError on the validation
to cover the requireKnownToUnknownCostBasisTranslationEitherDirection regression
path.

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: 3

Caution

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

⚠️ Outside diff range comments (1)
app/common/customerbalance.go (1)

89-117: ⚠️ Potential issue | 🔴 Critical

Wire the new lineage service here too.

This path now builds flatfeeservice and usagebasedservice without Lineage, but both constructors validate that dependency. With credits enabled, NewCustomerBalanceService will now fail fast with the new “lineage service cannot be null” error instead of starting up.

🛠️ Minimal fix sketch
+	lineageAdapter, err := lineageadapter.New(lineageadapter.Config{
+		Client: db,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	lineageService, err := lineageservice.New(lineageservice.Config{
+		Adapter: lineageAdapter,
+	})
+	if err != nil {
+		return nil, err
+	}
+
 	flatFeeService, err := flatfeeservice.New(flatfeeservice.Config{
 		Adapter:     flatFeeAdapter,
 		Handler:     ledgerchargeadapter.NewFlatFeeHandler(historicalLedger, transactions.ResolverDependencies{AccountService: accountResolver, SubAccountService: accountService}, collectorService),
+		Lineage:     lineageService,
 		MetaAdapter: metaAdapter,
 		Locker:      locker,
 	})
@@
 	usageService, err := usagebasedservice.New(usagebasedservice.Config{
 		Adapter:                 usageAdapter,
 		Handler:                 ledgerchargeadapter.NewUsageBasedHandler(collectorService),
+		Lineage:                 lineageService,
 		Locker:                  locker,
 		MetaAdapter:             metaAdapter,
 		CustomerOverrideService: billingRegistry.Billing,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/common/customerbalance.go` around lines 89 - 117, The flatfeeservice and
usagebasedservice are being constructed without the required Lineage dependency,
causing startup failures when the constructors validate it; obtain the existing
lineage service instance used elsewhere (e.g., lineageService or
billingRegistry.Lineage) and pass it into both flatfeeservice.New(...) and
usagebasedservice.New(...) by adding Lineage: lineageService (or the correct
identifier) to their Config structs so the constructors receive a non-nil
Lineage.
🧹 Nitpick comments (1)
openmeter/billing/charges/creditpurchase/service/invoice.go (1)

18-45: Consider pulling this initiation flow into one shared helper.

This block now matches onExternalCreditPurchase pretty closely: begin tx, call OnCreditPurchaseInitiated, backfill lineage, mark active, persist. Keeping that in one place would make future changes to ordering or lineage rules much less likely to drift.

As per coding guidelines: **/*.go: In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them.

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

In `@openmeter/billing/charges/creditpurchase/service/invoice.go` around lines 18
- 45, Extract the duplicated initiation flow into a single helper (e.g.,
processCreditPurchaseInitiation or handleCreditPurchaseInitiation) that
encapsulates the transaction.RunWithNoValue call and the sequence: call
s.handler.OnCreditPurchaseInitiated, set charge.State.CreditGrantRealization
with ledgertransaction.TimedGroupReference, conditionally call
s.lineage.BackfillAdvanceLineageSegments when
ledgerTransactionGroupReference.TransactionGroupID != "", set charge.Status =
meta.ChargeStatusActive, and persist via s.adapter.UpdateCharge; then replace
the matching blocks in invoice.go and onExternalCreditPurchase to call this
helper so ordering and lineage rules remain centralized and behavior is
unchanged.
🤖 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/lineage/adapter/lineage.go`:
- Around line 189-203: The CreateSegment write path (adapter.CreateSegment
handling lineage.CreateSegmentInput) must validate input invariants before
calling Save(): check that Amount is positive (>0) and that state-specific
requirements are met (e.g., if input.State == "advance_backfilled" then
input.BackingTransactionGroupID must be non-nil), returning a descriptive error
when validation fails; perform these checks at the start of CreateSegment
(before building create := tx.db.CreditRealizationLineageSegment.Create()) so
invalid combinations are rejected early and Save() is only called for valid
inputs.
- Around line 60-89: The CreateLineages method currently uses
TransactingRepoWithNoValue which only rebinds to an existing tx; ensure this
operation runs in a real DB transaction to avoid partial commits by either (a)
calling GetDriverFromContext(ctx) and opening a transaction if none exists (as
the lock methods do) or (b) explicitly starting a new transaction inside
CreateLineages, running the bulk CreateBulk/...Save calls within that tx and
rolling back on error; update CreateLineages to detect/obtain a driver/tx (via
GetDriverFromContext) and use it to create a proper transactional scope so both
CreditRealizationLineage and CreditRealizationLineageSegment creations succeed
or the whole operation is rolled back.

In `@openmeter/ent/schema/creditrealizationlineage.go`:
- Around line 36-40: The schema allows root_realization_id to be an empty
string; update the field declaration for root_realization_id in the
CreditRealizationLineage schema by adding a NotEmpty() constraint to the field
chain (alongside SchemaType(...).Immutable()) so the schema layer enforces
non-empty values like customer_id and lineage_id do; ensure the NotEmpty() is
applied on the field.String("root_realization_id") chain so DB contract matches
the domain model.

---

Outside diff comments:
In `@app/common/customerbalance.go`:
- Around line 89-117: The flatfeeservice and usagebasedservice are being
constructed without the required Lineage dependency, causing startup failures
when the constructors validate it; obtain the existing lineage service instance
used elsewhere (e.g., lineageService or billingRegistry.Lineage) and pass it
into both flatfeeservice.New(...) and usagebasedservice.New(...) by adding
Lineage: lineageService (or the correct identifier) to their Config structs so
the constructors receive a non-nil Lineage.

---

Nitpick comments:
In `@openmeter/billing/charges/creditpurchase/service/invoice.go`:
- Around line 18-45: Extract the duplicated initiation flow into a single helper
(e.g., processCreditPurchaseInitiation or handleCreditPurchaseInitiation) that
encapsulates the transaction.RunWithNoValue call and the sequence: call
s.handler.OnCreditPurchaseInitiated, set charge.State.CreditGrantRealization
with ledgertransaction.TimedGroupReference, conditionally call
s.lineage.BackfillAdvanceLineageSegments when
ledgerTransactionGroupReference.TransactionGroupID != "", set charge.Status =
meta.ChargeStatusActive, and persist via s.adapter.UpdateCharge; then replace
the matching blocks in invoice.go and onExternalCreditPurchase to call this
helper so ordering and lineage rules remain centralized and behavior is
unchanged.
🪄 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: 2bf42029-bed9-4036-8e62-bfecb3a95f61

📥 Commits

Reviewing files that changed from the base of the PR and between 691df57 and 924447d.

⛔ Files ignored due to path filters (27)
  • openmeter/ent/db/client.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage/creditrealizationlineage.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment/creditrealizationlineagesegment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineagesegment_update.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/**
📒 Files selected for processing (47)
  • AGENTS.md
  • app/common/charges.go
  • app/common/customerbalance.go
  • openmeter/billing/charges/creditpurchase/service/external.go
  • openmeter/billing/charges/creditpurchase/service/invoice.go
  • openmeter/billing/charges/creditpurchase/service/service.go
  • openmeter/billing/charges/flatfee/adapter/charge.go
  • openmeter/billing/charges/flatfee/adapter/credits.go
  • openmeter/billing/charges/flatfee/service/creditsonly.go
  • openmeter/billing/charges/flatfee/service/invoice.go
  • openmeter/billing/charges/flatfee/service/lineage.go
  • openmeter/billing/charges/flatfee/service/service.go
  • openmeter/billing/charges/lineage/adapter/adapter.go
  • openmeter/billing/charges/lineage/adapter/lineage.go
  • openmeter/billing/charges/lineage/lineage.go
  • openmeter/billing/charges/lineage/service.go
  • openmeter/billing/charges/lineage/service/service.go
  • openmeter/billing/charges/models/creditrealization/lineage.go
  • openmeter/billing/charges/models/creditrealization/lineage_segment.go
  • openmeter/billing/charges/models/creditrealization/lineage_specs.go
  • openmeter/billing/charges/models/creditrealization/models.go
  • openmeter/billing/charges/service/base_test.go
  • openmeter/billing/charges/service/lineage_test.go
  • openmeter/billing/charges/testutils/service.go
  • openmeter/billing/charges/usagebased/adapter/charge.go
  • openmeter/billing/charges/usagebased/adapter/creditallocation.go
  • openmeter/billing/charges/usagebased/service/creditsonly.go
  • openmeter/billing/charges/usagebased/service/lineage.go
  • openmeter/billing/charges/usagebased/service/service.go
  • openmeter/ent/schema/creditrealizationlineage.go
  • openmeter/ledger/annotations.go
  • openmeter/ledger/chargeadapter/creditpurchase.go
  • openmeter/ledger/chargeadapter/creditpurchase_test.go
  • openmeter/ledger/chargeadapter/flatfee.go
  • openmeter/ledger/chargeadapter/flatfee_test.go
  • openmeter/ledger/chargeadapter/helpers.go
  • openmeter/ledger/chargeadapter/usagebased.go
  • openmeter/ledger/chargeadapter/usagebased_test.go
  • openmeter/ledger/collector/collect.go
  • openmeter/ledger/collector/correct.go
  • openmeter/ledger/collector/service.go
  • openmeter/ledger/customerbalance/testenv_test.go
  • openmeter/ledger/historical/adapter/ledger.go
  • openmeter/ledger/historical/ledger.go
  • openmeter/ledger/historical/repo.go
  • openmeter/ledger/historical/transaction.go
  • openmeter/ledger/noop/noop.go
✅ Files skipped from review due to trivial changes (8)
  • openmeter/billing/charges/usagebased/adapter/creditallocation.go
  • openmeter/billing/charges/flatfee/adapter/credits.go
  • openmeter/billing/charges/flatfee/service/creditsonly.go
  • openmeter/billing/charges/usagebased/service/service.go
  • openmeter/ledger/chargeadapter/helpers.go
  • openmeter/billing/charges/lineage/service/service.go
  • openmeter/ledger/collector/correct.go
  • openmeter/ledger/collector/service.go
🚧 Files skipped from review as they are similar to previous changes (17)
  • openmeter/billing/charges/flatfee/service/invoice.go
  • openmeter/ledger/chargeadapter/creditpurchase.go
  • openmeter/billing/charges/creditpurchase/service/service.go
  • openmeter/ledger/chargeadapter/creditpurchase_test.go
  • openmeter/billing/charges/flatfee/service/service.go
  • openmeter/billing/charges/flatfee/adapter/charge.go
  • openmeter/billing/charges/usagebased/service/lineage.go
  • openmeter/billing/charges/models/creditrealization/models.go
  • openmeter/billing/charges/models/creditrealization/lineage_specs.go
  • openmeter/billing/charges/lineage/service.go
  • openmeter/billing/charges/usagebased/service/creditsonly.go
  • openmeter/billing/charges/flatfee/service/lineage.go
  • openmeter/billing/charges/lineage/lineage.go
  • openmeter/billing/charges/service/lineage_test.go
  • openmeter/ledger/annotations.go
  • app/common/charges.go
  • openmeter/billing/charges/models/creditrealization/lineage.go

Comment on lines +60 to +89
func (a *adapter) CreateLineages(ctx context.Context, input lineage.CreateLineagesInput) error {
return entutils.TransactingRepoWithNoValue(ctx, a, func(ctx context.Context, tx *adapter) error {
rootCreates := make([]*entdb.CreditRealizationLineageCreate, 0, len(input.Specs))
segmentCreates := make([]*entdb.CreditRealizationLineageSegmentCreate, 0, len(input.Specs))

for _, spec := range input.Specs {
rootCreates = append(rootCreates, tx.db.CreditRealizationLineage.Create().
SetID(spec.LineageID).
SetNamespace(input.Namespace).
SetRootRealizationID(spec.RootRealizationID).
SetCustomerID(input.CustomerID).
SetCurrency(input.Currency).
SetOriginKind(spec.OriginKind),
)
segmentCreates = append(segmentCreates, tx.db.CreditRealizationLineageSegment.Create().
SetLineageID(spec.LineageID).
SetAmount(spec.Amount).
SetState(spec.InitialState),
)
}

if _, err := tx.db.CreditRealizationLineage.CreateBulk(rootCreates...).Save(ctx); err != nil {
return fmt.Errorf("create credit realization lineages: %w", err)
}
if _, err := tx.db.CreditRealizationLineageSegment.CreateBulk(segmentCreates...).Save(ctx); err != nil {
return fmt.Errorf("create initial credit realization lineage segments: %w", err)
}

return nil
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Require a real transaction for the two-step lineage create.

TransactingRepoWithNoValue(...) only rebinds to a tx already present in ctx; it doesn’t open one. So if this method is called without a surrounding transaction, the root bulk insert can succeed and the segment bulk insert can still fail, leaving orphaned lineage rows behind. I’d either enforce GetDriverFromContext(ctx) here like the lock methods do, or make this method start its own transaction.

One simple guard
 func (a *adapter) CreateLineages(ctx context.Context, input lineage.CreateLineagesInput) error {
 	return entutils.TransactingRepoWithNoValue(ctx, a, func(ctx context.Context, tx *adapter) error {
+		if _, err := entutils.GetDriverFromContext(ctx); err != nil {
+			return fmt.Errorf("create lineages must be called in a transaction: %w", err)
+		}
+
 		rootCreates := make([]*entdb.CreditRealizationLineageCreate, 0, len(input.Specs))
 		segmentCreates := make([]*entdb.CreditRealizationLineageSegmentCreate, 0, len(input.Specs))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/lineage/adapter/lineage.go` around lines 60 - 89,
The CreateLineages method currently uses TransactingRepoWithNoValue which only
rebinds to an existing tx; ensure this operation runs in a real DB transaction
to avoid partial commits by either (a) calling GetDriverFromContext(ctx) and
opening a transaction if none exists (as the lock methods do) or (b) explicitly
starting a new transaction inside CreateLineages, running the bulk
CreateBulk/...Save calls within that tx and rolling back on error; update
CreateLineages to detect/obtain a driver/tx (via GetDriverFromContext) and use
it to create a proper transactional scope so both CreditRealizationLineage and
CreditRealizationLineageSegment creations succeed or the whole operation is
rolled back.

Comment thread openmeter/billing/charges/lineage/adapter/lineage.go
Comment thread openmeter/ent/schema/creditrealizationlineage.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

🧹 Nitpick comments (2)
openmeter/ent/schema/creditrealizationlineage.go (1)

70-74: Add a currency-aware customer lookup index.

LockAdvanceLineagesForBackfill now filters by namespace, customer_id, and currency before it touches segments. With only (namespace, customer_id) indexed, that path still has to scan all of a customer's lineages across currencies. I'd add a (namespace, customer_id, currency) index here, and keep the shorter one only if you still need that lookup shape elsewhere.

Possible tweak
 func (CreditRealizationLineage) Indexes() []ent.Index {
 	return []ent.Index{
 		index.Fields("namespace", "root_realization_id").Unique(),
 		index.Fields("namespace", "customer_id"),
+		index.Fields("namespace", "customer_id", "currency"),
 	}
 }

As per coding guidelines, "Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations (regardless of database) 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/ent/schema/creditrealizationlineage.go` around lines 70 - 74, The
Indexes() method on CreditRealizationLineage currently only defines (namespace,
customer_id) and (namespace, root_realization_id) index shapes; add a new
index.Fields("namespace", "customer_id", "currency") to the returned slice so
lookups in LockAdvanceLineagesForBackfill that filter by namespace, customer_id
and currency are covered and avoid full-customer scans; keep or remove the
existing (namespace, customer_id) index only if other code paths still rely on
that two-column shape.
openmeter/billing/charges/lineage/adapter/lineage.go (1)

60-89: Reuse the segment invariant check on the bulk-create path.

This is the only insert path that writes CreditRealizationLineageSegment rows without going through CreateSegmentInput.Validate(). Reusing the same validation here would keep initial lineage inserts aligned with later segment writes and stop a bad InitialLineageSpec from seeding invalid state.

One lightweight way to keep the rules aligned
 	for _, spec := range input.Specs {
+		if err := (lineage.CreateSegmentInput{
+			LineageID: spec.LineageID,
+			Amount:    spec.Amount,
+			State:     spec.InitialState,
+		}).Validate(); err != nil {
+			return fmt.Errorf("validate initial lineage segment %s: %w", spec.LineageID, err)
+		}
+
 		rootCreates = append(rootCreates, tx.db.CreditRealizationLineage.Create().
 			SetID(spec.LineageID).
 			SetNamespace(input.Namespace).
 			SetRootRealizationID(spec.RootRealizationID).
 			SetCustomerID(input.CustomerID).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/lineage/adapter/lineage.go` around lines 60 - 89,
The CreateLineages function currently bulk-inserts
CreditRealizationLineageSegment rows without running the same invariants as
CreateSegmentInput.Validate; update CreateLineages to validate each initial
segment (e.g., by calling the existing CreateSegmentInput.Validate or the shared
segment invariant helper) for every spec/InitialLineageSpec before building
segmentCreates and calling
tx.db.CreditRealizationLineageSegment.CreateBulk(...). Ensure you reference the
same validation routine used by segment creation (the Validate method or helper)
and return a descriptive error if validation fails so initial lineage inserts
cannot seed invalid segment state.
🤖 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/lineage/service.go`:
- Around line 64-81: The current
WritebackCorrectionLineageSegmentsInput.Validate() only checks
CorrectsRealizationID for correction-type realizations and misses full payload
validation (e.g., malformed amounts); update the method to invoke
i.Realizations.Validate() early (and return or append its error(s)) so the
writeback validation is as strict as CreateInitialLineagesInput; ensure you call
Realizations.Validate() inside
WritebackCorrectionLineageSegmentsInput.Validate() and incorporate its returned
error(s) into the final errors.Join result.

---

Nitpick comments:
In `@openmeter/billing/charges/lineage/adapter/lineage.go`:
- Around line 60-89: The CreateLineages function currently bulk-inserts
CreditRealizationLineageSegment rows without running the same invariants as
CreateSegmentInput.Validate; update CreateLineages to validate each initial
segment (e.g., by calling the existing CreateSegmentInput.Validate or the shared
segment invariant helper) for every spec/InitialLineageSpec before building
segmentCreates and calling
tx.db.CreditRealizationLineageSegment.CreateBulk(...). Ensure you reference the
same validation routine used by segment creation (the Validate method or helper)
and return a descriptive error if validation fails so initial lineage inserts
cannot seed invalid segment state.

In `@openmeter/ent/schema/creditrealizationlineage.go`:
- Around line 70-74: The Indexes() method on CreditRealizationLineage currently
only defines (namespace, customer_id) and (namespace, root_realization_id) index
shapes; add a new index.Fields("namespace", "customer_id", "currency") to the
returned slice so lookups in LockAdvanceLineagesForBackfill that filter by
namespace, customer_id and currency are covered and avoid full-customer scans;
keep or remove the existing (namespace, customer_id) index only if other code
paths still rely on that two-column shape.
🪄 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: 6a16dcec-b65b-4c56-9e5b-50ae644945c8

📥 Commits

Reviewing files that changed from the base of the PR and between 924447d and 1bfb5a8.

⛔ Files ignored due to path filters (3)
  • openmeter/ent/db/creditrealizationlineage/creditrealizationlineage.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/runtime.go is excluded by !**/ent/db/**
📒 Files selected for processing (4)
  • openmeter/billing/charges/lineage/adapter/lineage.go
  • openmeter/billing/charges/lineage/service.go
  • openmeter/billing/charges/service/lineage_test.go
  • openmeter/ent/schema/creditrealizationlineage.go
✅ Files skipped from review due to trivial changes (1)
  • openmeter/billing/charges/service/lineage_test.go

Comment on lines +64 to +81
func (i WritebackCorrectionLineageSegmentsInput) Validate() error {
var errs []error

if i.Namespace == "" {
errs = append(errs, errors.New("namespace is required"))
}

for idx, realization := range i.Realizations {
if realization.Type != creditrealization.TypeCorrection {
continue
}

if realization.CorrectsRealizationID == nil || *realization.CorrectsRealizationID == "" {
errs = append(errs, fmt.Errorf("realizations[%d]: corrects realization id is required for corrections", idx))
}
}

return errors.Join(errs...)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate the realization payload before writeback.

This method still derives writeback amounts from realization.Amount.Abs(), so checking only CorrectsRealizationID leaves a malformed correction slice able to consume coverage. Calling i.Realizations.Validate() here would make the writeback contract as strict as CreateInitialLineagesInput.

Small fix
 func (i WritebackCorrectionLineageSegmentsInput) Validate() error {
 	var errs []error

 	if i.Namespace == "" {
 		errs = append(errs, errors.New("namespace is required"))
 	}
+	if err := i.Realizations.Validate(); err != nil {
+		errs = append(errs, fmt.Errorf("realizations: %w", err))
+	}

 	for idx, realization := range i.Realizations {
 		if realization.Type != creditrealization.TypeCorrection {
 			continue
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (i WritebackCorrectionLineageSegmentsInput) Validate() error {
var errs []error
if i.Namespace == "" {
errs = append(errs, errors.New("namespace is required"))
}
for idx, realization := range i.Realizations {
if realization.Type != creditrealization.TypeCorrection {
continue
}
if realization.CorrectsRealizationID == nil || *realization.CorrectsRealizationID == "" {
errs = append(errs, fmt.Errorf("realizations[%d]: corrects realization id is required for corrections", idx))
}
}
return errors.Join(errs...)
func (i WritebackCorrectionLineageSegmentsInput) Validate() error {
var errs []error
if i.Namespace == "" {
errs = append(errs, errors.New("namespace is required"))
}
if err := i.Realizations.Validate(); err != nil {
errs = append(errs, fmt.Errorf("realizations: %w", err))
}
for idx, realization := range i.Realizations {
if realization.Type != creditrealization.TypeCorrection {
continue
}
if realization.CorrectsRealizationID == nil || *realization.CorrectsRealizationID == "" {
errs = append(errs, fmt.Errorf("realizations[%d]: corrects realization id is required for corrections", idx))
}
}
return errors.Join(errs...)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/lineage/service.go` around lines 64 - 81, The
current WritebackCorrectionLineageSegmentsInput.Validate() only checks
CorrectsRealizationID for correction-type realizations and misses full payload
validation (e.g., malformed amounts); update the method to invoke
i.Realizations.Validate() early (and return or append its error(s)) so the
writeback validation is as strict as CreateInitialLineagesInput; ensure you call
Realizations.Validate() inside
WritebackCorrectionLineageSegmentsInput.Validate() and incorporate its returned
error(s) into the final errors.Join result.

Comment thread app/common/charges.go
}

flatFeeHandler := NewChargesFlatFeeHandler(ledgerService, accountResolver, accountService)
collectorService := NewChargesCollectorService(ledgerService, accountResolver, accountService)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NewChargesFlatFeeService, NewChargesUsageBasedService, and NewChargesCreditPurchaseService all require a lineage.Service in their config now (Config.Validate() returns "lineage service cannot be null"), but none of them receive one here. The server will crash on startup.

Fix: create a lineage adapter + service here (same pattern as testutils/service.go:114-126) and pass it to all three service constructors. Same issue in customerbalance.go where the flat-fee and usage-based services are also constructed without lineage.


func (CreditRealizationLineage) Fields() []ent.Field {
return []ent.Field{
field.String("root_realization_id").
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would add at least a charge_id here and an FK to the charges table. It's not required for the code but will make our life easier when trying to find realizations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

FK is fair, adding it, but if you dont mind i wouldn't necessarily add the extra fields you can always just write simple sql to solve that

Comment on lines +18 to +34
if err := s.lineage.CreateInitialLineages(ctx, lineage.CreateInitialLineagesInput{
Namespace: charge.Namespace,
CustomerID: charge.Intent.CustomerID,
Currency: charge.Intent.Currency,
Realizations: realizations,
}); err != nil {
return creditrealization.Realizations{}, fmt.Errorf("create initial credit realization lineages: %w", err)
}

if err := s.lineage.WritebackCorrectionLineageSegments(ctx, lineage.WritebackCorrectionLineageSegmentsInput{
Namespace: charge.Namespace,
Realizations: realizations,
}); err != nil {
return creditrealization.Realizations{}, fmt.Errorf("write back correction lineage segments: %w", err)
}

lineage.AttachInitialActiveLineageSegments(realizations)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's compact this into a single call (e.g. Create can receive everything required to be created).


type Service interface {
CreateInitialLineages(ctx context.Context, input CreateInitialLineagesInput) error
WritebackCorrectionLineageSegments(ctx context.Context, input WritebackCorrectionLineageSegmentsInput) error
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Writeback is a strange word here. Update, Persist, etc might be better

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see your point, it felt very normal to me but i'm biased :D Persist works for me

type Service interface {
CreateInitialLineages(ctx context.Context, input CreateInitialLineagesInput) error
WritebackCorrectionLineageSegments(ctx context.Context, input WritebackCorrectionLineageSegmentsInput) error
BackfillAdvanceLineageSegments(ctx context.Context, input BackfillAdvanceLineageSegmentsInput) error
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is not really a backfill, but rather the result of cost basis being known. May RecognizeAdvanceLineageSegements would be a better name.

Honestly for me Backfill doesn't really capture what this does.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

"Backfilling the Advance" is the phrase i've been using throughout. You're technically right in the sense that it does more than just backfilling the advanced receivable (which it does do) but also "attributes cost-basis" on the accrued accounts

If you feel strongly about this lets talk but then we should rename this things throughout the code

Copy link
Copy Markdown
Member

@turip turip Apr 9, 2026

Choose a reason for hiding this comment

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

Then at least add a docstring on what it does, as right now I need to take a look at the implementation to see what's happening there.

The issue is that right now you are in context, but zooming out backfill feels like a db migration for me. It's just an overloaded word when not being laser focused on the ledger.

Comment on lines +15 to +16
LineageOriginKindRealCredit LineageOriginKind = "real_credit"
LineageOriginKindAdvance LineageOriginKind = "advance"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add description to these constants for the benefit of future readers.

Also maybe, as that's the thing we are capturing here:

Suggested change
LineageOriginKindRealCredit LineageOriginKind = "real_credit"
LineageOriginKindAdvance LineageOriginKind = "advance"
LineageOriginKindRealized LineageOriginKind = "realized"
LineageOriginKindAdvance LineageOriginKind = "advance"


const AnnotationLineageOriginKind = "billing.credit_realization.lineage_origin_kind"

type LineageOriginKind string
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Question: does it make sense to store this? I would think that this is not needed as the segments capture this information already.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

wdym? the segments don't rly capture this, once this is know they're written back after the fact

technically we could get this if we fetch the realization + some but its just easier to localize this (it basically captures the tx typename for the original usage acknowledgement)

Comment on lines +57 to +59
LineageSegmentStateRealCredit LineageSegmentState = "real_credit"
LineageSegmentStateAdvanceUncovered LineageSegmentState = "advance_uncovered"
LineageSegmentStateAdvanceBackfilled LineageSegmentState = "advance_backfilled"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add docstrings for the benefit of the next one reading this part :D

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

🧹 Nitpick comments (2)
openmeter/billing/charges/usagebased/service/creditsonly.go (1)

129-136: Batch the lineage fetch before the run loop.

LoadActiveSegmentsByRealizationID already takes a slice, but this version does one DB call per realization run. On charges with a lot of runs, delete/correction turns into an N+1 query pattern for no real gain. I'd load all realization IDs once, then reuse the returned map per run in memory. As per coding guidelines, performance should be a priority in critical code paths, including database operations.

🤖 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` around lines 129
- 136, The code currently calls
s.Service.lineage.LoadActiveSegmentsByRealizationID inside the loop over
s.Charge.Realizations, causing an N+1 DB query pattern; instead, collect all
realization IDs across s.Charge.Realizations (map/concat the IDs from each
run.CreditsAllocated), call s.Service.lineage.LoadActiveSegmentsByRealizationID
once to get lineageSegmentsByRealization, then inside the loop reuse that
returned map for each run (lookup by realization ID) rather than making per-run
DB calls; update error handling to reflect the single batched call and remove
the per-run LoadActiveSegmentsByRealizationID invocation.
openmeter/billing/charges/models/creditrealization/lineage.go (1)

12-17: Consider adding descriptions to LineageOriginKind constants.

The LineageSegmentState constants have nice docstrings (lines 57-65) explaining what each state represents. It would be helpful to add similar descriptions to LineageOriginKindRealCredit and LineageOriginKindAdvance for consistency and to help future readers understand what each origin represents.

📝 Suggested docstrings
 const (
+	// LineageOriginKindRealCredit marks a realization that originated from
+	// actual credit balance (FBO subaccount consumption).
 	LineageOriginKindRealCredit LineageOriginKind = "real_credit"
+	// LineageOriginKindAdvance marks a realization that originated from
+	// an advance (receivable-backed usage before credit purchase).
 	LineageOriginKindAdvance    LineageOriginKind = "advance"
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/models/creditrealization/lineage.go` around lines
12 - 17, Add concise doc comments for the LineageOriginKind constants so readers
know what each origin represents: add comments above LineageOriginKindRealCredit
and LineageOriginKindAdvance (referencing the LineageOriginKind type and the
constants LineageOriginKindRealCredit and LineageOriginKindAdvance) describing
their meanings (e.g., that real_credit denotes credits from actual
invoices/payments and advance denotes prepaid/advance payments) to match the
existing style used for LineageSegmentState.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@tools/migrate/migrations/20260409130630_add_credit_realization_lineage.up.sql`:
- Around line 14-15: Remove the redundant UNIQUE index creation for the
primary-key column: delete the CREATE UNIQUE INDEX "creditrealizationlineage_id"
ON "credit_realization_lineages" ("id") statement (and the similar duplicate at
lines 36-37) from the up migration, and also remove the matching DROP INDEX
statements from the down migration so you don't maintain duplicate indexes over
PRIMARY KEY ("id"); leave the table definitions and PKs intact.

---

Nitpick comments:
In `@openmeter/billing/charges/models/creditrealization/lineage.go`:
- Around line 12-17: Add concise doc comments for the LineageOriginKind
constants so readers know what each origin represents: add comments above
LineageOriginKindRealCredit and LineageOriginKindAdvance (referencing the
LineageOriginKind type and the constants LineageOriginKindRealCredit and
LineageOriginKindAdvance) describing their meanings (e.g., that real_credit
denotes credits from actual invoices/payments and advance denotes
prepaid/advance payments) to match the existing style used for
LineageSegmentState.

In `@openmeter/billing/charges/usagebased/service/creditsonly.go`:
- Around line 129-136: The code currently calls
s.Service.lineage.LoadActiveSegmentsByRealizationID inside the loop over
s.Charge.Realizations, causing an N+1 DB query pattern; instead, collect all
realization IDs across s.Charge.Realizations (map/concat the IDs from each
run.CreditsAllocated), call s.Service.lineage.LoadActiveSegmentsByRealizationID
once to get lineageSegmentsByRealization, then inside the loop reuse that
returned map for each run (lookup by realization ID) rather than making per-run
DB calls; update error handling to reflect the single batched call and remove
the per-run LoadActiveSegmentsByRealizationID invocation.
🪄 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: b3c3b3eb-6c08-4a6f-aeb3-30ae6e713359

📥 Commits

Reviewing files that changed from the base of the PR and between 1bfb5a8 and 4606fec.

⛔ Files ignored due to path filters (17)
  • 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/client.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage/creditrealizationlineage.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/creditrealizationlineage_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/**
  • openmeter/ent/db/runtime.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (24)
  • app/common/charges.go
  • app/common/customerbalance.go
  • openmeter/billing/charges/flatfee/adapter/charge.go
  • openmeter/billing/charges/flatfee/handler.go
  • openmeter/billing/charges/flatfee/service/creditsonly.go
  • openmeter/billing/charges/flatfee/service/lineage.go
  • openmeter/billing/charges/lineage/adapter/lineage.go
  • openmeter/billing/charges/lineage/lineage.go
  • openmeter/billing/charges/lineage/service.go
  • openmeter/billing/charges/lineage/service/service.go
  • openmeter/billing/charges/models/creditrealization/lineage.go
  • openmeter/billing/charges/service/lineage_test.go
  • openmeter/billing/charges/usagebased/adapter/charge.go
  • openmeter/billing/charges/usagebased/handler.go
  • openmeter/billing/charges/usagebased/service/creditsonly.go
  • openmeter/billing/charges/usagebased/service/lineage.go
  • openmeter/ent/schema/charges.go
  • openmeter/ent/schema/creditrealizationlineage.go
  • openmeter/ledger/chargeadapter/flatfee.go
  • openmeter/ledger/chargeadapter/usagebased.go
  • openmeter/ledger/collector/correct.go
  • openmeter/ledger/collector/service.go
  • tools/migrate/migrations/20260409130630_add_credit_realization_lineage.down.sql
  • tools/migrate/migrations/20260409130630_add_credit_realization_lineage.up.sql
✅ Files skipped from review due to trivial changes (6)
  • openmeter/billing/charges/flatfee/adapter/charge.go
  • openmeter/billing/charges/usagebased/adapter/charge.go
  • openmeter/billing/charges/service/lineage_test.go
  • openmeter/billing/charges/lineage/service.go
  • openmeter/ledger/collector/correct.go
  • openmeter/ledger/collector/service.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • openmeter/billing/charges/usagebased/service/lineage.go
  • openmeter/billing/charges/flatfee/service/creditsonly.go
  • openmeter/ent/schema/creditrealizationlineage.go
  • openmeter/billing/charges/lineage/adapter/lineage.go

Comment on lines +14 to +15
-- create index "creditrealizationlineage_id" to table: "credit_realization_lineages"
CREATE UNIQUE INDEX "creditrealizationlineage_id" ON "credit_realization_lineages" ("id");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Drop the duplicate PK indexes.

Both tables already get a unique index from PRIMARY KEY ("id"), so these extra UNIQUE INDEX ... ("id") statements just add extra index maintenance on every write. I'd remove them here and trim the matching DROP INDEX lines from the down migration too.

Also applies to: 36-37

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

In
`@tools/migrate/migrations/20260409130630_add_credit_realization_lineage.up.sql`
around lines 14 - 15, Remove the redundant UNIQUE index creation for the
primary-key column: delete the CREATE UNIQUE INDEX "creditrealizationlineage_id"
ON "credit_realization_lineages" ("id") statement (and the similar duplicate at
lines 36-37) from the up migration, and also remove the matching DROP INDEX
statements from the down migration so you don't maintain duplicate indexes over
PRIMARY KEY ("id"); leave the table definitions and PKs intact.

@GAlexIHU GAlexIHU merged commit 0643928 into main Apr 9, 2026
25 checks passed
@GAlexIHU GAlexIHU deleted the feat/ledger-ubp branch April 9, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/ignore Ignore this change when generating release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants