Skip to content
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
56 changes: 40 additions & 16 deletions openmeter/billing/adapter/invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/openmeterio/openmeter/api"
"github.com/openmeterio/openmeter/openmeter/app"
"github.com/openmeterio/openmeter/openmeter/billing"
"github.com/openmeterio/openmeter/openmeter/customer"
"github.com/openmeterio/openmeter/openmeter/ent/db"
"github.com/openmeterio/openmeter/openmeter/ent/db/billinginvoice"
"github.com/openmeterio/openmeter/openmeter/ent/db/billinginvoiceline"
Expand Down Expand Up @@ -606,33 +607,56 @@ func (a *adapter) UpdateInvoice(ctx context.Context, in billing.UpdateInvoiceAda

func (a *adapter) GetInvoiceOwnership(ctx context.Context, in billing.GetInvoiceOwnershipAdapterInput) (billing.GetOwnershipAdapterResponse, error) {
if err := in.Validate(); err != nil {
return billing.GetOwnershipAdapterResponse{}, billing.ValidationError{
return nil, billing.ValidationError{
Err: err,
}
}

return entutils.TransactingRepo(ctx, a, func(ctx context.Context, tx *adapter) (billing.GetOwnershipAdapterResponse, error) {
dbInvoice, err := tx.db.BillingInvoice.Query().
Where(billinginvoice.ID(in.ID)).
Where(billinginvoice.Namespace(in.Namespace)).
First(ctx)
dbInvoices, err := tx.db.BillingInvoice.Query().
Where(
billinginvoice.IDIn(
lo.Map(
in.InvoiceIDs,
func(invoiceID billing.InvoiceID, _ int) string {
return invoiceID.ID
},
)...,
),
).
All(ctx)
if err != nil {
if db.IsNotFound(err) {
return billing.GetOwnershipAdapterResponse{}, billing.NotFoundError{
Entity: billing.EntityInvoice,
ID: in.ID,
Err: err,
return nil, err
}

invoiceToCustomerID := lo.SliceToMap(dbInvoices, func(dbInvoice *db.BillingInvoice) (billing.InvoiceID, customer.CustomerID) {
return billing.InvoiceID{
Namespace: dbInvoice.Namespace,
ID: dbInvoice.ID,
}, customer.CustomerID{
Namespace: dbInvoice.Namespace,
ID: dbInvoice.CustomerID,
}
})

// Let's validate if we got all the invoices (and most importantly look up invoices with
// namespaceID, to prevent looking up invoices with different than expected namespace ID)
var notFoundErrs []error
for _, invoiceID := range in.InvoiceIDs {
if _, found := invoiceToCustomerID[invoiceID]; !found {
notFoundErrs = append(notFoundErrs, billing.NotFoundError{
Entity: billing.EntityInvoice,
ID: invoiceID.ID,
Err: fmt.Errorf("invoice not found: %s", invoiceID.ID),
})
}
}

return billing.GetOwnershipAdapterResponse{}, err
if len(notFoundErrs) > 0 {
return nil, errors.Join(notFoundErrs...)
}

return billing.GetOwnershipAdapterResponse{
Namespace: dbInvoice.Namespace,
InvoiceID: dbInvoice.ID,
CustomerID: dbInvoice.CustomerID,
}, nil
return invoiceToCustomerID, nil
})
}

Expand Down
30 changes: 25 additions & 5 deletions openmeter/billing/invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -867,14 +867,18 @@ type (

type UpdateInvoiceAdapterInput = Invoice

type GetInvoiceOwnershipAdapterInput = InvoiceID
type GetInvoiceOwnershipAdapterInput struct {
InvoiceIDs []InvoiceID
}

type GetOwnershipAdapterResponse struct {
Namespace string
InvoiceID string
CustomerID string
func (i GetInvoiceOwnershipAdapterInput) Validate() error {
if len(i.InvoiceIDs) == 0 {
return errors.New("invoice IDs are required")
}
}

type GetOwnershipAdapterResponse map[InvoiceID]customer.CustomerID

type DeleteInvoiceInput = InvoiceID

type UpdateInvoiceLinesInternalInput struct {
Expand Down Expand Up @@ -1061,3 +1065,19 @@ func (i UpdateInvoiceFieldsInput) Validate() error {
}

type RecalculateGatheringInvoicesInput = customer.CustomerID

type StandardImmutableInvoiceUpdate struct {
UpsertValidationIssues mo.Option[ValidationIssues]
}

type BulkUpdateInvoicesInput struct {
IncludeDeletedLines bool
Invoices []InvoiceID
GatheringInvoiceEditFunction func(*Invoice) error
StandardMutableInvoiceEditFunction func(*Invoice) error
StandardImmutableInvoiceEditFunction func(*Invoice) (StandardImmutableInvoiceUpdate, error)
}

type BulkUpdateInvoicesResult struct {
InvoicesByID map[InvoiceID]Invoice
}
1 change: 1 addition & 0 deletions openmeter/billing/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type InvoiceService interface {
DeleteInvoice(ctx context.Context, input DeleteInvoiceInput) error
// UpdateInvoice updates an invoice as a whole
UpdateInvoice(ctx context.Context, input UpdateInvoiceInput) (Invoice, error)
BulkUpdateInvoices(ctx context.Context, input BulkUpdateInvoicesInput) (BulkUpdateInvoicesResult, error)

// SimulateInvoice generates an invoice based on the provided input, but does not persist it
// can be used to execute the invoice generation logic without actually creating an invoice in the database
Expand Down
53 changes: 53 additions & 0 deletions openmeter/billing/service/invoicebulk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package billingservice

import (
"context"
"fmt"

"github.com/openmeterio/openmeter/openmeter/billing"
"github.com/openmeterio/openmeter/openmeter/customer"
"github.com/openmeterio/openmeter/pkg/framework/transaction"
)

func (s *Service) BulkUpdateInvoices(ctx context.Context, input billing.BulkUpdateInvoicesInput) (billing.BulkUpdateInvoicesResult, error) {
if err := input.Validate(); err != nil {
return billing.BulkUpdateInvoicesResult{}, billing.ValidationError{
Err: err,
}
}

return transaction.Run(ctx, s.adapter, func(ctx context.Context) (billing.BulkUpdateInvoicesResult, error) {
invoiceToCustomerID, err := s.adapter.GetInvoiceOwnership(ctx, billing.GetInvoiceOwnershipAdapterInput{
InvoiceIDs: input.Invoices,
})
if err != nil {
return billing.BulkUpdateInvoicesResult{}, fmt.Errorf("getting invoice ownership: %w", err)
}

invoiceIDsByCustomerID := map[customer.CustomerID][]billing.InvoiceID{}
for invoiceID, customerID := range invoiceToCustomerID {
invoiceIDsByCustomerID[customerID] = append(invoiceIDsByCustomerID[customerID], invoiceID)
}

for customerID, invoiceIDs := range invoiceIDsByCustomerID {
xxx, err := transcationForInvoiceManipulation(ctx, s, customerID, func(ctx context.Context) (billing.BulkUpdateInvoicesResult, error) {
invoices := make([]*billing.Invoice, 0, len(invoiceIDs))
for _, invoiceID := range invoiceIDs {
invoice, err := s.GetInvoiceByID(ctx, billing.GetInvoiceByIdInput{
Invoice: invoiceID,
Expand: billing.InvoiceExpandAll.
SetDeletedLines(input.IncludeDeletedLines),
})
if err != nil {
return billing.BulkUpdateInvoicesResult{}, fmt.Errorf("getting invoice[%s]: %w", invoiceID.ID, err)
}

invoices = append(invoices, &invoice)
}
})
if err != nil {
return billing.BulkUpdateInvoicesResult{}, fmt.Errorf("updating invoices: %w", err)
}
}
})
}
15 changes: 7 additions & 8 deletions openmeter/billing/service/lineservice/linebase.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ func (l lineBase) Validate(ctx context.Context, invoice *billing.Invoice) error
}
}

// Expanding the split lines are mandatory for the lineservice to work properly.
if l.line.SplitLineGroupID != nil && l.line.SplitLineHierarchy == nil {
return billing.ValidationError{
Err: fmt.Errorf("split line group[%s] has no expanded hierarchy, while being part of a split line group", *l.line.SplitLineGroupID),
}
}

return nil
}

Expand All @@ -114,10 +121,6 @@ func (l lineBase) IsLastInPeriod() bool {
return true
}

if l.line.SplitLineHierarchy == nil {
return true
}

if l.line.SplitLineHierarchy.Group.ServicePeriod.End.Equal(l.line.Period.End) {
return true
}
Expand All @@ -130,10 +133,6 @@ func (l lineBase) IsFirstInPeriod() bool {
return true
}

if l.line.SplitLineHierarchy == nil {
return true
}

if l.line.SplitLineHierarchy.Group.ServicePeriod.Start.Equal(l.line.Period.Start) {
return true
}
Expand Down
Loading