# Plan de ejecución — SysFarma Móvil hacia Play Store

Los hallazgos están en [`docs/auditoria-prelanzamiento.md`](auditoria-prelanzamiento.md),
numerados **B1-B9** (bloqueantes) e **I1-I15** (importantes).

---

## Dos aclaraciones que cambian el calendario (2026-07-27)

### 1. Subir a prueba cerrada NO es lanzar
El track de prueba cerrada es **privado**: el AAB solo lo ven los correos inscritos, no aparece en
la búsqueda de Play, ningún cliente lo instala por accidente. **No se está publicando nada** — se
está poniendo a correr un reloj de 14 días que es obligatorio y que **corre igual mientras se
sigue arreglando todo lo demás**.

Por lo tanto "quiero solucionar todo antes de subir" y "arrancar el reloj ya" **son compatibles**.
La única diferencia entre hacerlo y no hacerlo son **dos semanas de calendario**, y no se gana
nada esperando.

> Confirmar en Play Console qué declaraciones exige el track de prueba cerrada — igual van a hacer
> falta **B3** (política de privacidad) y **B4** (Data Safety) hechos.

### 2. Qué es "todo"
- ✅ **Objetivo sano:** los **9 bloqueantes + los 15 importantes**.
- ❌ **No hacer:** los 66 de la sección 3 de mejoras. Ahí adentro están el rediseño del paso de
  cobro, cuentas por cobrar, descuentos en POS y el offline real — **son features, no defectos**.
- De los 66, **lo único que va antes de lanzar es la ortografía**, y va **antes de las capturas**
  de la ficha.

### Calendario con la sesión 4-bis adentro
**6 a 8 semanas**, no 3 a 4. Es una decisión legítima — se lanza con un producto más completo —
pero hay que comunicarlo así a los clientes que estén esperando, **y arrancar los 14 días lo antes
posible**.

---

# ORDEN DE EJECUCIÓN (decidido 2026-07-27 — manda sobre la numeración de sesiones de abajo)

**Prioridad elegida: permisos primero, sucursal después, publicar al final.**

Ese orden es mejor que el inverso por una razón concreta: si el resolver de permisos se construye
primero, cerrar I11 pasa a ser *cablear el resolver en el eje establecimiento*, en vez de parchear
endpoint por endpoint y tirar el parche después.

## ⚠️ Corrección: arrancar el reloj NO es "sin código"
Para que empiecen a correr los 14 días hay que **subir un AAB que Play acepte**, y **B2 lo bloquea**
(librerías del escáner alineadas a 4 KB con targetSdk 36 → Play **rechaza en la subida**, no
advierte). B1 (firma real, hoy cae a debug) también es imprescindible.

**Mínimo técnico para arrancar el reloj: B1 + B2.** Son 1-2 días, casi todo B2.

## Secuencia

| # | Qué | Bloquea a | Código |
|---|---|---|---|
| **1** | ✅ ~~B5 — decidir marca y applicationId~~ **HECHO** `91186c26` — `pe.sistemax.sysfarma` | ~~todo lo demás~~ | no |
| **2** | ✅ ~~B1~~ **HECHO** `0e0c1fda` — firma real, verificada en APK y AAB. **Falta B2** — escáner a `mobile_scanner ^7` | la subida del AAB | sí (queda B2) |
| **3** | **B3 + B4** — política de privacidad y Data Safety (en paralelo con el 2) | crear la app | no |
| **4** | **Crear app en Play Console, subir AAB, inscribir testers** → **arranca el reloj de 14 días** | producción | no |
| **5** | **Sesión 1** — tests: Parte A (plata) + **Parte B (alcance)** | la 6 | sí |
| **6** | **Sesión 4-bis** — permisos: ✅ eje 1 (rama sin mergear, ver arriba) + resolver `data_scope` (eje 2, pendiente) | la 7 | sí |
| **7** | **I11** — cablear el resolver en `MobileEstablishmentAccess` | — | sí |
| **8** | Resto de bloqueantes e importantes (sesiones 2, 3, 5) | — | sí |
| **9** | **Publicar a producción** | — | no |

**Los pasos 5 a 8 corren mientras el reloj ya está andando.** Se pueden seguir subiendo builds
nuevos al mismo track de prueba cerrada — de hecho es para lo que sirve: los cajeros de cuidarte
van a estar probando los permisos justo cuando se estén construyendo.

**Dependencias duras que no se pueden saltear:**
- El paso 5 (tests de alcance) **antes** del 6. Son 22 scopes sin cobertura; sin los tests el
  refactor es a ciegas y no hay forma de detectar si se rompió el aislamiento al arreglarlo.
- El paso 6 **antes** del 7, que es justamente el cambio de orden decidido.

## Regla general para todas las sesiones

- **Una rama por sesión:** `git checkout -b fix/sesion-N-tema`
- **`/clear` entre sesiones.** Contexto arrastrado = alucinaciones sobre archivos ya cambiados.
- **Siempre pedir el plan antes del código** (Shift+Tab para plan mode, o pedirlo explícito).
- **Correr la app en un celular real al cerrar cada sesión**, no solo al final.

---

## Sesión 0 — Hotfix de producción (HOY, antes que nada)

Rama aparte del plan de lanzamiento. Ya está vivo afectando clientes reales. Solo backend.

1. **B9** — `routes/api.php:16-18`: ruta de creación de productos fuera de todo grupo de
   autenticación, y su FormRequest autoriza a cualquiera. Moverla al grupo autenticado que ya
   existe más abajo, corregir el `authorize()`, y verificar que no quede una ruta duplicada
   ganando por orden de registro.
2. **B7** — `app/Models/Tenant/Document.php:773`: `OR` sin agrupar que mezcla la condición de
   usuario con la búsqueda por id. Envolverlo en un grupo. Después buscar el mismo patrón
   (`orWhere` suelto dentro de un scope o query builder) en el resto de `app/Models/Tenant` y
   `app/Http/Controllers` — según la auditoría también rompe filtros de fecha y sede en reportes.
3. **PDFs sin autenticación** — encontrar ruta y controlador, y protegerla: sesión válida, o
   token firmado con vencimiento si hay algún flujo que dependa del enlace público.

> Mostrar los tres cambios como diff antes de aplicar. Para el punto 2, explicar qué devuelve
> la query antes y después.

**Después del deploy, a mano:** revisar las anulaciones de los últimos meses de los clientes con
usuarios tipo vendedor. Si el bug estuvo vivo, hay comprobantes anulados que no correspondían y
stock devuelto de más.

---

## Sesión 1 — La red de seguridad

No arregla ningún hallazgo. Es lo que hace que las sesiones 3, 4 y 4-bis no sean una apuesta.

**Parte A — lo que mueve plata:**
- Cálculo de totales, descuentos e IGV. Incluir el caso de IGV leído de la configuración del
  cliente, no el 18% hardcodeado.
- Armado del payload que se envía a SUNAT.
- Generación y reuso de la clave de idempotencia: mismo cobro reintentado → una sola clave.
- Cola de ventas offline: encolar, sincronizar, y el caso de carrera donde entra una venta nueva
  durante la sincronización.
- Parseo de montos con coma decimal ("20,50").

**Parte B — tests de ALCANCE (agregado 2026-07-27, habilitan la sesión 4-bis):**
Por cada scope `whereTypeUser` (son 22 modelos), un caso de **vendedor** y uno de **admin**,
asertando **qué filas ve cada uno**. Escritos **contra el comportamiento actual, antes de tocar
nada**.

Son la red que hace seguro el refactor de 4-bis: después de migrar los 22 hardcodeos al resolver,
estos tests tienen que quedar pasando **idénticos**. Esa es la prueba de que la migración fue
behavior-preserving. Sin esto no hay forma de saber si se rompió el aislamiento en el intento de
arreglarlo — y un refactor de alcance de datos sobre código sin cobertura es exactamente donde las
cosas se rompen en silencio.

Después: **agregar Flutter a la integración continua** — hoy el CI ni lo menciona, así que los
cuatro tests que existen no se ejecutan nunca.

---

## Sesión 2 — Build y release (sin lógica de negocio)

Arreglar **B1, B2, I12, I13, I14**. Todo es configuración de build y manifiesto.

- **B2** es el más largo: subir `mobile_scanner` a v7 y adaptar la pantalla del escáner a la
  nueva API. Verificar la alineación a 16 KB sobre el **AAB de release**, no sobre el debug.
- **B1:** que el build de release falle si no existe `key.properties`, en vez de caer a firma
  debug. Actualizar también las instrucciones de compilación.
- **I12:** desactivar el respaldo automático de Android en el manifiesto.
- **I13:** sacar el permiso de ubicación precisa. Confirmar antes que no haya ninguna llamada real.
- **I14:** activar minificación, ofuscación y símbolos de depuración nativos, y escribir un script
  de build que inyecte la clave de Sentry.

Con la ofuscación activa, revisar que no rompa nada que dependa de nombres de clase en runtime
(serialización JSON, reflexión).

**Fuera de Claude Code:** generar el keystore y guardar el `.jks` en dos lugares distintos fuera
del repo. Activar Play App Signing al crear la app — es lo que salva si se pierde el archivo.

---

## Sesión 3 — Dinero y datos

**Requiere la sesión 1 hecha.** Es el paquete de mayor riesgo.

Arreglar **B8, I2, I3, I6, I4**. Los tests de la sesión 1 tienen que seguir pasando al terminar.

- **B8:** `sales_screen.dart:2781`, el botón Cobrar solo se deshabilita con carrito vacío.
  Agregar el flag de envío en curso y activarlo al inicio de la función de envío, no en el
  `onPressed`. Usar el mismo patrón que la otra pantalla de cobro.
- **I2:** la clave de idempotencia debe generarse una vez por intento de cobro y reusarse en los
  reintentos. En el backend, un registro marcado como fallido no debe re-ejecutarse si el
  documento ya se guardó.
- **I3:** la sincronización toma un snapshot de la cola y después lo reescribe pisando lo que
  entró mientras tanto. Eliminar por id lo sincronizado en vez de reescribir la lista, y un
  candado que impida dos sincronizaciones en paralelo.
- **I6:** el reset post-venta no limpia los montos por método de pago.
- **I4:** el botón Descartar borra una venta cobrada sin confirmar. Confirmación con el patrón que
  ya usa la app, y mostrar cliente, total e ítems en vez de la clave técnica.

**Empezar por I3**, que es el que puede perder plata sin dejar rastro.

---

## Sesión 4 — Sesión, permisos y multi-sede

Arreglar **I1, I10, I11**.

- **I1:** cierre de sesión real en el servidor con revocación de token, rotar el token al cambiar
  contraseña, dejar de aceptarlo por query string (termina en los logs), y guardarlo cifrado.
- **I10:** la consulta y el cierre de caja no filtran por usuario. El filtro correcto ya existe en
  otro método del mismo controlador — reusarlo.
- **I11:** tres agujeros de aislamiento entre sedes. Evaluar un **scope global de sede** en vez de
  parchear cada endpoint. Si es viable, mostrar primero cómo quedaría y qué endpoints cambian de
  comportamiento.

---

## Sesión 4-bis — Permisos y alcance de datos (decisión 2026-07-27)

**Requiere la Parte B de la sesión 1 hecha.** Antes esto estaba marcado como post-lanzamiento
completo; la decisión fue partirlo, porque el paso caro no es el que parecía.

### Eje 1 — Permiso (barato, alto valor): que el móvil use lo que ya recibe

✅ **Código listo 2026-07-28, rama `fix/sesion-4bis-permisos` (commit `7ed5a640`), NO mergeada a
main — falta probar en celular real y subir un AAB nuevo antes de mergear.**

`ModuleLevel` funciona en la web y el backend **ya le manda `permissions.levels` al móvil**
(`MobileAppPayload.php`). La app lo descartaba: `tenant_session.dart` solo guardaba `userType`.

Hecho: `MobileAppPayload::user()`/`bootstrap()` resuelven `module_values`/`level_values` como los
mismos `value` string del sidebar web (mismo fallback: sin filas en `module_user` = todos los
módulos, no "ninguno" — cero regresión para quien nunca configuró permisos granulares).
`TenantSession` guarda esos módulos + los permisos por función
(`annular_purchase`/`edit_purchase`/`delete_purchase`/`annular_sale`/`edit_payment_method`) que el
login ya recibía y descartaba. Router y menú "Más" gatean por módulo real (`ModuleAccess`,
`mobile/lib/src/core/access/module_access.dart`) en vez de `isSeller`; dashboard con el mismo
criterio. Tests: `MobilePermissionGuardsTest.php` (backend, 7 casos) +
`mobile/test/module_access_test.dart` (7 casos) + `flutter analyze`/`flutter test` limpios.

**Hallazgo adicional al revisar las funciones** (no estaba en el alcance original de este párrafo):
anular compra/documento en el API móvil no verificaba `annular_purchase`/`annular_sale` en el
servidor, solo la UI ocultaba el botón — un vendedor sin el flag podía anular por API directa
igual. Se agregó `abort_unless` en los 4 endpoints (mismo patrón que
`CashMovementController::correctPayments`). **Fuera de alcance todavía:** `credit-note`/`debit-note`
y `voidedSunat` del API móvil siguen sin ese guard.

> ⚠️ **Los permisos del móvil sirven para DIBUJAR la interfaz, no para autorizar.**
> Si se esconde el botón de Compras pero el endpoint no verifica, cualquiera con el token entra
> igual — ya se comprobó en esta sesión que el guard del router es solo cliente y la API no lo
> replica. **La autorización real va en el backend, endpoint por endpoint.** Esconder el botón es
> UX; sin verificación del lado del servidor no es seguridad.

### Eje 2 fase A — Desacoplar el alcance del `type` (el paso caro, y va ahora)
Hoy 22 scopes preguntan `type === 'admin'`. La tentación es diseñar de una los tres niveles
(propio / sede / todo). **No** — el paso caro no es el tercer nivel, es dejar de derivar el alcance
del `type`. Eso se hace ahora, con **dos valores nada más**:

1. Campo explícito `users.data_scope`, valores `own` y `all`.
2. Migración que lo llena **derivando del `type` actual** → comportamiento idéntico al de hoy,
   cero regresión.
3. Reemplazar los 22 hardcodeos por una llamada a **un único resolver**.

Cuando cuidarte pida "que el encargado vea toda su sede", se agrega `establishment` como tercer
valor **en el resolver**. Los 22 scopes no se tocan nunca más.

### ⚠️ I11 NO se cierra solo con esto — verificado

✅ **Los huecos puntuales de esta sección ya se cerraron** 2026-07-28 (`09945b27`, deployado a
prod): `requestedEstablishmentId` ahora exige `isAdmin()` para el escape `'all'`, y
`requestedWarehouseId` valida `canUse()` antes de aceptar un `warehouse_id` explícito. Fue un fix
puntual, **no** el resolver `data_scope` del eje 2 — ver `MobileEstablishmentAccessIsolationTest.php`.

Suena a que sí, pero no. Los tres huecos de I11 viven en **otro eje y otro camino de código**:
`MobileEstablishmentAccess::requestedEstablishmentId` (el escape `'all'` devuelve `null` sin mirar
`$user->type`; el `canUse()` solo cubre ids numéricos explícitos) y `requestedWarehouseId` (acepta
cualquier `warehouse_id` con solo validar `> 0`). Eso es **eje establecimiento, resuelto por
parámetro de request**; `whereTypeUser` es **eje usuario, aplicado sobre el query del modelo**.

Para que I11 deje de ser un parche por endpoint hay que **cablear el resolver también en
`MobileEstablishmentAccess`** — trabajo adicional deliberado, no un efecto lateral gratis.
Si se asume que se cierra solo, se lanza con el hueco abierto.

### Fuera de alcance (sigue post-lanzamiento)
El tercer valor `establishment` del resolver. No hay evidencia de que el caso exista: 17 de 18
clientes reales tienen una sola sede. El instrumento para diseñarlo son los 14 días de prueba
cerrada con cajeros reales.

---

## Sesión 5 — Reportes, actualización y presentación

Arreglar **I7, I8** y la tanda de presentación de la sección 3.

- **I7:** siete reportes descartan si la respuesta fue exitosa y pintan el estado vacío. Extraer un
  componente común que distinga error / vacío / con datos y aplicarlo a los siete. El patrón
  correcto ya existe en el reporte general.
- **I8:** la actualización automática encadena descarga e instalación sin confirmar.
- **Ortografía:** ~130 textos sin tildes ni eñes. Pasada completa por todos los strings de UI.
- **Contraste:** botón Cobrar 2,8:1 y stock crítico 2,15:1 → llevar a 4,5:1.
- **Áreas táctiles:** los +/− del carrito a 48 dp con feedback visual.
- **Unificar versión:** login v0.1.0, perfil v1.0.0, bundle 5.161.118 → una sola fuente.
- El aviso de sin conexión se dibuja debajo de la barra de estado.
- Sacar las 24 impresiones de depuración que vuelcan direcciones de clientes y búsquedas por DNI.

> La ortografía va **antes** de tomar las capturas de la ficha de Play.

---

## Lo que Claude Code no puede hacer

| # | Qué | Notas |
|---|-----|-------|
| **B5** | Decidir marca e identificador | Bloquea crear la app en Play Console → bloquea B6 (14 días). **Decidir esta semana.** Sugerencia: `pe.sistemax.sysfarma`, dejando `pe.sistemax.emito` libre para el otro producto. |
| **B3** | Política de privacidad | Se puede redactar el borrador a partir del inventario de datos de B4. Publicarla y cargar la URL es manual. |
| **B4** | Formulario de Seguridad de Datos | El inventario ya está en la auditoría. Copiarlo tal cual, sin adornar. |
| **B6** | Prueba cerrada | Armar el grupo de testers ya, con correos reales de boticas cliente. Arranca el día que haya primer AAB firmado. |

---

## Decisiones adicionales (no están en la auditoría)

**Despliegue por etapas.** Publicar al 5%, mirar 48 horas, después 20%, después todo. Play permite
detenerlo. Para una primera app que emite comprobantes esto es gratis y evita un incendio.

**Criterios de corte de la prueba cerrada.** Definirlos antes de arrancar, o los 14 días se vuelven
un trámite. Propuesta:
- Cero comprobantes duplicados
- Cero ventas perdidas de la cola
- Cero anulaciones sobre el documento equivocado
- Tasa libre de fallos > 99%

Si alguno no se cumple, no se sube a producción.

**Sobre I5 (modo offline):** no construirlo antes de lanzar. Cambiar el copy para que no prometa lo
que no hace, y dejar el offline real para v1.1. Son horas contra días, y evita la primera tanda de
reseñas de una estrella en una ficha sin historial.
