From ba556a2ce225082eae28b4dd5dabc62ba2a84ba4 Mon Sep 17 00:00:00 2001 From: Peter Turi Date: Thu, 23 Jul 2026 09:17:55 +0200 Subject: [PATCH] feat(customcurrencies): support credit purchases --- .agents/skills/charges/SKILL.md | 3 + .../billing/charges/creditpurchase/charge.go | 5 +- .../charges/creditpurchase/service/create.go | 7 +- .../creditpurchase/service/external_test.go | 2 +- .../charges/creditpurchase/service/service.go | 23 ++- .../charges/creditpurchase/settlement.go | 2 +- .../charges/creditpurchase/settlement_test.go | 37 +++- .../billing/charges/flatfee/service/create.go | 4 +- .../charges/service/creditpurchase_test.go | 165 ++++++++++++++++-- .../charges/service/flatfee_costbasis_test.go | 14 +- .../charges/service/invoicable_test.go | 8 + .../billing/charges/service/taxcode_test.go | 4 +- .../service/usagebased_costbasis_test.go | 13 +- .../charges/usagebased/service/create.go | 4 +- .../billing/creditgrant/service/service.go | 4 +- .../chargeadapter/creditpurchase_test.go | 2 +- .../ledger/customerbalance/service_test.go | 2 +- .../ledger/customerbalance/testenv_test.go | 2 +- test/app/stripe/invoice_credits_test.go | 2 +- test/credits/credit_then_invoice_test.go | 2 +- test/credits/sanity_lifecycle_test.go | 7 +- test/credits/sanity_test.go | 14 +- 22 files changed, 256 insertions(+), 70 deletions(-) diff --git a/.agents/skills/charges/SKILL.md b/.agents/skills/charges/SKILL.md index b91fe41e23..f3ae16e94c 100644 --- a/.agents/skills/charges/SKILL.md +++ b/.agents/skills/charges/SKILL.md @@ -642,6 +642,7 @@ Use these conventions for lifecycle tests: - for credit-only charges (usage-based or flat fee), handler callbacks must not return credit allocations above the requested amount; exact allocation paths must return allocations that sum to the requested amount - for flat fee credit-only tests, use `mustAdvanceFlatFeeCharges(...)` helper — it filters the advance result to flat fee charges only - for credit-purchase state-machine unit tests, use testify `mock.Mock` with `On(...).Run(...).Return(...).Once()` for expected handler callbacks so missing or unexpected calls fail; in service-suite tests, leave callbacks unset when validating that a flow fails before callbacks, because the shared `CreditPurchaseTestHandler` already errors if an unset callback is invoked +- keep custom-currency support disabled on default charge service instances so tests continue covering the production unsupported boundary; when a test needs it enabled, use a semantic suite helper that constructs and enables the service instead of repeating `SetEnableCustomCurrency` interface assertions at call sites - when testing timestamp truncation, use sub-second fixtures and assert the persisted charge/run fields are second-aligned after create/advance - `time.Time` fields on domain models are value typed; use `s.False(ts.IsZero())` instead of `s.NotNil(ts)` when asserting they are populated - cover the temporary shrink/extend remap path as well; it synthesizes new intents and must normalize the replacement period ends before re-create @@ -716,6 +717,8 @@ Credit purchase handler interface (`creditpurchase.Handler`): When changing credit purchase charges: - `creditpurchase.ChargeBase` stores base-row data: `ManagedResource`, `Intent`, `Status` (own `creditpurchase.Status` type); `State` exists but is an empty struct +- `creditpurchase.Intent.Currency` is the currency being purchased. For external and invoice settlements, `GenericSettlement.Currency` is the fiat settlement currency (`currencyx.FiatCode`), so it intentionally differs from the intent currency when purchasing a custom currency. +- Custom-currency gathering lines must use the settlement fiat currency and convert the purchased credit amount using the settlement cost basis. Keep the purchased amount and currency visible in the line description so downstream billing retains both sides of the purchase. - `creditpurchase.Charge` embeds `ChargeBase` + `Realizations` — all lifecycle outcomes live in `Realizations`, not `State` - `creditpurchase.Realizations` holds `CreditGrantRealization`, `ExternalPaymentSettlement`, and `InvoiceSettlement` (all loaded from edge tables) - `CreditGrantRealization` is stored in its own `charge_credit_purchase_credit_grants` table, not on the base row diff --git a/openmeter/billing/charges/creditpurchase/charge.go b/openmeter/billing/charges/creditpurchase/charge.go index fc3c810def..77c221943a 100644 --- a/openmeter/billing/charges/creditpurchase/charge.go +++ b/openmeter/billing/charges/creditpurchase/charge.go @@ -14,6 +14,7 @@ import ( "github.com/openmeterio/openmeter/openmeter/currencies" "github.com/openmeterio/openmeter/openmeter/customer" "github.com/openmeterio/openmeter/pkg/clock" + "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/timeutil" ) @@ -225,12 +226,12 @@ func (i Intent) Validate() error { switch i.Settlement.Type() { case SettlementTypeInvoice: settlement, err := i.Settlement.AsInvoiceSettlement() - if err == nil && settlement.Currency != i.Currency.GetCode() { + if err == nil && !i.Currency.IsCustom() && currencyx.Code(settlement.Currency) != i.Currency.GetCode() { errs = append(errs, fmt.Errorf("settlement currency %q must match credit currency %q", settlement.Currency, i.Currency.GetCode())) } case SettlementTypeExternal: settlement, err := i.Settlement.AsExternalSettlement() - if err == nil && settlement.Currency != i.Currency.GetCode() { + if err == nil && !i.Currency.IsCustom() && currencyx.Code(settlement.Currency) != i.Currency.GetCode() { errs = append(errs, fmt.Errorf("settlement currency %q must match credit currency %q", settlement.Currency, i.Currency.GetCode())) } } diff --git a/openmeter/billing/charges/creditpurchase/service/create.go b/openmeter/billing/charges/creditpurchase/service/create.go index c83c48d590..aa3770360a 100644 --- a/openmeter/billing/charges/creditpurchase/service/create.go +++ b/openmeter/billing/charges/creditpurchase/service/create.go @@ -10,7 +10,6 @@ import ( "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase" "github.com/openmeterio/openmeter/openmeter/billing/charges/meta" "github.com/openmeterio/openmeter/openmeter/productcatalog" - "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/framework/transaction" "github.com/openmeterio/openmeter/pkg/models" ) @@ -21,7 +20,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.IsCustom() && input.Intent.Settlement.Type() != creditpurchase.SettlementTypePromotional { + if input.Intent.Currency.IsCustom() && !s.enableCustomCurrency.Load() { return creditpurchase.ChargeWithGatheringLine{}, fmt.Errorf("custom currency %s is not supported for credit purchases: %w", input.Intent.Currency.GetCode(), meta.ErrCustomCurrencyNotSupported) } @@ -95,7 +94,7 @@ func (s *service) buildInvoiceCreditPurchaseGatheringLine(charge creditpurchase. // Total cost = credit amount * cost basis (e.g., 100 credits * $0.5 = $50) totalCost := intent.CreditAmount.Mul(invoiceSettlement.CostBasis) - invoiceCurrency := currencyx.FiatCode(invoiceSettlement.Currency) + invoiceCurrency := invoiceSettlement.Currency calc, err := invoiceCurrency.AsFiatCurrency() if err != nil { return billing.GatheringLine{}, fmt.Errorf("creating currency calculator: %w", err) @@ -119,7 +118,7 @@ func (s *service) buildInvoiceCreditPurchaseGatheringLine(charge creditpurchase. GatheringLineBase: billing.GatheringLineBase{ ManagedResource: models.NewManagedResource(models.ManagedResourceInput{ Namespace: charge.Namespace, - Name: intent.Name, + Name: fmt.Sprintf("%s (%s %s credits)", intent.Name, intent.CreditAmount, intent.Currency.GetCode()), Description: intent.Description, }), Metadata: intent.Metadata.Clone(), diff --git a/openmeter/billing/charges/creditpurchase/service/external_test.go b/openmeter/billing/charges/creditpurchase/service/external_test.go index 89a05989b5..061b79a636 100644 --- a/openmeter/billing/charges/creditpurchase/service/external_test.go +++ b/openmeter/billing/charges/creditpurchase/service/external_test.go @@ -623,7 +623,7 @@ func newExternalStateMachineTestChargeWithInput(t *testing.T, input externalStat FeatureFilters: input.featureFilters, Settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: currencyx.Code("USD"), + Currency: currencyx.FiatCode("USD"), CostBasis: input.costBasis, }, InitialStatus: input.initialStatus, diff --git a/openmeter/billing/charges/creditpurchase/service/service.go b/openmeter/billing/charges/creditpurchase/service/service.go index 5bbbbf63a7..39bb6f5537 100644 --- a/openmeter/billing/charges/creditpurchase/service/service.go +++ b/openmeter/billing/charges/creditpurchase/service/service.go @@ -3,6 +3,8 @@ package service import ( "errors" "fmt" + "sync/atomic" + "testing" "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase" creditpurchaserealizations "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase/service/realizations" @@ -63,9 +65,20 @@ func New(config Config) (creditpurchase.Service, error) { } type service struct { - adapter creditpurchase.Adapter - metaAdapter meta.Adapter - handler creditpurchase.Handler - lineage lineage.Service - realizations *creditpurchaserealizations.Service + adapter creditpurchase.Adapter + metaAdapter meta.Adapter + handler creditpurchase.Handler + lineage lineage.Service + realizations *creditpurchaserealizations.Service + enableCustomCurrency atomic.Bool +} + +func (s *service) SetEnableCustomCurrency(t *testing.T, enabled bool) error { + if t == nil { + return errors.New("testing is nil") + } + + t.Helper() + s.enableCustomCurrency.Store(enabled) + return nil } diff --git a/openmeter/billing/charges/creditpurchase/settlement.go b/openmeter/billing/charges/creditpurchase/settlement.go index 8545c4f415..aa537ccba9 100644 --- a/openmeter/billing/charges/creditpurchase/settlement.go +++ b/openmeter/billing/charges/creditpurchase/settlement.go @@ -36,7 +36,7 @@ func (s SettlementType) Values() []string { } type GenericSettlement struct { - Currency currencyx.Code `json:"currency"` + Currency currencyx.FiatCode `json:"currency"` CostBasis alpacadecimal.Decimal `json:"costBasis"` } diff --git a/openmeter/billing/charges/creditpurchase/settlement_test.go b/openmeter/billing/charges/creditpurchase/settlement_test.go index c7385b4b30..10620898b5 100644 --- a/openmeter/billing/charges/creditpurchase/settlement_test.go +++ b/openmeter/billing/charges/creditpurchase/settlement_test.go @@ -1,6 +1,7 @@ package creditpurchase import ( + "encoding/json" "testing" "github.com/alpacahq/alpacadecimal" @@ -33,7 +34,7 @@ func TestGenericSettlementValidateRequiresPositiveCostBasis(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { settlement := GenericSettlement{ - Currency: currencyx.Code("USD"), + Currency: currencyx.FiatCode("USD"), CostBasis: tc.costBasis, } @@ -50,3 +51,37 @@ func TestGenericSettlementValidateRequiresPositiveCostBasis(t *testing.T) { }) } } + +func TestGenericSettlementRejectsCustomCurrencyCode(t *testing.T) { + settlement := GenericSettlement{ + Currency: currencyx.FiatCode("TOKENS"), + CostBasis: alpacadecimal.NewFromFloat(0.5), + } + + err := settlement.Validate() + + require.Error(t, err) + require.ErrorContains(t, err, "invalid fiat currency code: TOKENS") + require.True(t, models.IsGenericValidationError(err)) +} + +func TestSettlementJSONRoundTripPreservesFiatCurrencyCode(t *testing.T) { + settlement := NewSettlement(InvoiceSettlement{ + GenericSettlement: GenericSettlement{ + Currency: currencyx.FiatCode("USD"), + CostBasis: alpacadecimal.NewFromFloat(0.5), + }, + }) + + data, err := json.Marshal(settlement) + require.NoError(t, err) + require.JSONEq(t, `{"type":"invoice","currency":"USD","costBasis":"0.5"}`, string(data)) + + var decoded Settlement + require.NoError(t, json.Unmarshal(data, &decoded)) + + invoiceSettlement, err := decoded.AsInvoiceSettlement() + require.NoError(t, err) + require.Equal(t, currencyx.FiatCode("USD"), invoiceSettlement.Currency) + require.Equal(t, float64(0.5), invoiceSettlement.CostBasis.InexactFloat64()) +} diff --git a/openmeter/billing/charges/flatfee/service/create.go b/openmeter/billing/charges/flatfee/service/create.go index 087f5e0c06..8349926449 100644 --- a/openmeter/billing/charges/flatfee/service/create.go +++ b/openmeter/billing/charges/flatfee/service/create.go @@ -32,9 +32,7 @@ func (s *service) Create(ctx context.Context, input flatfee.CreateInput) ([]flat now := clock.Now().UTC() // 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() && - intent.SettlementMode == productcatalog.CreditThenInvoiceSettlementMode && - !s.enableCustomCurrency.Load() { + if intent.Currency.IsCustom() && !s.enableCustomCurrency.Load() { 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/creditpurchase_test.go b/openmeter/billing/charges/service/creditpurchase_test.go index 901697b5a5..da2e64dd8b 100644 --- a/openmeter/billing/charges/service/creditpurchase_test.go +++ b/openmeter/billing/charges/service/creditpurchase_test.go @@ -20,11 +20,13 @@ import ( "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/lineage" "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/openmeter/productcatalog" "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/datetime" @@ -49,6 +51,24 @@ func (s *CreditPurchaseTestSuite) TearDownTest() { s.BaseSuite.TearDownTest() } +func (s *CreditPurchaseTestSuite) getCustomCurrenciesEnabledCreditPurchaseService(lineageService lineage.Service) creditpurchase.Service { + s.T().Helper() + + creditPurchaseService, err := creditpurchaseservice.New(creditpurchaseservice.Config{ + Adapter: s.CreditPurchaseAdapter, + Handler: s.CreditPurchaseTestHandler, + Lineage: lineageService, + MetaAdapter: s.MetaAdapter, + }) + s.Require().NoError(err) + + customCurrencyEnabler, ok := creditPurchaseService.(customCurrencyEnabler) + s.Require().True(ok) + s.Require().NoError(customCurrencyEnabler.SetEnableCustomCurrency(s.T(), true)) + + return creditPurchaseService +} + func (s *CreditPurchaseTestSuite) TestPromotionalCreditPurchase() { ctx := context.Background() ns := s.GetUniqueNamespace("charges-service-promotional-credit-purchase") @@ -107,13 +127,14 @@ func (s *CreditPurchaseTestSuite) TestPromotionalCreditPurchaseWithCustomCurrenc var customCurrency currencies.Currency var customerID string + var defaultCreditGrantTaxCodeID 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) + defaultCreditGrantTaxCodeID = s.ProvisionDefaultTaxCodes(ctx, ns).CreditGrantTaxCodeID cust := s.CreateTestCustomer(ns, "test-subject") s.NotEmpty(cust.ID) @@ -133,11 +154,14 @@ func (s *CreditPurchaseTestSuite) TestPromotionalCreditPurchaseWithCustomCurrenc 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{ + creditPurchaseIntent := creditpurchase.Intent{ Intent: meta.Intent{ ManagedBy: billing.ManuallyManagedLine, CustomerID: customerID, Currency: customCurrency, + TaxConfig: productcatalog.TaxCodeConfig{ + TaxCodeID: defaultCreditGrantTaxCodeID, + }, }, IntentMutableFields: creditpurchase.IntentMutableFields{ IntentMutableFields: meta.IntentMutableFields{ @@ -149,7 +173,8 @@ func (s *CreditPurchaseTestSuite) TestPromotionalCreditPurchaseWithCustomCurrenc CreditAmount: alpacadecimal.NewFromFloat(100.1234), Settlement: creditpurchase.NewSettlement(creditpurchase.PromotionalSettlement{}), }, - }) + } + intent := charges.NewChargeIntent(creditPurchaseIntent) promotionalCallback := newCountedLedgerTransactionCallback[creditpurchase.Charge]() promotionalTransactionGroupID = promotionalCallback.id @@ -165,13 +190,21 @@ func (s *CreditPurchaseTestSuite) TestPromotionalCreditPurchaseWithCustomCurrenc Return(nil). Once() - customCurrencyCreditPurchaseService, err := creditpurchaseservice.New(creditpurchaseservice.Config{ + creditPurchaseService, err := creditpurchaseservice.New(creditpurchaseservice.Config{ Adapter: s.CreditPurchaseAdapter, Handler: s.CreditPurchaseTestHandler, Lineage: lineageMock, MetaAdapter: s.MetaAdapter, }) s.Require().NoError(err) + _, err = creditPurchaseService.Create(ctx, creditpurchase.CreateInput{ + Namespace: ns, + Intent: creditPurchaseIntent, + }) + s.Require().ErrorIs(err, meta.ErrCustomCurrencyNotSupported) + + customCurrencyCreditPurchaseService := s.getCustomCurrenciesEnabledCreditPurchaseService(lineageMock) + originalCreditPurchaseService := s.Charges.creditPurchaseService s.Charges.creditPurchaseService = customCurrencyCreditPurchaseService defer func() { @@ -212,6 +245,110 @@ func (s *CreditPurchaseTestSuite) TestPromotionalCreditPurchaseWithCustomCurrenc }) } +func (s *CreditPurchaseTestSuite) TestInvoiceCreditPurchaseWithCustomCurrency() { + ctx := s.T().Context() + ns := s.GetUniqueNamespace("charges-service-invoice-credit-purchase-custom-currency") + defaultTaxCodes := s.ProvisionDefaultTaxCodes(ctx, ns) + + cust := s.CreateTestCustomer(ns, "test-subject") + s.NotEmpty(cust.ID) + customCurrency := s.createTestCustomCurrency(ctx, ns) + + 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 := creditpurchase.Intent{ + Intent: meta.Intent{ + ManagedBy: billing.ManuallyManagedLine, + CustomerID: cust.ID, + Currency: customCurrency, + TaxConfig: productcatalog.TaxCodeConfig{ + TaxCodeID: defaultTaxCodes.CreditGrantTaxCodeID, + }, + }, + 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.InvoiceSettlement{ + GenericSettlement: creditpurchase.GenericSettlement{ + Currency: currencyx.FiatCode(currency.USD), + CostBasis: alpacadecimal.NewFromFloat(0.5), + }, + }), + }, + } + + creditPurchaseService, err := creditpurchaseservice.New(creditpurchaseservice.Config{ + Adapter: s.CreditPurchaseAdapter, + Handler: s.CreditPurchaseTestHandler, + Lineage: s.LineageService, + MetaAdapter: s.MetaAdapter, + }) + s.Require().NoError(err) + + s.Run("disabled by default", func() { + // given: + // - a custom-currency invoice credit purchase and the default service configuration + // when: + // - the credit-purchase service creates the charge + // then: + // - the service preserves the production unsupported boundary + _, err := creditPurchaseService.Create(ctx, creditpurchase.CreateInput{ + Namespace: ns, + Intent: intent, + }) + s.ErrorIs(err, meta.ErrCustomCurrencyNotSupported) + }) + + s.Run("enabled for tests", func() { + // given: + // - the test-only custom-currency gate is enabled + // when: + // - the invoice credit purchase is created + // then: + // - the charge keeps the purchased currency while the gathering line uses fiat settlement + customCurrencyCreditPurchaseService := s.getCustomCurrenciesEnabledCreditPurchaseService(s.LineageService) + + created, err := customCurrencyCreditPurchaseService.Create(ctx, creditpurchase.CreateInput{ + Namespace: ns, + Intent: intent, + }) + s.Require().NoError(err) + + s.True(created.Charge.Intent.Currency.IsCustom()) + s.Equal(customCurrency.ID, created.Charge.Intent.Currency.ID) + s.Equal(float64(100.123), created.Charge.Intent.CreditAmount.InexactFloat64()) + + s.Require().NotNil(created.GatheringLineToCreate) + gatheringLine := *created.GatheringLineToCreate + s.Equal(currencyx.FiatCode(currency.USD), gatheringLine.Currency) + s.Equal("Custom Currency Credit Purchase (100.123 TOKENS credits)", gatheringLine.Name) + + flatPrice, err := gatheringLine.Price.AsFlat() + s.Require().NoError(err) + s.Equal(float64(50.06), flatPrice.Amount.InexactFloat64()) + + persisted, err := customCurrencyCreditPurchaseService.GetByIDs(ctx, creditpurchase.GetByIDsInput{ + Namespace: ns, + IDs: []string{created.Charge.ID}, + }) + s.Require().NoError(err) + s.Require().Len(persisted, 1) + s.True(persisted[0].Intent.Currency.IsCustom()) + s.Equal(customCurrency.ID, persisted[0].Intent.Currency.ID) + + settlement, err := persisted[0].Intent.Settlement.AsInvoiceSettlement() + s.Require().NoError(err) + s.Equal(currencyx.FiatCode(currency.USD), settlement.Currency) + }) +} + func (s *CreditPurchaseTestSuite) TestCreditPurchaseRejectsMismatchedSettlementCurrency() { ctx := context.Background() ns := s.GetUniqueNamespace("charges-service-credit-purchase-mismatched-settlement-currency") @@ -234,7 +371,7 @@ func (s *CreditPurchaseTestSuite) TestCreditPurchaseRejectsMismatchedSettlementC settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, GenericSettlement: creditpurchase.GenericSettlement{ - Currency: currencyx.Code(currency.EUR), + Currency: currencyx.FiatCode(currency.EUR), CostBasis: alpacadecimal.NewFromFloat(0.5), }, }), @@ -243,7 +380,7 @@ func (s *CreditPurchaseTestSuite) TestCreditPurchaseRejectsMismatchedSettlementC name: "invoice", settlement: creditpurchase.NewSettlement(creditpurchase.InvoiceSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: currencyx.Code(currency.EUR), + Currency: currencyx.FiatCode(currency.EUR), CostBasis: alpacadecimal.NewFromFloat(0.5), }, }), @@ -293,7 +430,7 @@ func (s *CreditPurchaseTestSuite) TestCreditPurchaseRejectsNonPositiveSettlement settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: alpacadecimal.Zero, }, }), @@ -303,7 +440,7 @@ func (s *CreditPurchaseTestSuite) TestCreditPurchaseRejectsNonPositiveSettlement settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: alpacadecimal.NewFromFloat(-0.5), }, }), @@ -312,7 +449,7 @@ func (s *CreditPurchaseTestSuite) TestCreditPurchaseRejectsNonPositiveSettlement name: "invoice zero", settlement: creditpurchase.NewSettlement(creditpurchase.InvoiceSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: alpacadecimal.Zero, }, }), @@ -321,7 +458,7 @@ func (s *CreditPurchaseTestSuite) TestCreditPurchaseRejectsNonPositiveSettlement name: "invoice negative", settlement: creditpurchase.NewSettlement(creditpurchase.InvoiceSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: alpacadecimal.NewFromFloat(-0.5), }, }), @@ -451,7 +588,7 @@ func (s *CreditPurchaseTestSuite) TestExternalAuthorizedCreditPurchaseAutoSettle settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ InitialStatus: creditpurchase.SettledInitialPaymentSettlementStatus, GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: alpacadecimal.NewFromFloat(0.5), }, }), @@ -558,7 +695,7 @@ func (s *CreditPurchaseTestSuite) TestExternalAuthorizedCreditPurchaseManuallySe settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: alpacadecimal.NewFromFloat(0.5), }, }), @@ -694,7 +831,7 @@ func (s *CreditPurchaseTestSuite) TestStandardInvoiceCreditPurchase() { servicePeriod: servicePeriod, settlement: creditpurchase.NewSettlement(creditpurchase.InvoiceSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: alpacadecimal.NewFromFloat(0.5), }, }), @@ -931,7 +1068,7 @@ func (s *CreditPurchaseTestSuite) TestStandardInvoiceCreditPurchaseDeferred() { }, settlement: creditpurchase.NewSettlement(creditpurchase.InvoiceSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: alpacadecimal.NewFromFloat(0.5), }, }), diff --git a/openmeter/billing/charges/service/flatfee_costbasis_test.go b/openmeter/billing/charges/service/flatfee_costbasis_test.go index eaffb789d2..b1c4d2d08c 100644 --- a/openmeter/billing/charges/service/flatfee_costbasis_test.go +++ b/openmeter/billing/charges/service/flatfee_costbasis_test.go @@ -43,29 +43,24 @@ func (s *FlatFeeCostBasisCreateSuite) TearDownTest() { s.BaseSuite.TearDownTest() } -func (s *FlatFeeCostBasisCreateSuite) TestCustomCurrencyCreditThenInvoiceIsDisabledByDefault() { +func (s *FlatFeeCostBasisCreateSuite) TestCustomCurrencyCreditOnlyIsDisabledByDefault() { // given: - // - a valid manual cost-basis intent for a custom-currency flat fee + // - a valid custom-currency credit-only flat fee // - the test-only custom-currency switch left at its default value // when: // - the charge is created // then: - // - creation is rejected before any charge cost basis is persisted + // - creation is rejected before the charge is persisted ctx := s.T().Context() namespace := s.GetUniqueNamespace("flat-fee-cost-basis-disabled") defaults := s.ProvisionDefaultTaxCodes(ctx, namespace) customer := s.CreateTestCustomer(namespace, "flat-fee-cost-basis-disabled") currency := s.createTestCustomCurrency(ctx, namespace) - fiatCurrency := s.newFiatCurrency("USD") - intent := costbasis.NewIntent(costbasis.ManualIntent{ - FiatCurrency: fiatCurrency, - Rate: alpacadecimal.NewFromInt(2), - }) _, err := s.Charges.flatFeeService.Create(ctx, flatfee.CreateInput{ Namespace: namespace, Intents: []flatfee.Intent{ - s.newFlatFeeIntent(customer.ID, currency, defaults.InvoicingTaxCodeID, "disabled", productcatalog.CreditThenInvoiceSettlementMode, &intent), + s.newFlatFeeIntent(customer.ID, currency, defaults.InvoicingTaxCodeID, "disabled", productcatalog.CreditOnlySettlementMode, nil), }, FeatureMeters: feature.FeatureMeterCollection{}, }) @@ -463,6 +458,7 @@ func (s *FlatFeeCostBasisCreateSuite) TestCreateWithoutCostBasisLeavesChargeRefe defaults := s.ProvisionDefaultTaxCodes(ctx, namespace) customer := s.CreateTestCustomer(namespace, "flat-fee-without-cost-basis") currency := s.createTestCustomCurrency(ctx, namespace) + s.setCustomCurrencyEnabled(true) created, err := s.Charges.flatFeeService.Create(ctx, flatfee.CreateInput{ Namespace: namespace, diff --git a/openmeter/billing/charges/service/invoicable_test.go b/openmeter/billing/charges/service/invoicable_test.go index 938f97f73f..a426d499bd 100644 --- a/openmeter/billing/charges/service/invoicable_test.go +++ b/openmeter/billing/charges/service/invoicable_test.go @@ -3011,6 +3011,10 @@ func (s *InvoicableChargesTestSuite) TestFlatFeeCreditOnlyWithCustomCurrency() { Currencies: s.CurrencyService, }) s.Require().NoError(err) + customCurrencyEnabler, ok := customCurrencyFlatFeeService.(customCurrencyEnabler) + s.Require().True(ok) + s.Require().NoError(customCurrencyEnabler.SetEnableCustomCurrency(s.T(), true)) + originalFlatFeeService := s.Charges.flatFeeService s.Charges.flatFeeService = customCurrencyFlatFeeService defer func() { @@ -3170,6 +3174,10 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyWithCustomCurrency( StreamingConnector: s.MockStreamingConnector, }) s.Require().NoError(err) + customCurrencyEnabler, ok := customCurrencyUsageBasedService.(customCurrencyEnabler) + s.Require().True(ok) + s.Require().NoError(customCurrencyEnabler.SetEnableCustomCurrency(s.T(), true)) + originalUsageBasedService := s.Charges.usageBasedService s.Charges.usageBasedService = customCurrencyUsageBasedService defer func() { diff --git a/openmeter/billing/charges/service/taxcode_test.go b/openmeter/billing/charges/service/taxcode_test.go index 675c65bc7c..f2a4e95e9c 100644 --- a/openmeter/billing/charges/service/taxcode_test.go +++ b/openmeter/billing/charges/service/taxcode_test.go @@ -396,7 +396,7 @@ func (s *TaxCodePersistenceTestSuite) TestCreditPurchaseInvoiceSettlementPropaga CreditAmount: alpacadecimal.NewFromFloat(100), Settlement: creditpurchase.NewSettlement(creditpurchase.InvoiceSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: alpacadecimal.NewFromFloat(0.5), }, }), @@ -515,7 +515,7 @@ func (s *TaxCodePersistenceTestSuite) TestCreditPurchaseInvoiceSettlementNilTaxC CreditAmount: alpacadecimal.NewFromFloat(100), Settlement: creditpurchase.NewSettlement(creditpurchase.InvoiceSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: alpacadecimal.NewFromFloat(0.5), }, }), diff --git a/openmeter/billing/charges/service/usagebased_costbasis_test.go b/openmeter/billing/charges/service/usagebased_costbasis_test.go index f8d659dd2c..f5a2980c0b 100644 --- a/openmeter/billing/charges/service/usagebased_costbasis_test.go +++ b/openmeter/billing/charges/service/usagebased_costbasis_test.go @@ -42,29 +42,25 @@ func (s *UsageBasedCostBasisCreateSuite) TearDownTest() { s.BaseSuite.TearDownTest() } -func (s *UsageBasedCostBasisCreateSuite) TestCustomCurrencyCreditThenInvoiceIsDisabledByDefault() { +func (s *UsageBasedCostBasisCreateSuite) TestCustomCurrencyCreditOnlyIsDisabledByDefault() { // given: - // - a valid manual cost-basis intent for a custom-currency usage-based charge + // - a valid custom-currency credit-only usage-based charge // - the test-only custom-currency switch left at its default value // when: // - the charge is created // then: - // - creation is rejected before any charge cost basis is persisted + // - creation is rejected before the charge is persisted ctx := s.T().Context() namespace := s.GetUniqueNamespace("usage-based-cost-basis-disabled") defaults := s.ProvisionDefaultTaxCodes(ctx, namespace) customer := s.CreateTestCustomer(namespace, "usage-based-cost-basis-disabled") currency := s.createTestCustomCurrency(ctx, namespace) featureMeters := s.createFeatureMeters(ctx, namespace, "disabled-feature") - intent := costbasis.NewIntent(costbasis.ManualIntent{ - FiatCurrency: s.newFiatCurrency("USD"), - Rate: alpacadecimal.NewFromInt(2), - }) _, err := s.Charges.usageBasedService.Create(ctx, usagebased.CreateInput{ Namespace: namespace, Intents: []usagebased.Intent{ - s.newUsageBasedIntent(customer.ID, currency, defaults.InvoicingTaxCodeID, "disabled", "disabled-feature", productcatalog.CreditThenInvoiceSettlementMode, &intent), + s.newUsageBasedIntent(customer.ID, currency, defaults.InvoicingTaxCodeID, "disabled", "disabled-feature", productcatalog.CreditOnlySettlementMode, nil), }, FeatureMeters: featureMeters, }) @@ -422,6 +418,7 @@ func (s *UsageBasedCostBasisCreateSuite) TestCreateWithoutCostBasisLeavesChargeR customer := s.CreateTestCustomer(namespace, "usage-based-without-cost-basis") currency := s.createTestCustomCurrency(ctx, namespace) featureMeters := s.createFeatureMeters(ctx, namespace, "credit-only-feature") + s.setCustomCurrencyEnabled(true) created, err := s.Charges.usageBasedService.Create(ctx, usagebased.CreateInput{ Namespace: namespace, diff --git a/openmeter/billing/charges/usagebased/service/create.go b/openmeter/billing/charges/usagebased/service/create.go index 266da4992f..ffcefada1a 100644 --- a/openmeter/billing/charges/usagebased/service/create.go +++ b/openmeter/billing/charges/usagebased/service/create.go @@ -31,9 +31,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) { now := clock.Now().UTC() createIntents, err := slicesx.MapWithErr(input.Intents, func(intent usagebased.Intent) (usagebased.CreateIntent, error) { - if intent.Currency.IsCustom() && - intent.SettlementMode == productcatalog.CreditThenInvoiceSettlementMode && - !s.enableCustomCurrency.Load() { + if intent.Currency.IsCustom() && !s.enableCustomCurrency.Load() { return usagebased.CreateIntent{}, fmt.Errorf("creating usage based charge with custom currency %q: %w", intent.Currency.GetCode(), meta.ErrCustomCurrencyNotSupported) } diff --git a/openmeter/billing/creditgrant/service/service.go b/openmeter/billing/creditgrant/service/service.go index db40234691..472081bec5 100644 --- a/openmeter/billing/creditgrant/service/service.go +++ b/openmeter/billing/creditgrant/service/service.go @@ -421,7 +421,7 @@ func toSettlement(input creditgrant.CreateInput) creditpurchase.Settlement { case creditgrant.FundingMethodInvoice: settlement := creditpurchase.InvoiceSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: input.Purchase.Currency, + Currency: currencyx.FiatCode(input.Purchase.Currency), CostBasis: lo.FromPtrOr(input.Purchase.PerUnitCostBasis, alpacadecimal.NewFromInt(1)), }, } @@ -435,7 +435,7 @@ func toSettlement(input creditgrant.CreateInput) creditpurchase.Settlement { settlement := creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: input.Purchase.Currency, + Currency: currencyx.FiatCode(input.Purchase.Currency), CostBasis: lo.FromPtrOr(input.Purchase.PerUnitCostBasis, alpacadecimal.NewFromInt(1)), }, InitialStatus: initialStatus, diff --git a/openmeter/ledger/chargeadapter/creditpurchase_test.go b/openmeter/ledger/chargeadapter/creditpurchase_test.go index 3ffbe1fabf..e508cdf8ef 100644 --- a/openmeter/ledger/chargeadapter/creditpurchase_test.go +++ b/openmeter/ledger/chargeadapter/creditpurchase_test.go @@ -615,7 +615,7 @@ func (e *creditPurchaseHandlerTestEnv) newExternalCharge(amount, costBasis alpac Settlement: chargecreditpurchase.NewSettlement(chargecreditpurchase.ExternalSettlement{ InitialStatus: chargecreditpurchase.CreatedInitialPaymentSettlementStatus, GenericSettlement: chargecreditpurchase.GenericSettlement{ - Currency: currencyx.Code("USD"), + Currency: currencyx.FiatCode("USD"), CostBasis: costBasis, }, }), diff --git a/openmeter/ledger/customerbalance/service_test.go b/openmeter/ledger/customerbalance/service_test.go index 3a001131e8..73eb0e4d20 100644 --- a/openmeter/ledger/customerbalance/service_test.go +++ b/openmeter/ledger/customerbalance/service_test.go @@ -819,7 +819,7 @@ func TestIsPendingCreditGrantAt(t *testing.T) { CreditAmount: alpacadecimal.NewFromInt(10), Settlement: creditpurchase.NewSettlement(creditpurchase.InvoiceSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: currency, + Currency: currencyx.FiatCode(currency), CostBasis: alpacadecimal.NewFromInt(1), }, }), diff --git a/openmeter/ledger/customerbalance/testenv_test.go b/openmeter/ledger/customerbalance/testenv_test.go index fc87fef7b0..a4d005ac9a 100644 --- a/openmeter/ledger/customerbalance/testenv_test.go +++ b/openmeter/ledger/customerbalance/testenv_test.go @@ -585,7 +585,7 @@ func (e *testEnv) createPendingInvoiceCreditGrant(t *testing.T, amount alpacadec return e.createCreditPurchase(t, amount, currency, nil, creditpurchase.FeatureFilters(features), creditpurchase.NewSettlement(creditpurchase.InvoiceSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: currency, + Currency: currencyx.FiatCode(currency), CostBasis: alpacadecimal.NewFromFloat(1), }, })) diff --git a/test/app/stripe/invoice_credits_test.go b/test/app/stripe/invoice_credits_test.go index f498874396..f3692c96fa 100644 --- a/test/app/stripe/invoice_credits_test.go +++ b/test/app/stripe/invoice_credits_test.go @@ -124,7 +124,7 @@ func (s *StripeInvoiceTestSuite) TestUsageBasedCreditThenInvoiceProgressiveBilli servicePeriod: timeutil.ClosedPeriod{From: setupAt, To: setupAt}, settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: currencyx.Code("USD"), + Currency: currencyx.FiatCode("USD"), CostBasis: costBasis, }, InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, diff --git a/test/credits/credit_then_invoice_test.go b/test/credits/credit_then_invoice_test.go index 7a0f141413..8e1c5c1773 100644 --- a/test/credits/credit_then_invoice_test.go +++ b/test/credits/credit_then_invoice_test.go @@ -1993,7 +1993,7 @@ func (s *CreditThenInvoiceTestSuite) TestFlatFeeCreditThenInvoiceShrinkToZeroThe }, Settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: creditCostBasis, }, InitialStatus: creditpurchase.SettledInitialPaymentSettlementStatus, diff --git a/test/credits/sanity_lifecycle_test.go b/test/credits/sanity_lifecycle_test.go index 4cbf10353a..7735b4d1e6 100644 --- a/test/credits/sanity_lifecycle_test.go +++ b/test/credits/sanity_lifecycle_test.go @@ -21,6 +21,7 @@ import ( "github.com/openmeterio/openmeter/openmeter/productcatalog" streamingtestutils "github.com/openmeterio/openmeter/openmeter/streaming/testutils" "github.com/openmeterio/openmeter/pkg/clock" + "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/datetime" "github.com/openmeterio/openmeter/pkg/timeutil" billingtest "github.com/openmeterio/openmeter/test/billing" @@ -259,7 +260,7 @@ func (s *SanityLifecycleSuite) TestUsageBasedCreditOnlyLifecycleTwoChargesTwoPur }, Settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: costBasis1, }, InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, @@ -326,7 +327,7 @@ func (s *SanityLifecycleSuite) TestUsageBasedCreditOnlyLifecycleTwoChargesTwoPur }, Settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: costBasis2, }, InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, @@ -491,7 +492,7 @@ func (s *SanityLifecycleSuite) setupUsageBasedCreditOnlyLifecyclePartialBackfill }, Settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: costBasis, }, InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, diff --git a/test/credits/sanity_test.go b/test/credits/sanity_test.go index b36aedaf9f..e849884c6d 100644 --- a/test/credits/sanity_test.go +++ b/test/credits/sanity_test.go @@ -2025,7 +2025,7 @@ func (s *SanitySuite) TestUsageBasedCreditOnlyDeleteCorrectionWithPartialBackfil }, Settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: alpacadecimal.NewFromFloat(0.5), }, InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, @@ -2225,7 +2225,7 @@ func (s *SanitySuite) TestUsageBasedCreditOnlyDeleteCorrectionWithMixedFeatureAd }, Settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: costBasis, }, InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, @@ -2401,7 +2401,7 @@ func (s *SanitySuite) TestFlatFeeCreditThenInvoiceSanity() { }, Settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: alpacadecimal.NewFromFloat(0.5), }, InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, @@ -3177,7 +3177,7 @@ func (s *SanitySuite) TestFlatFeeCreditOnlySanity() { }, Settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: alpacadecimal.NewFromFloat(0.5), }, InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, @@ -3426,7 +3426,7 @@ func (s *SanitySuite) TestFlatFeeCreditOnlySanity() { }, Settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: externalCostBasis, }, InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, @@ -3665,7 +3665,7 @@ func (s *SanitySuite) TestCreditPurchaseAdvanceAttributionAcrossTaxCodeBuckets() }, Settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: purchaseCostBasis, }, InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus, @@ -3846,7 +3846,7 @@ func (s *SanitySuite) TestCreditPurchaseAdvanceAttributionClearsLegacyNilSpendFe }, Settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{ GenericSettlement: creditpurchase.GenericSettlement{ - Currency: USD, + Currency: currencyx.FiatCode(USD), CostBasis: purchaseCostBasis, }, InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus,