diff --git a/api/v3/handlers/apps/install_app.go b/api/v3/handlers/apps/install_app.go index ae71d2f082..667bd63cb1 100644 --- a/api/v3/handlers/apps/install_app.go +++ b/api/v3/handlers/apps/install_app.go @@ -11,8 +11,7 @@ import ( "github.com/openmeterio/openmeter/api/v3/apierrors" "github.com/openmeterio/openmeter/api/v3/request" "github.com/openmeterio/openmeter/openmeter/app" - appstripe "github.com/openmeterio/openmeter/openmeter/app/stripe" - "github.com/openmeterio/openmeter/openmeter/billing" + "github.com/openmeterio/openmeter/openmeter/app/billingprofile" "github.com/openmeterio/openmeter/pkg/framework/commonhttp" "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" ) @@ -112,8 +111,10 @@ func (h *handler) InstallApp() InstallAppHandler { } }, func(ctx context.Context, request InstallAppRequest) (InstallAppResponse, error) { - // make h.createBillingProfile transactional - request.CreateDefaultBillingProfileFn = h.createBillingProfile + // make the billing profile provisioning transactional + request.CreateDefaultBillingProfileFn = func(ctx context.Context, installedApp app.App) ([]app.CapabilityType, error) { + return billingprofile.CreateDefault(ctx, h.billingService, h.stripeAppService, installedApp) + } resp, err := h.appService.InstallApp(ctx, request) if err != nil { @@ -145,83 +146,3 @@ func (h *handler) InstallApp() InstallAppHandler { )..., ) } - -// createBillingProfile creates a default billing profile for the installed app based on its type -func (h *handler) createBillingProfile(ctx context.Context, installedApp app.App) ([]app.CapabilityType, error) { - switch installedApp.GetType() { - case app.AppTypeStripe: - return h.makeStripeDefaultBillingApp(ctx, installedApp) - case app.AppTypeSandbox: - namespace := installedApp.GetID().Namespace - if err := h.billingService.ProvisionDefaultBillingProfile(ctx, namespace); err != nil { - return nil, fmt.Errorf("provision default billing profile: %w", err) - } - return []app.CapabilityType{ - app.CapabilityTypeCalculateTax, - app.CapabilityTypeInvoiceCustomers, - app.CapabilityTypeCollectPayments, - }, nil - case app.AppTypeCustomInvoicing: - // TODO: Implement custom invoicing billing profile creation - return nil, nil - default: - return nil, fmt.Errorf("unknown app type: %s", installedApp.GetType()) - } -} - -// Make Stripe app the default billing app if current one is Sandbox app -func (h *handler) makeStripeDefaultBillingApp(ctx context.Context, stripeApp app.App) ([]app.CapabilityType, error) { - defaultForCapabilityTypes := []app.CapabilityType{} - - appID := stripeApp.GetID() - - // Check if it's a Stripe app - if stripeApp.GetType() != app.AppTypeStripe { - return defaultForCapabilityTypes, fmt.Errorf("app is not a stripe app: %s", appID.ID) - } - - // Check if the default billing profile is a sandbox app type - defaultBillingProfile, err := h.billingService.GetDefaultProfile(ctx, billing.GetDefaultProfileInput{ - Namespace: appID.Namespace, - }) - if err != nil { - return defaultForCapabilityTypes, fmt.Errorf("failed to get default billing profile: %w", err) - } - - // Set default billing profile if the current default is the sandbox - setDefaultBillingProfile := defaultBillingProfile != nil && defaultBillingProfile.Apps != nil && defaultBillingProfile.Apps.Invoicing.GetType() == app.AppTypeSandbox - - // Get supplier contract from stripe app - supplierContract, err := h.stripeAppService.GetSupplierContact(ctx, appstripe.GetSupplierContactInput{ - AppID: appID, - }) - if err != nil { - return defaultForCapabilityTypes, fmt.Errorf("failed to get supplier contract for stripe app %s: %w", appID.ID, err) - } - - // Create new default billing profile - _, err = h.billingService.CreateProfile(ctx, billing.CreateProfileInput{ - Namespace: appID.Namespace, - Name: "Stripe Billing Profile", - Description: lo.ToPtr("Stripe Billing Profile, created automatically"), - Default: setDefaultBillingProfile, - Supplier: supplierContract, - WorkflowConfig: billing.DefaultWorkflowConfig, - Apps: billing.ProfileAppReferences{ - Tax: appID, - Invoicing: appID, - Payment: appID, - }, - }) - if err != nil { - return defaultForCapabilityTypes, fmt.Errorf("failed to create billing profile for stripe app %s: %w", appID.ID, err) - } - - defaultForCapabilityTypes = []app.CapabilityType{ - app.CapabilityTypeCalculateTax, - app.CapabilityTypeInvoiceCustomers, - app.CapabilityTypeCollectPayments, - } - - return defaultForCapabilityTypes, nil -} diff --git a/openmeter/app/billingprofile/provision.go b/openmeter/app/billingprofile/provision.go new file mode 100644 index 0000000000..f15d6dbd4e --- /dev/null +++ b/openmeter/app/billingprofile/provision.go @@ -0,0 +1,98 @@ +// Package billingprofile provisions the default billing profile created when an app +// is installed with CreateDefaultBillingProfile set. It is shared by every HTTP driver +// that installs apps (currently the v1 marketplace endpoints and the v3 apps endpoint) +// so the provisioning rules stay identical across API versions. +package billingprofile + +import ( + "context" + "fmt" + + "github.com/samber/lo" + + "github.com/openmeterio/openmeter/openmeter/app" + appstripe "github.com/openmeterio/openmeter/openmeter/app/stripe" + "github.com/openmeterio/openmeter/openmeter/billing" +) + +// CreateDefault creates a default billing profile for the installed app based on its type. +// Assign it to app.InstallAppV3Input.CreateDefaultBillingProfileFn (bound to concrete +// billingService/stripeAppService instances) to enable CreateDefaultBillingProfile. +func CreateDefault(ctx context.Context, billingService billing.Service, stripeAppService appstripe.Service, installedApp app.App) ([]app.CapabilityType, error) { + switch installedApp.GetType() { + case app.AppTypeStripe: + return makeStripeDefaultBillingApp(ctx, billingService, stripeAppService, installedApp) + case app.AppTypeSandbox: + namespace := installedApp.GetID().Namespace + if err := billingService.ProvisionDefaultBillingProfile(ctx, namespace); err != nil { + return nil, fmt.Errorf("provision default billing profile: %w", err) + } + return []app.CapabilityType{ + app.CapabilityTypeCalculateTax, + app.CapabilityTypeInvoiceCustomers, + app.CapabilityTypeCollectPayments, + }, nil + case app.AppTypeCustomInvoicing: + // TODO: Implement custom invoicing billing profile creation + return nil, nil + default: + return nil, fmt.Errorf("unknown app type: %s", installedApp.GetType()) + } +} + +// Make Stripe app the default billing app if current one is Sandbox app +func makeStripeDefaultBillingApp(ctx context.Context, billingService billing.Service, stripeAppService appstripe.Service, stripeApp app.App) ([]app.CapabilityType, error) { + defaultForCapabilityTypes := []app.CapabilityType{} + + appID := stripeApp.GetID() + + // Check if it's a Stripe app + if stripeApp.GetType() != app.AppTypeStripe { + return defaultForCapabilityTypes, fmt.Errorf("app is not a stripe app: %s", appID.ID) + } + + // Check if the default billing profile is a sandbox app type + defaultBillingProfile, err := billingService.GetDefaultProfile(ctx, billing.GetDefaultProfileInput{ + Namespace: appID.Namespace, + }) + if err != nil { + return defaultForCapabilityTypes, fmt.Errorf("failed to get default billing profile: %w", err) + } + + // Set default billing profile if the current default is the sandbox + setDefaultBillingProfile := defaultBillingProfile != nil && defaultBillingProfile.Apps != nil && defaultBillingProfile.Apps.Invoicing.GetType() == app.AppTypeSandbox + + // Get supplier contract from stripe app + supplierContract, err := stripeAppService.GetSupplierContact(ctx, appstripe.GetSupplierContactInput{ + AppID: appID, + }) + if err != nil { + return defaultForCapabilityTypes, fmt.Errorf("failed to get supplier contract for stripe app %s: %w", appID.ID, err) + } + + // Create new default billing profile + _, err = billingService.CreateProfile(ctx, billing.CreateProfileInput{ + Namespace: appID.Namespace, + Name: "Stripe Billing Profile", + Description: lo.ToPtr("Stripe Billing Profile, created automatically"), + Default: setDefaultBillingProfile, + Supplier: supplierContract, + WorkflowConfig: billing.DefaultWorkflowConfig, + Apps: billing.ProfileAppReferences{ + Tax: appID, + Invoicing: appID, + Payment: appID, + }, + }) + if err != nil { + return defaultForCapabilityTypes, fmt.Errorf("failed to create billing profile for stripe app %s: %w", appID.ID, err) + } + + defaultForCapabilityTypes = []app.CapabilityType{ + app.CapabilityTypeCalculateTax, + app.CapabilityTypeInvoiceCustomers, + app.CapabilityTypeCollectPayments, + } + + return defaultForCapabilityTypes, nil +} diff --git a/openmeter/app/httpdriver/marketplace.go b/openmeter/app/httpdriver/marketplace.go index f80921655b..85ea4f8998 100644 --- a/openmeter/app/httpdriver/marketplace.go +++ b/openmeter/app/httpdriver/marketplace.go @@ -9,6 +9,7 @@ import ( "github.com/openmeterio/openmeter/api" "github.com/openmeterio/openmeter/openmeter/app" + "github.com/openmeterio/openmeter/openmeter/app/billingprofile" "github.com/openmeterio/openmeter/pkg/framework/commonhttp" "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" "github.com/openmeterio/openmeter/pkg/pagination" @@ -94,7 +95,6 @@ type ( type MarketplaceAppAPIKeyInstallRequest struct { app.InstallAppV3Input - CreateBillingProfile bool } // MarketplaceAppAPIKeyInstall returns a handler for installing an app type with an API key @@ -114,12 +114,12 @@ func (h *handler) MarketplaceAppAPIKeyInstall() MarketplaceAppAPIKeyInstallHandl req := MarketplaceAppAPIKeyInstallRequest{ InstallAppV3Input: app.InstallAppV3Input{ - MarketplaceListingID: app.MarketplaceListingID{Type: app.AppType(appType)}, - Namespace: namespace, - Name: lo.FromPtr(body.Name), - APIKey: lo.ToPtr(body.ApiKey), + MarketplaceListingID: app.MarketplaceListingID{Type: app.AppType(appType)}, + Namespace: namespace, + Name: lo.FromPtr(body.Name), + APIKey: lo.ToPtr(body.ApiKey), + CreateDefaultBillingProfile: lo.FromPtrOr(body.CreateBillingProfile, true), }, - CreateBillingProfile: lo.FromPtrOr(body.CreateBillingProfile, true), } return req, nil @@ -129,6 +129,11 @@ func (h *handler) MarketplaceAppAPIKeyInstall() MarketplaceAppAPIKeyInstallHandl DefaultForCapabilityTypes: []api.AppCapabilityType{}, } + // make the billing profile provisioning transactional + request.CreateDefaultBillingProfileFn = func(ctx context.Context, installedApp app.App) ([]app.CapabilityType, error) { + return billingprofile.CreateDefault(ctx, h.billingService, h.stripeAppService, installedApp) + } + // Install app installedApp, err := h.service.InstallApp(ctx, request.InstallAppV3Input) if err != nil { @@ -163,7 +168,6 @@ type ( type MarketplaceAppInstallRequest struct { app.InstallAppV3Input - CreateBillingProfile bool } // MarketplaceAppInstall returns a handler for installing an app type @@ -183,11 +187,11 @@ func (h *handler) MarketplaceAppInstall() MarketplaceAppInstallHandler { req := MarketplaceAppInstallRequest{ InstallAppV3Input: app.InstallAppV3Input{ - MarketplaceListingID: app.MarketplaceListingID{Type: app.AppType(appType)}, - Namespace: namespace, - Name: lo.FromPtr(body.Name), + MarketplaceListingID: app.MarketplaceListingID{Type: app.AppType(appType)}, + Namespace: namespace, + Name: lo.FromPtr(body.Name), + CreateDefaultBillingProfile: lo.FromPtrOr(body.CreateBillingProfile, true), }, - CreateBillingProfile: lo.FromPtrOr(body.CreateBillingProfile, true), } return req, nil @@ -197,6 +201,11 @@ func (h *handler) MarketplaceAppInstall() MarketplaceAppInstallHandler { DefaultForCapabilityTypes: []api.AppCapabilityType{}, } + // make the billing profile provisioning transactional + request.CreateDefaultBillingProfileFn = func(ctx context.Context, installedApp app.App) ([]app.CapabilityType, error) { + return billingprofile.CreateDefault(ctx, h.billingService, h.stripeAppService, installedApp) + } + // Install app installedApp, err := h.service.InstallApp(ctx, request.InstallAppV3Input) if err != nil {