queue-med/server/services/planLimits.ts

133 lines
4 KiB
TypeScript

import { and, eq, gte } from "drizzle-orm";
import { getDb, getSubscription } from "../db.js";
import { clinics, queueEntries } from "../schema.js";
import { sql } from "drizzle-orm";
export type PlanFeature =
| "maxClinics"
| "maxQueueEntriesPerDay"
| "multiPractitioner"
| "analyticsExport";
type PlanLimits = {
maxClinics: number;
maxQueueEntriesPerDay: number;
multiPractitioner: boolean;
analyticsExport: boolean;
};
// trial == basic level access during trial period; pro lifts everything.
export const PLAN_LIMITS: Record<"trial" | "basic" | "pro", PlanLimits> = {
trial: {
maxClinics: 1,
maxQueueEntriesPerDay: 50,
multiPractitioner: false,
analyticsExport: false,
},
basic: {
maxClinics: 1,
maxQueueEntriesPerDay: 200,
multiPractitioner: false,
analyticsExport: true,
},
pro: {
maxClinics: Infinity,
maxQueueEntriesPerDay: Infinity,
multiPractitioner: true,
analyticsExport: true,
},
};
export type PlanLimitCheck =
| { ok: true }
| { ok: false; reason: string; feature: PlanFeature };
async function getUserPlan(userId: number): Promise<"trial" | "basic" | "pro"> {
const sub = await getSubscription(userId);
return sub?.plan ?? "trial";
}
function limits(plan: "trial" | "basic" | "pro"): PlanLimits {
return PLAN_LIMITS[plan];
}
async function countClinicsForUser(userId: number): Promise<number> {
const db = await getDb();
const rows = await db
.select({ count: sql<number>`COUNT(*)` })
.from(clinics)
.where(eq(clinics.userId, userId));
return Number(rows[0]?.count ?? 0);
}
async function countQueueEntriesTodayForClinic(clinicId: number): Promise<number> {
const db = await getDb();
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const rows = await db
.select({ count: sql<number>`COUNT(*)` })
.from(queueEntries)
.where(
and(eq(queueEntries.clinicId, clinicId), gte(queueEntries.joinedAt, startOfDay))
);
return Number(rows[0]?.count ?? 0);
}
const FEATURE_MESSAGES: Record<PlanFeature, string> = {
maxClinics: "Limite de cabinets atteinte : passez au plan Pro pour en créer plus.",
maxQueueEntriesPerDay:
"Limite quotidienne de patients atteinte : passez au plan Pro pour des inscriptions illimitées.",
multiPractitioner:
"La gestion multi-praticiens est réservée au plan Pro. Mettez à niveau pour ajouter des praticiens.",
analyticsExport:
"L'export des statistiques est réservé aux plans payants. Mettez à niveau pour exporter vos données.",
};
export async function checkPlanLimit(
userId: number,
feature: PlanFeature,
context: { clinicId?: number } = {}
): Promise<PlanLimitCheck> {
const plan = await getUserPlan(userId);
const max = limits(plan);
switch (feature) {
case "maxClinics": {
if (max.maxClinics === Infinity) return { ok: true };
const current = await countClinicsForUser(userId);
if (current >= max.maxClinics) {
return { ok: false, feature, reason: FEATURE_MESSAGES.maxClinics };
}
return { ok: true };
}
case "maxQueueEntriesPerDay": {
if (max.maxQueueEntriesPerDay === Infinity) return { ok: true };
if (!context.clinicId) return { ok: true };
const today = await countQueueEntriesTodayForClinic(context.clinicId);
if (today >= max.maxQueueEntriesPerDay) {
return {
ok: false,
feature,
reason: FEATURE_MESSAGES.maxQueueEntriesPerDay,
};
}
return { ok: true };
}
case "multiPractitioner": {
if (max.multiPractitioner) return { ok: true };
return { ok: false, feature, reason: FEATURE_MESSAGES.multiPractitioner };
}
case "analyticsExport": {
if (max.analyticsExport) return { ok: true };
return { ok: false, feature, reason: FEATURE_MESSAGES.analyticsExport };
}
}
}
export async function getPlanLimitsForUser(userId: number): Promise<{
plan: "trial" | "basic" | "pro";
limits: PlanLimits;
}> {
const plan = await getUserPlan(userId);
return { plan, limits: limits(plan) };
}