From 1f895c64a6a8d86ddfd37a73838d7ce3824aae00 Mon Sep 17 00:00:00 2001 From: Peter Turi Date: Tue, 21 Jul 2026 16:31:21 +0200 Subject: [PATCH 1/2] feat(charges): support custom currency credits-only charges --- .../billing/adapter/stdinvoicelinemapper.go | 1 - openmeter/billing/adapter/stdinvoicelines.go | 2 +- .../charges/creditpurchase/service/create.go | 2 +- .../flatfee/adapter/detailedline_test.go | 1 - .../billing/charges/flatfee/service/create.go | 2 +- .../billing/charges/service/base_test.go | 37 +++ .../charges/service/creditpurchase_test.go | 116 ++++++- .../billing/charges/service/handlers_test.go | 19 ++ .../charges/service/invoicable_test.go | 309 +++++++++++++++++- .../usagebased/adapter/detailedline_test.go | 1 - .../charges/usagebased/detailedline.go | 2 - .../charges/usagebased/service/create.go | 2 +- .../usagebased/service/linemapper_test.go | 1 - .../usagebased/service/rating/delta/engine.go | 1 - .../service/rating/delta/engine_test.go | 4 - .../service/rating/periodpreserving/engine.go | 1 - .../usagebased/service/rating/service_test.go | 6 - .../service/rating/subtract/subtract.go | 33 -- .../service/rating/subtract/subtract_test.go | 44 --- openmeter/billing/httpdriver/invoiceline.go | 10 +- openmeter/billing/invoicedetailedline_test.go | 1 - .../billing/models/stddetailedline/create.go | 3 - .../models/stddetailedline/derived.gen.go | 1 - .../billing/models/stddetailedline/mapping.go | 3 - .../billing/models/stddetailedline/mixin.go | 4 +- .../billing/models/stddetailedline/model.go | 6 - .../billing/service/invoicecalc/details.go | 1 - openmeter/billing/stdinvoiceline.go | 6 - openmeter/billing/stdinvoiceline_test.go | 1 - .../db/billingstandardinvoicedetailedline.go | 13 +- .../billingstandardinvoicedetailedline.go | 5 +- .../where.go | 10 + ...llingstandardinvoicedetailedline_create.go | 15 +- ...llingstandardinvoicedetailedline_update.go | 6 + .../ent/db/chargeflatfeerundetailedline.go | 13 +- .../chargeflatfeerundetailedline.go | 5 +- .../db/chargeflatfeerundetailedline/where.go | 10 + .../db/chargeflatfeerundetailedline_create.go | 15 +- .../db/chargeflatfeerundetailedline_update.go | 6 + .../ent/db/chargeusagebasedrundetailedline.go | 13 +- .../chargeusagebasedrundetailedline.go | 5 +- .../chargeusagebasedrundetailedline/where.go | 10 + .../chargeusagebasedrundetailedline_create.go | 15 +- .../chargeusagebasedrundetailedline_update.go | 6 + openmeter/ent/db/entmixinaccessor.go | 6 +- openmeter/ent/db/migrate/schema.go | 6 +- openmeter/ent/db/mutation.go | 63 +++- openmeter/ent/db/runtime.go | 12 - test/billing/adapter_test.go | 1 - ..._deprecate_detailed_line_currency.down.sql | 6 + ...44_deprecate_detailed_line_currency.up.sql | 6 + tools/migrate/migrations/atlas.sum | 3 +- 52 files changed, 679 insertions(+), 191 deletions(-) create mode 100644 tools/migrate/migrations/20260721140844_deprecate_detailed_line_currency.down.sql create mode 100644 tools/migrate/migrations/20260721140844_deprecate_detailed_line_currency.up.sql diff --git a/openmeter/billing/adapter/stdinvoicelinemapper.go b/openmeter/billing/adapter/stdinvoicelinemapper.go index bb78b188ea..916d59ee21 100644 --- a/openmeter/billing/adapter/stdinvoicelinemapper.go +++ b/openmeter/billing/adapter/stdinvoicelinemapper.go @@ -187,7 +187,6 @@ func (a *adapter) mapStandardInvoiceDetailedLineFromDB(dbLine *db.BillingInvoice Category: dbLine.Edges.FlatFeeLine.Category, PaymentTerm: dbLine.Edges.FlatFeeLine.PaymentTerm, Index: dbLine.Edges.FlatFeeLine.Index, - Currency: dbLine.Currency, CreditsApplied: creditsApplied, Totals: totals.FromDB(dbLine), ExternalIDs: externalid.MapLineExternalIDFromDB(dbLine), diff --git a/openmeter/billing/adapter/stdinvoicelines.go b/openmeter/billing/adapter/stdinvoicelines.go index f75072dfc1..08f2647fb7 100644 --- a/openmeter/billing/adapter/stdinvoicelines.go +++ b/openmeter/billing/adapter/stdinvoicelines.go @@ -327,7 +327,7 @@ func (a *adapter) upsertDetailedLines(ctx context.Context, in detailedLineDiff) SetType(billing.InvoiceLineAdapterTypeFee). SetName(line.Name). SetNillableDescription(line.Description). - SetCurrency(line.Currency). + SetCurrency(lineWithParent.Parent.Currency). SetNillableChildUniqueReferenceID(lo.EmptyableToPtr(line.ChildUniqueReferenceID)) create = externalid.CreateLineExternalID(create, line.ExternalIDs) diff --git a/openmeter/billing/charges/creditpurchase/service/create.go b/openmeter/billing/charges/creditpurchase/service/create.go index 379a6e5ce9..e4a06d5bec 100644 --- a/openmeter/billing/charges/creditpurchase/service/create.go +++ b/openmeter/billing/charges/creditpurchase/service/create.go @@ -21,7 +21,7 @@ func (s *service) Create(ctx context.Context, input creditpurchase.CreateInput) } return transaction.Run(ctx, s.adapter, func(ctx context.Context) (creditpurchase.ChargeWithGatheringLine, error) { - if input.Intent.Currency.Type() == currencyx.CurrencyTypeCustom { + if input.Intent.Currency.IsCustom() && input.Intent.Settlement.Type() != creditpurchase.SettlementTypePromotional { return creditpurchase.ChargeWithGatheringLine{}, fmt.Errorf("custom currency %s is not supported for credit purchases: %w", input.Intent.Currency.GetCode(), meta.ErrCustomCurrencyNotSupported) } diff --git a/openmeter/billing/charges/flatfee/adapter/detailedline_test.go b/openmeter/billing/charges/flatfee/adapter/detailedline_test.go index a8ffde29e4..ab5f3680dc 100644 --- a/openmeter/billing/charges/flatfee/adapter/detailedline_test.go +++ b/openmeter/billing/charges/flatfee/adapter/detailedline_test.go @@ -271,7 +271,6 @@ func (s *FlatFeeDetailedLineAdapterSuite) newDetailedLine(input newDetailedLineI Description: input.Description, }), ServicePeriod: input.ServicePeriod, - Currency: input.Charge.Intent.GetCurrency().GetCode(), ChildUniqueReferenceID: input.ChildUniqueReferenceID, PaymentTerm: baseIntent.PaymentTerm, PerUnitAmount: alpacadecimal.NewFromFloat(0.1), diff --git a/openmeter/billing/charges/flatfee/service/create.go b/openmeter/billing/charges/flatfee/service/create.go index 7e595987ce..179296e7a1 100644 --- a/openmeter/billing/charges/flatfee/service/create.go +++ b/openmeter/billing/charges/flatfee/service/create.go @@ -29,7 +29,7 @@ func (s *service) Create(ctx context.Context, input flatfee.CreateInput) ([]flat return transaction.Run(ctx, s.adapter, func(ctx context.Context) ([]flatfee.ChargeWithGatheringLine, error) { // Let's create all the flat fee charges in bulk intentsWithStatus, err := slicesx.MapWithErr(input.Intents, func(intent flatfee.Intent) (flatfee.IntentWithInitialStatus, error) { - if intent.Currency.IsCustom() { + if intent.Currency.IsCustom() && intent.SettlementMode != productcatalog.CreditOnlySettlementMode { return flatfee.IntentWithInitialStatus{}, fmt.Errorf("creating flat fee charge with custom currency %q: %w", intent.Currency.GetCode(), meta.ErrCustomCurrencyNotSupported) } diff --git a/openmeter/billing/charges/service/base_test.go b/openmeter/billing/charges/service/base_test.go index e74e6ca582..5855ee0e04 100644 --- a/openmeter/billing/charges/service/base_test.go +++ b/openmeter/billing/charges/service/base_test.go @@ -20,6 +20,7 @@ import ( flatfeeadapter "github.com/openmeterio/openmeter/openmeter/billing/charges/flatfee/adapter" flatfeeservice "github.com/openmeterio/openmeter/openmeter/billing/charges/flatfee/service" "github.com/openmeterio/openmeter/openmeter/billing/charges/invoiceupdater" + "github.com/openmeterio/openmeter/openmeter/billing/charges/lineage" lineageadapter "github.com/openmeterio/openmeter/openmeter/billing/charges/lineage/adapter" lineageservice "github.com/openmeterio/openmeter/openmeter/billing/charges/lineage/service" chargeslinerouter "github.com/openmeterio/openmeter/openmeter/billing/charges/linerouter" @@ -29,6 +30,7 @@ import ( usagebasedadapter "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased/adapter" usagebasedservice "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased/service" billingratingservice "github.com/openmeterio/openmeter/openmeter/billing/rating/service" + "github.com/openmeterio/openmeter/openmeter/currencies" currencyadapter "github.com/openmeterio/openmeter/openmeter/currencies/adapter" "github.com/openmeterio/openmeter/openmeter/currencies/currencyresolver" currencyservice "github.com/openmeterio/openmeter/openmeter/currencies/service" @@ -56,6 +58,14 @@ type BaseSuite struct { Charges *service UsageBasedService usagebased.Service + CurrencyService currencies.Service + MetaAdapter meta.Adapter + LineageService lineage.Service + Locker *lockr.Locker + InvoiceUpdater invoiceupdater.Updater + FlatFeeAdapter flatfee.Adapter + CreditPurchaseAdapter creditpurchase.Adapter + UsageBasedAdapter usagebased.Adapter FlatFeeTestHandler *flatFeeTestHandler CreditPurchaseTestHandler *creditPurchaseTestHandler UsageBasedTestHandler *usageBasedTestHandler @@ -73,11 +83,13 @@ func (s *BaseSuite) SetupSuite() { Logger: slog.Default(), }) s.NoError(err) + s.MetaAdapter = metaAdapter locker, err := lockr.NewLocker(&lockr.LockerConfig{ Logger: slog.Default(), }) s.NoError(err) + s.Locker = locker lineageAdapter, err := lineageadapter.New(lineageadapter.Config{ Client: s.DBClient, @@ -88,6 +100,7 @@ func (s *BaseSuite) SetupSuite() { Adapter: lineageAdapter, }) s.NoError(err) + s.LineageService = lineageService flatFeeAdapter, err := flatfeeadapter.New(flatfeeadapter.Config{ Client: s.DBClient, @@ -95,6 +108,7 @@ func (s *BaseSuite) SetupSuite() { MetaAdapter: metaAdapter, }) s.NoError(err) + s.FlatFeeAdapter = flatFeeAdapter flatFeeService, err := flatfeeservice.New(flatfeeservice.Config{ Adapter: flatFeeAdapter, @@ -115,12 +129,14 @@ func (s *BaseSuite) SetupSuite() { MetaAdapter: metaAdapter, }) s.NoError(err) + s.UsageBasedAdapter = usageBasedAdapter invoiceUpdater, err := invoiceupdater.New(invoiceupdater.Config{ BillingService: s.BillingService, Logger: slog.Default(), }) s.NoError(err) + s.InvoiceUpdater = invoiceUpdater usageBasedService, err := usagebasedservice.New(usagebasedservice.Config{ Adapter: usageBasedAdapter, @@ -146,6 +162,7 @@ func (s *BaseSuite) SetupSuite() { MetaAdapter: metaAdapter, }) s.NoError(err) + s.CreditPurchaseAdapter = creditPurchaseAdapter creditPurchaseService, err := creditpurchaseservice.New(creditpurchaseservice.Config{ Adapter: creditPurchaseAdapter, @@ -185,6 +202,7 @@ func (s *BaseSuite) SetupSuite() { s.NoError(err) currencyService, err := currencyservice.New(currencyAdapter) s.NoError(err) + s.CurrencyService = currencyService currencyResolver, err := currencyresolver.New(currencyService) s.NoError(err) @@ -216,6 +234,25 @@ func (s *BaseSuite) TearDownTest() { clock.ResetTime() } +func (s *BaseSuite) createTestCustomCurrency(ctx context.Context, namespace string) currencies.Currency { + s.T().Helper() + + currency, err := s.CurrencyService.CreateCurrency(ctx, currencies.CreateCurrencyInput{ + Namespace: namespace, + CurrencyDetails: currencyx.CurrencyDetails{ + Code: "TOKENS", + Name: "Tokens", + Symbol: "T", + Precision: 3, + DecimalMark: ".", + ThousandsSeparator: ",", + }, + }) + s.Require().NoError(err) + + return currency +} + type createMockChargeIntentInput struct { customer customer.CustomerID currency currencyx.Code diff --git a/openmeter/billing/charges/service/creditpurchase_test.go b/openmeter/billing/charges/service/creditpurchase_test.go index f309a43545..83223a650c 100644 --- a/openmeter/billing/charges/service/creditpurchase_test.go +++ b/openmeter/billing/charges/service/creditpurchase_test.go @@ -11,6 +11,7 @@ import ( "github.com/invopop/gobl/currency" "github.com/samber/lo" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -18,8 +19,10 @@ import ( "github.com/openmeterio/openmeter/openmeter/billing" "github.com/openmeterio/openmeter/openmeter/billing/charges" "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase" + creditpurchaseservice "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase/service" "github.com/openmeterio/openmeter/openmeter/billing/charges/meta" "github.com/openmeterio/openmeter/openmeter/billing/charges/models/payment" + "github.com/openmeterio/openmeter/openmeter/currencies" currenciestestutils "github.com/openmeterio/openmeter/openmeter/currencies/testutils/currency" "github.com/openmeterio/openmeter/openmeter/customer" "github.com/openmeterio/openmeter/pkg/clock" @@ -98,6 +101,118 @@ func (s *CreditPurchaseTestSuite) TestPromotionalCreditPurchase() { s.Equal(creditpurchase.StatusFinal, updatedCPCharge.Status) } +func (s *CreditPurchaseTestSuite) TestPromotionalCreditPurchaseWithCustomCurrency() { + ctx := s.T().Context() + ns := s.GetUniqueNamespace("charges-service-promotional-credit-purchase-custom-currency") + + var customCurrency currencies.Currency + var customerID string + var createdCharge creditpurchase.Charge + var promotionalTransactionGroupID string + + s.Run("#1 setup customer and custom currency", func() { + // given: + // - a customer and a persisted custom currency + s.ProvisionDefaultTaxCodes(ctx, ns) + + cust := s.CreateTestCustomer(ns, "test-subject") + s.NotEmpty(cust.ID) + customerID = cust.ID + customCurrency = s.createTestCustomCurrency(ctx, ns) + + }) + + s.Run("#2 create promotional credit purchase", func() { + // given: + // - a promotional credit-purchase intent in the custom currency + // - mocked ledger and lineage callbacks + // when: + // - the charge is created through the root charges service + // then: + // - the callbacks run once and the charge reaches final with a persisted realization + servicePeriod := timeutil.ClosedPeriod{ + From: datetime.MustParseTimeInLocation(s.T(), "2026-01-01T00:00:00Z", time.UTC).AsTime(), + To: datetime.MustParseTimeInLocation(s.T(), "2026-02-01T00:00:00Z", time.UTC).AsTime(), + } + intent := charges.NewChargeIntent(creditpurchase.Intent{ + Intent: meta.Intent{ + ManagedBy: billing.ManuallyManagedLine, + CustomerID: customerID, + Currency: customCurrency, + }, + IntentMutableFields: creditpurchase.IntentMutableFields{ + IntentMutableFields: meta.IntentMutableFields{ + Name: "Custom Currency Credit Purchase", + ServicePeriod: servicePeriod, + BillingPeriod: servicePeriod, + FullServicePeriod: servicePeriod, + }, + CreditAmount: alpacadecimal.NewFromFloat(100.1234), + Settlement: creditpurchase.NewSettlement(creditpurchase.PromotionalSettlement{}), + }, + }) + + promotionalCallback := newCountedLedgerTransactionCallback[creditpurchase.Charge]() + promotionalTransactionGroupID = promotionalCallback.id + s.CreditPurchaseTestHandler.onPromotionalCreditPurchase = promotionalCallback.Handler(s.T(), func(t *testing.T, charge creditpurchase.Charge) { + assert.Equal(t, creditpurchase.SettlementTypePromotional, charge.Intent.Settlement.Type()) + assert.True(t, charge.Intent.Currency.IsCustom()) + assert.Equal(t, customCurrency.ID, charge.Intent.Currency.ID) + assert.Nil(t, charge.Realizations.CreditGrantRealization) + }) + + lineageMock := &mockLineageService{Service: s.LineageService} + lineageMock.On("BackfillAdvanceLineageSegments", mock.Anything, mock.Anything). + Return(nil). + Once() + + customCurrencyCreditPurchaseService, err := creditpurchaseservice.New(creditpurchaseservice.Config{ + Adapter: s.CreditPurchaseAdapter, + Handler: s.CreditPurchaseTestHandler, + Lineage: lineageMock, + MetaAdapter: s.MetaAdapter, + }) + s.Require().NoError(err) + originalCreditPurchaseService := s.Charges.creditPurchaseService + s.Charges.creditPurchaseService = customCurrencyCreditPurchaseService + defer func() { + s.Charges.creditPurchaseService = originalCreditPurchaseService + }() + + created, err := s.Charges.Create(ctx, charges.CreateInput{ + Namespace: ns, + Intents: charges.ChargeIntents{intent}, + }) + s.Require().NoError(err) + s.Require().Len(created, 1) + s.Equal(1, promotionalCallback.nrInvocations) + lineageMock.AssertExpectations(s.T()) + + createdCharge, err = created[0].AsCreditPurchaseCharge() + s.Require().NoError(err) + s.Equal(creditpurchase.StatusFinal, createdCharge.Status) + s.True(createdCharge.Intent.Currency.IsCustom()) + s.Equal(customCurrency.ID, createdCharge.Intent.Currency.ID) + s.Equal(float64(100.123), createdCharge.Intent.CreditAmount.InexactFloat64()) + s.Require().NotNil(createdCharge.Realizations.CreditGrantRealization) + s.Equal(promotionalTransactionGroupID, createdCharge.Realizations.CreditGrantRealization.TransactionGroupID) + }) + + s.Run("#3 reload persisted charge", func() { + // when: + // - the charge is loaded again from Postgres + // then: + // - its final state, custom currency, and realization are preserved + persisted, err := s.mustGetChargeByID(createdCharge.GetChargeID()).AsCreditPurchaseCharge() + s.Require().NoError(err) + s.Equal(creditpurchase.StatusFinal, persisted.Status) + s.True(persisted.Intent.Currency.IsCustom()) + s.Equal(customCurrency.ID, persisted.Intent.Currency.ID) + s.Require().NotNil(persisted.Realizations.CreditGrantRealization) + s.Equal(promotionalTransactionGroupID, persisted.Realizations.CreditGrantRealization.TransactionGroupID) + }) +} + func (s *CreditPurchaseTestSuite) TestCreditPurchaseRejectsMismatchedSettlementCurrency() { ctx := context.Background() ns := s.GetUniqueNamespace("charges-service-credit-purchase-mismatched-settlement-currency") @@ -725,7 +840,6 @@ func (s *CreditPurchaseTestSuite) TestStandardInvoiceCreditPurchase() { detailedLine := line.DetailedLines[0] - s.Equal(USD, detailedLine.Currency) s.Equal(alpacadecimal.NewFromFloat(50), detailedLine.PerUnitAmount) s.Equal(alpacadecimal.NewFromFloat(1), detailedLine.Quantity) s.Equal(alpacadecimal.NewFromFloat(50), detailedLine.Totals.Amount) diff --git a/openmeter/billing/charges/service/handlers_test.go b/openmeter/billing/charges/service/handlers_test.go index 12c095f748..eabfff1d06 100644 --- a/openmeter/billing/charges/service/handlers_test.go +++ b/openmeter/billing/charges/service/handlers_test.go @@ -7,14 +7,33 @@ import ( "github.com/alpacahq/alpacadecimal" "github.com/oklog/ulid/v2" + "github.com/stretchr/testify/mock" "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase" "github.com/openmeterio/openmeter/openmeter/billing/charges/flatfee" + "github.com/openmeterio/openmeter/openmeter/billing/charges/lineage" "github.com/openmeterio/openmeter/openmeter/billing/charges/models/creditrealization" "github.com/openmeterio/openmeter/openmeter/billing/charges/models/ledgertransaction" "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased" ) +type mockLineageService struct { + lineage.Service + mock.Mock +} + +func (m *mockLineageService) CreateInitialLineages(ctx context.Context, input lineage.CreateInitialLineagesInput) error { + return m.Called(ctx, input).Error(0) +} + +func (m *mockLineageService) PersistCorrectionLineageSegments(ctx context.Context, input lineage.PersistCorrectionLineageSegmentsInput) error { + return m.Called(ctx, input).Error(0) +} + +func (m *mockLineageService) BackfillAdvanceLineageSegments(ctx context.Context, input lineage.BackfillAdvanceLineageSegmentsInput) error { + return m.Called(ctx, input).Error(0) +} + var _ flatfee.Handler = (*flatFeeTestHandler)(nil) type flatFeeTestHandler struct { diff --git a/openmeter/billing/charges/service/invoicable_test.go b/openmeter/billing/charges/service/invoicable_test.go index 9adbd9dcae..1560e330a9 100644 --- a/openmeter/billing/charges/service/invoicable_test.go +++ b/openmeter/billing/charges/service/invoicable_test.go @@ -10,6 +10,7 @@ import ( "github.com/oklog/ulid/v2" "github.com/samber/lo" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing" @@ -17,12 +18,16 @@ import ( "github.com/openmeterio/openmeter/openmeter/billing/charges" "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase" "github.com/openmeterio/openmeter/openmeter/billing/charges/flatfee" + flatfeeservice "github.com/openmeterio/openmeter/openmeter/billing/charges/flatfee/service" "github.com/openmeterio/openmeter/openmeter/billing/charges/meta" "github.com/openmeterio/openmeter/openmeter/billing/charges/models/creditrealization" "github.com/openmeterio/openmeter/openmeter/billing/charges/models/ledgertransaction" "github.com/openmeterio/openmeter/openmeter/billing/charges/models/payment" "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased" + usagebasedservice "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased/service" billingtotals "github.com/openmeterio/openmeter/openmeter/billing/models/totals" + billingratingservice "github.com/openmeterio/openmeter/openmeter/billing/rating/service" + "github.com/openmeterio/openmeter/openmeter/currencies" currenciestestutils "github.com/openmeterio/openmeter/openmeter/currencies/testutils/currency" "github.com/openmeterio/openmeter/openmeter/customer" "github.com/openmeterio/openmeter/openmeter/productcatalog" @@ -2934,6 +2939,309 @@ func (s *InvoicableChargesTestSuite) TestFlatFeeCreditOnlyCreateImmediatelyFinal s.Equal(float64(50), dbFF.Realizations.CurrentRun.CreditRealizations[0].Amount.InexactFloat64()) } +func (s *InvoicableChargesTestSuite) TestFlatFeeCreditOnlyWithCustomCurrency() { + ctx := s.T().Context() + ns := s.GetUniqueNamespace("charges-service-flatfee-credit-only-custom-currency") + + var customCurrency currencies.Currency + var customerID string + var createdCharge flatfee.Charge + var allocationTransactionGroupID string + + s.Run("#1 setup customer and custom currency", func() { + // given: + // - a customer and a persisted custom currency + s.ProvisionDefaultTaxCodes(ctx, ns) + + cust := s.CreateTestCustomer(ns, "test-subject") + s.NotEmpty(cust.ID) + customerID = cust.ID + customCurrency = s.createTestCustomCurrency(ctx, ns) + }) + + s.Run("#2 create credits-only flat fee", func() { + // given: + // - an immediately due flat fee in the custom currency + // - mocked ledger allocation and lineage callbacks + // when: + // - the credits-only charge is created through the root charges service + // then: + // - the callbacks run once and the charge reaches final with a persisted allocation + servicePeriod := timeutil.ClosedPeriod{ + From: datetime.MustParseTimeInLocation(s.T(), "2026-01-01T00:00:00Z", time.UTC).AsTime(), + To: datetime.MustParseTimeInLocation(s.T(), "2026-02-01T00:00:00Z", time.UTC).AsTime(), + } + clock.FreezeTime(servicePeriod.From) + defer clock.UnFreeze() + + allocationCallback := newCountedCreditAllocationCallback[flatfee.OnAllocateCreditsInput]() + allocationTransactionGroupID = allocationCallback.id + s.FlatFeeTestHandler.onAllocateCredits = allocationCallback.Handler( + s.T(), + func(input flatfee.OnAllocateCreditsInput, transactionGroup ledgertransaction.GroupReference) creditrealization.CreateAllocationInputs { + return creditrealization.CreateAllocationInputs{ + { + ServicePeriod: input.ServicePeriod, + Amount: input.PreTaxAmountToAllocate, + LedgerTransaction: transactionGroup, + }, + } + }, + func(t *testing.T, input flatfee.OnAllocateCreditsInput) { + assert.True(t, input.Charge.Intent.GetCurrency().IsCustom()) + assert.Equal(t, customCurrency.ID, input.Charge.Intent.GetCurrency().ID) + }, + ) + + lineageMock := &mockLineageService{Service: s.LineageService} + lineageMock.On("CreateInitialLineages", mock.Anything, mock.Anything). + Return(nil). + Once() + lineageMock.On("PersistCorrectionLineageSegments", mock.Anything, mock.Anything). + Return(nil). + Once() + + customCurrencyFlatFeeService, err := flatfeeservice.New(flatfeeservice.Config{ + Adapter: s.FlatFeeAdapter, + Handler: s.FlatFeeTestHandler, + Lineage: lineageMock, + MetaAdapter: s.MetaAdapter, + Locker: s.Locker, + RatingService: billingratingservice.New(billingratingservice.Config{UnitConfigEnabled: s.UnitConfigEnabled}), + }) + s.Require().NoError(err) + originalFlatFeeService := s.Charges.flatFeeService + s.Charges.flatFeeService = customCurrencyFlatFeeService + defer func() { + s.Charges.flatFeeService = originalFlatFeeService + }() + + intent := charges.NewChargeIntent(flatfee.Intent{ + Intent: meta.Intent{ + ManagedBy: billing.ManuallyManagedLine, + CustomerID: customerID, + Currency: customCurrency, + }, + IntentMutableFields: flatfee.IntentMutableFields{ + IntentMutableFields: meta.IntentMutableFields{ + Name: "Custom Currency Flat Fee", + ServicePeriod: servicePeriod, + BillingPeriod: servicePeriod, + FullServicePeriod: servicePeriod, + }, + InvoiceAt: servicePeriod.From, + PaymentTerm: productcatalog.InAdvancePaymentTerm, + AmountBeforeProration: alpacadecimal.NewFromFloat(50.1234), + }, + SettlementMode: productcatalog.CreditOnlySettlementMode, + }) + + created, err := s.Charges.Create(ctx, charges.CreateInput{ + Namespace: ns, + Intents: charges.ChargeIntents{intent}, + }) + s.Require().NoError(err) + s.Require().Len(created, 1) + s.Equal(1, allocationCallback.nrInvocations) + lineageMock.AssertExpectations(s.T()) + + createdCharge, err = created[0].AsFlatFeeCharge() + s.Require().NoError(err) + s.Equal(flatfee.StatusFinal, createdCharge.Status) + s.True(createdCharge.Intent.GetCurrency().IsCustom()) + s.Equal(customCurrency.ID, createdCharge.Intent.GetCurrency().ID) + s.Require().NotNil(createdCharge.Realizations.CurrentRun) + s.Require().Len(createdCharge.Realizations.CurrentRun.CreditRealizations, 1) + s.Equal(float64(50.123), createdCharge.Realizations.CurrentRun.CreditRealizations[0].Amount.InexactFloat64()) + s.Equal(allocationTransactionGroupID, createdCharge.Realizations.CurrentRun.CreditRealizations[0].LedgerTransaction.TransactionGroupID) + }) + + s.Run("#3 reload persisted charge", func() { + // when: + // - the flat-fee charge is loaded again from Postgres + // then: + // - its final state, custom currency, totals, and allocation are preserved + persisted, err := s.mustGetChargeByID(createdCharge.GetChargeID()).AsFlatFeeCharge() + s.Require().NoError(err) + s.Equal(flatfee.StatusFinal, persisted.Status) + s.True(persisted.Intent.GetCurrency().IsCustom()) + s.Equal(customCurrency.ID, persisted.Intent.GetCurrency().ID) + s.Require().NotNil(persisted.Realizations.CurrentRun) + s.RequireTotals(billingtest.ExpectedTotals{ + Amount: 50.123, + CreditsTotal: 50.123, + }, persisted.Realizations.CurrentRun.Totals) + s.Require().Len(persisted.Realizations.CurrentRun.CreditRealizations, 1) + s.Equal(allocationTransactionGroupID, persisted.Realizations.CurrentRun.CreditRealizations[0].LedgerTransaction.TransactionGroupID) + }) +} + +func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyWithCustomCurrency() { + ctx := s.T().Context() + ns := s.GetUniqueNamespace("charges-service-usage-based-credit-only-custom-currency") + + var customCurrency currencies.Currency + var customerID string + var featureKey string + var createdCharge usagebased.Charge + var allocationTransactionGroupID string + + s.Run("#1 setup metered customer and custom currency", func() { + // given: + // - a metered customer, a persisted custom currency, and usage events + s.ProvisionDefaultTaxCodes(ctx, ns) + + customInvoicing := s.SetupCustomInvoicing(ns) + cust := s.CreateTestCustomer(ns, "test-subject") + s.NotEmpty(cust.ID) + customerID = cust.ID + customCurrency = s.createTestCustomCurrency(ctx, ns) + + _ = s.ProvisionBillingProfile(ctx, ns, customInvoicing.App.GetID(), + billingtest.WithProgressiveBilling(), + billingtest.WithCollectionInterval(datetime.MustParseDuration(s.T(), "P2D")), + billingtest.WithManualApproval(), + ) + + feature := s.SetupApiRequestsTotalFeature(ctx, ns) + featureKey = feature.Feature.Key + s.MockStreamingConnector.AddSimpleEvent(featureKey, 3, + datetime.MustParseTimeInLocation(s.T(), "2026-01-15T00:00:00Z", time.UTC).AsTime(), + ) + s.MockStreamingConnector.AddSimpleEvent(featureKey, 5, + datetime.MustParseTimeInLocation(s.T(), "2026-01-20T00:00:00Z", time.UTC).AsTime(), + ) + }) + + s.Run("#2 create credits-only usage charge", func() { + // given: + // - a usage-based intent whose collection period has ended + // - mocked ledger allocation and lineage callbacks + // when: + // - the credits-only charge is created through the root charges service + // then: + // - usage is rated and the charge reaches final with a persisted allocation + servicePeriod := timeutil.ClosedPeriod{ + From: datetime.MustParseTimeInLocation(s.T(), "2026-01-01T00:00:00Z", time.UTC).AsTime(), + To: datetime.MustParseTimeInLocation(s.T(), "2026-02-01T00:00:00Z", time.UTC).AsTime(), + } + finalAdvanceAt := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:01:00Z", time.UTC).AsTime() + clock.FreezeTime(finalAdvanceAt) + defer clock.UnFreeze() + + allocationCallback := newCountedCreditAllocationCallback[usagebased.CreditsOnlyUsageAccruedInput]() + allocationTransactionGroupID = allocationCallback.id + s.UsageBasedTestHandler.onCreditsOnlyUsageAccrued = allocationCallback.Handler( + s.T(), + func(input usagebased.CreditsOnlyUsageAccruedInput, transactionGroup ledgertransaction.GroupReference) creditrealization.CreateAllocationInputs { + return creditrealization.CreateAllocationInputs{ + { + ServicePeriod: input.Charge.Intent.GetEffectiveServicePeriod(), + Amount: input.AmountToAllocate, + LedgerTransaction: transactionGroup, + }, + } + }, + func(t *testing.T, input usagebased.CreditsOnlyUsageAccruedInput) { + assert.True(t, input.Charge.Intent.GetCurrency().IsCustom()) + assert.Equal(t, customCurrency.ID, input.Charge.Intent.GetCurrency().ID) + }, + ) + + lineageMock := &mockLineageService{Service: s.LineageService} + lineageMock.On("CreateInitialLineages", mock.Anything, mock.Anything). + Return(nil). + Once() + lineageMock.On("PersistCorrectionLineageSegments", mock.Anything, mock.Anything). + Return(nil). + Once() + customCurrencyUsageBasedService, err := usagebasedservice.New(usagebasedservice.Config{ + Adapter: s.UsageBasedAdapter, + Handler: s.UsageBasedTestHandler, + Lineage: lineageMock, + Locker: s.Locker, + MetaAdapter: s.MetaAdapter, + InvoiceUpdater: s.InvoiceUpdater, + CustomerOverrideService: s.BillingService, + FeatureService: s.FeatureService, + RatingService: billingratingservice.New(billingratingservice.Config{UnitConfigEnabled: s.UnitConfigEnabled}), + StreamingConnector: s.MockStreamingConnector, + }) + s.Require().NoError(err) + originalUsageBasedService := s.Charges.usageBasedService + s.Charges.usageBasedService = customCurrencyUsageBasedService + defer func() { + s.Charges.usageBasedService = originalUsageBasedService + }() + + intent := charges.NewChargeIntent(usagebased.Intent{ + Intent: meta.Intent{ + ManagedBy: billing.ManuallyManagedLine, + CustomerID: customerID, + Currency: customCurrency, + }, + FeatureKey: featureKey, + IntentMutableFields: usagebased.IntentMutableFields{ + IntentMutableFields: meta.IntentMutableFields{ + Name: "Custom Currency Usage", + ServicePeriod: servicePeriod, + BillingPeriod: servicePeriod, + FullServicePeriod: servicePeriod, + }, + InvoiceAt: servicePeriod.To, + Price: lo.FromPtr(productcatalog.NewPriceFrom(productcatalog.UnitPrice{ + Amount: alpacadecimal.NewFromFloat(1.234), + })), + }, + SettlementMode: productcatalog.CreditOnlySettlementMode, + }) + + created, err := s.Charges.Create(ctx, charges.CreateInput{ + Namespace: ns, + Intents: charges.ChargeIntents{intent}, + }) + s.Require().NoError(err) + s.Require().Len(created, 1) + s.Equal(1, allocationCallback.nrInvocations) + lineageMock.AssertExpectations(s.T()) + + createdCharge, err = created[0].AsUsageBasedCharge() + s.Require().NoError(err) + s.Equal(usagebased.StatusFinal, createdCharge.Status) + s.True(createdCharge.Intent.GetCurrency().IsCustom()) + s.Equal(customCurrency.ID, createdCharge.Intent.GetCurrency().ID) + s.Require().Len(createdCharge.Realizations, 1) + finalRun := createdCharge.Realizations[0] + s.Equal(float64(8), finalRun.MeteredQuantity.InexactFloat64()) + s.RequireTotals(billingtest.ExpectedTotals{ + Amount: 9.872, + CreditsTotal: 9.872, + }, finalRun.Totals) + s.Require().Len(finalRun.CreditsAllocated, 1) + s.Equal(float64(9.872), finalRun.CreditsAllocated[0].Amount.InexactFloat64()) + s.Equal(allocationTransactionGroupID, finalRun.CreditsAllocated[0].LedgerTransaction.TransactionGroupID) + }) + + s.Run("#3 reload persisted charge", func() { + // when: + // - the usage-based charge is loaded again from Postgres + // then: + // - its final state, custom currency, rated totals, and allocation are preserved + persisted := s.mustGetUsageBasedChargeByID(createdCharge.GetChargeID()) + s.Equal(usagebased.StatusFinal, persisted.Status) + s.True(persisted.Intent.GetCurrency().IsCustom()) + s.Equal(customCurrency.ID, persisted.Intent.GetCurrency().ID) + s.Require().Len(persisted.Realizations, 1) + s.Equal(float64(8), persisted.Realizations[0].MeteredQuantity.InexactFloat64()) + s.RequireTotals(billingtest.ExpectedTotals{ + Amount: 9.872, + CreditsTotal: 9.872, + }, persisted.Realizations[0].Totals) + s.Require().Len(persisted.Realizations[0].CreditsAllocated, 1) + s.Equal(allocationTransactionGroupID, persisted.Realizations[0].CreditsAllocated[0].LedgerTransaction.TransactionGroupID) + }) +} + func (s *InvoicableChargesTestSuite) TestFlatFeeCreditOnlyInArrearsAllocatesAtInvoiceAt() { defer s.FlatFeeTestHandler.Reset() @@ -3194,7 +3502,6 @@ func (s *InvoicableChargesTestSuite) assertFlatFeeCreditThenInvoiceLineAndRun(in s.Equal(detailedLine.Category, runDetailedLine.Category) s.Equal(detailedLine.PaymentTerm, runDetailedLine.PaymentTerm) s.Equal(detailedLine.ServicePeriod, runDetailedLine.ServicePeriod) - s.Equal(detailedLine.Currency, runDetailedLine.Currency) s.True(detailedLine.PerUnitAmount.Equal(runDetailedLine.PerUnitAmount), "persisted run detailed line per-unit amount should match standard detailed line") s.Equal(detailedLine.Quantity.String(), runDetailedLine.Quantity.String()) s.True(runDetailedLine.Totals.Equal(detailedLine.Totals), "persisted run detailed line totals should match standard detailed line totals") diff --git a/openmeter/billing/charges/usagebased/adapter/detailedline_test.go b/openmeter/billing/charges/usagebased/adapter/detailedline_test.go index e8f57c757e..a729d132a1 100644 --- a/openmeter/billing/charges/usagebased/adapter/detailedline_test.go +++ b/openmeter/billing/charges/usagebased/adapter/detailedline_test.go @@ -548,7 +548,6 @@ func (s *DetailedLineAdapterSuite) newDetailedLine(input newDetailedLineInput) u Description: input.Description, }), ServicePeriod: input.ServicePeriod, - Currency: input.Charge.Intent.GetCurrency().GetCode(), ChildUniqueReferenceID: input.ChildUniqueReferenceID, PaymentTerm: productcatalog.InArrearsPaymentTerm, PerUnitAmount: alpacadecimal.NewFromFloat(0.1), diff --git a/openmeter/billing/charges/usagebased/detailedline.go b/openmeter/billing/charges/usagebased/detailedline.go index e68da96f37..9f624c38b3 100644 --- a/openmeter/billing/charges/usagebased/detailedline.go +++ b/openmeter/billing/charges/usagebased/detailedline.go @@ -54,7 +54,6 @@ func (l DetailedLine) Validate() error { type DetailedLines []DetailedLine func NewDetailedLinesFromBilling( - intent Intent, defaultServicePeriod timeutil.ClosedPeriod, lines billingrating.DetailedLines, ) DetailedLines { @@ -77,7 +76,6 @@ func NewDetailedLinesFromBilling( }), ServicePeriod: period, Index: lo.ToPtr(idx), - Currency: intent.Intent.Currency.GetCode(), ChildUniqueReferenceID: line.ChildUniqueReferenceID, PaymentTerm: lo.CoalesceOrEmpty(line.PaymentTerm, productcatalog.InArrearsPaymentTerm), PerUnitAmount: line.PerUnitAmount, diff --git a/openmeter/billing/charges/usagebased/service/create.go b/openmeter/billing/charges/usagebased/service/create.go index 4c9d5791eb..698707e78c 100644 --- a/openmeter/billing/charges/usagebased/service/create.go +++ b/openmeter/billing/charges/usagebased/service/create.go @@ -28,7 +28,7 @@ func (s *service) Create(ctx context.Context, input usagebased.CreateInput) ([]u return transaction.Run(ctx, s.adapter, func(ctx context.Context) ([]usagebased.ChargeWithGatheringLine, error) { createIntents, err := slicesx.MapWithErr(input.Intents, func(intent usagebased.Intent) (usagebased.CreateIntent, error) { - if intent.Currency.IsCustom() { + if intent.Currency.IsCustom() && intent.SettlementMode != productcatalog.CreditOnlySettlementMode { return usagebased.CreateIntent{}, fmt.Errorf("creating usage based charge with custom currency %q: %w", intent.Currency.GetCode(), meta.ErrCustomCurrencyNotSupported) } diff --git a/openmeter/billing/charges/usagebased/service/linemapper_test.go b/openmeter/billing/charges/usagebased/service/linemapper_test.go index b86d2fb1a6..2cf92137b9 100644 --- a/openmeter/billing/charges/usagebased/service/linemapper_test.go +++ b/openmeter/billing/charges/usagebased/service/linemapper_test.go @@ -259,7 +259,6 @@ func newUsageBasedDetailedLineForTest(ref string, period timeutil.ClosedPeriod, Index: lo.ToPtr(0), PaymentTerm: productcatalog.InArrearsPaymentTerm, ServicePeriod: period, - Currency: currencyx.Code("USD"), PerUnitAmount: amount, Quantity: alpacadecimal.NewFromInt(1), Totals: totals.Totals{ diff --git a/openmeter/billing/charges/usagebased/service/rating/delta/engine.go b/openmeter/billing/charges/usagebased/service/rating/delta/engine.go index d7b86cbd8a..accf1110fe 100644 --- a/openmeter/billing/charges/usagebased/service/rating/delta/engine.go +++ b/openmeter/billing/charges/usagebased/service/rating/delta/engine.go @@ -93,7 +93,6 @@ func (e Engine) Rate(_ context.Context, in Input) (Result, error) { } currentDetailedLines := usagebased.NewDetailedLinesFromBilling( - in.Intent, in.CurrentPeriod.ServicePeriod, billingDetailedLines.DetailedLines, ) diff --git a/openmeter/billing/charges/usagebased/service/rating/delta/engine_test.go b/openmeter/billing/charges/usagebased/service/rating/delta/engine_test.go index c66fe47125..31e504c4f5 100644 --- a/openmeter/billing/charges/usagebased/service/rating/delta/engine_test.go +++ b/openmeter/billing/charges/usagebased/service/rating/delta/engine_test.go @@ -15,7 +15,6 @@ import ( "github.com/openmeterio/openmeter/openmeter/billing/models/totals" billingrating "github.com/openmeterio/openmeter/openmeter/billing/rating" "github.com/openmeterio/openmeter/openmeter/productcatalog" - "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/timeutil" ) @@ -164,7 +163,6 @@ func TestRateSubtractsAlreadyBilledLinesAndBooksDeltaOnCurrentPeriod(t *testing. Name: "Usage", }), ServicePeriod: priorPeriod, - Currency: currencyx.Code("USD"), ChildUniqueReferenceID: ratingtestutils.FormatDetailedLineChildUniqueReferenceID(billingrating.UnitPriceUsageChildUniqueReferenceID, priorPeriod), PaymentTerm: productcatalog.InArrearsPaymentTerm, PerUnitAmount: alpacadecimal.NewFromInt(10), @@ -232,7 +230,6 @@ func TestRateGeneratesCorrectionChildUniqueReferenceIDForPreviousOnlyReversal(t Name: "Usage", }), ServicePeriod: priorPeriod, - Currency: currencyx.Code("USD"), ChildUniqueReferenceID: ratingtestutils.FormatDetailedLineChildUniqueReferenceID(billingrating.UnitPriceUsageChildUniqueReferenceID, priorPeriod), PaymentTerm: productcatalog.InArrearsPaymentTerm, PerUnitAmount: alpacadecimal.NewFromInt(10), @@ -300,7 +297,6 @@ func TestRateErrorsWhenPreviousOnlyReversalDetailedLineIDIsMissing(t *testing.T) Name: "Usage", }), ServicePeriod: priorPeriod, - Currency: currencyx.Code("USD"), ChildUniqueReferenceID: ratingtestutils.FormatDetailedLineChildUniqueReferenceID(billingrating.UnitPriceUsageChildUniqueReferenceID, priorPeriod), PaymentTerm: productcatalog.InArrearsPaymentTerm, PerUnitAmount: alpacadecimal.NewFromInt(10), diff --git a/openmeter/billing/charges/usagebased/service/rating/periodpreserving/engine.go b/openmeter/billing/charges/usagebased/service/rating/periodpreserving/engine.go index 46c207e7a4..8b5630fd7f 100644 --- a/openmeter/billing/charges/usagebased/service/rating/periodpreserving/engine.go +++ b/openmeter/billing/charges/usagebased/service/rating/periodpreserving/engine.go @@ -233,7 +233,6 @@ func (e Engine) buildDetailsByEpoch(ctx context.Context, in Input) (map[epochClo } detailedLinesWithUsageFromPriorPeriods := usagebased.NewDetailedLinesFromBilling( - in.Intent, epoch.epochClosedPeriod.AsClosedPeriod(), billingDetailedLines.DetailedLines, ) diff --git a/openmeter/billing/charges/usagebased/service/rating/service_test.go b/openmeter/billing/charges/usagebased/service/rating/service_test.go index 6f6d9e35bd..632979ad71 100644 --- a/openmeter/billing/charges/usagebased/service/rating/service_test.go +++ b/openmeter/billing/charges/usagebased/service/rating/service_test.go @@ -25,7 +25,6 @@ import ( "github.com/openmeterio/openmeter/openmeter/productcatalog" "github.com/openmeterio/openmeter/openmeter/productcatalog/feature" streamingtestutils "github.com/openmeterio/openmeter/openmeter/streaming/testutils" - "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/timeutil" ) @@ -87,10 +86,7 @@ func TestNewDetailedLinesFromBilling(t *testing.T) { From: time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC), To: time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC), } - intent := newDetailedRatingTestCharge(t, defaultServicePeriod, nil).Intent - out := usagebased.NewDetailedLinesFromBilling( - intent.GetEffectiveIntent(), defaultServicePeriod, billingrating.DetailedLines{ { @@ -119,7 +115,6 @@ func TestNewDetailedLinesFromBilling(t *testing.T) { require.Empty(t, out[0].Namespace) require.Equal(t, "Usage", out[0].Name) require.Equal(t, defaultServicePeriod, out[0].ServicePeriod) - require.Equal(t, currencyx.Code("USD"), out[0].Currency) require.Equal(t, productcatalog.InArrearsPaymentTerm, out[0].PaymentTerm) require.Equal(t, stddetailedline.CategoryRegular, out[0].Category) require.Equal(t, float64(12), out[0].Quantity.InexactFloat64()) @@ -174,7 +169,6 @@ func TestGetDetailedRatingForUsageUsesPeriodPreservingRatingEngine(t *testing.T) Name: "Usage", }), ServicePeriod: priorPeriod, - Currency: currencyx.Code("USD"), ChildUniqueReferenceID: ratingtestutils.FormatDetailedLineChildUniqueReferenceID(billingrating.UnitPriceUsageChildUniqueReferenceID, priorPeriod), PaymentTerm: productcatalog.InArrearsPaymentTerm, PerUnitAmount: alpacadecimal.NewFromInt(3), diff --git a/openmeter/billing/charges/usagebased/service/rating/subtract/subtract.go b/openmeter/billing/charges/usagebased/service/rating/subtract/subtract.go index 74c4594ebb..86d595acff 100644 --- a/openmeter/billing/charges/usagebased/service/rating/subtract/subtract.go +++ b/openmeter/billing/charges/usagebased/service/rating/subtract/subtract.go @@ -3,7 +3,6 @@ package subtract import ( "fmt" "sort" - "strings" "github.com/alpacahq/alpacadecimal" "github.com/samber/lo" @@ -70,10 +69,6 @@ func SubtractRatedRunDetails( return nil, fmt.Errorf("previous detailed lines: %w", err) } - if err := validateCurrencyForSubtract(current, previous); err != nil { - return nil, err - } - out, err := subtractDetailedLinesByKey( sumDetailedLinesByKey(current), sumDetailedLinesByKey(previous), @@ -107,34 +102,6 @@ func validateDetailedLinesForSubtract(lines usagebased.DetailedLines) error { return nil } -func validateCurrencyForSubtract(current, previous usagebased.DetailedLines) error { - currentCurrencies := lo.Uniq(lo.Map(current, func(line usagebased.DetailedLine, _ int) string { - return string(line.Currency) - })) - - previousCurrencies := lo.Uniq(lo.Map(previous, func(line usagebased.DetailedLine, _ int) string { - return string(line.Currency) - })) - - if len(currentCurrencies) > 1 { - return fmt.Errorf("current detailed lines: multiple currencies: %v", strings.Join(currentCurrencies, ", ")) - } - - if len(previousCurrencies) > 1 { - return fmt.Errorf("previous detailed lines: multiple currencies: %v", strings.Join(previousCurrencies, ", ")) - } - - if len(currentCurrencies) == 0 || len(previousCurrencies) == 0 { - return nil - } - - if currentCurrencies[0] != previousCurrencies[0] { - return fmt.Errorf("current and previous detailed lines: currency mismatch: %s != %s", currentCurrencies[0], previousCurrencies[0]) - } - - return nil -} - func sumDetailedLinesByKey(lines usagebased.DetailedLines) map[detailedLineKey]usagebased.DetailedLines { grouped := make(map[detailedLineKey]usagebased.DetailedLines, len(lines)) diff --git a/openmeter/billing/charges/usagebased/service/rating/subtract/subtract_test.go b/openmeter/billing/charges/usagebased/service/rating/subtract/subtract_test.go index ad11c4c2c7..7e52728671 100644 --- a/openmeter/billing/charges/usagebased/service/rating/subtract/subtract_test.go +++ b/openmeter/billing/charges/usagebased/service/rating/subtract/subtract_test.go @@ -16,7 +16,6 @@ import ( billingratingservice "github.com/openmeterio/openmeter/openmeter/billing/rating/service" currenciestestutils "github.com/openmeterio/openmeter/openmeter/currencies/testutils/currency" "github.com/openmeterio/openmeter/openmeter/productcatalog" - "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/timeutil" ) @@ -648,47 +647,6 @@ func TestSubtractRatedRunDetailsDoesNotMatchDifferentPricerReferenceIDs(t *testi }, toExpectedDetailedLines(out)) } -func TestSubtractRatedRunDetailsRejectsCurrencyMismatch(t *testing.T) { - t.Parallel() - - servicePeriod := timeutil.ClosedPeriod{ - From: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), - To: time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC), - } - - current := makeRatedDetailedLineForTest(ratedDetailedLineForTestInput{ - referenceID: "unit-price-usage", - servicePeriod: servicePeriod, - perUnitAmount: 10, - quantity: 5, - totals: expectedTotals{ - Amount: 50, - Total: 50, - }, - }) - previous := makeRatedDetailedLineForTest(ratedDetailedLineForTestInput{ - referenceID: "unit-price-usage", - servicePeriod: servicePeriod, - perUnitAmount: 10, - quantity: 3, - totals: expectedTotals{ - Amount: 30, - Total: 30, - }, - }) - previous.Currency = currencyx.Code("EUR") - - // given: - // - current and previous detailed lines with different currencies - // when: - // - we subtract the previous detailed lines from the current detailed lines - // then: - // - subtraction rejects the invocation instead of treating currency as an arithmetic key - - _, err := SubtractRatedRunDetails(usagebased.DetailedLines{current}, usagebased.DetailedLines{previous}, NewMockUniqueReferenceIDGenerator(t)) - require.ErrorContains(t, err, "current and previous detailed lines: currency mismatch: USD != EUR") -} - func TestSubtractRatedRunDetailsPreservesMatchedCurrentServicePeriod(t *testing.T) { t.Parallel() @@ -1432,7 +1390,6 @@ func usageBasedDetailedLinesForTest(lines billingrating.DetailedLines, servicePe ChildUniqueReferenceID: line.ChildUniqueReferenceID, PaymentTerm: paymentTerm, ServicePeriod: period, - Currency: currencyx.Code("USD"), PerUnitAmount: line.PerUnitAmount, Quantity: line.Quantity, Totals: line.Totals, @@ -1569,7 +1526,6 @@ func makeRatedDetailedLineForTest(in ratedDetailedLineForTestInput) usagebased.D Index: in.index, PaymentTerm: productcatalog.InArrearsPaymentTerm, ServicePeriod: in.servicePeriod, - Currency: currencyx.Code("USD"), PerUnitAmount: alpacadecimal.NewFromFloat(in.perUnitAmount), Quantity: alpacadecimal.NewFromFloat(in.quantity), Totals: totals.Totals{ diff --git a/openmeter/billing/httpdriver/invoiceline.go b/openmeter/billing/httpdriver/invoiceline.go index 54cb6f9041..4df1ab90cb 100644 --- a/openmeter/billing/httpdriver/invoiceline.go +++ b/openmeter/billing/httpdriver/invoiceline.go @@ -259,9 +259,9 @@ func mapTaxConfigToAPI(to *productcatalog.TaxConfig) *api.TaxConfig { return lo.ToPtr(productcataloghttp.FromTaxConfig(*to)) } -func mapDetailedLinesToAPI(lines billing.DetailedLines, invoiceAt time.Time, taxConfig *productcatalog.TaxConfig) (*[]api.InvoiceDetailedLine, error) { +func mapDetailedLinesToAPI(lines billing.DetailedLines, currency currencyx.Code, invoiceAt time.Time, taxConfig *productcatalog.TaxConfig) (*[]api.InvoiceDetailedLine, error) { mappedLines, err := slicesx.MapWithErr(lines, func(line billing.DetailedLine) (api.InvoiceDetailedLine, error) { - return mapDetailedLineToAPI(line, invoiceAt, taxConfig) + return mapDetailedLineToAPI(line, currency, invoiceAt, taxConfig) }) if err != nil { return nil, fmt.Errorf("failed to map detailed lines: %w", err) @@ -270,7 +270,7 @@ func mapDetailedLinesToAPI(lines billing.DetailedLines, invoiceAt time.Time, tax return lo.ToPtr(mappedLines), nil } -func mapDetailedLineToAPI(line billing.DetailedLine, invoiceAt time.Time, taxConfig *productcatalog.TaxConfig) (api.InvoiceDetailedLine, error) { +func mapDetailedLineToAPI(line billing.DetailedLine, currency currencyx.Code, invoiceAt time.Time, taxConfig *productcatalog.TaxConfig) (api.InvoiceDetailedLine, error) { amountDiscountsAPI, err := mapInvoiceLineAmountDiscountsToAPI(line.AmountDiscounts) if err != nil { return api.InvoiceDetailedLine{}, fmt.Errorf("failed to map amount discounts: %w", err) @@ -292,7 +292,7 @@ func mapDetailedLineToAPI(line billing.DetailedLine, invoiceAt time.Time, taxCon UpdatedAt: line.UpdatedAt, InvoiceAt: invoiceAt, - Currency: string(line.Currency), + Currency: string(currency), Status: api.InvoiceLineStatusDetailed, Description: line.Description, @@ -356,7 +356,7 @@ func mapInvoiceLineToAPI(line *billing.StandardLine) (api.InvoiceLine, error) { return api.InvoiceLine{}, fmt.Errorf("failed to map price: %w", err) } - children, err := mapDetailedLinesToAPI(line.DetailedLines, line.InvoiceAt, line.TaxConfig.ToProductCatalog()) + children, err := mapDetailedLinesToAPI(line.DetailedLines, line.Currency, line.InvoiceAt, line.TaxConfig.ToProductCatalog()) if err != nil { return api.InvoiceLine{}, fmt.Errorf("failed to map children: %w", err) } diff --git a/openmeter/billing/invoicedetailedline_test.go b/openmeter/billing/invoicedetailedline_test.go index e92f679c6a..64f859de26 100644 --- a/openmeter/billing/invoicedetailedline_test.go +++ b/openmeter/billing/invoicedetailedline_test.go @@ -178,7 +178,6 @@ func validDetailedLineForValidation() DetailedLine { From: start, To: start.Add(time.Hour), }, - Currency: currencyx.Code("USD"), PerUnitAmount: alpacadecimal.NewFromInt(10), Quantity: alpacadecimal.NewFromInt(1), }, diff --git a/openmeter/billing/models/stddetailedline/create.go b/openmeter/billing/models/stddetailedline/create.go index 760da2feaf..198257556b 100644 --- a/openmeter/billing/models/stddetailedline/create.go +++ b/openmeter/billing/models/stddetailedline/create.go @@ -8,7 +8,6 @@ import ( "github.com/openmeterio/openmeter/openmeter/billing/models/externalid" billingtotals "github.com/openmeterio/openmeter/openmeter/billing/models/totals" "github.com/openmeterio/openmeter/openmeter/productcatalog" - "github.com/openmeterio/openmeter/pkg/currencyx" ) type Creator[T any] interface { @@ -17,7 +16,6 @@ type Creator[T any] interface { SetName(string) T SetNillableDescription(*string) T - SetCurrency(currencyx.Code) T SetServicePeriodStart(time.Time) T SetServicePeriodEnd(time.Time) T SetQuantity(alpacadecimal.Decimal) T @@ -33,7 +31,6 @@ func Create[T Creator[T]](creator Creator[T], line Base) T { create := creator. SetName(line.Name). SetNillableDescription(line.Description). - SetCurrency(line.Currency). SetServicePeriodStart(line.ServicePeriod.From.In(time.UTC)). SetServicePeriodEnd(line.ServicePeriod.To.In(time.UTC)). SetQuantity(line.Quantity). diff --git a/openmeter/billing/models/stddetailedline/derived.gen.go b/openmeter/billing/models/stddetailedline/derived.gen.go index 288115b727..c0dd730d59 100644 --- a/openmeter/billing/models/stddetailedline/derived.gen.go +++ b/openmeter/billing/models/stddetailedline/derived.gen.go @@ -17,7 +17,6 @@ func deriveEqualBase(this, that *Base) bool { ((this.Index == nil && that.Index == nil) || (this.Index != nil && that.Index != nil && *(this.Index) == *(that.Index))) && this.PaymentTerm == that.PaymentTerm && this.ServicePeriod.Equal(that.ServicePeriod) && - this.Currency.Equal(that.Currency) && this.PerUnitAmount.Equal(that.PerUnitAmount) && this.Quantity.Equal(that.Quantity) && this.Totals.Equal(that.Totals) && diff --git a/openmeter/billing/models/stddetailedline/mapping.go b/openmeter/billing/models/stddetailedline/mapping.go index cdc8e91928..d1f8cf05a7 100644 --- a/openmeter/billing/models/stddetailedline/mapping.go +++ b/openmeter/billing/models/stddetailedline/mapping.go @@ -11,7 +11,6 @@ import ( "github.com/openmeterio/openmeter/openmeter/billing/models/totals" "github.com/openmeterio/openmeter/openmeter/productcatalog" "github.com/openmeterio/openmeter/pkg/convert" - "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/timeutil" ) @@ -30,7 +29,6 @@ type DBGetter interface { GetPaymentTerm() productcatalog.PaymentTermType GetServicePeriodStart() time.Time GetServicePeriodEnd() time.Time - GetCurrency() currencyx.Code GetPerUnitAmount() alpacadecimal.Decimal GetQuantity() alpacadecimal.Decimal GetCreditsApplied() *creditsapplied.CreditsApplied @@ -58,7 +56,6 @@ func FromDB[T DBGetter](dbEntity T) Base { From: dbEntity.GetServicePeriodStart().In(time.UTC), To: dbEntity.GetServicePeriodEnd().In(time.UTC), }, - Currency: dbEntity.GetCurrency(), PerUnitAmount: dbEntity.GetPerUnitAmount(), Quantity: dbEntity.GetQuantity(), Totals: totals.FromDB(dbEntity), diff --git a/openmeter/billing/models/stddetailedline/mixin.go b/openmeter/billing/models/stddetailedline/mixin.go index 75ddae1321..cd472889f3 100644 --- a/openmeter/billing/models/stddetailedline/mixin.go +++ b/openmeter/billing/models/stddetailedline/mixin.go @@ -36,8 +36,10 @@ func (mixinBase) Fields() []ent.Field { return []ent.Field{ field.String("currency"). GoType(currencyx.Code("")). - NotEmpty(). + Optional(). + Nillable(). Immutable(). + Deprecated("currency is defined by the parent line or charge"). SchemaType(map[string]string{ dialect.Postgres: "varchar(3)", }), diff --git a/openmeter/billing/models/stddetailedline/model.go b/openmeter/billing/models/stddetailedline/model.go index 83e324f826..ddbcb42deb 100644 --- a/openmeter/billing/models/stddetailedline/model.go +++ b/openmeter/billing/models/stddetailedline/model.go @@ -13,7 +13,6 @@ import ( "github.com/openmeterio/openmeter/openmeter/billing/models/externalid" "github.com/openmeterio/openmeter/openmeter/billing/models/totals" "github.com/openmeterio/openmeter/openmeter/productcatalog" - "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/timeutil" ) @@ -53,7 +52,6 @@ type Base struct { PaymentTerm productcatalog.PaymentTermType `json:"paymentTerm"` ServicePeriod timeutil.ClosedPeriod `json:"servicePeriod"` - Currency currencyx.Code `json:"currency"` PerUnitAmount alpacadecimal.Decimal `json:"perUnitAmount"` Quantity alpacadecimal.Decimal `json:"quantity"` Totals totals.Totals `json:"totals"` @@ -106,10 +104,6 @@ func (l Base) Validate(opts ...ValidateOption) error { errs = append(errs, fmt.Errorf("service period: %w", err)) } - if err := l.Currency.Validate(); err != nil { - errs = append(errs, fmt.Errorf("currency: %w", err)) - } - if err := l.CreditsApplied.Validate(); err != nil { errs = append(errs, fmt.Errorf("credits applied: %w", err)) } diff --git a/openmeter/billing/service/invoicecalc/details.go b/openmeter/billing/service/invoicecalc/details.go index fa17000c40..1e24fd2dac 100644 --- a/openmeter/billing/service/invoicecalc/details.go +++ b/openmeter/billing/service/invoicecalc/details.go @@ -116,7 +116,6 @@ func newDetailedLines(line *billing.StandardLine, inputs ...rating.DetailedLine) Name: in.Name, }), ServicePeriod: period, - Currency: line.Currency, ChildUniqueReferenceID: in.ChildUniqueReferenceID, PaymentTerm: lo.CoalesceOrEmpty(in.PaymentTerm, productcatalog.InArrearsPaymentTerm), PerUnitAmount: in.PerUnitAmount, diff --git a/openmeter/billing/stdinvoiceline.go b/openmeter/billing/stdinvoiceline.go index 74994ff971..348c3b080c 100644 --- a/openmeter/billing/stdinvoiceline.go +++ b/openmeter/billing/stdinvoiceline.go @@ -713,12 +713,6 @@ func (i StandardLine) Validate() error { errs = append(errs, fmt.Errorf("detailed lines: %w", err)) } - for _, detailedLine := range i.DetailedLines { - if detailedLine.Currency != i.Currency { - errs = append(errs, fmt.Errorf("detailed line[%s]: currency[%s] is not equal to line currency[%s]", detailedLine.ID, detailedLine.Currency, i.Currency)) - } - } - if err := i.UsageBased.Validate(); err != nil { errs = append(errs, err) } diff --git a/openmeter/billing/stdinvoiceline_test.go b/openmeter/billing/stdinvoiceline_test.go index 6ae7b3a840..15c63ead02 100644 --- a/openmeter/billing/stdinvoiceline_test.go +++ b/openmeter/billing/stdinvoiceline_test.go @@ -53,7 +53,6 @@ func TestStandardLineValidateAllowsNegativeDetailedLineQuantityWithPositiveTotal ChildUniqueReferenceID: "detail_123", PaymentTerm: productcatalog.InArrearsPaymentTerm, ServicePeriod: line.Period, - Currency: line.Currency, PerUnitAmount: alpacadecimal.NewFromInt(10), Quantity: alpacadecimal.NewFromInt(-1), }, diff --git a/openmeter/ent/db/billingstandardinvoicedetailedline.go b/openmeter/ent/db/billingstandardinvoicedetailedline.go index c72818a374..bb28d7b755 100644 --- a/openmeter/ent/db/billingstandardinvoicedetailedline.go +++ b/openmeter/ent/db/billingstandardinvoicedetailedline.go @@ -27,7 +27,9 @@ type BillingStandardInvoiceDetailedLine struct { // ID of the ent. ID string `json:"id,omitempty"` // Currency holds the value of the "currency" field. - Currency currencyx.Code `json:"currency,omitempty"` + // + // Deprecated: currency is defined by the parent line or charge + Currency *currencyx.Code `json:"currency,omitempty"` // ServicePeriodStart holds the value of the "service_period_start" field. ServicePeriodStart time.Time `json:"service_period_start,omitempty"` // ServicePeriodEnd holds the value of the "service_period_end" field. @@ -178,7 +180,8 @@ func (_m *BillingStandardInvoiceDetailedLine) assignValues(columns []string, val if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field currency", values[i]) } else if value.Valid { - _m.Currency = currencyx.Code(value.String) + _m.Currency = new(currencyx.Code) + *_m.Currency = currencyx.Code(value.String) } case billingstandardinvoicedetailedline.FieldServicePeriodStart: if value, ok := values[i].(*sql.NullTime); !ok { @@ -409,8 +412,10 @@ func (_m *BillingStandardInvoiceDetailedLine) String() string { var builder strings.Builder builder.WriteString("BillingStandardInvoiceDetailedLine(") builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) - builder.WriteString("currency=") - builder.WriteString(fmt.Sprintf("%v", _m.Currency)) + if v := _m.Currency; v != nil { + builder.WriteString("currency=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } builder.WriteString(", ") builder.WriteString("service_period_start=") builder.WriteString(_m.ServicePeriodStart.Format(time.ANSIC)) diff --git a/openmeter/ent/db/billingstandardinvoicedetailedline/billingstandardinvoicedetailedline.go b/openmeter/ent/db/billingstandardinvoicedetailedline/billingstandardinvoicedetailedline.go index 0ea48650b5..9d46ef7cc3 100644 --- a/openmeter/ent/db/billingstandardinvoicedetailedline/billingstandardinvoicedetailedline.go +++ b/openmeter/ent/db/billingstandardinvoicedetailedline/billingstandardinvoicedetailedline.go @@ -109,7 +109,6 @@ const ( // Columns holds all SQL columns for billingstandardinvoicedetailedline fields. var Columns = []string{ FieldID, - FieldCurrency, FieldServicePeriodStart, FieldServicePeriodEnd, FieldQuantity, @@ -145,7 +144,7 @@ func ValidColumn(column string) bool { return true } } - for _, f := range [...]string{FieldAnnotations, FieldMetadata} { + for _, f := range [...]string{FieldCurrency, FieldAnnotations, FieldMetadata} { if column == f { return true } @@ -154,8 +153,6 @@ func ValidColumn(column string) bool { } var ( - // CurrencyValidator is a validator for the "currency" field. It is called by the builders before save. - CurrencyValidator func(string) error // ChildUniqueReferenceIDValidator is a validator for the "child_unique_reference_id" field. It is called by the builders before save. ChildUniqueReferenceIDValidator func(string) error // NamespaceValidator is a validator for the "namespace" field. It is called by the builders before save. diff --git a/openmeter/ent/db/billingstandardinvoicedetailedline/where.go b/openmeter/ent/db/billingstandardinvoicedetailedline/where.go index e751f4be34..00bab756a8 100644 --- a/openmeter/ent/db/billingstandardinvoicedetailedline/where.go +++ b/openmeter/ent/db/billingstandardinvoicedetailedline/where.go @@ -262,6 +262,16 @@ func CurrencyHasSuffix(v currencyx.Code) predicate.BillingStandardInvoiceDetaile return predicate.BillingStandardInvoiceDetailedLine(sql.FieldHasSuffix(FieldCurrency, vc)) } +// CurrencyIsNil applies the IsNil predicate on the "currency" field. +func CurrencyIsNil() predicate.BillingStandardInvoiceDetailedLine { + return predicate.BillingStandardInvoiceDetailedLine(sql.FieldIsNull(FieldCurrency)) +} + +// CurrencyNotNil applies the NotNil predicate on the "currency" field. +func CurrencyNotNil() predicate.BillingStandardInvoiceDetailedLine { + return predicate.BillingStandardInvoiceDetailedLine(sql.FieldNotNull(FieldCurrency)) +} + // CurrencyEqualFold applies the EqualFold predicate on the "currency" field. func CurrencyEqualFold(v currencyx.Code) predicate.BillingStandardInvoiceDetailedLine { vc := string(v) diff --git a/openmeter/ent/db/billingstandardinvoicedetailedline_create.go b/openmeter/ent/db/billingstandardinvoicedetailedline_create.go index 7a3381b7c2..5e9d31ed13 100644 --- a/openmeter/ent/db/billingstandardinvoicedetailedline_create.go +++ b/openmeter/ent/db/billingstandardinvoicedetailedline_create.go @@ -38,6 +38,14 @@ func (_c *BillingStandardInvoiceDetailedLineCreate) SetCurrency(v currencyx.Code return _c } +// SetNillableCurrency sets the "currency" field if the given value is not nil. +func (_c *BillingStandardInvoiceDetailedLineCreate) SetNillableCurrency(v *currencyx.Code) *BillingStandardInvoiceDetailedLineCreate { + if v != nil { + _c.SetCurrency(*v) + } + return _c +} + // SetServicePeriodStart sets the "service_period_start" field. func (_c *BillingStandardInvoiceDetailedLineCreate) SetServicePeriodStart(v time.Time) *BillingStandardInvoiceDetailedLineCreate { _c.mutation.SetServicePeriodStart(v) @@ -380,11 +388,8 @@ func (_c *BillingStandardInvoiceDetailedLineCreate) defaults() { // check runs all checks and user-defined validators on the builder. func (_c *BillingStandardInvoiceDetailedLineCreate) check() error { - if _, ok := _c.mutation.Currency(); !ok { - return &ValidationError{Name: "currency", err: errors.New(`db: missing required field "BillingStandardInvoiceDetailedLine.currency"`)} - } if v, ok := _c.mutation.Currency(); ok { - if err := billingstandardinvoicedetailedline.CurrencyValidator(string(v)); err != nil { + if err := v.Validate(); err != nil { return &ValidationError{Name: "currency", err: fmt.Errorf(`db: validator failed for field "BillingStandardInvoiceDetailedLine.currency": %w`, err)} } } @@ -520,7 +525,7 @@ func (_c *BillingStandardInvoiceDetailedLineCreate) createSpec() (*BillingStanda } if value, ok := _c.mutation.Currency(); ok { _spec.SetField(billingstandardinvoicedetailedline.FieldCurrency, field.TypeString, value) - _node.Currency = value + _node.Currency = &value } if value, ok := _c.mutation.ServicePeriodStart(); ok { _spec.SetField(billingstandardinvoicedetailedline.FieldServicePeriodStart, field.TypeTime, value) diff --git a/openmeter/ent/db/billingstandardinvoicedetailedline_update.go b/openmeter/ent/db/billingstandardinvoicedetailedline_update.go index 0ad1f8cd9b..dedddf6ae8 100644 --- a/openmeter/ent/db/billingstandardinvoicedetailedline_update.go +++ b/openmeter/ent/db/billingstandardinvoicedetailedline_update.go @@ -571,6 +571,9 @@ func (_u *BillingStandardInvoiceDetailedLineUpdate) sqlSave(ctx context.Context) } } } + if _u.mutation.CurrencyCleared() { + _spec.ClearField(billingstandardinvoicedetailedline.FieldCurrency, field.TypeString) + } if value, ok := _u.mutation.ServicePeriodStart(); ok { _spec.SetField(billingstandardinvoicedetailedline.FieldServicePeriodStart, field.TypeTime, value) } @@ -1355,6 +1358,9 @@ func (_u *BillingStandardInvoiceDetailedLineUpdateOne) sqlSave(ctx context.Conte } } } + if _u.mutation.CurrencyCleared() { + _spec.ClearField(billingstandardinvoicedetailedline.FieldCurrency, field.TypeString) + } if value, ok := _u.mutation.ServicePeriodStart(); ok { _spec.SetField(billingstandardinvoicedetailedline.FieldServicePeriodStart, field.TypeTime, value) } diff --git a/openmeter/ent/db/chargeflatfeerundetailedline.go b/openmeter/ent/db/chargeflatfeerundetailedline.go index 34294356c4..6ddf9786bf 100644 --- a/openmeter/ent/db/chargeflatfeerundetailedline.go +++ b/openmeter/ent/db/chargeflatfeerundetailedline.go @@ -26,7 +26,9 @@ type ChargeFlatFeeRunDetailedLine struct { // ID of the ent. ID string `json:"id,omitempty"` // Currency holds the value of the "currency" field. - Currency currencyx.Code `json:"currency,omitempty"` + // + // Deprecated: currency is defined by the parent line or charge + Currency *currencyx.Code `json:"currency,omitempty"` // ServicePeriodStart holds the value of the "service_period_start" field. ServicePeriodStart time.Time `json:"service_period_start,omitempty"` // ServicePeriodEnd holds the value of the "service_period_end" field. @@ -153,7 +155,8 @@ func (_m *ChargeFlatFeeRunDetailedLine) assignValues(columns []string, values [] if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field currency", values[i]) } else if value.Valid { - _m.Currency = currencyx.Code(value.String) + _m.Currency = new(currencyx.Code) + *_m.Currency = currencyx.Code(value.String) } case chargeflatfeerundetailedline.FieldServicePeriodStart: if value, ok := values[i].(*sql.NullTime); !ok { @@ -374,8 +377,10 @@ func (_m *ChargeFlatFeeRunDetailedLine) String() string { var builder strings.Builder builder.WriteString("ChargeFlatFeeRunDetailedLine(") builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) - builder.WriteString("currency=") - builder.WriteString(fmt.Sprintf("%v", _m.Currency)) + if v := _m.Currency; v != nil { + builder.WriteString("currency=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } builder.WriteString(", ") builder.WriteString("service_period_start=") builder.WriteString(_m.ServicePeriodStart.Format(time.ANSIC)) diff --git a/openmeter/ent/db/chargeflatfeerundetailedline/chargeflatfeerundetailedline.go b/openmeter/ent/db/chargeflatfeerundetailedline/chargeflatfeerundetailedline.go index 34668b791c..438db96c08 100644 --- a/openmeter/ent/db/chargeflatfeerundetailedline/chargeflatfeerundetailedline.go +++ b/openmeter/ent/db/chargeflatfeerundetailedline/chargeflatfeerundetailedline.go @@ -91,7 +91,6 @@ const ( // Columns holds all SQL columns for chargeflatfeerundetailedline fields. var Columns = []string{ FieldID, - FieldCurrency, FieldServicePeriodStart, FieldServicePeriodEnd, FieldQuantity, @@ -127,7 +126,7 @@ func ValidColumn(column string) bool { return true } } - for _, f := range [...]string{FieldAnnotations, FieldMetadata} { + for _, f := range [...]string{FieldCurrency, FieldAnnotations, FieldMetadata} { if column == f { return true } @@ -136,8 +135,6 @@ func ValidColumn(column string) bool { } var ( - // CurrencyValidator is a validator for the "currency" field. It is called by the builders before save. - CurrencyValidator func(string) error // ChildUniqueReferenceIDValidator is a validator for the "child_unique_reference_id" field. It is called by the builders before save. ChildUniqueReferenceIDValidator func(string) error // NamespaceValidator is a validator for the "namespace" field. It is called by the builders before save. diff --git a/openmeter/ent/db/chargeflatfeerundetailedline/where.go b/openmeter/ent/db/chargeflatfeerundetailedline/where.go index 25d01ec65f..b997d3b3ea 100644 --- a/openmeter/ent/db/chargeflatfeerundetailedline/where.go +++ b/openmeter/ent/db/chargeflatfeerundetailedline/where.go @@ -262,6 +262,16 @@ func CurrencyHasSuffix(v currencyx.Code) predicate.ChargeFlatFeeRunDetailedLine return predicate.ChargeFlatFeeRunDetailedLine(sql.FieldHasSuffix(FieldCurrency, vc)) } +// CurrencyIsNil applies the IsNil predicate on the "currency" field. +func CurrencyIsNil() predicate.ChargeFlatFeeRunDetailedLine { + return predicate.ChargeFlatFeeRunDetailedLine(sql.FieldIsNull(FieldCurrency)) +} + +// CurrencyNotNil applies the NotNil predicate on the "currency" field. +func CurrencyNotNil() predicate.ChargeFlatFeeRunDetailedLine { + return predicate.ChargeFlatFeeRunDetailedLine(sql.FieldNotNull(FieldCurrency)) +} + // CurrencyEqualFold applies the EqualFold predicate on the "currency" field. func CurrencyEqualFold(v currencyx.Code) predicate.ChargeFlatFeeRunDetailedLine { vc := string(v) diff --git a/openmeter/ent/db/chargeflatfeerundetailedline_create.go b/openmeter/ent/db/chargeflatfeerundetailedline_create.go index 64c0cd9b19..65030b1bbb 100644 --- a/openmeter/ent/db/chargeflatfeerundetailedline_create.go +++ b/openmeter/ent/db/chargeflatfeerundetailedline_create.go @@ -36,6 +36,14 @@ func (_c *ChargeFlatFeeRunDetailedLineCreate) SetCurrency(v currencyx.Code) *Cha return _c } +// SetNillableCurrency sets the "currency" field if the given value is not nil. +func (_c *ChargeFlatFeeRunDetailedLineCreate) SetNillableCurrency(v *currencyx.Code) *ChargeFlatFeeRunDetailedLineCreate { + if v != nil { + _c.SetCurrency(*v) + } + return _c +} + // SetServicePeriodStart sets the "service_period_start" field. func (_c *ChargeFlatFeeRunDetailedLineCreate) SetServicePeriodStart(v time.Time) *ChargeFlatFeeRunDetailedLineCreate { _c.mutation.SetServicePeriodStart(v) @@ -346,11 +354,8 @@ func (_c *ChargeFlatFeeRunDetailedLineCreate) defaults() { // check runs all checks and user-defined validators on the builder. func (_c *ChargeFlatFeeRunDetailedLineCreate) check() error { - if _, ok := _c.mutation.Currency(); !ok { - return &ValidationError{Name: "currency", err: errors.New(`db: missing required field "ChargeFlatFeeRunDetailedLine.currency"`)} - } if v, ok := _c.mutation.Currency(); ok { - if err := chargeflatfeerundetailedline.CurrencyValidator(string(v)); err != nil { + if err := v.Validate(); err != nil { return &ValidationError{Name: "currency", err: fmt.Errorf(`db: validator failed for field "ChargeFlatFeeRunDetailedLine.currency": %w`, err)} } } @@ -488,7 +493,7 @@ func (_c *ChargeFlatFeeRunDetailedLineCreate) createSpec() (*ChargeFlatFeeRunDet } if value, ok := _c.mutation.Currency(); ok { _spec.SetField(chargeflatfeerundetailedline.FieldCurrency, field.TypeString, value) - _node.Currency = value + _node.Currency = &value } if value, ok := _c.mutation.ServicePeriodStart(); ok { _spec.SetField(chargeflatfeerundetailedline.FieldServicePeriodStart, field.TypeTime, value) diff --git a/openmeter/ent/db/chargeflatfeerundetailedline_update.go b/openmeter/ent/db/chargeflatfeerundetailedline_update.go index 50d51f3a64..e3ae822d49 100644 --- a/openmeter/ent/db/chargeflatfeerundetailedline_update.go +++ b/openmeter/ent/db/chargeflatfeerundetailedline_update.go @@ -486,6 +486,9 @@ func (_u *ChargeFlatFeeRunDetailedLineUpdate) sqlSave(ctx context.Context) (_nod } } } + if _u.mutation.CurrencyCleared() { + _spec.ClearField(chargeflatfeerundetailedline.FieldCurrency, field.TypeString) + } if value, ok := _u.mutation.ServicePeriodStart(); ok { _spec.SetField(chargeflatfeerundetailedline.FieldServicePeriodStart, field.TypeTime, value) } @@ -1088,6 +1091,9 @@ func (_u *ChargeFlatFeeRunDetailedLineUpdateOne) sqlSave(ctx context.Context) (_ } } } + if _u.mutation.CurrencyCleared() { + _spec.ClearField(chargeflatfeerundetailedline.FieldCurrency, field.TypeString) + } if value, ok := _u.mutation.ServicePeriodStart(); ok { _spec.SetField(chargeflatfeerundetailedline.FieldServicePeriodStart, field.TypeTime, value) } diff --git a/openmeter/ent/db/chargeusagebasedrundetailedline.go b/openmeter/ent/db/chargeusagebasedrundetailedline.go index 643c62d1b4..21008d7050 100644 --- a/openmeter/ent/db/chargeusagebasedrundetailedline.go +++ b/openmeter/ent/db/chargeusagebasedrundetailedline.go @@ -27,7 +27,9 @@ type ChargeUsageBasedRunDetailedLine struct { // ID of the ent. ID string `json:"id,omitempty"` // Currency holds the value of the "currency" field. - Currency currencyx.Code `json:"currency,omitempty"` + // + // Deprecated: currency is defined by the parent line or charge + Currency *currencyx.Code `json:"currency,omitempty"` // ServicePeriodStart holds the value of the "service_period_start" field. ServicePeriodStart time.Time `json:"service_period_start,omitempty"` // ServicePeriodEnd holds the value of the "service_period_end" field. @@ -184,7 +186,8 @@ func (_m *ChargeUsageBasedRunDetailedLine) assignValues(columns []string, values if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field currency", values[i]) } else if value.Valid { - _m.Currency = currencyx.Code(value.String) + _m.Currency = new(currencyx.Code) + *_m.Currency = currencyx.Code(value.String) } case chargeusagebasedrundetailedline.FieldServicePeriodStart: if value, ok := values[i].(*sql.NullTime); !ok { @@ -428,8 +431,10 @@ func (_m *ChargeUsageBasedRunDetailedLine) String() string { var builder strings.Builder builder.WriteString("ChargeUsageBasedRunDetailedLine(") builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) - builder.WriteString("currency=") - builder.WriteString(fmt.Sprintf("%v", _m.Currency)) + if v := _m.Currency; v != nil { + builder.WriteString("currency=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } builder.WriteString(", ") builder.WriteString("service_period_start=") builder.WriteString(_m.ServicePeriodStart.Format(time.ANSIC)) diff --git a/openmeter/ent/db/chargeusagebasedrundetailedline/chargeusagebasedrundetailedline.go b/openmeter/ent/db/chargeusagebasedrundetailedline/chargeusagebasedrundetailedline.go index f30d513807..da66eb0b92 100644 --- a/openmeter/ent/db/chargeusagebasedrundetailedline/chargeusagebasedrundetailedline.go +++ b/openmeter/ent/db/chargeusagebasedrundetailedline/chargeusagebasedrundetailedline.go @@ -113,7 +113,6 @@ const ( // Columns holds all SQL columns for chargeusagebasedrundetailedline fields. var Columns = []string{ FieldID, - FieldCurrency, FieldServicePeriodStart, FieldServicePeriodEnd, FieldQuantity, @@ -151,7 +150,7 @@ func ValidColumn(column string) bool { return true } } - for _, f := range [...]string{FieldAnnotations, FieldMetadata} { + for _, f := range [...]string{FieldCurrency, FieldAnnotations, FieldMetadata} { if column == f { return true } @@ -160,8 +159,6 @@ func ValidColumn(column string) bool { } var ( - // CurrencyValidator is a validator for the "currency" field. It is called by the builders before save. - CurrencyValidator func(string) error // ChildUniqueReferenceIDValidator is a validator for the "child_unique_reference_id" field. It is called by the builders before save. ChildUniqueReferenceIDValidator func(string) error // NamespaceValidator is a validator for the "namespace" field. It is called by the builders before save. diff --git a/openmeter/ent/db/chargeusagebasedrundetailedline/where.go b/openmeter/ent/db/chargeusagebasedrundetailedline/where.go index 3b9b17aefb..a58ebfadfd 100644 --- a/openmeter/ent/db/chargeusagebasedrundetailedline/where.go +++ b/openmeter/ent/db/chargeusagebasedrundetailedline/where.go @@ -272,6 +272,16 @@ func CurrencyHasSuffix(v currencyx.Code) predicate.ChargeUsageBasedRunDetailedLi return predicate.ChargeUsageBasedRunDetailedLine(sql.FieldHasSuffix(FieldCurrency, vc)) } +// CurrencyIsNil applies the IsNil predicate on the "currency" field. +func CurrencyIsNil() predicate.ChargeUsageBasedRunDetailedLine { + return predicate.ChargeUsageBasedRunDetailedLine(sql.FieldIsNull(FieldCurrency)) +} + +// CurrencyNotNil applies the NotNil predicate on the "currency" field. +func CurrencyNotNil() predicate.ChargeUsageBasedRunDetailedLine { + return predicate.ChargeUsageBasedRunDetailedLine(sql.FieldNotNull(FieldCurrency)) +} + // CurrencyEqualFold applies the EqualFold predicate on the "currency" field. func CurrencyEqualFold(v currencyx.Code) predicate.ChargeUsageBasedRunDetailedLine { vc := string(v) diff --git a/openmeter/ent/db/chargeusagebasedrundetailedline_create.go b/openmeter/ent/db/chargeusagebasedrundetailedline_create.go index da07ea00ac..3bb908269c 100644 --- a/openmeter/ent/db/chargeusagebasedrundetailedline_create.go +++ b/openmeter/ent/db/chargeusagebasedrundetailedline_create.go @@ -37,6 +37,14 @@ func (_c *ChargeUsageBasedRunDetailedLineCreate) SetCurrency(v currencyx.Code) * return _c } +// SetNillableCurrency sets the "currency" field if the given value is not nil. +func (_c *ChargeUsageBasedRunDetailedLineCreate) SetNillableCurrency(v *currencyx.Code) *ChargeUsageBasedRunDetailedLineCreate { + if v != nil { + _c.SetCurrency(*v) + } + return _c +} + // SetServicePeriodStart sets the "service_period_start" field. func (_c *ChargeUsageBasedRunDetailedLineCreate) SetServicePeriodStart(v time.Time) *ChargeUsageBasedRunDetailedLineCreate { _c.mutation.SetServicePeriodStart(v) @@ -377,11 +385,8 @@ func (_c *ChargeUsageBasedRunDetailedLineCreate) defaults() { // check runs all checks and user-defined validators on the builder. func (_c *ChargeUsageBasedRunDetailedLineCreate) check() error { - if _, ok := _c.mutation.Currency(); !ok { - return &ValidationError{Name: "currency", err: errors.New(`db: missing required field "ChargeUsageBasedRunDetailedLine.currency"`)} - } if v, ok := _c.mutation.Currency(); ok { - if err := chargeusagebasedrundetailedline.CurrencyValidator(string(v)); err != nil { + if err := v.Validate(); err != nil { return &ValidationError{Name: "currency", err: fmt.Errorf(`db: validator failed for field "ChargeUsageBasedRunDetailedLine.currency": %w`, err)} } } @@ -530,7 +535,7 @@ func (_c *ChargeUsageBasedRunDetailedLineCreate) createSpec() (*ChargeUsageBased } if value, ok := _c.mutation.Currency(); ok { _spec.SetField(chargeusagebasedrundetailedline.FieldCurrency, field.TypeString, value) - _node.Currency = value + _node.Currency = &value } if value, ok := _c.mutation.ServicePeriodStart(); ok { _spec.SetField(chargeusagebasedrundetailedline.FieldServicePeriodStart, field.TypeTime, value) diff --git a/openmeter/ent/db/chargeusagebasedrundetailedline_update.go b/openmeter/ent/db/chargeusagebasedrundetailedline_update.go index a1d1417a6a..b138c9c401 100644 --- a/openmeter/ent/db/chargeusagebasedrundetailedline_update.go +++ b/openmeter/ent/db/chargeusagebasedrundetailedline_update.go @@ -577,6 +577,9 @@ func (_u *ChargeUsageBasedRunDetailedLineUpdate) sqlSave(ctx context.Context) (_ } } } + if _u.mutation.CurrencyCleared() { + _spec.ClearField(chargeusagebasedrundetailedline.FieldCurrency, field.TypeString) + } if value, ok := _u.mutation.ServicePeriodStart(); ok { _spec.SetField(chargeusagebasedrundetailedline.FieldServicePeriodStart, field.TypeTime, value) } @@ -1355,6 +1358,9 @@ func (_u *ChargeUsageBasedRunDetailedLineUpdateOne) sqlSave(ctx context.Context) } } } + if _u.mutation.CurrencyCleared() { + _spec.ClearField(chargeusagebasedrundetailedline.FieldCurrency, field.TypeString) + } if value, ok := _u.mutation.ServicePeriodStart(); ok { _spec.SetField(chargeusagebasedrundetailedline.FieldServicePeriodStart, field.TypeTime, value) } diff --git a/openmeter/ent/db/entmixinaccessor.go b/openmeter/ent/db/entmixinaccessor.go index 9fcb6ea806..022343dfb9 100644 --- a/openmeter/ent/db/entmixinaccessor.go +++ b/openmeter/ent/db/entmixinaccessor.go @@ -805,7 +805,7 @@ func (e *BillingStandardInvoiceDetailedLine) GetID() string { return e.ID } -func (e *BillingStandardInvoiceDetailedLine) GetCurrency() currencyx.Code { +func (e *BillingStandardInvoiceDetailedLine) GetCurrency() *currencyx.Code { return e.Currency } @@ -1465,7 +1465,7 @@ func (e *ChargeFlatFeeRunDetailedLine) GetID() string { return e.ID } -func (e *ChargeFlatFeeRunDetailedLine) GetCurrency() currencyx.Code { +func (e *ChargeFlatFeeRunDetailedLine) GetCurrency() *currencyx.Code { return e.Currency } @@ -1881,7 +1881,7 @@ func (e *ChargeUsageBasedRunDetailedLine) GetID() string { return e.ID } -func (e *ChargeUsageBasedRunDetailedLine) GetCurrency() currencyx.Code { +func (e *ChargeUsageBasedRunDetailedLine) GetCurrency() *currencyx.Code { return e.Currency } diff --git a/openmeter/ent/db/migrate/schema.go b/openmeter/ent/db/migrate/schema.go index 09ae834471..48c30189be 100644 --- a/openmeter/ent/db/migrate/schema.go +++ b/openmeter/ent/db/migrate/schema.go @@ -1498,7 +1498,7 @@ var ( // BillingStandardInvoiceDetailedLinesColumns holds the columns for the "billing_standard_invoice_detailed_lines" table. BillingStandardInvoiceDetailedLinesColumns = []*schema.Column{ {Name: "id", Type: field.TypeString, Unique: true, SchemaType: map[string]string{"postgres": "char(26)"}}, - {Name: "currency", Type: field.TypeString, SchemaType: map[string]string{"postgres": "varchar(3)"}}, + {Name: "currency", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "varchar(3)"}}, {Name: "service_period_start", Type: field.TypeTime}, {Name: "service_period_end", Type: field.TypeTime}, {Name: "quantity", Type: field.TypeOther, SchemaType: map[string]string{"postgres": "numeric"}}, @@ -2401,7 +2401,7 @@ var ( // ChargeFlatFeeRunDetailedLinesColumns holds the columns for the "charge_flat_fee_run_detailed_lines" table. ChargeFlatFeeRunDetailedLinesColumns = []*schema.Column{ {Name: "id", Type: field.TypeString, Unique: true, SchemaType: map[string]string{"postgres": "char(26)"}}, - {Name: "currency", Type: field.TypeString, SchemaType: map[string]string{"postgres": "varchar(3)"}}, + {Name: "currency", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "varchar(3)"}}, {Name: "service_period_start", Type: field.TypeTime}, {Name: "service_period_end", Type: field.TypeTime}, {Name: "quantity", Type: field.TypeOther, SchemaType: map[string]string{"postgres": "numeric"}}, @@ -2874,7 +2874,7 @@ var ( // ChargeUsageBasedRunDetailedLineColumns holds the columns for the "charge_usage_based_run_detailed_line" table. ChargeUsageBasedRunDetailedLineColumns = []*schema.Column{ {Name: "id", Type: field.TypeString, Unique: true, SchemaType: map[string]string{"postgres": "char(26)"}}, - {Name: "currency", Type: field.TypeString, SchemaType: map[string]string{"postgres": "varchar(3)"}}, + {Name: "currency", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "varchar(3)"}}, {Name: "service_period_start", Type: field.TypeTime}, {Name: "service_period_end", Type: field.TypeTime}, {Name: "quantity", Type: field.TypeOther, SchemaType: map[string]string{"postgres": "numeric"}}, diff --git a/openmeter/ent/db/mutation.go b/openmeter/ent/db/mutation.go index c557248cfe..1e1e116a49 100644 --- a/openmeter/ent/db/mutation.go +++ b/openmeter/ent/db/mutation.go @@ -33222,7 +33222,7 @@ func (m *BillingStandardInvoiceDetailedLineMutation) Currency() (r currencyx.Cod // OldCurrency returns the old "currency" field's value of the BillingStandardInvoiceDetailedLine entity. // If the BillingStandardInvoiceDetailedLine 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 *BillingStandardInvoiceDetailedLineMutation) OldCurrency(ctx context.Context) (v currencyx.Code, err error) { +func (m *BillingStandardInvoiceDetailedLineMutation) OldCurrency(ctx context.Context) (v *currencyx.Code, err error) { if !m.op.Is(OpUpdateOne) { return v, errors.New("OldCurrency is only allowed on UpdateOne operations") } @@ -33236,9 +33236,22 @@ func (m *BillingStandardInvoiceDetailedLineMutation) OldCurrency(ctx context.Con return oldValue.Currency, nil } +// ClearCurrency clears the value of the "currency" field. +func (m *BillingStandardInvoiceDetailedLineMutation) ClearCurrency() { + m.currency = nil + m.clearedFields[billingstandardinvoicedetailedline.FieldCurrency] = struct{}{} +} + +// CurrencyCleared returns if the "currency" field was cleared in this mutation. +func (m *BillingStandardInvoiceDetailedLineMutation) CurrencyCleared() bool { + _, ok := m.clearedFields[billingstandardinvoicedetailedline.FieldCurrency] + return ok +} + // ResetCurrency resets all changes to the "currency" field. func (m *BillingStandardInvoiceDetailedLineMutation) ResetCurrency() { m.currency = nil + delete(m.clearedFields, billingstandardinvoicedetailedline.FieldCurrency) } // SetServicePeriodStart sets the "service_period_start" field. @@ -35007,6 +35020,9 @@ func (m *BillingStandardInvoiceDetailedLineMutation) AddField(name string, value // mutation. func (m *BillingStandardInvoiceDetailedLineMutation) ClearedFields() []string { var fields []string + if m.FieldCleared(billingstandardinvoicedetailedline.FieldCurrency) { + fields = append(fields, billingstandardinvoicedetailedline.FieldCurrency) + } if m.FieldCleared(billingstandardinvoicedetailedline.FieldInvoicingAppExternalID) { fields = append(fields, billingstandardinvoicedetailedline.FieldInvoicingAppExternalID) } @@ -35042,6 +35058,9 @@ func (m *BillingStandardInvoiceDetailedLineMutation) FieldCleared(name string) b // error if the field is not defined in the schema. func (m *BillingStandardInvoiceDetailedLineMutation) ClearField(name string) error { switch name { + case billingstandardinvoicedetailedline.FieldCurrency: + m.ClearCurrency() + return nil case billingstandardinvoicedetailedline.FieldInvoicingAppExternalID: m.ClearInvoicingAppExternalID() return nil @@ -54152,7 +54171,7 @@ func (m *ChargeFlatFeeRunDetailedLineMutation) Currency() (r currencyx.Code, exi // OldCurrency returns the old "currency" field's value of the ChargeFlatFeeRunDetailedLine entity. // If the ChargeFlatFeeRunDetailedLine 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 *ChargeFlatFeeRunDetailedLineMutation) OldCurrency(ctx context.Context) (v currencyx.Code, err error) { +func (m *ChargeFlatFeeRunDetailedLineMutation) OldCurrency(ctx context.Context) (v *currencyx.Code, err error) { if !m.op.Is(OpUpdateOne) { return v, errors.New("OldCurrency is only allowed on UpdateOne operations") } @@ -54166,9 +54185,22 @@ func (m *ChargeFlatFeeRunDetailedLineMutation) OldCurrency(ctx context.Context) return oldValue.Currency, nil } +// ClearCurrency clears the value of the "currency" field. +func (m *ChargeFlatFeeRunDetailedLineMutation) ClearCurrency() { + m.currency = nil + m.clearedFields[chargeflatfeerundetailedline.FieldCurrency] = struct{}{} +} + +// CurrencyCleared returns if the "currency" field was cleared in this mutation. +func (m *ChargeFlatFeeRunDetailedLineMutation) CurrencyCleared() bool { + _, ok := m.clearedFields[chargeflatfeerundetailedline.FieldCurrency] + return ok +} + // ResetCurrency resets all changes to the "currency" field. func (m *ChargeFlatFeeRunDetailedLineMutation) ResetCurrency() { m.currency = nil + delete(m.clearedFields, chargeflatfeerundetailedline.FieldCurrency) } // SetServicePeriodStart sets the "service_period_start" field. @@ -55830,6 +55862,9 @@ func (m *ChargeFlatFeeRunDetailedLineMutation) AddField(name string, value ent.V // mutation. func (m *ChargeFlatFeeRunDetailedLineMutation) ClearedFields() []string { var fields []string + if m.FieldCleared(chargeflatfeerundetailedline.FieldCurrency) { + fields = append(fields, chargeflatfeerundetailedline.FieldCurrency) + } if m.FieldCleared(chargeflatfeerundetailedline.FieldInvoicingAppExternalID) { fields = append(fields, chargeflatfeerundetailedline.FieldInvoicingAppExternalID) } @@ -55865,6 +55900,9 @@ func (m *ChargeFlatFeeRunDetailedLineMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *ChargeFlatFeeRunDetailedLineMutation) ClearField(name string) error { switch name { + case chargeflatfeerundetailedline.FieldCurrency: + m.ClearCurrency() + return nil case chargeflatfeerundetailedline.FieldInvoicingAppExternalID: m.ClearInvoicingAppExternalID() return nil @@ -65067,7 +65105,7 @@ func (m *ChargeUsageBasedRunDetailedLineMutation) Currency() (r currencyx.Code, // OldCurrency returns the old "currency" field's value of the ChargeUsageBasedRunDetailedLine entity. // If the ChargeUsageBasedRunDetailedLine 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 *ChargeUsageBasedRunDetailedLineMutation) OldCurrency(ctx context.Context) (v currencyx.Code, err error) { +func (m *ChargeUsageBasedRunDetailedLineMutation) OldCurrency(ctx context.Context) (v *currencyx.Code, err error) { if !m.op.Is(OpUpdateOne) { return v, errors.New("OldCurrency is only allowed on UpdateOne operations") } @@ -65081,9 +65119,22 @@ func (m *ChargeUsageBasedRunDetailedLineMutation) OldCurrency(ctx context.Contex return oldValue.Currency, nil } +// ClearCurrency clears the value of the "currency" field. +func (m *ChargeUsageBasedRunDetailedLineMutation) ClearCurrency() { + m.currency = nil + m.clearedFields[chargeusagebasedrundetailedline.FieldCurrency] = struct{}{} +} + +// CurrencyCleared returns if the "currency" field was cleared in this mutation. +func (m *ChargeUsageBasedRunDetailedLineMutation) CurrencyCleared() bool { + _, ok := m.clearedFields[chargeusagebasedrundetailedline.FieldCurrency] + return ok +} + // ResetCurrency resets all changes to the "currency" field. func (m *ChargeUsageBasedRunDetailedLineMutation) ResetCurrency() { m.currency = nil + delete(m.clearedFields, chargeusagebasedrundetailedline.FieldCurrency) } // SetServicePeriodStart sets the "service_period_start" field. @@ -66912,6 +66963,9 @@ func (m *ChargeUsageBasedRunDetailedLineMutation) AddField(name string, value en // mutation. func (m *ChargeUsageBasedRunDetailedLineMutation) ClearedFields() []string { var fields []string + if m.FieldCleared(chargeusagebasedrundetailedline.FieldCurrency) { + fields = append(fields, chargeusagebasedrundetailedline.FieldCurrency) + } if m.FieldCleared(chargeusagebasedrundetailedline.FieldInvoicingAppExternalID) { fields = append(fields, chargeusagebasedrundetailedline.FieldInvoicingAppExternalID) } @@ -66950,6 +67004,9 @@ func (m *ChargeUsageBasedRunDetailedLineMutation) FieldCleared(name string) bool // error if the field is not defined in the schema. func (m *ChargeUsageBasedRunDetailedLineMutation) ClearField(name string) error { switch name { + case chargeusagebasedrundetailedline.FieldCurrency: + m.ClearCurrency() + return nil case chargeusagebasedrundetailedline.FieldInvoicingAppExternalID: m.ClearInvoicingAppExternalID() return nil diff --git a/openmeter/ent/db/runtime.go b/openmeter/ent/db/runtime.go index c61d64613b..d5eee985f0 100644 --- a/openmeter/ent/db/runtime.go +++ b/openmeter/ent/db/runtime.go @@ -869,10 +869,6 @@ func init() { _ = billingstandardinvoicedetailedlineMixinFields0 billingstandardinvoicedetailedlineFields := schema.BillingStandardInvoiceDetailedLine{}.Fields() _ = billingstandardinvoicedetailedlineFields - // billingstandardinvoicedetailedlineDescCurrency is the schema descriptor for currency field. - billingstandardinvoicedetailedlineDescCurrency := billingstandardinvoicedetailedlineMixinFields0[0].Descriptor() - // billingstandardinvoicedetailedline.CurrencyValidator is a validator for the "currency" field. It is called by the builders before save. - billingstandardinvoicedetailedline.CurrencyValidator = billingstandardinvoicedetailedlineDescCurrency.Validators[0].(func(string) error) // billingstandardinvoicedetailedlineDescChildUniqueReferenceID is the schema descriptor for child_unique_reference_id field. billingstandardinvoicedetailedlineDescChildUniqueReferenceID := billingstandardinvoicedetailedlineMixinFields0[5].Descriptor() // billingstandardinvoicedetailedline.ChildUniqueReferenceIDValidator is a validator for the "child_unique_reference_id" field. It is called by the builders before save. @@ -1282,10 +1278,6 @@ func init() { _ = chargeflatfeerundetailedlineMixinFields0 chargeflatfeerundetailedlineFields := schema.ChargeFlatFeeRunDetailedLine{}.Fields() _ = chargeflatfeerundetailedlineFields - // chargeflatfeerundetailedlineDescCurrency is the schema descriptor for currency field. - chargeflatfeerundetailedlineDescCurrency := chargeflatfeerundetailedlineMixinFields0[0].Descriptor() - // chargeflatfeerundetailedline.CurrencyValidator is a validator for the "currency" field. It is called by the builders before save. - chargeflatfeerundetailedline.CurrencyValidator = chargeflatfeerundetailedlineDescCurrency.Validators[0].(func(string) error) // chargeflatfeerundetailedlineDescChildUniqueReferenceID is the schema descriptor for child_unique_reference_id field. chargeflatfeerundetailedlineDescChildUniqueReferenceID := chargeflatfeerundetailedlineMixinFields0[5].Descriptor() // chargeflatfeerundetailedline.ChildUniqueReferenceIDValidator is a validator for the "child_unique_reference_id" field. It is called by the builders before save. @@ -1501,10 +1493,6 @@ func init() { _ = chargeusagebasedrundetailedlineMixinFields0 chargeusagebasedrundetailedlineFields := schema.ChargeUsageBasedRunDetailedLine{}.Fields() _ = chargeusagebasedrundetailedlineFields - // chargeusagebasedrundetailedlineDescCurrency is the schema descriptor for currency field. - chargeusagebasedrundetailedlineDescCurrency := chargeusagebasedrundetailedlineMixinFields0[0].Descriptor() - // chargeusagebasedrundetailedline.CurrencyValidator is a validator for the "currency" field. It is called by the builders before save. - chargeusagebasedrundetailedline.CurrencyValidator = chargeusagebasedrundetailedlineDescCurrency.Validators[0].(func(string) error) // chargeusagebasedrundetailedlineDescChildUniqueReferenceID is the schema descriptor for child_unique_reference_id field. chargeusagebasedrundetailedlineDescChildUniqueReferenceID := chargeusagebasedrundetailedlineMixinFields0[5].Descriptor() // chargeusagebasedrundetailedline.ChildUniqueReferenceIDValidator is a validator for the "child_unique_reference_id" field. It is called by the builders before save. diff --git a/test/billing/adapter_test.go b/test/billing/adapter_test.go index d2b3dbf5a6..2babf8c5c0 100644 --- a/test/billing/adapter_test.go +++ b/test/billing/adapter_test.go @@ -137,7 +137,6 @@ func newDetailedLine(in newLineInput) billing.DetailedLine { Namespace: in.Namespace, Name: in.Name, }), - Currency: in.Invoice.Currency, ServicePeriod: in.Period, ChildUniqueReferenceID: in.ChildUniqueReferenceID, PerUnitAmount: alpacadecimal.NewFromFloat(100), diff --git a/tools/migrate/migrations/20260721140844_deprecate_detailed_line_currency.down.sql b/tools/migrate/migrations/20260721140844_deprecate_detailed_line_currency.down.sql new file mode 100644 index 0000000000..9150e3e9b6 --- /dev/null +++ b/tools/migrate/migrations/20260721140844_deprecate_detailed_line_currency.down.sql @@ -0,0 +1,6 @@ +-- reverse: modify "charge_usage_based_run_detailed_line" table +ALTER TABLE "charge_usage_based_run_detailed_line" ALTER COLUMN "currency" SET NOT NULL; +-- reverse: modify "charge_flat_fee_run_detailed_lines" table +ALTER TABLE "charge_flat_fee_run_detailed_lines" ALTER COLUMN "currency" SET NOT NULL; +-- reverse: modify "billing_standard_invoice_detailed_lines" table +ALTER TABLE "billing_standard_invoice_detailed_lines" ALTER COLUMN "currency" SET NOT NULL; diff --git a/tools/migrate/migrations/20260721140844_deprecate_detailed_line_currency.up.sql b/tools/migrate/migrations/20260721140844_deprecate_detailed_line_currency.up.sql new file mode 100644 index 0000000000..6e55e917e5 --- /dev/null +++ b/tools/migrate/migrations/20260721140844_deprecate_detailed_line_currency.up.sql @@ -0,0 +1,6 @@ +-- modify "billing_standard_invoice_detailed_lines" table +ALTER TABLE "billing_standard_invoice_detailed_lines" ALTER COLUMN "currency" DROP NOT NULL; +-- modify "charge_flat_fee_run_detailed_lines" table +ALTER TABLE "charge_flat_fee_run_detailed_lines" ALTER COLUMN "currency" DROP NOT NULL; +-- modify "charge_usage_based_run_detailed_line" table +ALTER TABLE "charge_usage_based_run_detailed_line" ALTER COLUMN "currency" DROP NOT NULL; diff --git a/tools/migrate/migrations/atlas.sum b/tools/migrate/migrations/atlas.sum index 982501c0ae..d17e18fb55 100644 --- a/tools/migrate/migrations/atlas.sum +++ b/tools/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:f00I9PeE3w9+bBTdFVHzvUpjrFB+Wy4/OTrh7nAtjbk= +h1:zJM4ULiLNtWs2vWL7Mg2ppmFvdYazvpeGwgnfM2t2dM= 20240826120919_init.up.sql h1:tc1V91/smlmaeJGQ8h+MzTEeFjjnrrFDbDAjOYJK91o= 20240903155435_entitlement-expired-index.up.sql h1:Hp8u5uckmLXc1cRvWU0AtVnnK8ShlpzZNp8pbiJLhac= 20240917172257_billing-entities.up.sql h1:Q1dAMo0Vjiit76OybClNfYPGC5nmvov2/M2W1ioi4Kw= @@ -240,3 +240,4 @@ h1:f00I9PeE3w9+bBTdFVHzvUpjrFB+Wy4/OTrh7nAtjbk= 20260717060208_add_bulk_invoice_schema_level_2_migration_function.up.sql h1:kJCvXyNuRC9FMysGg9c6nesUXsfbkNq/RKfTIwnQzEw= 20260717160017_custom_currencies.up.sql h1:HHScXLfjVxumxzNrw/RdxZZZMo3EnCE5Min67fEX9jo= 20260720093016_charges-custom-currencies.up.sql h1:U9K4UDDHwNCCqcAQ3n9HFTXaibsm66KPebSa/7XB2OE= +20260721140844_deprecate_detailed_line_currency.up.sql h1:wG5tChhpzGEfeXdAVvpYmLzxKnmqPcACAYvcSdH9sJ0= From e14e4d26a8b2cb9802c0437d73df8feab176d319 Mon Sep 17 00:00:00 2001 From: Gergely Tamas Kurucz Date: Tue, 21 Jul 2026 18:13:01 +0200 Subject: [PATCH 2/2] style: satisfy linter --- openmeter/billing/charges/service/creditpurchase_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/openmeter/billing/charges/service/creditpurchase_test.go b/openmeter/billing/charges/service/creditpurchase_test.go index 83223a650c..a647e0e63a 100644 --- a/openmeter/billing/charges/service/creditpurchase_test.go +++ b/openmeter/billing/charges/service/creditpurchase_test.go @@ -119,7 +119,6 @@ func (s *CreditPurchaseTestSuite) TestPromotionalCreditPurchaseWithCustomCurrenc s.NotEmpty(cust.ID) customerID = cust.ID customCurrency = s.createTestCustomCurrency(ctx, ns) - }) s.Run("#2 create promotional credit purchase", func() {