Skip to content

chore: charges add custom currency support#4755

Draft
turip wants to merge 1 commit into
mainfrom
chore/add-custom-currency-support
Draft

chore: charges add custom currency support#4755
turip wants to merge 1 commit into
mainfrom
chore/add-custom-currency-support

Conversation

@turip

@turip turip commented Jul 20, 2026

Copy link
Copy Markdown
Member

Overview

Fixes #(issue)

Notes for reviewer

Greptile Summary

This PR adds custom-currency support to billing charges. The main changes are:

  • Stores either a fiat code or custom-currency reference on each charge.
  • Resolves currencies before creating credit-purchase charges.
  • Maps and filters charges using the new currency representation.
  • Updates Ent schemas, generated models, migrations, and search views.

Confidence Score: 4/5

Credit-purchase creation, currency-filtered activity queries, and database upgrades can fail on the updated paths.

  • The resolved currency never reaches the new persistence input.
  • Funded activity filtering requires mutually exclusive currency fields at once.
  • Charge mapping can fail when the new edge was not eagerly loaded.
  • Custom-currency grant flows are accepted and then rejected by lineage.
  • The migration leaves the search view stale and cannot roll back custom rows.

Credit-purchase adapters and lineage handling, shared charge mapping, and both custom-currency migration files.

Important Files Changed

Filename Overview
openmeter/billing/charges/creditpurchase/adapter/charge.go Adds custom-currency queries, but the create path does not pass the resolved currency into persistence.
openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity.go Maps custom currencies but combines fiat and custom filter alternatives with AND.
openmeter/billing/charges/models/chargemeta/model.go Introduces shared currency persistence and mapping that depends on the custom-currency edge being loaded.
openmeter/billing/charges/lineage/service.go Carries resolved currency objects into lineage while explicitly rejecting custom currencies.
openmeter/ent/schema/charges.go Adds mutually exclusive fiat and custom currency references and expands the search-view schema.
tools/migrate/migrations/20260720093016_charges-custom-currencies.up.sql Updates charge tables without recreating the persisted search view.
tools/migrate/migrations/20260720093016_charges-custom-currencies.down.sql Restores a non-null fiat column without handling rows that use custom currencies.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Credit purchase request] --> B[Resolve currency by namespace and code]
  B --> C[Normalize amount precision]
  C --> D[Persist charge]
  D --> E{Currency type}
  E -->|Fiat| F[Store currency code]
  E -->|Custom| G[Store custom currency ID]
  F --> H[Map charge with resolved currency]
  G --> H
  H --> I[Funded activity and lineage flows]
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[Credit purchase request] --> B[Resolve currency by namespace and code]
  B --> C[Normalize amount precision]
  C --> D[Persist charge]
  D --> E{Currency type}
  E -->|Fiat| F[Store currency code]
  E -->|Custom| G[Store custom currency ID]
  F --> H[Map charge with resolved currency]
  G --> H
  H --> I[Funded activity and lineage flows]
Loading

Comments Outside Diff (1)

  1. openmeter/billing/charges/creditpurchase/adapter/charge.go, line 84-89 (link)

    P1 Resolved Currency Is Dropped

    service.Create resolves the requested currency, but only uses it for amount normalization before calling this adapter. This new chargemeta.CreateInput requires Currency, yet the value is not carried through creditpurchase.CreateInput or set here, so every valid credit-purchase create returns currency is required before persistence.

    Context Used: AGENTS.md (source)

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: openmeter/billing/charges/creditpurchase/adapter/charge.go
    Line: 84-89
    
    Comment:
    **Resolved Currency Is Dropped**
    
    `service.Create` resolves the requested currency, but only uses it for amount normalization before calling this adapter. This new `chargemeta.CreateInput` requires `Currency`, yet the value is not carried through `creditpurchase.CreateInput` or set here, so every valid credit-purchase create returns `currency is required` before persistence.
    
    **Context Used:** AGENTS.md ([source](https://app.greptile.com/openmeter/github/openmeterio/openmeter/-/custom-context?memory=eb020e3b-8e3b-45ea-a8b7-4006b2b2065b))
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 6 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 6
openmeter/billing/charges/creditpurchase/adapter/charge.go:84-89
**Resolved Currency Is Dropped**

`service.Create` resolves the requested currency, but only uses it for amount normalization before calling this adapter. This new `chargemeta.CreateInput` requires `Currency`, yet the value is not carried through `creditpurchase.CreateInput` or set here, so every valid credit-purchase create returns `currency is required` before persistence.

### Issue 2 of 6
openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity.go:67-74
**Mutually Exclusive Filters Are ANDed**

`HasCreditPurchaseWith` combines these predicates with AND, but the new database constraint requires a charge to have either `fiat_currency_code` or `custom_currency_id`, never both. Supplying any currency filter therefore excludes the matching funded activities instead of returning them.

```suggestion
		query = query.Where(dbchargecreditpurchasecreditgrant.HasCreditPurchaseWith(
			dbchargecreditpurchase.Or(
				dbchargecreditpurchase.HasCustomCurrencyWith(
					dbcustomcurrency.CodeEQ(*input.Currency),
					dbcustomcurrency.Namespace(input.Customer.Namespace),
					dbcustomcurrency.DeletedAtIsNil(),
				),
				dbchargecreditpurchase.FiatCurrencyCodeEQ(*input.Currency),
			),
		))
```

### Issue 3 of 6
openmeter/billing/charges/models/chargemeta/model.go:255-263
**Mapping Requires an Eager-Loaded Edge**

Every charge mapping now calls `CustomCurrencyOrErr`, including fiat charges and entities returned by update or mark-voided operations. Those write queries do not load the new edge, so the accessor returns an edge-not-loaded error and otherwise successful operations fail while mapping their result. The mapper must not require an eager-loaded edge on every path, or every caller must load it first.

### Issue 4 of 6
openmeter/billing/charges/lineage/service.go:114-119
**Custom Credit Lifecycle Is Rejected**

The create service now accepts and resolves a custom currency, but credit-purchase grant flows call advance-lineage backfill with that resolved currency. This new guard rejects the operation, so a custom-currency promotional or external credit purchase cannot complete its lifecycle. Either reject this currency earlier as unsupported or add lineage support before accepting the charge.

### Issue 5 of 6
tools/migrate/migrations/20260720093016_charges-custom-currencies.up.sql:1-6
**Search View Keeps Its Old Shape**

The generated `ChargesSearchV1` model now selects `custom_currency_id`, but this migration never recreates the existing `charges_search_v1s` view with that column. After upgrading an existing database, charge-search queries request a column the installed view does not expose and fail at runtime. Recreate the view in this migration after altering its source tables.

### Issue 6 of 6
tools/migrate/migrations/20260720093016_charges-custom-currencies.down.sql:1-6
**Rollback Cannot Convert Custom Charges**

A custom-currency charge has `currency IS NULL` and only `custom_currency_id` populated. This rollback drops that ID and then restores `currency` to NOT NULL, so it fails as soon as any custom-currency charge exists. The rollback needs an explicit conversion or deletion policy before restoring the old constraint.

Reviews (1): Last reviewed commit: "chore: charges add custom currency suppo..." | Re-trigger Greptile

Greptile also left 5 inline comments on this PR.

Context used:

  • Context used - CLAUDE.md (source)
  • Context used - AGENTS.md (source)

@coderabbitai

coderabbitai Bot commented Jul 20, 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: 36bf0bc4-07e5-44a9-aded-c528edf99371

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 chore/add-custom-currency-support

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.

Comment on lines +67 to +74
query = query.Where(dbchargecreditpurchasecreditgrant.HasCreditPurchaseWith(
dbchargecreditpurchase.HasCustomCurrencyWith(
dbcustomcurrency.CodeEQ(*input.Currency),
dbcustomcurrency.Namespace(input.Customer.Namespace),
dbcustomcurrency.DeletedAtIsNil(),
),
dbchargecreditpurchase.FiatCurrencyCodeEQ(*input.Currency),
))

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.

P1 Mutually Exclusive Filters Are ANDed

HasCreditPurchaseWith combines these predicates with AND, but the new database constraint requires a charge to have either fiat_currency_code or custom_currency_id, never both. Supplying any currency filter therefore excludes the matching funded activities instead of returning them.

Suggested change
query = query.Where(dbchargecreditpurchasecreditgrant.HasCreditPurchaseWith(
dbchargecreditpurchase.HasCustomCurrencyWith(
dbcustomcurrency.CodeEQ(*input.Currency),
dbcustomcurrency.Namespace(input.Customer.Namespace),
dbcustomcurrency.DeletedAtIsNil(),
),
dbchargecreditpurchase.FiatCurrencyCodeEQ(*input.Currency),
))
query = query.Where(dbchargecreditpurchasecreditgrant.HasCreditPurchaseWith(
dbchargecreditpurchase.Or(
dbchargecreditpurchase.HasCustomCurrencyWith(
dbcustomcurrency.CodeEQ(*input.Currency),
dbcustomcurrency.Namespace(input.Customer.Namespace),
dbcustomcurrency.DeletedAtIsNil(),
),
dbchargecreditpurchase.FiatCurrencyCodeEQ(*input.Currency),
),
))
Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity.go
Line: 67-74

Comment:
**Mutually Exclusive Filters Are ANDed**

`HasCreditPurchaseWith` combines these predicates with AND, but the new database constraint requires a charge to have either `fiat_currency_code` or `custom_currency_id`, never both. Supplying any currency filter therefore excludes the matching funded activities instead of returning them.

```suggestion
		query = query.Where(dbchargecreditpurchasecreditgrant.HasCreditPurchaseWith(
			dbchargecreditpurchase.Or(
				dbchargecreditpurchase.HasCustomCurrencyWith(
					dbcustomcurrency.CodeEQ(*input.Currency),
					dbcustomcurrency.Namespace(input.Customer.Namespace),
					dbcustomcurrency.DeletedAtIsNil(),
				),
				dbchargecreditpurchase.FiatCurrencyCodeEQ(*input.Currency),
			),
		))
```

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

Fix in Claude Code Fix in Codex

Comment on lines +255 to +263
dbCustomCurrency, err := entedge.OrNilIfNotFound(edges.CustomCurrencyOrErr())
if err != nil {
return meta.Charge{}, fmt.Errorf("failed to get custom currency: %w", err)
}

resolvedCurrency, err := currenciesadapter.MapCustomCurrencyOrFiatCurrencyFromDB(currenciesadapter.CustomCurrencyOrFiatCurrency{
CustomCurrency: dbCustomCurrency,
FiatCurrency: entity.GetFiatCurrencyCode(),
})

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.

P1 Mapping Requires an Eager-Loaded Edge

Every charge mapping now calls CustomCurrencyOrErr, including fiat charges and entities returned by update or mark-voided operations. Those write queries do not load the new edge, so the accessor returns an edge-not-loaded error and otherwise successful operations fail while mapping their result. The mapper must not require an eager-loaded edge on every path, or every caller must load it first.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/billing/charges/models/chargemeta/model.go
Line: 255-263

Comment:
**Mapping Requires an Eager-Loaded Edge**

Every charge mapping now calls `CustomCurrencyOrErr`, including fiat charges and entities returned by update or mark-voided operations. Those write queries do not load the new edge, so the accessor returns an edge-not-loaded error and otherwise successful operations fail while mapping their result. The mapper must not require an eager-loaded edge on every path, or every caller must load it first.

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

Fix in Claude Code Fix in Codex

Comment on lines 114 to 119
}
if err := i.Currency.Validate(); err != nil {
errs = append(errs, fmt.Errorf("currency: %w", err))
} else if i.Currency.Type() == currencyx.CurrencyTypeCustom {
errs = append(errs, fmt.Errorf("advance lineage backfill: %w", meta.ErrCustomCurrencyNotSupported))
}

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.

P1 Custom Credit Lifecycle Is Rejected

The create service now accepts and resolves a custom currency, but credit-purchase grant flows call advance-lineage backfill with that resolved currency. This new guard rejects the operation, so a custom-currency promotional or external credit purchase cannot complete its lifecycle. Either reject this currency earlier as unsupported or add lineage support before accepting the charge.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/billing/charges/lineage/service.go
Line: 114-119

Comment:
**Custom Credit Lifecycle Is Rejected**

The create service now accepts and resolves a custom currency, but credit-purchase grant flows call advance-lineage backfill with that resolved currency. This new guard rejects the operation, so a custom-currency promotional or external credit purchase cannot complete its lifecycle. Either reject this currency earlier as unsupported or add lineage support before accepting the charge.

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

Fix in Claude Code Fix in Codex

Comment on lines +1 to +6
-- modify "charge_credit_purchases" table
ALTER TABLE "charge_credit_purchases" ADD CONSTRAINT "currency_reference" CHECK ((currency IS NULL) <> (custom_currency_id IS NULL)), ALTER COLUMN "currency" DROP NOT NULL, ADD COLUMN "custom_currency_id" character(26) NULL, ADD CONSTRAINT "charge_credit_purchases_custom_currencies_charges_credit_purcha" FOREIGN KEY ("custom_currency_id") REFERENCES "custom_currencies" ("id") ON UPDATE NO ACTION ON DELETE RESTRICT;
-- modify "charge_flat_fees" table
ALTER TABLE "charge_flat_fees" ADD CONSTRAINT "currency_reference" CHECK ((currency IS NULL) <> (custom_currency_id IS NULL)), ALTER COLUMN "currency" DROP NOT NULL, ADD COLUMN "custom_currency_id" character(26) NULL, ADD CONSTRAINT "charge_flat_fees_custom_currencies_charges_flat_fee" FOREIGN KEY ("custom_currency_id") REFERENCES "custom_currencies" ("id") ON UPDATE NO ACTION ON DELETE RESTRICT;
-- modify "charge_usage_based" table
ALTER TABLE "charge_usage_based" ADD CONSTRAINT "currency_reference" CHECK ((currency IS NULL) <> (custom_currency_id IS NULL)), ALTER COLUMN "currency" DROP NOT NULL, ADD COLUMN "custom_currency_id" character(26) NULL, ADD CONSTRAINT "charge_usage_based_custom_currencies_charges_usage_based" FOREIGN KEY ("custom_currency_id") REFERENCES "custom_currencies" ("id") ON UPDATE NO ACTION ON DELETE RESTRICT;

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.

P1 Search View Keeps Its Old Shape

The generated ChargesSearchV1 model now selects custom_currency_id, but this migration never recreates the existing charges_search_v1s view with that column. After upgrading an existing database, charge-search queries request a column the installed view does not expose and fail at runtime. Recreate the view in this migration after altering its source tables.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/migrate/migrations/20260720093016_charges-custom-currencies.up.sql
Line: 1-6

Comment:
**Search View Keeps Its Old Shape**

The generated `ChargesSearchV1` model now selects `custom_currency_id`, but this migration never recreates the existing `charges_search_v1s` view with that column. After upgrading an existing database, charge-search queries request a column the installed view does not expose and fail at runtime. Recreate the view in this migration after altering its source tables.

**Context Used:** AGENTS.md ([source](https://app.greptile.com/openmeter/github/openmeterio/openmeter/-/custom-context?memory=eb020e3b-8e3b-45ea-a8b7-4006b2b2065b))

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

Fix in Claude Code Fix in Codex

Comment on lines +1 to +6
-- reverse: modify "charge_usage_based" table
ALTER TABLE "charge_usage_based" DROP CONSTRAINT "charge_usage_based_custom_currencies_charges_usage_based", DROP COLUMN "custom_currency_id", ALTER COLUMN "currency" SET NOT NULL, DROP CONSTRAINT "currency_reference";
-- reverse: modify "charge_flat_fees" table
ALTER TABLE "charge_flat_fees" DROP CONSTRAINT "charge_flat_fees_custom_currencies_charges_flat_fee", DROP COLUMN "custom_currency_id", ALTER COLUMN "currency" SET NOT NULL, DROP CONSTRAINT "currency_reference";
-- reverse: modify "charge_credit_purchases" table
ALTER TABLE "charge_credit_purchases" DROP CONSTRAINT "charge_credit_purchases_custom_currencies_charges_credit_purcha", DROP COLUMN "custom_currency_id", ALTER COLUMN "currency" SET NOT NULL, DROP CONSTRAINT "currency_reference";

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.

P1 Rollback Cannot Convert Custom Charges

A custom-currency charge has currency IS NULL and only custom_currency_id populated. This rollback drops that ID and then restores currency to NOT NULL, so it fails as soon as any custom-currency charge exists. The rollback needs an explicit conversion or deletion policy before restoring the old constraint.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/migrate/migrations/20260720093016_charges-custom-currencies.down.sql
Line: 1-6

Comment:
**Rollback Cannot Convert Custom Charges**

A custom-currency charge has `currency IS NULL` and only `custom_currency_id` populated. This rollback drops that ID and then restores `currency` to NOT NULL, so it fails as soon as any custom-currency charge exists. The rollback needs an explicit conversion or deletion policy before restoring the old constraint.

**Context Used:** AGENTS.md ([source](https://app.greptile.com/openmeter/github/openmeterio/openmeter/-/custom-context?memory=eb020e3b-8e3b-45ea-a8b7-4006b2b2065b))

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant