Skip to content

Commit 5b4ce3d

Browse files
feat(billing): CreateInvoicePendingLines charges support (#4508)
1 parent 4355590 commit 5b4ce3d

9 files changed

Lines changed: 894 additions & 24 deletions

File tree

openmeter/billing/charges/service.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"time"
88

9+
"github.com/openmeterio/openmeter/openmeter/billing"
910
"github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase"
1011
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
1112
"github.com/openmeterio/openmeter/openmeter/billing/charges/models/payment"
@@ -26,6 +27,7 @@ type ChargeService interface {
2627
GetByID(ctx context.Context, input GetByIDInput) (Charge, error)
2728
GetByIDs(ctx context.Context, input GetByIDsInput) (Charges, error)
2829
Create(ctx context.Context, input CreateInput) (Charges, error)
30+
CreatePendingInvoiceLines(ctx context.Context, input CreatePendingInvoiceLinesInput) (*CreatePendingInvoiceLinesResult, error)
2931
UpdateSubscriptionItemID(ctx context.Context, charge Charge, newSubscriptionItemID string) (Charge, error)
3032

3133
AdvanceCharges(ctx context.Context, input AdvanceChargesInput) (Charges, error)
@@ -46,6 +48,11 @@ type CreateInput struct {
4648
Intents ChargeIntents
4749
}
4850

51+
type (
52+
CreatePendingInvoiceLinesInput = billing.CreatePendingInvoiceLinesInput
53+
CreatePendingInvoiceLinesResult = billing.CreatePendingInvoiceLinesResult
54+
)
55+
4956
func (i CreateInput) Validate() error {
5057
var errs []error
5158

openmeter/billing/charges/service/create.go

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
type chargesWithInvoiceNowActions struct {
2929
charges charges.Charges
3030
collectionAlignmentBypassedLines []invoicePendingLinesInput
31+
pendingLineResults []*billing.CreatePendingInvoiceLinesResult
3132
}
3233

3334
// applyDefaultTaxCodes fills in nil TaxCodeID on each intent's TaxConfig using the namespace's
@@ -67,6 +68,27 @@ func (s *service) applyDefaultTaxCodes(ctx context.Context, namespace string, in
6768
}
6869

6970
func (s *service) Create(ctx context.Context, input charges.CreateInput) (charges.Charges, error) {
71+
result, err := s.create(ctx, input)
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
if result == nil {
77+
return nil, fmt.Errorf("result is nil")
78+
}
79+
80+
// TODO: once we have proper state machine for credit purchases, we can remove this and make the
81+
// autoAdvanceCreatedCharges handle the invoice now actions.
82+
if len(result.collectionAlignmentBypassedLines) > 0 {
83+
if err := s.invokeInvoiceNowOnCreate(ctx, result.collectionAlignmentBypassedLines); err != nil {
84+
return nil, fmt.Errorf("invoking invoice now on create: %w", err)
85+
}
86+
}
87+
88+
return s.autoAdvanceCreatedCharges(ctx, result.charges)
89+
}
90+
91+
func (s *service) create(ctx context.Context, input charges.CreateInput) (*chargesWithInvoiceNowActions, error) {
7092
if input.Namespace == "" {
7193
return nil, fmt.Errorf("namespace is required")
7294
}
@@ -208,7 +230,7 @@ func (s *service) Create(ctx context.Context, input charges.CreateInput) (charge
208230
}
209231

210232
// Let's generate the gathering lines for the flat fees
211-
collectionAlignmentBypassedLines, err := s.createGatheringLines(ctx, gatheringLinesToCreate)
233+
gatheringLineResult, err := s.createGatheringLines(ctx, gatheringLinesToCreate)
212234
if err != nil {
213235
return nil, err
214236
}
@@ -225,26 +247,15 @@ func (s *service) Create(ctx context.Context, input charges.CreateInput) (charge
225247

226248
return &chargesWithInvoiceNowActions{
227249
charges: result,
228-
collectionAlignmentBypassedLines: collectionAlignmentBypassedLines,
250+
collectionAlignmentBypassedLines: gatheringLineResult.collectionAlignmentBypassedLines,
251+
pendingLineResults: gatheringLineResult.pendingLineResults,
229252
}, nil
230253
})
231254
if err != nil {
232255
return nil, err
233256
}
234257

235-
if result == nil {
236-
return nil, fmt.Errorf("result is nil")
237-
}
238-
239-
// TODO: once we have proper state machine for credit purchases, we can remove this and mek the
240-
// autoAdvanceCreatedCharges handle the invoice now actions.
241-
if len(result.collectionAlignmentBypassedLines) > 0 {
242-
if err := s.invokeInvoiceNowOnCreate(ctx, result.collectionAlignmentBypassedLines); err != nil {
243-
return nil, fmt.Errorf("invoking invoice now on create: %w", err)
244-
}
245-
}
246-
247-
return s.autoAdvanceCreatedCharges(ctx, result.charges)
258+
return result, nil
248259
}
249260

250261
// autoAdvanceCreatedCharges post-processes newly created charges
@@ -379,9 +390,14 @@ type invoicePendingLinesInput struct {
379390
LineID string
380391
}
381392

382-
func (s *service) createGatheringLines(ctx context.Context, gatheringLinesToCreate []gatheringLineWithCustomerID) ([]invoicePendingLinesInput, error) {
393+
type createGatheringLinesResult struct {
394+
collectionAlignmentBypassedLines []invoicePendingLinesInput
395+
pendingLineResults []*billing.CreatePendingInvoiceLinesResult
396+
}
397+
398+
func (s *service) createGatheringLines(ctx context.Context, gatheringLinesToCreate []gatheringLineWithCustomerID) (createGatheringLinesResult, error) {
383399
if len(gatheringLinesToCreate) == 0 {
384-
return nil, nil
400+
return createGatheringLinesResult{}, nil
385401
}
386402

387403
gatheringLinesByCurrencyAndCustomer := lo.GroupBy(gatheringLinesToCreate, func(item gatheringLineWithCustomerID) currencyAndCustomerID {
@@ -391,7 +407,10 @@ func (s *service) createGatheringLines(ctx context.Context, gatheringLinesToCrea
391407
}
392408
})
393409

394-
invoiceNowLines := make([]invoicePendingLinesInput, 0, len(gatheringLinesToCreate))
410+
out := createGatheringLinesResult{
411+
collectionAlignmentBypassedLines: make([]invoicePendingLinesInput, 0, len(gatheringLinesToCreate)),
412+
pendingLineResults: make([]*billing.CreatePendingInvoiceLinesResult, 0, len(gatheringLinesByCurrencyAndCustomer)),
413+
}
395414

396415
for custAndCurrency, lines := range gatheringLinesByCurrencyAndCustomer {
397416
// Let's create the gathering invoice on invoicing side
@@ -403,18 +422,43 @@ func (s *service) createGatheringLines(ctx context.Context, gatheringLinesToCrea
403422
}),
404423
})
405424
if err != nil {
406-
return nil, fmt.Errorf("creating pending invoice lines for charges: %w", err)
425+
return createGatheringLinesResult{}, fmt.Errorf("creating pending invoice lines for charges: %w", err)
426+
}
427+
if result == nil {
428+
return createGatheringLinesResult{}, fmt.Errorf("creating pending invoice lines for charges: result is nil")
407429
}
408430

409-
for idx, line := range result.Lines {
410-
if lines[idx].BypassCollectionAlignment {
411-
invoiceNowLines = append(invoiceNowLines, invoicePendingLinesInput{
431+
out.pendingLineResults = append(out.pendingLineResults, result)
432+
433+
// Correlate the returned lines back to their inputs by charge ID rather than by
434+
// position: billing may drop lines (e.g. zero-amount lines), which would make
435+
// index-based correlation silently read BypassCollectionAlignment from the wrong line.
436+
bypassChargeIDs := make(map[string]struct{}, len(lines))
437+
for _, line := range lines {
438+
if !line.BypassCollectionAlignment {
439+
continue
440+
}
441+
442+
if line.gatheringLine.ChargeID == nil {
443+
return createGatheringLinesResult{}, fmt.Errorf("creating pending invoice lines for charges: bypass collection alignment requested for line without charge ID")
444+
}
445+
446+
bypassChargeIDs[*line.gatheringLine.ChargeID] = struct{}{}
447+
}
448+
449+
for _, line := range result.Lines {
450+
if line.ChargeID == nil {
451+
continue
452+
}
453+
454+
if _, ok := bypassChargeIDs[*line.ChargeID]; ok {
455+
out.collectionAlignmentBypassedLines = append(out.collectionAlignmentBypassedLines, invoicePendingLinesInput{
412456
CustomerID: custAndCurrency.customerID,
413457
LineID: line.ID,
414458
})
415459
}
416460
}
417461
}
418462

419-
return invoiceNowLines, nil
463+
return out, nil
420464
}

0 commit comments

Comments
 (0)