Apariencia
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
Harvesten vez de ser un modelo aparte, yreferencia/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, defaultejecutado),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.nullsistatus = planificado(no hay libras/peso real todavía).survivalPct = harvestedCount / larvaeCount × 100— null si no hayharvestedCounto el ciclo no tiene siembra.diasCultivo = date − fechaSiembra(usaSiembra.date, oCycle.startDate).plan(solo si el registro tienebins):lbRaleoHa,totalRaleoLb,cM2Raleo— proyección del plan (verguia-campo-ab.md§5).nullsi no se cargaron bines.
Reglas de negocio
- Solo se registra sobre un ciclo
activa(si no → 400). statusdefaultejecutado; para planificar, enviarstatus: "planificado"en el POST (sinpounds/avgWeightG— en cambiotargetWeightG/binsopcionales). Un registroejecutadorequierepoundsyavgWeightG(400 si faltan).- Un plan NO cuenta en
summary.totalRaleoLbni cierra el ciclo si escosecha_final; ejecutarlo sí (PATCHconstatus: "ejecutado"+pounds+avgWeightG). - Un
ejecutadono puede volver aplanificado(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/avgWeightGexplícitos ennullsobre un registro que quedaejecutadose rechazan (400) — no se puede "vaciar" el dato real de una pesca ya hecha por esa vía. Y sobre un registro que queda/vuelveplanificado, cualquierpounds/avgWeightGdel body se ignora (se persistenull, 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 acerrada(libera la piscina). Unacosecha_finalplanificada no cierra el ciclo. La invariante "una sola por ciclo" la impone un índice único parcial en BD (harvests_cycleId_cosecha_final_unique, migraciónharvest_cosecha_final_unique; Prisma no expresa índices parciales en su DSL) — elfindFirstde 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_finalejecutada 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). typees 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; Bodeguerover. 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 permisoraleos:ver→ bloqueada y sin item de nav. - Selector de ciclo (
FormCombobox) desdeGET /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 (
FormSelectraleo/cosecha final, deshabilitado en edición), estado (FormSelectejecutado/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, permisoraleos:editar): diálogo aparte que muestra el plan (gramaje/bines/proyección) y pide libras + peso reales;PATCHconstatus: "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 (permisoeliminar) reabre el ciclo. Los errores 400/409 del backend se muestran vía toast. - Gating RBAC: "Nuevo registro" con
raleos:crear; "Editar"/"Ejecutar" conraleos:editar; "Eliminar" conraleos:eliminar.
Flujo mobile (camaroneras_mobile, feature raleos, offline-first)
- Rutas:
/raleos(picker de ciclo) →/raleos/:cycleId(harvests). Card en Home solo concanVerRaleos. 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 porquepounds/avgWeightGpasan a nullable) que guarda los derivados del backend; el summary se cachea enAppMetacomo 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 nuevocycleStatus). - 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 concanEditarRaleos. - Tests: repository (Drift in-memory: derivados + summary + replace borra ausentes + plan guarda pounds/avgWeightG null) + gating del FAB.