Apariencia
Máquinas de estado
Documentación consolidada de los tres FSM (finite-state machines) que rigen la plataforma. Toda transición pasa por validación del servicio correspondiente.
Vacancy
Enum: App\Enums\VacancyStateServicio: App\Services\VacancyStateMachine
borrador
│
▼
activa
│
▼
en_busqueda
│
▼
con_candidatos_asignados
│
▼
entrevistas_en_curso
│
▼
finalista_seleccionado
│
▼
cubierta (terminal)
[Desde cualquier no-terminal] → cancelada (terminal)Transiciones permitidas
| From | To permitidos |
|---|---|
borrador | activa, cancelada |
activa | en_busqueda, cancelada |
en_busqueda | con_candidatos_asignados, cancelada |
con_candidatos_asignados | entrevistas_en_curso, cancelada |
entrevistas_en_curso | finalista_seleccionado, cancelada |
finalista_seleccionado | cubierta, cancelada |
cubierta | (ninguna, terminal) |
cancelada | (ninguna, terminal) |
Significado de cada estado
| Estado | Descripción |
|---|---|
borrador | Company_user está llenando la vacante, aún no publicada |
activa | Publicada; visible a recruiters HUMAE |
en_busqueda | Recruiter asignado activamente buscando candidatos |
con_candidatos_asignados | Hay ≥ 1 VacancyAssignment en la vacante |
entrevistas_en_curso | Al menos una entrevista agendada |
finalista_seleccionado | Hay al menos 1 asignación en stage finalist |
cubierta | Contratación completada; vacante cerrada exitosamente |
cancelada | Cerrada sin contratación |
VacancyAssignment
Enum: App\Enums\AssignmentStageServicio: App\Services\AssignmentStageMachine
sourced ⇄ presented ⇄ interviewing ⇄ finalist ──▶ hired (terminal)
│ │ │ │
└─────────────┴──────────────┴──────────────┴───▶ [rejected | withdrawn] (terminales)Las flechas dobles indican que se puede retroceder un paso entre etapas activas para corregir movimientos erróneos. No se puede retroceder más de un paso ni desde una terminal.
Transiciones
| From | To permitidos |
|---|---|
sourced | presented, rejected, withdrawn |
presented | sourced, interviewing, rejected, withdrawn |
interviewing | presented, finalist, rejected, withdrawn |
finalist | interviewing, hired, rejected, withdrawn |
hired, rejected, withdrawn | (terminales) |
Timestamps
Cada transición guarda un timestamp específico:
| Stage | Campo | Seteo |
|---|---|---|
sourced | — | (initial) |
presented | presented_at | now() |
interviewing | interviewed_at | now() |
finalist | — | (sin timestamp propio) |
hired | hired_at | now() |
rejected | rejected_at | now() |
withdrawn | withdrawn_at | now() |
Implementado en AssignmentStageMachine::timestampField().
Retroceder no resetea timestamps
Cuando una asignación retrocede (ej. presented → sourced), los timestamps ya guardados se preservan. presented_at significa "primera vez que llegó a presentado", aunque la asignación esté ahora de vuelta en sourced. Esto mantiene la auditoría intacta.
Significado
| Stage | Quién |
|---|---|
sourced | Recruiter acaba de asignar al candidato; aún no se muestra a la empresa |
presented | Recruiter envía formalmente al candidato a la empresa |
interviewing | Hay entrevistas agendadas/en curso |
finalist | Entrevistas concluidas, finalista pendiente de confirmar |
hired | Contratado |
rejected | Descartado (con rejection_reason) |
withdrawn | Retirado voluntariamente o automáticamente |
Interview
Enum: App\Enums\InterviewStateServicio: App\Services\InterviewStateMachine
propuesta ──┬──▶ confirmada ──┬──▶ realizada (terminal)
│ │
│ ├──▶ no_asisto (terminal)
│ │
│ ├──▶ reprogramada
│ │
│ └──▶ cancelada (terminal)
│
├──▶ reprogramada
│ │
│ └──▶ propuesta (loop con nueva fecha)
│
└──▶ cancelada (terminal)Transiciones
| From | To permitidos |
|---|---|
propuesta | confirmada, reprogramada, cancelada |
confirmada | realizada, reprogramada, cancelada, no_asisto |
reprogramada | propuesta (con nueva fecha), cancelada |
realizada | (terminal) |
cancelada | (terminal) |
no_asisto | (terminal) |
Significado
| Estado | Quién puede moverla ahí |
|---|---|
propuesta | Recruiter agenda |
confirmada | Cualquiera de las partes confirma |
reprogramada | Hay nueva fecha pendiente de confirmar |
realizada | Recruiter marca tras finalizar + feedback |
cancelada | Cualquiera cancela |
no_asisto | Recruiter marca si el candidato no llegó |
Candidate
Enum: App\Enums\CandidateState
El estado del candidato no usa FSM estricta (es informativo; varios flujos lo mueven). Valores:
| Estado | Cuándo se usa |
|---|---|
sin_perfil | Usuario recién registrado, no tocó el perfil |
en_registro | Perfil en progreso |
activo | Perfil completo, membresía activa, aparece en directorio |
en_proceso | Tiene asignaciones activas |
presentado_empresa | Hay al menos 1 asignación en presented |
entrevistado | Tiene entrevistas confirmada o realizada |
contratado | Asignación hired |
rechazado | Muchos rechazos consecutivos (opcional, manual) |
retirado | Candidato decidió pausar |
inactivo | Membresía expirada |
El frontend filtra visibilidad en directorio con los estados: {activo, en_proceso, presentado_empresa, entrevistado}.
Estado del usuario (UserStatus)
Enum: App\Enums\UserStatus
Atributo de cuenta — no FSM. Define si un User puede iniciar sesión y aparecer como operativo. Se persiste en users.status.
| Valor | Cuándo se usa |
|---|---|
active | Cuenta operativa. Login OK si además email_verified_at no es nulo. |
pending_approval | Cuenta auto-registrada (reclutador o empresa) esperando que un admin la apruebe. Login devuelve 403 errors.code=pending_approval. |
suspended | Bloqueado manualmente por un admin (vía /admin/usuarios). Login devuelve 403 errors.code=account_inactive. |
inactive | Cuenta dada de baja o rechazada por un admin tras un auto-registro. Login devuelve 403 errors.code=account_inactive. |
Los candidatos creados por POST /auth/register quedan directamente en active (sólo necesitan verify-email). Los reclutadores y empresas creados por POST /auth/register/recruiter y POST /auth/register/company quedan en pending_approval hasta que un admin los aprueba o rechaza desde Gestión de usuarios →.
Categoría del candidato (CandidateKind)
Enum: App\Enums\CandidateKind
No es un estado de FSM, sino un atributo categórico que el candidato declara en su perfil (PDF cosasfaltanteshumae punto 2). Se persiste en candidate_profiles.candidate_kind.
| Valor | Etiqueta UI |
|---|---|
employee | Empleado |
intern | Practicante |
Si el candidato no marcó nada, queda null y no pasa los filtros estrictos por categoría del directorio.
Tipo de candidato requerido (VacancyTargetKind)
Enum: App\Enums\VacancyTargetKind
Atributo categórico de la vacante (PDF cosasfaltanteshumae · clasificación de vacantes). Se persiste en vacancies.target_candidate_kind con default any.
| Valor | Etiqueta UI | Comportamiento en matching |
|---|---|---|
employee | Empleado | exige candidate_kind = employee (25 pts en eje categoría si match) |
intern | Practicante | exige candidate_kind = intern (25 pts si match) |
any | Cualquiera (default) | acepta ambos; 60 % parcial (15 pts) en eje categoría |
Detalles del scoring: MatchingService.
Implementación de una FSM
Cada servicio tiene la misma firma:
php
class AssignmentStageMachine
{
public static function graph(): array { ... }
public static function allowedFrom(Stage $from): array { ... }
public static function canTransition(Stage $from, Stage $to): bool { ... }
public static function allowedValuesFrom(Stage $from): array { ... }
}Uso en controllers
php
$newStage = AssignmentStage::from($request->input('stage'));
$current = $assignment->stage;
if (! AssignmentStageMachine::canTransition($current, $newStage)) {
return $this->error('Transición no permitida', status: 422);
}Uso en frontend
El backend expone allowed_transitions en el recurso para que el frontend sepa qué botones mostrar:
json
{
"id": 42,
"stage": "presented",
"allowed_transitions": ["interviewing", "rejected", "withdrawn"]
}El frontend solo renderiza esos botones. Si se envía otro, el backend rechaza.
Tests
Cada FSM tiene tests unitarios en tests/Unit/Services/*StateMachineTest.php:
- Happy path completo
- Rejection / withdrawal desde todas las etapas
- Bloqueo de saltos de etapa
- Terminal states no tienen salidas
Siguiente
Permisos y autorización consolidados: Matriz de roles y permisos →

