- {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",
);
}
}