stripe.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. package billing
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "github.com/porter-dev/porter/api/types"
  7. "github.com/porter-dev/porter/internal/telemetry"
  8. "github.com/stripe/stripe-go/v76"
  9. "github.com/stripe/stripe-go/v76/customer"
  10. "github.com/stripe/stripe-go/v76/paymentmethod"
  11. "github.com/stripe/stripe-go/v76/setupintent"
  12. )
  13. // StripeClient interacts with the Stripe API to manage payment methods
  14. // and customers
  15. type StripeClient struct {
  16. SecretKey string
  17. PublishableKey string
  18. }
  19. // NewStripeClient creates a new client to call the Stripe API
  20. func NewStripeClient(secretKey string, publishableKey string) StripeClient {
  21. return StripeClient{
  22. SecretKey: secretKey,
  23. PublishableKey: publishableKey,
  24. }
  25. }
  26. // CreateCustomer will create a customer in Stripe only if the project doesn't have a BillingID
  27. func (s StripeClient) CreateCustomer(ctx context.Context, userEmail string, projectID uint, projectName string) (customerID string, err error) {
  28. ctx, span := telemetry.NewSpan(ctx, "create-stripe-customer")
  29. defer span.End()
  30. if projectID == 0 || projectName == "" {
  31. return "", fmt.Errorf("invalid project id or name")
  32. }
  33. stripe.Key = s.SecretKey
  34. // Create customer if not exists
  35. customerName := fmt.Sprintf("project_%s", projectName)
  36. projectIDStr := strconv.FormatUint(uint64(projectID), 10)
  37. params := &stripe.CustomerParams{
  38. Name: stripe.String(customerName),
  39. Email: stripe.String(userEmail),
  40. Metadata: map[string]string{
  41. "porter_project_id": projectIDStr,
  42. },
  43. }
  44. // Create in Stripe
  45. customer, err := customer.New(params)
  46. if err != nil {
  47. return "", telemetry.Error(ctx, span, err, "failed to create Stripe customer")
  48. }
  49. customerID = customer.ID
  50. telemetry.WithAttributes(span,
  51. telemetry.AttributeKV{Key: "project-id", Value: projectIDStr},
  52. telemetry.AttributeKV{Key: "customer-id", Value: customerID},
  53. telemetry.AttributeKV{Key: "user-email", Value: userEmail},
  54. )
  55. return customerID, nil
  56. }
  57. // DeleteCustomer will delete the customer from the billing provider
  58. func (s StripeClient) DeleteCustomer(ctx context.Context, customerID string) (err error) {
  59. ctx, span := telemetry.NewSpan(ctx, "delete-stripe-customer")
  60. defer span.End()
  61. if customerID == "" {
  62. return nil
  63. }
  64. stripe.Key = s.SecretKey
  65. telemetry.WithAttributes(span,
  66. telemetry.AttributeKV{Key: "billing-id", Value: customerID},
  67. )
  68. params := &stripe.CustomerParams{}
  69. _, err = customer.Del(customerID, params)
  70. if err != nil {
  71. return telemetry.Error(ctx, span, err, "failed to delete Stripe customer")
  72. }
  73. return nil
  74. }
  75. // CheckPaymentEnabled will return true if the project has a payment method added, false otherwise
  76. func (s StripeClient) CheckPaymentEnabled(ctx context.Context, customerID string) (paymentEnabled bool, err error) {
  77. _, span := telemetry.NewSpan(ctx, "check-stripe-payment-enabled")
  78. defer span.End()
  79. if customerID == "" {
  80. return false, fmt.Errorf("customer id cannot be empty")
  81. }
  82. stripe.Key = s.SecretKey
  83. params := &stripe.PaymentMethodListParams{
  84. Customer: stripe.String(customerID),
  85. Type: stripe.String(string(stripe.PaymentMethodTypeCard)),
  86. }
  87. result := paymentmethod.List(params)
  88. return result.Next(), nil
  89. }
  90. // ListPaymentMethod will return all payment methods for the project
  91. func (s StripeClient) ListPaymentMethod(ctx context.Context, customerID string) (paymentMethods []types.PaymentMethod, err error) {
  92. ctx, span := telemetry.NewSpan(ctx, "list-stripe-payment-method")
  93. defer span.End()
  94. if customerID == "" {
  95. return paymentMethods, fmt.Errorf("customer id cannot be empty")
  96. }
  97. stripe.Key = s.SecretKey
  98. // Get configured payment methods
  99. params := &stripe.PaymentMethodListParams{
  100. Customer: stripe.String(customerID),
  101. Type: stripe.String(string(stripe.PaymentMethodTypeCard)),
  102. }
  103. result := paymentmethod.List(params)
  104. defaultPaymentExists, defaultPaymentID, err := s.checkDefaultPaymentMethod(customerID)
  105. if err != nil {
  106. return paymentMethods, telemetry.Error(ctx, span, err, "failed to list Stripe payment method")
  107. }
  108. for result.Next() {
  109. stripePaymentMethod := result.PaymentMethod()
  110. var isDefaultPaymentMethod bool
  111. if stripePaymentMethod.ID == defaultPaymentID {
  112. isDefaultPaymentMethod = true
  113. }
  114. paymentMethods = append(paymentMethods, types.PaymentMethod{
  115. ID: stripePaymentMethod.ID,
  116. DisplayBrand: stripePaymentMethod.Card.DisplayBrand,
  117. Last4: stripePaymentMethod.Card.Last4,
  118. ExpMonth: stripePaymentMethod.Card.ExpMonth,
  119. ExpYear: stripePaymentMethod.Card.ExpYear,
  120. Default: isDefaultPaymentMethod,
  121. })
  122. }
  123. // Set default payment method when project has payment methods enabled but
  124. // no default setup
  125. if len(paymentMethods) > 0 && !defaultPaymentExists {
  126. err = s.SetDefaultPaymentMethod(ctx, paymentMethods[len(paymentMethods)-1].ID, customerID)
  127. if err != nil {
  128. return paymentMethods, telemetry.Error(ctx, span, err, "failed to list Stripe payment method")
  129. }
  130. }
  131. return paymentMethods, nil
  132. }
  133. // CreatePaymentMethod will add a new payment method to the project in Stripe
  134. func (s StripeClient) CreatePaymentMethod(ctx context.Context, customerID string) (clientSecret string, err error) {
  135. ctx, span := telemetry.NewSpan(ctx, "create-stripe-payment-method")
  136. defer span.End()
  137. if customerID == "" {
  138. return "", fmt.Errorf("customer id cannot be empty")
  139. }
  140. stripe.Key = s.SecretKey
  141. params := &stripe.SetupIntentParams{
  142. Customer: stripe.String(customerID),
  143. AutomaticPaymentMethods: &stripe.SetupIntentAutomaticPaymentMethodsParams{
  144. Enabled: stripe.Bool(false),
  145. },
  146. PaymentMethodTypes: []*string{stripe.String("card")},
  147. }
  148. intent, err := setupintent.New(params)
  149. if err != nil {
  150. return "", telemetry.Error(ctx, span, err, "failed to create Stripe payment method")
  151. }
  152. return intent.ClientSecret, nil
  153. }
  154. // SetDefaultPaymentMethod will add a new payment method to the project in Stripe
  155. func (s StripeClient) SetDefaultPaymentMethod(ctx context.Context, paymentMethodID string, customerID string) (err error) {
  156. ctx, span := telemetry.NewSpan(ctx, "set-default-stripe-payment-method")
  157. defer span.End()
  158. if customerID == "" || paymentMethodID == "" {
  159. return fmt.Errorf("empty customer id or payment method id")
  160. }
  161. stripe.Key = s.SecretKey
  162. params := &stripe.CustomerParams{
  163. InvoiceSettings: &stripe.CustomerInvoiceSettingsParams{
  164. DefaultPaymentMethod: stripe.String(paymentMethodID),
  165. },
  166. }
  167. _, err = customer.Update(customerID, params)
  168. if err != nil {
  169. return telemetry.Error(ctx, span, err, "failed to set default Stripe payment method")
  170. }
  171. return nil
  172. }
  173. // DeletePaymentMethod will remove a payment method for the project in Stripe
  174. func (s StripeClient) DeletePaymentMethod(ctx context.Context, paymentMethodID string) (err error) {
  175. ctx, span := telemetry.NewSpan(ctx, "delete-stripe-payment-method")
  176. defer span.End()
  177. if paymentMethodID == "" {
  178. return fmt.Errorf("payment method id cannot be empty")
  179. }
  180. stripe.Key = s.SecretKey
  181. _, err = paymentmethod.Detach(paymentMethodID, nil)
  182. if err != nil {
  183. return telemetry.Error(ctx, span, err, "failed to delete Stripe payment method")
  184. }
  185. return nil
  186. }
  187. // GetPublishableKey returns the Stripe publishable key
  188. func (s StripeClient) GetPublishableKey(ctx context.Context) (key string) {
  189. _, span := telemetry.NewSpan(ctx, "get-stripe-publishable-key")
  190. defer span.End()
  191. return s.PublishableKey
  192. }
  193. func (s StripeClient) checkDefaultPaymentMethod(customerID string) (defaultPaymentExists bool, defaultPaymentID string, err error) {
  194. // Get customer to check default payment method
  195. customer, err := customer.Get(customerID, nil)
  196. if err != nil {
  197. return defaultPaymentExists, defaultPaymentID, err
  198. }
  199. if customer.InvoiceSettings != nil && customer.InvoiceSettings.DefaultPaymentMethod != nil {
  200. defaultPaymentExists = true
  201. defaultPaymentID = customer.InvoiceSettings.DefaultPaymentMethod.ID
  202. }
  203. return defaultPaymentExists, defaultPaymentID, err
  204. }