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

Add plan details endpoint, add plan to frontend, fix credits endpoint

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

+ 68 - 22
api/server/handlers/billing/credits.go → api/server/handlers/billing/plan.go

@@ -12,39 +12,69 @@ import (
 	"github.com/porter-dev/porter/internal/telemetry"
 )
 
-// GetCreditsHandler is a handler for getting available credits
-type GetCreditsHandler struct {
+// ListPlansHandler is a handler for getting customer plans
+type ListPlansHandler struct {
 	handlers.PorterHandlerWriter
 }
 
-// GetUsageDashboardHandler returns an embeddable dashboard to display information related to customer usage.
-type GetUsageDashboardHandler struct {
-	handlers.PorterHandlerReadWriter
-}
-
-// NewGetCreditsHandler will create a new GetCreditsHandler
-func NewGetCreditsHandler(
+// NewListCreditsHandler will create a new ListPlansHandler
+func NewListPlansHandler(
 	config *config.Config,
 	writer shared.ResultWriter,
-) *GetCreditsHandler {
-	return &GetCreditsHandler{
+) *ListPlansHandler {
+	return &ListPlansHandler{
 		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
 	}
 }
 
-// NewGetUsageDashboardHandler returns a new GetUsageDashboardHandler
-func NewGetUsageDashboardHandler(
+func (c *ListPlansHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "list-plans-endpoint")
+	defer span.End()
+
+	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+	if !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) {
+		c.WriteResult(w, r, "")
+
+		telemetry.WithAttributes(span,
+			telemetry.AttributeKV{Key: "metronome-enabled", Value: false},
+		)
+		return
+	}
+
+	plan, err := c.Config().BillingManager.MetronomeClient.ListCustomerPlan(ctx, proj.UsageID)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error listing plans")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "metronome-enabled", Value: true},
+		telemetry.AttributeKV{Key: "project-id", Value: proj.ID},
+		telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
+	)
+
+	c.WriteResult(w, r, plan)
+}
+
+// ListCreditsHandler is a handler for getting available credits
+type ListCreditsHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+// NewListCreditsHandler will create a new ListCreditsHandler
+func NewListCreditsHandler(
 	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
-) *GetUsageDashboardHandler {
-	return &GetUsageDashboardHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+) *ListCreditsHandler {
+	return &ListCreditsHandler{
+		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")
+func (c *ListCreditsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "list-credits-endpoint")
 	defer span.End()
 
 	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
@@ -58,9 +88,9 @@ func (c *GetCreditsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	credits, err := c.Config().BillingManager.MetronomeClient.GetCustomerCredits(ctx, proj.UsageID)
+	credits, err := c.Config().BillingManager.MetronomeClient.ListCustomerCredits(ctx, proj.UsageID)
 	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error getting credits")
+		err := telemetry.Error(ctx, span, err, "error listing credits")
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
@@ -74,6 +104,22 @@ func (c *GetCreditsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	c.WriteResult(w, r, credits)
 }
 
+// GetUsageDashboardHandler returns an embeddable dashboard to display information related to customer usage.
+type GetUsageDashboardHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewGetUsageDashboardHandler returns a new GetUsageDashboardHandler
+func NewGetUsageDashboardHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetUsageDashboardHandler {
+	return &GetUsageDashboardHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
 func (c *GetUsageDashboardHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ctx, span := telemetry.NewSpan(r.Context(), "get-usage-dashboard-endpoint")
 	defer span.End()
@@ -97,7 +143,7 @@ func (c *GetUsageDashboardHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	credits, err := c.Config().BillingManager.MetronomeClient.GetCustomerDashboard(ctx, proj.UsageID, request.DashboardType)
+	credits, err := c.Config().BillingManager.MetronomeClient.GetCustomerDashboard(ctx, proj.UsageID, request.DashboardType, request.Options, request.ColorOverrides)
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "error getting customer dashboard")
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 31 - 4
api/server/router/project.go

@@ -341,8 +341,35 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/billing/plan -> project.NewListPlansHandler
+	listPlanEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/billing/plan",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listPlanHandler := billing.NewListPlansHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listPlanEndpoint,
+		Handler:  listPlanHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/billing/credits -> project.NewGetCreditsHandler
-	getCreditsEndpoint := factory.NewAPIEndpoint(
+	listCreditsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
@@ -357,14 +384,14 @@ func getProjectRoutes(
 		},
 	)
 
-	getCreditsHandler := billing.NewGetCreditsHandler(
+	listCreditsHandler := billing.NewListCreditsHandler(
 		config,
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &router.Route{
-		Endpoint: getCreditsEndpoint,
-		Handler:  getCreditsHandler,
+		Endpoint: listCreditsEndpoint,
+		Handler:  listCreditsHandler,
 		Router:   r,
 	})
 

+ 32 - 20
api/types/billing_metronome.go

@@ -71,16 +71,21 @@ type EmbeddableDashboardRequest struct {
 	ColorOverrides []ColorOverrides `json:"color_overrides,omitempty"`
 }
 
-// DashboardOptions are optional dashboard specific options
-type DashboardOptions struct {
-	Key   string
-	Value string
+// Plan is a pricing plan to which a user is currently subscribed
+type Plan struct {
+	ID                  uuid.UUID `json:"id"`
+	PlanID              uuid.UUID `json:"plan_id"`
+	PlanName            string    `json:"plan_name"`
+	PlanDescription     string    `json:"plan_description"`
+	StartingOn          string    `json:"starting_on"`
+	EndingBefore        string    `json:"ending_before"`
+	NetPaymentTermsDays int       `json:"net_payment_terms_days"`
+	TrialInfo           Trial     `json:"trial_info,omitempty"`
 }
 
-// ColorOverrides is an optional list of colors to override
-type ColorOverrides struct {
-	Name  string
-	Value string
+// Trial contains the information for a trial period
+type Trial struct {
+	EndingBefore string `json:"ending_before"`
 }
 
 // CreditType is the type of the credit used in the credit grant
@@ -89,19 +94,13 @@ type CreditType struct {
 	ID   string `json:"id"`
 }
 
-// GrantAmount represents the amount of credits granted
-type GrantAmount struct {
-	Amount     int64      `json:"amount"`
-	CreditType CreditType `json:"credit_type"`
-}
-
 // Balance represents the effective balance of the grant as of the end of the customer's
 // current billing period.
 type Balance struct {
 	// ExcludingPending is the grant's current balance excluding pending deductions
-	ExcludingPending int64 `json:"excluding_pending"`
+	ExcludingPending float64 `json:"excluding_pending"`
 	// IncludingPending is the grant's current balance including pending deductions
-	IncludingPending int64 `json:"including_pending"`
+	IncludingPending float64 `json:"including_pending"`
 	// EffectiveAt is a RFC3339 timestamp that can be used to filter credit grants by effective date
 	EffectiveAt string `json:"effective_at"`
 }
@@ -109,8 +108,21 @@ type Balance struct {
 // CreditGrant is a grant given to a specific user on a specific plan
 type CreditGrant struct {
 	ID          uuid.UUID `json:"id"`
-	Name        string
-	CustomerID  uuid.UUID
-	GrantAmount GrantAmount
-	Balance     Balance
+	Name        string    `json:"name"`
+	Balance     Balance   `json:"balance"`
+	Reason      string    `json:"reason"`
+	EffectiveAt string    `json:"effective_at"`
+	ExpiresAt   string    `json:"expires_at"`
+}
+
+// DashboardOptions are optional dashboard specific options
+type DashboardOptions struct {
+	Key   string `json:"key"`
+	Value string `json:"value"`
+}
+
+// ColorOverrides is an optional list of colors to override
+type ColorOverrides struct {
+	Name  string `json:"name"`
+	Value string `json:"value"`
 }

+ 30 - 1
dashboard/src/lib/billing/types.tsx

@@ -1,7 +1,6 @@
 import { z } from "zod";
 
 export type PaymentMethodList = PaymentMethod[];
-
 export type PaymentMethod = z.infer<typeof PaymentMethodValidator>;
 
 export const PaymentMethodValidator = z.object({
@@ -13,4 +12,34 @@ export const PaymentMethodValidator = z.object({
   is_default: z.boolean(),
 });
 
+type Trial = z.infer<typeof Trial>;
+
+const Trial = z.object({
+  ending_before: z.string(),
+});
+
+export type Plan = z.infer<typeof Plan>;
+export const Plan = z.object({
+  id: z.string(),
+  plan_name: z.string(),
+  plan_description: z.string(),
+  starting_on: z.string(),
+  trial_info: Trial,
+});
+
+export type CreditGrantList = CreditGrant[];
+export type CreditGrant = z.infer<typeof CreditGrantValidator>;
+export const CreditGrantValidator = z.object({
+  id: z.string(),
+  name: z.string(),
+  balance: z.object({
+    excluding_pending: z.number(),
+    including_pending: z.number(),
+    effective_at: z.string(),
+  }),
+  reason: z.string(),
+  effective_at: z.string(),
+  expires_at: z.string(),
+});
+
 export const ClientSecretResponse = z.string();

+ 65 - 3
dashboard/src/lib/hooks/useStripe.tsx

@@ -4,8 +4,10 @@ import { z } from "zod";
 
 import {
   ClientSecretResponse,
+  CreditGrantList,
   PaymentMethodList,
   PaymentMethodValidator,
+  Plan,
 } from "lib/billing/types";
 
 import api from "shared/api";
@@ -40,7 +42,30 @@ type TGetUsageDashboard = {
 };
 
 type TGetCredits = {
-  credits: number;
+  creditGrantsList: CreditGrantList;
+};
+
+type TGetPlan = {
+  plan: Plan;
+};
+
+const embeddableDashboardColors = {
+  standardText: "Gray_dark",
+  greyMedium: "Gray_medium",
+  borders: "Gray_light",
+  hover: "Gray_extralight",
+  background: "White",
+  primaryMedium: "Primary_medium",
+  primaryLight: "Primary_light",
+  usageLine0: "Usageline_0",
+  usageLine1: "Usageline_1",
+  usageLine2: "Usageline_2",
+  usageLine3: "Usageline_3",
+  usageLine4: "Usageline_4",
+  usageLine5: "Usageline_5",
+  usageLine6: "Usageline_6",
+  usageLine7: "Usageline_7",
+  usageLine8: "Usageline_8",
 };
 
 export const usePaymentMethods = (): TUsePaymentMethod => {
@@ -161,6 +186,16 @@ export const checkIfProjectHasPayment = (): TCheckHasPaymentEnabled => {
 export const useCustomerDashboard = (dashboard: string): TGetUsageDashboard => {
   const { currentProject } = useContext(Context);
 
+  const colorOverrides = [
+    { name: embeddableDashboardColors.background, value: "#121212" },
+    { name: embeddableDashboardColors.borders, value: "#121212" },
+    { name: embeddableDashboardColors.hover, value: "#DFDFE1" },
+    { name: embeddableDashboardColors.greyMedium, value: "#121212" },
+    { name: embeddableDashboardColors.primaryLight, value: "#121212" },
+    { name: embeddableDashboardColors.primaryMedium, value: "#DFDFE1" },
+    { name: embeddableDashboardColors.standardText, value: "#DFDFE1" },
+  ];
+
   // Return an embeddable dashboard for the customer
   const dashboardReq = useQuery(
     ["getUsageDashboard", currentProject?.id, dashboard],
@@ -172,6 +207,7 @@ export const useCustomerDashboard = (dashboard: string): TGetUsageDashboard => {
         "<token>",
         {
           dashboard,
+          color_overrides: colorOverrides,
         },
         {
           project_id: currentProject?.id,
@@ -191,7 +227,7 @@ export const useCustomerDashboard = (dashboard: string): TGetUsageDashboard => {
 };
 
 export const usePublishableKey = (): TGetPublishableKey => {
-  const { user, currentProject } = useContext(Context);
+  const { currentProject } = useContext(Context);
 
   // Fetch list of payment methods
   const keyReq = useQuery(
@@ -238,7 +274,33 @@ export const usePorterCredits = (): TGetCredits => {
   );
 
   return {
-    credits: creditsReq.data,
+    creditGrantsList: creditsReq.data,
+  };
+};
+
+export const useCustomerPlan = (): TGetPlan => {
+  const { currentProject } = useContext(Context);
+
+  // Fetch current plan
+  const planReq = useQuery(
+    ["getCustomerPlan", currentProject?.id],
+    async () => {
+      if (!currentProject?.id || currentProject.id === -1) {
+        return;
+      }
+      const res = await api.getCustomerPlan(
+        "<token>",
+        {},
+        {
+          project_id: currentProject?.id,
+        }
+      );
+      return res.data;
+    }
+  );
+
+  return {
+    plan: planReq.data,
   };
 };
 

+ 84 - 18
dashboard/src/main/home/project-settings/BillingPage.tsx

@@ -12,6 +12,7 @@ import Text from "components/porter/Text";
 import {
   checkIfProjectHasPayment,
   useCustomerDashboard,
+  useCustomerPlan,
   usePaymentMethods,
   usePorterCredits,
   useSetDefaultPaymentMethod,
@@ -20,6 +21,7 @@ import {
 import { Context } from "shared/Context";
 import cardIcon from "assets/credit-card.svg";
 import gift from "assets/gift.svg";
+import time from "assets/time.png";
 import trashIcon from "assets/trash.png";
 
 import BillingModal from "../modals/BillingModal";
@@ -29,7 +31,8 @@ function BillingPage(): JSX.Element {
   const [shouldCreate, setShouldCreate] = useState(false);
   const { currentProject } = useContext(Context);
 
-  const { credits } = usePorterCredits();
+  const { creditGrantsList } = usePorterCredits();
+  const { plan } = useCustomerPlan();
 
   const {
     paymentMethodList,
@@ -40,15 +43,42 @@ function BillingPage(): JSX.Element {
   const { setDefaultPaymentMethod } = useSetDefaultPaymentMethod();
 
   const { refetchPaymentEnabled } = checkIfProjectHasPayment();
-
-  const { url: creditsDashboard } = useCustomerDashboard("credits");
-  const { url: invoicesDashboard } = useCustomerDashboard("invoices");
   const { url: usageDashboard } = useCustomerDashboard("usage");
 
   const formatCredits = (credits: number): string => {
     return (credits / 100).toFixed(2);
   };
 
+  const monthDiff = (d1: Date, d2: Date) => {
+    var months;
+    months = (d2.getFullYear() - d1.getFullYear()) * 12;
+    months -= d1.getMonth();
+    months += d2.getMonth();
+    return months <= 0 ? 0 : months;
+  };
+
+  const daysDiff = (d1: Date, d2: Date) => {
+    const _MS_PER_DAY = 1000 * 60 * 60 * 24;
+    const utc1 = Date.UTC(d1.getFullYear(), d1.getMonth(), d1.getDate());
+    const utc2 = Date.UTC(d2.getFullYear(), d2.getMonth(), d2.getDate());
+
+    return Math.floor((utc2 - utc1) / _MS_PER_DAY);
+  };
+
+  const relativeTime = (timestampUTC: string): string => {
+    const tsDate = new Date(timestampUTC);
+    const now = new Date();
+
+    const remainingMonths = monthDiff(now, tsDate);
+    const remainingDays = daysDiff(now, tsDate);
+
+    const relativeFormat = remainingMonths > 0 ? "months" : "days";
+    const relativeValue = remainingMonths > 0 ? remainingMonths : remainingDays;
+
+    const rt = new Intl.RelativeTimeFormat("en", { style: "short" });
+    return rt.format(relativeValue, relativeFormat);
+  };
+
   const onCreate = async () => {
     await refetchPaymentMethods();
     setShouldCreate(false);
@@ -70,26 +100,62 @@ function BillingPage(): JSX.Element {
     <>
       {currentProject?.metronome_enabled ? (
         <div>
-          <Text size={16}>Porter credit balance</Text>
+          <Text size={16}>Porter credit grants</Text>
           <Spacer y={1} />
           <Text color="helper">
             View the amount of Porter credits you have available to spend on
             resources within this project.
           </Text>
           <Spacer y={1} />
-          <Container row>
-            <Image src={gift} style={{ marginTop: "-2px" }} />
-            <Spacer inline x={1} />
-            <Text size={20}>
-              {credits > 0 ? `$${formatCredits(credits)}` : "$ 0.00"}
-            </Text>
-          </Container>
-          <Spacer y={2} />
-          <iframe src={creditsDashboard} />
-          <Spacer y={2} />
-          <iframe src={invoicesDashboard} />
-          <Spacer y={2} />
-          <iframe src={usageDashboard} />
+
+          <div>
+            {creditGrantsList === undefined ? (
+              <Loading></Loading>
+            ) : creditGrantsList.length === 0 ? (
+              <div>No credit grants available.</div>
+            ) : (
+              creditGrantsList.map((grant) => (
+                <Container>
+                  <Image src={gift} style={{ marginTop: "-2px" }} />
+                  <Spacer inline x={1} />
+                  <Text size={20}>
+                    {grant.balance.including_pending > 0
+                      ? `$${formatCredits(grant.balance.including_pending)}`
+                      : "$ 0.00"}
+                  </Text>
+                </Container>
+              ))
+            )}
+            <Spacer y={1} />
+          </div>
+
+          <Text size={16}>Plan Details</Text>
+          <Spacer y={1} />
+          <Text color="helper">
+            View the details of the current plan you are subscribed to.
+          </Text>
+          <Spacer y={1} />
+
+          <div>
+            {plan === undefined ? (
+              <Loading></Loading>
+            ) : (
+              <Fieldset>
+                <Container row>
+                  <Text size={14}>{plan.plan_name}</Text>
+                </Container>
+                {plan.trial_info !== undefined ? (
+                  <div>
+                    <Text size={13}>
+                      Trial Ending {relativeTime(plan.trial_info.ending_before)}
+                    </Text>
+                  </div>
+                ) : (
+                  <div></div>
+                )}
+              </Fieldset>
+            )}
+          </div>
           <Spacer y={2} />
         </div>
       ) : (

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

@@ -3452,6 +3452,13 @@ const getPublishableKey = baseApi<
   ({ project_id }) => `/api/projects/${project_id}/billing/publishable_key`
 );
 
+const getCustomerPlan = baseApi<
+  {},
+  {
+    project_id?: number;
+  }
+>("GET", ({ project_id }) => `/api/projects/${project_id}/billing/plan`);
+
 const getPorterCredits = baseApi<
   {},
   {
@@ -3462,6 +3469,8 @@ const getPorterCredits = baseApi<
 const getUsageDashboard = baseApi<
   {
     dashboard: string;
+    dashboard_options?: { key: string; value: string }[];
+    color_overrides?: { name: string; value: string }[];
   },
   {
     project_id?: number;
@@ -3866,6 +3875,7 @@ export default {
   // BILLING
   getPublishableKey,
   getPorterCredits,
+  getCustomerPlan,
   getUsageDashboard,
   listPaymentMethod,
   addPaymentMethod,

+ 51 - 26
internal/billing/metronome.go

@@ -35,6 +35,26 @@ func NewMetronomeClient(metronomeApiKey string) MetronomeClient {
 	}
 }
 
+// CreateCustomerWithPlan will create the customer in Metronome and immediately add it to the plan
+func (m MetronomeClient) CreateCustomerWithPlan(ctx context.Context, userEmail string, projectName string, projectID uint, billingID string, planID string) (customerID uuid.UUID, customerPlanID uuid.UUID, err error) {
+	ctx, span := telemetry.NewSpan(ctx, "add-metronome-customer-plan")
+	defer span.End()
+
+	porterCloudPlanID, err := uuid.Parse(planID)
+	if err != nil {
+		return customerID, customerPlanID, telemetry.Error(ctx, span, err, "error parsing starter plan id")
+	}
+
+	customerID, err = m.createCustomer(ctx, userEmail, projectName, projectID, billingID)
+	if err != nil {
+		return customerID, customerPlanID, telemetry.Error(ctx, span, err, "error while creatinc customer with plan")
+	}
+
+	customerPlanID, err = m.addCustomerPlan(ctx, customerID, porterCloudPlanID)
+
+	return customerID, customerPlanID, err
+}
+
 // createCustomer will create the customer in Metronome
 func (m MetronomeClient) createCustomer(ctx context.Context, userEmail string, projectName string, projectID uint, billingID string) (customerID uuid.UUID, err error) {
 	ctx, span := telemetry.NewSpan(ctx, "create-metronome-customer")
@@ -63,7 +83,7 @@ func (m MetronomeClient) createCustomer(ctx context.Context, userEmail string, p
 		Data types.Customer `json:"data"`
 	}
 
-	err = post(path, m.ApiKey, customer, &result)
+	err = do(http.MethodPost, path, m.ApiKey, customer, &result)
 	if err != nil {
 		return customerID, telemetry.Error(ctx, span, err, "error creating customer")
 	}
@@ -97,7 +117,7 @@ func (m MetronomeClient) addCustomerPlan(ctx context.Context, customerID uuid.UU
 		} `json:"data"`
 	}
 
-	err = post(path, m.ApiKey, req, &result)
+	err = do(http.MethodPost, path, m.ApiKey, req, &result)
 	if err != nil {
 		return customerPlanID, telemetry.Error(ctx, span, err, "failed to add customer to plan")
 	}
@@ -105,24 +125,27 @@ func (m MetronomeClient) addCustomerPlan(ctx context.Context, customerID uuid.UU
 	return result.Data.CustomerPlanID, nil
 }
 
-// CreateCustomerWithPlan will create the customer in Metronome and immediately add it to the plan
-func (m MetronomeClient) CreateCustomerWithPlan(ctx context.Context, userEmail string, projectName string, projectID uint, billingID string, planID string) (customerID uuid.UUID, customerPlanID uuid.UUID, err error) {
-	ctx, span := telemetry.NewSpan(ctx, "add-metronome-customer-plan")
+// ListCustomerPlan will return the current active plan to which the user is subscribed
+func (m MetronomeClient) ListCustomerPlan(ctx context.Context, customerID uuid.UUID) (plan types.Plan, err error) {
+	ctx, span := telemetry.NewSpan(ctx, "list-customer-plans")
 	defer span.End()
 
-	porterCloudPlanID, err := uuid.Parse(planID)
-	if err != nil {
-		return customerID, customerPlanID, telemetry.Error(ctx, span, err, "error parsing starter plan id")
+	if customerID == uuid.Nil {
+		return plan, telemetry.Error(ctx, span, err, "customer id empty")
 	}
 
-	customerID, err = m.createCustomer(ctx, userEmail, projectName, projectID, billingID)
-	if err != nil {
-		return customerID, customerPlanID, telemetry.Error(ctx, span, err, "error while creatinc customer with plan")
+	path := fmt.Sprintf("/customers/%s/plans", customerID)
+
+	var result struct {
+		Data []types.Plan `json:"data"`
 	}
 
-	customerPlanID, err = m.addCustomerPlan(ctx, customerID, porterCloudPlanID)
+	err = do(http.MethodGet, path, m.ApiKey, nil, &result)
+	if err != nil {
+		return plan, telemetry.Error(ctx, span, err, "failed to list customer plans")
+	}
 
-	return customerID, customerPlanID, err
+	return result.Data[0], nil
 }
 
 // EndCustomerPlan will immediately end the plan for the given customer
@@ -145,7 +168,7 @@ func (m MetronomeClient) EndCustomerPlan(ctx context.Context, customerID uuid.UU
 		EndingBeforeUTC: endBefore,
 	}
 
-	err = post(path, m.ApiKey, req, nil)
+	err = do(http.MethodPost, path, m.ApiKey, req, nil)
 	if err != nil {
 		return telemetry.Error(ctx, span, err, "failed to end customer plan")
 	}
@@ -153,9 +176,9 @@ func (m MetronomeClient) EndCustomerPlan(ctx context.Context, customerID uuid.UU
 	return nil
 }
 
-// GetCustomerCredits will return the first credit grant for the customer
-func (m MetronomeClient) GetCustomerCredits(ctx context.Context, customerID uuid.UUID) (credits int64, err error) {
-	ctx, span := telemetry.NewSpan(ctx, "get-customer-credits")
+// ListCustomerCredits will return the list of credit grants for the customer
+func (m MetronomeClient) ListCustomerCredits(ctx context.Context, customerID uuid.UUID) (credits []types.CreditGrant, err error) {
+	ctx, span := telemetry.NewSpan(ctx, "list-customer-credits")
 	defer span.End()
 
 	if customerID == uuid.Nil {
@@ -174,15 +197,15 @@ func (m MetronomeClient) GetCustomerCredits(ctx context.Context, customerID uuid
 		Data []types.CreditGrant `json:"data"`
 	}
 
-	err = post(path, m.ApiKey, req, &result)
+	err = do(http.MethodPost, path, m.ApiKey, req, &result)
 	if err != nil {
-		return credits, telemetry.Error(ctx, span, err, "failed to get customer credits")
+		return credits, telemetry.Error(ctx, span, err, "failed to list customer credits")
 	}
 
-	return result.Data[0].Balance.IncludingPending, nil
+	return result.Data, nil
 }
 
-func (m MetronomeClient) GetCustomerDashboard(ctx context.Context, customerID uuid.UUID, dashboardType string) (url string, err error) {
+func (m MetronomeClient) GetCustomerDashboard(ctx context.Context, customerID uuid.UUID, dashboardType string, options []types.DashboardOptions, colorOverrides []types.ColorOverrides) (url string, err error) {
 	ctx, span := telemetry.NewSpan(ctx, "get-customer-usage-dashboard")
 	defer span.End()
 
@@ -193,15 +216,17 @@ func (m MetronomeClient) GetCustomerDashboard(ctx context.Context, customerID uu
 	path := "dashboards/getEmbeddableUrl"
 
 	req := types.EmbeddableDashboardRequest{
-		CustomerID:    customerID,
-		DashboardType: dashboardType,
+		CustomerID:     customerID,
+		Options:        options,
+		DashboardType:  dashboardType,
+		ColorOverrides: colorOverrides,
 	}
 
 	var result struct {
 		Data map[string]string `json:"data"`
 	}
 
-	err = post(path, m.ApiKey, req, &result)
+	err = do(http.MethodPost, path, m.ApiKey, req, &result)
 	if err != nil {
 		return url, telemetry.Error(ctx, span, err, "failed to get embeddable dashboard")
 	}
@@ -209,7 +234,7 @@ func (m MetronomeClient) GetCustomerDashboard(ctx context.Context, customerID uu
 	return result.Data["url"], nil
 }
 
-func post(path string, apiKey string, body interface{}, data interface{}) (err error) {
+func do(method string, path string, apiKey string, body interface{}, data interface{}) (err error) {
 	client := http.Client{}
 	endpoint, err := url.JoinPath(metronomeBaseUrl, path)
 	if err != nil {
@@ -224,7 +249,7 @@ func post(path string, apiKey string, body interface{}, data interface{}) (err e
 		}
 	}
 
-	req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(bodyJson))
+	req, err := http.NewRequest(method, endpoint, bytes.NewBuffer(bodyJson))
 	if err != nil {
 		return err
 	}