Skip to content

Commit 476cef5

Browse files
authored
fix: disallow cross fiat credit purchases (#4165)
1 parent edf64e5 commit 476cef5

2 files changed

Lines changed: 76 additions & 0 deletions

File tree

openmeter/billing/charges/creditpurchase/charge.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,23 @@ func (i Intent) Validate() error {
128128
errs = append(errs, fmt.Errorf("settlement: %w", err))
129129
}
130130

131+
switch i.Settlement.Type() {
132+
case SettlementTypeInvoice:
133+
settlement, err := i.Settlement.AsInvoiceSettlement()
134+
if err != nil {
135+
errs = append(errs, fmt.Errorf("settlement: %w", err))
136+
} else if settlement.Currency != i.Currency {
137+
errs = append(errs, fmt.Errorf("settlement currency %q must match credit currency %q", settlement.Currency, i.Currency))
138+
}
139+
case SettlementTypeExternal:
140+
settlement, err := i.Settlement.AsExternalSettlement()
141+
if err != nil {
142+
errs = append(errs, fmt.Errorf("settlement: %w", err))
143+
} else if settlement.Currency != i.Currency {
144+
errs = append(errs, fmt.Errorf("settlement currency %q must match credit currency %q", settlement.Currency, i.Currency))
145+
}
146+
}
147+
131148
if i.EffectiveAt != nil {
132149
return errors.New("effective at is not yet supported")
133150
}

openmeter/billing/charges/service/creditpurchase_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
"github.com/alpacahq/alpacadecimal"
11+
"github.com/invopop/gobl/currency"
1112
"github.com/samber/lo"
1213
"github.com/stretchr/testify/assert"
1314
"github.com/stretchr/testify/require"
@@ -94,6 +95,64 @@ func (s *CreditPurchaseTestSuite) TestPromotionalCreditPurchase() {
9495
s.Equal(creditpurchase.StatusFinal, updatedCPCharge.Status)
9596
}
9697

98+
func (s *CreditPurchaseTestSuite) TestCreditPurchaseRejectsMismatchedSettlementCurrency() {
99+
ctx := context.Background()
100+
ns := s.GetUniqueNamespace("charges-service-credit-purchase-mismatched-settlement-currency")
101+
102+
cust := s.CreateTestCustomer(ns, "test-subject")
103+
s.NotEmpty(cust.ID)
104+
105+
servicePeriod := timeutil.ClosedPeriod{
106+
From: datetime.MustParseTimeInLocation(s.T(), "2026-01-01T00:00:00Z", time.UTC).AsTime(),
107+
To: datetime.MustParseTimeInLocation(s.T(), "2026-02-01T00:00:00Z", time.UTC).AsTime(),
108+
}
109+
110+
for _, tc := range []struct {
111+
name string
112+
settlement creditpurchase.Settlement
113+
}{
114+
{
115+
name: "external",
116+
settlement: creditpurchase.NewSettlement(creditpurchase.ExternalSettlement{
117+
InitialStatus: creditpurchase.CreatedInitialPaymentSettlementStatus,
118+
GenericSettlement: creditpurchase.GenericSettlement{
119+
Currency: currencyx.Code(currency.EUR),
120+
CostBasis: alpacadecimal.NewFromFloat(0.5),
121+
},
122+
}),
123+
},
124+
{
125+
name: "invoice",
126+
settlement: creditpurchase.NewSettlement(creditpurchase.InvoiceSettlement{
127+
GenericSettlement: creditpurchase.GenericSettlement{
128+
Currency: currencyx.Code(currency.EUR),
129+
CostBasis: alpacadecimal.NewFromFloat(0.5),
130+
},
131+
}),
132+
},
133+
} {
134+
s.Run(tc.name, func() {
135+
intent := CreateCreditPurchaseIntent(s.T(), createCreditPurchaseIntentInput{
136+
customer: cust.GetID(),
137+
currency: USD,
138+
amount: alpacadecimal.NewFromFloat(100),
139+
servicePeriod: servicePeriod,
140+
settlement: tc.settlement,
141+
})
142+
143+
res, err := s.Charges.Create(ctx, charges.CreateInput{
144+
Namespace: ns,
145+
Intents: charges.ChargeIntents{
146+
intent,
147+
},
148+
})
149+
s.Error(err)
150+
s.ErrorContains(err, `settlement currency "EUR" must match credit currency "USD"`)
151+
s.Empty(res)
152+
})
153+
}
154+
}
155+
97156
type createCreditPurchaseIntentInput struct {
98157
customer customer.CustomerID
99158
currency currencyx.Code

0 commit comments

Comments
 (0)