Skip to content

feat(ent): Add custom currencies to the database schema#3888

Merged
turip merged 2 commits into
mainfrom
feat/currencies-v3-api-ent
Mar 3, 2026
Merged

feat(ent): Add custom currencies to the database schema#3888
turip merged 2 commits into
mainfrom
feat/currencies-v3-api-ent

Conversation

@mark-vass-konghq
Copy link
Copy Markdown
Contributor

@mark-vass-konghq mark-vass-konghq commented Feb 20, 2026

Features

  • Add custom currencies to the database schema

Summary by CodeRabbit

  • New Features

    • Support for custom currencies with configurable code, name, and symbol.
    • Cost-basis history for currencies, with separate effective-from entries to track rate changes over time.
    • Immutable records with creation timestamps to preserve auditability.
  • Database

    • Migrations add tables, relationships and unique indexes to persist custom currencies, cost-basis history, and effective-from history.

@mark-vass-konghq mark-vass-konghq self-assigned this Feb 20, 2026
@mark-vass-konghq mark-vass-konghq requested a review from a team as a code owner February 20, 2026 15:53
@mark-vass-konghq mark-vass-konghq added the release-note/feature Release note: Exciting New Features label Feb 20, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds new Ent schemas for custom currencies and cost-basis history with a TimeMixin and immutable fields, plus SQL migrations that create/drop the related tables, indexes, and foreign-key relationships (including a separate effective_from history table).

Changes

Cohort / File(s) Summary
Ent schema
openmeter/ent/schema/custom_currencies.go
Adds TimeMixin (immutable created_at), CustomCurrency (immutable code, name, symbol, edge cost_basis_history), CurrencyCostBasis (immutable fiat_code, rate, edge currency, index on (fiat_code, currency)), and CurrencyCostBasisEffectiveFrom (immutable effective_from, links to cost basis).
Migrations: initial add
tools/migrate/migrations/20260223180500_add_custom_currency.up.sql, tools/migrate/migrations/20260223180500_add_custom_currency.down.sql
Up: creates custom_currencies and currency_cost_bases tables, PKs, created_at, indexes, and FK from cost bases to custom currencies. Down: drops those indexes and tables.
Migrations: effective_from refactor
tools/migrate/migrations/20260224135901_add_effective_from_schema.up.sql, tools/migrate/migrations/20260224135901_add_effective_from_schema.down.sql
Up: removes effective_from column from currency_cost_bases, adds unique indexes (including on code), and creates currency_cost_basis_effective_froms table with FK to currency_cost_bases. Down: drops those indexes and table, and re-adds effective_from column to currency_cost_bases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

release-note/ignore

Suggested reviewers

  • tothandras
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding custom currencies to the database schema through new Ent schemas and migrations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/currencies-v3-api-ent

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.

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

🧹 Nitpick comments (8)
api/spec/src/v3/currencies/cost-bases/cost-basis.tsp (1)

9-12: Quick note on the @friendlyName

The friendly name is "BillingCostBasis" but the model is CostBasis under the Currencies namespace, not under Billing. This is totally fine if "Billing" is the intended domain prefix for generated code, just wanted to flag the naming mismatch in case it's unintentional.

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

In `@api/spec/src/v3/currencies/cost-bases/cost-basis.tsp` around lines 9 - 12,
The model CostBasis in the Currencies namespace has a `@friendlyName` of
"BillingCostBasis", creating a naming mismatch; confirm intent and if
unintended, update the `@friendlyName` to match the model/domain (e.g.,
"CurrenciesCostBasis" or "CostBasis") or move/rename the model to the Billing
domain; locate the CostBasis model declaration and the `@friendlyName` attribute
and make the name consistent with the intended generated-code prefix.
tools/migrate/migrations/20260220154446_add_custom_currency.up.sql (1)

10-11: Redundant unique indexes on primary key columns

Lines 11 and 26 create unique indexes on id columns that are already primary keys. PKs inherently have unique indexes. This is likely auto-generated by Ent so probably not worth fighting, but worth knowing they add a tiny bit of storage/write overhead for no benefit.

Also applies to: 25-26

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

In `@tools/migrate/migrations/20260220154446_add_custom_currency.up.sql` around
lines 10 - 11, Remove the redundant UNIQUE indexes on the primary key columns:
delete the CREATE UNIQUE INDEX "customcurrency_id" ON "custom_currencies"
("id"); statement (and the similar duplicate around lines 25-26) from the
migration so you don't create a second unique index on the PK; ensure there are
no other statements depending on those index names and re-generate or re-run the
migration after confirming the primary key constraint alone provides the
necessary uniqueness.
openmeter/ent/schema/custom_currencies.go (1)

17-25: Good catch adding a TimeMixin—this is new and solid!

There's no existing TimeMixin in the codebase, so you're introducing something useful. That said, I did notice that created_at field definitions are duplicated in a few places (like customer.go and notification.go). Consider using this mixin in those files going forward to keep things consistent and DRY. No need to refactor existing code as part of this PR, but it's worth thinking about as the pattern grows.

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

In `@openmeter/ent/schema/custom_currencies.go` around lines 17 - 25, You added a
new TimeMixin with TimeMixin.Fields() that defines the created_at field, but
created_at is still duplicated in other schemas (e.g., customer.go and
notification.go); update those schema files to remove their local created_at
field declarations and instead include the TimeMixin in their mixins() by
referencing TimeMixin (or embed mixin.Schema where appropriate) so the
created_at column is defined centrally by TimeMixin and not duplicated across
Customer and Notification schema definitions.
api/spec/src/v3/currencies/operations.tsp (1)

40-43: Redundant Currencies. namespace prefix on the request body

Since we're already inside namespace Currencies, Currencies.CustomCurrency can just be CustomCurrency. Tiny nit but it's cleaner.

✨ Suggested fix
   create(
     `@header` contentType: "application/json",
-    `@body` body: Currencies.CustomCurrency,
+    `@body` body: CustomCurrency,
   ): Shared.CreateResponse<CustomCurrency> | Common.ErrorResponses;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/spec/src/v3/currencies/operations.tsp` around lines 40 - 43, Inside the
Currencies namespace the create method's body type uses a redundant qualified
name; change the parameter type from Currencies.CustomCurrency to CustomCurrency
in the create(...) signature (the method named create in this file), leaving the
return type Shared.CreateResponse<CustomCurrency> unchanged so the local type is
used without the unnecessary Currencies. prefix.
api/v3/server/routes.go (1)

143-159: Stubs look good — just tracking the follow-up

The delegation to unimplemented is the right pattern for work in progress, and all four route methods are wired correctly. Once the actual handlers are ready, you'll want to add a currenciesHandler (or similar) to the Server struct and swap these out, following the same pattern as s.metersHandler, s.billingProfilesHandler, etc.

Want me to open a follow-up issue to track implementing the real handler?

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

In `@api/v3/server/routes.go` around lines 143 - 159, These route methods
currently delegate to unimplemented stubs; add a concrete handler field (e.g.,
currenciesHandler) to the Server struct and replace the unimplemented calls in
ListCurrencies, CreateCustomCurrency, CreateCostBasis, and GetCostBases to
delegate to s.currenciesHandler.ListCurrencies,
s.currenciesHandler.CreateCustomCurrency, s.currenciesHandler.CreateCostBasis,
and s.currenciesHandler.GetCostBases respectively (matching the existing pattern
used for s.metersHandler and s.billingProfilesHandler), and ensure the Server
constructor/initializer wires an instance of the new currenciesHandler so the
routes call real implementations instead of unimplemented.*.
api/spec/src/v3/currencies/custom.tsp (1)

1-1: Add explicit import "./base.tsp" for consistency

custom.tsp uses CurrencyBase and CustomCurrencyCode which are defined in base.tsp, but that file isn't imported here. It works through the shared compilation unit, but currency.tsp does this explicitly — it'd be great to keep things consistent so the dependency is clear at a glance.

✨ Suggested fix
 import "../shared/index.tsp";
+import "./base.tsp";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/spec/src/v3/currencies/custom.tsp` at line 1, Add an explicit import of
the base file in custom.tsp so the dependencies are obvious: update
api/spec/src/v3/currencies/custom.tsp to import "./base.tsp" in addition to
"../shared/index.tsp" so types used in this file (CurrencyBase and
CustomCurrencyCode) are directly referenced; ensure the import is placed at the
top of the file before any usage of CurrencyBase or CustomCurrencyCode.
api/v3/api.gen.go (1)

2048-2052: Request body types include read-only fields (id, created_at)

CreateCustomCurrencyJSONRequestBody = BillingCustomCurrency and CreateCostBasisJSONRequestBody = BillingCostBasis both include server-generated fields (id, created_at) that are @visibility(Lifecycle.Read) in the TypeSpec source. Clients can technically send these in the request payload, where they'll just be silently ignored.

This is a TypeSpec-level design choice — other endpoints in this API use dedicated "create request" types (e.g., CreateCustomerRequest, CreateBillingProfileRequest) that exclude read-only fields. Using a dedicated create type for CustomCurrency and CostBasis would make the contract clearer for API consumers.

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

In `@api/v3/api.gen.go` around lines 2048 - 2052, The request body aliases
CreateCustomCurrencyJSONRequestBody and CreateCostBasisJSONRequestBody currently
point to BillingCustomCurrency and BillingCostBasis (which include read-only
fields like id and created_at); change the API contract to use dedicated
create-only types that exclude read-only fields by defining new TypeSpec create
types (e.g., CreateCustomCurrencyRequest and CreateCostBasisRequest) and update
the generated aliases to point to these new types instead of
BillingCustomCurrency/BillingCostBasis; then regenerate api.gen.go so
CreateCustomCurrencyJSONRequestBody -> CreateCustomCurrencyRequest and
CreateCostBasisJSONRequestBody -> CreateCostBasisRequest, ensuring
server-generated fields remain omitted from create payloads.
api/spec/src/v3/currencies/base.tsp (1)

1-4: Add explicit import "@typespec/openapi" for clarity

base.tsp uses the TypeSpec.OpenAPI namespace but only imports @typespec/openapi3. While this currently works because currency.tsp (which does import @typespec/openapi) depends on this file, it creates an implicit transitive dependency. Making the import explicit would make the file self-contained and clearer about its actual dependencies.

Suggested fix
+import "@typespec/openapi";
 import "@typespec/openapi3";
 import "../shared/index.tsp";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/spec/src/v3/currencies/base.tsp` around lines 1 - 4, base.tsp relies on
the TypeSpec.OpenAPI namespace but only imports "@typespec/openapi3", creating a
transitive dependency; fix it by adding an explicit top-level import of
"@typespec/openapi" in base.tsp so the file is self-contained and still uses
using TypeSpec.OpenAPI; ensure the new import appears alongside the existing
imports before the using statement.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/spec/src/v3/currencies/base.tsp`:
- Line 12: The `@friendlyName` on CurrencyBase is misleadingly named
"BillingCustomCurrencyBase" even though both Currency and CustomCurrency extend
it; rename the decorator value to "BillingCurrencyBase" on the CurrencyBase type
(update the `@friendlyName`("BillingCustomCurrencyBase") to
`@friendlyName`("BillingCurrencyBase")) and then update all references/usages of
that friendly name elsewhere (tests, docs, or type refs) to the new
"BillingCurrencyBase" so names remain consistent with the shared base used by
Currency and CustomCurrency.
- Around line 26-30: The currency "symbol" field lacks length constraints which
can lead to DB/UI issues; update the symbol declaration in base.tsp (the symbol
field with `@visibility`(Lifecycle.Create, Lifecycle.Read)) to include length
decorators, e.g. add `@minLength`(1) and `@maxLength`(3) (or another agreed upper
bound) so the symbol is constrained to 1–3 characters to match typical currency
symbols.

In `@api/spec/src/v3/currencies/cost-bases/cost-basis.tsp`:
- Around line 14-18: The docstring for fiat_code is misleading—it's a fiat
currency code (e.g., "USD") not the parent custom currency ID—so update the
comment on fiat_code to clearly state its purpose (e.g., "The fiat currency code
for the exchange rate" or "The fiat currency code (e.g., 'USD') used for the
exchange rate") and optionally mention that the parent custom currency relation
is provided via the currencyId path parameter; modify the comment above
fiat_code in the cost-basis type accordingly.

In `@api/spec/src/v3/currencies/cost-bases/operations.tsp`:
- Around line 20-22: Update the doc comment to match the operation and return
type: change the comment for the get_cost_bases operation from "Get a cost
basis." to a plural form like "Get cost bases." to reflect that get_cost_bases
returns CostBases (a list); locate the comment above the get_cost_bases
operation in operations.tsp and make the wording consistent with the operation
name and return type.

In `@api/spec/src/v3/currencies/currency.tsp`:
- Around line 18-23: The doc comment for the `code` field is misleading because
`code: Shared.CurrencyCode | CustomCurrencyCode` can be a custom code when
`is_custom` is true; update the comment on `code` to state that it is an ISO
4217 currency code for standard currencies (Shared.CurrencyCode) or a custom
currency code when `is_custom` is true (CustomCurrencyCode), so API consumers
understand both cases and the relation to the `is_custom` flag.

In `@api/spec/src/v3/currencies/operations.tsp`:
- Around line 18-24: CurrenciesResponse currently only has `@pageItems` data:
Currency[] and no pagination meta or list query params; align it with other list
endpoints by adding a meta: PaginatedMeta (or Meta PaginatedMeta) field to the
CurrenciesResponse model and update the corresponding list operation to accept
the same page query params used by other endpoints so responses include
pagination metadata; if returning the full set is intentional instead, add a
short comment on the CurrenciesResponse model explaining that the currency list
is intentionally non-paginated to prevent future accidental changes.

In `@api/v3/api.gen.go`:
- Around line 882-885: The generated comment for FiatCode is incorrect: update
the upstream TypeSpec for the cost-basis spec (the field named FiatCode of type
CurrencyCode) so its doc/comment describes a currency code (e.g., "The currency
code this cost basis belongs to, e.g. 'USD' or 'EUR'") instead of "currency id",
then re-run the code generator to update api.gen.go; target the TypeSpec field
named FiatCode (type CurrencyCode) when making the change and regenerate
artifacts.

In `@openmeter/ent/schema/custom_currencies.go`:
- Around line 27-50: Add a unique database index on the currency code by
implementing the CustomCurrency Indexes() method: in the ent schema for
CustomCurrency, add an Indexes() []ent.Index that returns a unique index (e.g.,
index.Unique("custom_currency_code_idx").Fields("code")) so the "code" field
(defined in Fields()) is enforced unique at the DB level; reference the
CustomCurrency type and the "code" field when adding the index.

In `@tools/migrate/migrations/20260220154446_add_custom_currency.up.sql`:
- Around line 2-11: Add a unique constraint on the custom_currencies.code
column: update the Ent schema field for "code" to include .Unique() (so the
generated migration will create a unique index) and regenerate the migration;
then update the SQL migration to create a UNIQUE index on "code" in the
"custom_currencies" table instead of leaving it non-unique (ensure the index
name is distinct, e.g., customcurrency_code). This fixes duplicates by enforcing
uniqueness at the DB level for the code column referenced by the schema field
"code".

---

Nitpick comments:
In `@api/spec/src/v3/currencies/base.tsp`:
- Around line 1-4: base.tsp relies on the TypeSpec.OpenAPI namespace but only
imports "@typespec/openapi3", creating a transitive dependency; fix it by adding
an explicit top-level import of "@typespec/openapi" in base.tsp so the file is
self-contained and still uses using TypeSpec.OpenAPI; ensure the new import
appears alongside the existing imports before the using statement.

In `@api/spec/src/v3/currencies/cost-bases/cost-basis.tsp`:
- Around line 9-12: The model CostBasis in the Currencies namespace has a
`@friendlyName` of "BillingCostBasis", creating a naming mismatch; confirm intent
and if unintended, update the `@friendlyName` to match the model/domain (e.g.,
"CurrenciesCostBasis" or "CostBasis") or move/rename the model to the Billing
domain; locate the CostBasis model declaration and the `@friendlyName` attribute
and make the name consistent with the intended generated-code prefix.

In `@api/spec/src/v3/currencies/custom.tsp`:
- Line 1: Add an explicit import of the base file in custom.tsp so the
dependencies are obvious: update api/spec/src/v3/currencies/custom.tsp to import
"./base.tsp" in addition to "../shared/index.tsp" so types used in this file
(CurrencyBase and CustomCurrencyCode) are directly referenced; ensure the import
is placed at the top of the file before any usage of CurrencyBase or
CustomCurrencyCode.

In `@api/spec/src/v3/currencies/operations.tsp`:
- Around line 40-43: Inside the Currencies namespace the create method's body
type uses a redundant qualified name; change the parameter type from
Currencies.CustomCurrency to CustomCurrency in the create(...) signature (the
method named create in this file), leaving the return type
Shared.CreateResponse<CustomCurrency> unchanged so the local type is used
without the unnecessary Currencies. prefix.

In `@api/v3/api.gen.go`:
- Around line 2048-2052: The request body aliases
CreateCustomCurrencyJSONRequestBody and CreateCostBasisJSONRequestBody currently
point to BillingCustomCurrency and BillingCostBasis (which include read-only
fields like id and created_at); change the API contract to use dedicated
create-only types that exclude read-only fields by defining new TypeSpec create
types (e.g., CreateCustomCurrencyRequest and CreateCostBasisRequest) and update
the generated aliases to point to these new types instead of
BillingCustomCurrency/BillingCostBasis; then regenerate api.gen.go so
CreateCustomCurrencyJSONRequestBody -> CreateCustomCurrencyRequest and
CreateCostBasisJSONRequestBody -> CreateCostBasisRequest, ensuring
server-generated fields remain omitted from create payloads.

In `@api/v3/server/routes.go`:
- Around line 143-159: These route methods currently delegate to unimplemented
stubs; add a concrete handler field (e.g., currenciesHandler) to the Server
struct and replace the unimplemented calls in ListCurrencies,
CreateCustomCurrency, CreateCostBasis, and GetCostBases to delegate to
s.currenciesHandler.ListCurrencies, s.currenciesHandler.CreateCustomCurrency,
s.currenciesHandler.CreateCostBasis, and s.currenciesHandler.GetCostBases
respectively (matching the existing pattern used for s.metersHandler and
s.billingProfilesHandler), and ensure the Server constructor/initializer wires
an instance of the new currenciesHandler so the routes call real implementations
instead of unimplemented.*.

In `@openmeter/ent/schema/custom_currencies.go`:
- Around line 17-25: You added a new TimeMixin with TimeMixin.Fields() that
defines the created_at field, but created_at is still duplicated in other
schemas (e.g., customer.go and notification.go); update those schema files to
remove their local created_at field declarations and instead include the
TimeMixin in their mixins() by referencing TimeMixin (or embed mixin.Schema
where appropriate) so the created_at column is defined centrally by TimeMixin
and not duplicated across Customer and Notification schema definitions.

In `@tools/migrate/migrations/20260220154446_add_custom_currency.up.sql`:
- Around line 10-11: Remove the redundant UNIQUE indexes on the primary key
columns: delete the CREATE UNIQUE INDEX "customcurrency_id" ON
"custom_currencies" ("id"); statement (and the similar duplicate around lines
25-26) from the migration so you don't create a second unique index on the PK;
ensure there are no other statements depending on those index names and
re-generate or re-run the migration after confirming the primary key constraint
alone provides the necessary uniqueness.

Comment thread api/spec/src/v3/currencies/base.tsp Outdated
@maxLength(24)
scalar CustomCurrencyCode extends string;

@friendlyName("BillingCustomCurrencyBase")
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

@friendlyName("BillingCustomCurrencyBase") is a bit misleading

Both Currency and CustomCurrency spread CurrencyBase, so calling it BillingCustomCurrencyBase implies it's custom-only when it's actually the shared base for all currencies. BillingCurrencyBase would be clearer.

✨ Suggested fix
-@friendlyName("BillingCustomCurrencyBase")
+@friendlyName("BillingCurrencyBase")
 model CurrencyBase {
📝 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
@friendlyName("BillingCustomCurrencyBase")
`@friendlyName`("BillingCurrencyBase")
model CurrencyBase {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/spec/src/v3/currencies/base.tsp` at line 12, The `@friendlyName` on
CurrencyBase is misleadingly named "BillingCustomCurrencyBase" even though both
Currency and CustomCurrency extend it; rename the decorator value to
"BillingCurrencyBase" on the CurrencyBase type (update the
`@friendlyName`("BillingCustomCurrencyBase") to
`@friendlyName`("BillingCurrencyBase")) and then update all references/usages of
that friendly name elsewhere (tests, docs, or type refs) to the new
"BillingCurrencyBase" so names remain consistent with the shared base used by
Currency and CustomCurrency.

Comment thread api/spec/src/v3/currencies/base.tsp Outdated
Comment on lines +26 to +30
/**
* Symbol representation of the currency.
*/
@visibility(Lifecycle.Create, Lifecycle.Read)
symbol: string;
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

symbol field is missing a length constraint

name has @maxLength(256) but symbol has no bounds at all. Currency symbols are typically 1–3 characters — an unbounded string could cause surprises at the DB layer or in UI rendering. At minimum a reasonable @maxLength would be nice.

✨ Suggested fix
   /**
    * Symbol representation of the currency.
    */
   `@visibility`(Lifecycle.Create, Lifecycle.Read)
+  `@minLength`(1)
+  `@maxLength`(10)
   symbol: string;
📝 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
/**
* Symbol representation of the currency.
*/
@visibility(Lifecycle.Create, Lifecycle.Read)
symbol: string;
/**
* Symbol representation of the currency.
*/
`@visibility`(Lifecycle.Create, Lifecycle.Read)
`@minLength`(1)
`@maxLength`(10)
symbol: string;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/spec/src/v3/currencies/base.tsp` around lines 26 - 30, The currency
"symbol" field lacks length constraints which can lead to DB/UI issues; update
the symbol declaration in base.tsp (the symbol field with
`@visibility`(Lifecycle.Create, Lifecycle.Read)) to include length decorators,
e.g. add `@minLength`(1) and `@maxLength`(3) (or another agreed upper bound) so the
symbol is constrained to 1–3 characters to match typical currency symbols.

Comment thread api/spec/src/v3/currencies/cost-bases/cost-basis.tsp
Comment thread api/spec/src/v3/currencies/cost-bases/operations.tsp
Comment thread api/spec/src/v3/currencies/currency.tsp Outdated
Comment thread api/spec/src/v3/currencies/operations.tsp Outdated
Comment thread api/v3/api.gen.go
Comment thread tools/migrate/migrations/20260223180500_add_custom_currency.up.sql
@mark-vass-konghq mark-vass-konghq changed the base branch from main to feat/currencies-v3-api-spec February 23, 2026 16:08
@mark-vass-konghq mark-vass-konghq force-pushed the feat/currencies-v3-api-ent branch from 2014854 to e92a87a Compare February 23, 2026 16:21
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

♻️ Duplicate comments (1)
openmeter/ent/schema/custom_currencies.go (1)

27-50: CustomCurrency still missing a unique index on code.

Without a unique constraint on code, duplicate currency codes can be inserted, which would undermine data integrity. This was flagged in a prior review and still needs to be addressed.

Suggested fix
 func (CustomCurrency) Edges() []ent.Edge {
 	return []ent.Edge{
 		edge.To("cost_basis_history", CurrencyCostBasis.Type),
 	}
 }
+
+func (CustomCurrency) Indexes() []ent.Index {
+	return []ent.Index{
+		index.Fields("code").Unique(),
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/ent/schema/custom_currencies.go` around lines 27 - 50, The schema
lacks a unique index for the "code" field on CustomCurrency, so add an Indexes()
method on the CustomCurrency schema that returns an ent.Index built with
index.Fields("code").Unique() (i.e., implement func (CustomCurrency) Indexes()
[]ent.Index { return []ent.Index{ index.Fields("code").Unique() } }), keeping
the existing Fields() definition for "code" intact so duplicate currency codes
are prevented.
🤖 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/ent/schema/custom_currencies.go`:
- Around line 27-50: CustomCurrency and CurrencyCostBasis need tenant scoping:
add entutils.NamespaceMixin{} to the Mixin() return for both schemas (in
addition to existing entutils.IDMixin{} and TimeMixin{}) and add compound
indexes on the namespace field plus the unique identifying fields following the
pattern used in Subject, Subscription, Meter (e.g., include "namespace" in any
unique/index definitions for code/name on CustomCurrency and the relevant fields
on CurrencyCostBasis) so tenant isolation is enforced; update the Edges/Fields'
schema files for CustomCurrency and CurrencyCostBasis accordingly.

---

Duplicate comments:
In `@openmeter/ent/schema/custom_currencies.go`:
- Around line 27-50: The schema lacks a unique index for the "code" field on
CustomCurrency, so add an Indexes() method on the CustomCurrency schema that
returns an ent.Index built with index.Fields("code").Unique() (i.e., implement
func (CustomCurrency) Indexes() []ent.Index { return []ent.Index{
index.Fields("code").Unique() } }), keeping the existing Fields() definition for
"code" intact so duplicate currency codes are prevented.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2014854 and e92a87a.

⛔ Files ignored due to path filters (26)
  • go.sum is excluded by !**/*.sum, !**/*.sum
  • openmeter/ent/db/client.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis/currencycostbasis.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency/customcurrency.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/ent.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/tx.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (3)
  • openmeter/ent/schema/custom_currencies.go
  • tools/migrate/migrations/20260220154446_add_custom_currency.down.sql
  • tools/migrate/migrations/20260220154446_add_custom_currency.up.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • tools/migrate/migrations/20260220154446_add_custom_currency.up.sql

Comment thread openmeter/ent/schema/custom_currencies.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: 2

♻️ Duplicate comments (1)
tools/migrate/migrations/20260223180500_add_custom_currency.up.sql (1)

5-5: code still missing a unique constraint — this was flagged in a previous review round and doesn't appear to have been addressed yet.

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

In `@tools/migrate/migrations/20260223180500_add_custom_currency.up.sql` at line
5, The migration creates the custom_currency table but the code column lacks a
unique constraint; update the migration to enforce uniqueness by either changing
the column definition to "code" character varying NOT NULL UNIQUE or adding an
explicit statement like ALTER TABLE custom_currency ADD CONSTRAINT
custom_currency_code_key UNIQUE (code), ensuring the constraint name is
descriptive and the change is applied in this up migration.
🤖 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/20260223180500_add_custom_currency.up.sql`:
- Around line 10-11: The migration adds redundant UNIQUE indexes on primary key
columns; remove the CREATE UNIQUE INDEX statements for "customcurrency_id" on
table "custom_currencies" and the similar "currencycostbasis_id" index (lines
referenced) so you don't duplicate the automatically-created PK indexes and
avoid extra write overhead; locate and delete the CREATE UNIQUE INDEX
"customcurrency_id" ... ("id") and the CREATE UNIQUE INDEX
"currencycostbasis_id" ... statements from the migration file, leaving the
PRIMARY KEY definitions intact.
- Around line 19-24: The foreign key column custom_currency_cost_basis_history
on table currency_cost_bases lacks a dedicated index, causing sequential scans
when checking/deleting referenced rows in custom_currencies; add a standalone
index on currency_cost_bases(custom_currency_cost_basis_history) (e.g., name it
currencycostbasis_custom_currency_cost_basis_history_idx) so FK lookups and
cascades can use an index rather than the existing composite index
currencycostbasis_fiat_code_effective_from_custom_currency_cost.

---

Duplicate comments:
In `@tools/migrate/migrations/20260223180500_add_custom_currency.up.sql`:
- Line 5: The migration creates the custom_currency table but the code column
lacks a unique constraint; update the migration to enforce uniqueness by either
changing the column definition to "code" character varying NOT NULL UNIQUE or
adding an explicit statement like ALTER TABLE custom_currency ADD CONSTRAINT
custom_currency_code_key UNIQUE (code), ensuring the constraint name is
descriptive and the change is applied in this up migration.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e92a87a and c7f5eb5.

⛔ Files ignored due to path filters (1)
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (2)
  • tools/migrate/migrations/20260223180500_add_custom_currency.down.sql
  • tools/migrate/migrations/20260223180500_add_custom_currency.up.sql

Comment on lines +10 to +11
-- create index "customcurrency_id" to table: "custom_currencies"
CREATE UNIQUE INDEX "customcurrency_id" ON "custom_currencies" ("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

Redundant unique indexes on id — these are already covered by the PRIMARY KEY.

PostgreSQL automatically creates a unique index when a PRIMARY KEY is defined; there's no need to manually create indexes on those columns, as doing so would just duplicate the automatically-created index. Both customcurrency_id and currencycostbasis_id indexes can be dropped — they add write overhead without any benefit.

🗑️ Suggested fix
 -- create index "customcurrency_id" to table: "custom_currencies"
-CREATE UNIQUE INDEX "customcurrency_id" ON "custom_currencies" ("id");
 -- create index "currencycostbasis_id" to table: "currency_cost_bases"
-CREATE UNIQUE INDEX "currencycostbasis_id" ON "currency_cost_bases" ("id");

Also applies to: 25-26

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

In `@tools/migrate/migrations/20260223180500_add_custom_currency.up.sql` around
lines 10 - 11, The migration adds redundant UNIQUE indexes on primary key
columns; remove the CREATE UNIQUE INDEX statements for "customcurrency_id" on
table "custom_currencies" and the similar "currencycostbasis_id" index (lines
referenced) so you don't duplicate the automatically-created PK indexes and
avoid extra write overhead; locate and delete the CREATE UNIQUE INDEX
"customcurrency_id" ... ("id") and the CREATE UNIQUE INDEX
"currencycostbasis_id" ... statements from the migration file, leaving the
PRIMARY KEY definitions intact.

Comment on lines +19 to +24
"custom_currency_cost_basis_history" character(26) NOT NULL,
PRIMARY KEY ("id"),
CONSTRAINT "currency_cost_bases_custom_currencies_cost_basis_history" FOREIGN KEY ("custom_currency_cost_basis_history") REFERENCES "custom_currencies" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
);
-- create index "currencycostbasis_fiat_code_effective_from_custom_currency_cost" to table: "currency_cost_bases"
CREATE UNIQUE INDEX "currencycostbasis_fiat_code_effective_from_custom_currency_cost" ON "currency_cost_bases" ("fiat_code", "effective_from", "custom_currency_cost_basis_history");
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

Missing index on the FK column custom_currency_cost_basis_history.

The only index that covers this column is the composite one on (fiat_code, effective_from, custom_currency_cost_basis_history) where it's the third element. PostgreSQL can't use that index for FK enforcement or lookups filtered solely by custom_currency_cost_basis_history, resulting in a sequential scan on currency_cost_bases when the parent custom_currencies row is checked. Worth adding a dedicated index:

⚡ Suggested fix
+-- create index on FK column "custom_currency_cost_basis_history"
+CREATE INDEX "currencycostbasis_custom_currency" ON "currency_cost_bases" ("custom_currency_cost_basis_history");
📝 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
"custom_currency_cost_basis_history" character(26) NOT NULL,
PRIMARY KEY ("id"),
CONSTRAINT "currency_cost_bases_custom_currencies_cost_basis_history" FOREIGN KEY ("custom_currency_cost_basis_history") REFERENCES "custom_currencies" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
);
-- create index "currencycostbasis_fiat_code_effective_from_custom_currency_cost" to table: "currency_cost_bases"
CREATE UNIQUE INDEX "currencycostbasis_fiat_code_effective_from_custom_currency_cost" ON "currency_cost_bases" ("fiat_code", "effective_from", "custom_currency_cost_basis_history");
"custom_currency_cost_basis_history" character(26) NOT NULL,
PRIMARY KEY ("id"),
CONSTRAINT "currency_cost_bases_custom_currencies_cost_basis_history" FOREIGN KEY ("custom_currency_cost_basis_history") REFERENCES "custom_currencies" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
);
-- create index "currencycostbasis_fiat_code_effective_from_custom_currency_cost" to table: "currency_cost_bases"
CREATE UNIQUE INDEX "currencycostbasis_fiat_code_effective_from_custom_currency_cost" ON "currency_cost_bases" ("fiat_code", "effective_from", "custom_currency_cost_basis_history");
-- create index on FK column "custom_currency_cost_basis_history"
CREATE INDEX "currencycostbasis_custom_currency" ON "currency_cost_bases" ("custom_currency_cost_basis_history");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/migrate/migrations/20260223180500_add_custom_currency.up.sql` around
lines 19 - 24, The foreign key column custom_currency_cost_basis_history on
table currency_cost_bases lacks a dedicated index, causing sequential scans when
checking/deleting referenced rows in custom_currencies; add a standalone index
on currency_cost_bases(custom_currency_cost_basis_history) (e.g., name it
currencycostbasis_custom_currency_cost_basis_history_idx) so FK lookups and
cascades can use an index rather than the existing composite index
currencycostbasis_fiat_code_effective_from_custom_currency_cost.

@mark-vass-konghq mark-vass-konghq force-pushed the feat/currencies-v3-api-ent branch from c7f5eb5 to 9e5ab00 Compare February 24, 2026 13:53
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.

♻️ Duplicate comments (1)
openmeter/ent/schema/custom_currencies.go (1)

31-60: ⚠️ Potential issue | 🟠 Major

Missing tenant scoping on both schemas (repeat).
Still no NamespaceMixin or namespace-aware indexes here, so cross-tenant isolation can’t be enforced. This mirrors the earlier review note; please add entutils.NamespaceMixin{} and include namespace in unique/index definitions for both schemas.

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

In `@openmeter/ent/schema/custom_currencies.go` around lines 31 - 60, Add tenant
scoping by including entutils.NamespaceMixin{} in the Mixin() results for both
CustomCurrency and CurrencyCostBasis (update CustomCurrency.Mixin and
CurrencyCostBasis.Mixin). Also modify the unique/index definitions so they are
namespace-aware: make indexes/unique constraints include the namespace column
together with the entity key(s) (e.g., combine namespace with the "code" field
in CustomCurrency and with whatever identifying fields in CurrencyCostBasis) so
each constraint is enforced per-tenant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@openmeter/ent/schema/custom_currencies.go`:
- Around line 31-60: Add tenant scoping by including entutils.NamespaceMixin{}
in the Mixin() results for both CustomCurrency and CurrencyCostBasis (update
CustomCurrency.Mixin and CurrencyCostBasis.Mixin). Also modify the unique/index
definitions so they are namespace-aware: make indexes/unique constraints include
the namespace column together with the entity key(s) (e.g., combine namespace
with the "code" field in CustomCurrency and with whatever identifying fields in
CurrencyCostBasis) so each constraint is enforced per-tenant.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7f5eb5 and 9e5ab00.

⛔ Files ignored due to path filters (26)
  • go.sum is excluded by !**/*.sum, !**/*.sum
  • openmeter/ent/db/client.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis/currencycostbasis.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency/customcurrency.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/customcurrency_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/ent.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/tx.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (3)
  • openmeter/ent/schema/custom_currencies.go
  • tools/migrate/migrations/20260223180500_add_custom_currency.down.sql
  • tools/migrate/migrations/20260223180500_add_custom_currency.up.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • tools/migrate/migrations/20260223180500_add_custom_currency.up.sql

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

♻️ Duplicate comments (1)
openmeter/ent/schema/custom_currencies.go (1)

31-35: ⚠️ Potential issue | 🟠 Major

Add tenant scoping (NamespaceMixin + namespace‑scoped uniqueness).

Right now these schemas look tenant‑agnostic, which can lead to cross‑namespace collisions and unindexed tenant filters. I’d add entutils.NamespaceMixin{} and scope uniqueness/indexes by namespace (matching the existing multi‑tenant patterns elsewhere).

🔧 Possible direction
 func (CustomCurrency) Mixin() []ent.Mixin {
 	return []ent.Mixin{
 		entutils.IDMixin{},
+		entutils.NamespaceMixin{},
 		TimeMixin{},
 	}
 }

 func (CustomCurrency) Fields() []ent.Field {
 	return []ent.Field{
-		field.String("code").NotEmpty().MinLen(3).MaxLen(24).Unique().Immutable(),
+		field.String("code").NotEmpty().MinLen(3).MaxLen(24).Immutable(),
 		field.String("name").NotEmpty().MaxLen(100).Immutable(),
 		field.String("symbol").NotEmpty().MaxLen(10).Immutable(),
 	}
 }

+func (CustomCurrency) Indexes() []ent.Index {
+	return []ent.Index{
+		index.Fields("namespace", "code").Unique(),
+	}
+}
 
 func (CurrencyCostBasis) Mixin() []ent.Mixin {
 	return []ent.Mixin{
 		entutils.IDMixin{},
+		entutils.NamespaceMixin{},
 		TimeMixin{},
 	}
 }

 func (CurrencyCostBasis) Indexes() []ent.Index {
 	return []ent.Index{
-		index.Edges("currency").Fields("fiat_code").Unique(),
+		index.Edges("currency").Fields("namespace", "fiat_code").Unique(),
 	}
 }

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

Also applies to: 38-40, 56-60, 83-86

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

In `@openmeter/ent/schema/custom_currencies.go` around lines 31 - 35, Add tenant
scoping to the CustomCurrency ent schema by updating the CustomCurrency Mixin to
include entutils.NamespaceMixin{} (i.e., return entutils.IDMixin{}, TimeMixin{},
entutils.NamespaceMixin{}), and update any schema-level uniqueness/index
declarations in the CustomCurrency schema (and the other listed schemas at lines
~38-40, 56-60, 83-86) to include the namespace field so indexes/unique
constraints are namespace-scoped (e.g., make unique/index definitions include
"namespace" plus the existing fields) to prevent cross-namespace collisions and
allow efficient tenant filtering.
🧹 Nitpick comments (2)
tools/migrate/migrations/20260224135901_add_effective_from_schema.up.sql (1)

8-17: Avoid redundant PK index and add an FK index.
The PK already creates a unique index on id, so the extra unique index is redundant. Also, indexing currency_cost_basis_effective_from_history will help joins and deletes.

Suggested index tweak
- CREATE UNIQUE INDEX "currencycostbasiseffectivefrom_id" ON "currency_cost_basis_effective_froms" ("id");
+ CREATE INDEX "currencycostbasiseffectivefrom_history_idx"
+   ON "currency_cost_basis_effective_froms" ("currency_cost_basis_effective_from_history");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/migrate/migrations/20260224135901_add_effective_from_schema.up.sql`
around lines 8 - 17, Remove the redundant unique index
"currencycostbasiseffectivefrom_id" since PRIMARY KEY on "id" in table
"currency_cost_basis_effective_froms" already creates that unique index; instead
add a non-unique index on the foreign key column
"currency_cost_basis_effective_from_history" to speed joins/deletes (create
index on "currency_cost_basis_effective_froms"
("currency_cost_basis_effective_from_history")). Ensure the CREATE UNIQUE INDEX
line is deleted and replace with a CREATE INDEX targeting the FK column.
openmeter/ent/schema/custom_currencies.go (1)

21-24: Prefer clock.Now for created_at defaults.

Using time.Now here bypasses the project’s time‑freeze/drift logic. Swapping to clock.Now keeps testability and time behavior consistent.

🕒 Suggested tweak
-import (
-	"time"
+import (
 	"entgo.io/ent"
 	"entgo.io/ent/dialect"
 	"entgo.io/ent/schema/edge"
 	"entgo.io/ent/schema/field"
 	"entgo.io/ent/schema/index"
 	"entgo.io/ent/schema/mixin"
 	"github.com/alpacahq/alpacadecimal"

+	"github.com/openmeterio/openmeter/pkg/clock"
 	"github.com/openmeterio/openmeter/pkg/framework/entutils"
 )
 
 func (TimeMixin) Fields() []ent.Field {
 	return []ent.Field{
-		field.Time("created_at").Default(time.Now).Immutable(),
+		field.Time("created_at").Default(clock.Now).Immutable(),
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/ent/schema/custom_currencies.go` around lines 21 - 24, The
TimeMixin.Fields implementation uses time.Now for the created_at default; change
the field default to use the project's clock helper (clock.Now) instead to
respect time-freeze/drift logic: update the default in TimeMixin.Fields (the
field.Time("created_at").Default(...) call) to call clock.Now and add the
appropriate import for the clock package where custom_currencies.go defines
TimeMixin so tests and runtime follow the centralized clock behavior.
🤖 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/ent/schema/custom_currencies.go`:
- Around line 63-69: The schema currently enforces a unique key on the
currency+fiat pair which blocks adding new historical rates; update the
CurrencyCostBasis schema to either remove that uniqueness or expand the unique
index to include effective_from so multiple rows per currency+fiat with
different effective_from are allowed; locate the uniqueness definition in the
CurrencyCostBasis Indexes() (or schema index block) and either drop the unique
constraint on (currency, fiat_code) or change it to (currency, fiat_code,
effective_from) and keep fiat_code, rate, and Immutable() fields as-is to
preserve types.

In `@tools/migrate/migrations/20260224135901_add_effective_from_schema.down.sql`:
- Around line 1-10: The down migration currently drops the history table then
adds a NOT NULL effective_from column which will fail or lose data; instead
modify the down migration to (1) add "effective_from" to currency_cost_bases as
nullable (ALTER TABLE ... ADD COLUMN "effective_from" timestamptz NULL), (2)
backfill effective_from from the history table
currency_cost_basis_effective_froms (UPDATE currency_cost_bases SET
effective_from = <choose intended value from
currency_cost_basis_effective_froms, e.g. the latest/earliest matching row>
using the PK/FK relationship), (3) set the column NOT NULL (ALTER TABLE ...
ALTER COLUMN "effective_from" SET NOT NULL) once all rows are populated, and
only then (4) DROP TABLE "currency_cost_basis_effective_froms" and DROP/CREATE
the related indexes ("currencycostbasiseffectivefrom_id",
"custom_currencies_code_key",
"currencycostbasis_fiat_code_custom_currency_cost_basis_history") as needed;
ensure the backfill uses the correct join keys between currency_cost_bases and
currency_cost_basis_effective_froms.

In `@tools/migrate/migrations/20260224135901_add_effective_from_schema.up.sql`:
- Around line 1-2: Before dropping the effective_from column, create the new
table currency_cost_basis_effective_froms and backfill it with all existing
effective_from values from currency_cost_bases (preserving ids and timestamps),
verify the migration by checking row counts and non-null values, then in a
subsequent step drop the effective_from column from currency_cost_bases;
specifically: add a CREATE TABLE for currency_cost_basis_effective_froms, run an
INSERT ... SELECT to copy currency_cost_bases.id, effective_from (and any
required metadata), validate the copied data, and only then run the ALTER TABLE
"currency_cost_bases" DROP COLUMN "effective_from".

---

Duplicate comments:
In `@openmeter/ent/schema/custom_currencies.go`:
- Around line 31-35: Add tenant scoping to the CustomCurrency ent schema by
updating the CustomCurrency Mixin to include entutils.NamespaceMixin{} (i.e.,
return entutils.IDMixin{}, TimeMixin{}, entutils.NamespaceMixin{}), and update
any schema-level uniqueness/index declarations in the CustomCurrency schema (and
the other listed schemas at lines ~38-40, 56-60, 83-86) to include the namespace
field so indexes/unique constraints are namespace-scoped (e.g., make
unique/index definitions include "namespace" plus the existing fields) to
prevent cross-namespace collisions and allow efficient tenant filtering.

---

Nitpick comments:
In `@openmeter/ent/schema/custom_currencies.go`:
- Around line 21-24: The TimeMixin.Fields implementation uses time.Now for the
created_at default; change the field default to use the project's clock helper
(clock.Now) instead to respect time-freeze/drift logic: update the default in
TimeMixin.Fields (the field.Time("created_at").Default(...) call) to call
clock.Now and add the appropriate import for the clock package where
custom_currencies.go defines TimeMixin so tests and runtime follow the
centralized clock behavior.

In `@tools/migrate/migrations/20260224135901_add_effective_from_schema.up.sql`:
- Around line 8-17: Remove the redundant unique index
"currencycostbasiseffectivefrom_id" since PRIMARY KEY on "id" in table
"currency_cost_basis_effective_froms" already creates that unique index; instead
add a non-unique index on the foreign key column
"currency_cost_basis_effective_from_history" to speed joins/deletes (create
index on "currency_cost_basis_effective_froms"
("currency_cost_basis_effective_from_history")). Ensure the CREATE UNIQUE INDEX
line is deleted and replace with a CREATE INDEX targeting the FK column.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e5ab00 and 3a0caf5.

⛔ Files ignored due to path filters (24)
  • openmeter/ent/db/client.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis/currencycostbasis.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasis_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasiseffectivefrom.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasiseffectivefrom/currencycostbasiseffectivefrom.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasiseffectivefrom/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasiseffectivefrom_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasiseffectivefrom_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasiseffectivefrom_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/currencycostbasiseffectivefrom_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/ent.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/tx.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (3)
  • openmeter/ent/schema/custom_currencies.go
  • tools/migrate/migrations/20260224135901_add_effective_from_schema.down.sql
  • tools/migrate/migrations/20260224135901_add_effective_from_schema.up.sql

Comment thread openmeter/ent/schema/custom_currencies.go
Comment on lines +1 to +10
-- reverse: create index "currencycostbasiseffectivefrom_id" to table: "currency_cost_basis_effective_froms"
DROP INDEX "currencycostbasiseffectivefrom_id";
-- reverse: create "currency_cost_basis_effective_froms" table
DROP TABLE "currency_cost_basis_effective_froms";
-- reverse: create index "custom_currencies_code_key" to table: "custom_currencies"
DROP INDEX "custom_currencies_code_key";
-- reverse: create index "currencycostbasis_fiat_code_custom_currency_cost_basis_history" to table: "currency_cost_bases"
DROP INDEX "currencycostbasis_fiat_code_custom_currency_cost_basis_history";
-- reverse: modify "currency_cost_bases" table
ALTER TABLE "currency_cost_bases" ADD COLUMN "effective_from" timestamptz NOT NULL;
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

Rollback will lose data and may fail on non-empty tables.
Dropping the history table before restoring effective_from, then adding a NOT NULL column with no default, will either lose data or fail if rows exist. Please add the column nullable, backfill from history (choose the intended value), set NOT NULL, then drop the history table.

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

In `@tools/migrate/migrations/20260224135901_add_effective_from_schema.down.sql`
around lines 1 - 10, The down migration currently drops the history table then
adds a NOT NULL effective_from column which will fail or lose data; instead
modify the down migration to (1) add "effective_from" to currency_cost_bases as
nullable (ALTER TABLE ... ADD COLUMN "effective_from" timestamptz NULL), (2)
backfill effective_from from the history table
currency_cost_basis_effective_froms (UPDATE currency_cost_bases SET
effective_from = <choose intended value from
currency_cost_basis_effective_froms, e.g. the latest/earliest matching row>
using the PK/FK relationship), (3) set the column NOT NULL (ALTER TABLE ...
ALTER COLUMN "effective_from" SET NOT NULL) once all rows are populated, and
only then (4) DROP TABLE "currency_cost_basis_effective_froms" and DROP/CREATE
the related indexes ("currencycostbasiseffectivefrom_id",
"custom_currencies_code_key",
"currencycostbasis_fiat_code_custom_currency_cost_basis_history") as needed;
ensure the backfill uses the correct join keys between currency_cost_bases and
currency_cost_basis_effective_froms.

Comment on lines +1 to +2
-- modify "currency_cost_bases" table
ALTER TABLE "currency_cost_bases" DROP COLUMN "effective_from";
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 | 🔴 Critical

Backfill before dropping effective_from to avoid data loss.
Dropping the column at Line 2 will wipe any existing effective_from values. Please create and backfill currency_cost_basis_effective_froms first, then drop the column once data is safely migrated.

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

In `@tools/migrate/migrations/20260224135901_add_effective_from_schema.up.sql`
around lines 1 - 2, Before dropping the effective_from column, create the new
table currency_cost_basis_effective_froms and backfill it with all existing
effective_from values from currency_cost_bases (preserving ids and timestamps),
verify the migration by checking row counts and non-null values, then in a
subsequent step drop the effective_from column from currency_cost_bases;
specifically: add a CREATE TABLE for currency_cost_basis_effective_froms, run an
INSERT ... SELECT to copy currency_cost_bases.id, effective_from (and any
required metadata), validate the copied data, and only then run the ALTER TABLE
"currency_cost_bases" DROP COLUMN "effective_from".

@mark-vass-konghq mark-vass-konghq force-pushed the feat/currencies-v3-api-ent branch 2 times, most recently from 588dc17 to 4223e19 Compare February 24, 2026 15:31
Base automatically changed from feat/currencies-v3-api-spec to main February 24, 2026 16:12
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.

We've merged the schema, did anything change or are these leftovers to be removed?

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.

No, it's just a missing rebase

@mark-vass-konghq mark-vass-konghq force-pushed the feat/currencies-v3-api-ent branch 2 times, most recently from 81218fb to 0c492f3 Compare February 26, 2026 08:47
Comment on lines +17 to +25
type TimeMixin struct {
mixin.Schema
}

func (TimeMixin) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").Default(time.Now).Immutable(),
}
}
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.

Use the built-in entutils.TimeMixin, even if updated_at and deleted_at are not exposed on the APIs.

Comment thread openmeter/ent/schema/custom_currencies.go

func (CurrencyCostBasisEffectiveFrom) Fields() []ent.Field {
return []ent.Field{
field.Time("effective_from").Immutable(),
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.

Let's move effective_from to the CurrencyCostBasis model for now, and later we can decide if we want to change it to this pattern.

@mark-vass-konghq mark-vass-konghq force-pushed the feat/currencies-v3-api-ent branch 3 times, most recently from 7514cba to c8b2e0d Compare March 2, 2026 09:27
@tothandras tothandras force-pushed the feat/currencies-v3-api-ent branch 2 times, most recently from 6717db1 to 8cfc1f3 Compare March 2, 2026 13:37
@mark-vass-konghq mark-vass-konghq force-pushed the feat/currencies-v3-api-ent branch 2 times, most recently from 6af5848 to 1553d1e Compare March 2, 2026 13:59
@mark-vass-konghq mark-vass-konghq force-pushed the feat/currencies-v3-api-ent branch from 1553d1e to 9a79c79 Compare March 3, 2026 08:44
Comment on lines +29 to +30
field.String("name").NotEmpty().MaxLen(100).Immutable(),
field.String("symbol").NotEmpty().MaxLen(10).Immutable(),
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.

Suggested change
field.String("name").NotEmpty().MaxLen(100).Immutable(),
field.String("symbol").NotEmpty().MaxLen(10).Immutable(),
field.String("name").NotEmpty(),
field.String("symbol").NotEmpty(),


func (CurrencyCostBasis) Fields() []ent.Field {
return []ent.Field{
field.String("fiat_code").NotEmpty().MinLen(3).MaxLen(3).Immutable(),
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 use something like this

field.String("currency").
			GoType(currencyx.Code("")).

return []ent.Edge{
edge.To("cost_basis_history", CurrencyCostBasis.Type),
}
}
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 add a unique index to prevent duplicate codes. Example:

index.Fields("namespace", "key").
			Annotations(
				entsql.IndexWhere("deleted_at IS NULL"),
			).
			Unique(),

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 add one more index at least on namespace, id UNIQUE

Comment thread openmeter/ent/schema/custom_currencies.go

func (CustomCurrency) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cost_basis_history", CurrencyCostBasis.Type),
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 add cascade deletion: Annotations(entsql.OnDelete(entsql.Cascade)),


func (CurrencyCostBasis) Indexes() []ent.Index {
return []ent.Index{
index.Edges("currency").Fields("fiat_code", "effective_from").Unique(),
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.

Suggested change
index.Edges("currency").Fields("fiat_code", "effective_from").Unique(),
index.Fields("namespace", "currency_code_id", "fiat_code", "effective_from").
Annotations(
entsql.IndexWhere("deleted_at IS NULL"),
).
Unique(),

@mark-vass-konghq mark-vass-konghq force-pushed the feat/currencies-v3-api-ent branch from 9a79c79 to 589747a Compare March 3, 2026 09:45
Comment thread openmeter/ent/schema/custom_currencies.go
@mark-vass-konghq mark-vass-konghq requested a review from turip March 3, 2026 10:02
@mark-vass-konghq mark-vass-konghq force-pushed the feat/currencies-v3-api-ent branch from 0393cba to 8587407 Compare March 3, 2026 10:13
@turip turip dismissed tothandras’s stale review March 3, 2026 10:16

Did a few more passes.

@turip turip enabled auto-merge (squash) March 3, 2026 10:16
@turip turip merged commit cff5aa9 into main Mar 3, 2026
25 checks passed
@turip turip deleted the feat/currencies-v3-api-ent branch March 3, 2026 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants