list.go 6.9 KB

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