Skip to content

Commit 6457e4b

Browse files
authored
fix(charges): persist fiat payment amounts (#4779)
1 parent fab8c28 commit 6457e4b

35 files changed

Lines changed: 544 additions & 467 deletions

openmeter/billing/charges/creditpurchase/service/external_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func TestExternalCreditPurchaseStateMachineUsesRoundedCreditAmount(t *testing.T)
103103
// when:
104104
// - the current external helpers grant credits and authorize payment
105105
// then:
106-
// - lineage and payment realization both use the currency-rounded credit amount
106+
// - lineage and handlers use the currency-rounded credit amount
107107
expectedAmount := alpacadecimal.NewFromFloat(100.12)
108108
charge := newExternalStateMachineTestChargeWithInput(t, externalStateMachineTestChargeInput{
109109
status: creditpurchase.StatusActivePaymentPending,
@@ -155,7 +155,6 @@ func TestExternalCreditPurchaseStateMachineUsesRoundedCreditAmount(t *testing.T)
155155
err = stateMachine.FireAndActivate(t.Context(), billing.TriggerAuthorized)
156156
require.NoError(t, err)
157157

158-
require.Equal(t, expectedAmount.InexactFloat64(), adapter.createdExternalPayment.Amount.InexactFloat64())
159158
handler.AssertExpectations(t)
160159
lineageService.AssertExpectations(t)
161160
}
@@ -461,7 +460,7 @@ func TestExternalCreditPurchaseStateMachineAuthorizationUsesRealizationDuplicate
461460
},
462461
Base: payment.Base{
463462
ServicePeriod: charge.Intent.ServicePeriod,
464-
Amount: charge.Intent.CreditAmount,
463+
FiatAmount: charge.Intent.CreditAmount,
465464
Status: payment.StatusAuthorized,
466465
Authorized: &ledgertransaction.TimedGroupReference{
467466
GroupReference: ledgertransaction.GroupReference{TransactionGroupID: "authorized-ledger-tx"},

openmeter/billing/charges/creditpurchase/service/invoice.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func (s *service) PostInvoicePaymentAuthorized(ctx context.Context, charge credi
6767
Namespace: charge.Namespace,
6868
Base: payment.Base{
6969
ServicePeriod: charge.Intent.ServicePeriod,
70-
Amount: charge.Intent.CreditAmount,
70+
FiatAmount: lineWithHeader.Line.Totals.Total,
7171
Authorized: &ledgertransaction.TimedGroupReference{
7272
GroupReference: ledgerTransactionGroupReference,
7373
Time: eventAt,

openmeter/billing/charges/creditpurchase/service/realizations/service.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,20 @@ func (s *Service) AuthorizeExternalPayment(ctx context.Context, charge creditpur
109109
WithAttrs(charge.Realizations.ExternalPaymentSettlement.ErrorAttributes())
110110
}
111111

112+
externalSettlement, err := charge.Intent.Settlement.AsExternalSettlement()
113+
if err != nil {
114+
return creditpurchase.Charge{}, err
115+
}
116+
117+
fiatCurrency, err := externalSettlement.Currency.AsFiatCurrency()
118+
if err != nil {
119+
return creditpurchase.Charge{}, err
120+
}
121+
122+
fiatAmount := fiatCurrency.RoundToPrecision(
123+
charge.Intent.CreditAmount.Mul(externalSettlement.CostBasis),
124+
)
125+
112126
eventAt := clock.Now()
113127
ledgerTransactionGroupReference, err := s.handler.OnCreditPurchasePaymentAuthorized(ctx, creditpurchase.PaymentEventInput{
114128
Charge: charge,
@@ -122,7 +136,7 @@ func (s *Service) AuthorizeExternalPayment(ctx context.Context, charge creditpur
122136
Namespace: charge.Namespace,
123137
Base: payment.Base{
124138
ServicePeriod: charge.Intent.ServicePeriod,
125-
Amount: charge.Intent.CreditAmount,
139+
FiatAmount: fiatAmount,
126140
Authorized: &ledgertransaction.TimedGroupReference{
127141
GroupReference: ledgerTransactionGroupReference,
128142
Time: eventAt,

openmeter/billing/charges/flatfee/service/payment.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func (s *service) postInvoicePaymentAuthorized(ctx context.Context, charge flatf
6060
InvoiceID: lineWithHeader.Invoice.ID,
6161
Base: payment.Base{
6262
ServicePeriod: run.ServicePeriod,
63-
Amount: lineWithHeader.Line.Totals.Total,
63+
FiatAmount: lineWithHeader.Line.Totals.Total,
6464
Authorized: &ledgertransaction.TimedGroupReference{
6565
GroupReference: ledgertransaction.GroupReference{
6666
TransactionGroupID: ledgerTransactionGroupReference.TransactionGroupID,

openmeter/billing/charges/models/payment/mixin.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ func (mixin) Fields() []ent.Field {
3838
field.Enum("status").
3939
GoType(Status("")),
4040

41-
field.Other("amount", alpacadecimal.Decimal{}).
41+
field.Other("fiat_amount", alpacadecimal.Decimal{}).
42+
StorageKey("amount").
4243
SchemaType(map[string]string{
4344
dialect.Postgres: "numeric",
4445
}),
@@ -67,7 +68,7 @@ func (mixin) Fields() []ent.Field {
6768
}
6869

6970
type MutableFieldSetter[T any] interface {
70-
SetAmount(amount alpacadecimal.Decimal) T
71+
SetFiatAmount(fiatAmount alpacadecimal.Decimal) T
7172
SetStatus(status Status) T
7273
SetServicePeriodFrom(servicePeriodFrom time.Time) T
7374
SetServicePeriodTo(servicePeriodTo time.Time) T
@@ -89,7 +90,7 @@ func Create[T Creator[T]](creator Creator[T], namespace string, paymentSettlemen
8990
SetNamespace(namespace).
9091
SetServicePeriodFrom(paymentSettlement.ServicePeriod.From).
9192
SetServicePeriodTo(paymentSettlement.ServicePeriod.To).
92-
SetAmount(paymentSettlement.Amount).
93+
SetFiatAmount(paymentSettlement.FiatAmount).
9394
SetStatus(paymentSettlement.Status).
9495
SetNillableAuthorizedTransactionGroupID(paymentSettlement.Authorized.GetIDOrNull()).
9596
SetNillableAuthorizedAt(paymentSettlement.Authorized.GetTimeOrNull()).
@@ -107,7 +108,7 @@ func Update[T Updater[T]](updater Updater[T], in Payment) T {
107108
return updater.SetAnnotations(in.Annotations).
108109
SetServicePeriodFrom(in.ServicePeriod.From).
109110
SetServicePeriodTo(in.ServicePeriod.To).
110-
SetAmount(in.Amount).
111+
SetFiatAmount(in.FiatAmount).
111112
SetStatus(in.Status).
112113
SetNillableDeletedAt(convert.TimePtrIn(in.DeletedAt, time.UTC)).
113114
SetNillableAuthorizedTransactionGroupID(in.Authorized.GetIDOrNull()).
@@ -123,7 +124,7 @@ type Getter interface {
123124
entutils.AnnotationsMixinGetter
124125
GetServicePeriodFrom() time.Time
125126
GetServicePeriodTo() time.Time
126-
GetAmount() alpacadecimal.Decimal
127+
GetFiatAmount() alpacadecimal.Decimal
127128
GetStatus() Status
128129
GetAuthorizedTransactionGroupID() *string
129130
GetAuthorizedAt() *time.Time
@@ -139,7 +140,7 @@ func mapBaseFromDB(dbEntity Getter) Base {
139140
To: dbEntity.GetServicePeriodTo().In(time.UTC),
140141
},
141142
Status: dbEntity.GetStatus(),
142-
Amount: dbEntity.GetAmount(),
143+
FiatAmount: dbEntity.GetFiatAmount(),
143144
Authorized: mapTimedLedgerTransactionGroupReferenceFromDB(dbEntity.GetAuthorizedTransactionGroupID(), dbEntity.GetAuthorizedAt()),
144145
Settled: mapTimedLedgerTransactionGroupReferenceFromDB(dbEntity.GetSettledTransactionGroupID(), dbEntity.GetSettledAt()),
145146
}

openmeter/billing/charges/models/payment/models.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ type Base struct {
4848
Annotations models.Annotations `json:"annotations"`
4949
ServicePeriod timeutil.ClosedPeriod `json:"servicePeriod"`
5050

51-
Status Status `json:"status"`
52-
Amount alpacadecimal.Decimal `json:"amount"`
51+
Status Status `json:"status"`
52+
FiatAmount alpacadecimal.Decimal `json:"fiatAmount"`
5353

5454
Authorized *ledgertransaction.TimedGroupReference `json:"authorized"`
5555
Settled *ledgertransaction.TimedGroupReference `json:"settled"`
@@ -78,8 +78,8 @@ func (r Base) Validate() error {
7878
}
7979
}
8080

81-
if !r.Amount.IsPositive() {
82-
errs = append(errs, fmt.Errorf("amount must be positive"))
81+
if !r.FiatAmount.IsPositive() {
82+
errs = append(errs, fmt.Errorf("fiat amount must be positive"))
8383
}
8484

8585
switch r.Status {

openmeter/billing/charges/service/creditpurchase_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,12 +575,13 @@ func (s *CreditPurchaseTestSuite) TestExternalAuthorizedCreditPurchaseAutoSettle
575575
cust := s.CreateTestCustomer(ns, "test-subject")
576576
s.NotEmpty(cust.ID)
577577

578-
// Let's buy 100 USD credits for $0.50 each (total cost is $50)
578+
// Let's buy 100.123 USD credits for $0.50 each. The credit amount is rounded
579+
// to 100.12 USD, so the total fiat cost is $50.06.
579580
intent := CreateCreditPurchaseIntent(s.T(),
580581
createCreditPurchaseIntentInput{
581582
customer: cust.GetID(),
582583
currency: USD,
583-
amount: alpacadecimal.NewFromFloat(100),
584+
amount: alpacadecimal.NewFromFloat(100.123),
584585
servicePeriod: timeutil.ClosedPeriod{
585586
From: datetime.MustParseTimeInLocation(s.T(), "2026-01-01T00:00:00Z", time.UTC).AsTime(),
586587
To: datetime.MustParseTimeInLocation(s.T(), "2026-02-01T00:00:00Z", time.UTC).AsTime(),
@@ -667,6 +668,7 @@ func (s *CreditPurchaseTestSuite) TestExternalAuthorizedCreditPurchaseAutoSettle
667668
s.NotNil(creditPurchaseCharge.Realizations.ExternalPaymentSettlement, "external payment settlement should be set")
668669
s.Equal(authorizedCallback.id, creditPurchaseCharge.Realizations.ExternalPaymentSettlement.Authorized.TransactionGroupID, "authorized transaction group ID should be set")
669670
s.Equal(settledCallback.id, creditPurchaseCharge.Realizations.ExternalPaymentSettlement.Settled.TransactionGroupID, "settled transaction group ID should be set")
671+
s.Equal(float64(50.06), creditPurchaseCharge.Realizations.ExternalPaymentSettlement.FiatAmount.InexactFloat64())
670672

671673
// The charge should be final
672674
s.Equal(creditpurchase.StatusFinal, creditPurchaseCharge.Status)
@@ -1028,6 +1030,7 @@ func (s *CreditPurchaseTestSuite) TestStandardInvoiceCreditPurchase() {
10281030

10291031
s.Equal(1, authorizedCallback.nrInvocations)
10301032
s.Equal(settledCallback.id, creditPurchaseCharge.Realizations.InvoiceSettlement.Settled.TransactionGroupID)
1033+
s.Equal(float64(50), creditPurchaseCharge.Realizations.InvoiceSettlement.FiatAmount.InexactFloat64())
10311034
s.Equal(payment.StatusSettled, creditPurchaseCharge.Realizations.InvoiceSettlement.Status)
10321035
s.Equal(creditpurchase.StatusFinal, creditPurchaseCharge.Status)
10331036
})

openmeter/billing/charges/usagebased/service/run/payment.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func (s *Service) BookInvoicedPaymentAuthorized(ctx context.Context, in BookInvo
8787
InvoiceID: in.Invoice.ID,
8888
Base: payment.Base{
8989
ServicePeriod: in.Line.Period,
90-
Amount: in.Line.Totals.Total,
90+
FiatAmount: in.Line.Totals.Total,
9191
Authorized: &ledgertransaction.TimedGroupReference{
9292
GroupReference: ledgertransaction.GroupReference{
9393
TransactionGroupID: ledgerTransactionRef.TransactionGroupID,

openmeter/billing/charges/usagebased/service/run/payment_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func TestBookInvoicedPaymentAuthorizedInputValidate(t *testing.T) {
3535
Base: payment.Base{
3636
ServicePeriod: in.Line.Period,
3737
Status: payment.StatusAuthorized,
38-
Amount: in.Line.Totals.Total,
38+
FiatAmount: in.Line.Totals.Total,
3939
Authorized: &ledgertransaction.TimedGroupReference{
4040
GroupReference: ledgertransaction.GroupReference{
4141
TransactionGroupID: "authorized-group",
@@ -154,7 +154,7 @@ func newSettlePaymentInput(t testing.TB) SettleInvoicedPaymentInput {
154154
Base: payment.Base{
155155
ServicePeriod: authInput.Line.Period,
156156
Status: payment.StatusAuthorized,
157-
Amount: authInput.Line.Totals.Total,
157+
FiatAmount: authInput.Line.Totals.Total,
158158
Authorized: &ledgertransaction.TimedGroupReference{
159159
GroupReference: ledgertransaction.GroupReference{
160160
TransactionGroupID: "authorized-group",

openmeter/ent/db/chargecreditpurchaseexternalpayment.go

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)