Saltar al contenido principal

🔄 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:

  1. Solo fase SIGUIENTE: No puede modificar la fase actual
  2. Máximo +7 días: Extensión limitada
  3. Solo alargar: No puede acortar fechas
  4. Mantener orden: Debe preservar el orden de fechas

Matriz de Extensiones Permitidas

Estado ActualPuede Extender
REGISTRATIONstartsAt, submissionDeadline, judgingStartsAt, judgingEndsAt
RUNNINGsubmissionDeadline, judgingStartsAt, judgingEndsAt
JUDGINGjudgingEndsAt

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


Siguiente: Team Formation