Skip to content

Commit a433d7e

Browse files
authored
fix: tx list names (#4167)
1 parent 2007c53 commit a433d7e

6 files changed

Lines changed: 257 additions & 14 deletions

File tree

api/v3/handlers/customers/credits/convert.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -374,13 +374,14 @@ func toAPIBillingCreditTransactions(items []customerbalance.CreditTransaction) [
374374

375375
func toAPIBillingCreditTransaction(tx customerbalance.CreditTransaction) api.BillingCreditTransaction {
376376
apiTx := api.BillingCreditTransaction{
377-
Id: tx.ID.ID,
378-
CreatedAt: &tx.CreatedAt,
379-
BookedAt: tx.BookedAt,
380-
Type: toAPIBillingCreditTransactionType(tx.Type),
381-
Currency: api.BillingCurrencyCode(tx.Currency),
382-
Amount: tx.Amount.String(),
383-
Name: tx.Name,
377+
Id: tx.ID.ID,
378+
CreatedAt: &tx.CreatedAt,
379+
BookedAt: tx.BookedAt,
380+
Type: toAPIBillingCreditTransactionType(tx.Type),
381+
Currency: api.BillingCurrencyCode(tx.Currency),
382+
Amount: tx.Amount.String(),
383+
Name: tx.Name,
384+
Description: tx.Description,
384385
AvailableBalance: struct {
385386
After api.Numeric `json:"after"`
386387
Before api.Numeric `json:"before"`

api/v3/handlers/customers/credits/list_transactions_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func TestFromAPIBillingCreditTransactionType_Adjusted(t *testing.T) {
2626
func TestToAPIBillingCreditTransaction(t *testing.T) {
2727
createdAt := time.Date(2026, 4, 10, 9, 0, 0, 0, time.UTC)
2828
bookedAt := createdAt.Add(time.Second)
29+
description := "Welcome credits"
2930

3031
tx := toAPIBillingCreditTransaction(customerbalance.CreditTransaction{
3132
ID: models.NamespacedID{
@@ -41,7 +42,8 @@ func TestToAPIBillingCreditTransaction(t *testing.T) {
4142
Before: alpacadecimal.NewFromInt(52),
4243
After: alpacadecimal.NewFromInt(42),
4344
},
44-
Name: "credit_transaction",
45+
Name: "credit_transaction",
46+
Description: &description,
4547
Annotations: models.Annotations{
4648
ledger.AnnotationChargeID: "charge-1",
4749
},
@@ -53,6 +55,8 @@ func TestToAPIBillingCreditTransaction(t *testing.T) {
5355
require.Equal(t, api.Numeric("-10"), tx.Amount)
5456
require.Equal(t, api.Numeric("52"), tx.AvailableBalance.Before)
5557
require.Equal(t, api.Numeric("42"), tx.AvailableBalance.After)
58+
require.NotNil(t, tx.Description)
59+
require.Equal(t, description, *tx.Description)
5660
require.NotNil(t, tx.Labels)
5761
require.Equal(t, "charge-1", (*tx.Labels)["charge_id"])
5862
}

openmeter/ledger/customerbalance/service.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
// ----------------------------------------------------------------------------
2424

2525
type chargesService interface {
26+
GetByIDs(ctx context.Context, input charges.GetByIDsInput) (charges.Charges, error)
2627
ListCharges(ctx context.Context, input charges.ListChargesInput) (pagination.Result[charges.Charge], error)
2728
}
2829

openmeter/ledger/customerbalance/testenv_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,62 @@ type chargeStore struct {
366366
usageBasedService usagebased.Service
367367
}
368368

369+
func (l chargeStore) GetByIDs(ctx context.Context, input charges.GetByIDsInput) (charges.Charges, error) {
370+
searchResult, err := l.search.GetByIDs(ctx, input)
371+
if err != nil {
372+
return nil, err
373+
}
374+
375+
flatFeeIDs := make([]string, 0, len(searchResult))
376+
usageBasedIDs := make([]string, 0, len(searchResult))
377+
for _, item := range searchResult {
378+
switch item.Type {
379+
case chargemeta.ChargeTypeFlatFee:
380+
flatFeeIDs = append(flatFeeIDs, item.ID.ID)
381+
case chargemeta.ChargeTypeUsageBased:
382+
usageBasedIDs = append(usageBasedIDs, item.ID.ID)
383+
}
384+
}
385+
386+
flatFeeCharges, err := l.flatFeeService.GetByIDs(ctx, flatfee.GetByIDsInput{
387+
Namespace: input.Namespace,
388+
IDs: flatFeeIDs,
389+
Expands: input.Expands,
390+
})
391+
if err != nil {
392+
return nil, err
393+
}
394+
395+
usageBasedCharges, err := l.usageBasedService.GetByIDs(ctx, usagebased.GetByIDsInput{
396+
Namespace: input.Namespace,
397+
IDs: usageBasedIDs,
398+
Expands: input.Expands,
399+
})
400+
if err != nil {
401+
return nil, err
402+
}
403+
404+
chargesByID := make(map[string]charges.Charge, len(flatFeeCharges)+len(usageBasedCharges))
405+
for _, charge := range flatFeeCharges {
406+
chargesByID[charge.ID] = charges.NewCharge(charge)
407+
}
408+
for _, charge := range usageBasedCharges {
409+
chargesByID[charge.ID] = charges.NewCharge(charge)
410+
}
411+
412+
items := make(charges.Charges, 0, len(searchResult))
413+
for _, item := range searchResult {
414+
charge, ok := chargesByID[item.ID.ID]
415+
if !ok {
416+
continue
417+
}
418+
419+
items = append(items, charge)
420+
}
421+
422+
return items, nil
423+
}
424+
369425
func (l chargeStore) ListCharges(ctx context.Context, input charges.ListChargesInput) (pagination.Result[charges.Charge], error) {
370426
searchResult, err := l.search.ListCharges(ctx, input)
371427
if err != nil {

openmeter/ledger/customerbalance/transactions.go

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"github.com/alpacahq/alpacadecimal"
1010
"github.com/samber/lo"
1111

12+
"github.com/openmeterio/openmeter/openmeter/billing/charges"
13+
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
1214
"github.com/openmeterio/openmeter/openmeter/customer"
1315
"github.com/openmeterio/openmeter/openmeter/ledger"
1416
ledgeraccount "github.com/openmeterio/openmeter/openmeter/ledger/account"
@@ -77,6 +79,7 @@ type CreditTransaction struct {
7779
Amount alpacadecimal.Decimal
7880
Balance CreditTransactionBalance
7981
Name string
82+
Description *string
8083
Annotations models.Annotations
8184
}
8285

@@ -124,6 +127,8 @@ func (s *Service) ListCreditTransactions(ctx context.Context, input ListCreditTr
124127
return ListCreditTransactionsResult{}, err
125128
}
126129

130+
s.applyChargeMetadataToCreditTransactions(ctx, input.CustomerID.Namespace, items)
131+
127132
if len(items) > 0 {
128133
runningBalance, err := s.GetBalance(ctx, input.CustomerID, routeFilter(items[0].Currency), lo.ToPtr(result.Items[0].Cursor()))
129134
if err != nil {
@@ -212,7 +217,7 @@ func creditTransactionFromLedgerTransaction(tx ledger.Transaction) (CreditTransa
212217
Type: creditTransactionType(amount),
213218
Currency: entry.PostingAddress().Route().Route().Currency,
214219
Amount: amount,
215-
Name: creditTransactionName(tx),
220+
Name: "",
216221
Annotations: tx.Annotations(),
217222
}, nil
218223
}
@@ -251,11 +256,107 @@ func creditTransactionType(fboImpact alpacadecimal.Decimal) CreditTransactionTyp
251256
return CreditTransactionTypeAdjusted
252257
}
253258

254-
func creditTransactionName(tx ledger.Transaction) string {
255-
templateName, _ := ledger.TransactionTemplateNameFromAnnotations(tx.Annotations())
256-
if templateName != "" {
257-
return templateName
259+
type chargeDisplayMetadata struct {
260+
Name string
261+
Description *string
262+
}
263+
264+
func (s *Service) applyChargeMetadataToCreditTransactions(ctx context.Context, namespace string, items []CreditTransaction) {
265+
chargeIDs := lo.Uniq(lo.FilterMap(items, func(item CreditTransaction, _ int) (string, bool) {
266+
id := chargeIDFromAnnotations(item.Annotations)
267+
return id, id != ""
268+
}))
269+
270+
if len(chargeIDs) == 0 {
271+
return
272+
}
273+
274+
chargeEntities, err := s.ChargesService.GetByIDs(ctx, charges.GetByIDsInput{
275+
Namespace: namespace,
276+
IDs: chargeIDs,
277+
})
278+
if err != nil {
279+
return
280+
}
281+
282+
chargeDisplayByID := make(map[string]chargeDisplayMetadata, len(chargeEntities))
283+
for _, chargeEntity := range chargeEntities {
284+
chargeID, err := chargeEntity.GetChargeID()
285+
if err != nil {
286+
continue
287+
}
288+
289+
metadata, err := chargeDisplayMetadataFromCharge(chargeEntity)
290+
if err != nil {
291+
continue
292+
}
293+
294+
chargeDisplayByID[chargeID.ID] = metadata
295+
}
296+
297+
for i := range items {
298+
chargeID := chargeIDFromAnnotations(items[i].Annotations)
299+
if chargeID == "" {
300+
continue
301+
}
302+
303+
metadata, ok := chargeDisplayByID[chargeID]
304+
if !ok {
305+
continue
306+
}
307+
308+
items[i].Name = metadata.Name
309+
items[i].Description = metadata.Description
310+
}
311+
}
312+
313+
func chargeDisplayMetadataFromCharge(charge charges.Charge) (chargeDisplayMetadata, error) {
314+
switch charge.Type() {
315+
case meta.ChargeTypeFlatFee:
316+
flatFeeCharge, err := charge.AsFlatFeeCharge()
317+
if err != nil {
318+
return chargeDisplayMetadata{}, fmt.Errorf("map flat fee charge: %w", err)
319+
}
320+
321+
return chargeDisplayMetadata{
322+
Name: flatFeeCharge.Intent.Name,
323+
Description: flatFeeCharge.Intent.Description,
324+
}, nil
325+
case meta.ChargeTypeUsageBased:
326+
usageBasedCharge, err := charge.AsUsageBasedCharge()
327+
if err != nil {
328+
return chargeDisplayMetadata{}, fmt.Errorf("map usage based charge: %w", err)
329+
}
330+
331+
return chargeDisplayMetadata{
332+
Name: usageBasedCharge.Intent.Name,
333+
Description: usageBasedCharge.Intent.Description,
334+
}, nil
335+
case meta.ChargeTypeCreditPurchase:
336+
creditPurchaseCharge, err := charge.AsCreditPurchaseCharge()
337+
if err != nil {
338+
return chargeDisplayMetadata{}, fmt.Errorf("map credit purchase charge: %w", err)
339+
}
340+
341+
return chargeDisplayMetadata{
342+
Name: creditPurchaseCharge.Intent.Name,
343+
Description: creditPurchaseCharge.Intent.Description,
344+
}, nil
345+
default:
346+
return chargeDisplayMetadata{}, fmt.Errorf("unsupported charge type %s", charge.Type())
347+
}
348+
}
349+
350+
func chargeIDFromAnnotations(annotations models.Annotations) string {
351+
raw, ok := annotations[ledger.AnnotationChargeID]
352+
if !ok {
353+
return ""
354+
}
355+
356+
value, ok := raw.(string)
357+
if !ok {
358+
return ""
258359
}
259360

260-
return "credit_transaction"
361+
return value
261362
}

openmeter/ledger/customerbalance/transactions_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
package customerbalance
22

33
import (
4+
"context"
45
"testing"
56
"time"
67

78
"github.com/alpacahq/alpacadecimal"
9+
"github.com/samber/lo"
810
"github.com/stretchr/testify/require"
911

12+
"github.com/openmeterio/openmeter/openmeter/billing/charges"
13+
"github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase"
14+
chargemeta "github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
1015
"github.com/openmeterio/openmeter/openmeter/ledger"
1116
ledgeraccount "github.com/openmeterio/openmeter/openmeter/ledger/account"
1217
ledgerhistorical "github.com/openmeterio/openmeter/openmeter/ledger/historical"
1318
"github.com/openmeterio/openmeter/pkg/currencyx"
1419
"github.com/openmeterio/openmeter/pkg/models"
20+
"github.com/openmeterio/openmeter/pkg/pagination"
1521
)
1622

1723
func TestLedgerCreditMovement_AdjustedReturnsEmpty(t *testing.T) {
@@ -65,6 +71,58 @@ func TestApplyCreditTransactionBalances(t *testing.T) {
6571
require.True(t, items[0].Balance.Before.Equal(alpacadecimal.NewFromInt(52)))
6672
}
6773

74+
func TestApplyChargeMetadataToCreditTransactions(t *testing.T) {
75+
const (
76+
namespace = "ns"
77+
chargeID = "charge-1"
78+
)
79+
80+
description := "Welcome credits"
81+
82+
service := Service{
83+
ChargesService: staticChargeService{
84+
chargesByID: map[string]charges.Charge{
85+
chargeID: charges.NewCharge(creditpurchase.Charge{
86+
ChargeBase: creditpurchase.ChargeBase{
87+
ManagedResource: chargemeta.ManagedResource{
88+
NamespacedModel: models.NamespacedModel{
89+
Namespace: namespace,
90+
},
91+
ID: chargeID,
92+
},
93+
Intent: creditpurchase.Intent{
94+
Intent: chargemeta.Intent{
95+
Name: "Intro Credits",
96+
Description: lo.ToPtr(description),
97+
},
98+
},
99+
},
100+
}),
101+
},
102+
},
103+
}
104+
105+
items := []CreditTransaction{
106+
{
107+
Name: "IssueCustomerReceivableTemplate",
108+
Annotations: models.Annotations{
109+
ledger.AnnotationChargeID: chargeID,
110+
},
111+
},
112+
{
113+
Name: "",
114+
},
115+
}
116+
117+
service.applyChargeMetadataToCreditTransactions(t.Context(), namespace, items)
118+
119+
require.Equal(t, "Intro Credits", items[0].Name)
120+
require.NotNil(t, items[0].Description)
121+
require.Equal(t, description, *items[0].Description)
122+
require.Equal(t, "", items[1].Name)
123+
require.Nil(t, items[1].Description)
124+
}
125+
68126
func mustCustomerFBOAccount(t *testing.T, namespace, id string) *ledgeraccount.CustomerFBOAccount {
69127
t.Helper()
70128

@@ -145,3 +203,25 @@ func mustEntryData(t *testing.T, id string, accountType ledger.AccountType, curr
145203
TransactionID: "tx-1",
146204
}
147205
}
206+
207+
type staticChargeService struct {
208+
chargesByID map[string]charges.Charge
209+
}
210+
211+
func (s staticChargeService) GetByIDs(_ context.Context, input charges.GetByIDsInput) (charges.Charges, error) {
212+
items := make(charges.Charges, 0, len(input.IDs))
213+
for _, id := range input.IDs {
214+
charge, ok := s.chargesByID[id]
215+
if !ok {
216+
continue
217+
}
218+
219+
items = append(items, charge)
220+
}
221+
222+
return items, nil
223+
}
224+
225+
func (s staticChargeService) ListCharges(context.Context, charges.ListChargesInput) (pagination.Result[charges.Charge], error) {
226+
return pagination.Result[charges.Charge]{}, nil
227+
}

0 commit comments

Comments
 (0)