Skip to content

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

FromTo permitidos
borradoractiva, cancelada
activaen_busqueda, cancelada
en_busquedacon_candidatos_asignados, cancelada
con_candidatos_asignadosentrevistas_en_curso, cancelada
entrevistas_en_cursofinalista_seleccionado, cancelada
finalista_seleccionadocubierta, cancelada
cubierta(ninguna, terminal)
cancelada(ninguna, terminal)

Significado de cada estado

EstadoDescripción
borradorCompany_user está llenando la vacante, aún no publicada
activaPublicada; visible a recruiters HUMAE
en_busquedaRecruiter asignado activamente buscando candidatos
con_candidatos_asignadosHay ≥ 1 VacancyAssignment en la vacante
entrevistas_en_cursoAl menos una entrevista agendada
finalista_seleccionadoHay al menos 1 asignación en stage finalist
cubiertaContratación completada; vacante cerrada exitosamente
canceladaCerrada 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

FromTo permitidos
sourcedpresented, rejected, withdrawn
presentedsourced, interviewing, rejected, withdrawn
interviewingpresented, finalist, rejected, withdrawn
finalistinterviewing, hired, rejected, withdrawn
hired, rejected, withdrawn(terminales)

Timestamps

Cada transición guarda un timestamp específico:

StageCampoSeteo
sourced(initial)
presentedpresented_atnow()
interviewinginterviewed_atnow()
finalist(sin timestamp propio)
hiredhired_atnow()
rejectedrejected_atnow()
withdrawnwithdrawn_atnow()

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

StageQuién
sourcedRecruiter acaba de asignar al candidato; aún no se muestra a la empresa
presentedRecruiter envía formalmente al candidato a la empresa
interviewingHay entrevistas agendadas/en curso
finalistEntrevistas concluidas, finalista pendiente de confirmar
hiredContratado
rejectedDescartado (con rejection_reason)
withdrawnRetirado 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

FromTo permitidos
propuestaconfirmada, reprogramada, cancelada
confirmadarealizada, reprogramada, cancelada, no_asisto
reprogramadapropuesta (con nueva fecha), cancelada
realizada(terminal)
cancelada(terminal)
no_asisto(terminal)

Significado

EstadoQuién puede moverla ahí
propuestaRecruiter agenda
confirmadaCualquiera de las partes confirma
reprogramadaHay nueva fecha pendiente de confirmar
realizadaRecruiter marca tras finalizar + feedback
canceladaCualquiera cancela
no_asistoRecruiter 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:

EstadoCuándo se usa
sin_perfilUsuario recién registrado, no tocó el perfil
en_registroPerfil en progreso
activoPerfil completo, membresía activa, aparece en directorio
en_procesoTiene asignaciones activas
presentado_empresaHay al menos 1 asignación en presented
entrevistadoTiene entrevistas confirmada o realizada
contratadoAsignación hired
rechazadoMuchos rechazos consecutivos (opcional, manual)
retiradoCandidato decidió pausar
inactivoMembresí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.

ValorCuándo se usa
activeCuenta operativa. Login OK si además email_verified_at no es nulo.
pending_approvalCuenta auto-registrada (reclutador o empresa) esperando que un admin la apruebe. Login devuelve 403 errors.code=pending_approval.
suspendedBloqueado manualmente por un admin (vía /admin/usuarios). Login devuelve 403 errors.code=account_inactive.
inactiveCuenta 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.

ValorEtiqueta UI
employeeEmpleado
internPracticante

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.

ValorEtiqueta UIComportamiento en matching
employeeEmpleadoexige candidate_kind = employee (25 pts en eje categoría si match)
internPracticanteexige candidate_kind = intern (25 pts si match)
anyCualquiera (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 →

Manual de usuario HUMAE · Uso interno