diff --git a/app/config/config.go b/app/config/config.go index f84e1544f..1861b10c3 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -20,22 +20,23 @@ type Configuration struct { Telemetry TelemetryConfig - Aggregation AggregationConfiguration - Entitlements EntitlementsConfiguration - Dedupe DedupeConfiguration - Events EventsConfiguration - Ingest IngestConfiguration - Meters []*models.Meter - Namespace NamespaceConfiguration - Portal PortalConfiguration - Postgres PostgresConfig - Sink SinkConfiguration - BalanceWorker BalanceWorkerConfiguration - Notification NotificationConfiguration - Billing BillingConfiguration - Apps AppsConfiguration - StripeApp StripeAppConfig - Svix SvixConfig + Aggregation AggregationConfiguration + Entitlements EntitlementsConfiguration + Dedupe DedupeConfiguration + Events EventsConfiguration + Ingest IngestConfiguration + Meters []*models.Meter + Namespace NamespaceConfiguration + Portal PortalConfiguration + Postgres PostgresConfig + Sink SinkConfiguration + BalanceWorker BalanceWorkerConfiguration + Notification NotificationConfiguration + ProductCatalog ProductCatalogConfiguration + Billing BillingConfiguration + Apps AppsConfiguration + StripeApp StripeAppConfig + Svix SvixConfig } // Validate validates the configuration. @@ -114,6 +115,18 @@ func (c Configuration) Validate() error { errs = append(errs, errorsx.WithPrefix(err, "stripe app")) } + if err := c.ProductCatalog.Validate(); err != nil { + errs = append(errs, errorsx.WithPrefix(err, "product catalog")) + } + + if c.ProductCatalog.Enabled && !c.Entitlements.Enabled { + errs = append(errs, errors.New("entitlements must be enabled if product catalog is enabled")) + } + + if err := c.Billing.Validate(); err != nil { + errs = append(errs, errorsx.WithPrefix(err, "billing")) + } + if err := c.Apps.Validate(); err != nil { errs = append(errs, errorsx.WithPrefix(err, "apps")) } @@ -155,5 +168,6 @@ func SetViperDefaults(v *viper.Viper, flags *pflag.FlagSet) { ConfigureNotification(v) ConfigureStripe(v) ConfigureBilling(v) + ConfigureProductCatalog(v) ConfigureApps(v) } diff --git a/app/config/productcatalog.go b/app/config/productcatalog.go new file mode 100644 index 000000000..071a434eb --- /dev/null +++ b/app/config/productcatalog.go @@ -0,0 +1,15 @@ +package config + +import "github.com/spf13/viper" + +type ProductCatalogConfiguration struct { + Enabled bool +} + +func (c ProductCatalogConfiguration) Validate() error { + return nil +} + +func ConfigureProductCatalog(v *viper.Viper) { + v.SetDefault("productcatalog.enabled", false) +} diff --git a/cmd/server/main.go b/cmd/server/main.go index f4f8a3152..a4801f524 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -39,6 +39,9 @@ import ( notificationrepository "github.com/openmeterio/openmeter/openmeter/notification/repository" notificationservice "github.com/openmeterio/openmeter/openmeter/notification/service" notificationwebhook "github.com/openmeterio/openmeter/openmeter/notification/webhook" + plan "github.com/openmeterio/openmeter/openmeter/productcatalog/plan" + planadapter "github.com/openmeterio/openmeter/openmeter/productcatalog/plan/adapter" + planservice "github.com/openmeterio/openmeter/openmeter/productcatalog/plan/service" "github.com/openmeterio/openmeter/openmeter/registry" registrybuilder "github.com/openmeterio/openmeter/openmeter/registry/builder" secretadapter "github.com/openmeterio/openmeter/openmeter/secret/adapter" @@ -323,6 +326,29 @@ func main() { }() } + // Initialize plans + var planService plan.Service + if conf.ProductCatalog.Enabled { + adapter, err := planadapter.New(planadapter.Config{ + Client: app.EntClient, + Logger: logger.With("subsystem", "productcatalog.plan"), + }) + if err != nil { + logger.Error("failed to initialize plan adapter", "error", err) + os.Exit(1) + } + + planService, err = planservice.New(planservice.Config{ + Feature: entitlementConnRegistry.Feature, + Adapter: adapter, + Logger: logger.With("subsystem", "productcatalog.plan"), + }) + if err != nil { + logger.Error("failed to initialize plan service", "error", err) + os.Exit(1) + } + } + // Initialize billing var billingService billing.Service if conf.Billing.Enabled { @@ -365,12 +391,13 @@ func main() { Billing: billingService, Customer: customerService, DebugConnector: debugConnector, - FeatureConnector: entitlementConnRegistry.Feature, - EntitlementConnector: entitlementConnRegistry.Entitlement, EntitlementBalanceConnector: entitlementConnRegistry.MeteredEntitlement, + EntitlementConnector: entitlementConnRegistry.Entitlement, + FeatureConnector: entitlementConnRegistry.Feature, GrantConnector: entitlementConnRegistry.Grant, GrantRepo: entitlementConnRegistry.GrantRepo, Notification: notificationService, + Plan: planService, // modules EntitlementsEnabled: conf.Entitlements.Enabled, NotificationEnabled: conf.Notification.Enabled, diff --git a/config.example.yaml b/config.example.yaml index fa10d41ea..1512324da 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -40,6 +40,9 @@ telemetry: entitlements: enabled: true +productcatalog: + enabled: true + billing: enabled: true diff --git a/openmeter/productcatalog/plan/httpdriver/phase.go b/openmeter/productcatalog/plan/httpdriver/phase.go index 7f6677769..293ae720d 100644 --- a/openmeter/productcatalog/plan/httpdriver/phase.go +++ b/openmeter/productcatalog/plan/httpdriver/phase.go @@ -29,8 +29,11 @@ type PhaseKeyPlanParams struct { type ( ListPhasesRequest = plan.ListPhasesInput ListPhasesResponse = api.PlanPhasePaginatedResponse - ListPhasesParams = api.ListPlanPhasesParams - ListPhasesHandler httptransport.HandlerWithArgs[ListPhasesRequest, ListPhasesResponse, ListPhasesParams] + ListPhasesParams struct { + PlanID string + api.ListPlanPhasesParams + } + ListPhasesHandler httptransport.HandlerWithArgs[ListPhasesRequest, ListPhasesResponse, ListPhasesParams] ) func (h *handler) ListPhases() ListPhasesHandler { @@ -43,6 +46,7 @@ func (h *handler) ListPhases() ListPhasesHandler { req := ListPhasesRequest{ Namespaces: []string{ns}, + PlanIDs: []string{params.PlanID}, OrderBy: plan.OrderBy(lo.FromPtrOr(params.OrderBy, api.PhasesOrderByKey)), Order: sortx.Order(defaultx.WithDefault(params.Order, api.SortOrderDESC)), Page: pagination.Page{ diff --git a/openmeter/productcatalog/plan/httpdriver/plan.go b/openmeter/productcatalog/plan/httpdriver/plan.go index bf2b1cc5f..c52aa895c 100644 --- a/openmeter/productcatalog/plan/httpdriver/plan.go +++ b/openmeter/productcatalog/plan/httpdriver/plan.go @@ -87,12 +87,12 @@ func (h *handler) ListPlans() ListPlansHandler { type ( CreatePlanRequest = plan.CreatePlanInput CreatePlanResponse = api.Plan - CreatePlanHandler httptransport.HandlerWithArgs[CreatePlanRequest, CreatePlanResponse, string] + CreatePlanHandler httptransport.Handler[CreatePlanRequest, CreatePlanResponse] ) func (h *handler) CreatePlan() CreatePlanHandler { - return httptransport.NewHandlerWithArgs( - func(ctx context.Context, r *http.Request, planID string) (CreatePlanRequest, error) { + return httptransport.NewHandler( + func(ctx context.Context, r *http.Request) (CreatePlanRequest, error) { body := api.PlanCreate{} if err := commonhttp.JSONRequestBodyDecoder(r, &body); err != nil { return CreatePlanRequest{}, fmt.Errorf("failed to decode create plan request: %w", err) diff --git a/openmeter/server/router/plan.go b/openmeter/server/router/plan.go index 1322b97a7..42f5e632b 100644 --- a/openmeter/server/router/plan.go +++ b/openmeter/server/router/plan.go @@ -4,82 +4,165 @@ import ( "net/http" "github.com/openmeterio/openmeter/api" + planhttpdriver "github.com/openmeterio/openmeter/openmeter/productcatalog/plan/httpdriver" + "github.com/samber/lo" ) // List plans // (GET /api/v1/plans) func (a *Router) ListPlans(w http.ResponseWriter, r *http.Request, params api.ListPlansParams) { - w.WriteHeader(http.StatusNotImplemented) + if a.config.Plan == nil { + unimplemented.ListPlans(w, r, params) + return + } + + a.planHandler.ListPlans().With(params).ServeHTTP(w, r) } // Create a plan // (POST /api/v1/plans) func (a *Router) CreatePlan(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotImplemented) + if a.config.Plan == nil { + unimplemented.CreatePlan(w, r) + return + } + + a.planHandler.CreatePlan().ServeHTTP(w, r) } // Delete plan // (DELETE /api/v1/plans/{planId}) func (a *Router) DeletePlan(w http.ResponseWriter, r *http.Request, planId string) { - w.WriteHeader(http.StatusNotImplemented) + if a.config.Plan == nil { + unimplemented.DeletePlan(w, r, planId) + return + } + + a.planHandler.DeletePlan().With(planId).ServeHTTP(w, r) } // Get plan // (GET /api/v1/plans/{planId}) func (a *Router) GetPlan(w http.ResponseWriter, r *http.Request, planId string, params api.GetPlanParams) { - w.WriteHeader(http.StatusNotImplemented) + if a.config.Plan == nil { + unimplemented.GetPlan(w, r, planId, params) + return + } + + a.planHandler.GetPlan().With(planhttpdriver.GetPlanRequestParams{ + ID: planId, + IncludeLatest: lo.FromPtrOr(params.IncludeLatest, false), + }).ServeHTTP(w, r) } // Update a plan // (PUT /api/v1/plans/{planId}) func (a *Router) UpdatePlan(w http.ResponseWriter, r *http.Request, planId string) { - w.WriteHeader(http.StatusNotImplemented) + if a.config.Plan == nil { + unimplemented.UpdatePlan(w, r, planId) + return + } + + a.planHandler.UpdatePlan().With(planId).ServeHTTP(w, r) } // New draft plan // (POST /api/v1/plans/{planIdOrKey}/next) func (a *Router) NextPlan(w http.ResponseWriter, r *http.Request, planIdOrKey string) { - w.WriteHeader(http.StatusNotImplemented) + if a.config.Plan == nil { + unimplemented.NextPlan(w, r, planIdOrKey) + return + } + + // TODO: allow key as well + a.planHandler.NextPlan().With(planIdOrKey).ServeHTTP(w, r) } // List phases in plan // (GET /api/v1/plans/{planId}/phases) func (a *Router) ListPlanPhases(w http.ResponseWriter, r *http.Request, planId string, params api.ListPlanPhasesParams) { - w.WriteHeader(http.StatusNotImplemented) + if a.config.Plan == nil { + unimplemented.ListPlanPhases(w, r, planId, params) + return + } + + a.planHandler.ListPhases().With(planhttpdriver.ListPhasesParams{ + PlanID: planId, + ListPlanPhasesParams: params, + }).ServeHTTP(w, r) } // Create new phase in plan // (POST /api/v1/plans/{planId}/phases) func (a *Router) CreatePlanPhase(w http.ResponseWriter, r *http.Request, planId string) { - w.WriteHeader(http.StatusNotImplemented) + if a.config.Plan == nil { + unimplemented.CreatePlanPhase(w, r, planId) + return + } + + a.planHandler.CreatePhase().With(planId).ServeHTTP(w, r) } // Delete phase for plan // (DELETE /api/v1/plans/{planId}/phases/{planPhaseKey}) func (a *Router) DeletePlanPhase(w http.ResponseWriter, r *http.Request, planId string, planPhaseKey string) { - w.WriteHeader(http.StatusNotImplemented) + if a.config.Plan == nil { + unimplemented.DeletePlanPhase(w, r, planId, planPhaseKey) + return + } + + a.planHandler.DeletePhase().With(planhttpdriver.DeletePhaseRequestParams{ + PlanID: planId, + Key: planPhaseKey, + }).ServeHTTP(w, r) } // Get phase for plan // (GET /api/v1/plans/{planId}/phases/{planPhaseKey}) func (a *Router) GetPlanPhase(w http.ResponseWriter, r *http.Request, planId string, planPhaseKey string) { - w.WriteHeader(http.StatusNotImplemented) + if a.config.Plan == nil { + unimplemented.GetPlanPhase(w, r, planId, planPhaseKey) + return + } + + a.planHandler.GetPhase().With(planhttpdriver.PhaseKeyPlanParams{ + PlanID: planId, + Key: planPhaseKey, + }).ServeHTTP(w, r) } // Update phase in plan // (PUT /api/v1/plans/{planId}/phases/{planPhaseKey}) func (a *Router) UpdatePlanPhase(w http.ResponseWriter, r *http.Request, planId string, planPhaseKey string) { - w.WriteHeader(http.StatusNotImplemented) + if a.config.Plan == nil { + unimplemented.UpdatePlanPhase(w, r, planId, planPhaseKey) + return + } + + a.planHandler.UpdatePhase().With(planhttpdriver.PhaseKeyPlanParams{ + PlanID: planId, + Key: planPhaseKey, + }).ServeHTTP(w, r) } // Publish plan // (POST /api/v1/plans/{planId}/publish) func (a *Router) PublishPlan(w http.ResponseWriter, r *http.Request, planId string) { - w.WriteHeader(http.StatusNotImplemented) + if a.config.Plan == nil { + unimplemented.PublishPlan(w, r, planId) + return + } + + a.planHandler.PublishPlan().With(planId).ServeHTTP(w, r) } // Archive plan version // (POST /api/v1/plans/{planId}/archive) func (a *Router) ArchivePlan(w http.ResponseWriter, r *http.Request, planId string) { - w.WriteHeader(http.StatusNotImplemented) + if a.config.Plan == nil { + unimplemented.ArchivePlan(w, r, planId) + return + } + + a.planHandler.ArchivePlan().With(planId).ServeHTTP(w, r) } diff --git a/openmeter/server/router/router.go b/openmeter/server/router/router.go index 2ed87db31..db361f6c3 100644 --- a/openmeter/server/router/router.go +++ b/openmeter/server/router/router.go @@ -34,6 +34,8 @@ import ( notificationhttpdriver "github.com/openmeterio/openmeter/openmeter/notification/httpdriver" productcatalog_httpdriver "github.com/openmeterio/openmeter/openmeter/productcatalog/driver" "github.com/openmeterio/openmeter/openmeter/productcatalog/feature" + plan "github.com/openmeterio/openmeter/openmeter/productcatalog/plan" + planhttpdriver "github.com/openmeterio/openmeter/openmeter/productcatalog/plan/httpdriver" "github.com/openmeterio/openmeter/openmeter/server/authenticator" "github.com/openmeterio/openmeter/openmeter/streaming" "github.com/openmeterio/openmeter/pkg/errorsx" @@ -72,6 +74,7 @@ type Config struct { AppStripe appstripe.Service Customer customer.Service Billing billing.Service + Plan plan.Service DebugConnector debug.DebugConnector FeatureConnector feature.FeatureConnector EntitlementConnector entitlement.Connector @@ -168,6 +171,7 @@ type Router struct { appStripeHandler appstripehttpdriver.AppStripeHandler billingHandler billinghttpdriver.Handler featureHandler productcatalog_httpdriver.FeatureHandler + planHandler planhttpdriver.Handler creditHandler creditdriver.GrantHandler debugHandler debug_httpdriver.DebugHandler customerHandler customerhttpdriver.CustomerHandler @@ -261,5 +265,13 @@ func NewRouter(config Config) (*Router, error) { ) } + if config.Plan != nil { + router.planHandler = planhttpdriver.New( + staticNamespaceDecoder, + config.Plan, + httptransport.WithErrorHandler(config.ErrorHandler), + ) + } + return router, nil }