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
64 changes: 62 additions & 2 deletions openmeter/billing/charges/charge.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,22 @@ import (
"github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase"
"github.com/openmeterio/openmeter/openmeter/billing/charges/flatfee"
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
"github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased"
)

type Charge struct {
t meta.ChargeType

flatFee *flatfee.Charge
usageBased *usagebased.Charge
creditPurchase *creditpurchase.Charge
}

func (c Charge) Type() meta.ChargeType {
return c.t
}

func NewCharge[T flatfee.Charge | creditpurchase.Charge](ch T) Charge {
func NewCharge[T flatfee.Charge | usagebased.Charge | creditpurchase.Charge](ch T) Charge {
switch v := any(ch).(type) {
case flatfee.Charge:
return Charge{
Expand All @@ -32,6 +34,11 @@ func NewCharge[T flatfee.Charge | creditpurchase.Charge](ch T) Charge {
t: meta.ChargeTypeCreditPurchase,
creditPurchase: &v,
}
case usagebased.Charge:
return Charge{
t: meta.ChargeTypeUsageBased,
usageBased: &v,
}
}

return Charge{}
Expand All @@ -51,6 +58,12 @@ func (c Charge) Validate() error {
}

return c.creditPurchase.Validate()
case meta.ChargeTypeUsageBased:
if c.usageBased == nil {
return fmt.Errorf("usage based charge is nil")
}

return c.usageBased.Validate()
}

return fmt.Errorf("invalid charge type: %s", c.t)
Expand Down Expand Up @@ -80,6 +93,14 @@ func (c Charge) AsCreditPurchaseCharge() (creditpurchase.Charge, error) {
return *c.creditPurchase, nil
}

func (c Charge) AsUsageBasedCharge() (usagebased.Charge, error) {
if c.t != meta.ChargeTypeUsageBased {
return usagebased.Charge{}, fmt.Errorf("charge is not a usage based charge")
}

return *c.usageBased, nil
}

func (c Charge) GetChargeID() (meta.ChargeID, error) {
switch c.t {
case meta.ChargeTypeFlatFee:
Expand All @@ -94,6 +115,12 @@ func (c Charge) GetChargeID() (meta.ChargeID, error) {
}

return c.creditPurchase.GetChargeID(), nil
case meta.ChargeTypeUsageBased:
if c.usageBased == nil {
return meta.ChargeID{}, fmt.Errorf("usage based charge is nil")
}

return c.usageBased.GetChargeID(), nil
}

return meta.ChargeID{}, fmt.Errorf("invalid charge type: %s", c.t)
Expand All @@ -118,9 +145,10 @@ type ChargeIntent struct {

flatFee *flatfee.Intent
creditPurchase *creditpurchase.Intent
usageBased *usagebased.Intent
}

func NewChargeIntent[T flatfee.Intent | creditpurchase.Intent](ch T) ChargeIntent {
func NewChargeIntent[T flatfee.Intent | usagebased.Intent | creditpurchase.Intent](ch T) ChargeIntent {
switch v := any(ch).(type) {
case flatfee.Intent:
return ChargeIntent{
Expand All @@ -132,6 +160,11 @@ func NewChargeIntent[T flatfee.Intent | creditpurchase.Intent](ch T) ChargeInten
t: meta.ChargeTypeCreditPurchase,
creditPurchase: &v,
}
case usagebased.Intent:
return ChargeIntent{
t: meta.ChargeTypeUsageBased,
usageBased: &v,
}
}

return ChargeIntent{}
Expand All @@ -155,6 +188,12 @@ func (i ChargeIntent) Validate() error {
}

return i.creditPurchase.Validate()
case meta.ChargeTypeUsageBased:
if i.usageBased == nil {
return fmt.Errorf("usage based is nil")
}

return i.usageBased.Validate()
}

return fmt.Errorf("invalid charge type: %s", i.t)
Expand Down Expand Up @@ -184,6 +223,18 @@ func (i ChargeIntent) AsCreditPurchaseIntent() (creditpurchase.Intent, error) {
return *i.creditPurchase, nil
}

func (i ChargeIntent) AsUsageBasedIntent() (usagebased.Intent, error) {
if i.t != meta.ChargeTypeUsageBased {
return usagebased.Intent{}, fmt.Errorf("charge is not a usage based charge")
}

if i.usageBased == nil {
return usagebased.Intent{}, fmt.Errorf("usage based is nil")
}

return *i.usageBased, nil
}

type ChargeIntents []ChargeIntent

func (i ChargeIntents) Validate() error {
Expand All @@ -201,12 +252,14 @@ func (i ChargeIntents) Validate() error {
type ChargeIntentsByType struct {
FlatFee []WithIndex[flatfee.Intent]
CreditPurchase []WithIndex[creditpurchase.Intent]
UsageBased []WithIndex[usagebased.Intent]
}

func (i ChargeIntents) ByType() (ChargeIntentsByType, error) {
out := ChargeIntentsByType{
FlatFee: make([]WithIndex[flatfee.Intent], 0, len(i)),
CreditPurchase: make([]WithIndex[creditpurchase.Intent], 0, len(i)),
UsageBased: make([]WithIndex[usagebased.Intent], 0, len(i)),
}

for idx, ch := range i {
Expand All @@ -225,6 +278,13 @@ func (i ChargeIntents) ByType() (ChargeIntentsByType, error) {
}

out.CreditPurchase = append(out.CreditPurchase, WithIndex[creditpurchase.Intent]{Index: idx, Value: creditPurchase})
case meta.ChargeTypeUsageBased:
usageBased, err := ch.AsUsageBasedIntent()
if err != nil {
return ChargeIntentsByType{}, fmt.Errorf("converting usage based intent[%d]: %w", idx, err)
}

out.UsageBased = append(out.UsageBased, WithIndex[usagebased.Intent]{Index: idx, Value: usageBased})
default:
return ChargeIntentsByType{}, fmt.Errorf("unsupported charge type[%d]: %s", idx, ch.Type())
}
Expand Down
43 changes: 34 additions & 9 deletions openmeter/billing/charges/service/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/openmeterio/openmeter/openmeter/billing/charges"
"github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase"
"github.com/openmeterio/openmeter/openmeter/billing/charges/flatfee"
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
"github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased"
"github.com/openmeterio/openmeter/openmeter/customer"
"github.com/openmeterio/openmeter/pkg/currencyx"
"github.com/openmeterio/openmeter/pkg/framework/transaction"
Expand All @@ -21,14 +21,6 @@ func (s *service) Create(ctx context.Context, input charges.CreateInput) (charge
return nil, err
}

// Let's validate for unsupported charge types while we are building out the service
for _, charge := range input.Intents {
switch charge.Type() {
case meta.ChargeTypeUsageBased:
return nil, fmt.Errorf("unsupported charge type %s: %w", charge.Type(), meta.ErrUnsupported)
}
}

return transaction.Run(ctx, s.adapter, func(ctx context.Context) (charges.Charges, error) {
intentsByType, err := input.Intents.ByType()
if err != nil {
Expand Down Expand Up @@ -71,6 +63,39 @@ func (s *service) Create(ctx context.Context, input charges.CreateInput) (charge
}
}

// Let's create all the usage based charges in bulk
usageBasedCharges, err := s.usageBasedService.Create(ctx, usagebased.CreateInput{
Namespace: input.Namespace,
Intents: lo.Map(intentsByType.UsageBased, func(intent charges.WithIndex[usagebased.Intent], _ int) usagebased.Intent {
return intent.Value
}),
})
if err != nil {
return nil, err
}

createdCharges = append(
createdCharges,
lo.Map(usageBasedCharges, func(charge usagebased.ChargeWithGatheringLine, idx int) charges.WithIndex[charges.Charge] {
return charges.WithIndex[charges.Charge]{
Index: intentsByType.UsageBased[idx].Index,
Value: charges.NewCharge(charge.Charge),
}
})...,
)

for _, charge := range usageBasedCharges {
if charge.GatheringLineToCreate != nil {
gatheringLinesToCreate = append(gatheringLinesToCreate, gatheringLineWithCustomerID{
gatheringLine: *charge.GatheringLineToCreate,
customerID: customer.CustomerID{
Namespace: input.Namespace,
ID: charge.Charge.Intent.CustomerID,
},
})
}
}

// Let's generate the gathering lines for the flat fees
if err := s.createGatheringLines(ctx, gatheringLinesToCreate); err != nil {
return nil, err
Expand Down
23 changes: 19 additions & 4 deletions openmeter/billing/charges/service/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase"
"github.com/openmeterio/openmeter/openmeter/billing/charges/flatfee"
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
"github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased"
"github.com/openmeterio/openmeter/pkg/framework/transaction"
)

Expand Down Expand Up @@ -70,10 +71,6 @@ func (s *service) GetByIDs(ctx context.Context, input charges.GetByIDsInput) (ch
if err := refType.Validate(); err != nil {
return nil, err
}

if refType == meta.ChargeTypeUsageBased {
return nil, fmt.Errorf("usage based charges are not supported: %w", meta.ErrUnsupported)
}
}

out := make(charges.Charges, len(chargesWithIndex))
Expand All @@ -97,6 +94,24 @@ func (s *service) GetByIDs(ctx context.Context, input charges.GetByIDsInput) (ch
nrFetched++
}

// Let's fetch usage based charges
usageBasedCharges, err := s.usageBasedService.GetByIDs(ctx, usagebased.GetByIDsInput{
Namespace: input.Namespace,
Charges: lo.Map(chargesByType[meta.ChargeTypeUsageBased], func(chargeMeta charges.WithIndex[meta.Charge], _ int) meta.Charge {
return chargeMeta.Value
}),
Expands: input.Expands,
})
if err != nil {
return nil, err
}

for i, usageBasedCharge := range usageBasedCharges {
targetIndex := chargesByType[meta.ChargeTypeUsageBased][i].Index
out[targetIndex] = charges.NewCharge(usageBasedCharge)
nrFetched++
}

// Let's fetch credit purchases
creditPurchases, err := s.creditPurchaseService.GetByIDs(ctx, creditpurchase.GetByIDsInput{
Namespace: input.Namespace,
Expand Down
10 changes: 10 additions & 0 deletions openmeter/billing/charges/service/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ func (h *creditPurchaseTestHandler) Reset() {
*h = creditPurchaseTestHandler{}
}

type usageBasedTestHandler struct{}

func newUsageBasedTestHandler() *usageBasedTestHandler {
return &usageBasedTestHandler{}
}

func (h *usageBasedTestHandler) Reset() {
*h = usageBasedTestHandler{}
}

// helpers

type countedLedgerTransactionCallback[T any] struct {
Expand Down
8 changes: 8 additions & 0 deletions openmeter/billing/charges/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase"
"github.com/openmeterio/openmeter/openmeter/billing/charges/flatfee"
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
"github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased"
)

type service struct {
Expand All @@ -18,6 +19,7 @@ type service struct {

flatFeeService flatfee.Service
creditPurchaseService creditpurchase.Service
usageBasedService usagebased.Service
}

type Config struct {
Expand All @@ -26,6 +28,7 @@ type Config struct {

FlatFeeService flatfee.Service
CreditPurchaseService creditpurchase.Service
UsageBasedService usagebased.Service

BillingService billing.Service
}
Expand All @@ -49,6 +52,10 @@ func (c Config) Validate() error {
errs = append(errs, errors.New("credit purchase service cannot be null"))
}

if c.UsageBasedService == nil {
errs = append(errs, errors.New("usage based service cannot be null"))
}

if c.MetaAdapter == nil {
errs = append(errs, errors.New("meta adapter cannot be null"))
}
Expand All @@ -67,6 +74,7 @@ func New(config Config) (*service, error) {
metaAdapter: config.MetaAdapter,
flatFeeService: config.FlatFeeService,
creditPurchaseService: config.CreditPurchaseService,
usageBasedService: config.UsageBasedService,
}

standardInvoiceEventHandler := &standardInvoiceEventHandler{
Expand Down
Loading
Loading