Sfoglia il codice sorgente

Address review comments

Mauricio Araujo 2 anni fa
parent
commit
fd63c37fbe

+ 12 - 15
api/server/handlers/billing/plan.go

@@ -28,7 +28,7 @@ func NewListPlansHandler(
 }
 
 func (c *ListPlansHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	ctx, span := telemetry.NewSpan(r.Context(), "list-plans-endpoint")
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-list-plans")
 	defer span.End()
 
 	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
@@ -42,6 +42,11 @@ func (c *ListPlansHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "metronome-enabled", Value: true},
+		telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
+	)
+
 	plan, err := c.Config().BillingManager.MetronomeClient.ListCustomerPlan(ctx, proj.UsageID)
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "error listing plans")
@@ -49,12 +54,6 @@ func (c *ListPlansHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		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)
 }
 
@@ -74,7 +73,7 @@ func NewListCreditsHandler(
 }
 
 func (c *ListCreditsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	ctx, span := telemetry.NewSpan(r.Context(), "list-credits-endpoint")
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-list-credits")
 	defer span.End()
 
 	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
@@ -97,7 +96,6 @@ func (c *ListCreditsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	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},
 	)
 
@@ -135,6 +133,11 @@ func (c *GetUsageDashboardHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "metronome-enabled", Value: true},
+		telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
+	)
+
 	request := &types.EmbeddableDashboardRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
@@ -150,11 +153,5 @@ func (c *GetUsageDashboardHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		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, credits)
 }

+ 8 - 8
api/types/billing_metronome.go

@@ -33,8 +33,8 @@ type AddCustomerPlanRequest struct {
 	StartingOnUTC string `json:"starting_on"`
 	// EndingBeforeUTC is a RFC 3339 timestamp for when the plan ends (exclusive) for this customer. Must be at 0:00 UTC (midnight)
 	EndingBeforeUTC string `json:"ending_before,omitempty"`
-	// NetPaymentTermsDays is the number of days after issuance of invoice after which the invoice is due
-	NetPaymentTermsDays int `json:"net_payment_terms_days,omitempty"`
+	// NetPaymentTermDays is the number of days after issuance of invoice after which the invoice is due
+	NetPaymentTermDays int `json:"net_payment_terms_days,omitempty"`
 }
 
 // EndCustomerPlanRequest represents a request to end the plan for a specific customer.
@@ -73,9 +73,9 @@ type EmbeddableDashboardRequest struct {
 	// DashboardType is the type of dashboard to retrieve
 	DashboardType string `json:"dashboard"`
 	// Options are optional dashboard specific options
-	Options []DashboardOptions `json:"dashboard_options,omitempty"`
+	Options []DashboardOption `json:"dashboard_options,omitempty"`
 	//  ColorOverrides is an optional list of colors to override
-	ColorOverrides []ColorOverrides `json:"color_overrides,omitempty"`
+	ColorOverrides []ColorOverride `json:"color_overrides,omitempty"`
 }
 
 // Plan is a pricing plan to which a user is currently subscribed
@@ -129,14 +129,14 @@ type CreditGrant struct {
 	ExpiresAt   string      `json:"expires_at"`
 }
 
-// DashboardOptions are optional dashboard specific options
-type DashboardOptions struct {
+// DashboardOption are optional dashboard specific options
+type DashboardOption struct {
 	Key   string `json:"key"`
 	Value string `json:"value"`
 }
 
-// ColorOverrides is an optional list of colors to override
-type ColorOverrides struct {
+// ColorOverride is an optional list of colors to override
+type ColorOverride struct {
 	Name  string `json:"name"`
 	Value string `json:"value"`
 }

+ 1 - 0
dashboard/package.json

@@ -40,6 +40,7 @@
     "core-js": "^3.16.1",
     "cron-parser": "^4.3.0",
     "cron-validator": "^1.3.1",
+    "date-fns": "^3.6.0",
     "d3-array": "^2.11.0",
     "d3-time-format": "^3.0.0",
     "dayjs": "^1.11.5",

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

@@ -1,13 +1,15 @@
 import { useContext, useState } from "react";
-import { useQuery } from "@tanstack/react-query";
+import { useQuery, type UseQueryResult } from "@tanstack/react-query";
 import { z } from "zod";
 
 import {
   ClientSecretResponse,
-  CreditGrants,
-  PaymentMethodList,
+  CreditGrantsValidator,
   PaymentMethodValidator,
   Plan,
+  type CreditGrants,
+  type PaymentMethod,
+  type PaymentMethodList,
 } from "lib/billing/types";
 
 import api from "shared/api";
@@ -15,7 +17,10 @@ import { Context } from "shared/Context";
 
 type TUsePaymentMethod = {
   paymentMethodList: PaymentMethodList;
-  refetchPaymentMethods: any;
+  refetchPaymentMethods: (options: {
+    throwOnError: boolean;
+    cancelRefetch: boolean;
+  }) => Promise<UseQueryResult>;
   deletingIds: string[];
   deletePaymentMethod: (paymentMethodId: string) => Promise<void>;
 };
@@ -30,7 +35,10 @@ type TSetDefaultPaymentMethod = {
 
 type TCheckHasPaymentEnabled = {
   hasPaymentEnabled: boolean;
-  refetchPaymentEnabled: any;
+  refetchPaymentEnabled: (options: {
+    throwOnError: boolean;
+    cancelRefetch: boolean;
+  }) => Promise<UseQueryResult>;
 };
 
 type TGetPublishableKey = {
@@ -42,11 +50,11 @@ type TGetUsageDashboard = {
 };
 
 type TGetCredits = {
-  creditGrants: CreditGrants;
+  creditGrants: CreditGrants | undefined;
 };
 
 type TGetPlan = {
-  plan: Plan;
+  plan: Plan | undefined;
 };
 
 const embeddableDashboardColors = {
@@ -57,15 +65,6 @@ const embeddableDashboardColors = {
   white: "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 => {
@@ -81,9 +80,9 @@ export const usePaymentMethods = (): TUsePaymentMethod => {
   // Fetch list of payment methods
   const paymentMethodReq = useQuery(
     ["getPaymentMethods", currentProject?.id],
-    async () => {
+    async (): Promise<PaymentMethod[]> => {
       if (!currentProject?.id || currentProject.id === -1) {
-        return;
+        return [];
       }
       const listResponse = await api.listPaymentMethod(
         "<token>",
@@ -99,7 +98,9 @@ export const usePaymentMethods = (): TUsePaymentMethod => {
   );
 
   // Delete list of payment methods
-  const deletePaymentMethod = async (paymentMethodId: string) => {
+  const deletePaymentMethod = async (
+    paymentMethodId: string
+  ): Promise<void> => {
     if (!currentProject?.id) {
       throw new Error("Project ID is missing");
     }
@@ -138,7 +139,7 @@ export const usePaymentMethods = (): TUsePaymentMethod => {
 export const useCreatePaymentMethod = (): TCreatePaymentMethod => {
   const { currentProject } = useContext(Context);
 
-  const createPaymentMethod = async () => {
+  const createPaymentMethod = async (): Promise<string> => {
     const resp = await api.addPaymentMethod(
       "<token>",
       {},
@@ -165,7 +166,7 @@ export const checkIfProjectHasPayment = (): TCheckHasPaymentEnabled => {
   // Fetch list of payment methods
   const paymentEnabledReq = useQuery(
     ["checkPaymentEnabled", currentProject?.id],
-    async () => {
+    async (): Promise<boolean> => {
       const res = await api.getHasBilling(
         "<token>",
         {},
@@ -183,7 +184,9 @@ export const checkIfProjectHasPayment = (): TCheckHasPaymentEnabled => {
   };
 };
 
-export const useCustomerDashboard = (dashboard: string): TGetUsageDashboard => {
+export const useCustomeUsageDashboard = (
+  dashboard: string
+): TGetUsageDashboard => {
   const { currentProject } = useContext(Context);
 
   const colorOverrides = [
@@ -268,7 +271,7 @@ export const usePorterCredits = (): TGetCredits => {
           project_id: currentProject?.id,
         }
       );
-      return res.data;
+      return CreditGrantsValidator.parse(res.data);
     }
   );
 
@@ -294,19 +297,22 @@ export const useCustomerPlan = (): TGetPlan => {
           project_id: currentProject?.id,
         }
       );
-      return res.data;
+      const plan = Plan.parse(res.data);
+      return plan;
     }
   );
 
   return {
-    plan: planReq.data != "" ? planReq.data : null,
+    plan: planReq.data,
   };
 };
 
 export const useSetDefaultPaymentMethod = (): TSetDefaultPaymentMethod => {
   const { currentProject } = useContext(Context);
 
-  const setDefaultPaymentMethod = async (paymentMethodId: string) => {
+  const setDefaultPaymentMethod = async (
+    paymentMethodId: string
+  ): Promise<void> => {
     // Set payment method as default
     const res = await api.setDefaultPaymentMethod(
       "<token>",

+ 29 - 48
dashboard/src/main/home/project-settings/BillingPage.tsx

@@ -1,5 +1,6 @@
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext, useState } from "react";
 import ParentSize from "@visx/responsive/lib/components/ParentSize";
+import { intlFormatDistance } from "date-fns";
 import styled from "styled-components";
 
 import Loading from "components/Loading";
@@ -12,8 +13,8 @@ import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import {
   checkIfProjectHasPayment,
-  useCustomerDashboard,
   useCustomerPlan,
+  useCustomeUsageDashboard,
   usePaymentMethods,
   usePorterCredits,
   useSetDefaultPaymentMethod,
@@ -44,48 +45,24 @@ function BillingPage(): JSX.Element {
 
   const { refetchPaymentEnabled } = checkIfProjectHasPayment();
 
-  const { url: usageDashboard } = useCustomerDashboard("usage");
+  const { url: usageDashboard } = useCustomeUsageDashboard("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);
+    return intlFormatDistance(tsDate, now);
   };
 
-  const readableDate = (s: string) => new Date(s).toLocaleDateString();
+  const readableDate = (s: string): string => new Date(s).toLocaleDateString();
 
-  const onCreate = async () => {
-    await refetchPaymentMethods();
+  const onCreate = async (): Promise<void> => {
+    await refetchPaymentMethods({ throwOnError: false, cancelRefetch: false });
     setShouldCreate(false);
-    refetchPaymentEnabled();
+    await refetchPaymentEnabled({ throwOnError: false, cancelRefetch: false });
   };
 
   if (shouldCreate) {
@@ -131,25 +108,30 @@ function BillingPage(): JSX.Element {
                     <Container row={true}>
                       <DeleteButton
                         onClick={() => {
-                          setCurrentOverlay({
-                            message: `Are you sure you want to remove this payment method?`,
-                            onYes: () => {
-                              deletePaymentMethod(paymentMethod.id);
-                              setCurrentOverlay(null);
-                            },
-                            onNo: () => {
-                              setCurrentOverlay(null);
-                            },
-                          });
+                          if (setCurrentOverlay) {
+                            setCurrentOverlay({
+                              message: `Are you sure you want to remove this payment method?`,
+                              onYes: async () => {
+                                await deletePaymentMethod(paymentMethod.id);
+                                setCurrentOverlay(null);
+                              },
+                              onNo: () => {
+                                setCurrentOverlay(null);
+                              },
+                            });
+                          }
                         }}
                       >
                         <Icon src={trashIcon} height={"18px"} />
                       </DeleteButton>
                       <Spacer inline x={1} />
                       <Button
-                        onClick={() => {
-                          setDefaultPaymentMethod(paymentMethod.id);
-                          refetchPaymentMethods();
+                        onClick={async () => {
+                          await setDefaultPaymentMethod(paymentMethod.id);
+                          await refetchPaymentMethods({
+                            throwOnError: false,
+                            cancelRefetch: false,
+                          });
                         }}
                       >
                         Set as default
@@ -209,7 +191,7 @@ function BillingPage(): JSX.Element {
             </Text>
             <Spacer y={1} />
 
-            {plan != null && plan.plan_name != "" ? (
+            {plan !== undefined && plan.plan_name !== "" ? (
               <div>
                 <Text>Active Plan</Text>
                 <Spacer y={0.5} />
@@ -247,14 +229,13 @@ function BillingPage(): JSX.Element {
                         src={usageDashboard}
                         scrolling="no"
                         frameBorder={0}
-                        allowTransparency={true}
                       ></iframe>
                     )}
                   </ParentSize>
                 </Container>
               </div>
             ) : (
-              <Text>This project doesn't have an active billing plan.</Text>
+              <Text>This project does not have an active billing plan.</Text>
             )}
           </div>
         </div>

+ 1 - 1
internal/billing/metronome.go

@@ -230,7 +230,7 @@ func (m MetronomeClient) ListCustomerCredits(ctx context.Context, customerID uui
 }
 
 // GetCustomerDashboard will return an embeddable Metronome dashboard
-func (m MetronomeClient) GetCustomerDashboard(ctx context.Context, customerID uuid.UUID, dashboardType string, options []types.DashboardOptions, colorOverrides []types.ColorOverrides) (url string, err error) {
+func (m MetronomeClient) GetCustomerDashboard(ctx context.Context, customerID uuid.UUID, dashboardType string, options []types.DashboardOption, colorOverrides []types.ColorOverride) (url string, err error) {
 	ctx, span := telemetry.NewSpan(ctx, "get-customer-usage-dashboard")
 	defer span.End()