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) {