Просмотр исходного кода

Add stripe integration backend changes

Mauricio Araujo 2 лет назад
Родитель
Сommit
057318c4cb

+ 39 - 0
api/server/handlers/billing/create.go

@@ -0,0 +1,39 @@
+package billing
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type CreateBillingHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewCreateBillingHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreateBillingHandler {
+	return &CreateBillingHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *CreateBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	clientSecret, err := c.Config().BillingManager.CreatePaymentMethod(proj)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating payment method: %w", err)))
+		return
+	}
+
+	c.WriteResult(w, r, clientSecret)
+}

+ 58 - 0
api/server/handlers/billing/customer.go

@@ -0,0 +1,58 @@
+package billing
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type CreateBillingCustomerIfNotExists struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewCreateBillingCustomerIfNotExists(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreateBillingCustomerIfNotExists {
+	return &CreateBillingCustomerIfNotExists{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *CreateBillingCustomerIfNotExists) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	request := &types.CreateBillingCustomerRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	if proj.BillingID != "" {
+		c.WriteResult(w, r, "")
+		return
+	}
+
+	// Create customer in Stripe
+	customerID, err := c.Config().BillingManager.CreateCustomer(request.UserEmail, proj)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating billing customer: %w", err)))
+		return
+	}
+
+	// Update the project record with the customer ID
+	proj.BillingID = customerID
+	_, err = c.Repo().Project().UpdateProject(proj)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error updating record: %w", err)))
+		return
+	}
+
+	c.WriteResult(w, r, "")
+}

+ 42 - 0
api/server/handlers/billing/delete.go

@@ -0,0 +1,42 @@
+package billing
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+)
+
+type DeleteBillingHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewDeleteBillingHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *DeleteBillingHandler {
+	return &DeleteBillingHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *DeleteBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	paymentMethodID, reqErr := requestutils.GetURLParamString(r, types.URLParamPaymentMethodID)
+	if reqErr != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deleting payment method: %w", fmt.Errorf("invalid id parameter"))))
+		return
+	}
+
+	err := c.Config().BillingManager.DeletePaymentMethod(paymentMethodID)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deleting payment method: %w", err)))
+		return
+	}
+
+	c.WriteResult(w, r, "")
+}

+ 38 - 0
api/server/handlers/billing/list.go

@@ -0,0 +1,38 @@
+package billing
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type ListBillingHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewListBillingHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ListBillingHandler {
+	return &ListBillingHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *ListBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	paymentMethods, err := c.Config().BillingManager.ListPaymentMethod(proj)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error listing payment method: %w", err)))
+		return
+	}
+
+	c.WriteResult(w, r, paymentMethods)
+}

+ 53 - 21
api/server/router/project.go

@@ -285,14 +285,14 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
-	// GET /api/project/{project_id}/billing/redirect -> billing.NewRedirectBillingHandler
-	redirectBillingEndpoint := factory.NewAPIEndpoint(
+	// GET /api/projects/{project_id}/billing/payment_method -> project.NewListBillingHandler
+	listBillingEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/billing/redirect",
+				RelativePath: relPath + "/billing/payment_method",
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -301,25 +301,25 @@ func getProjectRoutes(
 		},
 	)
 
-	redirectBillingHandler := billing.NewRedirectBillingHandler(
+	listBillingHandler := billing.NewListBillingHandler(
 		config,
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &router.Route{
-		Endpoint: redirectBillingEndpoint,
-		Handler:  redirectBillingHandler,
+		Endpoint: listBillingEndpoint,
+		Handler:  listBillingHandler,
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/billing -> project.NewProjectGetBillingHandler
-	getBillingEndpoint := factory.NewAPIEndpoint(
+	// POST /api/projects/{project_id}/billing/payment_method -> project.NewCreateBillingHandler
+	createBillingEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
-			Verb:   types.APIVerbGet,
-			Method: types.HTTPVerbGet,
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/billing",
+				RelativePath: relPath + "/billing/payment_method",
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -328,38 +328,70 @@ func getProjectRoutes(
 		},
 	)
 
-	getBillingHandler := project.NewProjectGetBillingHandler(
+	createBillingHandler := billing.NewCreateBillingHandler(
 		config,
+		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &router.Route{
-		Endpoint: getBillingEndpoint,
-		Handler:  getBillingHandler,
+		Endpoint: createBillingEndpoint,
+		Handler:  createBillingHandler,
 		Router:   r,
 	})
 
-	// GET /api/billing_webhook -> billing.NewBillingWebhookHandler
-	getBillingWebhookEndpoint := factory.NewAPIEndpoint(
+	// DELETE /api/projects/{project_id}/billing/payment_method/{payment_method_id} -> project.NewDeleteBillingHandler
+	deleteBillingEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbDelete,
+			Method: types.HTTPVerbDelete,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/billing/payment_method/{%s}", relPath, types.URLParamPaymentMethodID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	deleteBillingHandler := billing.NewDeleteBillingHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: deleteBillingEndpoint,
+		Handler:  deleteBillingHandler,
+		Router:   r,
+	})
+
+	// POST /api/projects/{project_id}/billing/customer/ -> project.NewGetOrCreateCustomerHandler
+	getOrCreateBillingCustomerEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: "/billing_webhook",
+				RelativePath: relPath + "/billing/customer",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
 			},
-			Scopes: []types.PermissionScope{},
 		},
 	)
 
-	getBillingWebhookHandler := billing.NewBillingWebhookHandler(
+	getOrCreateBillingCustomerHandler := billing.NewCreateBillingCustomerIfNotExists(
 		config,
 		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &router.Route{
-		Endpoint: getBillingWebhookEndpoint,
-		Handler:  getBillingWebhookHandler,
+		Endpoint: getOrCreateBillingCustomerEndpoint,
+		Handler:  getOrCreateBillingCustomerHandler,
 		Router:   r,
 	})
 

+ 1 - 0
api/server/shared/config/env/envconfs.go

@@ -69,6 +69,7 @@ type ServerConf struct {
 	SendgridDeleteProjectTemplateID    string `env:"SENDGRID_DELETE_PROJECT_TEMPLATE_ID"`
 	SendgridSenderEmail                string `env:"SENDGRID_SENDER_EMAIL"`
 
+	StripeSecretKey   string `env:"STRIPE_SECRET_KEY"`
 	SlackClientID     string `env:"SLACK_CLIENT_ID"`
 	SlackClientSecret string `env:"SLACK_CLIENT_SECRET"`
 

+ 16 - 14
api/server/shared/config/loader/init_ee.go

@@ -4,9 +4,9 @@
 package loader
 
 import (
-	eeBilling "github.com/porter-dev/porter/ee/billing"
+	// TODO: delete once the billing code is cleaned up
+	// eeBilling "github.com/porter-dev/porter/ee/billing"
 	"github.com/porter-dev/porter/ee/models"
-	"github.com/porter-dev/porter/internal/billing"
 )
 
 func init() {
@@ -23,18 +23,20 @@ func init() {
 		key[i] = b
 	}
 
-	if InstanceEnvConf.ServerConf.BillingPrivateServerURL != "" && InstanceEnvConf.ServerConf.BillingPrivateKey != "" && InstanceEnvConf.ServerConf.BillingPublicServerURL != "" {
-		serverURL := InstanceEnvConf.ServerConf.BillingPrivateServerURL
-		publicServerURL := InstanceEnvConf.ServerConf.BillingPublicServerURL
-		apiKey := InstanceEnvConf.ServerConf.BillingPrivateKey
-		var err error
+	// TODO: delete once the billing code is cleaned up
 
-		InstanceBillingManager, err = eeBilling.NewClient(serverURL, publicServerURL, apiKey)
+	// if InstanceEnvConf.ServerConf.BillingPrivateServerURL != "" && InstanceEnvConf.ServerConf.BillingPrivateKey != "" && InstanceEnvConf.ServerConf.BillingPublicServerURL != "" {
+	// 	serverURL := InstanceEnvConf.ServerConf.BillingPrivateServerURL
+	// 	publicServerURL := InstanceEnvConf.ServerConf.BillingPublicServerURL
+	// 	apiKey := InstanceEnvConf.ServerConf.BillingPrivateKey
+	// 	var err error
 
-		if err != nil {
-			panic(err)
-		}
-	} else {
-		InstanceBillingManager = &billing.NoopBillingManager{}
-	}
+	// 	InstanceBillingManager, err = eeBilling.NewClient(serverURL, publicServerURL, apiKey)
+
+	// 	if err != nil {
+	// 		panic(err)
+	// 	}
+	// } else {
+	// 	InstanceBillingManager = &billing.StripeBillingManager{}
+	// }
 }

+ 7 - 1
api/server/shared/config/loader/loader.go

@@ -63,7 +63,9 @@ func sharedInit() {
 		panic(err)
 	}
 
-	InstanceBillingManager = &billing.NoopBillingManager{}
+	InstanceBillingManager = &billing.StripeBillingManager{
+		StripeSecretKey: InstanceEnvConf.ServerConf.StripeSecretKey,
+	}
 }
 
 func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
@@ -251,6 +253,10 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 	}
 	res.LaunchDarklyClient = launchDarklyClient
 
+	if sc.StripeSecretKey == "" {
+		return nil, fmt.Errorf("STRIPE_SECRET_KEY must be set")
+	}
+
 	if sc.SlackClientID != "" && sc.SlackClientSecret != "" {
 		res.Logger.Info().Msg("Creating Slack client")
 		res.SlackConf = oauth.NewSlackClient(&oauth.Config{

+ 8 - 11
api/types/billing.go

@@ -1,15 +1,12 @@
 package types
 
-type AddProjectBillingRequest struct {
-	ProjectID uint `json:"project_id" form:"required"`
+import "github.com/stripe/stripe-go/v76"
 
-	// Monthly price, in cents
-	Price uint `json:"price" form:"required"`
-
-	Users    uint `json:"users"`
-	Clusters uint `json:"clusters"`
-	CPU      uint `json:"cpu"`
-	Memory   uint `json:"memory"`
-
-	ExistingPlanName string `json:"existing_plan_name"`
+// AddProjectBillingRequest is a request for creating a new billing customer.
+type CreateBillingCustomerRequest struct {
+	UserEmail string `json:"user_email" form:"required"`
 }
+
+// PaymentMethod is a wrapper for the Stripe type, but it may be changed to include only
+// the necessary fields.
+type PaymentMethod = *stripe.PaymentMethod

+ 1 - 0
api/types/request.go

@@ -55,6 +55,7 @@ const (
 	URLParamAppRevisionID         URLParam = "app_revision_id"
 	URLParamDatastoreType         URLParam = "datastore_type"
 	URLParamDatastoreName         URLParam = "datastore_name"
+	URLParamPaymentMethodID       URLParam = "payment_method_id"
 	URLParamNotificationConfigID  URLParam = "notification_config_id"
 	URLParamNotificationID        URLParam = "notification_id"
 	URLParamCloudProviderType     URLParam = "cloud_provider_type"

+ 3 - 0
zarf/helm/.serverenv

@@ -66,3 +66,6 @@ HELM_APP_REPO_URL=http://chartmuseum:8080
 
 TELEMETRY_NAME=porter
 TELEMETRY_COLLECTOR_URL=otel-collector:4317
+
+# STRIPE_SECRET_KEY is required if billing is enabled
+STRIPE_SECRET_KEY=