Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions apps/web/src/components/payments/PlanDetails.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 (
<div>
<div className="capitalize text-lg">
{subscriptionQuery.data?.status === "active"
? planKey.toLowerCase()
: "free"}
{isEntitled ? planKey.toLowerCase() : "free"}
</div>
<div className="flex items-center gap-2">
<div className="text-muted-foreground text-sm">Current plan</div>
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/lib/subscription-status.ts
Original file line number Diff line number Diff line change
@@ -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));
}
20 changes: 20 additions & 0 deletions apps/web/src/lib/subscription-status.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
16 changes: 9 additions & 7 deletions apps/web/src/server/billing/payments.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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) {
Expand All @@ -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",
);
}
}
Expand Down
Loading