# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Qué es este proyecto

LIMS (sistema de laboratorio clínico) **"labSoft"** en CodeIgniter 4.6.5 / PHP 8.3 / MySQL,
en proceso de convertirse en un SaaS multi-tenant (`lab_saas`). Idioma del proyecto: **español**
(UI, comentarios, mensajes de commit).

Documentos de contexto y planes (carpeta **`docs/`** — leerlos antes de trabajar):
- **`docs/ANALISIS_CONTEXTO.md`** — lectura consolidada del estado del proyecto (empezar aquí).
- **`docs/PLAN_POS.md`** — punto de venta: `/cotizacion` (empleado) y embebido en
  `/dashboard` (admin); partials, decisiones, mapa y **pendientes** (P0–P3).
  Leer antes de tocar venta/carrito/cotización/dashboard.
- **`docs/PLAN_ACTUALIZACION.md`** — plan vivo de la transformación SaaS (fases, decisiones,
  tokens Modernize) y **bitácora de avances** al final. Registrar ahí todo avance.
- **`docs/PLAN_LANDING_REGISTRO.md`** + **`docs/MANUAL_IDENTIDAD_PUBLICA.md`** — landing,
  registro trial, copy **$0 en desarrollo / precios por determinarse**, GSAP.
- `docs/PLAN_MODERNIZACION.md` — historia de la modernización original (20 días) y bugs resueltos.
- `docs/INSTALACION.md` — instalación en producción (Hostinger).
- Índice: `docs/README.md`.

## Entorno de desarrollo (Docker/DAMP)

La app corre en el contenedor `labsaas` (FrankenPHP) sobre la red externa `damp`;
MySQL vive en el contenedor `damp-db` (BD `labsaas_db`, root/root). URL local: `https://labsaas.test`.

```bash
docker compose up -d                              # levantar la app
docker exec labsaas php spark migrate --all       # migraciones (--all incluye Shield/Settings)
docker exec labsaas vendor/bin/phpunit            # toda la suite
docker exec labsaas vendor/bin/phpunit tests/unit/HealthTest.php   # un test
docker exec labsaas vendor/bin/phpunit --filter nombreDelTest      # por nombre
docker exec labsaas php -l ruta/al/archivo.php    # lint
docker exec damp-db mysql -uroot -proot labsaas_db -e "..."        # SQL directo
```

Ojo: en CLI el `.env` tiene precedencia sobre las variables del contenedor — debe apuntar a
`damp-db` / `labsaas_db` (guion medio, no guion bajo).

Usuarios de prueba locales: `test_admin :: Prueba2026admin` (admin),
`test_empleado :: Prueba2026emp` (empleado, Sucursal Principal), `test_lab :: Prueba2026lab` (laboratorista).

## Arquitectura

**Roles y rutas (Shield, rutas planas).** Auth con **CodeIgniter Shield 1.3** (login por
`username`; sin flujos de email de Shield). Hay **registro público de laboratorio** en
`/registro` (no registro de usuario suelto): crea tenant trial vía `TenantProvisioner` y
auto-login al admin. Roles = grupos Shield (`app/Config/AuthGroups.php`): `admin`,
`laboratorista`, `empleado`, `superadmin`. Rutas **sin prefijo de rol** con filtros
`['auth', 'group:...']`. Homes: `/dashboard` (admin), `/cotizacion` (empleado),
`/pendientes` (lab), `/super` (superadmin). `GET /` = `Home::index`: **guest → landing**
(`public/landing`); auth → home por grupo. Landing: copy **$0 en fase de desarrollo**,
precios del software **por determinarse**; animaciones GSAP en `public/js/landing.js`.
Detalle POS: `docs/PLAN_POS.md`. Capturados lab: `/capturas`. Precio POS: columna `General`.
Tras el login, el evento `login` de Shield (`app/Config/Events.php`) llama a
`poblar_sesion_usuario()` (`lab_helper`), que puebla `idusuario`, `login`, `accesslevel`
(derivado del grupo, por compatibilidad), `idempleado`, `idsucursal`, `nombre_sucursal`.
CSRF activo globalmente **basado en sesión** (requerido por Shield). "Desactivar" un usuario
= `active=0` **más** `status='banned'` (sin activación por email, `active=0` solo no bloquea
el login; el ban sí). Regla de negocio: siempre debe quedar ≥1 admin activo
(`UserModel::contarAdminsActivos`).

**Auditoría (transversal — no romperla).** Todos los modelos extienden
`app/Models/BaseAuditModel.php`: timestamps, **soft delete** (`deleted_at`), responsables
(`created_by/updated_by/deleted_by` desde la sesión) y bitácora automática en la tabla
`auditoria`. Reglas derivadas:
- Un modelo nuevo debe extender `BaseAuditModel` (no `CodeIgniter\Model`). Excepción:
  `app/Models/UserModel.php` extiende el `UserModel` de Shield (tablas `users`/
  `auth_identities`/`auth_groups_users`; la vieja tabla `usuario` ya no existe) — el CRUD de
  usuarios audita manualmente con `auditoria_log()`.
- Toda query SQL cruda sobre tablas de negocio debe incluir `deleted_at IS NULL`
  (también en los JOIN, p. ej. a `abonar` al sumar saldos).
- Para eventos fuera de modelos existe el helper `auditoria_log()` (`app/Helpers/lab_helper.php`,
  autoloaded como `lab`).
- `consecutivo` y `maximos` no tienen soft delete ni modelo; se escriben con query builder crudo.

**Esquema de BD.** `database/schema.sql` + `seed.sql` son la base histórica (instalación nueva);
**todo cambio de esquema nuevo va en `app/Database/Migrations/`** (idempotentes, con `down()`).
Tablas centrales: `ventas`–`venta_muestra`–`muestra`–`resultado_detalle` (resultados por campo,
reemplazó XMLs), `abonar` (pagos parciales; el saldo se calcula sumando abonos), `consecutivo`
(folio por sucursal), `maximos` (config de puntos, 1 fila). Trampa: `ventas.idconsecutivo`
guarda el **número de folio**, no es FK a `consecutivo`.

**Flujo de negocio principal.** `app/Services/VentaService.php` es la transacción crítica:
bloquea el consecutivo (`FOR UPDATE`), crea muestras, genera el código **CDB**
(`app/Libraries/Cdb.php`, formato `{consecutivo}-{idusuario}-{idsucursal}-{idcliente}-{idmuestra}`),
aplica puntos/descuentos de `maximos` y escribe venta + abono inicial. El carrito vive en sesión
(`session('carrito')`). Los formularios de captura del lab se generan dinámicamente desde el JSON
`analisis.campos_resultado` (`Lab/Captura`). PDFs con FPDF (`app/ThirdParty/fpdf`) vía
`app/Libraries/PdfTicket.php` y `PdfResultado.php`.

**Frontend (sin build tooling).** Bootstrap 5.3 + Bootstrap Icons + DM Sans + jQuery 3.7 +
DataTables 2 + Select2 por CDN, y `public/css/app.css` con tokens **Modernize**. Layouts de
backoffice comparten `topbar` / `navmenu` / `flash`. Sitio marketing: `layouts/public.php`
(clases `public-*`, ver `MANUAL_IDENTIDAD_PUBLICA.md`). Landing carga GSAP por CDN +
`public/js/landing.js` (no en registro ni backoffice). Reglas `public/js/app.js`: tablas
`js-datatable`, `select.form-select` → Select2 (eventos jQuery, no nativos),
`LB.select2Remoto()` para autocompletes. Scripts de vista en `section('scripts')`. POST AJAX
rotan CSRF.

**Multi-tenancy: MT-0 + MT-1 implementados** (una BD + `tenant_id`, `BaseTenantModel`,
superadmin con `users.tenant_id NULL`, CRUD `/super/tenants`, signup público trial). Billing
y precios del software **aún no** — el copy público debe decir **$0 en desarrollo / precios
por determinarse**. Ver `docs/PLAN_ACTUALIZACION.md` y `docs/PLAN_MULTITENANT.md`.
