diff --git a/api/server/handlers/billing/create.go b/api/server/handlers/billing/create.go index d44276a3c3..881d8856d8 100644 --- a/api/server/handlers/billing/create.go +++ b/api/server/handlers/billing/create.go @@ -17,15 +17,6 @@ import ( "github.com/porter-dev/porter/internal/telemetry" ) -const ( - // defaultRewardAmountCents is the default amount in USD cents rewarded to users - // who successfully refer a new user - defaultRewardAmountCents = 1000 - // defaultPaidAmountCents is the amount paid by the user to get the credits - // grant, if set to 0 it means they are free - defaultPaidAmountCents = 0 -) - // CreateBillingHandler is a handler for creating payment methods type CreateBillingHandler struct { handlers.PorterHandlerWriter @@ -140,6 +131,16 @@ func (c *CreateBillingHandler) grantRewardIfReferral(ctx context.Context, referr return nil } + referralCount, err := c.Repo().Referral().CountReferralsByProjectID(referral.ProjectID, models.ReferralStatusCompleted) + if err != nil { + return telemetry.Error(ctx, span, err, "failed to get referral count by referrer id") + } + + maxReferralRewards := c.Config().BillingManager.MetronomeClient.MaxReferralRewards + if referralCount >= maxReferralRewards { + return nil + } + referrerProject, err := c.Repo().Project().ReadProject(referral.ProjectID) if err != nil { return telemetry.Error(ctx, span, err, "failed to find referrer project") @@ -150,7 +151,9 @@ func (c *CreateBillingHandler) grantRewardIfReferral(ctx context.Context, referr // practice will mean the credits will most likely run out before expiring expiresAt := time.Now().AddDate(5, 0, 0).Format(time.RFC3339) reason := "Referral reward" - err := c.Config().BillingManager.MetronomeClient.CreateCreditsGrant(ctx, referrerProject.UsageID, reason, defaultRewardAmountCents, defaultPaidAmountCents, expiresAt) + rewardAmount := c.Config().BillingManager.MetronomeClient.DefaultRewardAmountCents + paidAmount := c.Config().BillingManager.MetronomeClient.DefaultPaidAmountCents + err := c.Config().BillingManager.MetronomeClient.CreateCreditsGrant(ctx, referrerProject.UsageID, reason, rewardAmount, paidAmount, expiresAt) if err != nil { return telemetry.Error(ctx, span, err, "failed to grand credits reward") } diff --git a/api/server/handlers/project/referrals.go b/api/server/handlers/project/referrals.go index c8d3030f72..80669a2f95 100644 --- a/api/server/handlers/project/referrals.go +++ b/api/server/handlers/project/referrals.go @@ -68,11 +68,13 @@ func (c *GetProjectReferralDetailsHandler) ServeHTTP(w http.ResponseWriter, r *h } referralCodeResponse := struct { - Code string `json:"code"` - ReferralCount int64 `json:"referral_count"` + Code string `json:"code"` + ReferralCount int64 `json:"referral_count"` + MaxAllowedRewards int64 `json:"max_allowed_referrals"` }{ - Code: proj.ReferralCode, - ReferralCount: referralCount, + Code: proj.ReferralCode, + ReferralCount: referralCount, + MaxAllowedRewards: c.Config().BillingManager.MetronomeClient.MaxReferralRewards, } c.WriteResult(w, r, referralCodeResponse) diff --git a/dashboard/src/lib/billing/types.tsx b/dashboard/src/lib/billing/types.tsx index 9a64261975..5228fc835a 100644 --- a/dashboard/src/lib/billing/types.tsx +++ b/dashboard/src/lib/billing/types.tsx @@ -17,13 +17,15 @@ const TrialValidator = z.object({ }); export type Plan = z.infer; -export const PlanValidator = z.object({ - id: z.string(), - plan_name: z.string(), - plan_description: z.string(), - starting_on: z.string(), - trial_info: TrialValidator, -}).nullable(); +export const PlanValidator = z + .object({ + id: z.string(), + plan_name: z.string(), + plan_description: z.string(), + starting_on: z.string(), + trial_info: TrialValidator, + }) + .nullable(); export type UsageMetric = z.infer; export const UsageMetricValidator = z.object({ @@ -52,7 +54,10 @@ export const CreditGrantsValidator = z.object({ export const ClientSecretResponse = z.string(); export type ReferralDetails = z.infer; -export const ReferralDetailsValidator = z.object({ - code: z.string(), - referral_count: z.number(), -}).nullable(); +export const ReferralDetailsValidator = z + .object({ + code: z.string(), + referral_count: z.number(), + max_allowed_referrals: z.number(), + }) + .nullable(); diff --git a/dashboard/src/main/home/project-settings/BillingPage.tsx b/dashboard/src/main/home/project-settings/BillingPage.tsx index 623deb15a4..4095904a5b 100644 --- a/dashboard/src/main/home/project-settings/BillingPage.tsx +++ b/dashboard/src/main/home/project-settings/BillingPage.tsx @@ -57,6 +57,19 @@ function BillingPage(): JSX.Element { const { usage } = useCustomerUsage("day", true); + const trialEnding = (starting_on: string, ending_before: string): string => { + if (ending_before === undefined) { + return ""; + } + + const diff = dayjs(ending_before).diff(dayjs()); + if (diff <= 0) { + return `Started on ${readableDate(starting_on)}`; + } + + return `Free trial ends ${dayjs().to(dayjs(ending_before))}`; + }; + const processedData = useMemo(() => { const before = usage; const resultMap = new Map(); diff --git a/dashboard/src/main/home/project-settings/ReferralsPage.tsx b/dashboard/src/main/home/project-settings/ReferralsPage.tsx index 226ca0b238..2f2a19cef9 100644 --- a/dashboard/src/main/home/project-settings/ReferralsPage.tsx +++ b/dashboard/src/main/home/project-settings/ReferralsPage.tsx @@ -1,35 +1,36 @@ import React from "react"; + +import Link from "components/porter/Link"; import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; import { useReferralDetails } from "lib/hooks/useStripe"; -import Link from "components/porter/Link"; function ReferralsPage(): JSX.Element { - const { referralDetails } = useReferralDetails(); - const baseUrl = window.location.origin; + const { referralDetails } = useReferralDetails(); + const baseUrl = window.location.origin; - return ( + return ( + <> + Referrals + + Refer people to Porter to earn credits. + + {referralDetails !== null && ( <> - Referrals - - - Refer people to Porter to earn credits. - - - {referralDetails !== null && ( - <> - - Your referral link is {" "} - -