Quellcode durchsuchen

Add invoice list endpoint

Mauricio Araujo vor 2 Jahren
Ursprung
Commit
7d972768b5

+ 12 - 3
api/server/handlers/billing/invoices.go

@@ -15,16 +15,17 @@ import (
 
 // ListCustomerInvoicesHandler is a handler for listing payment methods
 type ListCustomerInvoicesHandler struct {
-	handlers.PorterHandlerWriter
+	handlers.PorterHandlerReadWriter
 }
 
 // NewListBillingHandler will create a new ListBillingHandler
 func NewListCustomerInvoicesHandler(
 	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *ListCustomerInvoicesHandler {
 	return &ListCustomerInvoicesHandler{
-		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
 	}
 }
 
@@ -34,7 +35,15 @@ func (c *ListCustomerInvoicesHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 
 	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
-	invoices, err := c.Config().BillingManager.MetronomeClient.ListInvoices(ctx, proj.UsageID)
+	req := &types.ListCustomerInvoicesRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, req); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding list customer usage request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	invoices, err := c.Config().BillingManager.MetronomeClient.ListCustomerInvoices(ctx, proj.UsageID, req.Status, req.StartingOn, req.EndingBefore)
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "error listing payment method")
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error listing payment method: %w", err)))

+ 28 - 0
api/server/router/project.go

@@ -452,6 +452,34 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/billing/invoices -> project.NewListCustomerInvoicesHandler
+	listCustomerInvoicesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/billing/invoices",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listCustomerInvoicesHandler := billing.NewListCustomerInvoicesHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listCustomerInvoicesEndpoint,
+		Handler:  listCustomerInvoicesHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/billing/ingest -> project.NewGetUsageDashboardHandler
 	ingestEventsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 7 - 0
api/types/billing_metronome.go

@@ -197,6 +197,13 @@ type BillingEvent struct {
 	Timestamp     string                 `json:"timestamp"`
 }
 
+type ListCustomerInvoicesRequest struct {
+	CustomerID   uuid.UUID `json:"customer_id"`
+	Status       string    `json:"status,omitempty"`
+	StartingOn   string    `json:"starting_on,omitempty"`
+	EndingBefore string    `json:"ending_before,omitempty"`
+}
+
 // Invoice represents a Metronome invoice.
 type Invoice struct {
 	ID             uuid.UUID  `json:"id"`

+ 13 - 0
dashboard/src/lib/billing/types.tsx

@@ -51,6 +51,19 @@ export const CreditGrantsValidator = z.object({
   remaining_credits: z.number(),
 });
 
+export type InvoiceList = Invoice[];
+export type Invoice = z.infer<typeof InvoiceValidator>;
+export const InvoiceValidator = z.object({
+  id: z.string(),
+  customer_id: z.string(),
+  credit_type: z.string(),
+  start_timestamp: z.string(),
+  end_timestamp: z.string(),
+  status: z.string(),
+  total: z.number(),
+  type: z.string(),
+});
+
 export const ClientSecretResponse = z.string();
 
 export type ReferralDetails = z.infer<typeof ReferralDetailsValidator>;

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

@@ -15,6 +15,8 @@ import {
   type UsageList,
   ReferralDetailsValidator,
   ReferralDetails
+  InvoiceList,
+  InvoiceValidator,
 } from "lib/billing/types";
 
 import api from "shared/api";
@@ -58,6 +60,10 @@ type TGetPlan = {
   plan: Plan | null;
 };
 
+type TGetInvoices = {
+  invoiceList: InvoiceList | null;
+};
+
 type TGetUsage = {
   usage: UsageList | null;
 };
@@ -407,3 +413,45 @@ export const useReferralDetails = (): TGetReferralDetails => {
     referralDetails: referralsReq.data ?? null,
   };
 };
+
+
+export const useCustomerInvoices = (): TGetInvoices => {
+  const { currentProject } = useContext(Context);
+
+  // Fetch current plan
+  const invoicesReq = useQuery(
+    ["getCustomerInvoices", currentProject?.id],
+    async (): Promise<InvoiceList | null> => {
+      if (!currentProject?.metronome_enabled) {
+        return null;
+      }
+
+      if (!currentProject?.id) {
+        return null;
+      }
+
+      try {
+        const now = new Date();
+        const startingDate = `${now.getFullYear()}-${now.getMonth()}-01`;
+        const endingDate = now.toISOString();
+        const res = await api.getCustomerInvoices(
+          "<token>",
+          {
+            status: "COMPLETED",
+            starting_on: startingDate,
+            ending_before: endingDate,
+          },
+          { project_id: currentProject.id }
+        );
+
+        const invoices = InvoiceValidator.array().parse(res.data);
+        return invoices;
+      } catch (error) {
+        return null
+      }
+    });
+
+  return {
+    invoiceList: invoicesReq.data ?? null,
+  };
+};

+ 2 - 0
dashboard/src/main/home/project-settings/BillingPage.tsx

@@ -17,6 +17,7 @@ import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import {
   checkIfProjectHasPayment,
+  useCustomerInvoices,
   useCustomerPlan,
   useCustomerUsage,
   usePaymentMethods,
@@ -44,6 +45,7 @@ function BillingPage(): JSX.Element {
 
   const { creditGrants } = usePorterCredits();
   const { plan } = useCustomerPlan();
+  const { invoiceList } = useCustomerInvoices();
 
   const {
     paymentMethodList,

+ 63 - 42
dashboard/src/shared/api.tsx

@@ -386,8 +386,9 @@ const getFeedEvents = baseApi<
   }
 >("GET", (pathParams) => {
   const { project_id, cluster_id, stack_name, page } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${page || 1
-    }`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${
+    page || 1
+  }`;
 });
 
 const createEnvironment = baseApi<
@@ -875,9 +876,11 @@ const detectBuildpack = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
 });
 
 const detectGitlabBuildpack = baseApi<
@@ -908,9 +911,11 @@ const getBranchContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/contents`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/contents`;
 });
 
 const getProcfileContents = baseApi<
@@ -926,9 +931,11 @@ const getProcfileContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/procfile`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/procfile`;
 });
 
 const getPorterYamlContents = baseApi<
@@ -944,9 +951,11 @@ const getPorterYamlContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/porteryaml`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/porteryaml`;
 });
 
 const parsePorterYaml = baseApi<
@@ -1006,30 +1015,32 @@ const getBranchHead = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/head`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/head`;
 });
 
 const createApp = baseApi<
   | {
-    name: string;
-    deployment_target_id: string;
-    type: "github";
-    git_repo_id: number;
-    git_branch: string;
-    git_repo_name: string;
-    porter_yaml_path: string;
-  }
+      name: string;
+      deployment_target_id: string;
+      type: "github";
+      git_repo_id: number;
+      git_branch: string;
+      git_repo_name: string;
+      porter_yaml_path: string;
+    }
   | {
-    name: string;
-    deployment_target_id: string;
-    type: "docker-registry";
-    image: {
-      repository: string;
-      tag: string;
-    };
-  },
+      name: string;
+      deployment_target_id: string;
+      type: "docker-registry";
+      image: {
+        repository: string;
+        tag: string;
+      };
+    },
   {
     project_id: number;
     cluster_id: number;
@@ -2298,9 +2309,11 @@ const getEnvGroup = baseApi<
     version?: number;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id
-    }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${pathParams.version ? "&version=" + pathParams.version : ""
-    }`;
+  return `/api/projects/${pathParams.id}/clusters/${
+    pathParams.cluster_id
+  }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${
+    pathParams.version ? "&version=" + pathParams.version : ""
+  }`;
 });
 
 const getConfigMap = baseApi<
@@ -3514,6 +3527,17 @@ const getCustomerUsage = baseApi<
   }
 >("POST", ({ project_id }) => `/api/projects/${project_id}/billing/usage`);
 
+const getCustomerInvoices = baseApi<
+  {
+    status: string;
+    starting_on: string;
+    ending_before: string;
+  },
+  {
+    project_id?: number;
+  }
+>("POST", ({ project_id }) => `/api/projects/${project_id}/billing/invoices`);
+
 const getCustomerPlan = baseApi<
   {},
   {
@@ -3582,13 +3606,9 @@ const getReferralDetails = baseApi<
   {
     project_id?: number;
   }
->(
-  "GET",
-  ({ project_id }) =>
-    `/api/projects/${project_id}/referrals/details`
-);
+>("GET", ({ project_id }) => `/api/projects/${project_id}/referrals/details`);
 
-const getGithubStatus = baseApi<{}, {}>("GET", ({ }) => `/api/status/github`);
+const getGithubStatus = baseApi<{}, {}>("GET", ({}) => `/api/status/github`);
 
 const createSecretAndOpenGitHubPullRequest = baseApi<
   {
@@ -3977,6 +3997,7 @@ export default {
   getPorterCredits,
   getCustomerPlan,
   getCustomerUsage,
+  getCustomerInvoices,
   listPaymentMethod,
   addPaymentMethod,
   setDefaultPaymentMethod,