@@ -11,15 +11,18 @@ import (
1111 "github.com/invopop/gobl/currency"
1212 "github.com/samber/lo"
1313 "github.com/stretchr/testify/assert"
14+ "github.com/stretchr/testify/mock"
1415 "github.com/stretchr/testify/require"
1516 "github.com/stretchr/testify/suite"
1617
1718 appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing"
1819 "github.com/openmeterio/openmeter/openmeter/billing"
1920 "github.com/openmeterio/openmeter/openmeter/billing/charges"
2021 "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase"
22+ creditpurchaseservice "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase/service"
2123 "github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
2224 "github.com/openmeterio/openmeter/openmeter/billing/charges/models/payment"
25+ "github.com/openmeterio/openmeter/openmeter/currencies"
2326 currenciestestutils "github.com/openmeterio/openmeter/openmeter/currencies/testutils/currency"
2427 "github.com/openmeterio/openmeter/openmeter/customer"
2528 "github.com/openmeterio/openmeter/pkg/clock"
@@ -98,6 +101,118 @@ func (s *CreditPurchaseTestSuite) TestPromotionalCreditPurchase() {
98101 s .Equal (creditpurchase .StatusFinal , updatedCPCharge .Status )
99102}
100103
104+ func (s * CreditPurchaseTestSuite ) TestPromotionalCreditPurchaseWithCustomCurrency () {
105+ ctx := s .T ().Context ()
106+ ns := s .GetUniqueNamespace ("charges-service-promotional-credit-purchase-custom-currency" )
107+
108+ var customCurrency currencies.Currency
109+ var customerID string
110+ var createdCharge creditpurchase.Charge
111+ var promotionalTransactionGroupID string
112+
113+ s .Run ("#1 setup customer and custom currency" , func () {
114+ // given:
115+ // - a customer and a persisted custom currency
116+ s .ProvisionDefaultTaxCodes (ctx , ns )
117+
118+ cust := s .CreateTestCustomer (ns , "test-subject" )
119+ s .NotEmpty (cust .ID )
120+ customerID = cust .ID
121+ customCurrency = s .createTestCustomCurrency (ctx , ns )
122+
123+ })
124+
125+ s .Run ("#2 create promotional credit purchase" , func () {
126+ // given:
127+ // - a promotional credit-purchase intent in the custom currency
128+ // - mocked ledger and lineage callbacks
129+ // when:
130+ // - the charge is created through the root charges service
131+ // then:
132+ // - the callbacks run once and the charge reaches final with a persisted realization
133+ servicePeriod := timeutil.ClosedPeriod {
134+ From : datetime .MustParseTimeInLocation (s .T (), "2026-01-01T00:00:00Z" , time .UTC ).AsTime (),
135+ To : datetime .MustParseTimeInLocation (s .T (), "2026-02-01T00:00:00Z" , time .UTC ).AsTime (),
136+ }
137+ intent := charges .NewChargeIntent (creditpurchase.Intent {
138+ Intent : meta.Intent {
139+ ManagedBy : billing .ManuallyManagedLine ,
140+ CustomerID : customerID ,
141+ Currency : customCurrency ,
142+ },
143+ IntentMutableFields : creditpurchase.IntentMutableFields {
144+ IntentMutableFields : meta.IntentMutableFields {
145+ Name : "Custom Currency Credit Purchase" ,
146+ ServicePeriod : servicePeriod ,
147+ BillingPeriod : servicePeriod ,
148+ FullServicePeriod : servicePeriod ,
149+ },
150+ CreditAmount : alpacadecimal .NewFromFloat (100.1234 ),
151+ Settlement : creditpurchase .NewSettlement (creditpurchase.PromotionalSettlement {}),
152+ },
153+ })
154+
155+ promotionalCallback := newCountedLedgerTransactionCallback [creditpurchase.Charge ]()
156+ promotionalTransactionGroupID = promotionalCallback .id
157+ s .CreditPurchaseTestHandler .onPromotionalCreditPurchase = promotionalCallback .Handler (s .T (), func (t * testing.T , charge creditpurchase.Charge ) {
158+ assert .Equal (t , creditpurchase .SettlementTypePromotional , charge .Intent .Settlement .Type ())
159+ assert .True (t , charge .Intent .Currency .IsCustom ())
160+ assert .Equal (t , customCurrency .ID , charge .Intent .Currency .ID )
161+ assert .Nil (t , charge .Realizations .CreditGrantRealization )
162+ })
163+
164+ lineageMock := & mockLineageService {Service : s .LineageService }
165+ lineageMock .On ("BackfillAdvanceLineageSegments" , mock .Anything , mock .Anything ).
166+ Return (nil ).
167+ Once ()
168+
169+ customCurrencyCreditPurchaseService , err := creditpurchaseservice .New (creditpurchaseservice.Config {
170+ Adapter : s .CreditPurchaseAdapter ,
171+ Handler : s .CreditPurchaseTestHandler ,
172+ Lineage : lineageMock ,
173+ MetaAdapter : s .MetaAdapter ,
174+ })
175+ s .Require ().NoError (err )
176+ originalCreditPurchaseService := s .Charges .creditPurchaseService
177+ s .Charges .creditPurchaseService = customCurrencyCreditPurchaseService
178+ defer func () {
179+ s .Charges .creditPurchaseService = originalCreditPurchaseService
180+ }()
181+
182+ created , err := s .Charges .Create (ctx , charges.CreateInput {
183+ Namespace : ns ,
184+ Intents : charges.ChargeIntents {intent },
185+ })
186+ s .Require ().NoError (err )
187+ s .Require ().Len (created , 1 )
188+ s .Equal (1 , promotionalCallback .nrInvocations )
189+ lineageMock .AssertExpectations (s .T ())
190+
191+ createdCharge , err = created [0 ].AsCreditPurchaseCharge ()
192+ s .Require ().NoError (err )
193+ s .Equal (creditpurchase .StatusFinal , createdCharge .Status )
194+ s .True (createdCharge .Intent .Currency .IsCustom ())
195+ s .Equal (customCurrency .ID , createdCharge .Intent .Currency .ID )
196+ s .Equal (float64 (100.123 ), createdCharge .Intent .CreditAmount .InexactFloat64 ())
197+ s .Require ().NotNil (createdCharge .Realizations .CreditGrantRealization )
198+ s .Equal (promotionalTransactionGroupID , createdCharge .Realizations .CreditGrantRealization .TransactionGroupID )
199+ })
200+
201+ s .Run ("#3 reload persisted charge" , func () {
202+ // when:
203+ // - the charge is loaded again from Postgres
204+ // then:
205+ // - its final state, custom currency, and realization are preserved
206+ persisted , err := s .mustGetChargeByID (createdCharge .GetChargeID ()).AsCreditPurchaseCharge ()
207+ s .Require ().NoError (err )
208+ s .Equal (creditpurchase .StatusFinal , persisted .Status )
209+ s .True (persisted .Intent .Currency .IsCustom ())
210+ s .Equal (customCurrency .ID , persisted .Intent .Currency .ID )
211+ s .Require ().NotNil (persisted .Realizations .CreditGrantRealization )
212+ s .Equal (promotionalTransactionGroupID , persisted .Realizations .CreditGrantRealization .TransactionGroupID )
213+ })
214+ }
215+
101216func (s * CreditPurchaseTestSuite ) TestCreditPurchaseRejectsMismatchedSettlementCurrency () {
102217 ctx := context .Background ()
103218 ns := s .GetUniqueNamespace ("charges-service-credit-purchase-mismatched-settlement-currency" )
@@ -725,7 +840,6 @@ func (s *CreditPurchaseTestSuite) TestStandardInvoiceCreditPurchase() {
725840
726841 detailedLine := line .DetailedLines [0 ]
727842
728- s .Equal (USD , detailedLine .Currency )
729843 s .Equal (alpacadecimal .NewFromFloat (50 ), detailedLine .PerUnitAmount )
730844 s .Equal (alpacadecimal .NewFromFloat (1 ), detailedLine .Quantity )
731845 s .Equal (alpacadecimal .NewFromFloat (50 ), detailedLine .Totals .Amount )
0 commit comments