Add paid-access and usage limit helpers for clean-pdf
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { decidePaidAccess, monthStartUtc } from "@/lib/clean-pdf/access";
|
||||||
|
|
||||||
|
const now = new Date("2026-07-06T10:00:00Z");
|
||||||
|
const freeSlug = "obsidian-start";
|
||||||
|
|
||||||
|
describe("decidePaidAccess", () => {
|
||||||
|
it("платный enrollment без срока — да", () => {
|
||||||
|
expect(decidePaidAccess({ role: "student", banned: false, freeSlug, now,
|
||||||
|
enrollments: [{ courseSlug: "zotero", expiresAt: null }] })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("только бесплатный курс — нет", () => {
|
||||||
|
expect(decidePaidAccess({ role: "student", banned: false, freeSlug, now,
|
||||||
|
enrollments: [{ courseSlug: "obsidian-start", expiresAt: null }] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("без enrollments — нет", () => {
|
||||||
|
expect(decidePaidAccess({ role: "student", banned: false, freeSlug, now, enrollments: [] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("истёкший платный — нет, живой срок — да", () => {
|
||||||
|
expect(decidePaidAccess({ role: "student", banned: false, freeSlug, now,
|
||||||
|
enrollments: [{ courseSlug: "zotero", expiresAt: new Date("2026-01-01") }] })).toBe(false);
|
||||||
|
expect(decidePaidAccess({ role: "student", banned: false, freeSlug, now,
|
||||||
|
enrollments: [{ courseSlug: "zotero", expiresAt: new Date("2027-01-01") }] })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("admin и curator — да даже без курсов", () => {
|
||||||
|
expect(decidePaidAccess({ role: "admin", banned: false, freeSlug, now, enrollments: [] })).toBe(true);
|
||||||
|
expect(decidePaidAccess({ role: "curator", banned: false, freeSlug, now, enrollments: [] })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("бан всё перекрывает", () => {
|
||||||
|
expect(decidePaidAccess({ role: "admin", banned: true, freeSlug, now, enrollments: [] })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("monthStartUtc", () => {
|
||||||
|
it("возвращает первое число месяца 00:00 UTC", () => {
|
||||||
|
expect(monthStartUtc(new Date("2026-07-06T23:59:59Z")).toISOString()).toBe("2026-07-01T00:00:00.000Z");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
// НЕ "clean-pdf": так CopyButton логирует копирования — они не должны тратить лимит
|
||||||
|
export const PDF_TOOL_ID = "clean-pdf-generate";
|
||||||
|
export const BURST_LIMIT = 5; // успешных генераций в минуту
|
||||||
|
const BURST_WINDOW_MS = 60_000;
|
||||||
|
|
||||||
|
export function decidePaidAccess(input: {
|
||||||
|
role: string;
|
||||||
|
banned: boolean;
|
||||||
|
enrollments: { courseSlug: string; expiresAt: Date | null }[];
|
||||||
|
freeSlug: string;
|
||||||
|
now?: Date;
|
||||||
|
}): boolean {
|
||||||
|
const now = input.now ?? new Date();
|
||||||
|
if (input.banned) return false;
|
||||||
|
if (input.role === "admin" || input.role === "curator") return true;
|
||||||
|
return input.enrollments.some(
|
||||||
|
(e) => e.courseSlug !== input.freeSlug && (e.expiresAt === null || e.expiresAt > now),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hasPaidAccess(userId: string): Promise<boolean> {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: {
|
||||||
|
role: true,
|
||||||
|
banned: true,
|
||||||
|
enrollments: { select: { expiresAt: true, course: { select: { slug: true } } } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!user) return false;
|
||||||
|
return decidePaidAccess({
|
||||||
|
role: user.role,
|
||||||
|
banned: user.banned ?? false,
|
||||||
|
freeSlug: process.env.FREE_COURSE_SLUG ?? "obsidian-start",
|
||||||
|
enrollments: user.enrollments.map((e) => ({ courseSlug: e.course.slug, expiresAt: e.expiresAt })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function monthStartUtc(now: Date): Date {
|
||||||
|
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMonthlyLimit(): number {
|
||||||
|
const n = Number(process.env.PDF_MONTHLY_LIMIT);
|
||||||
|
return Number.isFinite(n) && n > 0 ? n : 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMonthlyUsage(userId: string): Promise<number> {
|
||||||
|
return prisma.toolUsage.count({
|
||||||
|
where: { userId, tool: PDF_TOOL_ID, createdAt: { gte: monthStartUtc(new Date()) } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBurstUsage(userId: string): Promise<number> {
|
||||||
|
return prisma.toolUsage.count({
|
||||||
|
where: { userId, tool: PDF_TOOL_ID, createdAt: { gte: new Date(Date.now() - BURST_WINDOW_MS) } },
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user