Skip to content

Commit 9156b94

Browse files
committed
refactor: sideloading costbasis for currencies
1 parent 5083276 commit 9156b94

2 files changed

Lines changed: 129 additions & 46 deletions

File tree

openmeter/currencies/adapter/currencies.go

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -168,19 +168,10 @@ func (a *adapter) ListCustomCurrencies(ctx context.Context, params currencies.Li
168168
q = filter.ApplyToQuery(q, params.Code, customcurrency.FieldCode)
169169
}
170170

171-
now := time.Now()
171+
now := clock.Now()
172172

173173
if params.CurrencyExpandOptions.CostBasis {
174-
q = q.WithCostBasisHistory(func(cbq *entdb.CurrencyCostBasisQuery) {
175-
cbq.Where(
176-
currencycostbasis.Namespace(params.Namespace),
177-
currencycostbasis.EffectiveFromLTE(now),
178-
currencycostbasis.Or(
179-
currencycostbasis.EffectiveToIsNil(),
180-
currencycostbasis.EffectiveToGT(now),
181-
),
182-
)
183-
})
174+
q = WithCostBasis(q, now)
184175
}
185176

186177
order := entutils.GetOrdering(sortx.OrderDefault)
@@ -342,7 +333,7 @@ func (a *adapter) GetCurrency(ctx context.Context, params currencies.GetCurrency
342333
return entutils.TransactingRepo(ctx, a, func(ctx context.Context, tx *adapter) (currencies.Currency, error) {
343334
at := clock.Now()
344335

345-
qetQuery := tx.db.CustomCurrency.Query().
336+
q := tx.db.CustomCurrency.Query().
346337
Where(
347338
customcurrency.Namespace(params.Namespace),
348339
customcurrency.ID(params.ID),
@@ -352,7 +343,11 @@ func (a *adapter) GetCurrency(ctx context.Context, params currencies.GetCurrency
352343
),
353344
)
354345

355-
c, err := qetQuery.First(ctx)
346+
if params.CostBasis {
347+
q = WithCostBasis(q, at)
348+
}
349+
350+
c, err := q.First(ctx)
356351
if err != nil {
357352
if entdb.IsNotFound(err) {
358353
return currencies.Currency{}, models.NewGenericNotFoundError(
@@ -368,42 +363,42 @@ func (a *adapter) GetCurrency(ctx context.Context, params currencies.GetCurrency
368363
return currencies.Currency{}, fmt.Errorf("failed to map currency from database: %w", err)
369364
}
370365

371-
if params.CostBasis {
372-
if c.DeletedAt != nil {
373-
at = *c.DeletedAt
374-
}
366+
return curr, nil
367+
})
368+
}
375369

376-
costBasisQuery := tx.db.CurrencyCostBasis.Query().
377-
Where(
378-
currencycostbasis.Namespace(params.Namespace),
379-
currencycostbasis.CurrencyID(params.ID),
380-
currencycostbasis.EffectiveFromLTE(at),
381-
currencycostbasis.Or(
382-
currencycostbasis.EffectiveToIsNil(),
383-
currencycostbasis.EffectiveToGT(at),
370+
func WithCostBasis(q *entdb.CustomCurrencyQuery, at time.Time) *entdb.CustomCurrencyQuery {
371+
return q.WithCostBasisHistory(func(query *entdb.CurrencyCostBasisQuery) {
372+
query.Where(func(s *sql.Selector) {
373+
ct := sql.Table(customcurrency.Table)
374+
375+
s.Join(ct).On(ct.C(customcurrency.FieldID), s.C(currencycostbasis.FieldCurrencyID))
376+
377+
s.Where(
378+
sql.Or(
379+
sql.And(
380+
sql.NotNull(ct.C(customcurrency.FieldDeletedAt)),
381+
sql.ColumnsEQ(s.C(currencycostbasis.FieldDeletedAt), ct.C(currencycostbasis.FieldDeletedAt)),
382+
sql.ColumnsLTE(s.C(currencycostbasis.FieldEffectiveFrom), ct.C(customcurrency.FieldDeletedAt)),
383+
sql.Or(
384+
sql.IsNull(s.C(currencycostbasis.FieldEffectiveTo)),
385+
sql.ColumnsGT(s.C(currencycostbasis.FieldEffectiveTo), ct.C(customcurrency.FieldDeletedAt)),
386+
),
387+
),
388+
sql.And(
389+
sql.IsNull(ct.C(customcurrency.FieldDeletedAt)),
390+
sql.Or(
391+
sql.IsNull(s.C(currencycostbasis.FieldDeletedAt)),
392+
sql.GT(s.C(currencycostbasis.FieldDeletedAt), at),
393+
),
394+
sql.LTE(s.C(currencycostbasis.FieldEffectiveFrom), at),
395+
sql.Or(
396+
sql.IsNull(s.C(currencycostbasis.FieldEffectiveTo)),
397+
sql.GT(s.C(currencycostbasis.FieldEffectiveTo), at),
398+
),
384399
),
385-
)
386-
387-
cbs, err := costBasisQuery.All(ctx)
388-
if err != nil {
389-
if entdb.IsNotFound(err) {
390-
return currencies.Currency{}, models.NewGenericNotFoundError(
391-
fmt.Errorf("currency with id %s not found", params.ID),
392-
)
393-
}
394-
395-
return currencies.Currency{}, fmt.Errorf("failed to get currency: %w", err)
396-
}
397-
398-
curr.CostBasis = lo.ToPtr(
399-
lo.Map[*entdb.CurrencyCostBasis, currencies.CostBasis](cbs,
400-
func(item *entdb.CurrencyCostBasis, _ int) currencies.CostBasis {
401-
return mapCostBasisFromDB(item)
402-
},
403400
),
404401
)
405-
}
406-
407-
return curr, nil
402+
})
408403
})
409404
}

openmeter/currencies/service/service_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,94 @@ func TestCurrenciesService(t *testing.T) {
384384
})
385385
})
386386

387+
t.Run("ListDeletedCurrencyWithCostBasis", func(t *testing.T) {
388+
// given:
389+
// - a deleted custom currency whose cost-basis history contains entries that were expired, active, and future at deletion
390+
deletedNamespace := currenciestestutils.NewTestNamespace(t)
391+
deletedCurrency, err := env.Service.CreateCurrency(t.Context(), currencies.CreateCurrencyInput{
392+
Namespace: deletedNamespace,
393+
CurrencyDetails: currencyx.CurrencyDetails{
394+
Code: "CREDITS",
395+
Name: "Credits",
396+
Symbol: "C",
397+
Precision: 2,
398+
DecimalMark: ".",
399+
ThousandsSeparator: ",",
400+
},
401+
})
402+
require.NoError(t, err)
403+
404+
deletedAt := now.Add(-24 * time.Hour)
405+
costBasisFixtures := []struct {
406+
fiatCode currencyx.Code
407+
effectiveFrom time.Time
408+
effectiveTo *time.Time
409+
active bool
410+
}{
411+
{
412+
fiatCode: "EUR",
413+
effectiveFrom: deletedAt.Add(-48 * time.Hour),
414+
effectiveTo: lo.ToPtr(deletedAt.Add(-time.Hour)),
415+
},
416+
{
417+
fiatCode: "USD",
418+
effectiveFrom: deletedAt.Add(-time.Hour),
419+
effectiveTo: lo.ToPtr(deletedAt.Add(time.Hour)),
420+
active: true,
421+
},
422+
{
423+
fiatCode: "GBP",
424+
effectiveFrom: deletedAt.Add(time.Hour),
425+
},
426+
}
427+
428+
var activeCostBasisID string
429+
for _, fixture := range costBasisFixtures {
430+
costBasis, err := env.Client.CurrencyCostBasis.Create().
431+
SetNamespace(deletedNamespace).
432+
SetCurrencyID(deletedCurrency.ID).
433+
SetFiatCode(fixture.fiatCode).
434+
SetRate(alpacadecimal.RequireFromString("1")).
435+
SetEffectiveFrom(fixture.effectiveFrom).
436+
SetNillableEffectiveTo(fixture.effectiveTo).
437+
SetDeletedAt(deletedAt).
438+
Save(t.Context())
439+
require.NoError(t, err)
440+
441+
if fixture.active {
442+
activeCostBasisID = costBasis.ID
443+
}
444+
}
445+
require.NotEmpty(t, activeCostBasisID)
446+
447+
_, err = env.Client.CustomCurrency.UpdateOneID(deletedCurrency.ID).
448+
SetDeletedAt(deletedAt).
449+
Save(t.Context())
450+
require.NoError(t, err)
451+
452+
// when:
453+
// - the deleted currency is listed with cost-basis data expanded
454+
result, err := env.Service.ListCurrencies(t.Context(), currencies.ListCurrenciesInput{
455+
Page: pagination.NewPage(1, 10),
456+
Namespace: deletedNamespace,
457+
CurrencyType: lo.ToPtr(currencies.CurrencyTypeCustom),
458+
Code: &filter.FilterString{
459+
In: lo.ToPtr([]string{"CREDITS"}),
460+
},
461+
CurrencyExpandOptions: currencies.CurrencyExpandOptions{
462+
CostBasis: true,
463+
},
464+
})
465+
466+
// then:
467+
// - only the cost basis effective at the currency's deletion time is returned
468+
require.NoError(t, err)
469+
require.Len(t, result.Items, 1)
470+
require.NotNil(t, result.Items[0].CostBasis)
471+
require.Len(t, *result.Items[0].CostBasis, 1)
472+
assert.Equal(t, activeCostBasisID, (*result.Items[0].CostBasis)[0].ID)
473+
})
474+
387475
t.Run("List", func(t *testing.T) {
388476
// given:
389477
// - independently persisted custom currencies

0 commit comments

Comments
 (0)