diff --git a/.agents/skills/currencyx/SKILL.md b/.agents/skills/currencyx/SKILL.md new file mode 100644 index 0000000000..59678e3c5a --- /dev/null +++ b/.agents/skills/currencyx/SKILL.md @@ -0,0 +1,91 @@ +--- +name: currencyx +description: Work on OpenMeter currency primitives in pkg/currencyx for fiat and custom currency codes, shared currency interfaces, rounding modes, calculators, allocation precision, fiat/custom boundaries, and callers in billing, charges, ledger, product catalog, subscriptions, API, or currency registry code. +--- + +# Currencyx + +Use this skill when changes touch `pkg/currencyx` or any caller that depends on currency code shape, fiat/custom classification, calculator behavior, rounding, allocation, or invoice/ledger currency boundaries. + +Also load the domain skill for each touched caller area: `billing`, `charges`, `ledger`, `subscription`, `api`, `ent`, `db-migration`, and `test`. + +## Source Of Truth + +- Source code: `pkg/currencyx/*.go`. +- Primary tests: `pkg/currencyx/*_test.go`. +- Package usage examples: `pkg/currencyx/README.md`. +- This skill is a how-to/reference for agents. Update it whenever `Currency`, `Code`, `CustomCurrency`, `Calculator`, rounding, allocation, or validation behavior changes. +- One canonical repo skill lives at `.agents/skills/currencyx`; do not create duplicate currencyx guidance elsewhere. + +## Package Layout + +- `currency.go`: currency type constants, the shared `Currency` interface, `Code`, `CustomCurrency`, and fiat/custom constructors. +- `validation.go`: code format validation, fiat collision checks, precision validation, `PostgresCodeSchemaType`, and `CustomCurrency.Validate`. +- `fiat.go`: rounding modes, `Calculator`, and precision helpers. +- `allocation.go`: deterministic largest-remainder allocation using calculator precision. +- `README.md`: short examples for fiat, custom, and allocation usage. + +## Boundary Model + +- **Currency code**: durable identifier. Fiat and custom codes use `currencyx.Code`, but validation differs by boundary. +- **Currency interface**: shared behavior contract. Callers that know a configured currency should expose `CurrencyCode()`, `CurrencyType()`, `CurrencyPrecision()`, and `CurrencyRoundingMode()`. +- **Fiat currency**: `currencyx.Code` implements `currencyx.Currency` as fiat. `Code.Calculator()` preserves existing fiat behavior and derives precision from GOBL/ISO definitions. +- **Custom currency**: `currencyx.CustomCurrency` implements `currencyx.Currency`. Custom currencies carry configured precision and rounding mode; missing rounding mode defaults to bankers rounding. +- **Calculator**: construct with `currencyx.NewCalculator(currencyx.Currency)`. The calculator must branch from `CurrencyType()` and use `CurrencyPrecision()` / `CurrencyRoundingMode()` from the interface for custom currencies. +- **Allocation**: use calculator precision for units and largest-remainder distribution. Do not reach into fiat-only `Def.Subunits` for allocation logic. +- **Validation**: `Code.Validate()` remains fiat semantic for existing callers. Use `ValidateFormat()` for structural code checks and `ValidateCustom()` for custom currency codes. Custom codes must not contain the `|` route delimiter. +- **Registry boundary**: owns custom currency definition, fiat-code collision checks, archive/activation rules, cost-basis history, and future persisted rounding configuration. +- **Cost basis history**: entries are effective-dated with `effective_from` and optional `effective_to`; API responses expose the cost-basis `id`. Use `currency_id` terminology in domain/schema models, even when the current route is under custom currencies. If renaming old `custom_currency_id` storage, use data-preserving migrations. +- **Finance boundary**: snapshots fiat basis and applies fiat rounding when custom units become fiat amounts. +- **Ledger boundary**: records durable currency codes and balanced single-currency legs. Round before posting only when the upstream domain owns normalization. +- **Invoice boundary**: invoice currency stays fiat. Custom units must be materialized to fiat before invoice artifacts. + +## Rounding Rules + +- Preserve fiat rounding unless the task explicitly changes fiat money behavior. +- Custom currency default rounding is `RoundingModeBankers` (`RoundBank`, half-even). +- Custom currencies can opt into `RoundingModeHalfAwayFromZero` through `NewCustomCurrencyWithRounding`. +- `Calculator.RoundToPrecision` is the single place that applies the effective rounding mode. +- `Calculator.RoundDown` and `Calculator.Unit` are precision helpers; they should not apply banker/half-away rounding. +- `Calculator.IsRoundedToPrecision` must use `RoundToPrecision`, so it follows the configured rounding mode. + +## Process + +1. Name the surface before editing: code validation, type/interface, rounding, calculator, allocation, registry, ledger fact, fiat materialization, or invoice boundary. +2. Keep `pkg/currencyx` free of imports from `openmeter/...`; callers can implement `currencyx.Currency` to supply registry-backed custom settings. +3. Prefer `CurrencyType()` at the boundary that truly requires fiat or custom. Do not add broad split helpers unless the caller boundary needs a named domain rule. +4. Preserve `currencyx.Code(...).Calculator()` for existing fiat callers. +5. For custom currencies, validate structural code, route delimiter exclusion, fiat-code collisions, precision, and rounding mode. +6. Keep allocation deterministic: precision defines units, largest remainder distributes residual units, and tie-breakers remain stable. +7. After editing, run focused `pkg/currencyx` tests, `go vet`, and caller tests or compile checks for every touched boundary. + +## Test Checklist + +Cover the named risk introduced by the change: + +- Fiat regression behavior: code validation, precision from ISO definition, and existing rounding. +- Validation boundaries: `Code.Validate()` stays fiat-only while `ValidateFormat()` accepts structurally valid custom codes. +- Custom interface behavior: code, type, precision, and rounding mode all flow through `currencyx.Currency`. +- Banker ties: positive, negative, and zero-precision custom rounding tie to even. +- Alternate custom rounding: half-away-from-zero remains selectable and tested. +- Invalid config: bad precision or rounding mode fails validation. +- Allocation precision: custom precision affects units and largest-remainder allocation. +- Boundary tests: billing/invoice rejects custom invoice currency explicitly; ledger accepts structurally valid custom codes only when that domain supports them. + +Focused commands: + +```bash +env GOCACHE=/private/tmp/openmeter-go-build go test ./pkg/currencyx +env GOCACHE=/private/tmp/openmeter-go-build go vet ./pkg/currencyx +``` + +For caller compile checks, keep the package list scoped to touched boundaries and include `-tags=dynamic` when billing/ledger paths require it. + +## Review Checks + +- Fiat and custom currencies share the `currencyx.Currency` interface. +- `Calculator.RoundToPrecision` applies the effective rounding rule. +- `Calculator` does not require fiat definitions for custom currencies. +- Allocation code uses calculator methods, not fiat-only definition fields. +- Invalid rounding precision or mode fails validation. +- Tests cover banker ties, configured custom precision, fiat regression behavior, invalid rounding config, and allocation precision. diff --git a/.agents/skills/currencyx/agents/openai.yaml b/.agents/skills/currencyx/agents/openai.yaml new file mode 100644 index 0000000000..bbd2d30137 --- /dev/null +++ b/.agents/skills/currencyx/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Currencyx" + short_description: "Work on fiat and custom currency primitives" + default_prompt: "Use $currencyx to update OpenMeter currency code, rounding, calculator, allocation, or fiat/custom boundary behavior." diff --git a/api/spec/packages/aip-client-javascript/src/models/schemas.ts b/api/spec/packages/aip-client-javascript/src/models/schemas.ts index 058e7a5d34..8b91457306 100644 --- a/api/spec/packages/aip-client-javascript/src/models/schemas.ts +++ b/api/spec/packages/aip-client-javascript/src/models/schemas.ts @@ -1782,6 +1782,7 @@ export const costBasis = z fiat_code: currencyCode, rate: numeric, effective_from: dateTime.optional(), + effective_to: dateTime.optional(), created_at: dateTime, }) .describe('Describes currency basis supported by billing system.') @@ -1791,6 +1792,7 @@ export const createCostBasisRequest = z fiat_code: currencyCode, rate: numeric, effective_from: dateTime.optional(), + effective_to: dateTime.optional(), }) .describe('CostBasis create request.') diff --git a/api/spec/packages/aip-client-javascript/src/models/types.ts b/api/spec/packages/aip-client-javascript/src/models/types.ts index 64e04e7681..2130cc5491 100644 --- a/api/spec/packages/aip-client-javascript/src/models/types.ts +++ b/api/spec/packages/aip-client-javascript/src/models/types.ts @@ -968,6 +968,12 @@ export interface CostBasis { * `now` by the system. */ effective_from?: string + /** + * An ISO-8601 timestamp representation of the date until which the cost basis is + * effective. If provided, it must be later than `effective_from`. If not provided, + * it remains effective until superseded. + */ + effective_to?: string /** An ISO-8601 timestamp representation of entity creation date. */ created_at: string } @@ -984,6 +990,12 @@ export interface CreateCostBasisRequest { * `now` by the system. */ effective_from?: string + /** + * An ISO-8601 timestamp representation of the date until which the cost basis is + * effective. If provided, it must be later than `effective_from`. If not provided, + * it remains effective until superseded. + */ + effective_to?: string } /** A row in the result of a feature cost query. */ diff --git a/api/spec/packages/aip/src/currencies/cost-bases/cost-basis.tsp b/api/spec/packages/aip/src/currencies/cost-bases/cost-basis.tsp index 23856b37ea..6fa5416683 100644 --- a/api/spec/packages/aip/src/currencies/cost-bases/cost-basis.tsp +++ b/api/spec/packages/aip/src/currencies/cost-bases/cost-basis.tsp @@ -31,6 +31,14 @@ model CostBasis { @visibility(Lifecycle.Create, Lifecycle.Read) effective_from?: Shared.DateTime; + /** + * An ISO-8601 timestamp representation of the date until which the cost basis is + * effective. If provided, it must be later than `effective_from`. If not provided, + * it remains effective until superseded. + */ + @visibility(Lifecycle.Create, Lifecycle.Read) + effective_to?: Shared.DateTime; + /** * An ISO-8601 timestamp representation of entity creation date. */ diff --git a/api/v3/api.gen.go b/api/v3/api.gen.go index 505dde79f0..56c8054cb1 100644 --- a/api/v3/api.gen.go +++ b/api/v3/api.gen.go @@ -2442,6 +2442,11 @@ type BillingCostBasis struct { // `now` by the system. EffectiveFrom *DateTime `json:"effective_from,omitempty"` + // EffectiveTo An ISO-8601 timestamp representation of the date until which the cost basis is + // effective. If provided, it must be later than `effective_from`. If not provided, + // it remains effective until superseded. + EffectiveTo *DateTime `json:"effective_to,omitempty"` + // FiatCode The fiat currency code for the cost basis. FiatCode CurrencyCode `json:"fiat_code"` Id ULID `json:"id"` @@ -4767,6 +4772,11 @@ type CreateCostBasisRequest struct { // `now` by the system. EffectiveFrom *DateTime `json:"effective_from,omitempty"` + // EffectiveTo An ISO-8601 timestamp representation of the date until which the cost basis is + // effective. If provided, it must be later than `effective_from`. If not provided, + // it remains effective until superseded. + EffectiveTo *DateTime `json:"effective_to,omitempty"` + // FiatCode The fiat currency code for the cost basis. FiatCode CurrencyCode `json:"fiat_code"` @@ -12116,787 +12126,789 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v3/handlers/currencies/convert.go b/api/v3/handlers/currencies/convert.go index 69abba5ad0..eb9f0afe34 100644 --- a/api/v3/handlers/currencies/convert.go +++ b/api/v3/handlers/currencies/convert.go @@ -2,7 +2,6 @@ package currencies import ( "context" - "fmt" v3 "github.com/openmeterio/openmeter/api/v3" "github.com/openmeterio/openmeter/api/v3/apierrors" @@ -29,26 +28,11 @@ func FromAPIBillingCurrencyType(t v3.BillingCurrencyType) currencies.CurrencyTyp } } -func NewBillingCurrencyFrom[T v3.BillingCurrencyCustom | v3.BillingCurrencyFiat](v T) (v3.BillingCurrency, error) { - c := v3.BillingCurrency{} - switch any(v).(type) { - case v3.BillingCurrencyCustom: - custom := any(v).(v3.BillingCurrencyCustom) - if err := c.FromBillingCurrencyCustom(custom); err != nil { - return c, fmt.Errorf("failed to construct BillingCurrencyCustom: %w", err) - } - case v3.BillingCurrencyFiat: - fiat := any(v).(v3.BillingCurrencyFiat) - if err := c.FromBillingCurrencyFiat(fiat); err != nil { - return c, fmt.Errorf("failed to construct BillingCurrencyFiat: %w", err) - } - } - return c, nil -} - func ToAPIBillingCurrency(c currencies.Currency) (v3.BillingCurrency, error) { + out := v3.BillingCurrency{} + if c.ID != "" { - return NewBillingCurrencyFrom(v3.BillingCurrencyCustom{ + err := out.FromBillingCurrencyCustom(v3.BillingCurrencyCustom{ Id: c.ID, Code: c.Code, Name: c.Name, @@ -56,13 +40,16 @@ func ToAPIBillingCurrency(c currencies.Currency) (v3.BillingCurrency, error) { Type: v3.BillingCurrencyCustomTypeCustom, CreatedAt: c.CreatedAt, }) + return out, err } - return NewBillingCurrencyFrom(v3.BillingCurrencyFiat{ + + err := out.FromBillingCurrencyFiat(v3.BillingCurrencyFiat{ Code: c.Code, Name: c.Name, Symbol: &c.Symbol, Type: v3.BillingCurrencyFiatTypeFiat, }) + return out, err } func ToAPIBillingCostBasis(cb currencies.CostBasis) v3.BillingCostBasis { @@ -71,6 +58,7 @@ func ToAPIBillingCostBasis(cb currencies.CostBasis) v3.BillingCostBasis { FiatCode: cb.FiatCode, Rate: cb.Rate.String(), EffectiveFrom: &cb.EffectiveFrom, + EffectiveTo: cb.EffectiveTo, CreatedAt: cb.CreatedAt, } } diff --git a/api/v3/handlers/currencies/convert_test.go b/api/v3/handlers/currencies/convert_test.go new file mode 100644 index 0000000000..36872e8985 --- /dev/null +++ b/api/v3/handlers/currencies/convert_test.go @@ -0,0 +1,64 @@ +package currencies + +import ( + "testing" + "time" + + "github.com/alpacahq/alpacadecimal" + "github.com/stretchr/testify/require" + + "github.com/openmeterio/openmeter/openmeter/currencies" + "github.com/openmeterio/openmeter/pkg/models" +) + +func TestToAPIBillingCostBasisExposesIDAndEffectiveTo(t *testing.T) { + effectiveFrom := time.Date(2026, 6, 26, 12, 0, 0, 0, time.UTC) + effectiveTo := effectiveFrom.Add(24 * time.Hour) + createdAt := effectiveFrom.Add(-time.Hour) + + tests := []struct { + name string + effectiveTo *time.Time + }{ + { + name: "bounded", + effectiveTo: &effectiveTo, + }, + { + name: "open ended", + effectiveTo: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToAPIBillingCostBasis(currencies.CostBasis{ + NamespacedID: models.NamespacedID{ + ID: "01K00000000000000000000000", + Namespace: "test", + }, + ManagedModel: models.ManagedModel{ + CreatedAt: createdAt, + }, + CurrencyID: "01J00000000000000000000000", + FiatCode: "USD", + Rate: alpacadecimal.RequireFromString("0.5"), + EffectiveFrom: effectiveFrom, + EffectiveTo: tt.effectiveTo, + }) + + require.Equal(t, "01K00000000000000000000000", got.Id) + require.Equal(t, "USD", got.FiatCode) + require.Equal(t, "0.5", got.Rate) + require.NotNil(t, got.EffectiveFrom) + require.Equal(t, effectiveFrom, *got.EffectiveFrom) + if tt.effectiveTo == nil { + require.Nil(t, got.EffectiveTo) + } else { + require.NotNil(t, got.EffectiveTo) + require.Equal(t, *tt.effectiveTo, *got.EffectiveTo) + } + require.Equal(t, createdAt, got.CreatedAt) + }) + } +} diff --git a/api/v3/handlers/currencies/create.go b/api/v3/handlers/currencies/create.go index 34db2fa7f6..c1bb82d153 100644 --- a/api/v3/handlers/currencies/create.go +++ b/api/v3/handlers/currencies/create.go @@ -2,11 +2,11 @@ package currencies import ( "context" - "fmt" + "log/slog" "net/http" v3 "github.com/openmeterio/openmeter/api/v3" - "github.com/openmeterio/openmeter/api/v3/apierrors" + "github.com/openmeterio/openmeter/api/v3/request" "github.com/openmeterio/openmeter/openmeter/currencies" "github.com/openmeterio/openmeter/pkg/framework/commonhttp" "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" @@ -21,24 +21,30 @@ type ( func (h *handler) CreateCurrency() CreateCurrencyHandler { return httptransport.NewHandler( func(ctx context.Context, r *http.Request) (CreateCurrencyRequest, error) { - ns, ok := h.namespaceDecoder.GetNamespace(ctx) - if !ok { - return CreateCurrencyRequest{}, apierrors.NewInternalError(ctx, fmt.Errorf("failed to resolve namespace")) + ns, err := h.resolveNamespace(ctx) + if err != nil { + return CreateCurrencyRequest{}, err } - body := &CreateCurrencyRequest{} - if err := commonhttp.JSONRequestBodyDecoder(r, body); err != nil { + var body CreateCurrencyRequest + if err := request.ParseBody(r, &body); err != nil { return CreateCurrencyRequest{}, err } body.Namespace = ns - return *body, nil + return body, nil }, func(ctx context.Context, request CreateCurrencyRequest) (CreateCurrencyResponse, error) { resp, err := h.currencyService.CreateCurrency(ctx, request) if err != nil { return CreateCurrencyResponse{}, err } + slog.InfoContext(ctx, "created custom currency", + slog.String("operation", "create-custom-currency"), + slog.String("namespace", resp.Namespace), + slog.String("currency_id", resp.ID), + slog.String("currency_code", resp.Code), + ) return ToAPIBillingCurrency(resp) }, commonhttp.JSONResponseEncoderWithStatus[CreateCurrencyResponse](http.StatusCreated), diff --git a/api/v3/handlers/currencies/create_cost_basis.go b/api/v3/handlers/currencies/create_cost_basis.go index 5452ad5716..7bf5bb2c24 100644 --- a/api/v3/handlers/currencies/create_cost_basis.go +++ b/api/v3/handlers/currencies/create_cost_basis.go @@ -2,11 +2,11 @@ package currencies import ( "context" - "fmt" + "log/slog" "net/http" v3 "github.com/openmeterio/openmeter/api/v3" - "github.com/openmeterio/openmeter/api/v3/apierrors" + "github.com/openmeterio/openmeter/api/v3/request" "github.com/openmeterio/openmeter/openmeter/currencies" "github.com/openmeterio/openmeter/pkg/framework/commonhttp" "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" @@ -21,26 +21,33 @@ type ( func (h *handler) CreateCostBasis() CreateCostBasisHandler { return httptransport.NewHandlerWithArgs( func(ctx context.Context, r *http.Request, currencyID string) (CreateCostBasisRequest, error) { - ns, ok := h.namespaceDecoder.GetNamespace(ctx) - if !ok { - return CreateCostBasisRequest{}, apierrors.NewInternalError(ctx, fmt.Errorf("failed to resolve namespace")) + ns, err := h.resolveNamespace(ctx) + if err != nil { + return CreateCostBasisRequest{}, err } - body := &CreateCostBasisRequest{} - if err := commonhttp.JSONRequestBodyDecoder(r, body); err != nil { + var body CreateCostBasisRequest + if err := request.ParseBody(r, &body); err != nil { return CreateCostBasisRequest{}, err } body.Namespace = ns body.CurrencyID = currencyID - return *body, nil + return body, nil }, func(ctx context.Context, request CreateCostBasisRequest) (CreateCostBasisResponse, error) { resp, err := h.currencyService.CreateCostBasis(ctx, request) if err != nil { return CreateCostBasisResponse{}, err } + slog.InfoContext(ctx, "created currency cost basis", + slog.String("operation", "create-cost-basis"), + slog.String("namespace", resp.Namespace), + slog.String("currency_id", resp.CurrencyID), + slog.String("cost_basis_id", resp.ID), + slog.String("fiat_code", resp.FiatCode), + ) return ToAPIBillingCostBasis(resp), nil }, commonhttp.JSONResponseEncoderWithStatus[CreateCostBasisResponse](http.StatusCreated), diff --git a/api/v3/handlers/currencies/get_cost_bases.go b/api/v3/handlers/currencies/get_cost_bases.go index 32d26ee796..c15e88ed35 100644 --- a/api/v3/handlers/currencies/get_cost_bases.go +++ b/api/v3/handlers/currencies/get_cost_bases.go @@ -2,7 +2,7 @@ package currencies import ( "context" - "fmt" + "log/slog" "net/http" "github.com/samber/lo" @@ -31,9 +31,9 @@ type ListCostBasesArgs struct { func (h *handler) ListCostBases() ListCostBasesHandler { return httptransport.NewHandlerWithArgs( func(ctx context.Context, r *http.Request, args ListCostBasesArgs) (ListCostBasesRequest, error) { - ns, ok := h.namespaceDecoder.GetNamespace(ctx) - if !ok { - return ListCostBasesRequest{}, apierrors.NewInternalError(ctx, fmt.Errorf("failed to resolve namespace")) + ns, err := h.resolveNamespace(ctx) + if err != nil { + return ListCostBasesRequest{}, err } page := pagination.NewPage(1, 20) @@ -46,11 +46,7 @@ func (h *handler) ListCostBases() ListCostBasesHandler { if err := page.Validate(); err != nil { return ListCostBasesRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ - apierrors.InvalidParameter{ - Field: "page", - Reason: err.Error(), - Source: apierrors.InvalidParamSourceQuery, - }, + {Field: "page", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, }) } @@ -73,6 +69,20 @@ func (h *handler) ListCostBases() ListCostBasesHandler { return ListCostBasesResponse{}, err } + attrs := []slog.Attr{ + slog.String("operation", "list-cost-bases"), + slog.String("namespace", req.Namespace), + slog.String("currency_id", req.CurrencyID), + slog.Int("page_number", req.Page.PageNumber), + slog.Int("page_size", req.Page.PageSize), + slog.Int("result_count", len(result.Items)), + slog.Int("total_count", result.TotalCount), + } + if req.FilterFiatCode != nil { + attrs = append(attrs, slog.String("filter_fiat_code", *req.FilterFiatCode)) + } + slog.LogAttrs(ctx, slog.LevelDebug, "listed currency cost bases", attrs...) + items := lo.Map(result.Items, func(cb currencies.CostBasis, _ int) v3.BillingCostBasis { return ToAPIBillingCostBasis(cb) }) diff --git a/api/v3/handlers/currencies/handler.go b/api/v3/handlers/currencies/handler.go index 9e99497656..2b16175f63 100644 --- a/api/v3/handlers/currencies/handler.go +++ b/api/v3/handlers/currencies/handler.go @@ -1,8 +1,9 @@ package currencies import ( + "context" + "github.com/openmeterio/openmeter/openmeter/currencies" - "github.com/openmeterio/openmeter/openmeter/namespace/namespacedriver" "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" ) @@ -14,14 +15,18 @@ type Handler interface { } type handler struct { - namespaceDecoder namespacedriver.NamespaceDecoder + resolveNamespace func(ctx context.Context) (string, error) options []httptransport.HandlerOption currencyService currencies.CurrencyService } -func New(namespaceDecoder namespacedriver.NamespaceDecoder, currencyService currencies.CurrencyService, options ...httptransport.HandlerOption) Handler { +func New( + resolveNamespace func(ctx context.Context) (string, error), + currencyService currencies.CurrencyService, + options ...httptransport.HandlerOption, +) Handler { return &handler{ - namespaceDecoder: namespaceDecoder, + resolveNamespace: resolveNamespace, options: options, currencyService: currencyService, } diff --git a/api/v3/handlers/currencies/list.go b/api/v3/handlers/currencies/list.go index 6278acf60e..4bd3239ee4 100644 --- a/api/v3/handlers/currencies/list.go +++ b/api/v3/handlers/currencies/list.go @@ -2,7 +2,7 @@ package currencies import ( "context" - "fmt" + "log/slog" "net/http" "github.com/samber/lo" @@ -29,9 +29,9 @@ type ( func (h *handler) ListCurrencies() ListCurrenciesHandler { return httptransport.NewHandlerWithArgs( func(ctx context.Context, r *http.Request, params ListCurrenciesParams) (ListCurrenciesRequest, error) { - ns, ok := h.namespaceDecoder.GetNamespace(ctx) - if !ok { - return ListCurrenciesRequest{}, apierrors.NewInternalError(ctx, fmt.Errorf("failed to resolve namespace")) + ns, err := h.resolveNamespace(ctx) + if err != nil { + return ListCurrenciesRequest{}, err } page := pagination.NewPage(1, 100) @@ -44,11 +44,7 @@ func (h *handler) ListCurrencies() ListCurrenciesHandler { if err := page.Validate(); err != nil { return ListCurrenciesRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ - apierrors.InvalidParameter{ - Field: "page", - Reason: err.Error(), - Source: apierrors.InvalidParamSourceQuery, - }, + {Field: "page", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, }) } @@ -98,6 +94,19 @@ func (h *handler) ListCurrencies() ListCurrenciesHandler { return ListCurrenciesResponse{}, err } + attrs := []slog.Attr{ + slog.String("operation", "list-currencies"), + slog.String("namespace", request.Namespace), + slog.Int("page_number", request.Page.PageNumber), + slog.Int("page_size", request.Page.PageSize), + slog.Int("result_count", len(result.Items)), + slog.Int("total_count", result.TotalCount), + } + if request.FilterType != nil { + attrs = append(attrs, slog.String("filter_type", string(*request.FilterType))) + } + slog.LogAttrs(ctx, slog.LevelDebug, "listed currencies", attrs...) + items := make([]v3.BillingCurrency, 0, len(result.Items)) for _, c := range result.Items { item, err := ToAPIBillingCurrency(c) diff --git a/api/v3/openapi.yaml b/api/v3/openapi.yaml index bdc8feb73e..736126a1f3 100644 --- a/api/v3/openapi.yaml +++ b/api/v3/openapi.yaml @@ -4652,6 +4652,13 @@ components: An ISO-8601 timestamp representation of the date from which the cost basis is effective. If not provided, it will be effective immediately and will be set to `now` by the system. + effective_to: + allOf: + - $ref: '#/components/schemas/DateTime' + description: |- + An ISO-8601 timestamp representation of the date until which the cost basis is + effective. If provided, it must be later than `effective_from`. If not provided, + it remains effective until superseded. created_at: allOf: - $ref: '#/components/schemas/DateTime' @@ -8565,6 +8572,13 @@ components: An ISO-8601 timestamp representation of the date from which the cost basis is effective. If not provided, it will be effective immediately and will be set to `now` by the system. + effective_to: + allOf: + - $ref: '#/components/schemas/DateTime' + description: |- + An ISO-8601 timestamp representation of the date until which the cost basis is + effective. If provided, it must be later than `effective_from`. If not provided, + it remains effective until superseded. additionalProperties: false description: CostBasis create request. CreateCreditAdjustmentRequest: diff --git a/api/v3/server/server.go b/api/v3/server/server.go index 1b624ed3df..1f6e189dd8 100644 --- a/api/v3/server/server.go +++ b/api/v3/server/server.go @@ -332,7 +332,7 @@ func NewServer(config *Config) (*Server, error) { plansHandler := planshandler.New(resolveNamespace, config.PlanService, config.UnitConfig.Enabled, httptransport.WithErrorHandler(config.ErrorHandler)) planAddonsHandler := planaddonshandler.New(resolveNamespace, config.PlanService, config.PlanAddonService, httptransport.WithErrorHandler(config.ErrorHandler)) taxcodesHandler := taxcodeshandler.New(resolveNamespace, config.TaxCodeService, httptransport.WithErrorHandler(config.ErrorHandler)) - currenciesHandler := currencieshandler.New(config.NamespaceDecoder, config.CurrencyService, httptransport.WithErrorHandler(config.ErrorHandler)) + currenciesHandler := currencieshandler.New(resolveNamespace, config.CurrencyService, httptransport.WithErrorHandler(config.ErrorHandler)) var chargesH chargeshandler.Handler if config.ChargeService != nil { diff --git a/openmeter/app/stripe/calculator.go b/openmeter/app/stripe/calculator.go index 9c5f7cbf4b..1a8d6c5f89 100644 --- a/openmeter/app/stripe/calculator.go +++ b/openmeter/app/stripe/calculator.go @@ -21,7 +21,7 @@ func NewStripeCalculator(currency currencyx.Code) (StripeCalculator, error) { return StripeCalculator{ calculator: calculator, printer: message.NewPrinter(language.English), - multiplier: alpacadecimal.NewFromInt(10).Pow(alpacadecimal.NewFromInt(int64(calculator.Def.Subunits))), + multiplier: alpacadecimal.NewFromInt(10).Pow(alpacadecimal.NewFromInt(int64(calculator.CurrencyPrecision()))), }, nil } @@ -39,12 +39,13 @@ func (c StripeCalculator) RoundToAmount(amount alpacadecimal.Decimal) int64 { // FormatAmount formats the amount func (c StripeCalculator) FormatAmount(amount alpacadecimal.Decimal) string { + def := c.calculator.Definition() if amount.IsInteger() { - return c.calculator.Def.FormatAmount(num.MakeAmount(amount.IntPart(), 0)) + return def.FormatAmount(num.MakeAmount(amount.IntPart(), 0)) } am, _ := amount.Float64() - return c.calculator.Def.FormatAmount(num.AmountFromFloat64(am, uint32(amount.NumDigits()))) + return def.FormatAmount(num.AmountFromFloat64(am, uint32(amount.NumDigits()))) } // FormatQuantity formats the quantity to two decimal places. diff --git a/openmeter/billing/rating/service/rate/tieredgraduated.go b/openmeter/billing/rating/service/rate/tieredgraduated.go index bc005a2a0a..68d2164c57 100644 --- a/openmeter/billing/rating/service/rate/tieredgraduated.go +++ b/openmeter/billing/rating/service/rate/tieredgraduated.go @@ -130,7 +130,7 @@ func (i TieredPriceCalculatorInput) Validate() error { return fmt.Errorf("to quantity must be greater or equal to from quantity") } - if i.Currency.Currency == "" { + if i.Currency.CurrencyCode() == "" { return fmt.Errorf("currency calculator is required") } diff --git a/openmeter/billing/worker/subscriptionsync/service/reconciler/patchcharge_test.go b/openmeter/billing/worker/subscriptionsync/service/reconciler/patchcharge_test.go index 728cd95b05..495b813b3c 100644 --- a/openmeter/billing/worker/subscriptionsync/service/reconciler/patchcharge_test.go +++ b/openmeter/billing/worker/subscriptionsync/service/reconciler/patchcharge_test.go @@ -295,7 +295,7 @@ func newChargePatchTestExistingIntent(target targetstate.StateItem) chargesusage Intent: chargesmeta.Intent{ ManagedBy: billing.SubscriptionManagedLine, CustomerID: target.Subscription.CustomerId, - Currency: target.CurrencyCalculator.Currency, + Currency: target.CurrencyCalculator.CurrencyCode(), UniqueReferenceID: ptr("existing-charge"), TaxConfig: productcatalog.TaxCodeConfig{ TaxCodeID: "tax-code-id", diff --git a/openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeflatfee.go b/openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeflatfee.go index 3c0dd6801a..bf1dad1558 100644 --- a/openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeflatfee.go +++ b/openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeflatfee.go @@ -94,7 +94,7 @@ func newFlatFeeChargeIntent(target targetstate.StateItem) (charges.ChargeIntent, ManagedBy: billing.SubscriptionManagedLine, CustomerID: target.Subscription.CustomerId, Annotations: annotations, - Currency: target.CurrencyCalculator.Currency, + Currency: target.CurrencyCalculator.CurrencyCode(), UniqueReferenceID: &target.UniqueID, TaxConfig: productcatalog.TaxCodeConfigFrom(rateCardMeta.TaxConfig), Subscription: &chargesmeta.SubscriptionReference{ diff --git a/openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go b/openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go index c6b49d33de..2ecb21b95d 100644 --- a/openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go +++ b/openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go @@ -93,7 +93,7 @@ func newUsageBasedChargeIntent(target targetstate.StateItem) (charges.ChargeInte ManagedBy: billing.SubscriptionManagedLine, CustomerID: target.Subscription.CustomerId, Annotations: annotations, - Currency: target.CurrencyCalculator.Currency, + Currency: target.CurrencyCalculator.CurrencyCode(), UniqueReferenceID: &target.UniqueID, TaxConfig: productcatalog.TaxCodeConfigFrom(rateCardMeta.TaxConfig), Subscription: &chargesmeta.SubscriptionReference{ diff --git a/openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go b/openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go index e7b5e3dd5a..66a46a9cba 100644 --- a/openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go +++ b/openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go @@ -58,7 +58,7 @@ func (r StateItem) GetExpectedLine() (*billing.GatheringLine, error) { Description: r.Spec.RateCard.AsMeta().Description, }), ManagedBy: billing.SubscriptionManagedLine, - Currency: r.CurrencyCalculator.Currency, + Currency: r.CurrencyCalculator.CurrencyCode(), ChildUniqueReferenceID: &r.UniqueID, TaxConfig: r.Spec.RateCard.AsMeta().TaxConfig, ServicePeriod: r.GetServicePeriod(), diff --git a/openmeter/currencies/adapter.go b/openmeter/currencies/adapter.go index a5d839f2ae..54b4acca66 100644 --- a/openmeter/currencies/adapter.go +++ b/openmeter/currencies/adapter.go @@ -8,11 +8,8 @@ import ( ) type Adapter interface { - CurrenciesAdapter entutils.TxCreator -} -type CurrenciesAdapter interface { ListCustomCurrencies(ctx context.Context, params ListCurrenciesInput) (pagination.Result[Currency], error) CreateCurrency(ctx context.Context, params CreateCurrencyInput) (Currency, error) CreateCostBasis(ctx context.Context, params CreateCostBasisInput) (CostBasis, error) diff --git a/openmeter/currencies/adapter/adapter.go b/openmeter/currencies/adapter/adapter.go index 72062167bc..46c11a4bd0 100644 --- a/openmeter/currencies/adapter/adapter.go +++ b/openmeter/currencies/adapter/adapter.go @@ -5,7 +5,6 @@ import ( "database/sql" "errors" "fmt" - "log/slog" "github.com/openmeterio/openmeter/openmeter/currencies" entdb "github.com/openmeterio/openmeter/openmeter/ent/db" @@ -38,8 +37,7 @@ func New(config Config) (currencies.Adapter, error) { var _ currencies.Adapter = (*adapter)(nil) type adapter struct { - db *entdb.Client - logger *slog.Logger + db *entdb.Client } // Tx implements entutils.TxCreator interface @@ -56,8 +54,7 @@ func (a *adapter) Tx(ctx context.Context) (context.Context, transaction.Driver, func (a *adapter) WithTx(ctx context.Context, tx *entutils.TxDriver) *adapter { txClient := entdb.NewTxClientFromRawConfig(ctx, *tx.GetConfig()) return &adapter{ - db: txClient.Client(), - logger: a.logger, + db: txClient.Client(), } } diff --git a/openmeter/currencies/adapter/currencies.go b/openmeter/currencies/adapter/currencies.go index 7a1b9b1b8c..c2e7fc7ec3 100644 --- a/openmeter/currencies/adapter/currencies.go +++ b/openmeter/currencies/adapter/currencies.go @@ -6,7 +6,6 @@ import ( "time" "entgo.io/ent/dialect/sql" - "github.com/samber/lo" "github.com/openmeterio/openmeter/openmeter/currencies" entdb "github.com/openmeterio/openmeter/openmeter/ent/db" @@ -33,19 +32,26 @@ func mapCurrencyFromDB(c *entdb.CustomCurrency) currencies.Currency { } func mapCostBasisFromDB(c *entdb.CurrencyCostBasis) currencies.CostBasis { + var effectiveTo *time.Time + if c.EffectiveTo != nil { + t := c.EffectiveTo.In(time.UTC) + effectiveTo = &t + } + return currencies.CostBasis{ NamespacedID: models.NamespacedID{ID: c.ID, Namespace: c.Namespace}, ManagedModel: models.ManagedModel{CreatedAt: c.CreatedAt, UpdatedAt: c.UpdatedAt, DeletedAt: c.DeletedAt}, - CurrencyID: c.CustomCurrencyID, + CurrencyID: c.CurrencyID, FiatCode: string(c.FiatCode), Rate: c.Rate, EffectiveFrom: c.EffectiveFrom.In(time.UTC), + EffectiveTo: effectiveTo, } } func (a *adapter) ListCustomCurrencies(ctx context.Context, params currencies.ListCurrenciesInput) (pagination.Result[currencies.Currency], error) { return entutils.TransactingRepo(ctx, a, func(ctx context.Context, tx *adapter) (pagination.Result[currencies.Currency], error) { - q := a.db.CustomCurrency.Query(). + q := tx.db.CustomCurrency.Query(). Where(customcurrency.Namespace(params.Namespace)) q = filter.ApplyToQuery(q, params.Code, customcurrency.FieldCode) @@ -61,33 +67,18 @@ func (a *adapter) ListCustomCurrencies(ctx context.Context, params currencies.Li q = q.Order(customcurrency.ByCode(order...)) } - total, err := q.Count(ctx) - if err != nil { - return pagination.Result[currencies.Currency]{}, fmt.Errorf("failed to count currencies: %w", err) - } - - if params.Page.PageSize > 0 && params.Page.PageNumber > 0 { - q = q.Offset((params.Page.PageNumber - 1) * params.Page.PageSize).Limit(params.Page.PageSize) - } - - currencyRecords, err := q.All(ctx) + paged, err := q.Paginate(ctx, params.Page) if err != nil { return pagination.Result[currencies.Currency]{}, fmt.Errorf("failed to list currencies: %w", err) } - return pagination.Result[currencies.Currency]{ - Page: params.Page, - TotalCount: total, - Items: lo.Map(currencyRecords, func(c *entdb.CustomCurrency, _ int) currencies.Currency { - return mapCurrencyFromDB(c) - }), - }, nil + return pagination.MapResult(paged, mapCurrencyFromDB), nil }) } func (a *adapter) CreateCurrency(ctx context.Context, params currencies.CreateCurrencyInput) (currencies.Currency, error) { return entutils.TransactingRepo(ctx, a, func(ctx context.Context, tx *adapter) (currencies.Currency, error) { - curr, err := a.db.CustomCurrency.Create(). + curr, err := tx.db.CustomCurrency.Create(). SetNamespace(params.Namespace). SetCode(params.Code). SetName(params.Name). @@ -106,12 +97,19 @@ func (a *adapter) CreateCurrency(ctx context.Context, params currencies.CreateCu func (a *adapter) CreateCostBasis(ctx context.Context, params currencies.CreateCostBasisInput) (currencies.CostBasis, error) { return entutils.TransactingRepo(ctx, a, func(ctx context.Context, tx *adapter) (currencies.CostBasis, error) { - costBasis, err := a.db.CurrencyCostBasis.Create(). + var effectiveTo *time.Time + if params.EffectiveTo != nil { + t := params.EffectiveTo.In(time.UTC) + effectiveTo = &t + } + + costBasis, err := tx.db.CurrencyCostBasis.Create(). SetNamespace(params.Namespace). - SetCustomCurrencyID(params.CurrencyID). + SetCurrencyID(params.CurrencyID). SetFiatCode(currencyx.Code(params.FiatCode)). SetRate(params.Rate). SetEffectiveFrom(params.EffectiveFrom.In(time.UTC)). + SetNillableEffectiveTo(effectiveTo). Save(ctx) if err != nil { if entdb.IsConstraintError(err) { @@ -126,10 +124,10 @@ func (a *adapter) CreateCostBasis(ctx context.Context, params currencies.CreateC func (a *adapter) ListCostBases(ctx context.Context, params currencies.ListCostBasesInput) (pagination.Result[currencies.CostBasis], error) { return entutils.TransactingRepo(ctx, a, func(ctx context.Context, tx *adapter) (pagination.Result[currencies.CostBasis], error) { - q := a.db.CurrencyCostBasis.Query(). + q := tx.db.CurrencyCostBasis.Query(). Where( currencycostbasis.Namespace(params.Namespace), - currencycostbasis.CustomCurrencyID(params.CurrencyID), + currencycostbasis.CurrencyID(params.CurrencyID), ). Order(currencycostbasis.ByEffectiveFrom(sql.OrderDesc())) diff --git a/openmeter/currencies/models.go b/openmeter/currencies/models.go index c74281d10c..7e10c5d1e3 100644 --- a/openmeter/currencies/models.go +++ b/openmeter/currencies/models.go @@ -134,6 +134,7 @@ type CostBasis struct { FiatCode string `json:"fiat_code"` Rate alpacadecimal.Decimal `json:"rate"` EffectiveFrom time.Time `json:"effective_from"` + EffectiveTo *time.Time `json:"effective_to,omitempty"` } var _ models.Validator = (*CreateCostBasisInput)(nil) @@ -144,6 +145,7 @@ type CreateCostBasisInput struct { FiatCode string `json:"fiat_code"` Rate alpacadecimal.Decimal `json:"rate"` EffectiveFrom *time.Time `json:"effective_from,omitempty"` + EffectiveTo *time.Time `json:"effective_to,omitempty"` } func (i CreateCostBasisInput) Validate() error { diff --git a/openmeter/currencies/service/service.go b/openmeter/currencies/service/service.go index c64cd500ab..26755637bc 100644 --- a/openmeter/currencies/service/service.go +++ b/openmeter/currencies/service/service.go @@ -30,8 +30,8 @@ func New(adapter currencies.Adapter) *Service { } func (s *Service) ListCurrencies(ctx context.Context, params currencies.ListCurrenciesInput) (pagination.Result[currencies.Currency], error) { - if params.Validate() != nil { - return pagination.Result[currencies.Currency]{}, models.NewGenericValidationError(fmt.Errorf("invalid input parameters: %w", params.Validate())) + if err := params.Validate(); err != nil { + return pagination.Result[currencies.Currency]{}, models.NewGenericValidationError(fmt.Errorf("invalid input parameters: %w", err)) } return transaction.Run(ctx, s.adapter, func(ctx context.Context) (pagination.Result[currencies.Currency], error) { @@ -120,8 +120,8 @@ func (s *Service) ListCurrencies(ctx context.Context, params currencies.ListCurr } func (s *Service) CreateCurrency(ctx context.Context, params currencies.CreateCurrencyInput) (currencies.Currency, error) { - if params.Validate() != nil { - return currencies.Currency{}, models.NewGenericValidationError(fmt.Errorf("invalid input parameters: %w", params.Validate())) + if err := params.Validate(); err != nil { + return currencies.Currency{}, models.NewGenericValidationError(fmt.Errorf("invalid input parameters: %w", err)) } return transaction.Run(ctx, s.adapter, func(ctx context.Context) (currencies.Currency, error) { @@ -130,8 +130,8 @@ func (s *Service) CreateCurrency(ctx context.Context, params currencies.CreateCu } func (s *Service) CreateCostBasis(ctx context.Context, params currencies.CreateCostBasisInput) (currencies.CostBasis, error) { - if params.Validate() != nil { - return currencies.CostBasis{}, models.NewGenericValidationError(fmt.Errorf("invalid input parameters: %w", params.Validate())) + if err := params.Validate(); err != nil { + return currencies.CostBasis{}, models.NewGenericValidationError(fmt.Errorf("invalid input parameters: %w", err)) } return transaction.Run(ctx, s.adapter, func(ctx context.Context) (currencies.CostBasis, error) { @@ -150,13 +150,21 @@ func (s *Service) CreateCostBasis(ctx context.Context, params currencies.CreateC } params.EffectiveFrom = &effectiveFrom + if params.EffectiveTo != nil && !effectiveFrom.Before(*params.EffectiveTo) { + return currencies.CostBasis{}, models.NewGenericValidationError(fmt.Errorf( + "effective_to %s must be after effective_from %s", + params.EffectiveTo.UTC().Format(time.RFC3339), + effectiveFrom.UTC().Format(time.RFC3339), + )) + } + return s.adapter.CreateCostBasis(ctx, params) }) } func (s *Service) ListCostBases(ctx context.Context, params currencies.ListCostBasesInput) (pagination.Result[currencies.CostBasis], error) { - if params.Validate() != nil { - return pagination.Result[currencies.CostBasis]{}, models.NewGenericValidationError(fmt.Errorf("invalid input parameters: %w", params.Validate())) + if err := params.Validate(); err != nil { + return pagination.Result[currencies.CostBasis]{}, models.NewGenericValidationError(fmt.Errorf("invalid input parameters: %w", err)) } return transaction.Run(ctx, s.adapter, func(ctx context.Context) (pagination.Result[currencies.CostBasis], error) { diff --git a/openmeter/currencies/service/service_test.go b/openmeter/currencies/service/service_test.go index bac811cd62..faaccac8c4 100644 --- a/openmeter/currencies/service/service_test.go +++ b/openmeter/currencies/service/service_test.go @@ -2,8 +2,11 @@ package service import ( "context" + "errors" "testing" + "time" + "github.com/alpacahq/alpacadecimal" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -11,6 +14,7 @@ import ( "github.com/openmeterio/openmeter/openmeter/currencies" "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/framework/transaction" + "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/pagination" "github.com/openmeterio/openmeter/pkg/sortx" ) @@ -25,7 +29,8 @@ func (noopDriver) SavePoint() error { return nil } // fakeAdapter implements currencies.Adapter for unit testing the service layer. // ListCustomCurrencies applies the Code filter from params to simulate DB-level filtering. type fakeAdapter struct { - custom []currencies.Currency + custom []currencies.Currency + createCostBasis func(context.Context, currencies.CreateCostBasisInput) (currencies.CostBasis, error) } func (f *fakeAdapter) Tx(ctx context.Context) (context.Context, transaction.Driver, error) { @@ -47,15 +52,19 @@ func (f *fakeAdapter) ListCustomCurrencies(_ context.Context, params currencies. } func (f *fakeAdapter) CreateCurrency(_ context.Context, _ currencies.CreateCurrencyInput) (currencies.Currency, error) { - panic("not implemented") + return currencies.Currency{}, errors.New("fakeAdapter.CreateCurrency is not implemented") } -func (f *fakeAdapter) CreateCostBasis(_ context.Context, _ currencies.CreateCostBasisInput) (currencies.CostBasis, error) { - panic("not implemented") +func (f *fakeAdapter) CreateCostBasis(ctx context.Context, input currencies.CreateCostBasisInput) (currencies.CostBasis, error) { + if f.createCostBasis != nil { + return f.createCostBasis(ctx, input) + } + + return currencies.CostBasis{}, errors.New("fakeAdapter.CreateCostBasis is not implemented") } func (f *fakeAdapter) ListCostBases(_ context.Context, _ currencies.ListCostBasesInput) (pagination.Result[currencies.CostBasis], error) { - panic("not implemented") + return pagination.Result[currencies.CostBasis]{}, errors.New("fakeAdapter.ListCostBases is not implemented") } // newTestService creates a Service backed by a fake adapter seeded with custom currencies. @@ -221,3 +230,93 @@ func TestListCurrencies_CustomOnlyPath(t *testing.T) { assert.Equal(t, "MYCUSTOM", result.Items[0].Code) }) } + +func TestCreateCostBasis_EffectiveTo(t *testing.T) { + effectiveFrom := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second) + effectiveTo := effectiveFrom.Add(24 * time.Hour) + + var gotInput currencies.CreateCostBasisInput + svc := New(&fakeAdapter{ + createCostBasis: func(_ context.Context, input currencies.CreateCostBasisInput) (currencies.CostBasis, error) { + gotInput = input + + return currencies.CostBasis{ + NamespacedID: models.NamespacedID{ + ID: "01K00000000000000000000000", + Namespace: input.Namespace, + }, + CurrencyID: input.CurrencyID, + FiatCode: input.FiatCode, + Rate: input.Rate, + EffectiveFrom: *input.EffectiveFrom, + EffectiveTo: input.EffectiveTo, + }, nil + }, + }) + + result, err := svc.CreateCostBasis(t.Context(), currencies.CreateCostBasisInput{ + Namespace: "test", + CurrencyID: "01J00000000000000000000000", + FiatCode: "USD", + Rate: alpacadecimal.RequireFromString("0.5"), + EffectiveFrom: &effectiveFrom, + EffectiveTo: &effectiveTo, + }) + require.NoError(t, err) + + require.NotNil(t, gotInput.EffectiveFrom) + require.NotNil(t, gotInput.EffectiveTo) + require.Equal(t, effectiveFrom, *gotInput.EffectiveFrom) + require.Equal(t, effectiveTo, *gotInput.EffectiveTo) + require.Equal(t, "01K00000000000000000000000", result.ID) + require.NotNil(t, result.EffectiveTo) + require.Equal(t, effectiveTo, *result.EffectiveTo) +} + +func TestCreateCostBasis_DefaultEffectiveFromAllowsOpenEndedCostBasis(t *testing.T) { + var gotInput currencies.CreateCostBasisInput + svc := New(&fakeAdapter{ + createCostBasis: func(_ context.Context, input currencies.CreateCostBasisInput) (currencies.CostBasis, error) { + gotInput = input + + return currencies.CostBasis{ + CurrencyID: input.CurrencyID, + FiatCode: input.FiatCode, + Rate: input.Rate, + EffectiveFrom: *input.EffectiveFrom, + EffectiveTo: input.EffectiveTo, + }, nil + }, + }) + + _, err := svc.CreateCostBasis(t.Context(), currencies.CreateCostBasisInput{ + Namespace: "test", + CurrencyID: "01J00000000000000000000000", + FiatCode: "USD", + Rate: alpacadecimal.RequireFromString("0.5"), + }) + require.NoError(t, err) + + require.NotNil(t, gotInput.EffectiveFrom) + require.Nil(t, gotInput.EffectiveTo) +} + +func TestCreateCostBasis_RejectsInvalidEffectiveTo(t *testing.T) { + effectiveFrom := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second) + effectiveTo := effectiveFrom + + svc := New(&fakeAdapter{}) + + _, err := svc.CreateCostBasis(t.Context(), currencies.CreateCostBasisInput{ + Namespace: "test", + CurrencyID: "01J00000000000000000000000", + FiatCode: "USD", + Rate: alpacadecimal.RequireFromString("0.5"), + EffectiveFrom: &effectiveFrom, + EffectiveTo: &effectiveTo, + }) + require.Error(t, err) + require.True(t, models.IsGenericValidationError(err), "error must be a validation error") + require.Contains(t, err.Error(), "effective_to") + require.Contains(t, err.Error(), "must be after effective_from") +} diff --git a/openmeter/ent/db/currencycostbasis.go b/openmeter/ent/db/currencycostbasis.go index 44f2fb04cf..786bb0f224 100644 --- a/openmeter/ent/db/currencycostbasis.go +++ b/openmeter/ent/db/currencycostbasis.go @@ -28,14 +28,16 @@ type CurrencyCostBasis struct { UpdatedAt time.Time `json:"updated_at,omitempty"` // DeletedAt holds the value of the "deleted_at" field. DeletedAt *time.Time `json:"deleted_at,omitempty"` - // CustomCurrencyID holds the value of the "custom_currency_id" field. - CustomCurrencyID string `json:"custom_currency_id,omitempty"` + // CurrencyID holds the value of the "currency_id" field. + CurrencyID string `json:"currency_id,omitempty"` // FiatCode holds the value of the "fiat_code" field. FiatCode currencyx.Code `json:"fiat_code,omitempty"` // Rate holds the value of the "rate" field. Rate alpacadecimal.Decimal `json:"rate,omitempty"` // EffectiveFrom holds the value of the "effective_from" field. EffectiveFrom time.Time `json:"effective_from,omitempty"` + // EffectiveTo holds the value of the "effective_to" field. + EffectiveTo *time.Time `json:"effective_to,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the CurrencyCostBasisQuery when eager-loading is set. Edges CurrencyCostBasisEdges `json:"edges"` @@ -69,9 +71,9 @@ func (*CurrencyCostBasis) scanValues(columns []string) ([]any, error) { switch columns[i] { case currencycostbasis.FieldRate: values[i] = new(alpacadecimal.Decimal) - case currencycostbasis.FieldID, currencycostbasis.FieldNamespace, currencycostbasis.FieldCustomCurrencyID, currencycostbasis.FieldFiatCode: + case currencycostbasis.FieldID, currencycostbasis.FieldNamespace, currencycostbasis.FieldCurrencyID, currencycostbasis.FieldFiatCode: values[i] = new(sql.NullString) - case currencycostbasis.FieldCreatedAt, currencycostbasis.FieldUpdatedAt, currencycostbasis.FieldDeletedAt, currencycostbasis.FieldEffectiveFrom: + case currencycostbasis.FieldCreatedAt, currencycostbasis.FieldUpdatedAt, currencycostbasis.FieldDeletedAt, currencycostbasis.FieldEffectiveFrom, currencycostbasis.FieldEffectiveTo: values[i] = new(sql.NullTime) default: values[i] = new(sql.UnknownType) @@ -119,11 +121,11 @@ func (_m *CurrencyCostBasis) assignValues(columns []string, values []any) error _m.DeletedAt = new(time.Time) *_m.DeletedAt = value.Time } - case currencycostbasis.FieldCustomCurrencyID: + case currencycostbasis.FieldCurrencyID: if value, ok := values[i].(*sql.NullString); !ok { - return fmt.Errorf("unexpected type %T for field custom_currency_id", values[i]) + return fmt.Errorf("unexpected type %T for field currency_id", values[i]) } else if value.Valid { - _m.CustomCurrencyID = value.String + _m.CurrencyID = value.String } case currencycostbasis.FieldFiatCode: if value, ok := values[i].(*sql.NullString); !ok { @@ -143,6 +145,13 @@ func (_m *CurrencyCostBasis) assignValues(columns []string, values []any) error } else if value.Valid { _m.EffectiveFrom = value.Time } + case currencycostbasis.FieldEffectiveTo: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field effective_to", values[i]) + } else if value.Valid { + _m.EffectiveTo = new(time.Time) + *_m.EffectiveTo = value.Time + } default: _m.selectValues.Set(columns[i], values[i]) } @@ -198,8 +207,8 @@ func (_m *CurrencyCostBasis) String() string { builder.WriteString(v.Format(time.ANSIC)) } builder.WriteString(", ") - builder.WriteString("custom_currency_id=") - builder.WriteString(_m.CustomCurrencyID) + builder.WriteString("currency_id=") + builder.WriteString(_m.CurrencyID) builder.WriteString(", ") builder.WriteString("fiat_code=") builder.WriteString(fmt.Sprintf("%v", _m.FiatCode)) @@ -209,6 +218,11 @@ func (_m *CurrencyCostBasis) String() string { builder.WriteString(", ") builder.WriteString("effective_from=") builder.WriteString(_m.EffectiveFrom.Format(time.ANSIC)) + builder.WriteString(", ") + if v := _m.EffectiveTo; v != nil { + builder.WriteString("effective_to=") + builder.WriteString(v.Format(time.ANSIC)) + } builder.WriteByte(')') return builder.String() } diff --git a/openmeter/ent/db/currencycostbasis/currencycostbasis.go b/openmeter/ent/db/currencycostbasis/currencycostbasis.go index 5e1336afbc..2420172ebc 100644 --- a/openmeter/ent/db/currencycostbasis/currencycostbasis.go +++ b/openmeter/ent/db/currencycostbasis/currencycostbasis.go @@ -22,14 +22,16 @@ const ( FieldUpdatedAt = "updated_at" // FieldDeletedAt holds the string denoting the deleted_at field in the database. FieldDeletedAt = "deleted_at" - // FieldCustomCurrencyID holds the string denoting the custom_currency_id field in the database. - FieldCustomCurrencyID = "custom_currency_id" + // FieldCurrencyID holds the string denoting the currency_id field in the database. + FieldCurrencyID = "currency_id" // FieldFiatCode holds the string denoting the fiat_code field in the database. FieldFiatCode = "fiat_code" // FieldRate holds the string denoting the rate field in the database. FieldRate = "rate" // FieldEffectiveFrom holds the string denoting the effective_from field in the database. FieldEffectiveFrom = "effective_from" + // FieldEffectiveTo holds the string denoting the effective_to field in the database. + FieldEffectiveTo = "effective_to" // EdgeCurrency holds the string denoting the currency edge name in mutations. EdgeCurrency = "currency" // Table holds the table name of the currencycostbasis in the database. @@ -40,7 +42,7 @@ const ( // It exists in this package in order to avoid circular dependency with the "customcurrency" package. CurrencyInverseTable = "custom_currencies" // CurrencyColumn is the table column denoting the currency relation/edge. - CurrencyColumn = "custom_currency_id" + CurrencyColumn = "currency_id" ) // Columns holds all SQL columns for currencycostbasis fields. @@ -50,10 +52,11 @@ var Columns = []string{ FieldCreatedAt, FieldUpdatedAt, FieldDeletedAt, - FieldCustomCurrencyID, + FieldCurrencyID, FieldFiatCode, FieldRate, FieldEffectiveFrom, + FieldEffectiveTo, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -109,9 +112,9 @@ func ByDeletedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldDeletedAt, opts...).ToFunc() } -// ByCustomCurrencyID orders the results by the custom_currency_id field. -func ByCustomCurrencyID(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldCustomCurrencyID, opts...).ToFunc() +// ByCurrencyID orders the results by the currency_id field. +func ByCurrencyID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCurrencyID, opts...).ToFunc() } // ByFiatCode orders the results by the fiat_code field. @@ -129,6 +132,11 @@ func ByEffectiveFrom(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldEffectiveFrom, opts...).ToFunc() } +// ByEffectiveTo orders the results by the effective_to field. +func ByEffectiveTo(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldEffectiveTo, opts...).ToFunc() +} + // ByCurrencyField orders the results by currency field. func ByCurrencyField(field string, opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { diff --git a/openmeter/ent/db/currencycostbasis/where.go b/openmeter/ent/db/currencycostbasis/where.go index 516c18b377..cbb673efd3 100644 --- a/openmeter/ent/db/currencycostbasis/where.go +++ b/openmeter/ent/db/currencycostbasis/where.go @@ -87,9 +87,9 @@ func DeletedAt(v time.Time) predicate.CurrencyCostBasis { return predicate.CurrencyCostBasis(sql.FieldEQ(FieldDeletedAt, v)) } -// CustomCurrencyID applies equality check predicate on the "custom_currency_id" field. It's identical to CustomCurrencyIDEQ. -func CustomCurrencyID(v string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldEQ(FieldCustomCurrencyID, v)) +// CurrencyID applies equality check predicate on the "currency_id" field. It's identical to CurrencyIDEQ. +func CurrencyID(v string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldEQ(FieldCurrencyID, v)) } // FiatCode applies equality check predicate on the "fiat_code" field. It's identical to FiatCodeEQ. @@ -108,6 +108,11 @@ func EffectiveFrom(v time.Time) predicate.CurrencyCostBasis { return predicate.CurrencyCostBasis(sql.FieldEQ(FieldEffectiveFrom, v)) } +// EffectiveTo applies equality check predicate on the "effective_to" field. It's identical to EffectiveToEQ. +func EffectiveTo(v time.Time) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldEQ(FieldEffectiveTo, v)) +} + // NamespaceEQ applies the EQ predicate on the "namespace" field. func NamespaceEQ(v string) predicate.CurrencyCostBasis { return predicate.CurrencyCostBasis(sql.FieldEQ(FieldNamespace, v)) @@ -303,69 +308,69 @@ func DeletedAtNotNil() predicate.CurrencyCostBasis { return predicate.CurrencyCostBasis(sql.FieldNotNull(FieldDeletedAt)) } -// CustomCurrencyIDEQ applies the EQ predicate on the "custom_currency_id" field. -func CustomCurrencyIDEQ(v string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldEQ(FieldCustomCurrencyID, v)) +// CurrencyIDEQ applies the EQ predicate on the "currency_id" field. +func CurrencyIDEQ(v string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldEQ(FieldCurrencyID, v)) } -// CustomCurrencyIDNEQ applies the NEQ predicate on the "custom_currency_id" field. -func CustomCurrencyIDNEQ(v string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldNEQ(FieldCustomCurrencyID, v)) +// CurrencyIDNEQ applies the NEQ predicate on the "currency_id" field. +func CurrencyIDNEQ(v string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldNEQ(FieldCurrencyID, v)) } -// CustomCurrencyIDIn applies the In predicate on the "custom_currency_id" field. -func CustomCurrencyIDIn(vs ...string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldIn(FieldCustomCurrencyID, vs...)) +// CurrencyIDIn applies the In predicate on the "currency_id" field. +func CurrencyIDIn(vs ...string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldIn(FieldCurrencyID, vs...)) } -// CustomCurrencyIDNotIn applies the NotIn predicate on the "custom_currency_id" field. -func CustomCurrencyIDNotIn(vs ...string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldNotIn(FieldCustomCurrencyID, vs...)) +// CurrencyIDNotIn applies the NotIn predicate on the "currency_id" field. +func CurrencyIDNotIn(vs ...string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldNotIn(FieldCurrencyID, vs...)) } -// CustomCurrencyIDGT applies the GT predicate on the "custom_currency_id" field. -func CustomCurrencyIDGT(v string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldGT(FieldCustomCurrencyID, v)) +// CurrencyIDGT applies the GT predicate on the "currency_id" field. +func CurrencyIDGT(v string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldGT(FieldCurrencyID, v)) } -// CustomCurrencyIDGTE applies the GTE predicate on the "custom_currency_id" field. -func CustomCurrencyIDGTE(v string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldGTE(FieldCustomCurrencyID, v)) +// CurrencyIDGTE applies the GTE predicate on the "currency_id" field. +func CurrencyIDGTE(v string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldGTE(FieldCurrencyID, v)) } -// CustomCurrencyIDLT applies the LT predicate on the "custom_currency_id" field. -func CustomCurrencyIDLT(v string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldLT(FieldCustomCurrencyID, v)) +// CurrencyIDLT applies the LT predicate on the "currency_id" field. +func CurrencyIDLT(v string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldLT(FieldCurrencyID, v)) } -// CustomCurrencyIDLTE applies the LTE predicate on the "custom_currency_id" field. -func CustomCurrencyIDLTE(v string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldLTE(FieldCustomCurrencyID, v)) +// CurrencyIDLTE applies the LTE predicate on the "currency_id" field. +func CurrencyIDLTE(v string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldLTE(FieldCurrencyID, v)) } -// CustomCurrencyIDContains applies the Contains predicate on the "custom_currency_id" field. -func CustomCurrencyIDContains(v string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldContains(FieldCustomCurrencyID, v)) +// CurrencyIDContains applies the Contains predicate on the "currency_id" field. +func CurrencyIDContains(v string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldContains(FieldCurrencyID, v)) } -// CustomCurrencyIDHasPrefix applies the HasPrefix predicate on the "custom_currency_id" field. -func CustomCurrencyIDHasPrefix(v string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldHasPrefix(FieldCustomCurrencyID, v)) +// CurrencyIDHasPrefix applies the HasPrefix predicate on the "currency_id" field. +func CurrencyIDHasPrefix(v string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldHasPrefix(FieldCurrencyID, v)) } -// CustomCurrencyIDHasSuffix applies the HasSuffix predicate on the "custom_currency_id" field. -func CustomCurrencyIDHasSuffix(v string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldHasSuffix(FieldCustomCurrencyID, v)) +// CurrencyIDHasSuffix applies the HasSuffix predicate on the "currency_id" field. +func CurrencyIDHasSuffix(v string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldHasSuffix(FieldCurrencyID, v)) } -// CustomCurrencyIDEqualFold applies the EqualFold predicate on the "custom_currency_id" field. -func CustomCurrencyIDEqualFold(v string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldEqualFold(FieldCustomCurrencyID, v)) +// CurrencyIDEqualFold applies the EqualFold predicate on the "currency_id" field. +func CurrencyIDEqualFold(v string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldEqualFold(FieldCurrencyID, v)) } -// CustomCurrencyIDContainsFold applies the ContainsFold predicate on the "custom_currency_id" field. -func CustomCurrencyIDContainsFold(v string) predicate.CurrencyCostBasis { - return predicate.CurrencyCostBasis(sql.FieldContainsFold(FieldCustomCurrencyID, v)) +// CurrencyIDContainsFold applies the ContainsFold predicate on the "currency_id" field. +func CurrencyIDContainsFold(v string) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldContainsFold(FieldCurrencyID, v)) } // FiatCodeEQ applies the EQ predicate on the "fiat_code" field. @@ -532,6 +537,56 @@ func EffectiveFromLTE(v time.Time) predicate.CurrencyCostBasis { return predicate.CurrencyCostBasis(sql.FieldLTE(FieldEffectiveFrom, v)) } +// EffectiveToEQ applies the EQ predicate on the "effective_to" field. +func EffectiveToEQ(v time.Time) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldEQ(FieldEffectiveTo, v)) +} + +// EffectiveToNEQ applies the NEQ predicate on the "effective_to" field. +func EffectiveToNEQ(v time.Time) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldNEQ(FieldEffectiveTo, v)) +} + +// EffectiveToIn applies the In predicate on the "effective_to" field. +func EffectiveToIn(vs ...time.Time) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldIn(FieldEffectiveTo, vs...)) +} + +// EffectiveToNotIn applies the NotIn predicate on the "effective_to" field. +func EffectiveToNotIn(vs ...time.Time) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldNotIn(FieldEffectiveTo, vs...)) +} + +// EffectiveToGT applies the GT predicate on the "effective_to" field. +func EffectiveToGT(v time.Time) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldGT(FieldEffectiveTo, v)) +} + +// EffectiveToGTE applies the GTE predicate on the "effective_to" field. +func EffectiveToGTE(v time.Time) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldGTE(FieldEffectiveTo, v)) +} + +// EffectiveToLT applies the LT predicate on the "effective_to" field. +func EffectiveToLT(v time.Time) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldLT(FieldEffectiveTo, v)) +} + +// EffectiveToLTE applies the LTE predicate on the "effective_to" field. +func EffectiveToLTE(v time.Time) predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldLTE(FieldEffectiveTo, v)) +} + +// EffectiveToIsNil applies the IsNil predicate on the "effective_to" field. +func EffectiveToIsNil() predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldIsNull(FieldEffectiveTo)) +} + +// EffectiveToNotNil applies the NotNil predicate on the "effective_to" field. +func EffectiveToNotNil() predicate.CurrencyCostBasis { + return predicate.CurrencyCostBasis(sql.FieldNotNull(FieldEffectiveTo)) +} + // HasCurrency applies the HasEdge predicate on the "currency" edge. func HasCurrency() predicate.CurrencyCostBasis { return predicate.CurrencyCostBasis(func(s *sql.Selector) { diff --git a/openmeter/ent/db/currencycostbasis_create.go b/openmeter/ent/db/currencycostbasis_create.go index 05cd44b22a..98ba67b7c0 100644 --- a/openmeter/ent/db/currencycostbasis_create.go +++ b/openmeter/ent/db/currencycostbasis_create.go @@ -74,9 +74,9 @@ func (_c *CurrencyCostBasisCreate) SetNillableDeletedAt(v *time.Time) *CurrencyC return _c } -// SetCustomCurrencyID sets the "custom_currency_id" field. -func (_c *CurrencyCostBasisCreate) SetCustomCurrencyID(v string) *CurrencyCostBasisCreate { - _c.mutation.SetCustomCurrencyID(v) +// SetCurrencyID sets the "currency_id" field. +func (_c *CurrencyCostBasisCreate) SetCurrencyID(v string) *CurrencyCostBasisCreate { + _c.mutation.SetCurrencyID(v) return _c } @@ -98,6 +98,20 @@ func (_c *CurrencyCostBasisCreate) SetEffectiveFrom(v time.Time) *CurrencyCostBa return _c } +// SetEffectiveTo sets the "effective_to" field. +func (_c *CurrencyCostBasisCreate) SetEffectiveTo(v time.Time) *CurrencyCostBasisCreate { + _c.mutation.SetEffectiveTo(v) + return _c +} + +// SetNillableEffectiveTo sets the "effective_to" field if the given value is not nil. +func (_c *CurrencyCostBasisCreate) SetNillableEffectiveTo(v *time.Time) *CurrencyCostBasisCreate { + if v != nil { + _c.SetEffectiveTo(*v) + } + return _c +} + // SetID sets the "id" field. func (_c *CurrencyCostBasisCreate) SetID(v string) *CurrencyCostBasisCreate { _c.mutation.SetID(v) @@ -112,12 +126,6 @@ func (_c *CurrencyCostBasisCreate) SetNillableID(v *string) *CurrencyCostBasisCr return _c } -// SetCurrencyID sets the "currency" edge to the CustomCurrency entity by ID. -func (_c *CurrencyCostBasisCreate) SetCurrencyID(id string) *CurrencyCostBasisCreate { - _c.mutation.SetCurrencyID(id) - return _c -} - // SetCurrency sets the "currency" edge to the CustomCurrency entity. func (_c *CurrencyCostBasisCreate) SetCurrency(v *CustomCurrency) *CurrencyCostBasisCreate { return _c.SetCurrencyID(v.ID) @@ -188,8 +196,8 @@ func (_c *CurrencyCostBasisCreate) check() error { if _, ok := _c.mutation.UpdatedAt(); !ok { return &ValidationError{Name: "updated_at", err: errors.New(`db: missing required field "CurrencyCostBasis.updated_at"`)} } - if _, ok := _c.mutation.CustomCurrencyID(); !ok { - return &ValidationError{Name: "custom_currency_id", err: errors.New(`db: missing required field "CurrencyCostBasis.custom_currency_id"`)} + if _, ok := _c.mutation.CurrencyID(); !ok { + return &ValidationError{Name: "currency_id", err: errors.New(`db: missing required field "CurrencyCostBasis.currency_id"`)} } if _, ok := _c.mutation.FiatCode(); !ok { return &ValidationError{Name: "fiat_code", err: errors.New(`db: missing required field "CurrencyCostBasis.fiat_code"`)} @@ -272,6 +280,10 @@ func (_c *CurrencyCostBasisCreate) createSpec() (*CurrencyCostBasis, *sqlgraph.C _spec.SetField(currencycostbasis.FieldEffectiveFrom, field.TypeTime, value) _node.EffectiveFrom = value } + if value, ok := _c.mutation.EffectiveTo(); ok { + _spec.SetField(currencycostbasis.FieldEffectiveTo, field.TypeTime, value) + _node.EffectiveTo = &value + } if nodes := _c.mutation.CurrencyIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -286,7 +298,7 @@ func (_c *CurrencyCostBasisCreate) createSpec() (*CurrencyCostBasis, *sqlgraph.C for _, k := range nodes { edge.Target.Nodes = append(edge.Target.Nodes, k) } - _node.CustomCurrencyID = nodes[0] + _node.CurrencyID = nodes[0] _spec.Edges = append(_spec.Edges, edge) } return _node, _spec @@ -394,8 +406,8 @@ func (u *CurrencyCostBasisUpsertOne) UpdateNewValues() *CurrencyCostBasisUpsertO if _, exists := u.create.mutation.CreatedAt(); exists { s.SetIgnore(currencycostbasis.FieldCreatedAt) } - if _, exists := u.create.mutation.CustomCurrencyID(); exists { - s.SetIgnore(currencycostbasis.FieldCustomCurrencyID) + if _, exists := u.create.mutation.CurrencyID(); exists { + s.SetIgnore(currencycostbasis.FieldCurrencyID) } if _, exists := u.create.mutation.FiatCode(); exists { s.SetIgnore(currencycostbasis.FieldFiatCode) @@ -406,6 +418,9 @@ func (u *CurrencyCostBasisUpsertOne) UpdateNewValues() *CurrencyCostBasisUpsertO if _, exists := u.create.mutation.EffectiveFrom(); exists { s.SetIgnore(currencycostbasis.FieldEffectiveFrom) } + if _, exists := u.create.mutation.EffectiveTo(); exists { + s.SetIgnore(currencycostbasis.FieldEffectiveTo) + } })) return u } @@ -661,8 +676,8 @@ func (u *CurrencyCostBasisUpsertBulk) UpdateNewValues() *CurrencyCostBasisUpsert if _, exists := b.mutation.CreatedAt(); exists { s.SetIgnore(currencycostbasis.FieldCreatedAt) } - if _, exists := b.mutation.CustomCurrencyID(); exists { - s.SetIgnore(currencycostbasis.FieldCustomCurrencyID) + if _, exists := b.mutation.CurrencyID(); exists { + s.SetIgnore(currencycostbasis.FieldCurrencyID) } if _, exists := b.mutation.FiatCode(); exists { s.SetIgnore(currencycostbasis.FieldFiatCode) @@ -673,6 +688,9 @@ func (u *CurrencyCostBasisUpsertBulk) UpdateNewValues() *CurrencyCostBasisUpsert if _, exists := b.mutation.EffectiveFrom(); exists { s.SetIgnore(currencycostbasis.FieldEffectiveFrom) } + if _, exists := b.mutation.EffectiveTo(); exists { + s.SetIgnore(currencycostbasis.FieldEffectiveTo) + } } })) return u diff --git a/openmeter/ent/db/currencycostbasis_query.go b/openmeter/ent/db/currencycostbasis_query.go index a8cf17c464..ea1eb9c0cd 100644 --- a/openmeter/ent/db/currencycostbasis_query.go +++ b/openmeter/ent/db/currencycostbasis_query.go @@ -410,7 +410,7 @@ func (_q *CurrencyCostBasisQuery) loadCurrency(ctx context.Context, query *Custo ids := make([]string, 0, len(nodes)) nodeids := make(map[string][]*CurrencyCostBasis) for i := range nodes { - fk := nodes[i].CustomCurrencyID + fk := nodes[i].CurrencyID if _, ok := nodeids[fk]; !ok { ids = append(ids, fk) } @@ -427,7 +427,7 @@ func (_q *CurrencyCostBasisQuery) loadCurrency(ctx context.Context, query *Custo for _, n := range neighbors { nodes, ok := nodeids[n.ID] if !ok { - return fmt.Errorf(`unexpected foreign-key "custom_currency_id" returned %v`, n.ID) + return fmt.Errorf(`unexpected foreign-key "currency_id" returned %v`, n.ID) } for i := range nodes { assign(nodes[i], n) @@ -465,7 +465,7 @@ func (_q *CurrencyCostBasisQuery) querySpec() *sqlgraph.QuerySpec { } } if _q.withCurrency != nil { - _spec.Node.AddColumnOnce(currencycostbasis.FieldCustomCurrencyID) + _spec.Node.AddColumnOnce(currencycostbasis.FieldCurrencyID) } } if ps := _q.predicates; len(ps) > 0 { diff --git a/openmeter/ent/db/currencycostbasis_update.go b/openmeter/ent/db/currencycostbasis_update.go index 2e72b8f33f..d8ce4aff15 100644 --- a/openmeter/ent/db/currencycostbasis_update.go +++ b/openmeter/ent/db/currencycostbasis_update.go @@ -124,6 +124,9 @@ func (_u *CurrencyCostBasisUpdate) sqlSave(ctx context.Context) (_node int, err if _u.mutation.DeletedAtCleared() { _spec.ClearField(currencycostbasis.FieldDeletedAt, field.TypeTime) } + if _u.mutation.EffectiveToCleared() { + _spec.ClearField(currencycostbasis.FieldEffectiveTo, field.TypeTime) + } if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { err = &NotFoundError{currencycostbasis.Label} @@ -270,6 +273,9 @@ func (_u *CurrencyCostBasisUpdateOne) sqlSave(ctx context.Context) (_node *Curre if _u.mutation.DeletedAtCleared() { _spec.ClearField(currencycostbasis.FieldDeletedAt, field.TypeTime) } + if _u.mutation.EffectiveToCleared() { + _spec.ClearField(currencycostbasis.FieldEffectiveTo, field.TypeTime) + } _node = &CurrencyCostBasis{config: _u.config} _spec.Assign = _node.assignValues _spec.ScanValues = _node.scanValues diff --git a/openmeter/ent/db/customcurrency/customcurrency.go b/openmeter/ent/db/customcurrency/customcurrency.go index 3bbf475343..b75965f3b0 100644 --- a/openmeter/ent/db/customcurrency/customcurrency.go +++ b/openmeter/ent/db/customcurrency/customcurrency.go @@ -38,7 +38,7 @@ const ( // It exists in this package in order to avoid circular dependency with the "currencycostbasis" package. CostBasisHistoryInverseTable = "currency_cost_bases" // CostBasisHistoryColumn is the table column denoting the cost_basis_history relation/edge. - CostBasisHistoryColumn = "custom_currency_id" + CostBasisHistoryColumn = "currency_id" ) // Columns holds all SQL columns for customcurrency fields. diff --git a/openmeter/ent/db/customcurrency_query.go b/openmeter/ent/db/customcurrency_query.go index 902d5656f5..c60f04cbdc 100644 --- a/openmeter/ent/db/customcurrency_query.go +++ b/openmeter/ent/db/customcurrency_query.go @@ -421,7 +421,7 @@ func (_q *CustomCurrencyQuery) loadCostBasisHistory(ctx context.Context, query * } } if len(query.ctx.Fields) > 0 { - query.ctx.AppendFieldOnce(currencycostbasis.FieldCustomCurrencyID) + query.ctx.AppendFieldOnce(currencycostbasis.FieldCurrencyID) } query.Where(predicate.CurrencyCostBasis(func(s *sql.Selector) { s.Where(sql.InValues(s.C(customcurrency.CostBasisHistoryColumn), fks...)) @@ -431,10 +431,10 @@ func (_q *CustomCurrencyQuery) loadCostBasisHistory(ctx context.Context, query * return err } for _, n := range neighbors { - fk := n.CustomCurrencyID + fk := n.CurrencyID node, ok := nodeids[fk] if !ok { - return fmt.Errorf(`unexpected referenced foreign-key "custom_currency_id" returned %v for node %v`, fk, n.ID) + return fmt.Errorf(`unexpected referenced foreign-key "currency_id" returned %v for node %v`, fk, n.ID) } assign(node, n) } diff --git a/openmeter/ent/db/migrate/schema.go b/openmeter/ent/db/migrate/schema.go index 811cbe6319..4ca0592974 100644 --- a/openmeter/ent/db/migrate/schema.go +++ b/openmeter/ent/db/migrate/schema.go @@ -3104,7 +3104,8 @@ var ( {Name: "fiat_code", Type: field.TypeString}, {Name: "rate", Type: field.TypeOther, SchemaType: map[string]string{"postgres": "numeric"}}, {Name: "effective_from", Type: field.TypeTime}, - {Name: "custom_currency_id", Type: field.TypeString, SchemaType: map[string]string{"postgres": "char(26)"}}, + {Name: "effective_to", Type: field.TypeTime, Nullable: true}, + {Name: "currency_id", Type: field.TypeString, SchemaType: map[string]string{"postgres": "char(26)"}}, } // CurrencyCostBasesTable holds the schema information for the "currency_cost_bases" table. CurrencyCostBasesTable = &schema.Table{ @@ -3114,7 +3115,7 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "currency_cost_bases_custom_currencies_cost_basis_history", - Columns: []*schema.Column{CurrencyCostBasesColumns[8]}, + Columns: []*schema.Column{CurrencyCostBasesColumns[9]}, RefColumns: []*schema.Column{CustomCurrenciesColumns[0]}, OnDelete: schema.Cascade, }, @@ -3131,9 +3132,9 @@ var ( Columns: []*schema.Column{CurrencyCostBasesColumns[0]}, }, { - Name: "currencycostbasis_namespace_custom_currency_id_fiat_code_effective_from", + Name: "currencycostbasis_namespace_currency_id_fiat_code_effective_from", Unique: true, - Columns: []*schema.Column{CurrencyCostBasesColumns[1], CurrencyCostBasesColumns[8], CurrencyCostBasesColumns[5], CurrencyCostBasesColumns[7]}, + Columns: []*schema.Column{CurrencyCostBasesColumns[1], CurrencyCostBasesColumns[9], CurrencyCostBasesColumns[5], CurrencyCostBasesColumns[7]}, Annotation: &entsql.IndexAnnotation{ Where: "deleted_at IS NULL", }, diff --git a/openmeter/ent/db/mutation.go b/openmeter/ent/db/mutation.go index a8b482dc88..54d58e8819 100644 --- a/openmeter/ent/db/mutation.go +++ b/openmeter/ent/db/mutation.go @@ -70266,6 +70266,7 @@ type CurrencyCostBasisMutation struct { fiat_code *currencyx.Code rate *alpacadecimal.Decimal effective_from *time.Time + effective_to *time.Time clearedFields map[string]struct{} currency *string clearedcurrency bool @@ -70535,13 +70536,13 @@ func (m *CurrencyCostBasisMutation) ResetDeletedAt() { delete(m.clearedFields, currencycostbasis.FieldDeletedAt) } -// SetCustomCurrencyID sets the "custom_currency_id" field. -func (m *CurrencyCostBasisMutation) SetCustomCurrencyID(s string) { +// SetCurrencyID sets the "currency_id" field. +func (m *CurrencyCostBasisMutation) SetCurrencyID(s string) { m.currency = &s } -// CustomCurrencyID returns the value of the "custom_currency_id" field in the mutation. -func (m *CurrencyCostBasisMutation) CustomCurrencyID() (r string, exists bool) { +// CurrencyID returns the value of the "currency_id" field in the mutation. +func (m *CurrencyCostBasisMutation) CurrencyID() (r string, exists bool) { v := m.currency if v == nil { return @@ -70549,25 +70550,25 @@ func (m *CurrencyCostBasisMutation) CustomCurrencyID() (r string, exists bool) { return *v, true } -// OldCustomCurrencyID returns the old "custom_currency_id" field's value of the CurrencyCostBasis entity. +// OldCurrencyID returns the old "currency_id" field's value of the CurrencyCostBasis entity. // If the CurrencyCostBasis object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *CurrencyCostBasisMutation) OldCustomCurrencyID(ctx context.Context) (v string, err error) { +func (m *CurrencyCostBasisMutation) OldCurrencyID(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldCustomCurrencyID is only allowed on UpdateOne operations") + return v, errors.New("OldCurrencyID is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldCustomCurrencyID requires an ID field in the mutation") + return v, errors.New("OldCurrencyID requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldCustomCurrencyID: %w", err) + return v, fmt.Errorf("querying old value for OldCurrencyID: %w", err) } - return oldValue.CustomCurrencyID, nil + return oldValue.CurrencyID, nil } -// ResetCustomCurrencyID resets all changes to the "custom_currency_id" field. -func (m *CurrencyCostBasisMutation) ResetCustomCurrencyID() { +// ResetCurrencyID resets all changes to the "currency_id" field. +func (m *CurrencyCostBasisMutation) ResetCurrencyID() { m.currency = nil } @@ -70679,15 +70680,59 @@ func (m *CurrencyCostBasisMutation) ResetEffectiveFrom() { m.effective_from = nil } -// SetCurrencyID sets the "currency" edge to the CustomCurrency entity by id. -func (m *CurrencyCostBasisMutation) SetCurrencyID(id string) { - m.currency = &id +// SetEffectiveTo sets the "effective_to" field. +func (m *CurrencyCostBasisMutation) SetEffectiveTo(t time.Time) { + m.effective_to = &t +} + +// EffectiveTo returns the value of the "effective_to" field in the mutation. +func (m *CurrencyCostBasisMutation) EffectiveTo() (r time.Time, exists bool) { + v := m.effective_to + if v == nil { + return + } + return *v, true +} + +// OldEffectiveTo returns the old "effective_to" field's value of the CurrencyCostBasis entity. +// If the CurrencyCostBasis object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *CurrencyCostBasisMutation) OldEffectiveTo(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldEffectiveTo is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldEffectiveTo requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldEffectiveTo: %w", err) + } + return oldValue.EffectiveTo, nil +} + +// ClearEffectiveTo clears the value of the "effective_to" field. +func (m *CurrencyCostBasisMutation) ClearEffectiveTo() { + m.effective_to = nil + m.clearedFields[currencycostbasis.FieldEffectiveTo] = struct{}{} +} + +// EffectiveToCleared returns if the "effective_to" field was cleared in this mutation. +func (m *CurrencyCostBasisMutation) EffectiveToCleared() bool { + _, ok := m.clearedFields[currencycostbasis.FieldEffectiveTo] + return ok +} + +// ResetEffectiveTo resets all changes to the "effective_to" field. +func (m *CurrencyCostBasisMutation) ResetEffectiveTo() { + m.effective_to = nil + delete(m.clearedFields, currencycostbasis.FieldEffectiveTo) } // ClearCurrency clears the "currency" edge to the CustomCurrency entity. func (m *CurrencyCostBasisMutation) ClearCurrency() { m.clearedcurrency = true - m.clearedFields[currencycostbasis.FieldCustomCurrencyID] = struct{}{} + m.clearedFields[currencycostbasis.FieldCurrencyID] = struct{}{} } // CurrencyCleared reports if the "currency" edge to the CustomCurrency entity was cleared. @@ -70695,14 +70740,6 @@ func (m *CurrencyCostBasisMutation) CurrencyCleared() bool { return m.clearedcurrency } -// CurrencyID returns the "currency" edge ID in the mutation. -func (m *CurrencyCostBasisMutation) CurrencyID() (id string, exists bool) { - if m.currency != nil { - return *m.currency, true - } - return -} - // CurrencyIDs returns the "currency" edge IDs in the mutation. // Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use // CurrencyID instead. It exists only for internal usage by the builders. @@ -70753,7 +70790,7 @@ func (m *CurrencyCostBasisMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *CurrencyCostBasisMutation) Fields() []string { - fields := make([]string, 0, 8) + fields := make([]string, 0, 9) if m.namespace != nil { fields = append(fields, currencycostbasis.FieldNamespace) } @@ -70767,7 +70804,7 @@ func (m *CurrencyCostBasisMutation) Fields() []string { fields = append(fields, currencycostbasis.FieldDeletedAt) } if m.currency != nil { - fields = append(fields, currencycostbasis.FieldCustomCurrencyID) + fields = append(fields, currencycostbasis.FieldCurrencyID) } if m.fiat_code != nil { fields = append(fields, currencycostbasis.FieldFiatCode) @@ -70778,6 +70815,9 @@ func (m *CurrencyCostBasisMutation) Fields() []string { if m.effective_from != nil { fields = append(fields, currencycostbasis.FieldEffectiveFrom) } + if m.effective_to != nil { + fields = append(fields, currencycostbasis.FieldEffectiveTo) + } return fields } @@ -70794,14 +70834,16 @@ func (m *CurrencyCostBasisMutation) Field(name string) (ent.Value, bool) { return m.UpdatedAt() case currencycostbasis.FieldDeletedAt: return m.DeletedAt() - case currencycostbasis.FieldCustomCurrencyID: - return m.CustomCurrencyID() + case currencycostbasis.FieldCurrencyID: + return m.CurrencyID() case currencycostbasis.FieldFiatCode: return m.FiatCode() case currencycostbasis.FieldRate: return m.Rate() case currencycostbasis.FieldEffectiveFrom: return m.EffectiveFrom() + case currencycostbasis.FieldEffectiveTo: + return m.EffectiveTo() } return nil, false } @@ -70819,14 +70861,16 @@ func (m *CurrencyCostBasisMutation) OldField(ctx context.Context, name string) ( return m.OldUpdatedAt(ctx) case currencycostbasis.FieldDeletedAt: return m.OldDeletedAt(ctx) - case currencycostbasis.FieldCustomCurrencyID: - return m.OldCustomCurrencyID(ctx) + case currencycostbasis.FieldCurrencyID: + return m.OldCurrencyID(ctx) case currencycostbasis.FieldFiatCode: return m.OldFiatCode(ctx) case currencycostbasis.FieldRate: return m.OldRate(ctx) case currencycostbasis.FieldEffectiveFrom: return m.OldEffectiveFrom(ctx) + case currencycostbasis.FieldEffectiveTo: + return m.OldEffectiveTo(ctx) } return nil, fmt.Errorf("unknown CurrencyCostBasis field %s", name) } @@ -70864,12 +70908,12 @@ func (m *CurrencyCostBasisMutation) SetField(name string, value ent.Value) error } m.SetDeletedAt(v) return nil - case currencycostbasis.FieldCustomCurrencyID: + case currencycostbasis.FieldCurrencyID: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetCustomCurrencyID(v) + m.SetCurrencyID(v) return nil case currencycostbasis.FieldFiatCode: v, ok := value.(currencyx.Code) @@ -70892,6 +70936,13 @@ func (m *CurrencyCostBasisMutation) SetField(name string, value ent.Value) error } m.SetEffectiveFrom(v) return nil + case currencycostbasis.FieldEffectiveTo: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetEffectiveTo(v) + return nil } return fmt.Errorf("unknown CurrencyCostBasis field %s", name) } @@ -70925,6 +70976,9 @@ func (m *CurrencyCostBasisMutation) ClearedFields() []string { if m.FieldCleared(currencycostbasis.FieldDeletedAt) { fields = append(fields, currencycostbasis.FieldDeletedAt) } + if m.FieldCleared(currencycostbasis.FieldEffectiveTo) { + fields = append(fields, currencycostbasis.FieldEffectiveTo) + } return fields } @@ -70942,6 +70996,9 @@ func (m *CurrencyCostBasisMutation) ClearField(name string) error { case currencycostbasis.FieldDeletedAt: m.ClearDeletedAt() return nil + case currencycostbasis.FieldEffectiveTo: + m.ClearEffectiveTo() + return nil } return fmt.Errorf("unknown CurrencyCostBasis nullable field %s", name) } @@ -70962,8 +71019,8 @@ func (m *CurrencyCostBasisMutation) ResetField(name string) error { case currencycostbasis.FieldDeletedAt: m.ResetDeletedAt() return nil - case currencycostbasis.FieldCustomCurrencyID: - m.ResetCustomCurrencyID() + case currencycostbasis.FieldCurrencyID: + m.ResetCurrencyID() return nil case currencycostbasis.FieldFiatCode: m.ResetFiatCode() @@ -70974,6 +71031,9 @@ func (m *CurrencyCostBasisMutation) ResetField(name string) error { case currencycostbasis.FieldEffectiveFrom: m.ResetEffectiveFrom() return nil + case currencycostbasis.FieldEffectiveTo: + m.ResetEffectiveTo() + return nil } return fmt.Errorf("unknown CurrencyCostBasis field %s", name) } diff --git a/openmeter/ent/schema/custom_currencies.go b/openmeter/ent/schema/custom_currencies.go index f88db7373b..006129bd73 100644 --- a/openmeter/ent/schema/custom_currencies.go +++ b/openmeter/ent/schema/custom_currencies.go @@ -72,7 +72,7 @@ func (CurrencyCostBasis) Mixin() []ent.Mixin { func (CurrencyCostBasis) Fields() []ent.Field { return []ent.Field{ - field.String("custom_currency_id"). + field.String("currency_id"). SchemaType(map[string]string{ dialect.Postgres: "char(26)", }). @@ -88,6 +88,10 @@ func (CurrencyCostBasis) Fields() []ent.Field { Immutable(), field.Time("effective_from"). Immutable(), + field.Time("effective_to"). + Optional(). + Nillable(). + Immutable(), } } @@ -96,7 +100,7 @@ func (CurrencyCostBasis) Edges() []ent.Edge { // Many cost basis entries belong to one currency edge.From("currency", CustomCurrency.Type). Ref("cost_basis_history"). - Field("custom_currency_id"). + Field("currency_id"). Unique(). Required(). Immutable(), @@ -105,7 +109,7 @@ func (CurrencyCostBasis) Edges() []ent.Edge { func (CurrencyCostBasis) Indexes() []ent.Index { return []ent.Index{ - index.Fields("namespace", "custom_currency_id", "fiat_code", "effective_from"). + index.Fields("namespace", "currency_id", "fiat_code", "effective_from"). Annotations( entsql.IndexWhere("deleted_at IS NULL"), ). diff --git a/openmeter/server/router/router.go b/openmeter/server/router/router.go index 42dc6a5e25..a5d318effe 100644 --- a/openmeter/server/router/router.go +++ b/openmeter/server/router/router.go @@ -1,6 +1,7 @@ package router import ( + "context" "encoding/json" "errors" "fmt" @@ -12,6 +13,7 @@ import ( "github.com/getkin/kin-openapi/openapi3filter" "github.com/openmeterio/openmeter/api" + v3apierrors "github.com/openmeterio/openmeter/api/v3/apierrors" currencyhandler "github.com/openmeterio/openmeter/api/v3/handlers/currencies" "github.com/openmeterio/openmeter/app/config" "github.com/openmeterio/openmeter/openmeter/app" @@ -511,8 +513,17 @@ func NewRouter(config Config) (*Router, error) { ) // Currencies + resolveNamespace := func(ctx context.Context) (string, error) { + ns, ok := config.NamespaceDecoder.GetNamespace(ctx) + if !ok { + return "", v3apierrors.NewInternalError(ctx, errors.New("failed to resolve namespace")) + } + + return ns, nil + } + router.currencyHandler = currencyhandler.New( - config.NamespaceDecoder, + resolveNamespace, config.CurrencyService, httptransport.WithErrorHandler(config.ErrorHandler), ) diff --git a/pkg/currencyx/README.md b/pkg/currencyx/README.md new file mode 100644 index 0000000000..60d88b6c87 --- /dev/null +++ b/pkg/currencyx/README.md @@ -0,0 +1,48 @@ +# currencyx + +`currencyx` contains OpenMeter's shared currency primitives. It keeps fiat +behavior compatible with GOBL/ISO currency definitions while allowing product +and ledger code to pass configured custom currencies through the same +`Currency` interface. + +## Fiat Currency + +Use `Code` directly for fiat currencies. Fiat precision comes from the GOBL +currency definition and fiat rounding remains half-away-from-zero. + +```go +calculator, err := currencyx.Code("USD").Calculator() +if err != nil { + return err +} + +amount := calculator.RoundToPrecision(alpacadecimal.RequireFromString("1.235")) +// amount == 1.24 +``` + +## Custom Currency + +Use `NewCustomCurrency` when the currency is not a known fiat code. Custom +currencies carry explicit precision and default to bankers rounding +(half-even). Use `NewCustomCurrencyWithRounding` to opt into half-away-from-zero. + +```go +credits, err := currencyx.NewCustomCurrency(currencyx.Code("CREDITS"), 6) +if err != nil { + return err +} + +calculator, err := currencyx.NewCalculator(credits) +if err != nil { + return err +} + +amount := calculator.RoundToPrecision(alpacadecimal.RequireFromString("1.2345678")) +// amount == 1.234568 +``` + +## Allocation + +Allocation helpers use the calculator's precision and distribute residual units +with a deterministic largest-remainder method. Provide a `CompareKey` function +when equal remainders need a stable domain-specific tie-breaker. diff --git a/pkg/currencyx/allocation.go b/pkg/currencyx/allocation.go index c0f40a2647..5058bab048 100644 --- a/pkg/currencyx/allocation.go +++ b/pkg/currencyx/allocation.go @@ -85,7 +85,7 @@ func AllocateByWeight[T any](calculator Calculator, input WeightedAllocationInpu allocated := alpacadecimal.Zero for i, item := range input.Items { share := input.Amount.Mul(item.Weight).Div(totalWeight) - amount := share.RoundDown(int32(calculator.Def.Subunits)) + amount := calculator.RoundDown(share) candidates = append(candidates, allocationCandidate{ index: i, @@ -169,7 +169,7 @@ func AllocateByAmount[T any](calculator Calculator, input AmountAllocationInput[ allocated := alpacadecimal.Zero for i, item := range input.Items { share := input.Amount.Mul(item.Amount).Div(totalAmount) - floor := share.RoundDown(int32(calculator.Def.Subunits)) + floor := calculator.RoundDown(share) candidates = append(candidates, allocationCandidate{ index: i, @@ -312,5 +312,5 @@ func validateAmountAllocationInput[T any](calculator Calculator, input AmountAll } func currencyUnit(calculator Calculator) alpacadecimal.Decimal { - return alpacadecimal.NewFromInt(1).Shift(-int32(calculator.Def.Subunits)) + return calculator.Unit() } diff --git a/pkg/currencyx/allocation_test.go b/pkg/currencyx/allocation_test.go index 56c0b3aaa1..fedeaa6a3c 100644 --- a/pkg/currencyx/allocation_test.go +++ b/pkg/currencyx/allocation_test.go @@ -87,6 +87,24 @@ func TestAllocateByWeight(t *testing.T) { }, allocations) }) + t.Run("uses custom currency precision", func(t *testing.T) { + custom := testCustomCalculator(t, "CREDITS", 4) + + allocations, err := currencyx.AllocateByWeight(custom, currencyx.WeightedAllocationInput[string]{ + Amount: dec("0.0005"), + Items: []currencyx.WeightedAllocationItem[string]{ + {Key: "A", Weight: dec("1")}, + {Key: "B", Weight: dec("1")}, + }, + }) + require.NoError(t, err) + + requireAllocationsEqual(t, []currencyx.WeightedAllocation[string]{ + {Key: "A", Amount: dec("0.0003")}, + {Key: "B", Amount: dec("0.0002")}, + }, allocations) + }) + t.Run("omits zero allocations", func(t *testing.T) { allocations, err := currencyx.AllocateByWeight(usd, currencyx.WeightedAllocationInput[string]{ Amount: dec("0.01"), @@ -361,6 +379,18 @@ func testCalculator(t *testing.T, code string) currencyx.Calculator { return calculator } +func testCustomCalculator(t *testing.T, code string, precision int32) currencyx.Calculator { + t.Helper() + + currency, err := currencyx.NewCustomCurrency(currencyx.Code(code), precision) + require.NoError(t, err) + + calculator, err := currencyx.NewCalculator(currency) + require.NoError(t, err) + + return calculator +} + func requireAllocationsEqual[T comparable](t *testing.T, expected, actual []currencyx.WeightedAllocation[T]) { t.Helper() diff --git a/pkg/currencyx/currency.go b/pkg/currencyx/currency.go index 9a69042e29..b8078d1cc7 100644 --- a/pkg/currencyx/currency.go +++ b/pkg/currencyx/currency.go @@ -2,59 +2,128 @@ package currencyx import ( "errors" + "fmt" - "github.com/alpacahq/alpacadecimal" "github.com/invopop/gobl/currency" + + "github.com/openmeterio/openmeter/pkg/models" ) -// Currency represents a currency code. -// Three-letter [ISO4217](https://www.iso.org/iso-4217-currency-codes.html) currency code. -type Code currency.Code +type CurrencyType string -func (c Code) Validate() error { - if c == "" { - return errors.New("currency code is required") +const ( + CurrencyTypeFiat CurrencyType = "fiat" + CurrencyTypeCustom CurrencyType = "custom" +) + +func (t CurrencyType) Validate() error { + var errs []error + + switch t { + case CurrencyTypeFiat, CurrencyTypeCustom: + default: + errs = append(errs, fmt.Errorf("invalid currency type: %s", t)) } - return currency.Code(c).Validate() + return models.NewNillableGenericValidationError(errors.Join(errs...)) +} + +type Currency interface { + CurrencyCode() Code + CurrencyType() CurrencyType + CurrencyPrecision() int32 + CurrencyRoundingMode() RoundingMode +} + +// Code represents a fiat or custom currency code. Code values used directly as +// Currency values are treated as fiat currencies for backwards compatibility. +type Code currency.Code + +func (c Code) String() string { + return string(c) +} + +func (c Code) CurrencyCode() Code { + return c } -// Calculator provides a currency calculator object. This allows us to not to resolve def a lot of times, plus -// we can assume that def is always valid, thus we can avoid a lot of error handling. -func (c Code) Calculator() (Calculator, error) { - if err := c.Validate(); err != nil { - return Calculator{}, err +func (c Code) CurrencyType() CurrencyType { + return CurrencyTypeFiat +} + +func (c Code) CurrencyPrecision() int32 { + def := currency.Get(currency.Code(c)) + if def == nil { + return 0 } - return Calculator{ - Currency: c, - Def: currency.Get(currency.Code(c)), - }, nil + return int32(def.Subunits) } -// TODO: Better name?! -type Calculator struct { - Currency Code - Def *currency.Def +func (c Code) CurrencyRoundingMode() RoundingMode { + return RoundingModeHalfAwayFromZero } -func (c Calculator) RoundToPrecision(amount alpacadecimal.Decimal) alpacadecimal.Decimal { - // TODO: For now we are skipping the smallestDenomination, as that is a reference to the coins - // in circulation, but should not be an issue for online payments. - return amount.Round(int32(c.Def.Subunits)) +type CustomCurrency struct { + Code Code + Precision int32 + RoundingMode RoundingMode } -func (c Calculator) Validate() error { - var errs []error - if err := c.Currency.Validate(); err != nil { - errs = append(errs, err) +func NewCurrency(code Code, currencyType CurrencyType, precision int32) (Currency, error) { + switch currencyType { + case CurrencyTypeFiat: + return NewFiatCurrency(code) + case CurrencyTypeCustom: + return NewCustomCurrency(code, precision) + default: + return nil, currencyType.Validate() + } +} + +func NewFiatCurrency(code Code) (Code, error) { + def := currency.Get(currency.Code(code)) + if def == nil || def.ISONumeric == "" { + return "", fmt.Errorf("fiat currency definition is required for %s", code) } - if c.Def == nil { - errs = append(errs, errors.New("currency definition is required")) + + if err := code.Validate(); err != nil { + return "", err + } + + return code, nil +} + +func NewCustomCurrency(code Code, precision int32) (CustomCurrency, error) { + return NewCustomCurrencyWithRounding(code, precision, RoundingModeBankers) +} + +func NewCustomCurrencyWithRounding(code Code, precision int32, roundingMode RoundingMode) (CustomCurrency, error) { + out := CustomCurrency{ + Code: code, + Precision: precision, + RoundingMode: roundingMode, } - return errors.Join(errs...) + + return out, out.Validate() +} + +func (c CustomCurrency) CurrencyCode() Code { + return c.Code +} + +func (c CustomCurrency) CurrencyType() CurrencyType { + return CurrencyTypeCustom } -func (c Calculator) IsRoundedToPrecision(amount alpacadecimal.Decimal) bool { - return amount.Equal(c.RoundToPrecision(amount)) +func (c CustomCurrency) CurrencyPrecision() int32 { + return c.Precision +} + +func (c CustomCurrency) CurrencyRoundingMode() RoundingMode { + if c.RoundingMode == "" { + return RoundingModeBankers + } + + return c.RoundingMode } diff --git a/pkg/currencyx/currency_test.go b/pkg/currencyx/currency_test.go index 3ecadceaf8..fd0914a5e1 100644 --- a/pkg/currencyx/currency_test.go +++ b/pkg/currencyx/currency_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/openmeterio/openmeter/pkg/currencyx" + "github.com/openmeterio/openmeter/pkg/models" ) func TestRoundToPrecision(t *testing.T) { @@ -36,3 +37,259 @@ func TestRoundToPrecision(t *testing.T) { require.Equal(t, c.expected, result) } } + +func TestCurrencyInterface(t *testing.T) { + var fiat currencyx.Currency = currencyx.Code("USD") + require.Equal(t, currencyx.Code("USD"), fiat.CurrencyCode()) + require.Equal(t, currencyx.CurrencyTypeFiat, fiat.CurrencyType()) + require.Equal(t, int32(2), fiat.CurrencyPrecision()) + require.Equal(t, currencyx.RoundingModeHalfAwayFromZero, fiat.CurrencyRoundingMode()) + + custom, err := currencyx.NewCustomCurrency(currencyx.Code("CREDITS"), 6) + require.NoError(t, err) + + var currency currencyx.Currency = custom + require.Equal(t, currencyx.Code("CREDITS"), currency.CurrencyCode()) + require.Equal(t, currencyx.CurrencyTypeCustom, currency.CurrencyType()) + require.Equal(t, int32(6), currency.CurrencyPrecision()) + require.Equal(t, currencyx.RoundingModeBankers, currency.CurrencyRoundingMode()) +} + +func TestNewCurrency(t *testing.T) { + fiat, err := currencyx.NewCurrency(currencyx.Code("JPY"), currencyx.CurrencyTypeFiat, 0) + require.NoError(t, err) + require.Equal(t, currencyx.Code("JPY"), fiat.CurrencyCode()) + require.Equal(t, currencyx.CurrencyTypeFiat, fiat.CurrencyType()) + require.Equal(t, int32(0), fiat.CurrencyPrecision()) + require.Equal(t, currencyx.RoundingModeHalfAwayFromZero, fiat.CurrencyRoundingMode()) + + custom, err := currencyx.NewCurrency(currencyx.Code("CREDITS"), currencyx.CurrencyTypeCustom, 4) + require.NoError(t, err) + require.Equal(t, currencyx.Code("CREDITS"), custom.CurrencyCode()) + require.Equal(t, currencyx.CurrencyTypeCustom, custom.CurrencyType()) + require.Equal(t, int32(4), custom.CurrencyPrecision()) + require.Equal(t, currencyx.RoundingModeBankers, custom.CurrencyRoundingMode()) +} + +func TestCurrencyTypeValidation(t *testing.T) { + require.NoError(t, currencyx.CurrencyTypeFiat.Validate()) + require.NoError(t, currencyx.CurrencyTypeCustom.Validate()) + + err := currencyx.CurrencyType("unknown").Validate() + require.Error(t, err) + require.True(t, models.IsGenericValidationError(err), "error must be a validation error") + require.Contains(t, err.Error(), "invalid currency type: unknown") +} + +func TestRoundingModeValidation(t *testing.T) { + require.NoError(t, currencyx.RoundingModeHalfAwayFromZero.Validate()) + require.NoError(t, currencyx.RoundingModeBankers.Validate()) + + err := currencyx.RoundingMode("unknown").Validate() + require.Error(t, err) + require.True(t, models.IsGenericValidationError(err), "error must be a validation error") + require.Contains(t, err.Error(), "invalid rounding mode: unknown") +} + +func TestNewFiatCurrencyRequiresISOFiatDefinition(t *testing.T) { + fiat, err := currencyx.NewFiatCurrency(currencyx.Code("USD")) + require.NoError(t, err) + require.Equal(t, currencyx.Code("USD"), fiat) + + _, err = currencyx.NewFiatCurrency(currencyx.Code("BTC")) + require.Error(t, err) + require.Contains(t, err.Error(), "fiat currency definition is required for BTC") +} + +func TestCustomCurrencyValidation(t *testing.T) { + _, err := currencyx.NewCustomCurrency(currencyx.Code("USD"), 2) + require.Error(t, err) + require.True(t, models.IsGenericValidationError(err), "error must be a validation error") + require.Contains(t, err.Error(), "conflicts with fiat currency code") + + _, err = currencyx.NewCustomCurrency(currencyx.Code("CR"), 2) + require.Error(t, err) + require.Contains(t, err.Error(), "at least") + + _, err = currencyx.NewCustomCurrency(currencyx.Code("CRE|DITS"), 2) + require.Error(t, err) + require.Contains(t, err.Error(), "route delimiter") + + _, err = currencyx.NewCustomCurrency(currencyx.Code("CREDITS"), currencyx.MaxPrecision+1) + require.Error(t, err) + require.Contains(t, err.Error(), "precision") + + _, err = currencyx.NewCustomCurrencyWithRounding(currencyx.Code("CREDITS"), 2, currencyx.RoundingMode("unknown")) + require.Error(t, err) + require.Contains(t, err.Error(), "rounding mode") +} + +func TestCodeValidationBoundaries(t *testing.T) { + require.NoError(t, currencyx.Code("CREDITS").ValidateFormat()) + require.Error(t, currencyx.Code("CREDITS").Validate()) + require.NoError(t, currencyx.Code("USD").Validate()) + require.True(t, currencyx.Code("USD").IsKnownFiat()) +} + +func TestCalculatorUsesCurrencyInterface(t *testing.T) { + fiatCalculator, err := currencyx.NewCalculator(currencyx.Code("USD")) + require.NoError(t, err) + require.Equal(t, currencyx.CurrencyTypeFiat, fiatCalculator.CurrencyType()) + require.Equal(t, currencyx.Code("USD"), fiatCalculator.CurrencyCode()) + require.Equal(t, int32(2), fiatCalculator.CurrencyPrecision()) + require.Equal(t, currencyx.RoundingModeHalfAwayFromZero, fiatCalculator.RoundingMode()) + require.NotNil(t, fiatCalculator.Definition()) + require.Equal(t, "0.01", fiatCalculator.Unit().String()) + require.Equal(t, float64(1.24), fiatCalculator.RoundToPrecision(alpacadecimal.NewFromFloat(1.235)).InexactFloat64()) + + custom, err := currencyx.NewCustomCurrency(currencyx.Code("CREDITS"), 6) + require.NoError(t, err) + + customCalculator, err := currencyx.NewCalculator(custom) + require.NoError(t, err) + require.Equal(t, currencyx.CurrencyTypeCustom, customCalculator.CurrencyType()) + require.Equal(t, currencyx.Code("CREDITS"), customCalculator.CurrencyCode()) + require.Equal(t, int32(6), customCalculator.CurrencyPrecision()) + require.Equal(t, currencyx.RoundingModeBankers, customCalculator.RoundingMode()) + require.Nil(t, customCalculator.Definition()) + require.Equal(t, float64(1.234568), customCalculator.RoundToPrecision(alpacadecimal.NewFromFloat(1.2345678)).InexactFloat64()) +} + +func TestCalculatorUsesCurrencyTypeToSelectRounding(t *testing.T) { + fiat, err := currencyx.NewCalculator(testCurrency{ + code: currencyx.Code("USD"), + currencyType: currencyx.CurrencyTypeFiat, + precision: 12, + roundingMode: currencyx.RoundingModeBankers, + }) + require.NoError(t, err) + require.Equal(t, currencyx.CurrencyTypeFiat, fiat.CurrencyType()) + require.Equal(t, int32(2), fiat.CurrencyPrecision()) + require.Equal(t, currencyx.RoundingModeHalfAwayFromZero, fiat.RoundingMode()) + require.Equal(t, "1.23", fiat.RoundToPrecision(alpacadecimal.RequireFromString("1.225")).String()) + + custom, err := currencyx.NewCalculator(testCurrency{ + code: currencyx.Code("CREDITS"), + currencyType: currencyx.CurrencyTypeCustom, + precision: 2, + roundingMode: currencyx.RoundingModeBankers, + }) + require.NoError(t, err) + require.Equal(t, currencyx.CurrencyTypeCustom, custom.CurrencyType()) + require.Equal(t, int32(2), custom.CurrencyPrecision()) + require.Equal(t, currencyx.RoundingModeBankers, custom.RoundingMode()) + require.Equal(t, "1.22", custom.RoundToPrecision(alpacadecimal.RequireFromString("1.225")).String()) +} + +func TestCustomCurrencyRoundingModes(t *testing.T) { + bankersCurrency, err := currencyx.NewCustomCurrency(currencyx.Code("CREDITS"), 2) + require.NoError(t, err) + + bankersCalculator, err := currencyx.NewCalculator(bankersCurrency) + require.NoError(t, err) + require.Equal(t, currencyx.RoundingModeBankers, bankersCalculator.RoundingMode()) + require.Equal(t, "1.22", bankersCalculator.RoundToPrecision(alpacadecimal.RequireFromString("1.225")).String()) + require.Equal(t, "1.24", bankersCalculator.RoundToPrecision(alpacadecimal.RequireFromString("1.235")).String()) + + halfAwayCurrency, err := currencyx.NewCustomCurrencyWithRounding( + currencyx.Code("TOKENS"), + 2, + currencyx.RoundingModeHalfAwayFromZero, + ) + require.NoError(t, err) + + halfAwayCalculator, err := currencyx.NewCalculator(halfAwayCurrency) + require.NoError(t, err) + require.Equal(t, currencyx.RoundingModeHalfAwayFromZero, halfAwayCalculator.RoundingMode()) + require.Equal(t, "1.23", halfAwayCalculator.RoundToPrecision(alpacadecimal.RequireFromString("1.225")).String()) + require.Equal(t, "1.24", halfAwayCalculator.RoundToPrecision(alpacadecimal.RequireFromString("1.235")).String()) +} + +func TestCustomCurrencyBankersRoundingTiesToEven(t *testing.T) { + cases := []struct { + name string + precision int32 + amount string + expected string + }{ + {name: "down to even cent", precision: 2, amount: "1.225", expected: "1.22"}, + {name: "up to even cent", precision: 2, amount: "1.235", expected: "1.24"}, + {name: "negative down to even cent", precision: 2, amount: "-1.225", expected: "-1.22"}, + {name: "negative up to even cent", precision: 2, amount: "-1.235", expected: "-1.24"}, + {name: "zero precision down to even integer", precision: 0, amount: "2.5", expected: "2"}, + {name: "zero precision up to even integer", precision: 0, amount: "3.5", expected: "4"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + custom, err := currencyx.NewCustomCurrency(currencyx.Code("CREDITS"), tc.precision) + require.NoError(t, err) + + calculator, err := currencyx.NewCalculator(custom) + require.NoError(t, err) + + require.Equal( + t, + tc.expected, + calculator.RoundToPrecision(alpacadecimal.RequireFromString(tc.amount)).String(), + ) + }) + } +} + +func TestCustomCurrencyDefaultRoundingModeIsBankers(t *testing.T) { + calculator, err := currencyx.NewCalculator(currencyx.CustomCurrency{ + Code: currencyx.Code("CREDITS"), + Precision: 2, + }) + require.NoError(t, err) + + require.Equal(t, currencyx.RoundingModeBankers, calculator.RoundingMode()) + require.Equal(t, "1.22", calculator.RoundToPrecision(alpacadecimal.RequireFromString("1.225")).String()) +} + +func TestIsRoundedToPrecisionUsesConfiguredRoundingMode(t *testing.T) { + bankersCurrency, err := currencyx.NewCustomCurrency(currencyx.Code("CREDITS"), 2) + require.NoError(t, err) + + bankersCalculator, err := currencyx.NewCalculator(bankersCurrency) + require.NoError(t, err) + require.True(t, bankersCalculator.IsRoundedToPrecision(alpacadecimal.RequireFromString("1.22"))) + require.True(t, bankersCalculator.IsRoundedToPrecision(alpacadecimal.RequireFromString("1.23"))) + require.False(t, bankersCalculator.IsRoundedToPrecision(alpacadecimal.RequireFromString("1.225"))) + + halfAwayCurrency, err := currencyx.NewCustomCurrencyWithRounding( + currencyx.Code("TOKENS"), + 2, + currencyx.RoundingModeHalfAwayFromZero, + ) + require.NoError(t, err) + + halfAwayCalculator, err := currencyx.NewCalculator(halfAwayCurrency) + require.NoError(t, err) + require.True(t, halfAwayCalculator.IsRoundedToPrecision(alpacadecimal.RequireFromString("1.23"))) + require.False(t, halfAwayCalculator.IsRoundedToPrecision(alpacadecimal.RequireFromString("1.225"))) +} + +type testCurrency struct { + code currencyx.Code + currencyType currencyx.CurrencyType + precision int32 + roundingMode currencyx.RoundingMode +} + +func (c testCurrency) CurrencyCode() currencyx.Code { + return c.code +} + +func (c testCurrency) CurrencyType() currencyx.CurrencyType { + return c.currencyType +} + +func (c testCurrency) CurrencyPrecision() int32 { + return c.precision +} + +func (c testCurrency) CurrencyRoundingMode() currencyx.RoundingMode { + return c.roundingMode +} diff --git a/pkg/currencyx/fiat.go b/pkg/currencyx/fiat.go new file mode 100644 index 0000000000..e653c05b08 --- /dev/null +++ b/pkg/currencyx/fiat.go @@ -0,0 +1,174 @@ +package currencyx + +import ( + "errors" + "fmt" + + "github.com/alpacahq/alpacadecimal" + "github.com/invopop/gobl/currency" + + "github.com/openmeterio/openmeter/pkg/models" +) + +type RoundingMode string + +const ( + RoundingModeHalfAwayFromZero RoundingMode = "half_away_from_zero" + RoundingModeBankers RoundingMode = "bankers" +) + +func (m RoundingMode) Validate() error { + var errs []error + + switch m { + case RoundingModeHalfAwayFromZero, RoundingModeBankers: + return nil + default: + errs = append(errs, fmt.Errorf("invalid rounding mode: %s", m)) + } + + return models.NewNillableGenericValidationError(errors.Join(errs...)) +} + +// Calculator provides a currency calculator object. This allows callers to +// resolve fiat definitions once and then apply currency precision consistently. +func (c Code) Calculator() (Calculator, error) { + return NewCalculator(c) +} + +type Calculator struct { + currency Code + def *currency.Def + currencyType CurrencyType + precision int32 + roundingMode RoundingMode +} + +func NewCalculator(cur Currency) (Calculator, error) { + if cur == nil { + return Calculator{}, errors.New("currency is required") + } + + code := cur.CurrencyCode() + currencyType := cur.CurrencyType() + precision := cur.CurrencyPrecision() + roundingMode := cur.CurrencyRoundingMode() + + switch currencyType { + case CurrencyTypeFiat: + if err := code.Validate(); err != nil { + return Calculator{}, err + } + + def := currency.Get(currency.Code(code)) + if def == nil || def.ISONumeric == "" { + return Calculator{}, fmt.Errorf("fiat currency definition is required for %s", code) + } + + return Calculator{ + currency: code, + def: def, + currencyType: CurrencyTypeFiat, + precision: int32(def.Subunits), + roundingMode: RoundingModeHalfAwayFromZero, + }, nil + case CurrencyTypeCustom: + if err := code.ValidateCustom(); err != nil { + return Calculator{}, err + } + + if err := validatePrecision(precision); err != nil { + return Calculator{}, err + } + + if err := roundingMode.Validate(); err != nil { + return Calculator{}, err + } + + return Calculator{ + currency: code, + currencyType: CurrencyTypeCustom, + precision: precision, + roundingMode: roundingMode, + }, nil + default: + return Calculator{}, currencyType.Validate() + } +} + +func (c Calculator) CurrencyCode() Code { + return c.currency +} + +func (c Calculator) Definition() *currency.Def { + return c.def +} + +func (c Calculator) CurrencyType() CurrencyType { + return c.currencyType +} + +func (c Calculator) CurrencyPrecision() int32 { + return c.precision +} + +func (c Calculator) RoundingMode() RoundingMode { + return c.roundingMode +} + +func (c Calculator) RoundToPrecision(amount alpacadecimal.Decimal) alpacadecimal.Decimal { + // TODO: For now we are skipping the smallestDenomination, as that is a reference to the coins + // in circulation, but should not be an issue for online payments. + switch c.roundingMode { + case RoundingModeBankers: + return amount.RoundBank(c.precision) + default: + return amount.Round(c.precision) + } +} + +func (c Calculator) RoundDown(amount alpacadecimal.Decimal) alpacadecimal.Decimal { + return amount.RoundDown(c.precision) +} + +func (c Calculator) Unit() alpacadecimal.Decimal { + return alpacadecimal.NewFromInt(1).Shift(-c.precision) +} + +func (c Calculator) Validate() error { + var errs []error + + switch c.currencyType { + case CurrencyTypeFiat: + if err := c.currency.Validate(); err != nil { + errs = append(errs, err) + } + if c.def == nil || c.def.ISONumeric == "" { + errs = append(errs, fmt.Errorf("fiat currency definition is required for %s", c.currency)) + } + if c.def != nil && c.precision != int32(c.def.Subunits) { + errs = append(errs, errors.New("fiat currency precision must match currency definition")) + } + case CurrencyTypeCustom: + if err := c.currency.ValidateCustom(); err != nil { + errs = append(errs, err) + } + if err := validatePrecision(c.precision); err != nil { + errs = append(errs, err) + } + if err := c.roundingMode.Validate(); err != nil { + errs = append(errs, err) + } + default: + if c.currency == "" { + errs = append(errs, errors.New("currency code is required")) + } + errs = append(errs, c.currencyType.Validate()) + } + + return models.NewNillableGenericValidationError(errors.Join(errs...)) +} + +func (c Calculator) IsRoundedToPrecision(amount alpacadecimal.Decimal) bool { + return amount.Equal(c.RoundToPrecision(amount)) +} diff --git a/pkg/currencyx/fiat_test.go b/pkg/currencyx/fiat_test.go new file mode 100644 index 0000000000..fbb8a3c2cf --- /dev/null +++ b/pkg/currencyx/fiat_test.go @@ -0,0 +1,37 @@ +package currencyx + +import ( + "testing" + + "github.com/invopop/gobl/currency" + "github.com/stretchr/testify/require" + + "github.com/openmeterio/openmeter/pkg/models" +) + +func TestNewCalculatorRequiresISOFiatDefinition(t *testing.T) { + calculator, err := NewCalculator(Code("USD")) + require.NoError(t, err) + require.Equal(t, Code("USD"), calculator.CurrencyCode()) + + _, err = NewCalculator(Code("BTC")) + require.Error(t, err) + require.Contains(t, err.Error(), "fiat currency definition is required for BTC") +} + +func TestCalculatorValidateRequiresISOFiatDefinition(t *testing.T) { + def := currency.Get(currency.Code("BTC")) + require.NotNil(t, def) + require.Empty(t, def.ISONumeric) + + err := Calculator{ + currency: Code("BTC"), + def: def, + currencyType: CurrencyTypeFiat, + precision: int32(def.Subunits), + roundingMode: RoundingModeHalfAwayFromZero, + }.Validate() + require.Error(t, err) + require.True(t, models.IsGenericValidationError(err), "error must be a validation error") + require.Contains(t, err.Error(), "fiat currency definition is required for BTC") +} diff --git a/pkg/currencyx/validation.go b/pkg/currencyx/validation.go new file mode 100644 index 0000000000..80f0685a7f --- /dev/null +++ b/pkg/currencyx/validation.go @@ -0,0 +1,98 @@ +package currencyx + +import ( + "errors" + "fmt" + "strings" + + "github.com/invopop/gobl/currency" + + "github.com/openmeterio/openmeter/pkg/models" +) + +const ( + MinCodeLength = 3 + MaxCodeLength = 24 + MaxPrecision = 12 + + PostgresCodeSchemaType = "varchar(24)" +) + +func (c Code) Validate() error { + if c == "" { + return errors.New("currency code is required") + } + + return currency.Code(c).Validate() +} + +func (c Code) ValidateFormat() error { + value := c.String() + if value == "" { + return errors.New("currency code is required") + } + + if strings.TrimSpace(value) != value { + return errors.New("currency code must not contain leading or trailing whitespace") + } + + if len(value) < MinCodeLength { + return fmt.Errorf("currency code must be at least %d characters", MinCodeLength) + } + + if len(value) > MaxCodeLength { + return fmt.Errorf("currency code must be at most %d characters", MaxCodeLength) + } + + if strings.Contains(value, "|") { + return errors.New("currency code cannot contain route delimiter") + } + + return nil +} + +func (c Code) ValidateCustom() error { + if err := c.ValidateFormat(); err != nil { + return err + } + + if c.IsKnownFiat() { + return fmt.Errorf("custom currency code %s conflicts with fiat currency code", c) + } + + return nil +} + +func (c Code) IsKnownFiat() bool { + return c.Validate() == nil && currency.Get(currency.Code(c)) != nil +} + +func (c CustomCurrency) Validate() error { + var errs []error + + if err := c.Code.ValidateCustom(); err != nil { + errs = append(errs, err) + } + + if err := validatePrecision(c.Precision); err != nil { + errs = append(errs, err) + } + + if err := c.CurrencyRoundingMode().Validate(); err != nil { + errs = append(errs, err) + } + + return models.NewNillableGenericValidationError(errors.Join(errs...)) +} + +func validatePrecision(precision int32) error { + if precision < 0 { + return errors.New("currency precision must be non-negative") + } + + if precision > MaxPrecision { + return fmt.Errorf("currency precision must be at most %d", MaxPrecision) + } + + return nil +} diff --git a/tools/migrate/currency_cost_basis_test.go b/tools/migrate/currency_cost_basis_test.go new file mode 100644 index 0000000000..65f12ce309 --- /dev/null +++ b/tools/migrate/currency_cost_basis_test.go @@ -0,0 +1,109 @@ +package migrate_test + +import ( + "database/sql" + "os" + "path/filepath" + "testing" + "time" + + "github.com/oklog/ulid/v2" + "github.com/stretchr/testify/require" +) + +func TestCurrencyCostBasisEffectiveToMigrationPreservesRows(t *testing.T) { + currencyID := ulid.Make().String() + costBasisID := ulid.Make().String() + effectiveFrom := time.Date(2026, 6, 26, 12, 0, 0, 0, time.UTC) + + up := readMigration(t, "20260701084156_add_currency_cost_basis_effective_to.up.sql") + require.Contains(t, up, `RENAME COLUMN "custom_currency_id" TO "currency_id"`) + require.Contains(t, up, `ADD COLUMN "effective_to" timestamptz NULL`) + require.NotContains(t, up, `DROP COLUMN "custom_currency_id"`) + require.NotContains(t, up, `ADD COLUMN "currency_id"`) + + down := readMigration(t, "20260701084156_add_currency_cost_basis_effective_to.down.sql") + require.Contains(t, down, `RENAME COLUMN "currency_id" TO "custom_currency_id"`) + require.Contains(t, down, `DROP COLUMN "effective_to"`) + require.NotContains(t, down, `DROP COLUMN "currency_id"`) + require.NotContains(t, down, `ADD COLUMN "custom_currency_id"`) + + runner{ + stops: stops{ + { + version: 20260624135146, + direction: directionUp, + action: func(t *testing.T, db *sql.DB) { + _, err := db.Exec(` + INSERT INTO custom_currencies ( + id, namespace, created_at, updated_at, code, name, symbol + ) VALUES ( + $1, 'default', NOW(), NOW(), 'CREDITS', 'Credits', 'CR' + ) + `, currencyID) + require.NoError(t, err) + + _, err = db.Exec(` + INSERT INTO currency_cost_bases ( + id, namespace, created_at, updated_at, fiat_code, rate, effective_from, custom_currency_id + ) VALUES ( + $1, 'default', NOW(), NOW(), 'USD', 0.5, $2, $3 + ) + `, costBasisID, effectiveFrom, currencyID) + require.NoError(t, err) + }, + }, + { + version: 20260701084156, + direction: directionUp, + action: func(t *testing.T, db *sql.DB) { + var gotCurrencyID string + var effectiveTo sql.NullTime + err := db.QueryRow(` + SELECT currency_id, effective_to + FROM currency_cost_bases + WHERE id = $1 + `, costBasisID).Scan(&gotCurrencyID, &effectiveTo) + require.NoError(t, err) + require.Equal(t, currencyID, gotCurrencyID) + require.False(t, effectiveTo.Valid) + }, + }, + { + version: 20260624135146, + direction: directionDown, + action: func(t *testing.T, db *sql.DB) { + var gotCurrencyID string + err := db.QueryRow(` + SELECT custom_currency_id + FROM currency_cost_bases + WHERE id = $1 + `, costBasisID).Scan(&gotCurrencyID) + require.NoError(t, err) + require.Equal(t, currencyID, gotCurrencyID) + + var effectiveToExists bool + err = db.QueryRow(` + SELECT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'currency_cost_bases' + AND column_name = 'effective_to' + ) + `).Scan(&effectiveToExists) + require.NoError(t, err) + require.False(t, effectiveToExists) + }, + }, + }, + }.Test(t) +} + +func readMigration(t *testing.T, name string) string { + t.Helper() + + data, err := os.ReadFile(filepath.Join("migrations", name)) + require.NoError(t, err) + + return string(data) +} diff --git a/tools/migrate/migrations/20260701084156_add_currency_cost_basis_effective_to.down.sql b/tools/migrate/migrations/20260701084156_add_currency_cost_basis_effective_to.down.sql new file mode 100644 index 0000000000..eb4d684070 --- /dev/null +++ b/tools/migrate/migrations/20260701084156_add_currency_cost_basis_effective_to.down.sql @@ -0,0 +1,6 @@ +-- reverse: modify "currency_cost_bases" table +ALTER TABLE "currency_cost_bases" DROP CONSTRAINT "currency_cost_bases_custom_currencies_cost_basis_history"; +ALTER TABLE "currency_cost_bases" DROP COLUMN "effective_to"; +ALTER INDEX "currencycostbasis_namespace_currency_id_fiat_code_effective_fro" RENAME TO "currencycostbasis_namespace_custom_currency_id_fiat_code_effect"; +ALTER TABLE "currency_cost_bases" RENAME COLUMN "currency_id" TO "custom_currency_id"; +ALTER TABLE "currency_cost_bases" ADD CONSTRAINT "currency_cost_bases_custom_currencies_cost_basis_history" FOREIGN KEY ("custom_currency_id") REFERENCES "custom_currencies" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; diff --git a/tools/migrate/migrations/20260701084156_add_currency_cost_basis_effective_to.up.sql b/tools/migrate/migrations/20260701084156_add_currency_cost_basis_effective_to.up.sql new file mode 100644 index 0000000000..fa97ff9340 --- /dev/null +++ b/tools/migrate/migrations/20260701084156_add_currency_cost_basis_effective_to.up.sql @@ -0,0 +1,8 @@ +-- modify "currency_cost_bases" table +ALTER TABLE "currency_cost_bases" DROP CONSTRAINT "currency_cost_bases_custom_currencies_cost_basis_history"; +-- rename a column from "custom_currency_id" to "currency_id" +-- atlas:nolint BC102 +ALTER TABLE "currency_cost_bases" RENAME COLUMN "custom_currency_id" TO "currency_id"; +ALTER INDEX "currencycostbasis_namespace_custom_currency_id_fiat_code_effect" RENAME TO "currencycostbasis_namespace_currency_id_fiat_code_effective_fro"; +ALTER TABLE "currency_cost_bases" ADD COLUMN "effective_to" timestamptz NULL; +ALTER TABLE "currency_cost_bases" ADD CONSTRAINT "currency_cost_bases_custom_currencies_cost_basis_history" FOREIGN KEY ("currency_id") REFERENCES "custom_currencies" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; diff --git a/tools/migrate/migrations/atlas.sum b/tools/migrate/migrations/atlas.sum index 9bb27c5fe6..b1e720a2e2 100644 --- a/tools/migrate/migrations/atlas.sum +++ b/tools/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:HCKcVU2ba2uAfIwF81zTZR/rXanl16+8j8AkcWyjuDM= +h1:s2oRo/6B13zS2TWjmu07nC0DNRALM73DHEEgbOEait4= 20240826120919_init.up.sql h1:tc1V91/smlmaeJGQ8h+MzTEeFjjnrrFDbDAjOYJK91o= 20240903155435_entitlement-expired-index.up.sql h1:Hp8u5uckmLXc1cRvWU0AtVnnK8ShlpzZNp8pbiJLhac= 20240917172257_billing-entities.up.sql h1:Q1dAMo0Vjiit76OybClNfYPGC5nmvov2/M2W1ioi4Kw= @@ -222,3 +222,4 @@ h1:HCKcVU2ba2uAfIwF81zTZR/rXanl16+8j8AkcWyjuDM= 20260630163430_add_ledger_entry_charge_provenance.up.sql h1:7IS2lj4dYU9ACgyp1yNqE64PCfo2NLjM8317i1tTh4w= 20260630163845_add_ledger_entry_schema_version.up.sql h1:WX9XcECusLP6iIlnR2YBe6i654ocAq1q7xzeOgSHo7Q= 20260630181340_add_breakage_record_source_charge_id.up.sql h1:OLPQPKq1S5J941TXL7tHceMSrvMWk5E/nAc3rAnXhZM= +20260701084156_add_currency_cost_basis_effective_to.up.sql h1:31ASikUUszFmPpArKruD3TeAnTIA7ebqMJdt2DzMH6A=