diff --git a/apps/web/src/components/payments/PlanDetails.tsx b/apps/web/src/components/payments/PlanDetails.tsx index 2590e165..4b4c43bb 100644 --- a/apps/web/src/components/payments/PlanDetails.tsx +++ b/apps/web/src/components/payments/PlanDetails.tsx @@ -1,5 +1,6 @@ import { Plan } from "@prisma/client"; import { PLAN_PERKS } from "~/lib/constants/payments"; +import { isEntitledSubscriptionStatus } from "~/lib/subscription-status"; import { CheckCircle2 } from "lucide-react"; import { api } from "~/trpc/react"; import Spinner from "@usesend/ui/src/spinner"; @@ -17,13 +18,14 @@ export const PlanDetails = () => { const planKey = currentTeam.plan as keyof typeof PLAN_PERKS; const perks = PLAN_PERKS[planKey] || []; + const isEntitled = isEntitledSubscriptionStatus( + subscriptionQuery.data?.status, + ); return (
- {subscriptionQuery.data?.status === "active" - ? planKey.toLowerCase() - : "free"} + {isEntitled ? planKey.toLowerCase() : "free"}
Current plan
diff --git a/apps/web/src/lib/subscription-status.ts b/apps/web/src/lib/subscription-status.ts new file mode 100644 index 00000000..311a60d6 --- /dev/null +++ b/apps/web/src/lib/subscription-status.ts @@ -0,0 +1,11 @@ +const ENTITLED_SUBSCRIPTION_STATUSES = new Set([ + "active", + "trialing", + "past_due", +]); + +export function isEntitledSubscriptionStatus( + status: string | null | undefined, +) { + return Boolean(status && ENTITLED_SUBSCRIPTION_STATUSES.has(status)); +} diff --git a/apps/web/src/lib/subscription-status.unit.test.ts b/apps/web/src/lib/subscription-status.unit.test.ts new file mode 100644 index 00000000..ddbc9240 --- /dev/null +++ b/apps/web/src/lib/subscription-status.unit.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { isEntitledSubscriptionStatus } from "~/lib/subscription-status"; + +describe("isEntitledSubscriptionStatus", () => { + it("treats retrying subscriptions as entitled", () => { + expect(isEntitledSubscriptionStatus("past_due")).toBe(true); + }); + + it("treats active and trialing subscriptions as entitled", () => { + expect(isEntitledSubscriptionStatus("active")).toBe(true); + expect(isEntitledSubscriptionStatus("trialing")).toBe(true); + }); + + it("treats exhausted or incomplete subscriptions as not entitled", () => { + expect(isEntitledSubscriptionStatus("unpaid")).toBe(false); + expect(isEntitledSubscriptionStatus("canceled")).toBe(false); + expect(isEntitledSubscriptionStatus("incomplete")).toBe(false); + expect(isEntitledSubscriptionStatus(null)).toBe(false); + }); +}); diff --git a/apps/web/src/server/billing/payments.ts b/apps/web/src/server/billing/payments.ts index 00459e31..a73fd04f 100644 --- a/apps/web/src/server/billing/payments.ts +++ b/apps/web/src/server/billing/payments.ts @@ -1,5 +1,6 @@ import Stripe from "stripe"; import { env } from "~/env"; +import { isEntitledSubscriptionStatus } from "~/lib/subscription-status"; import { db } from "../db"; import { sendSubscriptionConfirmationEmail } from "../mailer"; import { TeamService } from "../service/team-service"; @@ -149,6 +150,7 @@ export async function syncStripeData(customerId: string) { .filter((id): id is string => Boolean(id)); const nextPlan = getPlanFromPriceIds(priceIds); + const isEntitled = isEntitledSubscriptionStatus(subscription.status); const isNowPaid = subscription.status === "active" && nextPlan !== "FREE"; const shouldSendSubscriptionConfirmation = !wasPaid && isNowPaid; @@ -159,10 +161,10 @@ export async function syncStripeData(customerId: string) { priceId: subscription.items.data[0]?.price?.id || "", priceIds: priceIds, currentPeriodEnd: new Date( - subscription.items.data[0]?.current_period_end * 1000 + subscription.items.data[0]?.current_period_end * 1000, ), currentPeriodStart: new Date( - subscription.items.data[0]?.current_period_start * 1000 + subscription.items.data[0]?.current_period_start * 1000, ), cancelAtPeriodEnd: subscription.cancel_at ? new Date(subscription.cancel_at * 1000) @@ -176,10 +178,10 @@ export async function syncStripeData(customerId: string) { priceId: subscription.items.data[0]?.price?.id || "", priceIds: priceIds, currentPeriodEnd: new Date( - subscription.items.data[0]?.current_period_end * 1000 + subscription.items.data[0]?.current_period_end * 1000, ), currentPeriodStart: new Date( - subscription.items.data[0]?.current_period_start * 1000 + subscription.items.data[0]?.current_period_start * 1000, ), cancelAtPeriodEnd: subscription.cancel_at ? new Date(subscription.cancel_at * 1000) @@ -191,7 +193,7 @@ export async function syncStripeData(customerId: string) { await TeamService.updateTeam(team.id, { plan: subscription.status === "canceled" ? "FREE" : nextPlan, - isActive: subscription.status === "active", + isActive: isEntitled, }); if (shouldSendSubscriptionConfirmation) { @@ -201,12 +203,12 @@ export async function syncStripeData(customerId: string) { teamUsers .map((tu) => tu.user?.email) .filter((email): email is string => Boolean(email)) - .map((email) => sendSubscriptionConfirmationEmail(email)) + .map((email) => sendSubscriptionConfirmationEmail(email)), ); } catch (err) { logger.error( { err, teamId: team.id }, - "[Billing]: Failed sending subscription confirmation email" + "[Billing]: Failed sending subscription confirmation email", ); } }