Skip to content

feat(custom-currencies): add custom currencies support to ledger#4549

Draft
mark-vass-konghq wants to merge 7 commits into
mainfrom
feat/add-custom-currencies-support-to-ledger
Draft

feat(custom-currencies): add custom currencies support to ledger#4549
mark-vass-konghq wants to merge 7 commits into
mainfrom
feat/add-custom-currencies-support-to-ledger

Conversation

@mark-vass-konghq

Copy link
Copy Markdown
Contributor

No description provided.

@mark-vass-konghq mark-vass-konghq self-assigned this Jun 22, 2026
@mark-vass-konghq mark-vass-konghq added release-note/feature Release note: Exciting New Features area/billing labels Jun 22, 2026
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3938dc49-907b-4631-a4c8-f53ac8a58dcc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-custom-currencies-support-to-ledger

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.

@mark-vass-konghq mark-vass-konghq force-pushed the feat/add-custom-currencies-support-to-ledger branch from 4dbfd70 to e82d034 Compare June 22, 2026 11:38
Comment on lines +4 to +20
ALTER TABLE "billing_invoice_lines" ALTER COLUMN "currency" TYPE character varying(24);
-- modify "billing_invoice_split_line_groups" table
ALTER TABLE "billing_invoice_split_line_groups" ALTER COLUMN "currency" TYPE character varying(24);
-- modify "billing_invoices" table
ALTER TABLE "billing_invoices" ALTER COLUMN "currency" TYPE character varying(24);
-- modify "billing_standard_invoice_detailed_lines" table
ALTER TABLE "billing_standard_invoice_detailed_lines" ALTER COLUMN "currency" TYPE character varying(24);
-- modify "charge_credit_purchases" table
ALTER TABLE "charge_credit_purchases" ALTER COLUMN "currency" TYPE character varying(24);
-- modify "charge_flat_fee_run_detailed_lines" table
ALTER TABLE "charge_flat_fee_run_detailed_lines" ALTER COLUMN "currency" TYPE character varying(24);
-- modify "charge_flat_fees" table
ALTER TABLE "charge_flat_fees" ALTER COLUMN "currency" TYPE character varying(24);
-- modify "charge_usage_based" table
ALTER TABLE "charge_usage_based" ALTER COLUMN "currency" TYPE character varying(24);
-- modify "charge_usage_based_run_detailed_line" table
ALTER TABLE "charge_usage_based_run_detailed_line" ALTER COLUMN "currency" TYPE character varying(24);

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 only change those parts of the db where this makes sense for this change if possible.

e.g. billing will not use non-fiat currencies as currency.

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 mean that's my recommendation.

@mark-vass-konghq mark-vass-konghq requested a review from turip June 23, 2026 07:59
@mark-vass-konghq mark-vass-konghq force-pushed the feat/add-custom-currencies-support-to-ledger branch from 7ee9bcb to 7ecad00 Compare July 2, 2026 13:58
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends the ledger to support custom (non-ISO) currency codes alongside standard fiat currencies. The main changes are: a new V3 routing key that adds a source field identifying the fiat currency that funded a custom-currency bucket; widened varchar(3)varchar(24) currency columns across charge and lineage tables; a ValidateIntent override hook in chargemeta.Create/Update so credit-purchase intents can carry custom currency codes while generic billing validators continue to reject them; and a custom-currency path in allocateAccruedAttribution that builds a precision-0 calculator when the fiat registry lookup fails.

  • Routing layer: a new RoutingKeyVersionV3 is introduced, selected automatically when Route.Source != nil. V1/V2 builders now explicitly reject a non-nil Source. The RouteFilter gains a three-state Source field (None = don't filter, Some(nil) = require null, Some(&code) = require exact match), mirrored in both the sub-account adapter and the historical entry query.
  • Validation: ValidateCurrency now uses ValidateFormat() (shape-only) so historical custom-currency facts remain readable; ValidateCurrencySource enforces fiat-only sources and prohibits sources on fiat-currency routes. Settlement, gathering-invoice, seq, and std-invoice validators gain explicit fiat-only guards.
  • Migrations: two migrations — one widens currency columns to 24 chars (with a dependent view drop/recreate), another adds the nullable source column to ledger_sub_account_routes.

Confidence Score: 4/5

Safe to merge with minor cleanup; the routing, validation, and migration changes are structurally correct and well-tested, with one small API inconsistency in BuildRoutingKeyV3.

The core routing versioning logic is correct: V3 keys are only auto-selected when Source is non-nil, V1/V2 guards reject Source, and the DB predicate and normalization paths handle the three-state filter consistently. The migration correctly drops and recreates the dependent view before widening columns. The main concern is that BuildRoutingKeyV3 — unlike BuildRoutingKeyV1 and BuildRoutingKeyV2 — does not reject a nil Source, so a caller who passes a sourceless route to the explicit V3 builder gets a key that diverges from what BuildRoutingKey would produce for the same route, which could create duplicate sub-accounts. The credit-purchase Intent.Validate() duplication is a maintainability risk rather than a present defect.

openmeter/ledger/routing.go (BuildRoutingKeyV3 guard), openmeter/billing/charges/creditpurchase/charge.go (inline meta.Intent duplication)

Important Files Changed

Filename Overview
openmeter/ledger/routing.go Adds V3 routing key (source segment), ValidateCurrencySource helper, and optionalCurrencyValue encoder; ValidateCurrency now uses ValidateFormat; BuildRoutingKeyV3 lacks a nil-Source guard unlike V1/V2 siblings
openmeter/ledger/chargeadapter/creditpurchase.go allocateAccruedAttribution accepts currencyx.Code instead of Calculator; falls back to precision-0 CustomCurrency when fiat registry lookup fails for custom-currency allocation
openmeter/billing/charges/creditpurchase/charge.go Intent.Validate() expanded to use ValidateFormat() for currency (allowing custom codes) and inlines all meta.Intent field checks; settlement-currency cross-check retained
openmeter/billing/charges/models/chargemeta/mixin.go Adds ValidateIntent override hook to CreateInput/UpdateInput so credit-purchase callers can substitute a custom validator; currency schema type widened to varchar(24)
openmeter/billing/charges/creditpurchase/settlement.go GenericSettlement.Validate() now requires fiat currency via IsKnownFiat() after format check; settlement currency-match cross-check preserved in Intent.Validate()
tools/migrate/migrations/20260702111040_widen_currency_codes.up.sql Widens seven currency columns from varchar(3) to varchar(24); correctly drops and recreates the charges_search_v1s dependent view around the ALTER TABLE statements
openmeter/ledger/transactions/fx.go ConvertCurrencyTemplate.resolve() now sets Source on FBO and brokerage target sub-account routes when the source is a known fiat currency and the target is custom
openmeter/ledger/account/adapter/subaccount.go resolveOrCreateRoute now stores Source via SetNillableSource; ListSubAccounts adds SourceIsNil/Source predicate to route filter; MapSubAccountData maps Source from DB entity

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Route: Currency + Source] --> B{Source nil?}
    B -- Yes --> C{TaxBehavior nil?}
    B -- No --> D[V3 key selected\ncurrency + source + all fields]
    C -- Yes --> E[V1 key selected\ncurrency only fields]
    C -- No --> F[V2 key selected\ncurrency + tax_behavior fields]

    D --> G[ValidateCurrencySource\nsource must be known fiat\ncurrency must NOT be fiat]

    H[ConvertCurrencyTemplate\nfiat source to custom target] --> I{source IsKnownFiat\nAND target not fiat?}
    I -- Yes --> J[targetSource set to SourceCurrency\nFBO and Brokerage routes carry Source]
    I -- No --> K[targetSource nil\nno Source on routes]

    L[allocateAccruedAttribution] --> M{currency.Calculator succeeds?}
    M -- Yes fiat --> N[Use fiat calculator for allocation]
    M -- No custom --> O[ValidateCurrency format check\nBuild CustomCurrency precision 0\nBuild Calculator]
    O --> N
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Route: Currency + Source] --> B{Source nil?}
    B -- Yes --> C{TaxBehavior nil?}
    B -- No --> D[V3 key selected\ncurrency + source + all fields]
    C -- Yes --> E[V1 key selected\ncurrency only fields]
    C -- No --> F[V2 key selected\ncurrency + tax_behavior fields]

    D --> G[ValidateCurrencySource\nsource must be known fiat\ncurrency must NOT be fiat]

    H[ConvertCurrencyTemplate\nfiat source to custom target] --> I{source IsKnownFiat\nAND target not fiat?}
    I -- Yes --> J[targetSource set to SourceCurrency\nFBO and Brokerage routes carry Source]
    I -- No --> K[targetSource nil\nno Source on routes]

    L[allocateAccruedAttribution] --> M{currency.Calculator succeeds?}
    M -- Yes fiat --> N[Use fiat calculator for allocation]
    M -- No custom --> O[ValidateCurrency format check\nBuild CustomCurrency precision 0\nBuild Calculator]
    O --> N
Loading

Reviews (2): Last reviewed commit: "fix: comments" | Re-trigger Greptile

Comment thread openmeter/ledger/routing.go
Comment on lines 786 to 810
})
}

allocations, err := currencyx.AllocateByAmount(calculator, currencyx.AmountAllocationInput[accruedBackfillBucketKey]{
Amount: amount,
Items: items,
CompareKey: cmpx.Compare[accruedBackfillBucketKey],
})
calculator, err := currency.Calculator()
if err == nil {
allocations, err := currencyx.AllocateByAmount(calculator, currencyx.AmountAllocationInput[accruedBackfillBucketKey]{
Amount: amount,
Items: items,
CompareKey: cmpx.Compare[accruedBackfillBucketKey],
})
if err != nil {
return nil, fmt.Errorf("allocate accrued attribution: %w", err)
}

return allocations, nil
}

if err := ledger.ValidateCurrency(currency); err != nil {
return nil, fmt.Errorf("currency: %w", err)
}

allocations, err := allocateAccruedAttributionExactly(amount, items)
if err != nil {
return nil, fmt.Errorf("allocate accrued attribution: %w", 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.

P2 No currency-precision rounding in the custom-currency allocation path

allocateAccruedAttributionExactly does not round allocated amounts to the custom currency's precision. When the proportional split produces a repeating decimal (e.g., splitting 1 CREDIT among 3 equal buckets yields 0.333…), the ledger entries will carry arbitrary-precision amounts. The entry-amount precision guard in validateEntryAmountPrecision (validations.go) also skips custom currencies — when currency.Calculator() fails it falls through to ValidateCurrency, which only validates the code format. The combination means fractional amounts can be booked without any precision enforcement. Current tests use integer amounts so this doesn't surface yet, but any proportional split of an odd custom-currency amount will silently produce sub-unit entries.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/ledger/chargeadapter/creditpurchase.go
Line: 786-810

Comment:
**No currency-precision rounding in the custom-currency allocation path**

`allocateAccruedAttributionExactly` does not round allocated amounts to the custom currency's precision. When the proportional split produces a repeating decimal (e.g., splitting 1 CREDIT among 3 equal buckets yields 0.333…), the ledger entries will carry arbitrary-precision amounts. The entry-amount precision guard in `validateEntryAmountPrecision` (`validations.go`) also skips custom currencies — when `currency.Calculator()` fails it falls through to `ValidateCurrency`, which only validates the code format. The combination means fractional amounts can be booked without any precision enforcement. Current tests use integer amounts so this doesn't surface yet, but any proportional split of an odd custom-currency amount will silently produce sub-unit entries.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

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