| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- package billing
- import (
- "context"
- "fmt"
- "net/http"
- "github.com/porter-dev/porter/api/server/handlers"
- "github.com/porter-dev/porter/api/server/shared"
- "github.com/porter-dev/porter/api/server/shared/apierrors"
- "github.com/porter-dev/porter/api/server/shared/config"
- "github.com/porter-dev/porter/api/types"
- "github.com/porter-dev/porter/internal/models"
- "github.com/porter-dev/porter/internal/telemetry"
- )
- // ListBillingHandler is a handler for listing payment methods
- type ListBillingHandler struct {
- handlers.PorterHandlerWriter
- }
- // CheckPaymentEnabledHandler is a handler for checking if payment is setup
- type CheckPaymentEnabledHandler struct {
- handlers.PorterHandlerWriter
- }
- // NewListBillingHandler will create a new ListBillingHandler
- func NewListBillingHandler(
- config *config.Config,
- writer shared.ResultWriter,
- ) *ListBillingHandler {
- return &ListBillingHandler{
- PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
- }
- }
- func (c *ListBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- ctx, span := telemetry.NewSpan(r.Context(), "serve-list-payment-methods")
- defer span.End()
- proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
- paymentMethods, err := c.Config().BillingManager.StripeClient.ListPaymentMethod(ctx, proj.BillingID)
- if err != nil {
- err := telemetry.Error(ctx, span, err, "error listing payment method")
- c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error listing payment method: %w", err)))
- return
- }
- c.WriteResult(w, r, paymentMethods)
- }
- // NewCheckPaymentEnabledHandler will create a new CheckPaymentEnabledHandler
- func NewCheckPaymentEnabledHandler(
- config *config.Config,
- writer shared.ResultWriter,
- ) *CheckPaymentEnabledHandler {
- return &CheckPaymentEnabledHandler{
- PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
- }
- }
- func (c *CheckPaymentEnabledHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- ctx, span := telemetry.NewSpan(r.Context(), "serve-check-payment-enabled")
- defer span.End()
- proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
- currentUser, _ := ctx.Value(types.UserScope).(*models.User)
- err := c.ensureBillingSetup(ctx, proj, currentUser)
- if err != nil {
- err := telemetry.Error(ctx, span, err, "error ensuring billing setup")
- c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
- return
- }
- telemetry.WithAttributes(span,
- telemetry.AttributeKV{Key: "project-id", Value: proj.ID},
- telemetry.AttributeKV{Key: "customer-id", Value: proj.BillingID},
- )
- paymentEnabled, err := c.Config().BillingManager.StripeClient.CheckPaymentEnabled(ctx, proj.BillingID)
- if err != nil {
- err := telemetry.Error(ctx, span, err, "error checking if payment enabled")
- c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
- return
- }
- telemetry.WithAttributes(span,
- telemetry.AttributeKV{Key: "payment-enabled", Value: paymentEnabled},
- )
- c.WriteResult(w, r, paymentEnabled)
- }
- func (c *CheckPaymentEnabledHandler) ensureBillingSetup(ctx context.Context, proj *models.Project, user *models.User) (err error) {
- ctx, span := telemetry.NewSpan(ctx, "ensure-billing-setup")
- defer span.End()
- telemetry.WithAttributes(span,
- telemetry.AttributeKV{Key: "billing-id", Value: proj.BillingID},
- )
- if proj.BillingID == "" {
- adminUser, err := c.getAdminUser(ctx, proj.ID)
- if err != nil {
- return telemetry.Error(ctx, span, err, "error getting admin user")
- }
- // If the admin user is not found, use the current user as last resort
- if adminUser == nil {
- adminUser = user
- }
- // Create billing customer for project and set the billing ID if it doesn't exist
- err = c.ensureStripeCustomerExists(ctx, adminUser.Email, proj)
- if err != nil {
- return telemetry.Error(ctx, span, err, "error ensuring Stripe customer exists")
- }
- }
- lagoCustomerExists := false
- if !lagoCustomerExists {
- adminUser, err := c.getAdminUser(ctx, proj.ID)
- if err != nil {
- return telemetry.Error(ctx, span, err, "error getting admin user")
- }
- // Create usage customer for project and set the usage ID if it doesn't exist
- err = c.ensureLagoCustomerExists(ctx, adminUser.Email, proj)
- if err != nil {
- return telemetry.Error(ctx, span, err, "error ensuring Lago customer exists")
- }
- }
- return nil
- }
- func (c *CheckPaymentEnabledHandler) getAdminUser(ctx context.Context, projectID uint) (adminUser *models.User, err error) {
- ctx, span := telemetry.NewSpan(ctx, "get-project-admin-role")
- defer span.End()
- // Get project roles
- roles, err := c.Repo().Project().ListProjectRolesOrdered(projectID)
- if err != nil {
- return adminUser, telemetry.Error(ctx, span, err, "error listing project roles")
- }
- // Get the project admin user
- for _, role := range roles {
- if role.Kind != types.RoleAdmin {
- continue
- }
- adminUser, err = c.Repo().User().ReadUser(role.UserID)
- if err != nil {
- // If the user is not found, continue to the next role
- continue
- }
- break
- }
- telemetry.WithAttributes(span,
- telemetry.AttributeKV{Key: "admin-user-id", Value: adminUser.ID},
- telemetry.AttributeKV{Key: "admin-user-email", Value: adminUser.Email},
- )
- return adminUser, nil
- }
- func (c *CheckPaymentEnabledHandler) ensureStripeCustomerExists(ctx context.Context, adminUserEmail string, proj *models.Project) (err error) {
- ctx, span := telemetry.NewSpan(ctx, "ensure-stripe-customer-exists")
- defer span.End()
- if !c.Config().BillingManager.StripeConfigLoaded || !proj.GetFeatureFlag(models.BillingEnabled, c.Config().LaunchDarklyClient) || proj.BillingID != "" {
- return nil
- }
- billingID, err := c.Config().BillingManager.StripeClient.CreateCustomer(ctx, adminUserEmail, proj.ID, proj.Name)
- if err != nil {
- return telemetry.Error(ctx, span, err, "error creating billing customer")
- }
- telemetry.WithAttributes(span,
- telemetry.AttributeKV{Key: "billing-id", Value: proj.BillingID},
- )
- proj.BillingID = billingID
- _, err = c.Repo().Project().UpdateProject(proj)
- if err != nil {
- return telemetry.Error(ctx, span, err, "error updating project")
- }
- return nil
- }
- func (c *CheckPaymentEnabledHandler) ensureLagoCustomerExists(ctx context.Context, adminUserEmail string, proj *models.Project) (err error) {
- ctx, span := telemetry.NewSpan(ctx, "ensure-lago-customer-exists")
- defer span.End()
- if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) {
- return nil
- }
- // Check if the customer already exists
- exists, err := c.Config().BillingManager.LagoClient.CheckIfCustomerExists(ctx, proj.ID, proj.EnableSandbox)
- if err != nil {
- return telemetry.Error(ctx, span, err, "error while checking if customer exists")
- }
- telemetry.WithAttributes(span,
- telemetry.AttributeKV{Key: "customer-exists", Value: exists},
- )
- if exists {
- return nil
- }
- err = c.Config().BillingManager.LagoClient.CreateCustomerWithPlan(ctx, adminUserEmail, proj.Name, proj.ID, proj.BillingID, proj.EnableSandbox)
- if err != nil {
- return telemetry.Error(ctx, span, err, "error creating Lago customer")
- }
- return nil
- }
|