stripe.go 7.7 KB

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