Apariencia
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_atEstados 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):
titlerequerido, 1–200 chars.descriptionrequerido, 1–10000.- FKs verificadas contra catálogos activos.
target_candidate_kinddebe ser uno deemployee,intern,any(defaultany). Valores fuera del enum responden422.salary_min <= salary_max.min_years_of_experience <= max_years_of_experience.closes_at > todaysi 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_idse 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:
| Valor | Significado |
|---|---|
employee | Solo candidatos que se registraron como Empleado. |
intern | Solo 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:EmpleadooPracticante(color marca / ámbar).Cualquierano 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
statedeborradoraactiva. - Setea
published_at = now(). - Dispara
VacancyPublishedNotificationa todos los recruiters HUMAE (in-app). - Si hay
assigned_recruiter_id, también al recruiter específico.
Acciones según el estado
| Estado | Acciones permitidas |
|---|---|
borrador | Editar, publicar, cancelar, eliminar |
activa | Editar (campos menores), cancelar, asignar recruiter |
en_busqueda / con_candidatos_asignados / entrevistas_en_curso | Editar descripción, cancelar |
finalista_seleccionado | Confirmar hire, reabrir, cancelar |
cubierta | Solo lectura (histórico) |
cancelada | Solo 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
withdrawncon 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_userpuede ver/editar vacantes de suCompany.recruiterpuede ver/editar cualquier vacante (para curar el pipeline).adminpuede 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:
statepriorityassigned_recruiter_idcompany_id(solo para admin/recruiter)search(en title + description)
Siguiente
Cómo la empresa revisa los candidatos que HUMAE le presenta: Candidatos propuestos →

