stripe.go 8.8 KB

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