Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/invoice line details calculations #1834

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be registered in httpdriver

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these are wrapped into ValidationErrors or whatever error it should be reported as, and that is the thing that the HTTP driver handles.

I can only add a fallback, but regardless the previous logic should ensure proper error codes.

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
Loading