| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- package billing
- import (
- "context"
- "fmt"
- "strconv"
- "time"
- "github.com/porter-dev/porter/api/types"
- "github.com/porter-dev/porter/internal/telemetry"
- "github.com/stripe/stripe-go/v76"
- "github.com/stripe/stripe-go/v76/customer"
- "github.com/stripe/stripe-go/v76/invoice"
- "github.com/stripe/stripe-go/v76/paymentmethod"
- "github.com/stripe/stripe-go/v76/setupintent"
- )
- // StripeClient interacts with the Stripe API to manage payment methods
- // and customers
- type StripeClient struct {
- SecretKey string
- PublishableKey string
- }
- // NewStripeClient creates a new client to call the Stripe API
- func NewStripeClient(secretKey string, publishableKey string) StripeClient {
- return StripeClient{
- SecretKey: secretKey,
- PublishableKey: publishableKey,
- }
- }
- // CreateCustomer will create a customer in Stripe only if the project doesn't have a BillingID
- func (s StripeClient) CreateCustomer(ctx context.Context, userEmail string, projectID uint, projectName string) (customerID string, err error) {
- ctx, span := telemetry.NewSpan(ctx, "create-stripe-customer")
- defer span.End()
- if projectID == 0 || projectName == "" {
- return "", fmt.Errorf("invalid project id or name")
- }
- stripe.Key = s.SecretKey
- // Create customer if not exists
- customerName := fmt.Sprintf("project_%s", projectName)
- projectIDStr := strconv.FormatUint(uint64(projectID), 10)
- params := &stripe.CustomerParams{
- Name: stripe.String(customerName),
- Email: stripe.String(userEmail),
- Metadata: map[string]string{
- "porter_project_id": projectIDStr,
- },
- }
- // Create in Stripe
- customer, err := customer.New(params)
- if err != nil {
- return "", telemetry.Error(ctx, span, err, "failed to create Stripe customer")
- }
- customerID = customer.ID
- telemetry.WithAttributes(span,
- telemetry.AttributeKV{Key: "project-id", Value: projectIDStr},
- telemetry.AttributeKV{Key: "customer-id", Value: customerID},
- telemetry.AttributeKV{Key: "user-email", Value: userEmail},
- )
- return customerID, nil
- }
- // DeleteCustomer will delete the customer from the billing provider
- func (s StripeClient) DeleteCustomer(ctx context.Context, customerID string) (err error) {
- ctx, span := telemetry.NewSpan(ctx, "delete-stripe-customer")
- defer span.End()
- if customerID == "" {
- return nil
- }
- stripe.Key = s.SecretKey
- telemetry.WithAttributes(span,
- telemetry.AttributeKV{Key: "billing-id", Value: customerID},
- )
- params := &stripe.CustomerParams{}
- _, err = customer.Del(customerID, params)
- if err != nil {
- return telemetry.Error(ctx, span, err, "failed to delete Stripe customer")
- }
- return nil
- }
- // CheckPaymentEnabled will return true if the project has a payment method added, false otherwise
- func (s StripeClient) CheckPaymentEnabled(ctx context.Context, customerID string) (paymentEnabled bool, err error) {
- _, span := telemetry.NewSpan(ctx, "check-stripe-payment-enabled")
- defer span.End()
- if customerID == "" {
- return false, fmt.Errorf("customer id cannot be empty")
- }
- stripe.Key = s.SecretKey
- params := &stripe.PaymentMethodListParams{
- Customer: stripe.String(customerID),
- Type: stripe.String(string(stripe.PaymentMethodTypeCard)),
- }
- result := paymentmethod.List(params)
- return result.Next(), nil
- }
- // ListPaymentMethod will return all payment methods for the project
- func (s StripeClient) ListPaymentMethod(ctx context.Context, customerID string) (paymentMethods []types.PaymentMethod, err error) {
- ctx, span := telemetry.NewSpan(ctx, "list-stripe-payment-method")
- defer span.End()
- if customerID == "" {
- return paymentMethods, fmt.Errorf("customer id cannot be empty")
- }
- stripe.Key = s.SecretKey
- // Get configured payment methods
- params := &stripe.PaymentMethodListParams{
- Customer: stripe.String(customerID),
- Type: stripe.String(string(stripe.PaymentMethodTypeCard)),
- }
- result := paymentmethod.List(params)
- defaultPaymentExists, defaultPaymentID, err := s.checkDefaultPaymentMethod(customerID)
- if err != nil {
- return paymentMethods, telemetry.Error(ctx, span, err, "failed to list Stripe payment method")
- }
- for result.Next() {
- stripePaymentMethod := result.PaymentMethod()
- var isDefaultPaymentMethod bool
- if stripePaymentMethod.ID == defaultPaymentID {
- isDefaultPaymentMethod = true
- }
- paymentMethods = append(paymentMethods, types.PaymentMethod{
- ID: stripePaymentMethod.ID,
- DisplayBrand: stripePaymentMethod.Card.DisplayBrand,
- Last4: stripePaymentMethod.Card.Last4,
- ExpMonth: stripePaymentMethod.Card.ExpMonth,
- ExpYear: stripePaymentMethod.Card.ExpYear,
- Default: isDefaultPaymentMethod,
- })
- }
- // Set default payment method when project has payment methods enabled but
- // no default setup
- if len(paymentMethods) > 0 && !defaultPaymentExists {
- err = s.SetDefaultPaymentMethod(ctx, paymentMethods[len(paymentMethods)-1].ID, customerID)
- if err != nil {
- return paymentMethods, telemetry.Error(ctx, span, err, "failed to list Stripe payment method")
- }
- }
- return paymentMethods, nil
- }
- // CreatePaymentMethod will add a new payment method to the project in Stripe
- func (s StripeClient) CreatePaymentMethod(ctx context.Context, customerID string) (clientSecret string, err error) {
- ctx, span := telemetry.NewSpan(ctx, "create-stripe-payment-method")
- defer span.End()
- if customerID == "" {
- return "", fmt.Errorf("customer id cannot be empty")
- }
- stripe.Key = s.SecretKey
- params := &stripe.SetupIntentParams{
- Customer: stripe.String(customerID),
- AutomaticPaymentMethods: &stripe.SetupIntentAutomaticPaymentMethodsParams{
- Enabled: stripe.Bool(false),
- },
- PaymentMethodTypes: []*string{stripe.String("card")},
- }
- intent, err := setupintent.New(params)
- if err != nil {
- return "", telemetry.Error(ctx, span, err, "failed to create Stripe payment method")
- }
- return intent.ClientSecret, nil
- }
- // SetDefaultPaymentMethod will add a new payment method to the project in Stripe
- func (s StripeClient) SetDefaultPaymentMethod(ctx context.Context, paymentMethodID string, customerID string) (err error) {
- ctx, span := telemetry.NewSpan(ctx, "set-default-stripe-payment-method")
- defer span.End()
- if customerID == "" || paymentMethodID == "" {
- return fmt.Errorf("empty customer id or payment method id")
- }
- stripe.Key = s.SecretKey
- params := &stripe.CustomerParams{
- InvoiceSettings: &stripe.CustomerInvoiceSettingsParams{
- DefaultPaymentMethod: stripe.String(paymentMethodID),
- },
- }
- _, err = customer.Update(customerID, params)
- if err != nil {
- return telemetry.Error(ctx, span, err, "failed to set default Stripe payment method")
- }
- return nil
- }
- // DeletePaymentMethod will remove a payment method for the project in Stripe
- func (s StripeClient) DeletePaymentMethod(ctx context.Context, paymentMethodID string) (err error) {
- ctx, span := telemetry.NewSpan(ctx, "delete-stripe-payment-method")
- defer span.End()
- if paymentMethodID == "" {
- return fmt.Errorf("payment method id cannot be empty")
- }
- stripe.Key = s.SecretKey
- _, err = paymentmethod.Detach(paymentMethodID, nil)
- if err != nil {
- return telemetry.Error(ctx, span, err, "failed to delete Stripe payment method")
- }
- return nil
- }
- // GetPublishableKey returns the Stripe publishable key
- func (s StripeClient) GetPublishableKey(ctx context.Context) (key string) {
- _, span := telemetry.NewSpan(ctx, "get-stripe-publishable-key")
- defer span.End()
- return s.PublishableKey
- }
- // ListCustomerInvoices will return all invoices for the customer with the given status
- func (s StripeClient) ListCustomerInvoices(ctx context.Context, customerID string, status string) (invoiceList []types.Invoice, err error) {
- ctx, span := telemetry.NewSpan(ctx, "populate-invoice-urls")
- defer span.End()
- if customerID == "" {
- return invoiceList, telemetry.Error(ctx, span, err, "customer id cannot be empty")
- }
- stripe.Key = s.SecretKey
- params := &stripe.InvoiceListParams{
- Customer: stripe.String(customerID),
- Status: stripe.String(status),
- }
- result := invoice.List(params)
- for result.Next() {
- invoice := result.Current().(*stripe.Invoice)
- if invoice == nil {
- continue
- }
- createdTimestamp := time.Unix(invoice.Created, 0)
- invoiceList = append(invoiceList, types.Invoice{
- HostedInvoiceURL: invoice.HostedInvoiceURL,
- Status: string(invoice.Status),
- Created: createdTimestamp.Format(time.RFC3339),
- })
- }
- return invoiceList, nil
- }
- func (s StripeClient) checkDefaultPaymentMethod(customerID string) (defaultPaymentExists bool, defaultPaymentID string, err error) {
- // Get customer to check default payment method
- customer, err := customer.Get(customerID, nil)
- if err != nil {
- return defaultPaymentExists, defaultPaymentID, err
- }
- if customer.InvoiceSettings != nil && customer.InvoiceSettings.DefaultPaymentMethod != nil {
- defaultPaymentExists = true
- defaultPaymentID = customer.InvoiceSettings.DefaultPaymentMethod.ID
- }
- return defaultPaymentExists, defaultPaymentID, err
- }
|