Skip to content

Commit 90e752f

Browse files
committed
fix: properly use feature filters in charge impact calculations
1 parent b0b9d94 commit 90e752f

3 files changed

Lines changed: 108 additions & 12 deletions

File tree

openmeter/ledger/customerbalance/service.go

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ func (s *service) GetBalance(ctx context.Context, input GetBalanceServiceInput)
232232

233233
// Pending balance remains a current projection from open charges.
234234
// Historical cursor/as-of filtering only affects the booked/settled side for now.
235-
impacts, err := s.getChargePendingBalanceImpacts(ctx, input.CustomerID, input.Currency)
235+
impacts, err := s.getChargePendingBalanceImpacts(ctx, input.CustomerID, input.Currency, normalizeFeatureFilter(input.FeatureFilter))
236236
if err != nil {
237237
return nil, fmt.Errorf("get charge pending balance impacts: %w", err)
238238
}
@@ -275,7 +275,7 @@ func (s *service) GetFBOCurrencies(ctx context.Context, customerID customer.Cust
275275
return codes, nil
276276
}
277277

278-
func (s *service) getChargePendingBalanceImpacts(ctx context.Context, customerID customer.CustomerID, currency currencyx.Code) ([]Impact, error) {
278+
func (s *service) getChargePendingBalanceImpacts(ctx context.Context, customerID customer.CustomerID, currency currencyx.Code, featureFilter mo.Option[creditpurchase.FeatureFilters]) ([]Impact, error) {
279279
items, err := pagination.CollectAll(
280280
ctx,
281281
pagination.NewPaginator(func(ctx context.Context, page pagination.Page) (pagination.Result[charges.Charge], error) {
@@ -299,7 +299,7 @@ func (s *service) getChargePendingBalanceImpacts(ctx context.Context, customerID
299299

300300
impacts := make([]Impact, 0, len(items))
301301
for _, charge := range items {
302-
impact, err := s.getChargePendingBalanceImpact(ctx, charge, currency)
302+
impact, err := s.getChargePendingBalanceImpact(ctx, charge, currency, featureFilter)
303303
if err != nil {
304304
return nil, err
305305
}
@@ -314,22 +314,22 @@ func (s *service) getChargePendingBalanceImpacts(ctx context.Context, customerID
314314
return impacts, nil
315315
}
316316

317-
func (s *service) getChargePendingBalanceImpact(ctx context.Context, charge charges.Charge, currency currencyx.Code) (*Impact, error) {
317+
func (s *service) getChargePendingBalanceImpact(ctx context.Context, charge charges.Charge, currency currencyx.Code, featureFilter mo.Option[creditpurchase.FeatureFilters]) (*Impact, error) {
318318
if !chargeHasStarted(charge) {
319319
return nil, nil
320320
}
321321

322322
switch charge.Type() {
323323
case meta.ChargeTypeFlatFee:
324-
return getFlatFeeChargePendingBalanceImpact(charge, currency)
324+
return getFlatFeeChargePendingBalanceImpact(charge, currency, featureFilter)
325325
case meta.ChargeTypeUsageBased:
326-
return s.getUsageBasedChargePendingBalanceImpact(ctx, charge, currency)
326+
return s.getUsageBasedChargePendingBalanceImpact(ctx, charge, currency, featureFilter)
327327
default:
328328
return nil, nil
329329
}
330330
}
331331

332-
func getFlatFeeChargePendingBalanceImpact(charge charges.Charge, currency currencyx.Code) (*Impact, error) {
332+
func getFlatFeeChargePendingBalanceImpact(charge charges.Charge, currency currencyx.Code, featureFilter mo.Option[creditpurchase.FeatureFilters]) (*Impact, error) {
333333
flatFeeCharge, err := charge.AsFlatFeeCharge()
334334
if err != nil {
335335
return nil, fmt.Errorf("map flat fee charge: %w", err)
@@ -339,10 +339,14 @@ func getFlatFeeChargePendingBalanceImpact(charge charges.Charge, currency curren
339339
return nil, nil
340340
}
341341

342+
if !featureFilterMatchesChargeFeatureKey(featureFilter, flatFeeCharge.Intent.FeatureKey) {
343+
return nil, nil
344+
}
345+
342346
return newImpactOrNil(charge, flatFeeCharge.State.AmountAfterProration)
343347
}
344348

345-
func (s *service) getUsageBasedChargePendingBalanceImpact(ctx context.Context, charge charges.Charge, currency currencyx.Code) (*Impact, error) {
349+
func (s *service) getUsageBasedChargePendingBalanceImpact(ctx context.Context, charge charges.Charge, currency currencyx.Code, featureFilter mo.Option[creditpurchase.FeatureFilters]) (*Impact, error) {
346350
usageBasedCharge, err := charge.AsUsageBasedCharge()
347351
if err != nil {
348352
return nil, fmt.Errorf("map usage based charge: %w", err)
@@ -352,6 +356,10 @@ func (s *service) getUsageBasedChargePendingBalanceImpact(ctx context.Context, c
352356
return nil, nil
353357
}
354358

359+
if !featureFilterMatchesChargeFeatureKey(featureFilter, usageBasedCharge.Intent.FeatureKey) {
360+
return nil, nil
361+
}
362+
355363
currentTotals, err := s.UsageBasedService.GetCurrentTotals(ctx, usagebased.GetCurrentTotalsInput{
356364
ChargeID: usageBasedCharge.GetChargeID(),
357365
})
@@ -362,6 +370,23 @@ func (s *service) getUsageBasedChargePendingBalanceImpact(ctx context.Context, c
362370
return newImpactOrNil(charges.NewCharge(currentTotals.Charge), currentTotals.DueTotals.Total)
363371
}
364372

373+
func featureFilterMatchesChargeFeatureKey(featureFilter mo.Option[creditpurchase.FeatureFilters], featureKey string) bool {
374+
if featureFilter.IsAbsent() {
375+
return true
376+
}
377+
378+
features := featureFilter.OrEmpty()
379+
if features == nil {
380+
return featureKey == ""
381+
}
382+
383+
if featureKey == "" {
384+
return true
385+
}
386+
387+
return len(features) == 1 && features[0] == featureKey
388+
}
389+
365390
func chargeHasStarted(charge charges.Charge) bool {
366391
now := clock.Now()
367392

openmeter/ledger/customerbalance/service_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,65 @@ func TestGetBalanceFeatureFilter(t *testing.T) {
460460
require.NoError(t, err)
461461
require.Equal(t, float64(10), exactFeatureABalance.Settled().InexactFloat64())
462462
}
463+
464+
func TestGetBalanceFeatureFilterPendingChargeImpacts(t *testing.T) {
465+
env := newTestEnv(t)
466+
467+
env.bookFBOBalanceWithFeatures(t, alpacadecimal.NewFromInt(100), nil)
468+
env.fundOpenReceivableWithFeatures(t, alpacadecimal.NewFromInt(100), nil)
469+
env.bookFBOBalanceWithFeatures(t, alpacadecimal.NewFromInt(10), []string{testFeatureKey})
470+
env.fundOpenReceivableWithFeatures(t, alpacadecimal.NewFromInt(10), []string{testFeatureKey})
471+
env.bookFBOBalanceWithFeatures(t, alpacadecimal.NewFromInt(20), []string{"storage"})
472+
env.fundOpenReceivableWithFeatures(t, alpacadecimal.NewFromInt(20), []string{"storage"})
473+
474+
env.addUsage(30, clock.Now().Add(-30*time.Minute))
475+
env.createFlatFeeCharge(t, alpacadecimal.NewFromInt(5), productcatalog.CreditOnlySettlementMode, env.sp())
476+
env.createFlatFeeCharge(t, alpacadecimal.NewFromInt(7), productcatalog.CreditOnlySettlementMode, env.sp(), testFeatureKey)
477+
env.createUsageBasedCharge(t, alpacadecimal.NewFromInt(1), productcatalog.CreditOnlySettlementMode, env.sp())
478+
479+
tests := []struct {
480+
name string
481+
filter mo.Option[creditpurchase.FeatureFilters]
482+
wantSettled float64
483+
wantPending float64
484+
}{
485+
{
486+
name: "omitted filter includes every charge impact",
487+
filter: AllFeatureFilter(),
488+
wantSettled: 130,
489+
wantPending: 88,
490+
},
491+
{
492+
name: "unrestricted filter includes only unrestricted charge impacts",
493+
filter: NewUnrestrictedFeatureFilter(),
494+
wantSettled: 100,
495+
wantPending: 95,
496+
},
497+
{
498+
name: "matching feature filter includes unrestricted and matching charge impacts",
499+
filter: NewFeatureFilter([]string{testFeatureKey}),
500+
wantSettled: 110,
501+
wantPending: 68,
502+
},
503+
{
504+
name: "non-matching feature filter excludes restricted charge impacts for other features",
505+
filter: NewFeatureFilter([]string{"storage"}),
506+
wantSettled: 120,
507+
wantPending: 115,
508+
},
509+
}
510+
511+
for _, tt := range tests {
512+
t.Run(tt.name, func(t *testing.T) {
513+
balance, err := env.Service.GetBalance(t.Context(), GetBalanceServiceInput{
514+
CustomerID: env.CustomerID,
515+
Currency: env.Currency,
516+
FeatureFilter: tt.filter,
517+
})
518+
require.NoError(t, err)
519+
520+
require.Equal(t, tt.wantSettled, balance.Settled().InexactFloat64())
521+
require.Equal(t, tt.wantPending, balance.Pending().InexactFloat64())
522+
})
523+
}
524+
}

openmeter/ledger/customerbalance/testenv_test.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -447,15 +447,23 @@ func (e *testEnv) createUsageBasedChargeInCurrency(t *testing.T, unitPrice alpac
447447
return createdCharges[0].Charge
448448
}
449449

450-
func (e *testEnv) createFlatFeeCharge(t *testing.T, amount alpacadecimal.Decimal, settlementMode productcatalog.SettlementMode, servicePeriod timeutil.ClosedPeriod) flatfee.Charge {
451-
return e.createFlatFeeChargeInCurrency(t, amount, settlementMode, servicePeriod, e.Currency)
450+
func (e *testEnv) createFlatFeeCharge(t *testing.T, amount alpacadecimal.Decimal, settlementMode productcatalog.SettlementMode, servicePeriod timeutil.ClosedPeriod, featureKeys ...string) flatfee.Charge {
451+
return e.createFlatFeeChargeInCurrency(t, amount, settlementMode, servicePeriod, e.Currency, featureKeys...)
452452
}
453453

454-
func (e *testEnv) createFlatFeeChargeInCurrency(t *testing.T, amount alpacadecimal.Decimal, settlementMode productcatalog.SettlementMode, servicePeriod timeutil.ClosedPeriod, currency currencyx.Code) flatfee.Charge {
454+
func (e *testEnv) createFlatFeeChargeInCurrency(t *testing.T, amount alpacadecimal.Decimal, settlementMode productcatalog.SettlementMode, servicePeriod timeutil.ClosedPeriod, currency currencyx.Code, featureKeys ...string) flatfee.Charge {
455455
t.Helper()
456456

457+
require.LessOrEqual(t, len(featureKeys), 1)
458+
459+
featureKey := ""
460+
if len(featureKeys) == 1 {
461+
featureKey = featureKeys[0]
462+
}
463+
457464
createdCharges, err := e.flatFeeService.Create(t.Context(), flatfee.CreateInput{
458-
Namespace: e.Namespace,
465+
Namespace: e.Namespace,
466+
FeatureMeters: e.featureMeters,
459467
Intents: []flatfee.Intent{
460468
{
461469
Intent: chargemeta.Intent{
@@ -470,6 +478,7 @@ func (e *testEnv) createFlatFeeChargeInCurrency(t *testing.T, amount alpacadecim
470478
InvoiceAt: e.Now().Add(-time.Minute),
471479
SettlementMode: settlementMode,
472480
PaymentTerm: productcatalog.InAdvancePaymentTerm,
481+
FeatureKey: featureKey,
473482
AmountBeforeProration: amount,
474483
},
475484
},

0 commit comments

Comments
 (0)