Skip to content

Flujo del reclutador

15 casos ejecutables: directorio, pipeline kanban, asignaciones, entrevistas, favoritos, cierre de vacante con auto-retiros. Ejecuta estos tests con el user reclutador creado en Plan general.

Duración estimada: 45 minutos.

Directorio de talento

TC-REC-001 · Acceder al directorio

Severidad: 🔴 Crítica

Precondiciones:

  • Recruiter logueado
  • Al menos 3 candidatos con membresía activa (ejecutar TC-CAND-009 × 3 si no existen)

Pasos:

  1. Nav → "Reclutamiento" → "Directorio" (/recruiter/directorio)

Resultado esperado:

  • Lista de candidatos con membresía activa (por default).
  • Cada card muestra: foto, nombre, headline, ubicación, años exp, skills principales, rango salarial.
  • Sidebar de filtros visible.

Variaciones:

  • Acceder como candidatoAuthGuard bloquea y redirige.
  • Sin candidatos que cumplan filtros → empty state.

TC-REC-002 · Filtrar por skill único

Severidad: 🟠 Alta

Pasos:

  1. Sidebar → "Habilidades" → buscar "React" → marcar el checkbox.

Resultado esperado:

  • Lista se restringe a candidatos que tengan React.
  • URL refleja el filtro: ?skills[]=X.

TC-REC-003 · Filtrar por múltiples skills (AND)

Severidad: 🟠 Alta

Pasos:

  1. Marcar skills: React + TypeScript + AWS.

Resultado esperado: Solo candidatos con las 3 skills (AND semántico en backend vía whereHas múltiple).


TC-REC-004 · Filtrar por salario máximo

Severidad: 🟠 Alta

Pasos:

  1. Campo "Presupuesto máximo (MXN/mes)" = 40000.

Resultado esperado:

  • Aparecen candidatos con expected_salary_min ≤ 40000 o expected_salary_min = null.

TC-REC-005 · Búsqueda de texto (q)

Severidad: 🟡 Media

Pasos:

  1. Input "Buscar" → escribir "backend" → enter.

Resultado esperado: Candidatos cuyo first_name, last_name, headline o summary contengan "backend".


TC-REC-005b · Filtrar por categoría empleado/practicante

Severidad: 🔴 Crítica (PDF cosasfaltanteshumae punto 2)

Precondiciones: Correr php artisan db:seed --class=PdfDemoSeeder para tener 3 empleados + 2 practicantes demo, y cualquiera con membresía activa.

Pasos:

  1. /recruiter/directorio.
  2. En el sidebar, sección "Categoría" → click "Practicante".

Resultado esperado:

  • Query string contiene ?candidate_kind=intern.
  • La lista muestra solo a Pablo y María (los 2 practicantes del seed).
  • Cada card muestra badge ámbar Practicante.
  • Click en "Empleado" → muestra Juan, Sofía y Lucía con badge color marca.
  • Click en "Todos" → vuelve a 5 candidatos.

Variaciones:

  • Combinar con otros filtros (skill + categoría) → ambos aplican como AND a nivel de servicio.
  • Candidato sin candidate_kind → no aparece bajo ningún filtro distinto de "Todos".

TC-REC-005c · Filtrar por áreas de interés (multi-select OR)

Severidad: 🔴 Crítica (PDF cosasfaltanteshumae punto 1)

Precondiciones: Demo seeder corrido. Catálogo con áreas Producción, Calidad, Sistemas, Logística, Almacén, RH.

Pasos:

  1. Sidebar → expandir el bloque "Áreas de interés".
  2. Marcar el checkbox de Sistemas.

Resultado esperado: Aparece solo María Torres (única con Sistemas como principal).

  1. Marcar también Producción y Calidad.

Resultado esperado:

  • Query string ?functional_area_ids[]=...&functional_area_ids[]=...&functional_area_ids[]=....
  • Aparecen María (Sistemas) + Juan (Producción y Calidad) + Pablo (Producción secundaria).
  • En el detalle de cada uno, el card "Áreas de interés" muestra los chips con la principal marcada con ⭐.

Variaciones:

  • Desmarcar todas → vuelve al listado completo.
  • Filtrar por un área que ningún candidato tiene → empty state con "No hay candidatos para los filtros aplicados".

TC-REC-006 · Ver detalle de candidato

Severidad: 🟠 Alta

Pasos:

  1. Clic en una card del directorio.

Resultado esperado:

  • Ruta /recruiter/directorio/{id}.
  • Secciones: datos básicos (nombre, headline, contacto), resumen, experiencias, educación, habilidades, idiomas.
  • Botones: "Asignar a vacante", "Favorito", "Descargar CV".

TC-REC-007 · Agregar a favoritos + ver listado

Severidad: 🟡 Media

Pasos:

  1. Detalle del candidato → botón "Favorito".
  2. Nav → "Reclutamiento" → "Favoritos" (/recruiter/favoritos).

Resultado esperado:

  • POST /api/v1/directory/candidates/{id}/favorite — toggle.
  • Row en directory_favorites con recruiter_id + candidate_profile_id.
  • /recruiter/favoritos lista al candidato con el mismo card del directorio.

Variaciones:

  • Toggle doble → el endpoint alterna.
  • Candidato sin membresía activa → sigue apareciendo en favoritos (el filtro desactiva has_active_membership default).

TC-REC-008 · Descargar CV del candidato

Severidad: 🟠 Alta

Pasos:

  1. Detalle del candidato → "Descargar CV".

Resultado esperado:

  • GET /api/v1/directory/candidates/{id}/cv.pdf → 200.
  • PDF descarga con filename CV_Nombre_Apellido.pdf.

Pipeline — asignaciones

TC-REC-009 · Asignar candidato a vacante

Severidad: 🔴 Crítica

Precondiciones:

  • Candidato activo en el directorio.
  • Vacante en estado activa o derivados (creada por el reclutador o por la empresa).

Pasos:

  1. Desde la card del candidato en el directorio o desde el detalle (/recruiter/directorio/{id}) → botón "Asignar a vacante".
  2. Modal con selector de vacantes activas.
  3. Elegir vacante + prioridad (low / normal / high / urgent) + nota interna opcional.
  4. Confirmar.

Resultado esperado:

  • Toast "Candidato asignado".
  • Row en vacancy_assignments con stage=sourced.
  • Si era la primera asignación: Vacancy.state pasa de activa a con_candidatos_asignados.
  • El candidato aparece en /recruiter/vacantes/{id}/pipeline, columna Sourced.

Variaciones:

  • Asignar candidato ya asignado a la misma vacante → la vacante ya no aparece en el selector (filtro excluding_assigned_candidate_id). Si se forzara desde API, 422.
  • Vacante cubierta o cancelada → no aparece en el selector.

TC-REC-010 · Mover candidato a stage siguiente (sourced → presented)

Severidad: 🔴 Crítica

Pasos:

  1. /recruiter/vacantes/{id}/pipeline
  2. Arrastrar la card desde la columna Identificado hasta la columna Presentado y soltar.

Resultado esperado:

  • VacancyAssignment.stage = presented, presented_at = now().
  • La card aparece en la columna Presentado tras el invalidateQueries.
  • Toast "Movido a Presentado".
  • Mientras se arrastra, las columnas que no están en allowed_transitions se atenúan a 50% opacidad.

Variaciones:

  • Soltar en una columna no permitida (ej. sourcedhired) → toast "Transición no permitida desde esta etapa". Sin llamada al backend.
  • Teclado: Tab a la card, Space para tomarla, flechas para mover entre columnas, Enter para soltar.

TC-REC-011 · Mover a rejected con motivo obligatorio

Severidad: 🟠 Alta

Pasos:

  1. Arrastrar la card a la columna Rechazado.
  2. Se abre modal "Rechazar candidato" con textarea obligatoria (mínimo 3 caracteres).
  3. Escribir motivo → clic "Rechazar".

Resultado esperado:

  • stage = rejected, rejected_at = now(), rejection_reason guardado.
  • Toast "Candidato movido a rechazado".
  • La card aparece en la columna Rechazado (terminal).

Variaciones:

  • Cerrar el modal sin completarlo → la card vuelve visualmente a su columna original (no se commitea nada).
  • Motivo vacío → botón "Rechazar" queda deshabilitado.

TC-REC-011b · Retroceder una etapa por error

Severidad: 🟡 Media

Pasos:

  1. Card en columna Presentado.
  2. Arrastrar la card de regreso a Identificado.

Resultado esperado:

  • stage = sourced. La card aparece en Identificado.
  • presented_at no se resetea (queda como histórico de "primera vez que llegó a presentado").
  • Toast "Movido a Identificado".

Variaciones:

  • Retroceder más de un paso (ej. interviewing → sourced) → toast "Transición no permitida desde esta etapa". Sin llamada al backend.
  • Retroceder desde terminal (hired, rejected, withdrawn) → bloqueado. Para revivir un descarte se elimina la asignación y se crea una nueva.
  • Cerrar sin guardar → la asignación no cambia.

TC-REC-012 · Cerrar vacante (hire final) — auto-withdraw + notificaciones

Severidad: 🔴 Crítica

Precondiciones: Asignación en stage finalist con otras asignaciones activas (sourced|presented|interviewing).

Pasos:

  1. Arrastrar la card desde la columna Finalista hasta la columna Contratado y soltar.

Resultado esperado (HireService transaccional):

  • assignment.stage = hired, hired_at = now().
  • vacancy.state = cubierta, filled_at = now().
  • Otras asignaciones activas pasan a withdrawn con rejection_reason = "Vacante cubierta por otro candidato" y withdrawn_at = now().
  • Notificaciones (database + mail):
    • CandidateHiredNotification al candidato hired.
    • CandidateHiredNotification a owners/managers de la empresa.
    • AssignmentRejectedNotification a cada candidato auto-retirado.
  • Las asignaciones que ya estaban rejected no se tocan.

Variaciones:

  • Asignación no está en finalist (ej. sourced) → la columna Contratado se atenúa al arrastrar y muestra "No permitido"; el drop dispara toast "Transición no permitida desde esta etapa". Vía API → 409.

TC-REC-013 · Agregar nota al pipeline + toggle visible para empresa

Severidad: 🟡 Media

Pasos:

  1. En la card de la asignación → icono mensaje → abre form.
  2. Escribir nota + marcar "Visible para la empresa" → "Guardar nota".
  3. Clic en "Ver notas anteriores" → se expande la lista con badge "Empresa" / "Interna".

Resultado esperado:

  • POST /api/v1/assignments/{id}/notes con visibility=company|internal.
  • Badge correspondiente en la lista.
  • Company_user ve la nota si visibility=company; oculta si internal.

Variaciones:

  • Sin marcar el toggle → nota se guarda como internal y la empresa no la ve.

Sugerencias de candidatos (matching)

TC-REC-013b · Ver candidatos sugeridos para una vacante

Severidad: 🔴 Crítica (PDF cosasfaltanteshumae, "Ajuste en la lógica de matching")

Precondiciones: Demo seeder corrido. Vacante "Practicante de Ingeniería Industrial" (HUM-DEMO-0001) en estado activa.

Pasos:

  1. /recruiter/vacantes/{id}/pipeline para esa vacante.
  2. En el header, click en el botón ✨ Ver sugerencias.

Resultado esperado:

  • Petición GET /api/v1/vacancies/{id}/suggested-candidates.
  • Aparece un panel arriba del kanban con candidatos ordenados por score descendente.
  • Pablo Sánchez (intern · principal Ingeniería · secundaria Producción · 0 años) está primero con score ≈ 95.
  • Cada card muestra: avatar, nombre, badge categoría, chips de áreas (estrella en la principal), barra de score con color según umbral, número del score y botón "+ Asignar".
  • Hover sobre la barra muestra tooltip nativo con el breakdown completo (Categoría: 25 · Áreas: 25 · Educación: 15 · …).

Variaciones:

  • Cambiar filtro de score (botones Todos / ≥50 / ≥70 / ≥85) → solo aparecen los que cumplen el umbral.
  • Click en + Asignar de Pablo → toast "Pablo Sánchez asignada/o a la vacante", el card desaparece de las sugerencias y aparece en la columna sourced del kanban.
  • Refrescar → Pablo ya no vuelve a la lista de sugerencias (excluido por la query whereDoesntHave('assignmentsForVacancy')).
  • Vacante con target_candidate_kind = any → todos los candidatos válidos pasan; el eje "Categoría" da 60 % parcial.
  • Vacante sin functional_area_id → eje "Áreas" da 40 % neutro.
  • Cliente role candidate intentando llamar el endpoint → 403.

TC-REC-013c · Verificar que el matching es determinista

Severidad: 🟠 Alta

Pasos:

  1. Llamar GET /vacancies/{id}/suggested-candidates 3 veces seguidas sin tocar nada.
  2. Comparar data[*].score y data[*].breakdown.

Resultado esperado: Idénticos en cada llamada (mismas reglas, mismos datos, mismo resultado). No hay aleatoriedad.


Entrevistas — gestión

TC-REC-014 · Agendar entrevista

Severidad: 🔴 Crítica

Precondiciones: Asignación en stage presented o interviewing.

Pasos:

  1. En la card → icono calendario → dialog "Proponer entrevista".
  2. Fecha+hora (scheduled_at), modo (online / presencial / telefonica).
    • Si online: meeting_url.
    • Si presencial: location.
  3. Enviar.

Resultado esperado:

  • POST /api/v1/interviews → 201.
  • Interview.state = propuesta, round = 1.
  • Notificaciones: candidato, owners/managers de la empresa, recruiter asignado.

Variaciones:

  • scheduled_at en el pasado → 422.
  • mode=online sin meeting_url → 422.
  • Vacante en activa / borrador → 409 "La vacante no está en un estado que admita entrevistas". Recordatorio: la vacante avanza automáticamente a con_candidatos_asignados apenas se crea la primera asignación (ver PipelineService::assign); si ves este 409 en producción, revisa que la asignación se haya creado correctamente.

TC-REC-014b · Confirmar entrevista propuesta

Severidad: 🔴 Crítica

Precondiciones: Entrevista en estado propuesta.

Pasos:

  1. /me/entrevistas → card de la entrevista → botón "Confirmar".

Resultado esperado:

  • POST /api/v1/interviews/{id}/confirm → 200.
  • Interview.state = confirmada.
  • InterviewConfirmedNotification (database + mail) al candidato, owners/managers de la empresa, y al recruiter de la asignación.
  • La card cambia su badge de "Propuesta" a "Confirmada".

Variaciones:

  • Entrevista ya en confirmada → idempotente: el service devuelve la misma entrevista sin cambios ni notificación duplicada.
  • Entrevista en estado terminal (realizada, cancelada, no_asisto) → 409 "La entrevista no puede confirmarse en este estado".
  • Confirma el company_user → permitido (ambos lados pueden confirmar lo agendado por el otro).
  • Confirma un usuario sin acceso a la asignación → 403.

TC-REC-014c · Cancelar entrevista (con motivo opcional)

Severidad: 🟠 Alta

Precondiciones: Entrevista en propuesta o confirmada.

Pasos:

  1. /me/entrevistas → card de la entrevista → botón "Cancelar".
  2. Prompt de motivo (opcional, máx 500 chars).
  3. Confirmar.

Resultado esperado:

  • POST /api/v1/interviews/{id}/cancel con { "reason": "…" } → 200.
  • Interview.state = cancelada (terminal).
  • Si se pasó reason: se appendea a recruiter_feedback con prefijo [cancelado].
  • InterviewCancelledNotification (database + mail) al candidato, owners/managers de la empresa, y al recruiter de la asignación. El motivo viaja en la notificación.

Variaciones:

  • Sin motivo → la cancelación se acepta; la notificación va sin reason.
  • Motivo > 500 chars → 422.
  • Entrevista en realizada / cancelada / no_asisto → 409 "La entrevista ya no puede cancelarse".
  • Cancelar entrevista que ya tenía feedback → el [cancelado] motivo se appendea preservando el feedback existente.

TC-REC-015 · Marcar entrevista como realizada con feedback

Severidad: 🟠 Alta

Precondiciones: Entrevista en confirmada.

Pasos:

  1. /me/entrevistas → card de la entrevista → botón "Marcar realizada".
  2. Dialog "Cerrar entrevista" con:
    • Textarea de feedback (obligatorio, ≥ 5 chars).
    • Select de recomendación: advance / hold / reject.
    • Input opcional de rating 0–10.
  3. "Guardar y cerrar".

Resultado esperado:

  • POST /api/v1/interviews/{id}/complete → 200.
  • state = realizada (terminal), recruiter_feedback, recommendation, rating persistidos.

Variaciones:

  • Feedback vacío o < 5 chars → botón deshabilitado / 422.
  • Recomendación no en enum → 422.
  • Entrevista en propuesta (no confirmada) → 409 "no puede marcarse como realizada".
  • Company_user intenta → 403.

TC-REC-016 · Ver feedback de la entrevista realizada

Severidad: 🟡 Media

Precondiciones: Entrevista en realizada con recruiter_feedback, recommendation y opcionalmente rating ya capturados.

Pasos:

  1. /me/entrevistas → tab "Historial" o filtro de estado realizada.
  2. Card de la entrevista → click en la sección "Resultado de la entrevista".

Resultado esperado:

  • La sección expande mostrando:
    • Badge de recomendación (verde "Avanzar", ámbar "Mantener en evaluación", rojo "Rechazar").
    • Rating con icono de estrella (X/10) cuando existe.
    • Texto íntegro de recruiter_feedback (preserva saltos de línea).
    • Texto íntegro de company_feedback cuando lo hay.
  • La sección se inicializa expandida automáticamente cuando la entrevista está en realizada.

Variaciones:

  • Sin feedback escrito (sólo recomendación + rating) → la sección sigue visible mostrando la metadata; bajo el divisor aparece "Aún no hay feedback escrito".
  • Candidato logueado en su propia entrevista → los campos recruiter_feedback, company_feedback, recommendation y rating no se devuelven en el JSON (gated en InterviewResource por rol). La sección no se renderiza.
  • Entrevista en cancelada con motivo → el [cancelado] motivo queda anexado al recruiter_feedback y se muestra ahí, útil para auditoría.

TC-REC-017 · Filtrar listado de entrevistas

Severidad: 🟡 Media

Precondiciones: Recruiter con ≥ 5 entrevistas en distintos estados y rangos de fecha.

Pasos:

  1. /me/entrevistas.
  2. Tab "Historial" → muestra sólo realizada / cancelada / no_asisto.
  3. Input "Buscar" → escribir parte del nombre del candidato → la lista se reduce client-side.
  4. Select "Estado" → "Propuesta" → llama al endpoint con ?state=propuesta.
  5. Inputs "Desde" / "Hasta" → seleccionar rango → llama al endpoint con ?from=…&to=….
  6. Botón "Limpiar filtros" → reset.

Resultado esperado:

  • Cada cambio dispara una nueva query (key [\"interviews\", filtros]).
  • El contador (N resultados) aparece cuando hay algún filtro activo.
  • Empty state contextual: "No hay entrevistas que coincidan con tus filtros" + CTA "Limpiar filtros".

Variaciones:

  • Tab "Todas" sin filtros → trae los 30 primeros (paginación backend default).
  • from > to → backend acepta y devuelve lista vacía (no validación cruzada en MVP).

Siguiente

Flujo de la empresa →

Manual de usuario HUMAE · Uso interno