create.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. package billing
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "time"
  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/server/shared/requestutils"
  12. "github.com/porter-dev/porter/api/types"
  13. "github.com/porter-dev/porter/internal/analytics"
  14. "github.com/porter-dev/porter/internal/models"
  15. "github.com/porter-dev/porter/internal/telemetry"
  16. )
  17. // CreateBillingHandler is a handler for creating payment methods
  18. type CreateBillingHandler struct {
  19. handlers.PorterHandlerWriter
  20. }
  21. // SetDefaultBillingHandler is a handler for setting default payment method
  22. type SetDefaultBillingHandler struct {
  23. handlers.PorterHandlerWriter
  24. }
  25. // NewCreateBillingHandler will create a new CreateBillingHandler
  26. func NewCreateBillingHandler(
  27. config *config.Config,
  28. decoderValidator shared.RequestDecoderValidator,
  29. writer shared.ResultWriter,
  30. ) *CreateBillingHandler {
  31. return &CreateBillingHandler{
  32. PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  33. }
  34. }
  35. func (c *CreateBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  36. ctx, span := telemetry.NewSpan(r.Context(), "serve-create-billing-method")
  37. defer span.End()
  38. proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
  39. user, _ := ctx.Value(types.UserScope).(*models.User)
  40. clientSecret, err := c.Config().BillingManager.StripeClient.CreatePaymentMethod(ctx, proj.BillingID)
  41. if err != nil {
  42. err := telemetry.Error(ctx, span, err, "error creating payment method")
  43. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating payment method: %w", err)))
  44. return
  45. }
  46. telemetry.WithAttributes(span,
  47. telemetry.AttributeKV{Key: "project-id", Value: proj.ID},
  48. telemetry.AttributeKV{Key: "customer-id", Value: proj.BillingID},
  49. )
  50. if proj.EnableSandbox {
  51. // Grant a reward to the project that referred this user after linking a payment method
  52. err = c.grantRewardIfReferral(ctx, user.ID)
  53. if err != nil {
  54. err := telemetry.Error(ctx, span, err, "error granting credits reward")
  55. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  56. return
  57. }
  58. }
  59. c.WriteResult(w, r, clientSecret)
  60. }
  61. // NewSetDefaultBillingHandler will create a new CreateBillingHandler
  62. func NewSetDefaultBillingHandler(
  63. config *config.Config,
  64. writer shared.ResultWriter,
  65. ) *SetDefaultBillingHandler {
  66. return &SetDefaultBillingHandler{
  67. PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
  68. }
  69. }
  70. func (c *SetDefaultBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  71. ctx, span := telemetry.NewSpan(r.Context(), "serve-set-default-billing-method")
  72. defer span.End()
  73. user, _ := r.Context().Value(types.UserScope).(*models.User)
  74. proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
  75. paymentMethodID, reqErr := requestutils.GetURLParamString(r, types.URLParamPaymentMethodID)
  76. if reqErr != nil {
  77. err := telemetry.Error(ctx, span, reqErr, "error setting default payment method")
  78. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error setting default payment method: %w", err)))
  79. return
  80. }
  81. err := c.Config().BillingManager.StripeClient.SetDefaultPaymentMethod(ctx, paymentMethodID, proj.BillingID)
  82. if err != nil {
  83. err := telemetry.Error(ctx, span, err, "error setting default payment method")
  84. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error setting default payment method: %w", err)))
  85. return
  86. }
  87. telemetry.WithAttributes(span,
  88. telemetry.AttributeKV{Key: "project-id", Value: proj.ID},
  89. telemetry.AttributeKV{Key: "customer-id", Value: proj.BillingID},
  90. telemetry.AttributeKV{Key: "payment-method-id", Value: paymentMethodID},
  91. )
  92. _ = c.Config().AnalyticsClient.Track(analytics.PaymentMethodAttachedTrack(&analytics.PaymentMethodCreateDeleteTrackOpts{
  93. ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, proj.ID),
  94. Email: user.Email,
  95. FirstName: user.FirstName,
  96. LastName: user.LastName,
  97. CompanyName: user.CompanyName,
  98. }))
  99. c.WriteResult(w, r, "")
  100. }
  101. func (c *CreateBillingHandler) grantRewardIfReferral(ctx context.Context, referredUserID uint) (err error) {
  102. ctx, span := telemetry.NewSpan(ctx, "grant-referral-reward")
  103. defer span.End()
  104. referral, err := c.Repo().Referral().GetReferralByReferredID(referredUserID)
  105. if err != nil {
  106. return telemetry.Error(ctx, span, err, "failed to find referral by referred id")
  107. }
  108. if referral == nil {
  109. return nil
  110. }
  111. referralCount, err := c.Repo().Referral().CountReferralsByProjectID(referral.ProjectID, models.ReferralStatusCompleted)
  112. if err != nil {
  113. return telemetry.Error(ctx, span, err, "failed to get referral count by referrer id")
  114. }
  115. maxReferralRewards := c.Config().BillingManager.MetronomeClient.MaxReferralRewards
  116. if referralCount >= maxReferralRewards {
  117. return nil
  118. }
  119. referrerProject, err := c.Repo().Project().ReadProject(referral.ProjectID)
  120. if err != nil {
  121. return telemetry.Error(ctx, span, err, "failed to find referrer project")
  122. }
  123. if referral != nil && referral.Status != models.ReferralStatusCompleted {
  124. // Metronome requires an expiration to be passed in, so we set it to 5 years which in
  125. // practice will mean the credits will most likely run out before expiring
  126. expiresAt := time.Now().AddDate(5, 0, 0).Format(time.RFC3339)
  127. reason := "Referral reward"
  128. rewardAmount := c.Config().BillingManager.MetronomeClient.DefaultRewardAmountCents
  129. paidAmount := c.Config().BillingManager.MetronomeClient.DefaultPaidAmountCents
  130. err := c.Config().BillingManager.MetronomeClient.CreateCreditsGrant(ctx, referrerProject.UsageID, reason, rewardAmount, paidAmount, expiresAt)
  131. if err != nil {
  132. return telemetry.Error(ctx, span, err, "failed to grand credits reward")
  133. }
  134. referral.Status = models.ReferralStatusCompleted
  135. _, err = c.Repo().Referral().UpdateReferral(referral)
  136. if err != nil {
  137. return telemetry.Error(ctx, span, err, "error while updating referral")
  138. }
  139. }
  140. return nil
  141. }