list.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. package billing
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "github.com/google/uuid"
  7. "github.com/porter-dev/porter/api/server/handlers"
  8. "github.com/porter-dev/porter/api/server/shared"
  9. "github.com/porter-dev/porter/api/server/shared/apierrors"
  10. "github.com/porter-dev/porter/api/server/shared/config"
  11. "github.com/porter-dev/porter/api/types"
  12. "github.com/porter-dev/porter/internal/models"
  13. "github.com/porter-dev/porter/internal/telemetry"
  14. )
  15. // ListBillingHandler is a handler for listing payment methods
  16. type ListBillingHandler struct {
  17. handlers.PorterHandlerWriter
  18. }
  19. // CheckPaymentEnabledHandler is a handler for checking if payment is setup
  20. type CheckPaymentEnabledHandler struct {
  21. handlers.PorterHandlerWriter
  22. }
  23. // NewListBillingHandler will create a new ListBillingHandler
  24. func NewListBillingHandler(
  25. config *config.Config,
  26. writer shared.ResultWriter,
  27. ) *ListBillingHandler {
  28. return &ListBillingHandler{
  29. PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
  30. }
  31. }
  32. func (c *ListBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  33. ctx, span := telemetry.NewSpan(r.Context(), "serve-list-payment-methods")
  34. defer span.End()
  35. proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
  36. paymentMethods, err := c.Config().BillingManager.StripeClient.ListPaymentMethod(ctx, proj.BillingID)
  37. if err != nil {
  38. err := telemetry.Error(ctx, span, err, "error listing payment method")
  39. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error listing payment method: %w", err)))
  40. return
  41. }
  42. c.WriteResult(w, r, paymentMethods)
  43. }
  44. // NewCheckPaymentEnabledHandler will create a new CheckPaymentEnabledHandler
  45. func NewCheckPaymentEnabledHandler(
  46. config *config.Config,
  47. writer shared.ResultWriter,
  48. ) *CheckPaymentEnabledHandler {
  49. return &CheckPaymentEnabledHandler{
  50. PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
  51. }
  52. }
  53. func (c *CheckPaymentEnabledHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  54. ctx, span := telemetry.NewSpan(r.Context(), "serve-check-payment-enabled")
  55. defer span.End()
  56. proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
  57. currentUser, _ := ctx.Value(types.UserScope).(*models.User)
  58. err := c.ensureBillingSetup(ctx, proj, currentUser)
  59. if err != nil {
  60. err := telemetry.Error(ctx, span, err, "error ensuring billing setup")
  61. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  62. return
  63. }
  64. telemetry.WithAttributes(span,
  65. telemetry.AttributeKV{Key: "project-id", Value: proj.ID},
  66. telemetry.AttributeKV{Key: "customer-id", Value: proj.BillingID},
  67. )
  68. paymentEnabled, err := c.Config().BillingManager.StripeClient.CheckPaymentEnabled(ctx, proj.BillingID)
  69. if err != nil {
  70. err := telemetry.Error(ctx, span, err, "error checking if payment enabled")
  71. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  72. return
  73. }
  74. telemetry.WithAttributes(span,
  75. telemetry.AttributeKV{Key: "payment-enabled", Value: paymentEnabled},
  76. )
  77. c.WriteResult(w, r, paymentEnabled)
  78. }
  79. func (c *CheckPaymentEnabledHandler) ensureBillingSetup(ctx context.Context, proj *models.Project, user *models.User) (err error) {
  80. ctx, span := telemetry.NewSpan(ctx, "ensure-billing-setup")
  81. defer span.End()
  82. telemetry.WithAttributes(span,
  83. telemetry.AttributeKV{Key: "billing-id", Value: proj.BillingID},
  84. telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
  85. )
  86. if proj.BillingID == "" || proj.UsageID == uuid.Nil {
  87. adminUser, err := c.getAdminUser(ctx, proj.ID)
  88. if err != nil {
  89. return telemetry.Error(ctx, span, err, "error getting admin user")
  90. }
  91. // If the admin user is not found, use the current user as last resort
  92. if adminUser == nil {
  93. adminUser = user
  94. }
  95. // Create billing customer for project and set the billing ID if it doesn't exist
  96. err = c.ensureStripeCustomerExists(ctx, adminUser.Email, proj)
  97. if err != nil {
  98. return telemetry.Error(ctx, span, err, "error ensuring Stripe customer exists")
  99. }
  100. // Create usage customer for project and set the usage ID if it doesn't exist
  101. err = c.ensureMetronomeCustomerExists(ctx, adminUser.Email, proj)
  102. if err != nil {
  103. return telemetry.Error(ctx, span, err, "error ensuring Metronome customer exists")
  104. }
  105. }
  106. return nil
  107. }
  108. func (c *CheckPaymentEnabledHandler) getAdminUser(ctx context.Context, projectID uint) (adminUser *models.User, err error) {
  109. ctx, span := telemetry.NewSpan(ctx, "get-project-admin-role")
  110. defer span.End()
  111. // Get project roles
  112. roles, err := c.Repo().Project().ListProjectRolesOrdered(projectID)
  113. if err != nil {
  114. return adminUser, telemetry.Error(ctx, span, err, "error listing project roles")
  115. }
  116. // Get the project admin user
  117. for _, role := range roles {
  118. if role.Kind != types.RoleAdmin {
  119. continue
  120. }
  121. adminUser, err = c.Repo().User().ReadUser(role.UserID)
  122. if err != nil {
  123. // If the user is not found, continue to the next role
  124. continue
  125. }
  126. break
  127. }
  128. telemetry.WithAttributes(span,
  129. telemetry.AttributeKV{Key: "admin-user-id", Value: adminUser.ID},
  130. telemetry.AttributeKV{Key: "admin-user-email", Value: adminUser.Email},
  131. )
  132. return adminUser, nil
  133. }
  134. func (c *CheckPaymentEnabledHandler) ensureStripeCustomerExists(ctx context.Context, adminUserEmail string, proj *models.Project) (err error) {
  135. ctx, span := telemetry.NewSpan(ctx, "ensure-stripe-customer-exists")
  136. defer span.End()
  137. if !c.Config().BillingManager.StripeConfigLoaded || !proj.GetFeatureFlag(models.BillingEnabled, c.Config().LaunchDarklyClient) || proj.BillingID != "" {
  138. return nil
  139. }
  140. billingID, err := c.Config().BillingManager.StripeClient.CreateCustomer(ctx, adminUserEmail, proj.ID, proj.Name)
  141. if err != nil {
  142. return telemetry.Error(ctx, span, err, "error creating billing customer")
  143. }
  144. telemetry.WithAttributes(span,
  145. telemetry.AttributeKV{Key: "billing-id", Value: proj.BillingID},
  146. )
  147. proj.BillingID = billingID
  148. _, err = c.Repo().Project().UpdateProject(proj)
  149. if err != nil {
  150. return telemetry.Error(ctx, span, err, "error updating project")
  151. }
  152. return nil
  153. }
  154. func (c *CheckPaymentEnabledHandler) ensureMetronomeCustomerExists(ctx context.Context, adminUserEmail string, proj *models.Project) (err error) {
  155. ctx, span := telemetry.NewSpan(ctx, "ensure-metronome-customer-exists")
  156. defer span.End()
  157. if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) || proj.UsageID != uuid.Nil {
  158. return nil
  159. }
  160. customerID, customerPlanID, err := c.Config().BillingManager.MetronomeClient.CreateCustomerWithPlan(ctx, adminUserEmail, proj.Name, proj.ID, proj.BillingID, proj.EnableSandbox)
  161. if err != nil {
  162. return telemetry.Error(ctx, span, err, "error creating Metronome customer")
  163. }
  164. telemetry.WithAttributes(span,
  165. telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
  166. telemetry.AttributeKV{Key: "usage-plan-id", Value: proj.UsagePlanID},
  167. )
  168. proj.UsageID = customerID
  169. proj.UsagePlanID = customerPlanID
  170. _, err = c.Repo().Project().UpdateProject(proj)
  171. if err != nil {
  172. return telemetry.Error(ctx, span, err, "error updating project")
  173. }
  174. return nil
  175. }