🔄 Ciclo de Vida de Hackathons
Visión General
Los hackathons en PuntoHack pasan por 5 estados bien definidos, con transiciones automáticas y manuales controladas por fechas y validaciones.
Diagrama de Estados
Estados del Hackathon
1. DRAFT (Borrador)
Características:
- Hackathon recién creado
- No visible para PARTICIPANTs
- Solo visible para ORGANIZER y ADMIN
Acciones Permitidas:
- ✅ Configurar información básica
- ✅ Configurar fechas
- ✅ Crear criterios de evaluación
- ✅ Asignar jueces
- ✅ Editar cualquier campo
Transición:
- Manual: ORGANIZER publica cuando esté listo
- Condiciones:
now >= registrationOpensAt- Tiene al menos 1 criterio
- Tiene al menos 1 juez asignado
- Orden de fechas válido
2. REGISTRATION (Registro Abierto)
Características:
- Visible para todos (público)
- PARTICIPANTs pueden registrarse
- PARTICIPANTs pueden formar equipos
Acciones Permitidas:
- ✅ PARTICIPANTs: Registrarse
- ✅ PARTICIPANTs: Crear/unirse a equipos
- ✅ ORGANIZER: Monitorear, gestionar participantes
- ✅ ORGANIZER: Extender fechas de fase siguiente
Transición Automática:
- Trigger:
now >= registrationClosesAt - Cron Job: Ejecuta cada minuto
- Resultado:
REGISTRATION → RUNNING
3. RUNNING (En Ejecución)
Características:
- Hackathon en curso
- PARTICIPANTs trabajando en proyectos
- Submissions pueden enviarse/editarse
Acciones Permitidas:
- ✅ PARTICIPANTs: Enviar submissions
- ✅ PARTICIPANTs: Editar submissions (hasta
submissionDeadline) - ✅ PARTICIPANTs: Crear/unirse/salir de equipos (hasta
submissionDeadline) - ✅ ORGANIZER: Monitorear, extender fechas
Restricciones:
- ❌ No se pueden crear nuevos equipos después de
submissionDeadline - ❌ No se pueden editar submissions después de
submissionDeadline
Transición Automática:
- Trigger:
now >= judgingStartsAt - Cron Job: Ejecuta cada minuto
- Resultado:
RUNNING → JUDGING
4. JUDGING (Evaluación)
Características:
- Fase de evaluación
- JUDGEs evalúan submissions
- PARTICIPANTs solo lectura
Acciones Permitidas:
- ✅ JUDGEs: Ver submissions asignados
- ✅ JUDGEs: Evaluar proyectos (asignar scores)
- ✅ ORGANIZER: Monitorear progreso
- ✅ PARTICIPANTs: Ver sus submissions (solo lectura)
Restricciones:
- ❌ PARTICIPANTs: No pueden editar submissions
- ❌ JUDGEs: No pueden ver scores de otros (hasta FINISHED)
- ❌ JUDGEs: No pueden editar scores enviados
Transición Automática:
- Trigger:
now >= judgingEndsAt - Cron Job: Ejecuta cada minuto
- Resultado:
JUDGING → FINISHED
5. FINISHED (Finalizado)
Características:
- Hackathon completado
- Leaderboard final visible
- Todos los scores visibles
Acciones Permitidas:
- ✅ Todos: Ver leaderboard completo
- ✅ Todos: Ver scores de todos los jueces
- ✅ SPONSORs: Ver ganadores de challenges
- ✅ PARTICIPANTs: Ver feedback completo
Restricciones:
- ❌ No se pueden hacer cambios
- ❌ Solo lectura
Orden de Fechas (Validación Estricta)
Orden Requerido:
registrationOpensAt < registrationClosesAt < startsAt <
submissionDeadline < judgingStartsAt < judgingEndsAt
Validación:
export function validateHackathonDates(dates: HackathonDates): void {
if (dates.registrationOpensAt >= dates.registrationClosesAt) {
throw new Error('registrationOpensAt debe ser anterior a registrationClosesAt');
}
if (dates.registrationClosesAt >= dates.startsAt) {
throw new Error('registrationClosesAt debe ser anterior a startsAt');
}
if (dates.startsAt >= dates.submissionDeadline) {
throw new Error('startsAt debe ser anterior a submissionDeadline');
}
if (dates.submissionDeadline >= dates.judgingStartsAt) {
throw new Error('submissionDeadline debe ser anterior a judgingStartsAt');
}
if (dates.judgingStartsAt >= dates.judgingEndsAt) {
throw new Error('judgingStartsAt debe ser anterior a judgingEndsAt');
}
}
Transiciones Automáticas (Cron Job)
Implementación
Archivo: src/app/api/cron/update-hackathon-states/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/core/db';
import { HackathonStatus } from '@prisma/client';
export async function GET(request: NextRequest) {
// Validar cron secret
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const now = new Date();
const updates = {
regToRunning: 0,
runningToJudging: 0,
judgingToFinished: 0,
};
try {
// REGISTRATION → RUNNING
const regToRunning = await db.hackathon.updateMany({
where: {
status: HackathonStatus.REGISTRATION,
registrationClosesAt: { lte: now },
},
data: { status: HackathonStatus.RUNNING },
});
updates.regToRunning = regToRunning.count;
// RUNNING → JUDGING
const runningToJudging = await db.hackathon.updateMany({
where: {
status: HackathonStatus.RUNNING,
judgingStartsAt: { lte: now },
},
data: { status: HackathonStatus.JUDGING },
});
updates.runningToJudging = runningToJudging.count;
// JUDGING → FINISHED
const judgingToFinished = await db.hackathon.updateMany({
where: {
status: HackathonStatus.JUDGING,
judgingEndsAt: { lte: now },
},
data: { status: HackathonStatus.FINISHED },
});
updates.judgingToFinished = judgingToFinished.count;
return NextResponse.json({
success: true,
timestamp: now.toISOString(),
updates,
});
} catch (error) {
console.error('[Cron Error]', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Configuración en Vercel
Archivo: vercel.json
{
"crons": [
{
"path": "/api/cron/update-hackathon-states",
"schedule": "* * * * *"
}
]
}
Frecuencia: Cada minuto (* * * * *)
Reglas del Cron
- ✅ Solo procesa hackathons publicados (
status != DRAFT) - ❌ NO publica hackathons en DRAFT (publicación es manual)
- ✅ Ejecuta transiciones automáticas basadas en fechas
- ✅ No afecta
submissionDeadline(solo bloquea edición, no cambia estado)
Publicación Manual
Proceso de Publicación
Validaciones de Publicación
export async function publishHackathon(hackathonId: string) {
const user = await getCurrentUser();
requireRole(user, ['ORGANIZER', 'ADMIN']);
const hackathon = await db.hackathon.findUnique({
where: { id: hackathonId },
include: {
criteria: true,
judges: true,
},
});
if (!hackathon) {
throw new Error('Hackathon no encontrado');
}
if (hackathon.status !== HackathonStatus.DRAFT) {
throw new Error('Solo se pueden publicar hackathons en DRAFT');
}
// Validar criterios
if (hackathon.criteria.length === 0) {
throw new Error('Debe tener al menos un criterio de evaluación');
}
// Validar jueces
if (hackathon.judges.length === 0) {
throw new Error('Debe tener al menos un juez asignado');
}
// Validar fecha de apertura
const now = new Date();
if (now < hackathon.registrationOpensAt) {
throw new Error('La fecha de apertura de registro no ha llegado');
}
// Validar orden de fechas
validateHackathonDates({
registrationOpensAt: hackathon.registrationOpensAt,
registrationClosesAt: hackathon.registrationClosesAt,
startsAt: hackathon.startsAt,
endsAt: hackathon.endsAt,
submissionDeadline: hackathon.submissionDeadline,
judgingStartsAt: hackathon.judgingStartsAt,
judgingEndsAt: hackathon.judgingEndsAt,
});
// Publicar
await db.hackathon.update({
where: { id: hackathonId },
data: { status: HackathonStatus.REGISTRATION },
});
return { success: true };
}
Extensión de Fechas
Reglas de Extensión
ORGANIZER puede alargar fechas con restricciones:
- Solo fase SIGUIENTE: No puede modificar la fase actual
- Máximo +7 días: Extensión limitada
- Solo alargar: No puede acortar fechas
- Mantener orden: Debe preservar el orden de fechas
Matriz de Extensiones Permitidas
| Estado Actual | Puede Extender |
|---|---|
| REGISTRATION | startsAt, submissionDeadline, judgingStartsAt, judgingEndsAt |
| RUNNING | submissionDeadline, judgingStartsAt, judgingEndsAt |
| JUDGING | judgingEndsAt |
Ejemplo de Extensión
// Hackathon en estado REGISTRATION
// ORGANIZER quiere extender submissionDeadline +3 días
const newDeadline = new Date(currentDeadline);
newDeadline.setDate(newDeadline.getDate() + 3);
// Validar extensión
validateDateExtension(
'REGISTRATION', // Estado actual
currentDates,
{ submissionDeadline: newDeadline }
);
// Actualizar
await db.hackathon.update({
where: { id: hackathonId },
data: { submissionDeadline: newDeadline },
});
submissionDeadline: Fecha Crítica
Propósito
submissionDeadline es una fecha especial que:
- ✅ NO cambia el estado del hackathon
- ✅ Bloquea edición de submissions
- ✅ Bloquea cambios en equipos (crear/unirse/salir)
- ✅ Solo afecta validaciones en Server Actions
Validaciones Basadas en submissionDeadline
// Al editar submission
export async function updateSubmission(submissionId: string, data: UpdateSubmissionInput) {
const submission = await db.submission.findUnique({
where: { id: submissionId },
include: { hackathon: true },
});
const now = new Date();
// Validar deadline
if (now >= submission.hackathon.submissionDeadline) {
throw new Error('La fecha límite para editar submissions ha pasado');
}
// Continuar con actualización...
}
Diagrama de Secuencia: Transición Completa
Próximos Pasos
- Team Formation - Formación de equipos
- Evaluation System - Sistema de evaluación
- Leaderboard Calculation - Cálculo de leaderboard
Siguiente: Team Formation