import { AttendanceStatus } from '@generated/prisma/client'; import type { NotifyAbsenceResult, ProblemDetails, Result } from '@gruperly/shared'; import { err, ok } from '@gruperly/shared'; import { notFoundResourceProblem, validationProblem } from '@/http/problem-builders'; import type { UnitOfWork } from '@/lib/prisma'; import { type ClassesDb, sessionNotFound } from './helpers'; type RegisterAbsenceDeps = { db: Pick; unitOfWork: UnitOfWork; }; type RegisterAbsenceInput = { attendeeId: string; classSessionId: string; now: Date; releaseHours: number; }; // Núcleo compartido del aviso de ausencia (profesor autenticado y alumno con token): // - Marca la asistencia como EXCUSED (upsert idempotente). // - Si la anticipación alcanza `releaseHours`, crea/conserva un SlotRelease para liberar el cupo. export async function registerAbsence( deps: RegisterAbsenceDeps, input: RegisterAbsenceInput, ): Promise> { const { db, unitOfWork } = deps; const { attendeeId, classSessionId, now, releaseHours } = input; const session = await db.classSession.findUnique({ where: { id: classSessionId } }); if (!session) { return err(sessionNotFound(classSessionId)); } const attendee = await db.attendee.findUnique({ where: { id: attendeeId } }); if (!attendee) { return err(notFoundResourceProblem('Attendee', attendeeId)); } if (attendee.groupId !== session.groupId) { return err( validationProblem({ detail: 'El alumno no pertenece al grupo de esta clase.' }), ); } const anticipationMs = session.startsAt.getTime() - now.getTime(); const slotReleased = anticipationMs >= releaseHours * 60 * 60 * 1000; return unitOfWork.executeResult(async (tx) => { await tx.attendance.upsert({ where: { attendeeId_classSessionId: { attendeeId, classSessionId, }, }, create: { attendeeId, classSessionId, status: AttendanceStatus.EXCUSED, markedAt: now, }, update: { status: AttendanceStatus.EXCUSED, markedAt: now, }, }); if (slotReleased) { await tx.slotRelease.upsert({ where: { attendeeId_classSessionId: { attendeeId, classSessionId, }, }, create: { attendeeId, classSessionId, isClaimed: false, }, update: {}, }); } return ok({ classSessionId, notified: true, slotReleased, }); }); }