Skip to content

Commit 27f39d2

Browse files
committed
refactor: disambiguate lines vs detailed lines
1 parent daa356d commit 27f39d2

28 files changed

+520
-806
lines changed

openmeter/app/stripe/entity/app/invoice.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,12 @@ func (a App) createInvoice(ctx context.Context, invoice billing.Invoice) (*billi
205205
// Add lines to the Stripe invoice
206206
var stripeLineAdd []*stripe.InvoiceItemParams
207207

208-
leafLines := invoice.GetLeafLinesWithConsolidatedTaxBehavior()
208+
leafLines := invoice.GetDetailedLinesWithConsolidatedTaxBehavior()
209209

210210
// Iterate over the leaf lines
211211
for _, line := range leafLines {
212212
// Add discounts for line if any
213-
for _, discount := range line.Discounts.Amount {
213+
for _, discount := range line.AmountDiscounts {
214214
stripeLineAdd = append(stripeLineAdd, getDiscountStripeAddInvoiceItemParams(calculator, line, discount, stripeCustomerData.StripeCustomerID))
215215
}
216216

@@ -304,7 +304,7 @@ func (a App) updateInvoice(ctx context.Context, invoice billing.Invoice) (*billi
304304
stripeLinesRemove []string
305305
)
306306

307-
leafLines := invoice.GetLeafLinesWithConsolidatedTaxBehavior()
307+
leafLines := invoice.GetDetailedLinesWithConsolidatedTaxBehavior()
308308

309309
// Helper to get a Stripe line item by ID
310310
stripeLinesByID := make(map[string]*stripe.InvoiceLineItem)
@@ -319,7 +319,7 @@ func (a App) updateInvoice(ctx context.Context, invoice billing.Invoice) (*billi
319319

320320
// Iterate over the leaf lines
321321
for _, line := range leafLines {
322-
amountDiscountsById, err := line.Discounts.Amount.GetByID()
322+
amountDiscountsById, err := line.AmountDiscounts.GetByID()
323323
if err != nil {
324324
return nil, fmt.Errorf("failed to get amount discounts by ID: %w", err)
325325
}
@@ -452,7 +452,7 @@ func sortInvoiceLines[K StripeInvoiceLineOperationParams](stripeLineAdd []*K) {
452452
// getDiscountStripeUpdateInvoiceItemParams returns the Stripe line item for a discount
453453
func getDiscountStripeUpdateInvoiceItemParams(
454454
calculator StripeCalculator,
455-
line *billing.Line,
455+
line billing.DetailedLine,
456456
discount billing.AmountLineDiscountManaged,
457457
stripeLine *stripe.InvoiceLineItem,
458458
) *stripeclient.StripeInvoiceItemWithID {
@@ -463,7 +463,7 @@ func getDiscountStripeUpdateInvoiceItemParams(
463463
}
464464

465465
// getDiscountStripeInvoiceItemParams returns the Stripe line item for a discount
466-
func getDiscountStripeInvoiceItemParams(calculator StripeCalculator, line *billing.Line, discount billing.AmountLineDiscountManaged) *stripe.InvoiceItemParams {
466+
func getDiscountStripeInvoiceItemParams(calculator StripeCalculator, line billing.DetailedLine, discount billing.AmountLineDiscountManaged) *stripe.InvoiceItemParams {
467467
name := getDiscountLineName(line, discount)
468468
period := getPeriod(line)
469469

@@ -480,14 +480,14 @@ func getDiscountStripeInvoiceItemParams(calculator StripeCalculator, line *billi
480480
return applyTaxSettingsToInvoiceItem(addParams, line)
481481
}
482482

483-
func getDiscountStripeAddInvoiceItemParams(calculator StripeCalculator, line *billing.Line, discount billing.AmountLineDiscountManaged, stripeCustomerID string) *stripe.InvoiceItemParams {
483+
func getDiscountStripeAddInvoiceItemParams(calculator StripeCalculator, line billing.DetailedLine, discount billing.AmountLineDiscountManaged, stripeCustomerID string) *stripe.InvoiceItemParams {
484484
params := getDiscountStripeInvoiceItemParams(calculator, line, discount)
485485
// Customer is required for adds
486486
params.Customer = stripe.String(stripeCustomerID)
487487
return params
488488
}
489489

490-
func applyTaxSettingsToInvoiceItem(add *stripe.InvoiceItemParams, line *billing.Line) *stripe.InvoiceItemParams {
490+
func applyTaxSettingsToInvoiceItem(add *stripe.InvoiceItemParams, line billing.DetailedLine) *stripe.InvoiceItemParams {
491491
if line.TaxConfig != nil && !lo.IsEmpty(line.TaxConfig) {
492492
if line.TaxConfig.Behavior != nil {
493493
add.TaxBehavior = getStripeTaxBehavior(line.TaxConfig.Behavior)
@@ -504,7 +504,7 @@ func applyTaxSettingsToInvoiceItem(add *stripe.InvoiceItemParams, line *billing.
504504
// getStripeUpdateInvoiceItemParams returns the Stripe update line params
505505
func getStripeUpdateInvoiceItemParams(
506506
calculator StripeCalculator,
507-
line *billing.Line,
507+
line billing.DetailedLine,
508508
stripeLine *stripe.InvoiceLineItem,
509509
) *stripeclient.StripeInvoiceItemWithID {
510510
return &stripeclient.StripeInvoiceItemWithID{
@@ -514,7 +514,7 @@ func getStripeUpdateInvoiceItemParams(
514514
}
515515

516516
// getStripeAddLinesLineParams returns the Stripe line item
517-
func getStripeInvoiceItemParams(line *billing.Line, calculator StripeCalculator) *stripe.InvoiceItemParams {
517+
func getStripeInvoiceItemParams(line billing.DetailedLine, calculator StripeCalculator) *stripe.InvoiceItemParams {
518518
description := getLineName(line)
519519
period := getPeriod(line)
520520
amount := line.Totals.Amount
@@ -551,22 +551,22 @@ func getStripeInvoiceItemParams(line *billing.Line, calculator StripeCalculator)
551551
}
552552

553553
// getStripeAddInvoiceItemParams returns the Stripe line item
554-
func getStripeAddInvoiceItemParams(line *billing.Line, calculator StripeCalculator, stripeCustomerID string) *stripe.InvoiceItemParams {
554+
func getStripeAddInvoiceItemParams(line billing.DetailedLine, calculator StripeCalculator, stripeCustomerID string) *stripe.InvoiceItemParams {
555555
params := getStripeInvoiceItemParams(line, calculator)
556556
params.Customer = stripe.String(stripeCustomerID)
557557
return params
558558
}
559559

560560
// getPeriod returns the period
561-
func getPeriod(line *billing.Line) *stripe.InvoiceItemPeriodParams {
561+
func getPeriod(line billing.DetailedLine) *stripe.InvoiceItemPeriodParams {
562562
return &stripe.InvoiceItemPeriodParams{
563563
Start: lo.ToPtr(line.Period.Start.Unix()),
564564
End: lo.ToPtr(line.Period.End.Unix()),
565565
}
566566
}
567567

568568
// getDiscountLineName returns the line name
569-
func getDiscountLineName(line *billing.Line, discount billing.AmountLineDiscountManaged) string {
569+
func getDiscountLineName(line billing.DetailedLine, discount billing.AmountLineDiscountManaged) string {
570570
name := line.Name
571571
if discount.Description != nil {
572572
name = fmt.Sprintf("%s (%s)", name, *discount.Description)
@@ -576,7 +576,7 @@ func getDiscountLineName(line *billing.Line, discount billing.AmountLineDiscount
576576
}
577577

578578
// getLineName returns the line name
579-
func getLineName(line *billing.Line) string {
579+
func getLineName(line billing.DetailedLine) string {
580580
name := line.Name
581581
if line.Description != nil {
582582
name = fmt.Sprintf("%s (%s)", name, *line.Description)

openmeter/billing/adapter/invoicelines.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ func (a *adapter) UpsertInvoiceLines(ctx context.Context, inputIn billing.Upsert
8080
SetNillableSplitLineGroupID(line.SplitLineGroupID).
8181
SetNillableDeletedAt(line.DeletedAt).
8282
SetInvoiceAt(line.InvoiceAt.In(time.UTC)).
83-
SetStatus(line.Status).
83+
SetStatus(billing.InvoiceLineStatusValid).
8484
SetManagedBy(line.ManagedBy).
85-
SetType(line.Type).
85+
SetType(billing.InvoiceLineTypeUsageBased).
8686
SetName(line.Name).
8787
SetNillableDescription(line.Description).
8888
SetCurrency(line.Currency).
@@ -288,9 +288,9 @@ func (a *adapter) UpsertInvoiceLines(ctx context.Context, inputIn billing.Upsert
288288
})
289289
}
290290

291-
func (a *adapter) upsertFeeLineConfig(ctx context.Context, in diff[*billing.Line]) error {
292-
return upsertWithOptions(ctx, a.db, in, upsertInput[*billing.Line, *db.BillingInvoiceFlatFeeLineConfigCreate]{
293-
Create: func(tx *db.Client, line *billing.Line) (*db.BillingInvoiceFlatFeeLineConfigCreate, error) {
291+
func (a *adapter) upsertFeeLineConfig(ctx context.Context, in diff[*billing.DetailedLine]) error {
292+
return upsertWithOptions(ctx, a.db, in, upsertInput[*billing.DetailedLine, *db.BillingInvoiceFlatFeeLineConfigCreate]{
293+
Create: func(tx *db.Client, line *billing.DetailedLine) (*db.BillingInvoiceFlatFeeLineConfigCreate, error) {
294294
if line.FlatFee.ConfigID == "" {
295295
line.FlatFee.ConfigID = ulid.Make().String()
296296
}
@@ -302,10 +302,7 @@ func (a *adapter) upsertFeeLineConfig(ctx context.Context, in diff[*billing.Line
302302
SetPaymentTerm(line.FlatFee.PaymentTerm).
303303
SetID(line.FlatFee.ConfigID)
304304

305-
if line.Status == billing.InvoiceLineStatusDetailed {
306-
// TODO[later]: Detailed lines must be a separate entity, so that we don't need these hacks (like line config or type specific sets)
307-
create = create.SetNillableIndex(line.FlatFee.Index)
308-
}
305+
create = create.SetNillableIndex(line.FlatFee.Index)
309306

310307
return create, nil
311308
},

openmeter/billing/app.go

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"strings"
78
"time"
89

10+
"github.com/samber/lo"
911
"github.com/samber/mo"
1012

1113
"github.com/openmeterio/openmeter/openmeter/app"
@@ -217,44 +219,54 @@ func (r UpsertInvoiceResult) MergeIntoInvoice(invoice *Invoice) error {
217219
if invoiceNumber, ok := r.GetInvoiceNumber(); ok {
218220
invoice.Number = invoiceNumber
219221
}
220-
221222
if externalID, ok := r.GetExternalID(); ok {
222223
invoice.ExternalIDs.Invoicing = externalID
223224
}
224225

226+
if !invoice.Lines.IsPresent() {
227+
return errors.New("invoice has no expanded lines")
228+
}
229+
225230
var outErr error
226231

227232
// Let's merge the line IDs
228-
if len(r.GetLineExternalIDs()) > 0 {
229-
flattenedLines := invoice.FlattenLinesByID()
230-
231-
// Merge the line IDs
232-
for lineID, externalID := range r.GetLineExternalIDs() {
233-
if line, ok := flattenedLines[lineID]; ok {
234-
line.ExternalIDs.Invoicing = externalID
235-
} else {
236-
outErr = errors.Join(outErr, fmt.Errorf("line not found in invoice: %s", lineID))
237-
}
233+
lineIDToExternalID := r.GetLineExternalIDs()
234+
dicountIDToExternalID := r.GetLineDiscountExternalIDs()
235+
236+
lines := invoice.Lines.OrEmpty()
237+
238+
for _, line := range lines {
239+
if externalID, ok := lineIDToExternalID[line.ID]; ok {
240+
line.ExternalIDs.Invoicing = externalID
241+
delete(lineIDToExternalID, line.ID)
238242
}
239243

240-
// Let's merge the line discount IDs
241-
dicountIDToExternalID := r.GetLineDiscountExternalIDs()
244+
foundIDs := line.SetDiscountExternalIDs(dicountIDToExternalID)
245+
for _, id := range foundIDs {
246+
delete(dicountIDToExternalID, id)
247+
}
242248

243-
for _, line := range flattenedLines {
244-
for idx, discount := range line.Discounts.Amount {
245-
if externalID, ok := dicountIDToExternalID[discount.ID]; ok {
246-
line.Discounts.Amount[idx].ExternalIDs.Invoicing = externalID
247-
}
249+
for idx, detailedLine := range line.Children {
250+
if externalID, ok := lineIDToExternalID[detailedLine.ID]; ok {
251+
line.Children[idx].ExternalIDs.Invoicing = externalID
252+
delete(lineIDToExternalID, detailedLine.ID)
248253
}
249254

250-
for idx, discount := range line.Discounts.Usage {
251-
if externalID, ok := dicountIDToExternalID[discount.ID]; ok {
252-
line.Discounts.Usage[idx].ExternalIDs.Invoicing = externalID
253-
}
255+
foundIDs := line.Children[idx].AmountDiscounts.SetDiscountExternalIDs(dicountIDToExternalID)
256+
for _, id := range foundIDs {
257+
delete(dicountIDToExternalID, id)
254258
}
255259
}
256260
}
257261

262+
if len(lineIDToExternalID) > 0 {
263+
outErr = errors.Join(outErr, fmt.Errorf("some lines were not found in the invoice: ids=[%s]", strings.Join(lo.Keys(lineIDToExternalID), ", ")))
264+
}
265+
266+
if len(dicountIDToExternalID) > 0 {
267+
outErr = errors.Join(outErr, fmt.Errorf("some line discounts were not found in the invoice: ids=[%s]", strings.Join(lo.Keys(dicountIDToExternalID), ", ")))
268+
}
269+
258270
return outErr
259271
}
260272

openmeter/billing/invoice.go

Lines changed: 37 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -404,12 +404,10 @@ func (i Invoice) RemoveMetaForCompare() Invoice {
404404
return invoice
405405
}
406406

407-
func (i *Invoice) FlattenLinesByID() map[string]*Line {
408-
out := make(map[string]*Line, len(i.Lines.OrEmpty()))
407+
func (i *Invoice) GetDetailedLinesByID() map[string]DetailedLine {
408+
out := make(map[string]DetailedLine, len(i.Lines.OrEmpty()))
409409

410410
for _, line := range i.Lines.OrEmpty() {
411-
out[line.ID] = line
412-
413411
for _, child := range line.Children {
414412
out[child.ID] = child
415413
}
@@ -418,31 +416,15 @@ func (i *Invoice) FlattenLinesByID() map[string]*Line {
418416
return out
419417
}
420418

421-
// getLeafLines returns the leaf lines
422-
func (i *Invoice) getLeafLines() []*Line {
423-
var leafLines []*Line
424-
425-
for _, line := range i.FlattenLinesByID() {
426-
// Skip non leaf nodes
427-
if line.Type != InvoiceLineTypeFee {
428-
continue
429-
}
430-
431-
leafLines = append(leafLines, line)
432-
}
433-
434-
return leafLines
435-
}
436-
437-
// GetLeafLinesWithConsolidatedTaxBehavior returns the leaf lines with the tax behavior set to the invoice's tax behavior
419+
// GetDetailedLinesWithConsolidatedTaxBehavior returns the detailed lines with the tax behavior set to the invoice's tax behavior
438420
// unless the line already has a tax behavior set.
439-
func (i *Invoice) GetLeafLinesWithConsolidatedTaxBehavior() []*Line {
440-
leafLines := i.getLeafLines()
421+
func (i *Invoice) GetDetailedLinesWithConsolidatedTaxBehavior() []DetailedLine {
422+
leafLines := lo.Values(i.GetDetailedLinesByID())
441423
if i.Workflow.Config.Invoicing.DefaultTaxConfig == nil {
442424
return leafLines
443425
}
444426

445-
return lo.Map(leafLines, func(line *Line, _ int) *Line {
427+
return lo.Map(leafLines, func(line DetailedLine, _ int) DetailedLine {
446428
line.TaxConfig = productcatalog.MergeTaxConfigs(i.Workflow.Config.Invoicing.DefaultTaxConfig, line.TaxConfig)
447429
return line
448430
})
@@ -469,55 +451,7 @@ func (i Invoice) RemoveCircularReferences() Invoice {
469451
}
470452

471453
func (i *Invoice) SortLines() {
472-
if !i.Lines.IsPresent() {
473-
return
474-
}
475-
476-
lines := i.Lines.OrEmpty()
477-
478-
sortLines(lines)
479-
480-
i.Lines = NewInvoiceLines(lines)
481-
}
482-
483-
func sortLines(lines []*Line) {
484-
sort.Slice(lines, func(a, b int) bool {
485-
lineA := lines[a]
486-
lineB := lines[b]
487-
488-
// If both lines are flat fee lines, we sort them by index if possible
489-
if lineA.Type == InvoiceLineTypeFee && lineB.Type == InvoiceLineTypeFee {
490-
if lineA.FlatFee.Index != nil && lineB.FlatFee.Index != nil {
491-
return *lineA.FlatFee.Index < *lineB.FlatFee.Index
492-
}
493-
494-
if lineA.FlatFee.Index != nil {
495-
return true
496-
}
497-
498-
if lineB.FlatFee.Index != nil {
499-
return false
500-
}
501-
}
502-
503-
if nameOrder := strings.Compare(lineA.Name, lineB.Name); nameOrder != 0 {
504-
return nameOrder < 0
505-
}
506-
507-
if !lineA.Period.Start.Equal(lineB.Period.Start) {
508-
return lineA.Period.Start.Before(lineB.Period.Start)
509-
}
510-
511-
return strings.Compare(lineA.ID, lineB.ID) < 0
512-
})
513-
514-
for idx, line := range lines {
515-
if line.Type == InvoiceLineTypeUsageBased {
516-
sortLines(line.Children)
517-
}
518-
519-
lines[idx] = line
520-
}
454+
i.Lines.Sort()
521455
}
522456

523457
type InvoiceLines struct {
@@ -586,10 +520,39 @@ func (c *InvoiceLines) ReplaceByID(id string, newLine *Line) bool {
586520
return false
587521
}
588522

523+
func (c *InvoiceLines) Sort() {
524+
if c.IsAbsent() {
525+
return
526+
}
527+
528+
lines := c.OrEmpty()
529+
530+
sort.Slice(lines, func(a, b int) bool {
531+
lineA := lines[a]
532+
lineB := lines[b]
533+
534+
if nameOrder := strings.Compare(lineA.Name, lineB.Name); nameOrder != 0 {
535+
return nameOrder < 0
536+
}
537+
538+
if !lineA.Period.Start.Equal(lineB.Period.Start) {
539+
return lineA.Period.Start.Before(lineB.Period.Start)
540+
}
541+
542+
return strings.Compare(lineA.ID, lineB.ID) < 0
543+
})
544+
545+
for _, line := range lines {
546+
line.SortDetailedLines()
547+
}
548+
549+
c.Option = mo.Some(lines)
550+
}
551+
589552
// NonDeletedLineCount returns the number of lines that are not deleted and have a valid status (e.g. we are ignoring split lines)
590553
func (c InvoiceLines) NonDeletedLineCount() int {
591554
return lo.CountBy(c.OrEmpty(), func(l *Line) bool {
592-
return l.DeletedAt == nil && l.Status == InvoiceLineStatusValid
555+
return l.DeletedAt == nil
593556
})
594557
}
595558

0 commit comments

Comments
 (0)