Job es la unidad de trabajo entre un cliente y un camello: se crea, se asigna, se acepta o se rechaza, y se cierra por finalización o cancelación.
Origen: EXPLORE vs REQUEST
Un Job nace de dos maneras distintas, y el campo source registra cuál:
EXPLORE: el cliente eligió un camello concreto en el explorador y el body de creación incluyecamelloId.REQUEST: el cliente pidió el servicio sin elegir camello (por ejemplo desde/trabajos/nuevo); no haycamelloIdy el admin asigna uno después.
Ciclo de vida
PENDIENTE, EN_PROGRESO, COMPLETADO y CANCELADO. En texto:
- El cliente crea el Job (
POST /api/jobs). - El admin asigna un camello a un Job
PENDIENTEsin camello (POST /api/admin/jobs/:id/assign). - El camello acepta (
POST /api/jobs/:id/accept): pasa dePENDIENTEaEN_PROGRESO. O rechaza (POST /api/jobs/:id/reject): se le quita elcamelloId, se borra el precio referencial y el Job vuelve a quedar sin asignar. complete(POST /api/jobs/:id/complete) solo es válido desdeEN_PROGRESO.- Tras aceptar, la orden de servicio queda en
/trabajos/:id/comprobante(también actúa como comprobante al completar). LlevaorderNumbery, si existe, el precio referencial. El detalle está en Precio referencial del trabajo. - El camello puede calificar al cliente con
POST /api/jobs/:id/client-reviewcuando el job estáCOMPLETADO. cancel(POST /api/jobs/:id/cancel) solo sale dePENDIENTEoEN_PROGRESO. El motivo opcional queda descrito en Motivo de cancelación.
Quién puede cada transición
Restricciones de asignación
POST /api/admin/jobs/:id/assign exige que:
- el Job esté
PENDIENTEy sincamelloIdtodavía; - el camello esté
ACTIVO; - el camello tenga
userId(esté vinculado a una cuenta de usuario, no sea solo una ficha del catálogo).
El endpoint de asignación no comprueba que el camello ofrezca el
serviceId del Job: ese filtro solo aplica al pool de GET /api/admin/jobs/:id/candidates, así que “mismo servicio” es una convención de la UI del admin (que asigna desde ese pool), no una garantía que imponga /assign.Quién asignó
POST /api/admin/jobs/:id/assign guarda assignedById con el admin de la sesión, además de assignedAt. El trabajo lo expone como assignedBy (id y name). Si el cliente eligió el camello al crear el trabajo (EXPLORE), no hay admin: assignedBy queda en null aunque assignedAt sí se escriba. Un rechazo posterior no borra ese dato; la siguiente asignación lo pisa.
Precio referencial del trabajo
El ítem UNI-04 del backlog de unificación (origenandago9/camellap, Job.price) se retiró el 2026-10-22. Job.quotedPrice es un entero opcional en pesos colombianos. No es un cobro. La cita (scheduledStart / scheduledEnd) no forma parte de este dato.
El ítem UNI-08 se retiró el mismo día. Job.quotedPriceUnit guarda la unidad que tenía el camello en ese momento (hora, servicio o dia). Viaja con el monto.
El monto se copia de Camello.priceFrom y quotedPriceUnit de Camello.priceUnit en los mismos tres momentos:
- al crear un trabajo
EXPLORE, cuando el cliente ya eligió camello; - al asignar con
POST /api/admin/jobs/:id/assign; - al aceptar con
POST /api/jobs/:id/accept, solo si el trabajo todavía no tiene monto.
0 es un monto válido. Un rechazo borra los dos campos junto con el camello, para que el siguiente no herede el precio del anterior.
La unidad guardada es hora, servicio o dia. En /trabajos la tarjeta lo muestra como «Precio referencial» en COP cuando hay monto y, si hay unidad, el mismo sufijo que la ficha (/ hora, / servicio, / día). El mismo texto sale en /trabajos/:id/comprobante. Sin unidad, solo el monto. El origen en la ficha del camello está en Precio referencial.
Motivo de cancelación
El ítem UNI-05 del backlog de unificación (origenandago9/camellap, Job.cancelReason) se retiró el 2026-10-22. El cliente dueño o un admin cancela desde la tarjeta de /trabajos mientras el trabajo está PENDIENTE o EN_PROGRESO. El camello no cancela: si sigue pendiente, lo rechaza y el trabajo vuelve a la cola.
POST /api/jobs/:id/cancel acepta { reason }, texto opcional de hasta 500 caracteres. Sin cuerpo, o con {}, cancela y deja cancelReason en null. Un JSON ilegible responde 400 «JSON invalido». Un reason que no es texto o pasa de 500 caracteres responde 400 «Parametros invalidos» y no cancela. Si el trabajo ya está COMPLETADO o CANCELADO, responde 409 y no pisa cancelledAt ni el motivo.
El texto se guarda en Job.cancelReason junto con cancelledAt. En /trabajos la tarjeta lo muestra como «Motivo: …». El aviso JOB_STATUS_CHANGED incluye ese texto cuando existe.
Al eliminar la cuenta (DELETE /api/me), los trabajos abiertos del cliente pasan a CANCELADO con el motivo «La cuenta del cliente fue eliminada». Ese camino no envía notificación.
Lista en /trabajos
La página parte la lista en cinco pestañas: Todos, Pendientes, En progreso, Completados y Cancelados. Un trabajo CANCELADO sigue apareciendo en Todos y, además, en su propia pestaña.
?estado= abre la pestaña correspondiente: pendiente, en-progreso, completado o cancelado. Un valor que no está en esa lista abre Todos. El admin puede sumar ?sinAsignar=1 para ver solo trabajos sin camello. Cambiar de pestaña en la página no reescribe la URL.
Visibilidad de GET /api/jobs
La lista de trabajos depende del rol de la sesión: el cliente ve los que creó, el camello los que tiene asignados a su perfil, y el admin los ve todos.
Un Job ajeno responde 404, no 403, tanto en
GET /api/jobs/:id como en las acciones sobre un Job concreto. Es intencional: no se revela que el recurso existe a quien no tiene visibilidad sobre él.Cita
scheduledStart y scheduledEnd son opcionales. La cita dura 60 minutos y
solo existe si el trabajo tiene camello. Una solicitud abierta (REQUEST)
nace sin hora; el cliente la fija con POST /api/jobs/:id/schedule cuando ya
hay camello y el estado es PENDIENTE o EN_PROGRESO.
Si el cliente eligió camello al crear el trabajo, puede mandar scheduledStart
en POST /api/jobs. Sin camelloId, esa fecha responde 400.
Cuando el camello rechaza, la cita se borra junto con el camelloId: el hueco
vuelve a quedar libre. Un trabajo CANCELADO tampoco bloquea el horario.
El detalle de ventanas y huecos está en Agenda.