Skip to content

Commit 6d456fb

Browse files
authored
refactor: billing entity diffing (#3233)
1 parent 38cd5f3 commit 6d456fb

File tree

12 files changed

+639
-715
lines changed

12 files changed

+639
-715
lines changed

openmeter/billing/adapter/invoicelinediff.go

Lines changed: 191 additions & 414 deletions
Large diffs are not rendered by default.

openmeter/billing/adapter/invoicelinediff_test.go

Lines changed: 83 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,26 @@ import (
1010

1111
"github.com/openmeterio/openmeter/openmeter/billing"
1212
"github.com/openmeterio/openmeter/pkg/clock"
13+
"github.com/openmeterio/openmeter/pkg/entitydiff"
1314
"github.com/openmeterio/openmeter/pkg/models"
1415
)
1516

16-
type idDiff = diff[string]
17+
type idDiff struct {
18+
ToCreate []string
19+
ToUpdate []string
20+
ToDelete []string
21+
}
1722

1823
type lineDiffExpectation struct {
19-
LineBase idDiff
20-
FlatFee idDiff
21-
UsageBased idDiff
24+
Line idDiff
2225

2326
AmountDiscounts idDiff
2427

25-
AffectedLineIDs []string
26-
ChildrenDiff *lineDiffExpectation
28+
DetailedLine idDiff
29+
DetailedLineAmountDiscounts idDiff
30+
31+
AffectedLineIDs []string
32+
DetailedLineAffectedLineIDs []string
2733
}
2834

2935
func TestInvoiceLineDiffing(t *testing.T) {
@@ -33,9 +39,9 @@ func TestInvoiceLineDiffing(t *testing.T) {
3339
ManagedResource: models.NewManagedResource(models.ManagedResourceInput{
3440
ID: "1",
3541
}),
36-
Type: billing.InvoiceLineTypeFee,
42+
Type: billing.InvoiceLineTypeUsageBased,
3743
},
38-
FlatFee: &billing.FlatFeeLine{},
44+
UsageBased: &billing.UsageBasedLine{},
3945
},
4046
{
4147
LineBase: billing.LineBase{
@@ -76,25 +82,14 @@ func TestInvoiceLineDiffing(t *testing.T) {
7682
require.NoError(t, err)
7783

7884
requireDiff(t, lineDiffExpectation{
79-
LineBase: idDiff{
85+
Line: idDiff{
8086
ToCreate: []string{"1", "2"},
8187
},
82-
FlatFee: idDiff{
83-
ToCreate: []string{"1"},
88+
DetailedLine: idDiff{
89+
ToCreate: []string{"2.1", "2.2"},
8490
},
85-
UsageBased: idDiff{
86-
ToCreate: []string{"2"},
87-
},
88-
ChildrenDiff: &lineDiffExpectation{
89-
LineBase: idDiff{
90-
ToCreate: []string{"2.1", "2.2"},
91-
},
92-
FlatFee: idDiff{
93-
ToCreate: []string{"2.1", "2.2"},
94-
},
95-
AmountDiscounts: idDiff{
96-
ToCreate: []string{"D2.1.1"},
97-
},
91+
DetailedLineAmountDiscounts: idDiff{
92+
ToCreate: []string{"D2.1.1"},
9893
},
9994
}, lineDiff)
10095
})
@@ -120,16 +115,11 @@ func TestInvoiceLineDiffing(t *testing.T) {
120115

121116
requireDiff(t, lineDiffExpectation{
122117
AffectedLineIDs: []string{"2"},
123-
ChildrenDiff: &lineDiffExpectation{
124-
LineBase: idDiff{
125-
ToDelete: []string{"2.1"},
126-
},
127-
// Flat fee is not deleted as it does not have soft delete, so it's enough to mark the line as deleted
128-
FlatFee: idDiff{},
129-
AmountDiscounts: idDiff{
130-
// Discounts also get deleted
131-
ToDelete: []string{"D2.1.1"},
132-
},
118+
DetailedLine: idDiff{
119+
ToDelete: []string{"2.1"},
120+
},
121+
DetailedLineAmountDiscounts: idDiff{
122+
ToDelete: []string{"D2.1.1"},
133123
},
134124
}, lineDiff)
135125
})
@@ -145,13 +135,8 @@ func TestInvoiceLineDiffing(t *testing.T) {
145135

146136
requireDiff(t, lineDiffExpectation{
147137
AffectedLineIDs: []string{"2"},
148-
ChildrenDiff: &lineDiffExpectation{
149-
LineBase: idDiff{
150-
ToUpdate: []string{"2.1"},
151-
},
152-
FlatFee: idDiff{
153-
ToUpdate: []string{"2.1"},
154-
},
138+
DetailedLine: idDiff{
139+
ToUpdate: []string{"2.1"},
155140
},
156141
}, lineDiff)
157142
})
@@ -166,10 +151,7 @@ func TestInvoiceLineDiffing(t *testing.T) {
166151
require.NoError(t, err)
167152

168153
requireDiff(t, lineDiffExpectation{
169-
LineBase: idDiff{
170-
ToUpdate: []string{"2"},
171-
},
172-
UsageBased: idDiff{
154+
Line: idDiff{
173155
ToUpdate: []string{"2"},
174156
},
175157
}, lineDiff)
@@ -191,19 +173,14 @@ func TestInvoiceLineDiffing(t *testing.T) {
191173

192174
requireDiff(t, lineDiffExpectation{
193175
AffectedLineIDs: []string{"2"},
194-
ChildrenDiff: &lineDiffExpectation{
195-
LineBase: idDiff{
196-
ToDelete: []string{"2.1"},
197-
ToCreate: []string{"2.3"},
198-
},
199-
FlatFee: idDiff{
200-
ToCreate: []string{"2.3"},
201-
},
202-
AmountDiscounts: idDiff{
203-
// The discount gets deleted + created
204-
ToCreate: []string{"D2.1.3"},
205-
ToDelete: []string{"D2.1.1"},
206-
},
176+
DetailedLine: idDiff{
177+
ToDelete: []string{"2.1"},
178+
ToCreate: []string{"2.3"},
179+
},
180+
DetailedLineAmountDiscounts: idDiff{
181+
// The discount gets deleted + created
182+
ToCreate: []string{"D2.1.3"},
183+
ToDelete: []string{"D2.1.1"},
207184
},
208185
}, lineDiff)
209186
})
@@ -219,11 +196,10 @@ func TestInvoiceLineDiffing(t *testing.T) {
219196
require.NoError(t, err)
220197

221198
requireDiff(t, lineDiffExpectation{
222-
AffectedLineIDs: []string{"2", "2.1"},
223-
ChildrenDiff: &lineDiffExpectation{
224-
AmountDiscounts: idDiff{
225-
ToDelete: []string{"D2.1.1"},
226-
},
199+
AffectedLineIDs: []string{"2"},
200+
DetailedLineAffectedLineIDs: []string{"2.1"},
201+
DetailedLineAmountDiscounts: idDiff{
202+
ToDelete: []string{"D2.1.1"},
227203
},
228204
}, lineDiff)
229205
})
@@ -238,11 +214,10 @@ func TestInvoiceLineDiffing(t *testing.T) {
238214
require.NoError(t, err)
239215

240216
requireDiff(t, lineDiffExpectation{
241-
AffectedLineIDs: []string{"2", "2.1"},
242-
ChildrenDiff: &lineDiffExpectation{
243-
AmountDiscounts: idDiff{
244-
ToUpdate: []string{"D2.1.1"},
245-
},
217+
AffectedLineIDs: []string{"2"},
218+
DetailedLineAffectedLineIDs: []string{"2.1"},
219+
DetailedLineAmountDiscounts: idDiff{
220+
ToUpdate: []string{"D2.1.1"},
246221
},
247222
}, lineDiff)
248223
})
@@ -260,12 +235,11 @@ func TestInvoiceLineDiffing(t *testing.T) {
260235
require.NoError(t, err)
261236

262237
requireDiff(t, lineDiffExpectation{
263-
AffectedLineIDs: []string{"2", "2.1"},
264-
ChildrenDiff: &lineDiffExpectation{
265-
AmountDiscounts: idDiff{
266-
ToCreate: []string{"D2.1.2"},
267-
ToDelete: []string{"D2.1.1"},
268-
},
238+
AffectedLineIDs: []string{"2"},
239+
DetailedLineAffectedLineIDs: []string{"2.1"},
240+
DetailedLineAmountDiscounts: idDiff{
241+
ToCreate: []string{"D2.1.2"},
242+
ToDelete: []string{"D2.1.1"},
269243
},
270244
}, lineDiff)
271245
})
@@ -282,13 +256,11 @@ func TestInvoiceLineDiffing(t *testing.T) {
282256

283257
requireDiff(t, lineDiffExpectation{
284258
AffectedLineIDs: []string{"2"},
285-
ChildrenDiff: &lineDiffExpectation{
286-
LineBase: idDiff{
287-
ToDelete: []string{"2.1"},
288-
},
289-
AmountDiscounts: idDiff{
290-
ToDelete: []string{"D2.1.1"},
291-
},
259+
DetailedLine: idDiff{
260+
ToDelete: []string{"2.1"},
261+
},
262+
DetailedLineAmountDiscounts: idDiff{
263+
ToDelete: []string{"D2.1.1"},
292264
},
293265
}, lineDiff)
294266
})
@@ -303,16 +275,14 @@ func TestInvoiceLineDiffing(t *testing.T) {
303275
require.NoError(t, err)
304276

305277
requireDiff(t, lineDiffExpectation{
306-
LineBase: idDiff{
278+
Line: idDiff{
307279
ToDelete: []string{"2"},
308280
},
309-
ChildrenDiff: &lineDiffExpectation{
310-
LineBase: idDiff{
311-
ToDelete: []string{"2.1", "2.2"},
312-
},
313-
AmountDiscounts: idDiff{
314-
ToDelete: []string{"D2.1.1"},
315-
},
281+
DetailedLine: idDiff{
282+
ToDelete: []string{"2.1", "2.2"},
283+
},
284+
DetailedLineAmountDiscounts: idDiff{
285+
ToDelete: []string{"D2.1.1"},
316286
},
317287
}, lineDiff)
318288
})
@@ -327,7 +297,7 @@ func TestInvoiceLineDiffing(t *testing.T) {
327297
require.NoError(t, err)
328298

329299
requireDiff(t, lineDiffExpectation{
330-
LineBase: idDiff{
300+
Line: idDiff{
331301
ToDelete: []string{"1"},
332302
},
333303
}, lineDiff)
@@ -344,66 +314,19 @@ func TestInvoiceLineDiffing(t *testing.T) {
344314

345315
requireDiff(t, lineDiffExpectation{}, lineDiff)
346316
})
347-
348-
t.Run("deleted, changed lines are not triggering updates", func(t *testing.T) {
349-
base := cloneLines(template)
350-
base[1].DeletedAt = lo.ToPtr(clock.Now())
351-
base[1].Children.GetByID("2.1").DeletedAt = lo.ToPtr(clock.Now())
352-
353-
snapshotAsDBState(base)
354-
base[1].DeletedAt = nil
355-
base[1].Children.GetByID("2.1").DeletedAt = nil
356-
357-
lineDiff, err := diffInvoiceLines(base)
358-
require.NoError(t, err)
359-
360-
requireDiff(t, lineDiffExpectation{
361-
LineBase: idDiff{
362-
ToUpdate: []string{"2"},
363-
},
364-
ChildrenDiff: &lineDiffExpectation{
365-
LineBase: idDiff{
366-
ToUpdate: []string{"2.1"},
367-
},
368-
},
369-
}, lineDiff)
370-
})
371-
}
372-
373-
func mapLinesToIDs(lines []*billing.Line) []string {
374-
return lo.Map(lines, func(line *billing.Line, _ int) string {
375-
// Use description as ID if it's set, so that we can predict the new line's ID for new
376-
// line testcases
377-
if line.Description != nil {
378-
return *line.Description
379-
}
380-
return line.ID
381-
})
382-
}
383-
384-
func mapLineDiffToIDs(in diff[*billing.Line]) idDiff {
385-
return idDiff{
386-
ToCreate: mapLinesToIDs(in.ToCreate),
387-
ToUpdate: mapLinesToIDs(in.ToUpdate),
388-
ToDelete: mapLinesToIDs(in.ToDelete),
389-
}
390-
}
391-
392-
func mapLineDiscountsToIDs(t *testing.T, discounts []withParent[billing.AmountLineDiscountManaged, *billing.Line]) []string {
393-
return lo.Map(discounts, func(d withParent[billing.AmountLineDiscountManaged, *billing.Line], _ int) string {
394-
if d.Entity.Description != nil {
395-
return *d.Entity.Description
396-
}
397-
398-
return d.Entity.GetID()
399-
})
400317
}
401318

402-
func mapLineDiscountDiffToIDs(t *testing.T, in diff[withParent[billing.AmountLineDiscountManaged, *billing.Line]]) idDiff {
319+
func mapDiffToIDs[T entitydiff.Entity](in entitydiff.Diff[T], getDescription func(T) *string) idDiff {
403320
return idDiff{
404-
ToCreate: mapLineDiscountsToIDs(t, in.ToCreate),
405-
ToUpdate: mapLineDiscountsToIDs(t, in.ToUpdate),
406-
ToDelete: mapLineDiscountsToIDs(t, in.ToDelete),
321+
ToCreate: lo.Map(in.Create, func(item T, _ int) string {
322+
return lo.FromPtrOr(getDescription(item), item.GetID())
323+
}),
324+
ToUpdate: lo.Map(in.Update, func(item entitydiff.DiffUpdate[T], _ int) string {
325+
return lo.FromPtrOr(getDescription(item.PersistedState), item.PersistedState.GetID())
326+
}),
327+
ToDelete: lo.Map(in.Delete, func(item T, _ int) string {
328+
return lo.FromPtrOr(getDescription(item), item.GetID())
329+
}),
407330
}
408331
}
409332

@@ -420,35 +343,27 @@ func msgPrefix(prefix string, in ...interface{}) []interface{} {
420343
return in
421344
}
422345

423-
func requireIdDiffMatches(t *testing.T, a, b idDiff, msgAndArgs ...interface{}) {
424-
t.Helper()
425-
426-
require.ElementsMatch(t, a.ToCreate, b.ToCreate, msgPrefix("ToCreate", msgAndArgs...))
427-
require.ElementsMatch(t, a.ToUpdate, b.ToUpdate, msgPrefix("ToUpdate", msgAndArgs...))
428-
require.ElementsMatch(t, a.ToDelete, b.ToDelete, msgPrefix("ToDelete", msgAndArgs...))
429-
}
430-
431-
func requireDiffWithoutChildren(t *testing.T, expected lineDiffExpectation, actual *invoiceLineDiff, prefix string) {
346+
func requireIdDiffMatches[T entitydiff.Entity](t *testing.T, a idDiff, b entitydiff.Diff[T], getDescription func(T) *string, msgAndArgs ...interface{}) {
432347
t.Helper()
433348

434-
requireIdDiffMatches(t, expected.LineBase, mapLineDiffToIDs(actual.LineBase), prefix+": LineBase")
435-
requireIdDiffMatches(t, expected.FlatFee, mapLineDiffToIDs(actual.FlatFee), prefix+": FlatFee")
436-
requireIdDiffMatches(t, expected.UsageBased, mapLineDiffToIDs(actual.UsageBased), prefix+": UsageBased")
349+
idDiffB := mapDiffToIDs(b, getDescription)
437350

438-
requireIdDiffMatches(t, expected.AmountDiscounts, mapLineDiscountDiffToIDs(t, actual.AmountDiscounts), prefix+": AmountDiscounts")
351+
require.ElementsMatch(t, a.ToCreate, idDiffB.ToCreate, msgPrefix("ToCreate", msgAndArgs...))
352+
require.ElementsMatch(t, a.ToUpdate, idDiffB.ToUpdate, msgPrefix("ToUpdate", msgAndArgs...))
353+
require.ElementsMatch(t, a.ToDelete, idDiffB.ToDelete, msgPrefix("ToDelete", msgAndArgs...))
439354
}
440355

441-
func requireDiff(t *testing.T, expected lineDiffExpectation, actual *invoiceLineDiff) {
356+
func requireDiff(t *testing.T, expected lineDiffExpectation, actual invoiceLineDiff) {
442357
t.Helper()
443-
requireDiffWithoutChildren(t, expected, actual, "root diff")
444-
require.ElementsMatch(t, expected.AffectedLineIDs, actual.AffectedLineIDs.AsSlice(), "affectedLineIDs")
445358

446-
childrenExpectation := expected.ChildrenDiff
447-
if childrenExpectation == nil {
448-
childrenExpectation = &lineDiffExpectation{}
449-
}
359+
requireIdDiffMatches(t, expected.Line, actual.Line, func(line *billing.Line) *string { return line.GetDescription() }, "line diff")
360+
requireIdDiffMatches(t, expected.AmountDiscounts, actual.AmountDiscounts, func(discount amountLineDiscountManagedWithLine) *string { return discount.Entity.Description }, "amount discounts")
361+
362+
requireIdDiffMatches(t, expected.DetailedLine, actual.DetailedLine, func(line detailedLineWithParent) *string { return line.Entity.GetDescription() }, "detailed line diff")
363+
requireIdDiffMatches(t, expected.DetailedLineAmountDiscounts, actual.DetailedLineAmountDiscounts, func(discount amountLineDiscountManagedWithLine) *string { return discount.Entity.Description }, "detailed line amount discounts")
450364

451-
requireDiffWithoutChildren(t, *childrenExpectation, actual.ChildrenDiff, "children diff")
365+
require.ElementsMatch(t, expected.AffectedLineIDs, actual.AffectedLineIDs.AsSlice(), "affected line IDs")
366+
require.ElementsMatch(t, expected.DetailedLineAffectedLineIDs, actual.DetailedLineAffectedLineIDs.AsSlice(), "detailed line affected line IDs")
452367
}
453368

454369
func cloneLines(lines []*billing.Line) []*billing.Line {

0 commit comments

Comments
 (0)