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

+ 4 - 4
api/server/handlers/billing/invoices.go

@@ -18,14 +18,14 @@ type ListCustomerInvoicesHandler struct {
 	handlers.PorterHandlerReadWriter
 }
 
-// NewListBillingHandler will create a new ListBillingHandler
+// NewListCustomerInvoicesHandler will create a new ListCustomerInvoicesHandler
 func NewListCustomerInvoicesHandler(
 	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *ListCustomerInvoicesHandler {
 	return &ListCustomerInvoicesHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
 	}
 }
 
@@ -45,8 +45,8 @@ func (c *ListCustomerInvoicesHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 
 	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)))
+		err := telemetry.Error(ctx, span, err, "error listing customer invoices")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error listing customer invoices: %w", err)))
 		return
 	}
 

+ 2 - 2
api/server/router/project.go

@@ -455,8 +455,8 @@ func getProjectRoutes(
 	// GET /api/projects/{project_id}/billing/invoices -> project.NewListCustomerInvoicesHandler
 	listCustomerInvoicesEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
-			Method: types.HTTPVerbPost,
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
 				RelativePath: relPath + "/billing/invoices",

+ 28 - 12
api/types/billing_metronome.go

@@ -197,21 +197,37 @@ type BillingEvent struct {
 	Timestamp     string                 `json:"timestamp"`
 }
 
+// ListCustomerInvoicesRequest is the request to list invoices for a customer
 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"`
+	Status       string `schema:"status,omitempty"`
+	StartingOn   string `schema:"starting_on,omitempty"`
+	EndingBefore string `schema:"ending_before,omitempty"`
 }
 
 // Invoice represents a Metronome invoice.
 type Invoice struct {
-	ID             uuid.UUID  `json:"id"`
-	CustomerID     uuid.UUID  `json:"customer_id"`
-	CreditType     CreditType `json:"credit_type"`
-	StartTimestamp string     `json:"start_timestamp"`
-	EndTimestamp   string     `json:"end_timestamp"`
-	Status         string     `json:"status"`
-	Total          float64    `json:"total"`
-	Type           string     `json:"type"`
+	ID              uuid.UUID       `json:"id"`
+	CustomerID      uuid.UUID       `json:"customer_id"`
+	CreditType      CreditType      `json:"credit_type"`
+	StartTimestamp  string          `json:"start_timestamp"`
+	EndTimestamp    string          `json:"end_timestamp"`
+	Status          string          `json:"status"`
+	Subtotal        float64         `json:"subtotal"`
+	Total           float64         `json:"total"`
+	Type            string          `json:"type"`
+	LineItems       []LineItem      `json:"line_items"`
+	ExternalInvoice ExternalInvoice `json:"external_invoice"`
+}
+
+type ExternalInvoice struct {
+	BillingProviderType string `json:"billing_provider_type"`
+	InvoiceID           string `json:"invoice_id"`
+	IssuedAt            string `json:"issued_at"`
+	ExternalStatus      string `json:"external_status"`
+}
+
+type LineItem struct {
+	Name       string     `json:"name"`
+	Total      float64    `json:"total"`
+	CreditType CreditType `json:"credit_type"`
 }

+ 4 - 2
dashboard/src/lib/hooks/useStripe.tsx

@@ -432,18 +432,20 @@ export const useCustomerInvoices = (): TGetInvoices => {
 
       try {
         const now = new Date();
-        const startingDate = `${now.getFullYear()}-${now.getMonth()}-01`;
+        const startingDate = new Date(now.setMonth(now.getMonth() - 1)).toISOString();
         const endingDate = now.toISOString();
         const res = await api.getCustomerInvoices(
           "<token>",
           {
-            status: "COMPLETED",
+            status: "DRAFT",
             starting_on: startingDate,
             ending_before: endingDate,
           },
           { project_id: currentProject.id }
         );
 
+        console.log(res)
+
         const invoices = InvoiceValidator.array().parse(res.data);
         return invoices;
       } catch (error) {

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

@@ -46,6 +46,7 @@ function BillingPage(): JSX.Element {
   const { creditGrants } = usePorterCredits();
   const { plan } = useCustomerPlan();
   const { invoiceList } = useCustomerInvoices();
+  console.log(invoiceList)
 
   const {
     paymentMethodList,

+ 1 - 1
dashboard/src/shared/api.tsx

@@ -3536,7 +3536,7 @@ const getCustomerInvoices = baseApi<
   {
     project_id?: number;
   }
->("POST", ({ project_id }) => `/api/projects/${project_id}/billing/invoices`);
+>("GET", ({ project_id }) => `/api/projects/${project_id}/billing/invoices`);
 
 const getCustomerPlan = baseApi<
   {},

+ 35 - 13
internal/billing/metronome.go

@@ -119,7 +119,7 @@ func (m MetronomeClient) createCustomer(ctx context.Context, userEmail string, p
 		Data types.Customer `json:"data"`
 	}
 
-	_, err = m.do(http.MethodPost, path, customer, &result)
+	_, err = m.do(http.MethodPost, path, nil, customer, &result)
 	if err != nil {
 		return customerID, telemetry.Error(ctx, span, err, "error creating customer")
 	}
@@ -159,7 +159,7 @@ func (m MetronomeClient) addCustomerPlan(ctx context.Context, customerID uuid.UU
 		} `json:"data"`
 	}
 
-	_, err = m.do(http.MethodPost, path, req, &result)
+	_, err = m.do(http.MethodPost, path, nil, req, &result)
 	if err != nil {
 		return customerPlanID, telemetry.Error(ctx, span, err, "failed to add customer to plan")
 	}
@@ -182,7 +182,7 @@ func (m MetronomeClient) ListCustomerPlan(ctx context.Context, customerID uuid.U
 		Data []types.Plan `json:"data"`
 	}
 
-	_, err = m.do(http.MethodGet, path, nil, &result)
+	_, err = m.do(http.MethodGet, path, nil, nil, &result)
 	if err != nil {
 		return plan, telemetry.Error(ctx, span, err, "failed to list customer plans")
 	}
@@ -214,7 +214,7 @@ func (m MetronomeClient) EndCustomerPlan(ctx context.Context, customerID uuid.UU
 		EndingBeforeUTC: endBefore,
 	}
 
-	_, err = m.do(http.MethodPost, path, req, nil)
+	_, err = m.do(http.MethodPost, path, nil, req, nil)
 	if err != nil {
 		return telemetry.Error(ctx, span, err, "failed to end customer plan")
 	}
@@ -243,7 +243,7 @@ func (m MetronomeClient) ListCustomerCredits(ctx context.Context, customerID uui
 		Data []types.CreditGrant `json:"data"`
 	}
 
-	_, err = m.do(http.MethodPost, path, req, &result)
+	_, err = m.do(http.MethodPost, path, nil, req, &result)
 	if err != nil {
 		return credits, telemetry.Error(ctx, span, err, "failed to list customer credits")
 	}
@@ -289,7 +289,7 @@ func (m MetronomeClient) CreateCreditsGrant(ctx context.Context, customerID uuid
 		Priority:  1,
 	}
 
-	statusCode, err := m.do(http.MethodPost, path, req, nil)
+	statusCode, err := m.do(http.MethodPost, path, nil, req, nil)
 	if err != nil && statusCode != http.StatusConflict {
 		// a conflict response indicates the grant already exists
 		return telemetry.Error(ctx, span, err, "failed to create credits grant")
@@ -341,7 +341,7 @@ func (m MetronomeClient) ListCustomerUsage(ctx context.Context, customerID uuid.
 		}
 
 		baseReq.BillableMetricID = billableMetric.ID
-		_, err = m.do(http.MethodPost, path, baseReq, &result)
+		_, err = m.do(http.MethodPost, path, nil, baseReq, &result)
 		if err != nil {
 			return usage, telemetry.Error(ctx, span, err, "failed to get customer usage")
 		}
@@ -355,6 +355,7 @@ func (m MetronomeClient) ListCustomerUsage(ctx context.Context, customerID uuid.
 	return usage, nil
 }
 
+// ListCustomerInvoices will return the invoices for a customer for the given status and time range
 func (m MetronomeClient) ListCustomerInvoices(ctx context.Context, customerID uuid.UUID, status string, startingOn string, endingBefore string) (invoices []types.Invoice, err error) {
 	ctx, span := telemetry.NewSpan(ctx, "list-customer-invoices")
 	defer span.End()
@@ -363,12 +364,25 @@ func (m MetronomeClient) ListCustomerInvoices(ctx context.Context, customerID uu
 		return invoices, telemetry.Error(ctx, span, err, "customer id empty")
 	}
 
-	path := fmt.Sprintf("/customers/%s/invoices?status=%s&starting_on=%s&ending_before=%s", customerID, status, startingOn, endingBefore)
+	path := fmt.Sprintf("customers/%s/invoices", customerID)
+
+	queryParams := map[string]string{
+		"status": status,
+	}
+
+	if startingOn != "" {
+		queryParams["starting_on"] = startingOn
+	}
+
+	if endingBefore != "" {
+		queryParams["ending_before"] = endingBefore
+	}
+
 	var result struct {
 		Data []types.Invoice `json:"data"`
 	}
 
-	_, err = m.do(http.MethodGet, path, nil, &result)
+	_, err = m.do(http.MethodGet, path, queryParams, nil, &result)
 	if err != nil {
 		return invoices, telemetry.Error(ctx, span, err, "failed to list customer invoices")
 	}
@@ -389,7 +403,7 @@ func (m MetronomeClient) IngestEvents(ctx context.Context, events []types.Billin
 
 	var currentAttempts int
 	for currentAttempts < defaultMaxRetries {
-		statusCode, err := m.do(http.MethodPost, path, events, nil)
+		statusCode, err := m.do(http.MethodPost, path, nil, events, nil)
 		// Check errors that are not from error http codes
 		if statusCode == 0 && err != nil {
 			return telemetry.Error(ctx, span, err, "failed to ingest billing events")
@@ -428,7 +442,7 @@ func (m MetronomeClient) listBillableMetricIDs(ctx context.Context, customerID u
 		Data []types.BillableMetric `json:"data"`
 	}
 
-	_, err = m.do(http.MethodGet, path, nil, &result)
+	_, err = m.do(http.MethodGet, path, nil, nil, &result)
 	if err != nil {
 		return billableMetrics, telemetry.Error(ctx, span, err, "failed to retrieve billable metrics from metronome")
 	}
@@ -446,7 +460,7 @@ func (m MetronomeClient) getCreditTypeID(ctx context.Context, currencyCode strin
 		Data []types.PricingUnit `json:"data"`
 	}
 
-	_, err = m.do(http.MethodGet, path, nil, &result)
+	_, err = m.do(http.MethodGet, path, nil, nil, &result)
 	if err != nil {
 		return creditTypeID, telemetry.Error(ctx, span, err, "failed to retrieve billable metrics from metronome")
 	}
@@ -460,7 +474,7 @@ func (m MetronomeClient) getCreditTypeID(ctx context.Context, currencyCode strin
 	return creditTypeID, telemetry.Error(ctx, span, fmt.Errorf("credit type not found for currency code %s", currencyCode), "failed to find credit type")
 }
 
-func (m MetronomeClient) do(method string, path string, body interface{}, data interface{}) (statusCode int, err error) {
+func (m MetronomeClient) do(method string, path string, queryParams map[string]string, body interface{}, data interface{}) (statusCode int, err error) {
 	client := http.Client{}
 	endpoint, err := url.JoinPath(metronomeBaseUrl, path)
 	if err != nil {
@@ -475,6 +489,14 @@ func (m MetronomeClient) do(method string, path string, body interface{}, data i
 		}
 	}
 
+	if queryParams != nil {
+		q := url.Values{}
+		for k, v := range queryParams {
+			q.Add(k, v)
+		}
+		endpoint = fmt.Sprintf("%s?%s", endpoint, q.Encode())
+	}
+
 	req, err := http.NewRequest(method, endpoint, bytes.NewBuffer(bodyJson))
 	if err != nil {
 		return statusCode, err