소스 검색

Add endpoint for retrieving customer credits

Mauricio Araujo 2 년 전
부모
커밋
729af4f5f5

+ 44 - 0
api/server/handlers/billing/credits.go

@@ -0,0 +1,44 @@
+package billing
+
+import (
+	"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"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// GetCreditsHandler is a handler for getting available credits
+type GetCreditsHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+// NewGetCreditsHandler will create a new GetCreditsHandler
+func NewGetCreditsHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *GetCreditsHandler {
+	return &GetCreditsHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *GetCreditsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "get-credits-endpoint")
+	defer span.End()
+
+	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+	credits, err := c.Config().BillingManager.MetronomeClient.GetCustomerCredits(proj.UsageID)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting credits")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, credits)
+}

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

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"net/http"
 
+	"github.com/google/uuid"
 	"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"
@@ -55,6 +56,37 @@ func (c *CreateBillingCustomerHandler) ServeHTTP(w http.ResponseWriter, r *http.
 		telemetry.AttributeKV{Key: "user-email", Value: user.Email},
 	)
 
+	// Create Metronome customer and add to starter plan
+	if c.Config().ServerConf.MetronomeAPIKey != "" && c.Config().ServerConf.PorterCloudPlanID != "" &&
+		c.Config().ServerConf.EnableSandbox {
+		// Create Metronome Customer
+		if c.Config().ServerConf.MetronomeAPIKey != "" {
+			usageID, err := c.Config().BillingManager.MetronomeClient.CreateCustomer(user.CompanyName, proj.Name, proj.ID, proj.BillingID)
+			if err != nil {
+				err = telemetry.Error(ctx, span, err, "error creating billing customer")
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
+			proj.UsageID = usageID
+		}
+
+		porterCloudPlanID, err := uuid.Parse(c.Config().ServerConf.PorterCloudPlanID)
+		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error parsing starter plan id")
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		// Add to starter plan
+		customerPlanID, err := c.Config().BillingManager.MetronomeClient.AddCustomerPlan(proj.UsageID, porterCloudPlanID)
+		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error adding customer to starter plan")
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+		proj.UsagePlanID = customerPlanID
+	}
+
 	// Update the project record with the customer ID
 	proj.BillingID = customerID
 	_, err = c.Repo().Project().UpdateProject(proj)

+ 14 - 12
api/server/handlers/project/create.go

@@ -98,26 +98,28 @@ func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		)
 	}
 
-	// Create Metronome Customer
-	if p.Config().ServerConf.MetronomeAPIKey != "" {
-		usageID, err := p.Config().BillingManager.MetronomeClient.CreateCustomer(user.CompanyName, proj.Name, proj.ID, proj.BillingID)
-		if err != nil {
-			err = telemetry.Error(ctx, span, err, "error creating billing customer")
-			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
-		}
-		proj.UsageID = usageID
-	}
-
-	// Add customer to starter plan
+	// Create Metronome customer and add to starter plan
 	if p.Config().ServerConf.MetronomeAPIKey != "" && p.Config().ServerConf.PorterCloudPlanID != "" &&
 		p.Config().ServerConf.EnableSandbox {
+		// Create Metronome Customer
+		if p.Config().ServerConf.MetronomeAPIKey != "" {
+			usageID, err := p.Config().BillingManager.MetronomeClient.CreateCustomer(user.CompanyName, proj.Name, proj.ID, proj.BillingID)
+			if err != nil {
+				err = telemetry.Error(ctx, span, err, "error creating billing customer")
+				p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
+			proj.UsageID = usageID
+		}
+
 		porterCloudPlanID, err := uuid.Parse(p.Config().ServerConf.PorterCloudPlanID)
 		if err != nil {
 			err = telemetry.Error(ctx, span, err, "error parsing starter plan id")
 			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 			return
 		}
+
+		// Add to starter plan
 		customerPlanID, err := p.Config().BillingManager.MetronomeClient.AddCustomerPlan(proj.UsageID, porterCloudPlanID)
 		if err != nil {
 			err = telemetry.Error(ctx, span, err, "error adding customer to starter plan")

+ 0 - 16
api/server/handlers/project/delete.go

@@ -108,22 +108,6 @@ func (p *ProjectDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 			return
 		}
-
-		err = p.Config().BillingManager.MetronomeClient.DeleteCustomer(proj.UsageID)
-		if err != nil {
-			e := "error deleting project in usage provider"
-			err = telemetry.Error(ctx, span, err, e)
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-			return
-		}
-	}
-
-	err = p.Config().BillingManager.StripeClient.DeleteCustomer(ctx, proj)
-	if err != nil {
-		e := "error deleting project in billing provider"
-		err = telemetry.Error(ctx, span, err, e)
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
 	}
 
 	deletedProject, err := p.Repo().Project().DeleteProject(proj)

+ 43 - 0
api/types/billing.go

@@ -61,3 +61,46 @@ type EndCustomerPlanRequest struct {
 	VoidInvoices       bool   `json:"void_invoices"`           // If true, plan end date can be before the last finalized invoice date. Any invoices generated after the plan end date will be voided.
 	VoidStripeInvoices bool   `json:"void_stripe_invoices"`    // Will void Stripe invoices if VoidInvoices is set to true. Drafts will be deleted.
 }
+
+type ListCreditGrantsRequest struct {
+	// An array of credit type IDs. This must not be specified if
+	// credit_grant_ids is specified.
+	CreditTypeIDs []uuid.UUID `json:"credit_type_ids"`
+	// An array of Metronome customer IDs. This must not be specified if
+	// credit_grant_ids is specified.
+	CustomerIDs []uuid.UUID `json:"customer_ids"`
+	// An array of credit grant IDs. If this is specified, neither
+	// credit_type_ids nor customer_ids may be specified.
+	CreditGrantIDs []uuid.UUID `json:"credit_grant_ids"`
+	// Only return credit grants that expire at or after this RFC 3339 timestamp.
+	NotExpiringBefore string `json:"not_expiring_before"`
+	// Only return credit grants that are effective before this RFC 3339 timestamp
+	// (exclusive).
+	EffectiveBefore string `json:"effective_before"`
+}
+
+type CreditType struct {
+	Name string `json:"name"` // The name of the credit type
+	ID   string `json:"id"`   // The UUID of the credit type
+}
+
+type GrantAmount struct {
+	Amount     int64      `json:"amount"`      // The amount of credits granted
+	CreditType CreditType `json:"credit_type"` // The credit type for the amount granted
+}
+
+// Balance represents the effective balance of the grant as of the end of the customer's
+// current billing period.
+type Balance struct {
+	ExcludingPending int64  `json:"excluding_pending"` // The grant's current balance excluding all pending deductions.
+	IncludingPending int64  `json:"including_pending"` // The grant's current balance including all posted and pending deductions.
+	EffectiveAt      string `json:"effective_at"`      // The end date of the customer's current billing period in RFC 3339 format.
+}
+
+type CreditGrant struct {
+	ID          uuid.UUID `json:"id"`
+	Name        string
+	CustomerID  uuid.UUID
+	GrantAmount GrantAmount
+	Balance     Balance
+}

+ 26 - 0
dashboard/src/lib/hooks/useStripe.tsx

@@ -176,6 +176,32 @@ export const usePublishableKey = (): TGetPublishableKey => {
   };
 };
 
+export const usePorterCredits = (): TGetPublishableKey => {
+  const { currentProject } = useContext(Context);
+
+  // Fetch list of payment methods
+  const keyReq = useQuery(
+    ["getPublishableKey", currentProject?.id],
+    async () => {
+      if (!currentProject?.id || currentProject.id === -1) {
+        return;
+      }
+      const res = await api.getPorterCredits(
+        "<token>",
+        {},
+        {
+          project_id: currentProject?.id,
+        }
+      );
+      return res.data;
+    }
+  );
+
+  return {
+    publishableKey: keyReq.data,
+  };
+};
+
 export const useSetDefaultPaymentMethod = (): TSetDefaultPaymentMethod => {
   const { currentProject } = useContext(Context);
 

+ 8 - 0
dashboard/src/shared/api.tsx

@@ -3451,6 +3451,13 @@ const getPublishableKey = baseApi<
   ({ project_id }) => `/api/projects/${project_id}/billing/publishable_key`
 );
 
+const getPorterCredits = baseApi<
+  {},
+  {
+    project_id?: number;
+  }
+>("GET", ({ project_id }) => `/api/projects/${project_id}/billing/credits`);
+
 const getHasBilling = baseApi<{}, { project_id: number }>(
   "GET",
   ({ project_id }) => `/api/projects/${project_id}/billing`
@@ -3848,6 +3855,7 @@ export default {
 
   // BILLING
   getPublishableKey,
+  getPorterCredits,
   listPaymentMethod,
   addPaymentMethod,
   setDefaultPaymentMethod,

+ 22 - 18
internal/billing/metronome.go

@@ -58,24 +58,6 @@ func (m *MetronomeClient) CreateCustomer(orgName string, projectName string, pro
 	return result.Data.ID, nil
 }
 
-func (m *MetronomeClient) DeleteCustomer(customerID uuid.UUID) (err error) {
-	if customerID == uuid.Nil {
-		return fmt.Errorf("customer id cannot be empty")
-	}
-	path := "/customers/archive"
-
-	req := types.CustomerArchiveRequest{
-		CustomerID: customerID,
-	}
-
-	err = post(path, http.MethodPost, m.ApiKey, req, nil)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
 func (m *MetronomeClient) AddCustomerPlan(customerID uuid.UUID, planID uuid.UUID) (customerPlanID uuid.UUID, err error) {
 	if customerID == uuid.Nil || planID == uuid.Nil {
 		return customerPlanID, fmt.Errorf("customer or plan id empty")
@@ -127,6 +109,28 @@ func (m *MetronomeClient) EndCustomerPlan(customerID uuid.UUID, customerPlanID u
 	return nil
 }
 
+func (m *MetronomeClient) GetCustomerCredits(customerID uuid.UUID) (credits int64, err error) {
+	if customerID == uuid.Nil {
+		return credits, fmt.Errorf("customer id empty")
+	}
+
+	path := fmt.Sprintf("credits/listGrants")
+
+	req := types.ListCreditGrantsRequest{
+		CustomerIDs: []uuid.UUID{
+			customerID,
+		},
+	}
+
+	var result types.CreditGrant
+	err = post(path, http.MethodPost, m.ApiKey, req, result)
+	if err != nil {
+		return credits, err
+	}
+
+	return result.Balance.ExcludingPending, nil
+}
+
 func post(path string, method string, apiKey string, body interface{}, data interface{}) (err error) {
 	client := http.Client{}
 	endpoint, err := url.JoinPath(metronomeBaseUrl, path)