Skip to content

Commit 9b4d970

Browse files
committed
refactor: disambiguate line children
LineChildren type was used for two reasons: - Collection of invoice lines (with the option of being expanded) - Collection of detailed lines (expand does not make sense) This patch makes sure that we have seperate types as perparation for the introduction of specific detailed line types.
1 parent 0805745 commit 9b4d970

File tree

20 files changed

+212
-205
lines changed

20 files changed

+212
-205
lines changed

openmeter/billing/adapter/invoice.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ func (a *adapter) UpdateInvoice(ctx context.Context, in billing.UpdateInvoiceAda
552552
return in, err
553553
}
554554

555-
updatedLines := billing.LineChildren{}
555+
updatedLines := billing.InvoiceLines{}
556556
if in.Lines.IsPresent() {
557557
// Note: this only supports adding new lines or setting the DeletedAt field
558558
// we don't support moving lines between invoices here, as the cross invoice
@@ -567,14 +567,14 @@ func (a *adapter) UpdateInvoice(ctx context.Context, in billing.UpdateInvoiceAda
567567
return in, err
568568
}
569569

570-
updatedLines = billing.NewLineChildren(lines)
570+
updatedLines = billing.NewInvoiceLines(lines)
571571
}
572572

573573
// Let's return the updated invoice
574574
if !in.ExpandedFields.DeletedLines && updatedLines.IsPresent() {
575575
// If we haven't requested deleted lines, let's filter them out, as if there were lines marked deleted
576576
// the adapter update would return them as well.
577-
updatedLines = billing.NewLineChildren(
577+
updatedLines = billing.NewInvoiceLines(
578578
lo.Filter(updatedLines.OrEmpty(), func(line *billing.Line, _ int) bool {
579579
return line.DeletedAt == nil
580580
}),
@@ -761,7 +761,7 @@ func (a *adapter) mapInvoiceFromDB(ctx context.Context, invoice *db.BillingInvoi
761761
return billing.Invoice{}, err
762762
}
763763

764-
res.Lines = billing.NewLineChildren(mappedLines)
764+
res.Lines = billing.NewInvoiceLines(mappedLines)
765765
}
766766

767767
if len(invoice.Edges.BillingInvoiceValidationIssues) > 0 {

openmeter/billing/adapter/invoicelinediff.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ func diffInvoiceLines(lines []*billing.Line) (*invoiceLineDiff, error) {
179179
}
180180

181181
// Any child of a new item is also new => let's create them
182-
for _, child := range workItem.Children.OrEmpty() {
182+
for _, child := range workItem.Children {
183183
diff.ChildrenDiff.LineBase.NeedsCreate(child)
184184
switch child.Type {
185185
case billing.InvoiceLineTypeFee:
@@ -197,13 +197,13 @@ func diffInvoiceLines(lines []*billing.Line) (*invoiceLineDiff, error) {
197197
// Let's figure out what we need to do about child lines
198198
for _, childUpdate := range childUpdates {
199199
// If the children are not present, we don't need to do anything (a.k.a. do not touch)
200-
if !childUpdate.Children.IsPresent() {
200+
if len(childUpdate.Children) == 0 {
201201
continue
202202
}
203203

204204
if err := getChildrenActions(
205-
childUpdate.DBState.Children.OrEmpty(),
206-
childUpdate.Children.OrEmpty(),
205+
childUpdate.DBState.Children,
206+
childUpdate.Children,
207207
diff.ChildrenDiff,
208208
); err != nil {
209209
outErr = errors.Join(outErr, err)
@@ -255,11 +255,9 @@ func diffLineBaseEntities(line *billing.Line, out *invoiceLineDiff) error {
255255
out.LineBase.NeedsDelete(line)
256256
out.AffectedLineIDs.Add(getParentIDAsSlice(line)...)
257257

258-
if line.Children.IsPresent() {
259-
// We need to delete the children as well
260-
if err := deleteLineChildren(line, out); err != nil {
261-
return err
262-
}
258+
// We need to delete the children as well
259+
if err := deleteLineChildren(line, out); err != nil {
260+
return err
263261
}
264262

265263
if err := handleLineDependantEntities(line, operationDelete, out); err != nil {
@@ -363,7 +361,7 @@ func getChildrenActions(dbSave []*billing.Line, current []*billing.Line, out *in
363361
}
364362

365363
func deleteLineChildren(line *billing.Line, out *invoiceLineDiff) error {
366-
for _, child := range line.DBState.Children.OrEmpty() {
364+
for _, child := range line.DBState.Children {
367365
out.ChildrenDiff.LineBase.NeedsDelete(child)
368366

369367
if err := handleLineDependantEntities(child, operationDelete, out.ChildrenDiff); err != nil {

openmeter/billing/adapter/invoicelinediff_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ func cloneLines(lines []*billing.Line) []*billing.Line {
461461

462462
func fixParentReferences(lines []*billing.Line) []*billing.Line {
463463
for _, line := range lines {
464-
for _, child := range line.Children.OrEmpty() {
464+
for _, child := range line.Children {
465465
child.ParentLineID = lo.ToPtr(line.ID)
466466
child.ParentLine = line
467467
}

openmeter/billing/adapter/invoicelinemapper.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (a *adapter) mapInvoiceLineFromDB(ctx context.Context, in mapInvoiceLineFro
7373
entity.ParentLine = parent
7474
// We only add children references if we know that those has been properly resolved
7575
if _, ok := resolvedChildrenOfIDs[parent.ID]; ok {
76-
parent.Children.Append(entity)
76+
parent.Children = append(parent.Children, entity)
7777
}
7878
}
7979
}
@@ -85,13 +85,6 @@ func (a *adapter) mapInvoiceLineFromDB(ctx context.Context, in mapInvoiceLineFro
8585
return nil, fmt.Errorf("missing entity[%s]", dbEntity.ID)
8686
}
8787

88-
// Given that we did the one level deep resolution, all the children should be resolved
89-
// if it's not present it means that the line has no children, but before we only rely on Append
90-
// to set the Present flag implicitly.
91-
if !entity.Children.IsPresent() {
92-
entity.Children = billing.NewLineChildren(nil)
93-
}
94-
9588
entity.SaveDBSnapshot()
9689

9790
result = append(result, entity)

openmeter/billing/adapter/invoicelines.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,13 @@ func (a *adapter) UpsertInvoiceLines(ctx context.Context, inputIn billing.Upsert
156156

157157
// Step 3: Let's create the detailed lines
158158
flattenedDetailedLines := lo.FlatMap(input.Lines, func(_ *billing.Line, idx int) []*billing.Line {
159-
return input.Lines[idx].Children.OrEmpty()
159+
return input.Lines[idx].Children
160160
})
161161

162162
if len(flattenedDetailedLines) > 0 {
163163
// Let's restore the parent <-> child relationship in terms of the ParentLineID field
164164
for _, line := range input.Lines {
165-
for _, child := range line.Children.OrEmpty() {
165+
for _, child := range line.Children {
166166
child.ParentLineID = lo.ToPtr(line.ID)
167167
}
168168
}

openmeter/billing/httpdriver/invoice.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ func (h *handler) SimulateInvoice() SimulateInvoiceHandler {
383383

384384
Number: body.Number,
385385
Currency: currencyx.Code(body.Currency),
386-
Lines: billing.NewLineChildren(lines),
386+
Lines: billing.NewInvoiceLines(lines),
387387
}, nil
388388
},
389389
func(ctx context.Context, request SimulateInvoiceRequest) (SimulateInvoiceResponse, error) {

openmeter/billing/httpdriver/invoiceline.go

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,7 @@ func mapTaxConfigToAPI(to *productcatalog.TaxConfig) *api.TaxConfig {
183183
return lo.ToPtr(productcataloghttp.FromTaxConfig(*to))
184184
}
185185

186-
func mapDetailedLinesToAPI(optChildren billing.LineChildren) (*[]api.InvoiceDetailedLine, error) {
187-
if optChildren.IsAbsent() {
188-
return nil, nil
189-
}
190-
191-
children := optChildren.OrEmpty()
192-
186+
func mapDetailedLinesToAPI(children billing.LineChildren) (*[]api.InvoiceDetailedLine, error) {
193187
out := make([]api.InvoiceDetailedLine, 0, len(children))
194188

195189
for _, child := range children {
@@ -697,7 +691,7 @@ func mergeLineFromInvoiceLineReplaceUpdate(existing *billing.Line, line api.Invo
697691
return existing, wasChange, nil
698692
}
699693

700-
func (h *handler) mergeInvoiceLinesFromAPI(ctx context.Context, invoice *billing.Invoice, updatedLines []api.InvoiceLineReplaceUpdate) (billing.LineChildren, error) {
694+
func (h *handler) mergeInvoiceLinesFromAPI(ctx context.Context, invoice *billing.Invoice, updatedLines []api.InvoiceLineReplaceUpdate) (billing.InvoiceLines, error) {
701695
linesByID, _ := slicesx.UniqueGroupBy(invoice.Lines.OrEmpty(), func(line *billing.Line) string {
702696
return line.ID
703697
})
@@ -716,11 +710,11 @@ func (h *handler) mergeInvoiceLinesFromAPI(ctx context.Context, invoice *billing
716710
// but we are not persisting them to the database
717711
newLine, err := lineFromInvoiceLineReplaceUpdate(line, invoice)
718712
if err != nil {
719-
return billing.LineChildren{}, fmt.Errorf("failed to create new line: %w", err)
713+
return billing.InvoiceLines{}, fmt.Errorf("failed to create new line: %w", err)
720714
}
721715

722716
if newLine.Type == billing.InvoiceLineTypeFee {
723-
return billing.LineChildren{}, billing.ValidationError{
717+
return billing.InvoiceLines{}, billing.ValidationError{
724718
Err: fmt.Errorf("creating flat fee lines is not supported, please use usage based lines instead"),
725719
}
726720
}
@@ -731,7 +725,7 @@ func (h *handler) mergeInvoiceLinesFromAPI(ctx context.Context, invoice *billing
731725
Line: newLine,
732726
})
733727
if err != nil {
734-
return billing.LineChildren{}, fmt.Errorf("failed to snapshot quantity: %w", err)
728+
return billing.InvoiceLines{}, fmt.Errorf("failed to snapshot quantity: %w", err)
735729
}
736730
}
737731

@@ -742,11 +736,11 @@ func (h *handler) mergeInvoiceLinesFromAPI(ctx context.Context, invoice *billing
742736
foundLines.Add(id)
743737
mergedLine, changed, err := mergeLineFromInvoiceLineReplaceUpdate(existingLine, line)
744738
if err != nil {
745-
return billing.LineChildren{}, fmt.Errorf("failed to merge line: %w", err)
739+
return billing.InvoiceLines{}, fmt.Errorf("failed to merge line: %w", err)
746740
}
747741

748742
if changed && mergedLine.Type == billing.InvoiceLineTypeFee {
749-
return billing.LineChildren{}, billing.ValidationError{
743+
return billing.InvoiceLines{}, billing.ValidationError{
750744
Err: fmt.Errorf("updating flat fee lines is not supported, please use usage based lines instead"),
751745
}
752746
}
@@ -757,7 +751,7 @@ func (h *handler) mergeInvoiceLinesFromAPI(ctx context.Context, invoice *billing
757751
Line: mergedLine,
758752
})
759753
if err != nil {
760-
return billing.LineChildren{}, fmt.Errorf("failed to snapshot quantity: %w", err)
754+
return billing.InvoiceLines{}, fmt.Errorf("failed to snapshot quantity: %w", err)
761755
}
762756
}
763757

@@ -773,5 +767,5 @@ func (h *handler) mergeInvoiceLinesFromAPI(ctx context.Context, invoice *billing
773767
out = append(out, existingLine)
774768
}
775769

776-
return billing.NewLineChildren(out), nil
770+
return billing.NewInvoiceLines(out), nil
777771
}

openmeter/billing/invoice.go

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ type Invoice struct {
332332
InvoiceBase `json:",inline"`
333333

334334
// Entities external to the invoice itself
335-
Lines LineChildren `json:"lines,omitempty"`
335+
Lines InvoiceLines `json:"lines,omitempty"`
336336
ValidationIssues ValidationIssues `json:"validationIssues,omitempty"`
337337

338338
Totals Totals `json:"totals"`
@@ -410,7 +410,7 @@ func (i *Invoice) FlattenLinesByID() map[string]*Line {
410410
for _, line := range i.Lines.OrEmpty() {
411411
out[line.ID] = line
412412

413-
for _, child := range line.Children.OrEmpty() {
413+
for _, child := range line.Children {
414414
out[child.ID] = child
415415
}
416416
}
@@ -477,7 +477,7 @@ func (i *Invoice) SortLines() {
477477

478478
sortLines(lines)
479479

480-
i.Lines = NewLineChildren(lines)
480+
i.Lines = NewInvoiceLines(lines)
481481
}
482482

483483
func sortLines(lines []*Line) {
@@ -512,17 +512,106 @@ func sortLines(lines []*Line) {
512512
})
513513

514514
for idx, line := range lines {
515-
if line.Type == InvoiceLineTypeUsageBased && line.Children.IsPresent() {
516-
children := line.Children.OrEmpty()
517-
sortLines(children)
518-
519-
line.Children = NewLineChildren(children)
515+
if line.Type == InvoiceLineTypeUsageBased {
516+
sortLines(line.Children)
520517
}
521518

522519
lines[idx] = line
523520
}
524521
}
525522

523+
type InvoiceLines struct {
524+
mo.Option[[]*Line]
525+
}
526+
527+
func NewInvoiceLines(children []*Line) InvoiceLines {
528+
// Note: this helps with test equality checks
529+
if len(children) == 0 {
530+
children = nil
531+
}
532+
533+
return InvoiceLines{mo.Some(children)}
534+
}
535+
536+
func (i InvoiceLines) Validate() error {
537+
return errors.Join(lo.Map(i.OrEmpty(), func(line *Line, idx int) error {
538+
return ValidationWithFieldPrefix(fmt.Sprintf("%d", idx), line.Validate())
539+
})...)
540+
}
541+
542+
func (c InvoiceLines) Map(fn func(*Line) *Line) InvoiceLines {
543+
if !c.IsPresent() {
544+
return c
545+
}
546+
547+
return InvoiceLines{
548+
mo.Some(
549+
lo.Map(c.OrEmpty(), func(item *Line, _ int) *Line {
550+
return fn(item)
551+
}),
552+
),
553+
}
554+
}
555+
556+
func (c InvoiceLines) Clone() InvoiceLines {
557+
return c.Map(func(l *Line) *Line {
558+
return l.Clone()
559+
})
560+
}
561+
562+
func (c InvoiceLines) GetByID(id string) *Line {
563+
return lo.FindOrElse(c.Option.OrEmpty(), nil, func(line *Line) bool {
564+
return line.ID == id
565+
})
566+
}
567+
568+
func (c *InvoiceLines) ReplaceByID(id string, newLine *Line) bool {
569+
if c.IsAbsent() {
570+
return false
571+
}
572+
573+
lines := c.OrEmpty()
574+
575+
for i, line := range lines {
576+
if line.ID == id {
577+
// Let's preserve the DB state of the original line (as we are only replacing the current state)
578+
originalDBState := line.DBState
579+
580+
lines[i] = newLine
581+
lines[i].DBState = originalDBState
582+
return true
583+
}
584+
}
585+
586+
return false
587+
}
588+
589+
// NonDeletedLineCount returns the number of lines that are not deleted and have a valid status (e.g. we are ignoring split lines)
590+
func (c InvoiceLines) NonDeletedLineCount() int {
591+
return lo.CountBy(c.OrEmpty(), func(l *Line) bool {
592+
return l.DeletedAt == nil && l.Status == InvoiceLineStatusValid
593+
})
594+
}
595+
596+
func (c *InvoiceLines) Append(l ...*Line) {
597+
c.Option = mo.Some(append(c.OrEmpty(), l...))
598+
}
599+
600+
func (c *InvoiceLines) RemoveByID(id string) bool {
601+
toBeRemoved := c.GetByID(id)
602+
if toBeRemoved == nil {
603+
return false
604+
}
605+
606+
c.Option = mo.Some(
607+
lo.Filter(c.Option.OrEmpty(), func(l *Line, _ int) bool {
608+
return l.ID != id
609+
}),
610+
)
611+
612+
return true
613+
}
614+
526615
type InvoiceExternalIDs struct {
527616
Invoicing string `json:"invoicing,omitempty"`
528617
Payment string `json:"payment,omitempty"`
@@ -959,7 +1048,7 @@ type SimulateInvoiceInput struct {
9591048

9601049
Number *string
9611050
Currency currencyx.Code
962-
Lines LineChildren
1051+
Lines InvoiceLines
9631052
}
9641053

9651054
func (i SimulateInvoiceInput) Validate() error {
@@ -991,7 +1080,7 @@ func (i SimulateInvoiceInput) Validate() error {
9911080
return errors.New("currency is required")
9921081
}
9931082

994-
if i.Lines.IsAbsent() || len(i.Lines.OrEmpty()) == 0 {
1083+
if len(i.Lines.OrEmpty()) == 0 {
9951084
return errors.New("lines are required")
9961085
}
9971086

openmeter/billing/invoice_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func TestSortLines(t *testing.T) {
6969

7070
require.Equal(t, *lines[0].Description, "index=0")
7171
require.Equal(t, *lines[1].Description, "index=1")
72-
children := lines[1].Children.OrEmpty()
72+
children := lines[1].Children
7373
require.Equal(t, *children[0].Description, "index=1.0")
7474
require.Equal(t, *children[1].Description, "index=1.1")
7575
}

0 commit comments

Comments
 (0)