import { db } from "@/lib/prisma"; import { isFinalExam, parseLessonDescriptionMeta } from "@/lib/courses/lessonContent"; type CertificatePrismaClient = { certificate: { create: (args: object) => Promise<{ id: string; certificateNumber?: string }>; findFirst: (args: object) => Promise<{ id: string; certificateNumber?: string } | null>; }; }; export type CertificateIssueResult = { certificateId: string | null; certificateNumber: string | null; newlyIssued: boolean; }; function getText(value: unknown): string { if (!value) return ""; if (typeof value === "string") return value; if (typeof value === "object") { const record = value as Record; if (typeof record.en === "string") return record.en; if (typeof record.es === "string") return record.es; } return ""; } function escapePdfText(value: string): string { return value.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)"); } function buildMinimalPdf(lines: string[]): Uint8Array { const contentLines = lines .map((line, index) => `BT /F1 14 Tf 72 ${730 - index * 24} Td (${escapePdfText(line)}) Tj ET`) .join("\n"); const stream = `${contentLines}\n`; const objects = [ "1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj", "2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj", "3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >> endobj", "4 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj", `5 0 obj << /Length ${stream.length} >> stream\n${stream}endstream endobj`, ]; let pdf = "%PDF-1.4\n"; const offsets: number[] = [0]; for (const object of objects) { offsets.push(pdf.length); pdf += `${object}\n`; } const xrefStart = pdf.length; pdf += `xref\n0 ${objects.length + 1}\n`; pdf += "0000000000 65535 f \n"; for (let i = 1; i <= objects.length; i += 1) { pdf += `${offsets[i].toString().padStart(10, "0")} 00000 n \n`; } pdf += `trailer << /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefStart}\n%%EOF`; return new TextEncoder().encode(pdf); } export async function issueCertificateIfEligible(userId: string, courseId: string): Promise { const prismaAny = db as unknown as CertificatePrismaClient; try { const existing = await prismaAny.certificate.findFirst({ where: { userId, courseId }, select: { id: true, certificateNumber: true }, }); if (existing) { return { certificateId: existing.id, certificateNumber: existing.certificateNumber ?? null, newlyIssued: false, }; } const [course, completedCount, lessons] = await Promise.all([ db.course.findUnique({ where: { id: courseId }, select: { id: true, title: true, slug: true }, }), db.userProgress.count({ where: { userId, isCompleted: true, lesson: { module: { courseId, }, }, }, }), db.lesson.findMany({ where: { module: { courseId, }, }, select: { id: true, description: true, }, }), ]); const totalCount = lessons.length; if (!course || totalCount === 0 || completedCount < totalCount) { return { certificateId: null, certificateNumber: null, newlyIssued: false }; } const finalExamLessonIds = lessons .filter((lesson) => isFinalExam(parseLessonDescriptionMeta(lesson.description).contentType)) .map((lesson) => lesson.id); if (finalExamLessonIds.length > 0) { const completedFinalExams = await db.userProgress.count({ where: { userId, isCompleted: true, lessonId: { in: finalExamLessonIds, }, }, }); if (completedFinalExams < finalExamLessonIds.length) { return { certificateId: null, certificateNumber: null, newlyIssued: false }; } } const membership = await db.membership.findFirst({ where: { userId, isActive: true }, select: { companyId: true }, }); const certificateNumber = `ACVE-${new Date().getFullYear()}-${Math.floor( 100000 + Math.random() * 900000, )}`; const certificate = await prismaAny.certificate.create({ data: { userId, courseId, companyId: membership?.companyId ?? null, certificateNumber, pdfVersion: 1, metadataSnapshot: { courseId: course.id, courseSlug: course.slug, courseTitle: getText(course.title) || "Untitled course", certificateNumber, completionPercent: 100, issuedAt: new Date().toISOString(), brandingVersion: "ACVE-2026-01", }, }, select: { id: true }, }); return { certificateId: certificate.id, certificateNumber: certificate.certificateNumber ?? certificateNumber, newlyIssued: true, }; } catch { return { certificateId: null, certificateNumber: null, newlyIssued: false }; } } export function buildCertificatePdf(input: { certificateNumber: string; learnerName: string; learnerEmail: string; courseTitle: string; issuedAt: Date; }): Uint8Array { const date = new Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(input.issuedAt); return buildMinimalPdf([ "ACVE - Certificate of Completion", "", `Certificate No: ${input.certificateNumber}`, "", "This certifies that", input.learnerName, `(${input.learnerEmail})`, "", "has successfully completed the course", input.courseTitle, "", `Issued on ${date}`, ]); }