Skip to content
Merged
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
11 changes: 7 additions & 4 deletions app/common/billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func NewBillingRegistry(
if err != nil {
return BillingRegistry{}, err
}
subscriptionSyncService, err := NewBillingSubscriptionSyncService(logger, subscriptionServices, billingRegistry, subscriptionSyncAdapter, tracer)
subscriptionSyncService, err := NewBillingSubscriptionSyncService(logger, subscriptionServices, billingRegistry, subscriptionSyncAdapter, tracer, creditsConfig)
if err != nil {
return BillingRegistry{}, err
}
Expand Down Expand Up @@ -276,13 +276,16 @@ func NewBillingSubscriptionSyncAdapter(db *entdb.Client) (subscriptionsync.Adapt
})
}

func NewBillingSubscriptionSyncService(logger *slog.Logger, subsServices SubscriptionServiceWithWorkflow, billingRegistry BillingRegistry, subscriptionSyncAdapter subscriptionsync.Adapter, tracer trace.Tracer) (subscriptionsync.Service, error) {
func NewBillingSubscriptionSyncService(logger *slog.Logger, subsServices SubscriptionServiceWithWorkflow, billingRegistry BillingRegistry, subscriptionSyncAdapter subscriptionsync.Adapter, tracer trace.Tracer, creditsConfig config.CreditsConfiguration) (subscriptionsync.Service, error) {
return subscriptionsyncservice.New(subscriptionsyncservice.Config{
SubscriptionService: subsServices.Service,
BillingService: billingRegistry.Billing,
ChargesService: billingRegistry.ChargesServiceOrNil(),
SubscriptionSyncAdapter: subscriptionSyncAdapter,
Logger: logger,
Tracer: tracer,
FeatureFlags: subscriptionsyncservice.FeatureFlags{
EnableCreditThenInvoice: creditsConfig.EnableCreditThenInvoice,
},
Logger: logger,
Tracer: tracer,
})
}
3 changes: 2 additions & 1 deletion app/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ func TestComplete(t *testing.T) {
},
},
Credits: CreditsConfiguration{
Enabled: false,
Enabled: false,
EnableCreditThenInvoice: false,
},
Sink: SinkConfiguration{
GroupId: "openmeter-sink-worker",
Expand Down
4 changes: 3 additions & 1 deletion app/config/credits.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import (
)

type CreditsConfiguration struct {
Enabled bool `yaml:"enabled"`
Enabled bool `yaml:"enabled"`
EnableCreditThenInvoice bool `yaml:"enable_credit_then_invoice"`
}

func (c CreditsConfiguration) Validate() error {
Expand All @@ -20,4 +21,5 @@ func (c CreditsConfiguration) Validate() error {

func ConfigureCredits(v *viper.Viper) {
v.SetDefault("credits.enabled", false)
v.SetDefault("credits.enable_credit_then_invoice", false)
}
2 changes: 1 addition & 1 deletion cmd/billing-worker/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cmd/jobs/internal/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ billing:

credits:
enabled: true
enable_credit_then_invoice: false

apps:
baseURL: https://example.com
Expand Down
1 change: 1 addition & 0 deletions e2e/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ postgres:
# to verify that credit_only settlement mode is rejected when credits are disabled.
credits:
enabled: false
enable_credit_then_invoice: false

meters:
- slug: ingest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,44 @@ type patchCollectionRouter struct {
hierarchyCollection *lineHierarchyPatchCollection
flatFeeChargeCollection *flatFeeChargeCollection
usageBasedChargeCollection *usageBasedChargeCollection
creditThenInvoiceEnabled bool
creditsEnabled bool
}

func newPatchCollectionRouter(capacity int, invoices persistedstate.Invoices) (*patchCollectionRouter, error) {
if invoices == nil {
return nil, fmt.Errorf("invoices is required")
type patchCollectionRouterConfig struct {
capacity int
invoices persistedstate.Invoices
creditThenInvoiceEnabled bool
creditsEnabled bool
}

func (c patchCollectionRouterConfig) Validate() error {
if c.capacity <= 0 {
return fmt.Errorf("capacity is required")
}
Comment thread
turip marked this conversation as resolved.
if c.invoices == nil {
return fmt.Errorf("invoices is required")
}
return nil
}

lineCollection, err := newLineInvoicePatchCollection(invoices, capacity)
func newPatchCollectionRouter(cfg patchCollectionRouterConfig) (*patchCollectionRouter, error) {
if err := cfg.Validate(); err != nil {
return nil, err
}

lineCollection, err := newLineInvoicePatchCollection(cfg.invoices, cfg.capacity)
if err != nil {
return nil, fmt.Errorf("creating line collection: %w", err)
}

return &patchCollectionRouter{
lineCollection: lineCollection,
hierarchyCollection: newLineHierarchyPatchCollection(capacity),
flatFeeChargeCollection: newFlatFeeChargeCollection(capacity),
usageBasedChargeCollection: newUsageBasedChargeCollection(capacity),
hierarchyCollection: newLineHierarchyPatchCollection(cfg.capacity),
flatFeeChargeCollection: newFlatFeeChargeCollection(cfg.capacity),
usageBasedChargeCollection: newUsageBasedChargeCollection(cfg.capacity),
creditThenInvoiceEnabled: cfg.creditThenInvoiceEnabled,
creditsEnabled: cfg.creditsEnabled,
}, nil
}

Expand All @@ -96,7 +117,12 @@ func (c patchCollectionRouter) GetCollectionFor(item persistedstate.Item) (Patch
}

func (c patchCollectionRouter) ResolveDefaultCollection(target targetstate.StateItem) (PatchCollection, error) {
if target.Subscription.SettlementMode != productcatalog.CreditOnlySettlementMode {
if !c.creditsEnabled {
return c.lineCollection, nil
}

// If credit then invoice is not enabled, we return the lineCollection which is generally an invoice_only settlement mode implementation.
if target.Subscription.SettlementMode == productcatalog.CreditThenInvoiceSettlementMode && !c.creditThenInvoiceEnabled {
return c.lineCollection, nil
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package reconciler

import (
"testing"

"github.com/alpacahq/alpacadecimal"
"github.com/stretchr/testify/require"

"github.com/openmeterio/openmeter/openmeter/billing/worker/subscriptionsync/service/persistedstate"
"github.com/openmeterio/openmeter/openmeter/billing/worker/subscriptionsync/service/targetstate"
"github.com/openmeterio/openmeter/openmeter/productcatalog"
"github.com/openmeterio/openmeter/openmeter/subscription"
)

func TestPatchCollectionRouterResolveDefaultCollection(t *testing.T) {
t.Parallel()

flatRateCard := &productcatalog.FlatFeeRateCard{
RateCardMeta: productcatalog.RateCardMeta{
Key: "flat",
Price: productcatalog.NewPriceFrom(productcatalog.FlatPrice{
Amount: alpacadecimal.NewFromInt(100),
PaymentTerm: productcatalog.InAdvancePaymentTerm,
}),
},
}
usageRateCard := &productcatalog.UsageBasedRateCard{
RateCardMeta: productcatalog.RateCardMeta{
Key: "usage",
Price: productcatalog.NewPriceFrom(productcatalog.UnitPrice{Amount: alpacadecimal.NewFromInt(1)}),
},
}

testCases := []struct {
name string
settlementMode productcatalog.SettlementMode
enableCreditThenInvoice bool
enableCredits bool
rateCard productcatalog.RateCard
expectedCollection any
}{
{
name: "invoice only stays on invoice lines",
settlementMode: productcatalog.InvoiceOnlySettlementMode,
rateCard: flatRateCard,
expectedCollection: &lineInvoicePatchCollection{},
},
{
name: "credit only flat fee uses flat fee charges",
settlementMode: productcatalog.CreditOnlySettlementMode,
rateCard: flatRateCard,
expectedCollection: &flatFeeChargeCollection{},
enableCredits: true,
},
{
name: "credit only usage uses usage based charges",
settlementMode: productcatalog.CreditOnlySettlementMode,
rateCard: usageRateCard,
expectedCollection: &usageBasedChargeCollection{},
enableCredits: true,
},
{
name: "credit then invoice disabled stays on invoice lines",
settlementMode: productcatalog.CreditThenInvoiceSettlementMode,
enableCredits: true,
enableCreditThenInvoice: false,
rateCard: flatRateCard,
expectedCollection: &lineInvoicePatchCollection{},
},
{
name: "credit then invoice enabled flat fee uses flat fee charges",
settlementMode: productcatalog.CreditThenInvoiceSettlementMode,
enableCredits: true,
enableCreditThenInvoice: true,
rateCard: flatRateCard,
expectedCollection: &flatFeeChargeCollection{},
},
{
name: "credit then invoice enabled usage uses usage based charges",
settlementMode: productcatalog.CreditThenInvoiceSettlementMode,
enableCredits: true,
enableCreditThenInvoice: true,
rateCard: usageRateCard,
expectedCollection: &usageBasedChargeCollection{},
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

router, err := newPatchCollectionRouter(patchCollectionRouterConfig{
capacity: 1,
invoices: persistedstate.Invoices{},
creditThenInvoiceEnabled: tt.enableCreditThenInvoice,
creditsEnabled: tt.enableCredits,
})
require.NoError(t, err)

collection, err := router.ResolveDefaultCollection(testTargetStateItem(tt.settlementMode, tt.rateCard))
require.NoError(t, err)
require.IsType(t, tt.expectedCollection, collection)
})
}
}

func testTargetStateItem(settlementMode productcatalog.SettlementMode, rateCard productcatalog.RateCard) targetstate.StateItem {
return targetstate.StateItem{
SubscriptionItemWithPeriods: targetstate.SubscriptionItemWithPeriods{
UniqueID: "item-1",
SubscriptionItemView: subscription.SubscriptionItemView{
Spec: subscription.SubscriptionItemSpec{
CreateSubscriptionItemInput: subscription.CreateSubscriptionItemInput{
CreateSubscriptionItemPlanInput: subscription.CreateSubscriptionItemPlanInput{
PhaseKey: "phase-1",
ItemKey: "item-1",
RateCard: rateCard,
},
},
},
},
},
Subscription: subscription.Subscription{
SettlementMode: settlementMode,
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,33 @@ type Reconciler interface {
}

type Config struct {
BillingService billing.Service
ChargesService charges.Service
Logger *slog.Logger
BillingService billing.Service
ChargesService charges.Service
EnableCreditThenInvoice bool
Logger *slog.Logger
}

func (c Config) Validate() error {
if c.BillingService == nil {
return fmt.Errorf("billing service is required")
}

if c.Logger == nil {
return fmt.Errorf("logger is required")
}

if c.EnableCreditThenInvoice && c.ChargesService == nil {
return fmt.Errorf("charges service is required when credit then invoice is enabled")
}

return nil
}

type Service struct {
billingService billing.Service
chargesService charges.Service
logger *slog.Logger
billingService billing.Service
chargesService charges.Service
logger *slog.Logger
enableCreditThenInvoice bool

invoiceUpdater *invoiceupdater.Updater
}
Expand All @@ -57,10 +65,11 @@ func New(config Config) (*Service, error) {
}

return &Service{
billingService: config.BillingService,
logger: config.Logger,
invoiceUpdater: invoiceupdater.New(config.BillingService, config.Logger),
chargesService: config.ChargesService,
billingService: config.BillingService,
logger: config.Logger,
invoiceUpdater: invoiceupdater.New(config.BillingService, config.Logger),
chargesService: config.ChargesService,
enableCreditThenInvoice: config.EnableCreditThenInvoice && config.ChargesService != nil,
}, nil
}

Expand Down Expand Up @@ -205,7 +214,13 @@ func (s *Service) Plan(ctx context.Context, input PlanInput) (*Plan, error) {
return nil, fmt.Errorf("credit only settlement mode is not supported without charges service enabled")
}

patchCollections, err := newPatchCollectionRouter(len(input.Target.Items)+len(input.Persisted.ByUniqueID), input.Persisted.Invoices)
patchCollections, err := newPatchCollectionRouter(
patchCollectionRouterConfig{
capacity: len(input.Target.Items) + len(input.Persisted.ByUniqueID),
invoices: input.Persisted.Invoices,
creditThenInvoiceEnabled: s.enableCreditThenInvoice,
creditsEnabled: s.chargesService != nil,
})
if err != nil {
return nil, fmt.Errorf("creating collection by type: %w", err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
type FeatureFlags struct {
EnableFlatFeeInAdvanceProrating bool
EnableFlatFeeInArrearsProrating bool
EnableCreditThenInvoice bool
}

type Config struct {
Expand Down Expand Up @@ -73,9 +74,10 @@ func New(config Config) (*Service, error) {
return nil, err
}
reconcilerSvc, err := reconciler.New(reconciler.Config{
BillingService: config.BillingService,
ChargesService: config.ChargesService,
Logger: config.Logger,
BillingService: config.BillingService,
ChargesService: config.ChargesService,
EnableCreditThenInvoice: config.FeatureFlags.EnableCreditThenInvoice,
Logger: config.Logger,
})
if err != nil {
return nil, err
Expand Down
Loading