Skip to content

Publicar una vacante

Una Vacante (Vacancy) es el documento que describe qué puesto busca llenar la empresa, con toda la info que necesita HUMAE para curar candidatos.

Nota de nomenclatura

Internamente en el código, el modelo se llama Vacancy, no Job. Esto evita una colisión de nombres con la tabla jobs del queue driver de Laravel.

Modelo

Vacancy
├─ company_id              FK Company
├─ title                   string (required)
├─ description             text (markdown)
├─ responsibilities        text
├─ requirements            text
├─ benefits                text
├─ vacancy_category_id     FK (catálogo)
├─ target_candidate_kind   enum 'employee' | 'intern' | 'any'  (default 'any')
├─ vacancy_type_id         FK (tiempo completo, medio tiempo, freelance, ...)
├─ vacancy_shift_id        FK (diurno, nocturno, mixto, ...)
├─ priority                Priority enum
├─ state                   VacancyState enum
├─ country_id, state_id, city_id, address, is_remote, is_hybrid
├─ salary_currency_id, salary_min, salary_max, salary_period
├─ years_exp_min, years_exp_max
├─ career_level_id, functional_area_id, position_id
├─ education_level_required FK degree_levels
├─ positions_count         int (cuántas plazas)
├─ published_at            timestamp
├─ expires_at              timestamp
├─ assigned_recruiter_id   FK User (recruiter asignado)
├─ created_at / updated_at / deleted_at

Estados de la vacante

  borrador  ─▶ activa  ─▶ en_busqueda  ─▶ con_candidatos_asignados


                                          entrevistas_en_curso


                                          finalista_seleccionado


                                               cubierta (terminal)

  Desde cualquier estado no terminal → cancelada (terminal)

Definido en App\Services\VacancyStateMachine.

Crear una vacante

URL: /me/empresa/vacantes/crearEndpoint: POST /api/v1/me/company/vacancies (alias para company_user) o POST /api/v1/vacancies (recruiter/admin)

Body ejemplo:

json
{
  "title": "Backend Engineer Senior",
  "description": "Buscamos a alguien con experiencia en...",
  "responsibilities": "- Diseñar y mantener APIs REST\n- ...",
  "requirements": "- 5+ años con Node.js\n- Experiencia con PostgreSQL\n- Inglés B2+",
  "benefits": "- Remoto\n- Seguro médico mayor\n- ...",
  "vacancy_category_id": 3,
  "target_candidate_kind": "intern",
  "vacancy_type_id": 1,
  "vacancy_shift_id": 1,
  "priority": "high",
  "country_id": 32,
  "state_id": 9,
  "city_id": 101,
  "is_remote": false,
  "is_hybrid": true,
  "salary_currency_id": 1,
  "salary_min": 60000,
  "salary_max": 85000,
  "salary_period": "mensual",
  "years_exp_min": 3,
  "years_exp_max": 8,
  "career_level_id": 4,
  "functional_area_id": 3,
  "position_id": 17,
  "education_level_required": 3,
  "positions_count": 1,
  "expires_at": "2026-07-01T00:00:00Z"
}

Validación (VacancyRequest):

  • title requerido, 1–200 chars.
  • description requerido, 1–10000.
  • FKs verificadas contra catálogos activos.
  • target_candidate_kind debe ser uno de employee, intern, any (default any). Valores fuera del enum responden 422.
  • salary_min <= salary_max.
  • min_years_of_experience <= max_years_of_experience.
  • closes_at > today si se provee.

Errores de validación en el form

El VacancyForm del frontend hace surface field-level de los errores 422 del backend: cuando recibe errors: { closes_at: ["..."] }, llama a form.setError("closes_at", ...) por cada campo y dispara un toast con el primer mensaje. Si la validación de Zod falla en el cliente antes de mandar (ej. salary_max < salary_min), un toast "Hay campos por corregir" resume el primer error. Las traducciones al español de Laravel viven en humae_backend/lang/es/validation.php (incluye los attributes con los nombres bonitos de cada campo).

Al crear:

  • Estado inicial: borrador.
  • assigned_recruiter_id se queda null hasta que un admin asigne.
  • Cada vacante arranca con al menos 1 cambio en el log.

Tipo de candidato requerido (empleado, practicante o cualquiera)

El campo target_candidate_kind es el segundo punto del PDF cosasfaltanteshumae: la vacante debe poder declarar si busca un empleado, un practicante, o le da igual. Tres valores:

ValorSignificado
employeeSolo candidatos que se registraron como Empleado.
internSolo candidatos que se registraron como Practicante.
any (default)Acepta ambos; el matching da puntaje parcial al eje "Categoría".

En la UI (form de vacante en /me/empresa/vacantes/crear o edición):

Tipo de candidato requerido          [ Cualquiera ▾ ]

Define si la vacante busca un empleado, un practicante, o
cualquiera de los dos. HUMAE usará este campo para filtrar
candidatos compatibles.

Cómo afecta:

  • En el detalle de la vacante (/me/empresa/vacantes/{id}) aparece un badge junto al estado: Empleado o Practicante (color marca / ámbar). Cualquiera no muestra badge.
  • En el panel de Sugerencias del pipeline aporta 25 puntos al score si match exacto, 15 si la vacante es any, 0 si discrepa.
  • El recruiter lo respeta también al asignar manualmente: si arrastra un Empleado al pipeline de una vacante intern, no hay error técnico (no es un constraint duro), pero el sistema lo señala con score 0 en categoría.

Anexos: skills y languages requeridos

Después de crear la vacante, se le agregan skills e idiomas con sus niveles requeridos:

POST /vacancies/{id}/skills
{ "skill_id": 5, "level": "avanzado", "is_required": true }
POST /vacancies/{id}/languages
{ "language_id": 2, "level": "B2", "is_required": true }

Estos filtran el directorio cuando el recruiter busca candidatos para esta vacante.

Publicar

Cuando la empresa termina de llenar todos los datos:

Endpoint: POST /api/v1/vacancies/{id}/publish

  • Valida que tenga los campos mínimos (title, description, requirements, al menos 1 skill required).
  • Cambia state de borrador a activa.
  • Setea published_at = now().
  • Dispara VacancyPublishedNotification a todos los recruiters HUMAE (in-app).
  • Si hay assigned_recruiter_id, también al recruiter específico.

Acciones según el estado

EstadoAcciones permitidas
borradorEditar, publicar, cancelar, eliminar
activaEditar (campos menores), cancelar, asignar recruiter
en_busqueda / con_candidatos_asignados / entrevistas_en_cursoEditar descripción, cancelar
finalista_seleccionadoConfirmar hire, reabrir, cancelar
cubiertaSolo lectura (histórico)
canceladaSolo lectura

Asignar recruiter HUMAE

Un admin asigna un recruiter responsable de la vacante:

Endpoint: PATCH /api/v1/vacancies/{id} con {"assigned_recruiter_id": 12}.

Esto:

  • Aparece en el dashboard del recruiter.
  • Recibe VacancyAssignedToYouNotification.
  • Es el contacto default para la empresa cuando tenga dudas.

Cerrar la vacante

Dos formas:

1. Contratar a un finalista

  • Mover asignación a hired.
  • Automáticamente la vacante pasa a cubierta.
  • Se notifica a todas las partes.
  • Otras asignaciones activas pasan a withdrawn con motivo "vacante cubierta".

2. Cancelar

Endpoint: POST /api/v1/vacancies/{id}/cancel con {"reason": "..."}.

  • Disponible desde cualquier estado no terminal.
  • Cambia a cancelada.
  • Notifica a la empresa y al recruiter.
  • Las asignaciones activas quedan en withdrawn.

Expiración automática

Si expires_at <= now() y la vacante sigue abierta, un job diario la marca como cancelada con motivo expired. (Pendiente de implementar completamente — por ahora solo manual).

Permisos

VacancyPolicy:

  • company_user puede ver/editar vacantes de su Company.
  • recruiter puede ver/editar cualquier vacante (para curar el pipeline).
  • admin puede todo, incluido eliminar.

Tags y categorización

Una vacante puede tener tags libres para búsqueda:

Endpoint: POST /vacancies/{id}/tags con {"tag": "bootcamp"}.

  • Los tags son string libres (no catálogo).
  • Visibles en el detalle.
  • Buscables desde el dashboard.

Listado de vacantes

Para la empresa

GET /me/empresa/vacantes devuelve las de company_id del user.

Para el recruiter

GET /recruiter/vacantes devuelve todas las vacantes activas.

Filtros:

  • state
  • priority
  • assigned_recruiter_id
  • company_id (solo para admin/recruiter)
  • search (en title + description)

Siguiente

Cómo la empresa revisa los candidatos que HUMAE le presenta: Candidatos propuestos →

Manual de usuario HUMAE · Uso interno