import type { PrismaClient } from '@generated/prisma/client'; import type { AttendeePaymentStatus, ClassToday, ProblemDetails, Result, } from '@gruperly/shared'; import { err } from '@gruperly/shared'; import { noGroupAccessProblem, notFoundResourceProblem } from '@/http/problem-builders'; export type ClassesDb = Pick< PrismaClient, 'group' | 'classSession' | 'attendee' | 'payment' | 'attendance' | 'slotRelease' >; type PaymentLike = { status: 'PENDING' | 'PAID' | 'OVERDUE' | 'CANCELLED'; dueDate: Date; }; // Estado de cobro calculado en tiempo real (sin columna denormalizada): // PENDIENTE si hay un pago vencido (OVERDUE) o un PENDING cuyo vencimiento ya pasó. export function resolvePaymentStatus( payments: PaymentLike[], startOfToday: Date, ): AttendeePaymentStatus { const hasUnpaid = payments.some( (payment) => payment.status === 'OVERDUE' || (payment.status === 'PENDING' && payment.dueDate.getTime() < startOfToday.getTime()), ); return hasUnpaid ? 'PENDING' : 'UP_TO_DATE'; } export function absenceReleaseHours(): number { const raw = process.env.ABSENCE_RELEASE_HOURS; if (raw == null || raw.trim() === '') { return 12; } const parsed = Number(raw); return Number.isFinite(parsed) && parsed >= 0 ? parsed : 12; } export type ClassSessionRecord = { id: string; groupId: string; startsAt: Date; }; type GroupWithAccess = { createdById: string; members: { userId: string }[]; }; // Verifica que el usuario sea owner o miembro del grupo dueño de la sesión. export function ensureSessionAccess( session: ClassSessionRecord & { group: GroupWithAccess }, userId: string, ): Result { const isOwner = session.group.createdById === userId; const isMember = session.group.members.length > 0; if (!isOwner && !isMember) { return err(noGroupAccessProblem()); } return { ok: true, value: { id: session.id, groupId: session.groupId, startsAt: session.startsAt }, }; } export function sessionNotFound(sessionId: string): ProblemDetails { return notFoundResourceProblem('Class session', sessionId); } export function toClassToday( session: { id: string; startsAt: Date }, group: { id: string; name: string; capacity: number | null }, enrolledCount: number, hasAttendance: boolean, ): ClassToday { return { sessionId: session.id, groupId: group.id, groupName: group.name, startsAt: session.startsAt.toISOString(), enrolledCount, capacity: group.capacity, availableSlots: group.capacity == null ? null : Math.max(0, group.capacity - enrolledCount), hasAttendance, }; }