Skip to content

Commit

Permalink
feat: invoice line calculation algos
Browse files Browse the repository at this point in the history
  • Loading branch information
turip committed Nov 12, 2024
1 parent 4ea4a49 commit b617e83
Show file tree
Hide file tree
Showing 18 changed files with 2,448 additions and 48 deletions.
19 changes: 19 additions & 0 deletions openmeter/billing/adapter/invoicelines.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"

"github.com/alpacahq/alpacadecimal"
"github.com/oklog/ulid/v2"
"github.com/samber/lo"

"github.com/openmeterio/openmeter/openmeter/billing"
Expand Down Expand Up @@ -44,6 +45,14 @@ func (r *adapter) CreateInvoiceLines(ctx context.Context, input billing.CreateIn
newEnt = newEnt.SetTaxConfig(*line.TaxConfig)
}

if line.ChildUniqueReferenceID != nil {
newEnt = newEnt.SetChildUniqueReferenceID(*line.ChildUniqueReferenceID)
} else {
id := ulid.Make().String()
newEnt = newEnt.SetChildUniqueReferenceID(id).
SetID(id)
}

edges := db.BillingInvoiceLineEdges{}

switch line.Type {
Expand Down Expand Up @@ -230,6 +239,12 @@ func (r *adapter) UpdateInvoiceLine(ctx context.Context, input billing.UpdateInv
SetStatus(input.Status).
SetOrClearTaxConfig(input.TaxConfig)

if input.ChildUniqueReferenceID != nil {
updateLine = updateLine.SetChildUniqueReferenceID(*input.ChildUniqueReferenceID)
} else {
updateLine = updateLine.SetChildUniqueReferenceID(existingLine.ID)
}

edges := db.BillingInvoiceLineEdges{}

// Let's update the line based on the type
Expand Down Expand Up @@ -370,6 +385,10 @@ func mapInvoiceLineFromDB(dbLine *db.BillingInvoiceLine) (billingentity.Line, er
},

ParentLineID: dbLine.ParentLineID,
ChildUniqueReferenceID: lo.If(
dbLine.ChildUniqueReferenceID != dbLine.ID,
lo.ToPtr(dbLine.ChildUniqueReferenceID),
).Else(nil),

InvoiceAt: dbLine.InvoiceAt,

Expand Down
2 changes: 2 additions & 0 deletions openmeter/billing/entity/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ var (
ErrInvoiceActionNotAvailable = NewValidationError("invoice_action_not_available", "invoice action not available")

ErrInvoiceLineFeatureHasNoMeters = NewValidationError("invoice_line_feature_has_no_meters", "usage based invoice line: feature has no meters")
ErrInvoiceLineGraduatedSplitNotSupported = NewValidationError("invoice_line_graduated_split_not_supported", "graduated tiered pricing is not supported for split periods")
ErrInvoiceLineNoTiers = NewValidationError("invoice_line_no_tiers", "usage based invoice line: no tiers found")
ErrInvoiceCreateNoLines = NewValidationError("invoice_create_no_lines", "the new invoice would have no lines")
ErrInvoiceCreateUBPLineCustomerHasNoSubjects = NewValidationError("invoice_create_ubp_line_customer_has_no_subjects", "creating an usage based line: customer has no subjects")
ErrInvoiceCreateUBPLinePeriodIsEmpty = NewValidationError("invoice_create_ubp_line_period_is_empty", "creating an usage based line: truncated period is empty")
Expand Down
40 changes: 35 additions & 5 deletions openmeter/billing/entity/invoiceline.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const (
InvoiceLineStatusValid InvoiceLineStatus = "valid"
// InvoiceLineStatusSplit is a split invoice line (the child lines will have this set as parent).
InvoiceLineStatusSplit InvoiceLineStatus = "split"
// InvoiceLineStatusDetailed is a detailed invoice line.
InvoiceLineStatusDetailed InvoiceLineStatus = "detailed"
)

func (InvoiceLineStatus) Values() []string {
Expand Down Expand Up @@ -109,12 +111,14 @@ type LineBase struct {
// TODO: Add discounts etc

// Relationships
ParentLineID *string `json:"parentLine,omitempty"`
ParentLine *Line `json:"parent,omitempty"`
RelatedLines []string `json:"relatedLine,omitempty"`
Status InvoiceLineStatus `json:"status"`
ParentLineID *string `json:"parentLine,omitempty"`
ParentLine *Line `json:"parent,omitempty"`
DetailedLines []Line `json:"detailedLines,omitempty"`
Status InvoiceLineStatus `json:"status"`
ChildUniqueReferenceID *string `json:"childUniqueReferenceID,omitempty"`

TaxConfig *TaxConfig `json:"taxOverrides,omitempty"`
TaxConfig *TaxConfig `json:"taxOverrides,omitempty"`
Discounts []LineDiscount `json:"discounts,omitempty"`

Total alpacadecimal.Decimal `json:"total"`
}
Expand Down Expand Up @@ -228,3 +232,29 @@ func (i UsageBasedLine) Validate() error {

return nil
}

type LineDiscountSource string

const (
// ManualLineDiscountSource is a manually added discount.
ManualLineDiscountSource LineDiscountSource = "manual"
// CalculatedLineDiscountSource is a discount applied due to maximum spend.
CalculatedLineDiscountSource LineDiscountSource = "calculated"
)

type LineDiscountType string

const (
// MaximumSpendLineDiscountType is a discount applied due to maximum spend.
MaximumSpendLineDiscountType LineDiscountType = "maximum_spend"
// CappedTierLineDiscountType is a discount applied due to capped tier (e.g. we are over the biggest tier and the tier structure is not open ended).
CappedTierLineDiscountType LineDiscountType = "capped_tier"
)

type LineDiscount struct {
ID string `json:"id"`
Amount alpacadecimal.Decimal `json:"amount"`
Description *string `json:"description,omitempty"`
Type *LineDiscountType `json:"type,omitempty"`
Source LineDiscountSource `json:"source"`
}
14 changes: 14 additions & 0 deletions openmeter/billing/service/lineservice/linebase.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ type LineBase interface {
Period() billingentity.Period
Status() billingentity.InvoiceLineStatus
HasParent() bool
// IsLastInPeriod returns true if the line is the last line in the period that is going to be invoiced.
IsLastInPeriod() bool

CloneForCreate(in UpdateInput) Line
Update(in UpdateInput) Line
Expand Down Expand Up @@ -112,6 +114,18 @@ func (l lineBase) Validate(ctx context.Context, invoice *billingentity.Invoice)
return nil
}

func (l lineBase) IsLastInPeriod() bool {
return (l.line.Status == billingentity.InvoiceLineStatusValid && // We only care about valid lines
(l.line.ParentLineID == nil || // Either we haven't split the line
l.line.Period.End.Equal(l.line.ParentLine.Period.End))) // Or we have split the line and this is the last split
}

func (l lineBase) IsFirstInPeriod() bool {
return (l.line.Status == billingentity.InvoiceLineStatusValid && // We only care about valid lines
(l.line.ParentLineID == nil || // Either we haven't split the line
l.line.Period.Start.Equal(l.line.ParentLine.Period.Start))) // Or we have split the line and this is the last split
}

func (l lineBase) Save(ctx context.Context) (Line, error) {
line, err := l.service.BillingAdapter.UpdateInvoiceLine(ctx, billing.UpdateInvoiceLineAdapterInput(l.line))
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions openmeter/billing/service/lineservice/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ func (s *Service) AssociateLinesToInvoice(ctx context.Context, invoice *billinge
}

type snapshotQuantityResult struct {
Line Line
// TODO[OM-980]: Detailed lines should be returned here, that we are upserting based on the qty as described in README.md (see `Detailed Lines vs Splitting`)
Line Line
DetailedLines []Line
}

type Line interface {
Expand Down
Loading

0 comments on commit b617e83

Please sign in to comment.