Skip to content
Open
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
89 changes: 5 additions & 84 deletions api/v3/handlers/apps/install_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
98 changes: 98 additions & 0 deletions openmeter/app/billingprofile/provision.go
Original file line number Diff line number Diff line change
@@ -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:
Comment on lines +36 to +38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Custom invoicing skips provisioning

When a Custom Invoicing app is installed with createBillingProfile enabled or omitted, this branch returns success without creating a billing profile, causing the installation to complete while the documented flag remains ineffective and the response reports no default capabilities.

Knowledge Base Used: App Framework (Marketplace Integrations)

Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/app/billingprofile/provision.go
Line: 36-38

Comment:
**Custom invoicing skips provisioning**

When a Custom Invoicing app is installed with `createBillingProfile` enabled or omitted, this branch returns success without creating a billing profile, causing the installation to complete while the documented flag remains ineffective and the response reports no default capabilities.

**Knowledge Base Used:** [App Framework (Marketplace Integrations)](https://app.greptile.com/openmeter/-/custom-context/knowledge-base/openmeterio/openmeter/-/docs/app.md)

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

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
}
31 changes: 20 additions & 11 deletions openmeter/app/httpdriver/marketplace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -163,7 +168,6 @@ type (

type MarketplaceAppInstallRequest struct {
app.InstallAppV3Input
CreateBillingProfile bool
}

// MarketplaceAppInstall returns a handler for installing an app type
Expand All @@ -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
Expand All @@ -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 {
Expand Down