diff --git a/openmeter/app/httpdriver/marketplace.go b/openmeter/app/httpdriver/marketplace.go index ff2f145900..75ae045334 100644 --- a/openmeter/app/httpdriver/marketplace.go +++ b/openmeter/app/httpdriver/marketplace.go @@ -249,8 +249,15 @@ func (h *handler) createBillingProfile(ctx context.Context, installedApp app.App case app.AppTypeStripe: return h.makeStripeDefaultBillingApp(ctx, installedApp) case app.AppTypeSandbox: - // TODO: Implement sandbox billing profile creation - return nil, nil + namespace := installedApp.GetID().Namespace + if err := h.billingService.ProvisionDefaultBillingProfile(ctx, namespace); err != nil { + return nil, fmt.Errorf("provision default billing profile: %w", err) + } + return []api.AppCapabilityType{ + api.AppCapabilityType(app.CapabilityTypeCalculateTax), + api.AppCapabilityType(app.CapabilityTypeInvoiceCustomers), + api.AppCapabilityType(app.CapabilityTypeCollectPayments), + }, nil case app.AppTypeCustomInvoicing: // TODO: Implement custom invoicing billing profile creation return nil, nil diff --git a/openmeter/app/sandbox/app.go b/openmeter/app/sandbox/app.go index f7e52206b4..2f019252d8 100644 --- a/openmeter/app/sandbox/app.go +++ b/openmeter/app/sandbox/app.go @@ -4,11 +4,14 @@ import ( "context" "fmt" + "github.com/samber/lo" + "github.com/openmeterio/openmeter/openmeter/app" "github.com/openmeterio/openmeter/openmeter/billing" "github.com/openmeterio/openmeter/openmeter/customer" customerapp "github.com/openmeterio/openmeter/openmeter/customer/app" "github.com/openmeterio/openmeter/pkg/clock" + "github.com/openmeterio/openmeter/pkg/models" ) const ( @@ -214,12 +217,25 @@ func (a *Factory) NewApp(_ context.Context, appBase app.AppBase) (app.App, error }, nil } -func (a *Factory) InstallAppWithAPIKey(ctx context.Context, input app.AppFactoryInstallAppWithAPIKeyInput) (app.App, error) { - // Validate input +func (a *Factory) InstallApp(ctx context.Context, input app.AppFactoryInstallAppInput) (app.App, error) { if err := input.Validate(); err != nil { return nil, fmt.Errorf("invalid input: %w", err) } + // Sandbox is a singleton per namespace — only one instance makes sense since all + // instances are functionally identical (no credentials, no external state). + existing, err := a.appService.ListApps(ctx, app.ListAppInput{ + Namespace: input.Namespace, + Type: lo.ToPtr(app.AppTypeSandbox), + }) + if err != nil { + return nil, fmt.Errorf("failed to list sandbox apps: %w", err) + } + + if existing.TotalCount > 0 { + return nil, models.NewGenericConflictError(fmt.Errorf("sandbox app: %s already exists", existing.Items[0].GetName())) + } + appBase, err := a.appService.CreateApp(ctx, app.CreateAppInput{ Namespace: input.Namespace, Name: input.Name,