diff --git a/.github/workflows/porter_stack_porter-ui copy.yml b/.github/workflows/porter_stack_porter-ui copy.yml
new file mode 100644
index 00000000000..8f3ac709681
--- /dev/null
+++ b/.github/workflows/porter_stack_porter-ui copy.yml
@@ -0,0 +1,37 @@
+'on':
+ push:
+ branches:
+ - metronome-integration
+name: Deploy Porter to Internal Tooling
+jobs:
+ build-go:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ - name: build-go
+ uses: ./.github/actions/build-go
+
+ build-npm:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ - name: build-npm
+ uses: ./.github/actions/build-npm
+
+ porter-deploy:
+ runs-on: ubuntu-latest
+ needs: [build-go, build-npm]
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ - name: porter-deploy
+ timeout-minutes: 30
+ uses: ./.github/actions/porter-deploy
+ with:
+ app: porter-ui
+ cluster: '37'
+ host: https://dashboard.internal-tools.porter.run
+ project: '18'
+ token: ${{ secrets.PORTER_STAGING_DEPLOYMENT }}
diff --git a/api/server/handlers/project/create.go b/api/server/handlers/project/create.go
index 75cbe335287..1c53bf9c02f 100644
--- a/api/server/handlers/project/create.go
+++ b/api/server/handlers/project/create.go
@@ -99,8 +99,7 @@ func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
}
// Create Metronome customer and add to starter plan
- if p.Config().ServerConf.MetronomeAPIKey != "" && p.Config().ServerConf.PorterCloudPlanID != "" &&
- p.Config().ServerConf.EnableSandbox {
+ if p.Config().ServerConf.MetronomeAPIKey != "" && p.Config().ServerConf.PorterCloudPlanID != "" {
// Create Metronome Customer
if p.Config().ServerConf.MetronomeAPIKey != "" {
usageID, err := p.Config().BillingManager.MetronomeClient.CreateCustomer(user.CompanyName, proj.Name, proj.ID, proj.BillingID)
diff --git a/api/server/handlers/project/delete.go b/api/server/handlers/project/delete.go
index 58c1c99411f..a8804a23662 100644
--- a/api/server/handlers/project/delete.go
+++ b/api/server/handlers/project/delete.go
@@ -4,7 +4,6 @@ import (
"net/http"
"connectrpc.com/connect"
- "github.com/google/uuid"
porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
"github.com/porter-dev/porter/api/server/handlers"
"github.com/porter-dev/porter/api/server/shared"
@@ -94,16 +93,9 @@ func (p *ProjectDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
}
if p.Config().ServerConf.MetronomeAPIKey != "" && p.Config().ServerConf.PorterCloudPlanID != "" {
- porterCloudPlanID, err := uuid.Parse(p.Config().ServerConf.PorterCloudPlanID)
+ err = p.Config().BillingManager.MetronomeClient.EndCustomerPlan(proj.UsageID, proj.UsagePlanID)
if err != nil {
- err = telemetry.Error(ctx, span, err, "error parsing starter plan id")
- p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
- return
- }
-
- err = p.Config().BillingManager.MetronomeClient.EndCustomerPlan(proj.UsageID, porterCloudPlanID)
- if err != nil {
- e := "error deleting project in usage provider"
+ e := "error ending billing plan"
err = telemetry.Error(ctx, span, err, e)
p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
diff --git a/api/server/router/project.go b/api/server/router/project.go
index 8cb0d8f8a3e..728aa45fcda 100644
--- a/api/server/router/project.go
+++ b/api/server/router/project.go
@@ -341,6 +341,33 @@ func getProjectRoutes(
Router: r,
})
+ // GET /api/projects/{project_id}/billing/credits -> project.NewGetCreditsHandler
+ getCreditsEndpoint := factory.NewAPIEndpoint(
+ &types.APIRequestMetadata{
+ Verb: types.APIVerbGet,
+ Method: types.HTTPVerbGet,
+ Path: &types.Path{
+ Parent: basePath,
+ RelativePath: relPath + "/billing/credits",
+ },
+ Scopes: []types.PermissionScope{
+ types.UserScope,
+ types.ProjectScope,
+ },
+ },
+ )
+
+ getCreditsHandler := billing.NewGetCreditsHandler(
+ config,
+ factory.GetResultWriter(),
+ )
+
+ routes = append(routes, &router.Route{
+ Endpoint: getCreditsEndpoint,
+ Handler: getCreditsHandler,
+ Router: r,
+ })
+
// POST /api/projects/{project_id}/billing/payment_method -> project.NewCreateBillingHandler
createBillingEndpoint := factory.NewAPIEndpoint(
&types.APIRequestMetadata{
diff --git a/api/types/billing.go b/api/types/billing.go
index d693761de8e..1f36a3e6c26 100644
--- a/api/types/billing.go
+++ b/api/types/billing.go
@@ -65,18 +65,22 @@ type EndCustomerPlanRequest struct {
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"`
+ CreditTypeIDs []uuid.UUID `json:"credit_type_ids,omitempty"`
// An array of Metronome customer IDs. This must not be specified if
// credit_grant_ids is specified.
- CustomerIDs []uuid.UUID `json:"customer_ids"`
+ CustomerIDs []uuid.UUID `json:"customer_ids,omitempty"`
// 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"`
+ CreditGrantIDs []uuid.UUID `json:"credit_grant_ids,omitempty"`
// Only return credit grants that expire at or after this RFC 3339 timestamp.
- NotExpiringBefore string `json:"not_expiring_before"`
+ NotExpiringBefore string `json:"not_expiring_before,omitempty"`
// Only return credit grants that are effective before this RFC 3339 timestamp
// (exclusive).
- EffectiveBefore string `json:"effective_before"`
+ EffectiveBefore string `json:"effective_before,omitempty"`
+}
+
+type ListCreditGrantsResponse struct {
+ Data []CreditGrant `json:"data"`
}
type CreditType struct {
diff --git a/dashboard/src/lib/hooks/useStripe.tsx b/dashboard/src/lib/hooks/useStripe.tsx
index 86e0fb754e7..fefa2d5f802 100644
--- a/dashboard/src/lib/hooks/useStripe.tsx
+++ b/dashboard/src/lib/hooks/useStripe.tsx
@@ -35,6 +35,10 @@ type TGetPublishableKey = {
publishableKey: string;
};
+type TGetCredits = {
+ credits: number;
+};
+
export const usePaymentMethods = (): TUsePaymentMethod => {
const { currentProject } = useContext(Context);
@@ -197,12 +201,12 @@ export const usePublishableKey = (): TGetPublishableKey => {
};
};
-export const usePorterCredits = (): TGetPublishableKey => {
+export const usePorterCredits = (): TGetCredits => {
const { currentProject } = useContext(Context);
- // Fetch list of payment methods
- const keyReq = useQuery(
- ["getPublishableKey", currentProject?.id],
+ // Fetch available credits
+ const creditsReq = useQuery(
+ ["getPorterCredits", currentProject?.id],
async () => {
if (!currentProject?.id || currentProject.id === -1) {
return;
@@ -219,7 +223,7 @@ export const usePorterCredits = (): TGetPublishableKey => {
);
return {
- publishableKey: keyReq.data,
+ credits: creditsReq.data,
};
};
diff --git a/dashboard/src/main/home/project-settings/BillingPage.tsx b/dashboard/src/main/home/project-settings/BillingPage.tsx
index f8ce7240268..2a5b855c4a2 100644
--- a/dashboard/src/main/home/project-settings/BillingPage.tsx
+++ b/dashboard/src/main/home/project-settings/BillingPage.tsx
@@ -12,6 +12,7 @@ import Text from "components/porter/Text";
import {
checkIfProjectHasPayment,
usePaymentMethods,
+ usePorterCredits,
useSetDefaultPaymentMethod,
} from "lib/hooks/useStripe";
@@ -36,6 +37,12 @@ function BillingPage(): JSX.Element {
const { refetchPaymentEnabled } = checkIfProjectHasPayment();
+ const { credits } = usePorterCredits();
+
+ const formatCredits = (credits: number): string => {
+ return (credits / 100).toFixed(2);
+ };
+
const onCreate = async () => {
await refetchPaymentMethods();
setShouldCreate(false);
@@ -66,7 +73,7 @@ function BillingPage(): JSX.Element {
- {paymentMethodList?.length > 0 ? "$ 5.00" : "$ 0.00"}
+ {credits > 0 ? `$${formatCredits(credits)}` : "$ 0.00"}
diff --git a/internal/billing/metronome.go b/internal/billing/metronome.go
index c58fddd617f..c6ec36062f7 100644
--- a/internal/billing/metronome.go
+++ b/internal/billing/metronome.go
@@ -77,7 +77,7 @@ func (m *MetronomeClient) AddCustomerPlan(customerID uuid.UUID, planID uuid.UUID
var result types.AddCustomerPlanResponse
- err = post(path, http.MethodPost, m.ApiKey, req, result)
+ err = post(path, http.MethodPost, m.ApiKey, req, &result)
if err != nil {
return customerPlanID, err
}
@@ -114,7 +114,7 @@ func (m *MetronomeClient) GetCustomerCredits(customerID uuid.UUID) (credits int6
return credits, fmt.Errorf("customer id empty")
}
- path := fmt.Sprintf("credits/listGrants")
+ path := "credits/listGrants"
req := types.ListCreditGrantsRequest{
CustomerIDs: []uuid.UUID{
@@ -122,13 +122,13 @@ func (m *MetronomeClient) GetCustomerCredits(customerID uuid.UUID) (credits int6
},
}
- var result types.CreditGrant
- err = post(path, http.MethodPost, m.ApiKey, req, result)
+ var result types.ListCreditGrantsResponse
+ err = post(path, http.MethodPost, m.ApiKey, req, &result)
if err != nil {
return credits, err
}
- return result.Balance.ExcludingPending, nil
+ return result.Data[0].Balance.IncludingPending, nil
}
func post(path string, method string, apiKey string, body interface{}, data interface{}) (err error) {