Initial commit: PodcastYes — AI podcast platform
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getPlan, type Plan, type PlanKey, type FeatureKey } from "./plans";
|
||||
|
||||
export interface UpsertSubscriptionInput {
|
||||
provider: "stripe" | "paypal";
|
||||
referenceId: string;
|
||||
plan: PlanKey;
|
||||
status: string;
|
||||
billingInterval?: "month" | "year" | null;
|
||||
seats?: number | null;
|
||||
stripeCustomerId?: string | null;
|
||||
stripeSubscriptionId?: string | null;
|
||||
paypalSubscriptionId?: string | null;
|
||||
paypalPlanId?: string | null;
|
||||
periodStart?: Date | null;
|
||||
periodEnd?: Date | null;
|
||||
cancelAtPeriodEnd?: boolean | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The single writer both billing providers funnel into. Idempotent on the
|
||||
* provider subscription id, so duplicate/replayed webhooks converge on one row.
|
||||
*/
|
||||
export async function upsertSubscription(input: UpsertSubscriptionInput) {
|
||||
const existing = input.stripeSubscriptionId
|
||||
? await prisma.subscription.findFirst({
|
||||
where: { stripeSubscriptionId: input.stripeSubscriptionId },
|
||||
})
|
||||
: input.paypalSubscriptionId
|
||||
? await prisma.subscription.findFirst({
|
||||
where: { paypalSubscriptionId: input.paypalSubscriptionId },
|
||||
})
|
||||
: null;
|
||||
|
||||
const data = {
|
||||
provider: input.provider,
|
||||
plan: input.plan,
|
||||
status: input.status,
|
||||
billingInterval: input.billingInterval ?? undefined,
|
||||
seats: input.seats ?? undefined,
|
||||
stripeCustomerId: input.stripeCustomerId ?? undefined,
|
||||
stripeSubscriptionId: input.stripeSubscriptionId ?? undefined,
|
||||
paypalSubscriptionId: input.paypalSubscriptionId ?? undefined,
|
||||
paypalPlanId: input.paypalPlanId ?? undefined,
|
||||
periodStart: input.periodStart ?? undefined,
|
||||
periodEnd: input.periodEnd ?? undefined,
|
||||
cancelAtPeriodEnd: input.cancelAtPeriodEnd ?? undefined,
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
return prisma.subscription.update({ where: { id: existing.id }, data });
|
||||
}
|
||||
return prisma.subscription.create({ data: { referenceId: input.referenceId, ...data } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the active plan for a billing subject (a user id, or an organization id
|
||||
* for Agency). Returns "free" when there is no active subscription.
|
||||
*
|
||||
* Both Stripe and PayPal write into the same `subscription` table keyed by
|
||||
* `referenceId`, so this is provider-agnostic by construction.
|
||||
*/
|
||||
export async function getSubjectPlanKey(referenceId: string): Promise<PlanKey> {
|
||||
const sub = await prisma.subscription.findFirst({
|
||||
where: { referenceId, status: { in: ["active", "trialing"] } },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
return (sub?.plan as PlanKey) ?? "free";
|
||||
}
|
||||
|
||||
/** The active subscription row for a subject, if any. */
|
||||
export function getActiveSubscription(referenceId: string) {
|
||||
return prisma.subscription.findFirst({
|
||||
where: { referenceId, status: { in: ["active", "trialing", "past_due"] } },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective plan for the current request. When an organization is
|
||||
* active (Agency workspace), the org's plan governs; otherwise the user's own.
|
||||
*/
|
||||
export async function getEffectivePlan(
|
||||
userId: string,
|
||||
activeOrgId?: string | null
|
||||
): Promise<{ plan: Plan; key: PlanKey; subjectId: string; subjectType: "user" | "organization" }> {
|
||||
if (activeOrgId) {
|
||||
const key = await getSubjectPlanKey(activeOrgId);
|
||||
if (key !== "free") {
|
||||
return { plan: getPlan(key), key, subjectId: activeOrgId, subjectType: "organization" };
|
||||
}
|
||||
}
|
||||
const key = await getSubjectPlanKey(userId);
|
||||
return { plan: getPlan(key), key, subjectId: userId, subjectType: "user" };
|
||||
}
|
||||
|
||||
export async function subjectHasFeature(
|
||||
userId: string,
|
||||
feature: FeatureKey,
|
||||
activeOrgId?: string | null
|
||||
): Promise<boolean> {
|
||||
const { plan } = await getEffectivePlan(userId, activeOrgId);
|
||||
return plan.features.includes(feature);
|
||||
}
|
||||
Reference in New Issue
Block a user