Skip to content

feat: add support for negative credit allocations#4035

Merged
turip merged 16 commits intomainfrom
feat/add-support-for-negative-credit-allocations
Apr 1, 2026
Merged

feat: add support for negative credit allocations#4035
turip merged 16 commits intomainfrom
feat/add-support-for-negative-credit-allocations

Conversation

@turip
Copy link
Copy Markdown
Member

@turip turip commented Mar 30, 2026

Overview

Introduces explicit allocation vs correction credit-realization types, standardizes sign handling so allocations are positive and corrections are negative everywhere in charges.

Adds correction creation/validation logic plus coverage for over-correction and remaining-balance behavior.

On the usage-based side, it fixes final-realization run state handling, adds support for persisted negative credit corrections during finalization, and ensures rated totals are rounded to currency precision before they are stored or handed to allocation/correction handlers.

It also adds lifecycle coverage for tiered usage corrections and updates related charge/subscription-sync test scaffolding.

Notes for reviewer

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for correcting and reversing credit allocations in usage-based billing.
    • Enhanced monetary precision handling across all billing calculations.
  • Improvements

    • Refined credit realization workflow for improved accuracy in allocation and correction management.
    • Strengthened database integrity for credit transaction tracking.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

This PR introduces credit-realization correction flows and refactors credit allocation/correction handling across flat-fee and usage-based charging. It defines separate allocation and correction input types with validation, updates handler interfaces to return typed inputs, adds correction request orchestration with multi-step validation, extends the database schema to support self-referential corrections and type tracking, and refactors usage-based and flat-fee state machines to handle re-rating with delta-based branching (allocating additional credits, correcting prior allocations, or performing no action).

Changes

Cohort / File(s) Summary
Credit Realization Model Core
openmeter/billing/charges/models/creditrealization/allocation.go, ...models.go, ...realizations.go
Introduced CreateAllocationInput/CreateAllocationInputs types with validation and AsCreateInputs() conversion. Extended CreateInput with ID, Type (allocation/correction), and CorrectsRealizationID fields. Added Realizations collection with Sum(), AllocationsByID(), CreateCorrectionRequest(), and Correct() orchestration methods.
Correction Model & Validation
openmeter/billing/charges/models/creditrealization/correction.go, ...correction_test.go
Introduced CorrectionRequest, CorrectionRequestItem, CreateCorrectionInput, and CreateCorrectionInputs types with cross-input validation. Implemented allocation capacity tracking, amount sign enforcement, precision rounding checks, and conversion to adapter create inputs. Added comprehensive test suite covering correction flows, validation scenarios, and error handling.
Realization Mixin & Schema
openmeter/billing/charges/models/creditrealization/mixin.go
Converted Mixin from type alias to struct with self-referential edge support. Extended Creator/Getter interfaces with ID, Type, and CorrectsRealizationID methods. Added persisted type enum field and optional self-referential corrects_realization_id foreign key.
Database Migrations & Schema
tools/migrate/migrations/20260331103521_charges-negative-realization-runs.*, openmeter/ent/schema/chargesusagebased.go, chargesflatfee.go
Added type and corrects_realization_id columns to both flat-fee and usage-based credit allocation tables. Added self-referential foreign keys with ON DELETE SET NULL. Updated ent schema to enable self-referencing via SelfReferenceType mixin configuration and explicit edge storage keys.
Handler Interface Updates
openmeter/billing/charges/flatfee/handler.go, ...usagebased/handler.go
Changed flat-fee handler return types from []CreateInput to CreateAllocationInputs for both OnAssignedToInvoice and OnCreditsOnlyUsageAccrued. Replaced usage-based OnCollectionStarted/OnCollectionFinalized with OnCreditsOnlyUsageAccrued (returning allocation inputs) and OnCreditsOnlyUsageAccruedCorrection (returning correction inputs). Added CreditsOnlyUsageAccruedInput and CreditsOnlyUsageAccruedCorrectionInput types.
Flat-Fee Service Implementation
openmeter/billing/charges/flatfee/service/creditsonly.go, ...invoice.go
Updated AllocateCredits and PostLineAssignedToInvoice to use CreateAllocationInputs, calling AsCreateInputs() before adapter persistence. Validation ensures allocation sum matches proration amount.
Usage-Based Service Refactoring
openmeter/billing/charges/usagebased/service/creditsonly.go, ...mutations.go, ...statemachine.go, ...triggers.go
Refactored allocateCredits to accept CreditsOnlyUsageAccruedInput with Run field. Updated StartFinalRealizationRun/FinalizeRealizationRun to handle positive allocations (via OnCreditsOnlyUsageAccrued) and negative deltas (via correction orchestration with OnCreditsOnlyUsageAccruedCorrection). Added CurrencyCalculator field to StateMachine/StateMachineConfig. Removed embedded credit allocation from realization-run creation; now persisted separately via adapter.
Usage-Based Adapter & Models
openmeter/billing/charges/usagebased/adapter.go, ...adapter/creditallocation.go, ...charge.go, ...realizationrun.go
Renamed CreateRunCreditAllocations to CreateRunCreditRealization to handle both allocations and corrections. Added GetCurrentRealizationRun() method on Charge. Added SetRealizationRun() mutation method on RealizationRuns slice.
Ledger Integration
openmeter/ledger/chargeadapter/flatfee.go, ...flatfee_test.go
Updated flatFeeHandler methods to return CreateAllocationInputs instead of slice. Updated creditRealizationsFromCollectedInputs helper to construct CreateAllocationInput objects and return the typed collection. Test helper updated to call AsCreateInputs() and assign IDs to allocations.
Test Infrastructure & Mocks
openmeter/billing/charges/service/handlers_test.go, ...invoicable_test.go, ...testutils/handlers.go
Updated test handlers and mocks to return allocation/correction inputs matching new handler interfaces. Added new test case TestUsageBasedCreditOnlyLifecycleVolumeTieredCorrection validating allocation/correction behavior. Mock OnCreditsOnlyUsageAccruedCorrection generates correction inputs from correction request items.
Currency & Totals Utilities
pkg/currencyx/currency.go, openmeter/billing/models/totals/model.go, ...model_test.go, openmeter/billing/rating/service/detailedline.go
Added IsRoundedToPrecision(amount) precision check on currency calculator. Added Totals.RoundToPrecision(calc) method to round all monetary fields. Updated getTotalsFromDetailedLines and calculateDetailedLineTotals to apply precision rounding.
Documentation & Misc
.agents/skills/charges/SKILL.md, openmeter/billing/charges/usagebased/service/rating.go
Updated skill documentation describing credits-only lifecycle with re-rating, delta branching, correction orchestration, and new handler interfaces. Added guidance on precision rounding checks. Minor spacing change in rating service.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add support for negative credit allocations' directly reflects the main objective of this PR: introducing explicit support for negative credit realizations (corrections) alongside positive allocations.

✏️ 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/add-support-for-negative-credit-allocations

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/add-support-for-negative-credit-allocations branch from fbc0eed to 65c9683 Compare March 31, 2026 14:07
@turip turip added release-note/misc Miscellaneous changes area/billing release-note/feature Release note: Exciting New Features and removed release-note/misc Miscellaneous changes labels Mar 31, 2026
@turip turip marked this pull request as ready for review March 31, 2026 14:25
@turip turip requested a review from a team as a code owner March 31, 2026 14:25
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

90-99: Consider centralizing totals rounding to reduce future drift.

roundTotalsToCurrencyPrecision is clear, but it’s easy to miss new monetary fields if totals.Totals evolves. A totals-level rounding helper would make this safer long-term.

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

In `@openmeter/billing/charges/usagebased/service/rating.go` around lines 90 - 99,
The current roundTotalsToCurrencyPrecision implementation individually rounds
each field on totals.Totals which is fragile if totals.Totals grows; create a
helper method on the totals type (e.g., add Totals.RoundToPrecision(calc
currencyx.Calculator) or a function totals.Round(calc currencyx.Calculator, t
*totals.Totals)) that encapsulates rounding logic and call that from
roundTotalsToCurrencyPrecision (or replace it entirely) so any future monetary
fields are handled in one place; update usages to call the new totals-level
helper and remove per-field rounding to avoid drift.
openmeter/billing/charges/models/creditrealization/models.go (1)

66-79: Consider whether zero amounts should be explicitly handled.

The validation rejects non-positive amounts for allocations and non-negative amounts for corrections, which means zero is rejected for both types. This seems intentional (why create a zero-value realization?), but worth confirming this is the desired behavior.

If zero should be allowed in edge cases, the checks would need to change. If zero should always be rejected, you might want to add a brief comment explaining why.

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

In `@openmeter/billing/charges/models/creditrealization/models.go` around lines 66
- 79, The validation currently treats zero as invalid for both TypeAllocation
and TypeCorrection (using Amount.IsPositive() and Amount.IsNegative()); decide
whether zero realizations are allowed — if you want to allow zero, change the
checks in the switch for TypeAllocation to use a non-negative check (e.g.,
IsNonNegative or equivalent) and for TypeCorrection to use a non-positive check
(and keep verifying CorrectsRealizationID for TypeCorrection); if zero should
remain invalid, add a short comment near the switch (and on
TypeAllocation/TypeCorrection behavior) explaining why zero amounts are rejected
to make the intent clear for future readers and tests.
🤖 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/models/creditrealization/correction_test.go`:
- Around line 1044-1068: The test currently returns a single correction of -15
which triggers the capacity guard instead of the mismatch-total path; change the
callback in the "error: callback returns mismatched corrections" test (the
lambda passed to Realizations.Correct) to return corrections whose summed Amount
does NOT equal the requested -5 (e.g., return a single correction of -4 or -6
for the same alloc.ID) so the code exercises the total-mismatch branch in
Realizations.Correct/CorrectionRequest handling, and update the assertion to
check the error text for the mismatch-specific message (e.g.,
assert.Contains(err.Error(), "mismatch" or the actual mismatch error string
emitted by Correct)).

In `@openmeter/billing/charges/models/creditrealization/correction.go`:
- Around line 96-140: The ValidateWith method fails to enforce that the provided
totalAmountToCorrect equals the sum of absolute correction inputs; compute
sumAbs := sum of input.Amount.Abs() across CreateCorrectionInputs (after
per-input validation) and compare it to totalAmountToCorrect (respecting
currency precision/rounding check already done); if they differ append a
validation error like "sum of correction amounts must equal
totalAmountToCorrect" and return via
models.NewNillableGenericValidationError(errors.Join(errs...)). Update
CreateCorrectionInputs.ValidateWith to perform this check before calling
existingRealizations.allocationsWithCorrections() so we fail fast when totals
don't reconcile.

---

Nitpick comments:
In `@openmeter/billing/charges/models/creditrealization/models.go`:
- Around line 66-79: The validation currently treats zero as invalid for both
TypeAllocation and TypeCorrection (using Amount.IsPositive() and
Amount.IsNegative()); decide whether zero realizations are allowed — if you want
to allow zero, change the checks in the switch for TypeAllocation to use a
non-negative check (e.g., IsNonNegative or equivalent) and for TypeCorrection to
use a non-positive check (and keep verifying CorrectsRealizationID for
TypeCorrection); if zero should remain invalid, add a short comment near the
switch (and on TypeAllocation/TypeCorrection behavior) explaining why zero
amounts are rejected to make the intent clear for future readers and tests.

In `@openmeter/billing/charges/usagebased/service/rating.go`:
- Around line 90-99: The current roundTotalsToCurrencyPrecision implementation
individually rounds each field on totals.Totals which is fragile if
totals.Totals grows; create a helper method on the totals type (e.g., add
Totals.RoundToPrecision(calc currencyx.Calculator) or a function
totals.Round(calc currencyx.Calculator, t *totals.Totals)) that encapsulates
rounding logic and call that from roundTotalsToCurrencyPrecision (or replace it
entirely) so any future monetary fields are handled in one place; update usages
to call the new totals-level helper and remove per-field rounding to avoid
drift.
🪄 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: b1b1a8bb-eec0-4099-a2fc-a19196b3308b

📥 Commits

Reviewing files that changed from the base of the PR and between 6a2c190 and 062d67d.

⛔ Files ignored due to path filters (17)
  • go.sum is excluded by !**/*.sum, !**/*.sum
  • openmeter/ent/db/chargeflatfeecreditallocations.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeecreditallocations/chargeflatfeecreditallocations.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeecreditallocations/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeecreditallocations_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeecreditallocations_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_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/entmixinaccessor.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/**
  • openmeter/ent/db/setorclear.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (29)
  • .agents/skills/charges/SKILL.md
  • openmeter/billing/charges/flatfee/handler.go
  • openmeter/billing/charges/flatfee/service/creditsonly.go
  • openmeter/billing/charges/flatfee/service/invoice.go
  • openmeter/billing/charges/models/creditrealization/allocation.go
  • openmeter/billing/charges/models/creditrealization/correction.go
  • openmeter/billing/charges/models/creditrealization/correction_test.go
  • openmeter/billing/charges/models/creditrealization/mixin.go
  • openmeter/billing/charges/models/creditrealization/models.go
  • openmeter/billing/charges/models/creditrealization/realizations.go
  • openmeter/billing/charges/service/handlers_test.go
  • openmeter/billing/charges/service/invoicable_test.go
  • openmeter/billing/charges/testutils/handlers.go
  • openmeter/billing/charges/usagebased/adapter.go
  • openmeter/billing/charges/usagebased/adapter/creditallocation.go
  • openmeter/billing/charges/usagebased/charge.go
  • openmeter/billing/charges/usagebased/handler.go
  • openmeter/billing/charges/usagebased/realizationrun.go
  • openmeter/billing/charges/usagebased/service/creditsonly.go
  • openmeter/billing/charges/usagebased/service/mutations.go
  • openmeter/billing/charges/usagebased/service/rating.go
  • openmeter/billing/charges/usagebased/service/rating_test.go
  • openmeter/billing/charges/usagebased/service/statemachine.go
  • openmeter/billing/charges/usagebased/service/triggers.go
  • openmeter/ledger/chargeadapter/flatfee.go
  • openmeter/ledger/chargeadapter/flatfee_test.go
  • pkg/currencyx/currency.go
  • tools/migrate/migrations/20260331103521_charges-negative-realization-runs.down.sql
  • tools/migrate/migrations/20260331103521_charges-negative-realization-runs.up.sql

Comment thread openmeter/billing/charges/models/creditrealization/correction_test.go Outdated
Comment thread openmeter/billing/charges/models/creditrealization/correction.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 (1)
openmeter/billing/charges/models/creditrealization/mixin.go (1)

91-95: Consider scoping allocation to sibling realizations.

This edge only proves that corrects_realization_id exists in the table. It can still point at another charge's / run's realization, or at another correction row. If those states are invalid for the domain, I’d backstop that in the concrete schema/migration or reject them in the adapter before insert.

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

In `@openmeter/billing/charges/models/creditrealization/mixin.go` around lines 91
- 95, The edge "allocation" defined in mixin.go currently only enforces
existence of corrects_realization_id and can point to realizations from other
charges or runs; update the schema and adapter to prevent cross-sibling links by
adding a scope check and/or DB constraint: in the concrete schema migration add
a composite foreign key or check constraint that ensures corrects_realization_id
references a realization with the same parent keys (e.g., same charge_id or
run_id), and additionally add a validation in the adapter logic that
creates/updates the allocation (the code path that sets corrects_realization_id)
to reject or rewrite references that do not belong to the same sibling
realization. Ensure you reference the "allocation" edge and
"corrects_realization_id" when making the migration and adapter change so the
constraint and validation target the correct field.
🤖 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/models/creditrealization/mixin.go`:
- Around line 70-76: Add a DB index on the "corrects_realization_id" column to
speed up self-referencing lookups: update the mixin to return an ent.Index that
includes index.Fields("corrects_realization_id") (e.g., implement or extend the
mixin's Indexes() to append ent.Index{}.Fields("corrects_realization_id")). Keep
the field declaration (field.String("corrects_realization_id")...) unchanged and
ensure the new index targets that exact field name so
QueryCorrections/WithCorrections use the index.

---

Nitpick comments:
In `@openmeter/billing/charges/models/creditrealization/mixin.go`:
- Around line 91-95: The edge "allocation" defined in mixin.go currently only
enforces existence of corrects_realization_id and can point to realizations from
other charges or runs; update the schema and adapter to prevent cross-sibling
links by adding a scope check and/or DB constraint: in the concrete schema
migration add a composite foreign key or check constraint that ensures
corrects_realization_id references a realization with the same parent keys
(e.g., same charge_id or run_id), and additionally add a validation in the
adapter logic that creates/updates the allocation (the code path that sets
corrects_realization_id) to reject or rewrite references that do not belong to
the same sibling realization. Ensure you reference the "allocation" edge and
"corrects_realization_id" when making the migration and adapter change so the
constraint and validation target the correct field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fb4e2b6a-71d3-4a20-b881-cd8f002b4d29

📥 Commits

Reviewing files that changed from the base of the PR and between 062d67d and e5bcc34.

⛔ Files ignored due to path filters (16)
  • openmeter/ent/db/chargeflatfeecreditallocations.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeecreditallocations/chargeflatfeecreditallocations.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeecreditallocations/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeecreditallocations_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeecreditallocations_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeecreditallocations_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_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruncreditallocations_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/client.go is excluded by !**/ent/db/**
  • openmeter/ent/db/migrate/schema.go is excluded by !**/ent/db/**
  • openmeter/ent/db/mutation.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (5)
  • openmeter/billing/charges/models/creditrealization/mixin.go
  • openmeter/ent/schema/chargesflatfee.go
  • openmeter/ent/schema/chargesusagebased.go
  • tools/migrate/migrations/20260331103521_charges-negative-realization-runs.down.sql
  • tools/migrate/migrations/20260331103521_charges-negative-realization-runs.up.sql
🚧 Files skipped from review as they are similar to previous changes (2)
  • tools/migrate/migrations/20260331103521_charges-negative-realization-runs.down.sql
  • tools/migrate/migrations/20260331103521_charges-negative-realization-runs.up.sql

Comment thread openmeter/billing/charges/models/creditrealization/mixin.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 (3)
openmeter/billing/charges/models/creditrealization/correction.go (1)

123-123: Consider using alpacadecimal.Zero for consistency.

A tiny style nit - you can use alpacadecimal.Zero instead of alpacadecimal.NewFromFloat(0) to match the pattern used elsewhere (like in your tests at line 103 of the test file).

✨ Suggested fix
-	correctionTotal := alpacadecimal.NewFromFloat(0)
+	correctionTotal := alpacadecimal.Zero
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/models/creditrealization/correction.go` at line
123, Replace the explicit zero construction for correctionTotal with the
package-level constant to match project style: change the initialization of
correctionTotal (the variable in correction.go) from using
alpacadecimal.NewFromFloat(0) to using alpacadecimal.Zero so it aligns with
other uses (e.g., tests) and avoids unnecessary allocation.
openmeter/billing/models/totals/model_test.go (1)

16-17: Consider adding a test case for zero-decimal currencies.

USD has 2 decimal places, but currencies like JPY have 0. Adding a quick test with a zero-subunit currency would catch any edge cases in the rounding logic and increase confidence that the method handles different precisions correctly.

💡 Example additional test case
func TestTotalsRoundToPrecision_ZeroDecimal(t *testing.T) {
	t.Parallel()

	calc, err := currencyx.Code(currency.JPY).Calculator()
	require.NoError(t, err)

	in := Totals{
		Amount: alpacadecimal.NewFromFloat(100.5),
		Total:  alpacadecimal.NewFromFloat(99.4),
	}

	got := in.RoundToPrecision(calc)

	require.Equal(t, "101", got.Amount.String())
	require.Equal(t, "99", got.Total.String())
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/models/totals/model_test.go` around lines 16 - 17, Add a
unit test that covers zero-decimal currencies to ensure RoundToPrecision handles
precision=0 correctly: in openmeter/billing/models/totals/model_test.go create a
new test (e.g., TestTotalsRoundToPrecision_ZeroDecimal) that obtains a
calculator with currencyx.Code(currency.JPY).Calculator(), assert no error with
require.NoError, construct a Totals value (e.g., Amount 100.5, Total 99.4), call
in.RoundToPrecision(calc) and assert the rounded results with require.Equal
(expect "101" for Amount and "99" for Total) to validate rounding behavior for
JPY.
openmeter/billing/rating/service/detailedline.go (1)

100-120: Inconsistent rounding between detailed line and invoice total calculations.

The detailed lines calculation rounds the aggregated sum (in.Totals = totals.Sum(...).RoundToPrecision(calc)), but the invoice calculation sums pre-rounded line totals without a final RoundToPrecision() call. Since each StandardLine.Totals comes from the already-rounded GenerateDetailedLinesResult.Totals, the pattern works, but it's asymmetric. Consider applying final rounding at the invoice level too for consistency—especially if precision handling ever changes in either path.

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

In `@openmeter/billing/rating/service/detailedline.go` around lines 100 - 120, The
invoice-level totals assignment in getTotalsFromDetailedLines is missing a final
rounding step: after computing in.Totals via totals.Sum(lo.Map(in.DetailedLines,
... )...), call RoundToPrecision(calc) on that result (same as used for line
totals) so in.Totals = totals.Sum(...).RoundToPrecision(calc); this ensures
GenerateDetailedLinesResult.Totals uses the same precision logic as the
individual DetailedLine.Totals and keeps rounding behavior symmetric if
precision handling changes later.
🤖 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/models/creditrealization/correction.go`:
- Around line 134-138: The error text in the validation block (where errs is
appended via fmt.Errorf for input.CorrectsRealizationID) is misleading because
the lookup map only contains allocations (from allocationsWithCorrections()), so
change the message to say "not found or is not an allocation" instead of "not
found or is not a correction"; update the fmt.Errorf string used when !ok (for
correction input[%d] ...) so it accurately reflects that the referenced
realization id is missing or isn't an allocation.

---

Nitpick comments:
In `@openmeter/billing/charges/models/creditrealization/correction.go`:
- Line 123: Replace the explicit zero construction for correctionTotal with the
package-level constant to match project style: change the initialization of
correctionTotal (the variable in correction.go) from using
alpacadecimal.NewFromFloat(0) to using alpacadecimal.Zero so it aligns with
other uses (e.g., tests) and avoids unnecessary allocation.

In `@openmeter/billing/models/totals/model_test.go`:
- Around line 16-17: Add a unit test that covers zero-decimal currencies to
ensure RoundToPrecision handles precision=0 correctly: in
openmeter/billing/models/totals/model_test.go create a new test (e.g.,
TestTotalsRoundToPrecision_ZeroDecimal) that obtains a calculator with
currencyx.Code(currency.JPY).Calculator(), assert no error with require.NoError,
construct a Totals value (e.g., Amount 100.5, Total 99.4), call
in.RoundToPrecision(calc) and assert the rounded results with require.Equal
(expect "101" for Amount and "99" for Total) to validate rounding behavior for
JPY.

In `@openmeter/billing/rating/service/detailedline.go`:
- Around line 100-120: The invoice-level totals assignment in
getTotalsFromDetailedLines is missing a final rounding step: after computing
in.Totals via totals.Sum(lo.Map(in.DetailedLines, ... )...), call
RoundToPrecision(calc) on that result (same as used for line totals) so
in.Totals = totals.Sum(...).RoundToPrecision(calc); this ensures
GenerateDetailedLinesResult.Totals uses the same precision logic as the
individual DetailedLine.Totals and keeps rounding behavior symmetric if
precision handling changes later.
🪄 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: 4da16dab-b671-4a68-801b-df0d0aee1fd8

📥 Commits

Reviewing files that changed from the base of the PR and between e5bcc34 and 2d7b893.

📒 Files selected for processing (7)
  • openmeter/billing/charges/models/creditrealization/correction.go
  • openmeter/billing/charges/models/creditrealization/correction_test.go
  • openmeter/billing/charges/usagebased/service/rating.go
  • openmeter/billing/models/totals/model.go
  • openmeter/billing/models/totals/model_test.go
  • openmeter/billing/rating/service/detailedline.go
  • tools/migrate/migrations/20260331103521_charges-negative-realization-runs.down.sql
✅ Files skipped from review due to trivial changes (1)
  • openmeter/billing/charges/usagebased/service/rating.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • tools/migrate/migrations/20260331103521_charges-negative-realization-runs.down.sql

Comment thread openmeter/billing/charges/models/creditrealization/correction.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants