diff --git a/api/v3/handlers/currencies/handler.go b/api/v3/handlers/currencies/handler.go index e64f2fc942..a98846f568 100644 --- a/api/v3/handlers/currencies/handler.go +++ b/api/v3/handlers/currencies/handler.go @@ -2,6 +2,8 @@ package currencies import ( "context" + "errors" + "fmt" "github.com/openmeterio/openmeter/openmeter/currencies" "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" @@ -24,10 +26,21 @@ func New( resolveNamespace func(ctx context.Context) (string, error), currencyService currencies.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if resolveNamespace == nil { + errs = append(errs, errors.New("namespace resolver is required")) + } + if currencyService == nil { + errs = append(errs, errors.New("currency service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid currency handler config: %w", err) + } + return &handler{ resolveNamespace: resolveNamespace, options: options, service: currencyService, - } + }, nil } diff --git a/api/v3/handlers/customers/credits/disabled.go b/api/v3/handlers/customers/credits/disabled.go new file mode 100644 index 0000000000..d068e68d47 --- /dev/null +++ b/api/v3/handlers/customers/credits/disabled.go @@ -0,0 +1,93 @@ +package customerscredits + +import ( + "context" + "net/http" + + "github.com/openmeterio/openmeter/api/v3/apierrors" + "github.com/openmeterio/openmeter/pkg/framework/commonhttp" + "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" +) + +const creditsDisabledPrecondition = "credits is not enabled" + +type disabledHandler struct { + options []httptransport.HandlerOption +} + +func NewDisabled(options ...httptransport.HandlerOption) Handler { + return disabledHandler{ + options: options, + } +} + +func (h disabledHandler) GetCustomerCreditBalance() GetCustomerCreditBalanceHandler { + return disabledHandlerWithArgs[GetCustomerCreditBalanceRequest, GetCustomerCreditBalanceResponse, GetCustomerCreditBalanceParams]( + "get-customer-credit-balance", + h.options, + ) +} + +func (h disabledHandler) ListCreditGrants() ListCreditGrantsHandler { + return disabledHandlerWithArgs[ListCreditGrantsRequest, ListCreditGrantsResponse, ListCreditGrantsParams]( + "list-credit-grants", + h.options, + ) +} + +func (h disabledHandler) CreateCreditGrant() CreateCreditGrantHandler { + return disabledHandlerWithArgs[CreateCreditGrantRequest, CreateCreditGrantResponse, CreateCreditGrantParams]( + "create-credit-grant", + h.options, + ) +} + +func (h disabledHandler) GetCreditGrant() GetCreditGrantHandler { + return disabledHandlerWithArgs[GetCreditGrantRequest, GetCreditGrantResponse, GetCreditGrantParams]( + "get-credit-grant", + h.options, + ) +} + +func (h disabledHandler) VoidCreditGrant() VoidCreditGrantHandler { + return disabledHandlerWithArgs[VoidCreditGrantRequest, VoidCreditGrantResponse, VoidCreditGrantParams]( + "void-credit-grant", + h.options, + ) +} + +func (h disabledHandler) UpdateCreditGrantExternalSettlement() UpdateCreditGrantExternalSettlementHandler { + return disabledHandlerWithArgs[UpdateCreditGrantExternalSettlementRequest, UpdateCreditGrantExternalSettlementResponse, UpdateCreditGrantExternalSettlementParams]( + "update-credit-grant-external-settlement", + h.options, + ) +} + +func (h disabledHandler) ListCreditTransactions() ListCreditTransactionsHandler { + return disabledHandlerWithArgs[ListCreditTransactionsRequest, ListCreditTransactionsResponse, ListCreditTransactionsParams]( + "list-credit-transactions", + h.options, + ) +} + +func disabledHandlerWithArgs[Request, Response, Params any]( + operationName string, + options []httptransport.HandlerOption, +) httptransport.HandlerWithArgs[Request, Response, Params] { + return httptransport.NewHandlerWithArgs( + func(ctx context.Context, _ *http.Request, _ Params) (Request, error) { + var request Request + return request, apierrors.NewPreconditionFailedError(ctx, creditsDisabledPrecondition) + }, + func(context.Context, Request) (Response, error) { + var response Response + return response, nil + }, + commonhttp.JSONResponseEncoder[Response], + httptransport.AppendOptions( + options, + httptransport.WithOperationName(operationName), + httptransport.WithErrorEncoder(apierrors.GenericErrorEncoder()), + )..., + ) +} diff --git a/api/v3/server/routes.go b/api/v3/server/routes.go index 33cd61850e..38205817e6 100644 --- a/api/v3/server/routes.go +++ b/api/v3/server/routes.go @@ -441,7 +441,7 @@ func (s *Server) DeletePlanAddon(w http.ResponseWriter, r *http.Request, planId var unimplemented = api.Unimplemented{} func (s *Server) GetCustomerCreditBalance(w http.ResponseWriter, r *http.Request, customerId api.ULID, params api.GetCustomerCreditBalanceParams) { - if !s.Credits.Enabled || s.customersCreditsHandler == nil { + if s.customersCreditsHandler == nil { unimplemented.GetCustomerCreditBalance(w, r, customerId, params) return } @@ -453,7 +453,7 @@ func (s *Server) GetCustomerCreditBalance(w http.ResponseWriter, r *http.Request } func (s *Server) ListCreditGrants(w http.ResponseWriter, r *http.Request, customerId api.ULID, params api.ListCreditGrantsParams) { - if !s.Credits.Enabled || s.customersCreditsHandler == nil || s.CreditGrantService == nil { + if s.customersCreditsHandler == nil { unimplemented.ListCreditGrants(w, r, customerId, params) return } @@ -465,7 +465,7 @@ func (s *Server) ListCreditGrants(w http.ResponseWriter, r *http.Request, custom } func (s *Server) CreateCreditGrant(w http.ResponseWriter, r *http.Request, customerId api.ULID) { - if !s.Credits.Enabled || s.customersCreditsHandler == nil || s.CreditGrantService == nil { + if s.customersCreditsHandler == nil { unimplemented.CreateCreditGrant(w, r, customerId) return } @@ -476,7 +476,7 @@ func (s *Server) CreateCreditGrant(w http.ResponseWriter, r *http.Request, custo } func (s *Server) GetCreditGrant(w http.ResponseWriter, r *http.Request, customerId api.ULID, creditGrantId api.ULID) { - if !s.Credits.Enabled || s.customersCreditsHandler == nil || s.CreditGrantService == nil { + if s.customersCreditsHandler == nil { unimplemented.GetCreditGrant(w, r, customerId, creditGrantId) return } @@ -492,7 +492,7 @@ func (s *Server) CreateCreditAdjustment(w http.ResponseWriter, r *http.Request, } func (s *Server) VoidCreditGrant(w http.ResponseWriter, r *http.Request, customerId api.ULID, creditGrantId api.ULID) { - if !s.Credits.Enabled || s.customersCreditsHandler == nil || s.CreditGrantService == nil { + if s.customersCreditsHandler == nil { unimplemented.VoidCreditGrant(w, r, customerId, creditGrantId) return } @@ -504,7 +504,7 @@ func (s *Server) VoidCreditGrant(w http.ResponseWriter, r *http.Request, custome } func (s *Server) UpdateCreditGrantExternalSettlement(w http.ResponseWriter, r *http.Request, customerId api.ULID, creditGrantId api.ULID) { - if !s.Credits.Enabled || s.customersCreditsHandler == nil || s.CreditGrantService == nil { + if s.customersCreditsHandler == nil { unimplemented.UpdateCreditGrantExternalSettlement(w, r, customerId, creditGrantId) return } @@ -516,7 +516,7 @@ func (s *Server) UpdateCreditGrantExternalSettlement(w http.ResponseWriter, r *h } func (s *Server) ListCreditTransactions(w http.ResponseWriter, r *http.Request, customerId api.ULID, params api.ListCreditTransactionsParams) { - if !s.Credits.Enabled || s.customersCreditsHandler == nil || s.Ledger == nil { + if s.customersCreditsHandler == nil { unimplemented.ListCreditTransactions(w, r, customerId, params) return } diff --git a/api/v3/server/server.go b/api/v3/server/server.go index 89da116fc2..81f25ac33e 100644 --- a/api/v3/server/server.go +++ b/api/v3/server/server.go @@ -52,7 +52,6 @@ import ( "github.com/openmeterio/openmeter/openmeter/ingest" "github.com/openmeterio/openmeter/openmeter/ledger" "github.com/openmeterio/openmeter/openmeter/ledger/customerbalance" - ledgernoop "github.com/openmeterio/openmeter/openmeter/ledger/noop" "github.com/openmeterio/openmeter/openmeter/llmcost" "github.com/openmeterio/openmeter/openmeter/meter" "github.com/openmeterio/openmeter/openmeter/meterevent" @@ -308,21 +307,22 @@ func NewServer(config *Config) (*Server, error) { eventsHandler := eventshandler.New(resolveNamespace, config.IngestService, config.MeterEventService, httptransport.WithErrorHandler(config.ErrorHandler)) customersHandler := customershandler.New(resolveNamespace, config.CustomerService, httptransport.WithErrorHandler(config.ErrorHandler)) customersBillingHandler := customersbillinghandler.New(resolveNamespace, config.BillingService, config.CustomerService, config.StripeService, httptransport.WithErrorHandler(config.ErrorHandler)) - customerBalanceFacade := config.CustomerBalanceFacade - creditGrantService := config.CreditGrantService - ledgerService := config.Ledger - accountResolver := config.AccountResolver - if !config.Credits.Enabled { - customerBalanceFacade, err = customerbalance.NewFacade(customerbalance.NewNoopService()) - if err != nil { - return nil, fmt.Errorf("create noop customer balance facade: %w", err) - } - - creditGrantService = creditgrant.NewNoopService() - ledgerService = ledgernoop.Ledger{} - accountResolver = ledgernoop.AccountResolver{} + var customersCreditsHandler customerscreditshandler.Handler + if config.Credits.Enabled { + customersCreditsHandler = customerscreditshandler.New( + resolveNamespace, + config.CustomerService, + config.CustomerBalanceFacade, + config.CreditGrantService, + config.Ledger, + config.AccountResolver, + httptransport.WithErrorHandler(config.ErrorHandler), + ) + } else { + customersCreditsHandler = customerscreditshandler.NewDisabled( + httptransport.WithErrorHandler(config.ErrorHandler), + ) } - customersCreditsHandler := customerscreditshandler.New(resolveNamespace, config.CustomerService, customerBalanceFacade, creditGrantService, ledgerService, accountResolver, httptransport.WithErrorHandler(config.ErrorHandler)) customersEntitlementHandler := customersentitlementhandler.New(resolveNamespace, config.CustomerService, config.EntitlementService, httptransport.WithErrorHandler(config.ErrorHandler)) metersHandler := metershandler.New(resolveNamespace, config.MeterService, config.StreamingConnector, config.CustomerService, httptransport.WithErrorHandler(config.ErrorHandler)) subscriptionsHandler := subscriptionshandler.New(resolveNamespace, config.CustomerService, config.PlanService, config.PlanSubscriptionService, config.SubscriptionService, httptransport.WithErrorHandler(config.ErrorHandler)) @@ -332,7 +332,10 @@ func NewServer(config *Config) (*Server, error) { plansHandler := planshandler.New(resolveNamespace, config.PlanService, config.UnitConfig.Enabled, httptransport.WithErrorHandler(config.ErrorHandler)) planAddonsHandler := planaddonshandler.New(resolveNamespace, config.PlanService, config.PlanAddonService, httptransport.WithErrorHandler(config.ErrorHandler)) taxcodesHandler := taxcodeshandler.New(resolveNamespace, config.TaxCodeService, httptransport.WithErrorHandler(config.ErrorHandler)) - currenciesHandler := currencieshandler.New(resolveNamespace, config.CurrencyService, httptransport.WithErrorHandler(config.ErrorHandler)) + currenciesHandler, err := currencieshandler.New(resolveNamespace, config.CurrencyService, httptransport.WithErrorHandler(config.ErrorHandler)) + if err != nil { + return nil, fmt.Errorf("currency handler: %w", err) + } var chargesH chargeshandler.Handler if config.ChargeService != nil { diff --git a/openmeter/app/custominvoicing/httpdriver/handler.go b/openmeter/app/custominvoicing/httpdriver/handler.go index f5fa5733e7..2c2afa160e 100644 --- a/openmeter/app/custominvoicing/httpdriver/handler.go +++ b/openmeter/app/custominvoicing/httpdriver/handler.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "net/http" appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing" @@ -43,10 +44,21 @@ func New( service appcustominvoicing.SyncService, namespaceDecoder namespacedriver.NamespaceDecoder, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if service == nil { + errs = append(errs, errors.New("app custom invoicing service is required")) + } + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid app custom invoicing handler config: %w", err) + } + return &handler{ service: service, namespaceDecoder: namespaceDecoder, options: options, - } + }, nil } diff --git a/openmeter/app/httpdriver/handler.go b/openmeter/app/httpdriver/handler.go index f7a6a05594..3b9ff5c88e 100644 --- a/openmeter/app/httpdriver/handler.go +++ b/openmeter/app/httpdriver/handler.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "log/slog" "net/http" @@ -68,7 +69,30 @@ func New( customerService customer.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if logger == nil { + errs = append(errs, errors.New("logger is required")) + } + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if appService == nil { + errs = append(errs, errors.New("app service is required")) + } + if appStripeService == nil { + errs = append(errs, errors.New("app stripe service is required")) + } + if billingService == nil { + errs = append(errs, errors.New("billing service is required")) + } + if customerService == nil { + errs = append(errs, errors.New("customer service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid app handler config: %w", err) + } + return &handler{ service: appService, namespaceDecoder: namespaceDecoder, @@ -76,5 +100,5 @@ func New( billingService: billingService, customerService: customerService, options: options, - } + }, nil } diff --git a/openmeter/app/stripe/httpdriver/handler.go b/openmeter/app/stripe/httpdriver/handler.go index c8294cf326..adb601c5b8 100644 --- a/openmeter/app/stripe/httpdriver/handler.go +++ b/openmeter/app/stripe/httpdriver/handler.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "net/http" appstripe "github.com/openmeterio/openmeter/openmeter/app/stripe" @@ -55,12 +56,29 @@ func New( billingService billing.Service, customerService customer.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if service == nil { + errs = append(errs, errors.New("app stripe service is required")) + } + if billingService == nil { + errs = append(errs, errors.New("billing service is required")) + } + if customerService == nil { + errs = append(errs, errors.New("customer service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid app stripe handler config: %w", err) + } + return &handler{ service: service, billingService: billingService, customerService: customerService, namespaceDecoder: namespaceDecoder, options: options, - } + }, nil } diff --git a/openmeter/billing/httpdriver/handler.go b/openmeter/billing/httpdriver/handler.go index 10a4313ead..dd4f861dc1 100644 --- a/openmeter/billing/httpdriver/handler.go +++ b/openmeter/billing/httpdriver/handler.go @@ -3,12 +3,12 @@ package httpdriver import ( "context" "errors" + "fmt" "log/slog" "net/http" "github.com/openmeterio/openmeter/app/config" "github.com/openmeterio/openmeter/openmeter/app" - appstripe "github.com/openmeterio/openmeter/openmeter/app/stripe" "github.com/openmeterio/openmeter/openmeter/billing" billingcharges "github.com/openmeterio/openmeter/openmeter/billing/charges" "github.com/openmeterio/openmeter/openmeter/namespace/namespacedriver" @@ -80,12 +80,36 @@ func New( featureSwitches config.BillingFeatureSwitchesConfiguration, service billing.Service, appService app.Service, - stripeAppService appstripe.Service, chargeService billingcharges.ChargeService, credits config.CreditsConfiguration, featureGate *featuregate.FeatureGateChecker, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if logger == nil { + errs = append(errs, errors.New("logger is required")) + } + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if service == nil { + errs = append(errs, errors.New("billing service is required")) + } + if appService == nil { + errs = append(errs, errors.New("app service is required")) + } + if chargeService == nil { + errs = append(errs, errors.New("charge service is required")) + } + if featureGate == nil { + errs = append(errs, errors.New("feature gate is required")) + } else if err := featureGate.Validate(); err != nil { + errs = append(errs, fmt.Errorf("feature gate: %w", err)) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid billing handler config: %w", err) + } + return &handler{ service: service, chargeService: chargeService, @@ -96,5 +120,5 @@ func New( featureSwitches: featureSwitches, credits: credits, featureGate: featureGate, - } + }, nil } diff --git a/openmeter/credit/driver/grant.go b/openmeter/credit/driver/grant.go index 29c9bd02a2..d619836a5f 100644 --- a/openmeter/credit/driver/grant.go +++ b/openmeter/credit/driver/grant.go @@ -3,6 +3,7 @@ package creditdriver import ( "context" "errors" + "fmt" "net/http" "slices" "time" @@ -45,14 +46,31 @@ func NewGrantHandler( grantRepo grant.Repo, customerService customer.Service, options ...httptransport.HandlerOption, -) GrantHandler { +) (GrantHandler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if grantConnector == nil { + errs = append(errs, errors.New("grant connector is required")) + } + if grantRepo == nil { + errs = append(errs, errors.New("grant repo is required")) + } + if customerService == nil { + errs = append(errs, errors.New("customer service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid grant handler config: %w", err) + } + return &grantHandler{ namespaceDecoder: namespaceDecoder, grantConnector: grantConnector, grantRepo: grantRepo, customerService: customerService, options: options, - } + }, nil } type ( diff --git a/openmeter/customer/httpdriver/handler.go b/openmeter/customer/httpdriver/handler.go index 6657acaf12..bff8eba09a 100644 --- a/openmeter/customer/httpdriver/handler.go +++ b/openmeter/customer/httpdriver/handler.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "net/http" "github.com/openmeterio/openmeter/openmeter/customer" @@ -54,12 +55,29 @@ func New( subscriptionService subscription.Service, entitlementService entitlement.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if service == nil { + errs = append(errs, errors.New("customer service is required")) + } + if subscriptionService == nil { + errs = append(errs, errors.New("subscription service is required")) + } + if entitlementService == nil { + errs = append(errs, errors.New("entitlement service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid customer handler config: %w", err) + } + return &handler{ service: service, subscriptionService: subscriptionService, namespaceDecoder: namespaceDecoder, entitlementService: entitlementService, options: options, - } + }, nil } diff --git a/openmeter/debug/httpdriver/metrics.go b/openmeter/debug/httpdriver/metrics.go index b367744d2b..702fd354b7 100644 --- a/openmeter/debug/httpdriver/metrics.go +++ b/openmeter/debug/httpdriver/metrics.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "net/http" "github.com/openmeterio/openmeter/openmeter/debug" @@ -26,12 +27,23 @@ func NewDebugHandler( namespaceDecoder namespacedriver.NamespaceDecoder, debugConnector debug.DebugConnector, options ...httptransport.HandlerOption, -) DebugHandler { +) (DebugHandler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if debugConnector == nil { + errs = append(errs, errors.New("debug connector is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid debug handler config: %w", err) + } + return &debugHandler{ namespaceDecoder: namespaceDecoder, debugConnector: debugConnector, options: options, - } + }, nil } type GetMetricsHandlerRequestParams struct { diff --git a/openmeter/entitlement/driver/entitlement.go b/openmeter/entitlement/driver/entitlement.go index 73fc84c9a6..e1d93e9bc5 100644 --- a/openmeter/entitlement/driver/entitlement.go +++ b/openmeter/entitlement/driver/entitlement.go @@ -52,14 +52,31 @@ func NewEntitlementHandler( subjectService subject.Service, namespaceDecoder namespacedriver.NamespaceDecoder, options ...httptransport.HandlerOption, -) EntitlementHandler { +) (EntitlementHandler, error) { + var errs []error + if connector == nil { + errs = append(errs, errors.New("entitlement connector is required")) + } + if customerService == nil { + errs = append(errs, errors.New("customer service is required")) + } + if subjectService == nil { + errs = append(errs, errors.New("subject service is required")) + } + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid entitlement handler config: %w", err) + } + return &entitlementHandler{ namespaceDecoder: namespaceDecoder, options: options, connector: connector, customerService: customerService, subjectService: subjectService, - } + }, nil } type ( diff --git a/openmeter/entitlement/driver/metered.go b/openmeter/entitlement/driver/metered.go index 5fcbca66f7..5ff243316b 100644 --- a/openmeter/entitlement/driver/metered.go +++ b/openmeter/entitlement/driver/metered.go @@ -50,7 +50,27 @@ func NewMeteredEntitlementHandler( subjectService subject.Service, namespaceDecoder namespacedriver.NamespaceDecoder, options ...httptransport.HandlerOption, -) MeteredEntitlementHandler { +) (MeteredEntitlementHandler, error) { + var errs []error + if entitlementConnector == nil { + errs = append(errs, errors.New("entitlement connector is required")) + } + if balanceConnector == nil { + errs = append(errs, errors.New("entitlement balance connector is required")) + } + if customerService == nil { + errs = append(errs, errors.New("customer service is required")) + } + if subjectService == nil { + errs = append(errs, errors.New("subject service is required")) + } + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid metered entitlement handler config: %w", err) + } + return &meteredEntitlementHandler{ entitlementConnector: entitlementConnector, balanceConnector: balanceConnector, @@ -58,7 +78,7 @@ func NewMeteredEntitlementHandler( subjectService: subjectService, namespaceDecoder: namespaceDecoder, options: options, - } + }, nil } type ( diff --git a/openmeter/entitlement/driver/v2/handler.go b/openmeter/entitlement/driver/v2/handler.go index cccd101a56..30afbc50a0 100644 --- a/openmeter/entitlement/driver/v2/handler.go +++ b/openmeter/entitlement/driver/v2/handler.go @@ -1,6 +1,9 @@ package entitlementdriverv2 import ( + "errors" + "fmt" + "github.com/openmeterio/openmeter/openmeter/customer" "github.com/openmeterio/openmeter/openmeter/entitlement" meteredentitlement "github.com/openmeterio/openmeter/openmeter/entitlement/metered" @@ -37,12 +40,29 @@ func NewEntitlementHandler( customerService customer.Service, namespaceDecoder namespacedriver.NamespaceDecoder, options ...httptransport.HandlerOption, -) EntitlementHandler { +) (EntitlementHandler, error) { + var errs []error + if connector == nil { + errs = append(errs, errors.New("entitlement connector is required")) + } + if balanceConnector == nil { + errs = append(errs, errors.New("entitlement balance connector is required")) + } + if customerService == nil { + errs = append(errs, errors.New("customer service is required")) + } + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid entitlement v2 handler config: %w", err) + } + return &entitlementHandler{ namespaceDecoder: namespaceDecoder, options: options, connector: connector, balanceConnector: balanceConnector, customerService: customerService, - } + }, nil } diff --git a/openmeter/ingest/httpdriver/handler.go b/openmeter/ingest/httpdriver/handler.go index ecc8abcc63..1b8a59f37e 100644 --- a/openmeter/ingest/httpdriver/handler.go +++ b/openmeter/ingest/httpdriver/handler.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "net/http" "github.com/openmeterio/openmeter/openmeter/ingest" @@ -38,10 +39,21 @@ func New( namespaceDecoder namespacedriver.NamespaceDecoder, service ingest.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if service == nil { + errs = append(errs, errors.New("ingest service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid ingest handler config: %w", err) + } + return &handler{ namespaceDecoder: namespaceDecoder, service: service, options: options, - } + }, nil } diff --git a/openmeter/ingest/httpdriver/ingest_test.go b/openmeter/ingest/httpdriver/ingest_test.go index 9043afda24..867d6f10ca 100644 --- a/openmeter/ingest/httpdriver/ingest_test.go +++ b/openmeter/ingest/httpdriver/ingest_test.go @@ -29,10 +29,11 @@ func TestIngestEvents(t *testing.T) { }) require.NoError(t, err) - handler := httpdriver.New( + handler, err := httpdriver.New( namespacedriver.StaticNamespaceDecoder("test"), ingestSvc, ) + require.NoError(t, err) server := httptest.NewServer(handler.IngestEvents()) client := server.Client() @@ -78,10 +79,11 @@ func TestIngestEvents_InvalidEvent(t *testing.T) { }) require.NoError(t, err) - handler := httpdriver.New( + handler, err := httpdriver.New( namespacedriver.StaticNamespaceDecoder("test"), ingestSvc, ) + require.NoError(t, err) server := httptest.NewServer(handler.IngestEvents()) client := server.Client() @@ -103,10 +105,11 @@ func TestBatchHandler(t *testing.T) { }) require.NoError(t, err) - handler := httpdriver.New( + handler, err := httpdriver.New( namespacedriver.StaticNamespaceDecoder("test"), ingestSvc, ) + require.NoError(t, err) server := httptest.NewServer(handler.IngestEvents()) client := server.Client() diff --git a/openmeter/meter/httphandler/handler.go b/openmeter/meter/httphandler/handler.go index a9058a3bc8..9ec95ccf7c 100644 --- a/openmeter/meter/httphandler/handler.go +++ b/openmeter/meter/httphandler/handler.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "net/http" "github.com/openmeterio/openmeter/openmeter/customer" @@ -59,7 +60,27 @@ func New( streaming streaming.Connector, subjectService subject.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if customerService == nil { + errs = append(errs, errors.New("customer service is required")) + } + if meterService == nil { + errs = append(errs, errors.New("meter service is required")) + } + if streaming == nil { + errs = append(errs, errors.New("streaming connector is required")) + } + if subjectService == nil { + errs = append(errs, errors.New("subject service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid meter handler config: %w", err) + } + return &handler{ namespaceDecoder: namespaceDecoder, options: options, @@ -67,5 +88,5 @@ func New( meterService: meterService, streaming: streaming, subjectService: subjectService, - } + }, nil } diff --git a/openmeter/meterevent/httphandler/handler.go b/openmeter/meterevent/httphandler/handler.go index d6e825d9e8..0fa207906c 100644 --- a/openmeter/meterevent/httphandler/handler.go +++ b/openmeter/meterevent/httphandler/handler.go @@ -3,6 +3,7 @@ package httphandler import ( "context" "errors" + "fmt" "net/http" "github.com/openmeterio/openmeter/openmeter/meterevent" @@ -41,10 +42,21 @@ func New( namespaceDecoder namespacedriver.NamespaceDecoder, metereventService meterevent.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if metereventService == nil { + errs = append(errs, errors.New("meter event service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid meter event handler config: %w", err) + } + return &handler{ namespaceDecoder: namespaceDecoder, metereventService: metereventService, options: options, - } + }, nil } diff --git a/openmeter/notification/httpdriver/handler.go b/openmeter/notification/httpdriver/handler.go index c43376be2b..3979f92164 100644 --- a/openmeter/notification/httpdriver/handler.go +++ b/openmeter/notification/httpdriver/handler.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "net/http" "github.com/openmeterio/openmeter/openmeter/billing" @@ -65,11 +66,25 @@ func New( service notification.Service, billingService billing.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if service == nil { + errs = append(errs, errors.New("notification service is required")) + } + if billingService == nil { + errs = append(errs, errors.New("billing service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid notification handler config: %w", err) + } + return &handler{ service: service, testEventGenerator: internal.NewTestEventGenerator(billingService), namespaceDecoder: namespaceDecoder, options: options, - } + }, nil } diff --git a/openmeter/portal/httphandler/handler.go b/openmeter/portal/httphandler/handler.go index 0b33109f78..dc69d833b7 100644 --- a/openmeter/portal/httphandler/handler.go +++ b/openmeter/portal/httphandler/handler.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "net/http" "github.com/openmeterio/openmeter/openmeter/meter" @@ -45,11 +46,25 @@ func New( portalService portal.Service, meterService meter.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if portalService == nil { + errs = append(errs, errors.New("portal service is required")) + } + if meterService == nil { + errs = append(errs, errors.New("meter service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid portal handler config: %w", err) + } + return &handler{ namespaceDecoder: namespaceDecoder, portalService: portalService, meterService: meterService, options: options, - } + }, nil } diff --git a/openmeter/productcatalog/addon/httpdriver/driver.go b/openmeter/productcatalog/addon/httpdriver/driver.go index dca2f3c118..7917962288 100644 --- a/openmeter/productcatalog/addon/httpdriver/driver.go +++ b/openmeter/productcatalog/addon/httpdriver/driver.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "net/http" "github.com/openmeterio/openmeter/openmeter/namespace/namespacedriver" @@ -46,10 +47,21 @@ func New( namespaceDecoder namespacedriver.NamespaceDecoder, service addon.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if service == nil { + errs = append(errs, errors.New("add-on service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid add-on handler config: %w", err) + } + return &handler{ service: service, namespaceDecoder: namespaceDecoder, options: options, - } + }, nil } diff --git a/openmeter/productcatalog/driver/feature.go b/openmeter/productcatalog/driver/feature.go index 4dad4fd358..35bb3669df 100644 --- a/openmeter/productcatalog/driver/feature.go +++ b/openmeter/productcatalog/driver/feature.go @@ -45,14 +45,31 @@ func NewFeatureHandler( meterService meter.Service, llmcostService llmcost.Service, options ...httptransport.HandlerOption, -) FeatureHandler { +) (FeatureHandler, error) { + var errs []error + if connector == nil { + errs = append(errs, errors.New("feature connector is required")) + } + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if meterService == nil { + errs = append(errs, errors.New("meter service is required")) + } + if llmcostService == nil { + errs = append(errs, errors.New("llm cost service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid feature handler config: %w", err) + } + return &featureHandlers{ namespaceDecoder: namespaceDecoder, options: options, connector: connector, meterService: meterService, llmcostService: llmcostService, - } + }, nil } type ( diff --git a/openmeter/productcatalog/plan/httpdriver/driver.go b/openmeter/productcatalog/plan/httpdriver/driver.go index d95ce13682..223e6917dc 100644 --- a/openmeter/productcatalog/plan/httpdriver/driver.go +++ b/openmeter/productcatalog/plan/httpdriver/driver.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "net/http" "github.com/openmeterio/openmeter/openmeter/namespace/namespacedriver" @@ -47,10 +48,21 @@ func New( namespaceDecoder namespacedriver.NamespaceDecoder, service plan.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if service == nil { + errs = append(errs, errors.New("plan service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid plan handler config: %w", err) + } + return &handler{ service: service, namespaceDecoder: namespaceDecoder, options: options, - } + }, nil } diff --git a/openmeter/productcatalog/planaddon/httpdriver/driver.go b/openmeter/productcatalog/planaddon/httpdriver/driver.go index 6858a3b957..11380aa1f0 100644 --- a/openmeter/productcatalog/planaddon/httpdriver/driver.go +++ b/openmeter/productcatalog/planaddon/httpdriver/driver.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "net/http" "github.com/openmeterio/openmeter/openmeter/namespace/namespacedriver" @@ -44,10 +45,21 @@ func New( namespaceDecoder namespacedriver.NamespaceDecoder, service planaddon.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if service == nil { + errs = append(errs, errors.New("plan add-on service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid plan add-on handler config: %w", err) + } + return &handler{ service: service, namespaceDecoder: namespaceDecoder, options: options, - } + }, nil } diff --git a/openmeter/productcatalog/subscription/http/handler.go b/openmeter/productcatalog/subscription/http/handler.go index d29688d4a0..46f55a412a 100644 --- a/openmeter/productcatalog/subscription/http/handler.go +++ b/openmeter/productcatalog/subscription/http/handler.go @@ -3,8 +3,10 @@ package httpdriver import ( "context" "errors" + "fmt" "log/slog" "net/http" + "reflect" appconfig "github.com/openmeterio/openmeter/app/config" "github.com/openmeterio/openmeter/openmeter/customer" @@ -39,6 +41,36 @@ type HandlerConfig struct { Credits appconfig.CreditsConfiguration } +func (c HandlerConfig) Validate() error { + var errs []error + + if isNil(c.SubscriptionWorkflowService) { + errs = append(errs, errors.New("subscription workflow service is required")) + } + + if isNil(c.SubscriptionService) { + errs = append(errs, errors.New("subscription service is required")) + } + + if isNil(c.CustomerService) { + errs = append(errs, errors.New("customer service is required")) + } + + if isNil(c.PlanSubscriptionService) { + errs = append(errs, errors.New("plan subscription service is required")) + } + + if isNil(c.NamespaceDecoder) { + errs = append(errs, errors.New("namespace decoder is required")) + } + + if isNil(c.Logger) { + errs = append(errs, errors.New("logger is required")) + } + + return errors.Join(errs...) +} + type handler struct { HandlerConfig Options []httptransport.HandlerOption @@ -53,9 +85,27 @@ func (h *handler) resolveNamespace(ctx context.Context) (string, error) { return ns, nil } -func NewHandler(config HandlerConfig, options ...httptransport.HandlerOption) Handler { +func NewHandler(config HandlerConfig, options ...httptransport.HandlerOption) (Handler, error) { + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("invalid subscription handler config: %w", err) + } + return &handler{ HandlerConfig: config, Options: options, + }, nil +} + +func isNil(value any) bool { + if value == nil { + return true + } + + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return v.IsNil() + default: + return false } } diff --git a/openmeter/productcatalog/subscription/http/handler_test.go b/openmeter/productcatalog/subscription/http/handler_test.go new file mode 100644 index 0000000000..93d67f820c --- /dev/null +++ b/openmeter/productcatalog/subscription/http/handler_test.go @@ -0,0 +1,49 @@ +package httpdriver + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHandlerConfigValidate(t *testing.T) { + err := HandlerConfig{}.Validate() + require.Error(t, err) + + for _, want := range []string{ + "subscription workflow service is required", + "subscription service is required", + "customer service is required", + "plan subscription service is required", + "namespace decoder is required", + "logger is required", + } { + require.ErrorContains(t, err, want) + } +} + +func TestNewHandlerInvalidConfig(t *testing.T) { + handler, err := NewHandler(HandlerConfig{}) + require.Error(t, err) + require.Nil(t, handler) + require.ErrorContains(t, err, "invalid subscription handler config") + require.ErrorContains(t, err, "plan subscription service is required") +} + +func TestHandlerConfigValidateTypedNilDependency(t *testing.T) { + var namespaceDecoder *nilNamespaceDecoder + + err := HandlerConfig{ + NamespaceDecoder: namespaceDecoder, + }.Validate() + + require.Error(t, err) + require.ErrorContains(t, err, "namespace decoder is required") +} + +type nilNamespaceDecoder struct{} + +func (*nilNamespaceDecoder) GetNamespace(context.Context) (string, bool) { + return "", false +} diff --git a/openmeter/progressmanager/httpdriver/handler.go b/openmeter/progressmanager/httpdriver/handler.go index 2502c960eb..df5b293a41 100644 --- a/openmeter/progressmanager/httpdriver/handler.go +++ b/openmeter/progressmanager/httpdriver/handler.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "net/http" "github.com/openmeterio/openmeter/openmeter/namespace/namespacedriver" @@ -40,10 +41,21 @@ func New( namespaceDecoder namespacedriver.NamespaceDecoder, service progressmanager.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if service == nil { + errs = append(errs, errors.New("progress manager service is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid progress handler config: %w", err) + } + return &handler{ service: service, namespaceDecoder: namespaceDecoder, options: options, - } + }, nil } diff --git a/openmeter/server/router/router.go b/openmeter/server/router/router.go index b20dfc950e..1544187355 100644 --- a/openmeter/server/router/router.go +++ b/openmeter/server/router/router.go @@ -175,6 +175,10 @@ func (c Config) Validate() error { return errors.New("app custom invoicing service is required") } + if c.ChargeService == nil { + return errors.New("charge service is required") + } + if c.Customer == nil { return errors.New("customer service is required") } @@ -207,22 +211,46 @@ func (c Config) Validate() error { return errors.New("grant connector is required") } + if c.GrantRepo == nil { + return errors.New("grant repo is required") + } + if c.LLMCostService == nil { return errors.New("llm cost service is required") } + if c.Logger == nil { + return errors.New("logger is required") + } + if c.MeterManageService == nil { return errors.New("meter manage service is required") } + if c.MeterEventService == nil { + return errors.New("meter event service is required") + } + if c.Notification == nil { return errors.New("notification service is required") } + if c.Plan == nil { + return errors.New("plan service is required") + } + if c.PlanAddon == nil { return errors.New("plan add-on service is required") } + if c.PlanSubscriptionService == nil { + return errors.New("plan subscription service is required") + } + + if c.Portal == nil { + return errors.New("portal service is required") + } + if c.ProgressManager == nil { return errors.New("progress manager service is required") } @@ -251,6 +279,10 @@ func (c Config) Validate() error { return errors.New("tax code service is required") } + if c.FeatureGate == nil { + return errors.New("feature gate is required") + } + if err := c.FeatureGate.Validate(); err != nil { return err } @@ -304,38 +336,60 @@ func NewRouter(config Config) (*Router, error) { config: config, } - router.debugHandler = debug_httpdriver.NewDebugHandler( + if err := router.initHandlers(config); err != nil { + return nil, err + } + + return router, nil +} + +func (router *Router) initHandlers(config Config) error { + var err error + + router.debugHandler, err = debug_httpdriver.NewDebugHandler( config.NamespaceDecoder, config.DebugConnector, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("debug handler: %w", err) + } - router.featureHandler = productcatalog_httpdriver.NewFeatureHandler( + router.featureHandler, err = productcatalog_httpdriver.NewFeatureHandler( config.FeatureConnector, config.NamespaceDecoder, config.MeterManageService, config.LLMCostService, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("feature handler: %w", err) + } - router.entitlementHandler = entitlementdriver.NewEntitlementHandler( + router.entitlementHandler, err = entitlementdriver.NewEntitlementHandler( config.EntitlementConnector, config.Customer, config.SubjectService, config.NamespaceDecoder, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("entitlement handler: %w", err) + } // V2 entitlement handler for customer-scoped operations - router.entitlementV2Handler = entitlementdriverv2.NewEntitlementHandler( + router.entitlementV2Handler, err = entitlementdriverv2.NewEntitlementHandler( config.EntitlementConnector, config.EntitlementBalanceConnector, config.Customer, config.NamespaceDecoder, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("entitlement v2 handler: %w", err) + } - router.meterHandler = meterhttphandler.New( + router.meterHandler, err = meterhttphandler.New( config.NamespaceDecoder, config.Customer, config.MeterManageService, @@ -343,20 +397,29 @@ func NewRouter(config Config) (*Router, error) { config.SubjectService, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("meter handler: %w", err) + } - router.ingestHandler = ingesthttpdriver.New( + router.ingestHandler, err = ingesthttpdriver.New( config.NamespaceDecoder, config.IngestService, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("ingest handler: %w", err) + } - router.meterEventHandler = metereventhttphandler.New( + router.meterEventHandler, err = metereventhttphandler.New( config.NamespaceDecoder, config.MeterEventService, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("meter event handler: %w", err) + } - router.meteredEntitlementHandler = entitlementdriver.NewMeteredEntitlementHandler( + router.meteredEntitlementHandler, err = entitlementdriver.NewMeteredEntitlementHandler( config.EntitlementConnector, config.EntitlementBalanceConnector, config.Customer, @@ -364,43 +427,58 @@ func NewRouter(config Config) (*Router, error) { config.NamespaceDecoder, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("metered entitlement handler: %w", err) + } - router.creditHandler = creditdriver.NewGrantHandler( + router.creditHandler, err = creditdriver.NewGrantHandler( config.NamespaceDecoder, config.GrantConnector, config.GrantRepo, config.Customer, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("grant handler: %w", err) + } - router.notificationHandler = notificationhttpdriver.New( + router.notificationHandler, err = notificationhttpdriver.New( config.NamespaceDecoder, config.Notification, config.Billing, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("notification handler: %w", err) + } router.infoHandler = infohttpdriver.New( httptransport.WithErrorHandler(config.ErrorHandler), ) - router.progressHandler = progresshttpdriver.New( + router.progressHandler, err = progresshttpdriver.New( config.NamespaceDecoder, config.ProgressManager, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("progress handler: %w", err) + } // Customer - router.customerHandler = customerhttpdriver.New( + router.customerHandler, err = customerhttpdriver.New( config.NamespaceDecoder, config.Customer, config.SubscriptionService, config.EntitlementConnector, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("customer handler: %w", err) + } // App - router.appHandler = apphttpdriver.New( + router.appHandler, err = apphttpdriver.New( config.Logger, config.NamespaceDecoder, config.App, @@ -409,78 +487,88 @@ func NewRouter(config Config) (*Router, error) { config.Customer, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("app handler: %w", err) + } // App Stripe - router.appStripeHandler = appstripehttpdriver.New( + router.appStripeHandler, err = appstripehttpdriver.New( config.NamespaceDecoder, config.AppStripe, config.Billing, config.Customer, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("app stripe handler: %w", err) + } // App Custom Invoicing - router.appCustomInvoicingHandler = appcustominvoicinghttpdriver.New( + router.appCustomInvoicingHandler, err = appcustominvoicinghttpdriver.New( config.AppCustomInvoicing, config.NamespaceDecoder, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("app custom invoicing handler: %w", err) + } // Billing - router.billingHandler = billinghttpdriver.New( + router.billingHandler, err = billinghttpdriver.New( config.Logger, config.NamespaceDecoder, config.BillingFeatureSwitches, config.Billing, config.App, - config.AppStripe, config.ChargeService, config.Credits, config.FeatureGate, httptransport.WithErrorHandler(config.ErrorHandler), ) - - // Product Catalog - if config.Plan == nil { - return nil, errors.New("plan service is required") + if err != nil { + return fmt.Errorf("billing handler: %w", err) } - router.planHandler = planhttpdriver.New( + // Product Catalog + router.planHandler, err = planhttpdriver.New( config.NamespaceDecoder, config.Plan, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("plan handler: %w", err) + } - router.addonHandler = addonhttpdriver.New( + router.addonHandler, err = addonhttpdriver.New( config.NamespaceDecoder, config.Addon, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("add-on handler: %w", err) + } - router.planAddonHandler = planaddonhttpdriver.New( + router.planAddonHandler, err = planaddonhttpdriver.New( config.NamespaceDecoder, config.PlanAddon, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("plan add-on handler: %w", err) + } - router.subjectHandler = subjecthttphandler.New( + router.subjectHandler, err = subjecthttphandler.New( config.NamespaceDecoder, config.Logger, config.SubjectService, config.EntitlementConnector, httptransport.WithErrorHandler(config.ErrorHandler), ) - - if config.SubscriptionService == nil || config.SubscriptionWorkflowService == nil || config.PlanSubscriptionService == nil { - return nil, errors.New("subscription services are required") + if err != nil { + return fmt.Errorf("subject handler: %w", err) } - if config.Logger == nil { - return nil, errors.New("logger is required") - } - - // Subscription - router.subscriptionHandler = subscriptionhttpdriver.NewHandler( + subscriptionHandler, err := subscriptionhttpdriver.NewHandler( subscriptionhttpdriver.HandlerConfig{ SubscriptionWorkflowService: config.SubscriptionWorkflowService, SubscriptionService: config.SubscriptionService, @@ -492,8 +580,13 @@ func NewRouter(config Config) (*Router, error) { }, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("subscription handler: %w", err) + } + + router.subscriptionHandler = subscriptionHandler - router.subscriptionAddonHandler = subscriptionaddonhttpdriver.NewHandler( + subscriptionAddonHandler, err := subscriptionaddonhttpdriver.NewHandler( subscriptionaddonhttpdriver.HandlerConfig{ SubscriptionAddonService: config.SubscriptionAddonService, SubscriptionWorkflowService: config.SubscriptionWorkflowService, @@ -504,14 +597,22 @@ func NewRouter(config Config) (*Router, error) { }, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("subscription add-on handler: %w", err) + } + + router.subscriptionAddonHandler = subscriptionAddonHandler // Portal - router.portalHandler = portalhttphandler.New( + router.portalHandler, err = portalhttphandler.New( config.NamespaceDecoder, config.Portal, config.MeterManageService, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("portal handler: %w", err) + } // Currencies resolveNamespace := func(ctx context.Context) (string, error) { @@ -523,11 +624,14 @@ func NewRouter(config Config) (*Router, error) { return ns, nil } - router.currencyHandler = currencyhandler.New( + router.currencyHandler, err = currencyhandler.New( resolveNamespace, config.CurrencyService, httptransport.WithErrorHandler(config.ErrorHandler), ) + if err != nil { + return fmt.Errorf("currency handler: %w", err) + } - return router, nil + return nil } diff --git a/openmeter/subject/httphandler/handler.go b/openmeter/subject/httphandler/handler.go index 26c2e0f6ea..5767abe77d 100644 --- a/openmeter/subject/httphandler/handler.go +++ b/openmeter/subject/httphandler/handler.go @@ -3,6 +3,7 @@ package httpdriver import ( "context" "errors" + "fmt" "log/slog" "net/http" @@ -49,12 +50,29 @@ func New( subjectService subject.Service, entitlementConnector entitlement.Service, options ...httptransport.HandlerOption, -) Handler { +) (Handler, error) { + var errs []error + if namespaceDecoder == nil { + errs = append(errs, errors.New("namespace decoder is required")) + } + if logger == nil { + errs = append(errs, errors.New("logger is required")) + } + if subjectService == nil { + errs = append(errs, errors.New("subject service is required")) + } + if entitlementConnector == nil { + errs = append(errs, errors.New("entitlement connector is required")) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid subject handler config: %w", err) + } + return &handler{ namespaceDecoder: namespaceDecoder, options: options, logger: logger, subjectService: subjectService, entitlementConnector: entitlementConnector, - } + }, nil } diff --git a/openmeter/subscription/addon/http/handler.go b/openmeter/subscription/addon/http/handler.go index dbb932bdea..7eef8d3ae2 100644 --- a/openmeter/subscription/addon/http/handler.go +++ b/openmeter/subscription/addon/http/handler.go @@ -3,8 +3,10 @@ package httpdriver import ( "context" "errors" + "fmt" "log/slog" "net/http" + "reflect" "github.com/openmeterio/openmeter/openmeter/namespace/namespacedriver" "github.com/openmeterio/openmeter/openmeter/productcatalog/addon" @@ -31,11 +33,45 @@ type HandlerConfig struct { Logger *slog.Logger } -func NewHandler(config HandlerConfig, options ...httptransport.HandlerOption) Handler { +func (c HandlerConfig) Validate() error { + var errs []error + + if isNil(c.SubscriptionAddonService) { + errs = append(errs, errors.New("subscription add-on service is required")) + } + + if isNil(c.SubscriptionWorkflowService) { + errs = append(errs, errors.New("subscription workflow service is required")) + } + + if isNil(c.SubscriptionService) { + errs = append(errs, errors.New("subscription service is required")) + } + + if isNil(c.AddonService) { + errs = append(errs, errors.New("add-on service is required")) + } + + if isNil(c.NamespaceDecoder) { + errs = append(errs, errors.New("namespace decoder is required")) + } + + if isNil(c.Logger) { + errs = append(errs, errors.New("logger is required")) + } + + return errors.Join(errs...) +} + +func NewHandler(config HandlerConfig, options ...httptransport.HandlerOption) (Handler, error) { + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("invalid subscription add-on handler config: %w", err) + } + return &handler{ HandlerConfig: config, Options: options, - } + }, nil } type handler struct { @@ -51,3 +87,17 @@ func (h *handler) resolveNamespace(ctx context.Context) (string, error) { return ns, nil } + +func isNil(value any) bool { + if value == nil { + return true + } + + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return v.IsNil() + default: + return false + } +} diff --git a/openmeter/subscription/addon/http/handler_test.go b/openmeter/subscription/addon/http/handler_test.go new file mode 100644 index 0000000000..ebe1fe384a --- /dev/null +++ b/openmeter/subscription/addon/http/handler_test.go @@ -0,0 +1,49 @@ +package httpdriver + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHandlerConfigValidate(t *testing.T) { + err := HandlerConfig{}.Validate() + require.Error(t, err) + + for _, want := range []string{ + "subscription add-on service is required", + "subscription workflow service is required", + "subscription service is required", + "add-on service is required", + "namespace decoder is required", + "logger is required", + } { + require.ErrorContains(t, err, want) + } +} + +func TestNewHandlerInvalidConfig(t *testing.T) { + handler, err := NewHandler(HandlerConfig{}) + require.Error(t, err) + require.Nil(t, handler) + require.ErrorContains(t, err, "invalid subscription add-on handler config") + require.ErrorContains(t, err, "add-on service is required") +} + +func TestHandlerConfigValidateTypedNilDependency(t *testing.T) { + var namespaceDecoder *nilNamespaceDecoder + + err := HandlerConfig{ + NamespaceDecoder: namespaceDecoder, + }.Validate() + + require.Error(t, err) + require.ErrorContains(t, err, "namespace decoder is required") +} + +type nilNamespaceDecoder struct{} + +func (*nilNamespaceDecoder) GetNamespace(context.Context) (string, bool) { + return "", false +}