Skip to content

Flujo Raleos y cosechas

Base URL: /api/v1. Backend: camaroneras_backend (módulo raleos) ✅. Admin: camaroneras_admin (feature raleos) ✅. Mobile: camaroneras_mobile (feature raleos, offline-first) ✅. Séptimo módulo de negocio; depende de ciclos (Cycle + Siembra). Registro de raleos (pesca parcial) y cosecha final (cierra el ciclo → piscina vacía), con planificación (gramaje objetivo + bines → proyección) que se completa al pescar. Con derivados. Sin destino de venta (confidencial).

Requests y responses 2xx: bodies (tipos, opcionales, enums) y el shape de las respuestas exitosas están en la API Reference, generada desde el spec OpenAPI. Acá quedan solo las respuestas de error (4xx/5xx) y las reglas de negocio/flujo.

Ver ADR-0043 por qué la planificación extiende Harvest en vez de ser un modelo aparte, y referencia/guia-campo-ab.md §5 por las fórmulas del plan.

Modelo

Cycle ──< Harvest   (raleo | cosecha_final; status: planificado | ejecutado)
  • Harvest: cycleId, type (raleo | cosecha_final), status (planificado | ejecutado, default ejecutado), date, pounds? (lbs, null si planificado), avgWeightG? (peso promedio g, null si planificado), targetWeightG? (gramaje objetivo, plan), bins? (# de bines, plan), notes?.
  • onDelete: Cycle → Cascade.

Derivados (calculados al leer — ver referencia/sabana-calculos.md y guia-campo-ab.md)

  • harvestedCount = round(pounds × 454 / avgWeightG) — animales cosechados. null si status = planificado (no hay libras/peso real todavía).
  • survivalPct = harvestedCount / larvaeCount × 100 — null si no hay harvestedCount o el ciclo no tiene siembra.
  • diasCultivo = date − fechaSiembra (usa Siembra.date, o Cycle.startDate).
  • plan (solo si el registro tiene bins): lbRaleoHa, totalRaleoLb, cM2Raleo — proyección del plan (ver guia-campo-ab.md §5). null si no se cargaron bines.

Reglas de negocio

  • Solo se registra sobre un ciclo activa (si no → 400).
  • status default ejecutado; para planificar, enviar status: "planificado" en el POST (sin pounds/avgWeightG — en cambio targetWeightG/bins opcionales). Un registro ejecutado requiere pounds y avgWeightG (400 si faltan).
  • Un plan NO cuenta en summary.totalRaleoLb ni cierra el ciclo si es cosecha_final; ejecutarlo sí (PATCH con status: "ejecutado" + pounds + avgWeightG).
  • Un ejecutado no puede volver a planificado (400) — ya es un registro real.
  • Ejecutar cualquier plan (raleo o cosecha final) requiere que el ciclo siga activa (400 si no) — no solo al ejecutar una cosecha final; un raleo planificado mientras el ciclo estaba activo no se puede ejecutar después de que el ciclo se cerró por otra vía (ej. una cosecha final aparte).
  • pounds/avgWeightG explícitos en null sobre un registro que queda ejecutado se rechazan (400) — no se puede "vaciar" el dato real de una pesca ya hecha por esa vía. Y sobre un registro que queda/vuelve planificado, cualquier pounds/avgWeightG del body se ignora (se persiste null, igual que al planificar): un plan nunca guarda catch real hasta que se ejecuta.
  • cosecha_final: una sola por ciclo, planificada o ejecutada (2ª → 409 si aún activa); al crearla ejecutada (o al ejecutar un plan existente) el ciclo pasa a cerrada (libera la piscina). Una cosecha_final planificada no cierra el ciclo. La invariante "una sola por ciclo" la impone un índice único parcial en BD (harvests_cycleId_cosecha_final_unique, migración harvest_cosecha_final_unique; Prisma no expresa índices parciales en su DSL) — el findFirst de la app es solo una salida rápida, la carrera entre dos requests concurrentes la resuelve la base.
  • Con el ciclo cerrada, nuevos raleos/cosechas → 400.
  • Borrar una cosecha_final ejecutada reabre el ciclo (cerrada → activa), salvo que la piscina ya tenga otro ciclo activo (→ 409). Borrar un plan nunca reabre nada (nunca cerró el ciclo).
  • type es inmutable en update.

RBAC

  • Módulo raleos (label "Raleos y cosechas") en el catálogo, clients ["web","mobile"].
  • Defaults: Administrador todo; Técnico ver/crear/editar; Bodeguero ver. Backfill automático.

Endpoints (protegidos: AccessJwtGuard + PermissionGuard; tenant del JWT)

GET /cycles/:cycleId/harvests — (raleos:ver)

Lista raleos/cosechas del ciclo (más reciente primero) con derivados + summary.

POST /cycles/:cycleId/harvests — (raleos:crear)

Registra un raleo/cosecha, ejecutado (default) o planificado (status: "planificado"). La cosecha final ejecutada cierra el ciclo; planificada, no.

json
// response 400 (ciclo no activa)
{ "error": "...", "message": "El ciclo no está activo; no se pueden registrar raleos ni cosechas", "statusCode": 400 }
json
// response 400 (ejecutado sin libras/peso)
{ "error": "...", "message": "Un registro ejecutado requiere libras y peso promedio", "statusCode": 400 }
json
// response 409 (2ª cosecha final, planificada o ejecutada)
{ "error": "...", "message": "El ciclo ya tiene una cosecha final", "statusCode": 409 }

PATCH /harvests/:id — (raleos:editar)

Edita fecha/lbs/pesoProm/gramaje objetivo/bines/notas (no type). Recalcula derivados. También ejecuta un plan: enviar status: "ejecutado" + pounds + avgWeightG.

json
// response 400 (ejecutado → planificado)
{ "error": "...", "message": "Un registro ejecutado no puede volver a planificado", "statusCode": 400 }
json
// response 400 (ejecutar sin libras/peso, o pounds/avgWeightG explícitos en null)
{ "error": "...", "message": "Un registro ejecutado requiere libras y peso promedio", "statusCode": 400 }
json
// response 400 (ciclo ya no activo al ejecutar un plan, raleo o cosecha final)
{ "error": "...", "message": "El ciclo no está activo; no se puede ejecutar este raleo/cosecha", "statusCode": 400 }

DELETE /harvests/:id — (raleos:eliminar)

Elimina. Si es cosecha_final ejecutada, reabre el ciclo (activa); si es un plan (nunca cerró nada), solo elimina.

json
// response 409 (la piscina ya tiene otro ciclo activo al reabrir)
{ "error": "...", "message": "La piscina ya tiene un ciclo activo; no se puede reabrir este ciclo", "statusCode": 409 }
json
// response 404 (no existe o de otra organización)
{ "error": "...", "message": "Cosecha no encontrada", "statusCode": 404 }

Flujo admin (camaroneras_admin, feature raleos)

  • Ruta: /raleos — protegida (ProtectedRoute module="raleos" action="ver"). Sin sesión → /login; sin permiso raleos:ver → bloqueada y sin item de nav.
  • Selector de ciclo (FormCombobox) desde GET /cycles. Sin selección → aviso.
  • Summary: total lb de raleo (solo ejecutados) · cosecha final (Sí/No) · estado del ciclo.
  • Tabla: Fecha · Tipo · Estado (Ejecutado/Planificado) · Lbs · Peso prom · Gramaje obj (g) · Bines · Animales · % Sob · Días (derivados del backend).
  • Diálogo crear/editar: tipo (FormSelect raleo/cosecha final, deshabilitado en edición), estado (FormSelect ejecutado/planificado, solo en creación), fecha, y campos condicionales: si planificado → gramaje objetivo + # de bines; si ejecutado → libras + peso promedio. Notas siempre. Botón deshabilitado según el modo (planificado: solo requiere fecha; ejecutado: fecha + lbs>0 + pesoProm>0).
  • "Ejecutar" (solo en filas planificado, permiso raleos:editar): diálogo aparte que muestra el plan (gramaje/bines/proyección) y pide libras + peso reales; PATCH con status: "ejecutado".
  • Bloqueo por estado: si el ciclo está cerrada (tras cosecha final ejecutada), se oculta "Nuevo registro" y se muestra un aviso. Borrar la cosecha final ejecutada (permiso eliminar) reabre el ciclo. Los errores 400/409 del backend se muestran vía toast.
  • Gating RBAC: "Nuevo registro" con raleos:crear; "Editar"/"Ejecutar" con raleos:editar; "Eliminar" con raleos:eliminar.

Flujo mobile (camaroneras_mobile, feature raleos, offline-first)

  • Rutas: /raleos (picker de ciclo) → /raleos/:cycleId (harvests). Card en Home solo con canVerRaleos. La app ya exige sesión (go_router → /login).
  • Offline-first: tabla Drift Harvests (schema v10 — status, targetWeightG, bins, planJson; recreada en la migración porque pounds/avgWeightG pasan a nullable) que guarda los derivados del backend; el summary se cachea en AppMeta como JSON por ciclo. Lectura desde Drift; sync() refresca y conserva el cache.
  • Escrituras online: crear/editar/eliminar/ejecutar requieren red (_netError → "Sin conexión…"); tras cada escritura se re-sincroniza el ciclo (trae el nuevo cycleStatus).
  • Bloqueo por estado: el FAB "Registrar" se oculta cuando cycleStatus != 'activa' (ciclo cerrado tras la cosecha final ejecutada); se muestra un aviso. Borrar la cosecha final ejecutada reabre.
  • Sheet: toggle de tipo (raleo/cosecha_final, inmutable en edición) y de estado (ejecutado/planificado, solo al crear), fecha, y campos condicionales igual que admin. Botón "Ejecutar" (icono play) en la tarjeta de un plan abre ExecuteHarvestSheet (libras + peso reales → status: "ejecutado").
  • Gating RBAC: FAB con canCrearRaleos; editar/eliminar/ejecutar con canEditarRaleos.
  • Tests: repository (Drift in-memory: derivados + summary + replace borra ausentes + plan guarda pounds/avgWeightG null) + gating del FAB.