Every time a user submits a final exam,Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Stivenz3/Nexu/llms.txt
Use this file to discover all available pages before exploring further.
LessonExamPage checks whether this passing score is the last piece needed to unlock the full BPM course certificate. If all 8 lessons are now passed, it triggers the issuance pipeline — a chain of service calls that culminates in two atomic Firestore writes and a redirect to the official certificate view.
Trigger: LessonExamPage
After a user passes an exam (score >= passingPercent), LessonExamPage calls tryIssueCourseCertificateAfterLesson with the current lesson context and the authenticated user’s profile data:
/certificado/documento.
Input Interface
The full input shape passed fromLessonExamPage to the service:
Complete Issuance Flow
Exam submitted and score calculated
LessonExamPage scores the exam. If score >= examConfig.passingPercent, it calls saveExamResult to persist the result in users/{userId}/lessonProgress/{lessonId}, then proceeds to certificate issuance.Check all 8 lessons passed
tryIssueCourseCertificateAfterLesson calls areAllCourseLessonsPassed(userId). This iterates over all 8 lesson positions (order 1–8), verifies each lesson is published (isActive: true) in Firestore, and confirms progress.status === 'passed' for each.If any lesson is unpublished or not yet passed, the function returns null — no certificate is created.Idempotency check — look for existing certificate
getCourseCertificate(userId) queries certificates where userId == userId and courseId == 'nexu_bpm_curso'. If a certificate already exists, it is returned immediately. No duplicate is ever written.Build completed lessons snapshot
buildCompletedLessonsSnapshot(userId) iterates over the 8 lessons again and builds an array of CompletedLessonSnapshot objects — one per passed lesson — capturing lessonId, lessonTitle, and finalScore (from bestEvalScore in the progress record).Compute average score
The average score across all completed lessons is calculated:This
avgScore becomes the finalScore stored in the certificate record.Generate certificate identifiers
issueCourseCertificate generates two identifiers:certificateId— deterministic:cert_{userId.slice(0, 8)}_nexu_bpm_cursoverifyCode— random 13-character code using the character setABCDEFGHJKLMNPQRSTUVWXYZ23456789, formatted asNX-XXXXXXXXXX
qrVerifyUrl is built by passing verifyCode to buildVerifyUrl(verifyCode) from src/lib/verifyUrl.ts.Write private record to certificates/{certificateId}
A
CertificateRecord document is written with setDoc. It contains the full unmasked userDocument, the completedLessons snapshot, timestamps (issuedAt = now, expiresAt = +1 year), and isValid: true.Firestore rules allow this create only when request.auth.uid == data.userId and data.isValid == true. Updates and deletes are denied.Write public record to certificatePublic/{verifyCode}
A The public document is readable by anyone (
CertificatePublicRecord document is written to certificatePublic/{verifyCode}. The document number is masked before writing:allow read: if true). Create requires auth; update and delete are denied.Document ID Determinism
ThecertificateId is deterministic, not random:
setDoc with that ID is safe to call multiple times — the inner getCourseCertificate guard prevents any actual second write, but the deterministic ID also ensures there can never be two certificates for the same user in the same Firestore collection.
Deprecated: issueCertificate
The original issueCertificate(input) export is marked @deprecated. It now delegates internally to tryIssueCourseCertificateAfterLesson and throws an error if all 8 lessons are not yet passed. Do not use it in new code.
Adding a new lesson? You do not need to change anything in
certificateService.ts. LessonExamPage already calls tryIssueCourseCertificateAfterLesson with the correct lessonId and lessonTitle derived from the route parameter. As long as the new lesson exists in Firestore with a valid lessonId and isActive: true, the certificate pipeline will include it automatically in the 8-lesson check.