# Plan — Novasis Cobros (app móvil v1)

## Contexto

Extender la app RN existente `novasis-print` (hoy bridge HTTP→Bluetooth para imprimir recibos) hacia una **app móvil completa de cobranza en ruta**, manteniendo el bridge actual en paralelo. La app consume el backend NestJS existente (`smartfactvoice-backend`) vía un nuevo módulo `mobile/v1` optimizado para pantalla chica + idempotencia. Reutiliza por dentro los services existentes (cobros, clientes, caja, vendedores-cobradores, contabilidad). Branding visible: **Novasis Cobros**.

Proyectos involucrados:
- Backend: `/var/www/html/proyectos/smartfactvoice-backend`
- App móvil: `/var/www/html/proyectos/novasis-print` (se extiende, no se crea uno nuevo)
- Web (sin cambios funcionales en v1): `/var/www/html/proyectos/pos-ventas`

---

## Decisiones clave (cerradas en Q&A previo)

| Tema | Decisión |
|---|---|
| Proyecto | Extender `novasis-print`, mismo APK, mismo package id |
| Auth | Mismo login que web + selector de empresa si el usuario tiene varias |
| Conectividad | Online para cobrar, caché de lectura para navegar |
| Cobro | Imputado a facturas con saldo + multi-factura (un recibo cubre varias) |
| Formas de pago | Efectivo / Cheque / Transferencia |
| Cheque | Campos completos (banco, número, titular, emisión, vencimiento). **Sin foto** |
| Caja | Caja propia del cobrador con apertura / cierre / rendición |
| Anulación | Solo desde web (no en móvil v1) |
| Impresión | Llamada directa a `PrintQueue` + mismo template ESC/POS actual |
| Recibo sin impresora | Cobro se registra igual, queda en cola para reimprimir |
| Reportes | Resumen día / Mis cobros / Cartera vencida / Estado cuenta / Rendición pendiente |
| Cartera | Asignada priorizada + búsqueda libre |
| Geo | Lat/lon en cada cobro (sin mapa en v1) |
| Visual | Mismo tema que la web, dark + light toggle |
| Navegación | Bottom tabs (5 ítems) |
| Backend | Módulo `mobile/v1/*` con header `Idempotency-Key` |
| Push / OTA | No en v1 |
| Distribución | APK directo desde servidor propio + endpoint `/mobile/v1/version` |
| Multimoneda | PYG + USD desde v1 |
| Permisos Android | Bluetooth + Ubicación (cámara no necesaria) |
| Telemetría | Sentry |
| Android mínimo | API 26 (Android 8+) |
| i18n | `i18next` con `es` como único bundle inicial, estructura lista para más idiomas |

---

## Arquitectura global

```
[Android phone] Novasis Cobros (RN)
   ├─ AuthStack (Login + Empresa)
   ├─ AppStack
   │    └─ BottomTabs (Home, Cobros, Clientes, Reportes, Ajustes)
   ├─ Servicios singleton (existentes, intactos)
   │    ├─ HttpBridge      (HTTP local en :3005, sigue sirviendo a la LAN)
   │    ├─ PrintQueue       (FIFO persistente)
   │    ├─ PrinterService   (Bluetooth SPP)
   │    └─ ForegroundService
   └─ Servicios nuevos
        ├─ ApiClient        (axios + JWT + empresa header + idempotency)
        ├─ AuthService      (login, refresh, storage seguro)
        ├─ GeoService       (lat/lng al confirmar cobro)
        ├─ VersionCheck     (compara con /mobile/v1/version)
        └─ Cache (React Query + AsyncStorage persister)
                      │ HTTPS
                      ▼
[Backend NestJS]  src/mobile/v1/*
   ├─ auth, dashboard, clientes, cobros, caja, reportes, sistema
   ├─ IdempotencyInterceptor (mobile_idempotency)
   └─ Reusa services existentes: CobrosService, CajaService, ClientesService,
                                  LiquidacionesService, ContabilidadIntegracion
                      │
                      ▼
[PostgreSQL] tablas existentes + columnas nuevas (geo, origen, cobrador_asignado)
             + tabla nueva mobile_idempotency
```

---

## Fase 0 — Reestructuración del proyecto RN

Path: `/var/www/html/proyectos/novasis-print`

- **Display name** → "Novasis Cobros" (`app.json` + `android/app/src/main/res/values/strings.xml`).
- **Package id no cambia** (`com.smartfactprintbridge`) para no romper instalaciones existentes en clientes que ya usan el bridge.
- Nuevos assets: `assets/icon.png`, `assets/splash.png`, `assets/adaptive-icon.png`.
- Estructura de carpetas final:

```
src/
  app/                     # App.tsx, RootNavigator, providers
  auth/                    # LoginScreen, EmpresaSelectorScreen, AuthService, useAuth
  services/                # EXISTENTES: HttpBridge, PrintQueue, PrinterService
                           # NUEVOS:    ApiClient, AuthService, GeoService, VersionCheck
  screens/
    login/
    home/                  # dashboard del cobrador
    cobros/
      BuscarClienteScreen.tsx
      EstadoCuentaScreen.tsx
      NuevoCobroScreen.tsx
      ConfirmacionCobroScreen.tsx
    clientes/
      ClientesScreen.tsx
      ClienteDetalleScreen.tsx
    caja/
      AperturaCajaScreen.tsx
      CajaActualScreen.tsx
      RendicionScreen.tsx
    reportes/
      ReportesScreen.tsx
      HistorialCobrosScreen.tsx
    ajustes/               # contiene como sub-screens StatusScreen + SetupScreen actuales
  components/              # MoneyInput, KpiCard, ListItem, Badge, EmptyState, ScreenGuia
  theme/                   # tokens.ts, ThemeProvider.tsx (dark/light)
  i18n/                    # i18next config + locales/es.json
  api/                     # endpoints tipados de /mobile/v1/*
  hooks/                   # useAuth, useImpresion, useGeo, useCajaAbierta, useDeuda
  cache/                   # React Query persister
  types/                   # cobro.ts, cliente.ts, caja.ts (existentes preservados)
  utils/                   # fecha.ts (mirror utils/fecha.js de la web), money.ts
```

### Dependencias nuevas

- Navegación: `@react-navigation/native-stack` (ya hay `@react-navigation/native` y `bottom-tabs`).
- HTTP: `axios`.
- Datos / caché: `@tanstack/react-query` + `@tanstack/react-query-persist-client` + `@tanstack/query-async-storage-persister`.
- Storage rápido: seguir con `@react-native-async-storage/async-storage` (ya está; OK para v1).
- Geo: `react-native-geolocation-service`.
- Red: `@react-native-community/netinfo`.
- Telemetría: `@sentry/react-native`.
- i18n: `i18next`, `react-i18next`, `react-native-localize`.
- Formularios: `react-hook-form`, `zod`.
- UI: `styled-components`, iconos lucide vía `@iconify/react-native` o `react-native-vector-icons`.
- Fechas: `date-fns`, `date-fns-tz`.
- Config / env: `react-native-config`.

---

## Fase 1 — Backend: módulo `mobile/v1`

Path: `/var/www/html/proyectos/smartfactvoice-backend/src/mobile/`

### 1.1 Estructura

```
src/mobile/
  v1/
    mobile-v1.module.ts
    auth/
      mobile-auth.controller.ts          # POST /mobile/v1/auth/login
                                          # POST /mobile/v1/auth/refresh
                                          # GET  /mobile/v1/auth/empresas (del usuario actual)
                                          # POST /mobile/v1/auth/logout
    dashboard/
      mobile-dashboard.controller.ts     # GET  /mobile/v1/dashboard
    clientes/
      mobile-clientes.controller.ts      # GET  /mobile/v1/clientes?q=&cartera=true&vencida=true
                                          # GET  /mobile/v1/clientes/:id/estado-cuenta
    cobros/
      mobile-cobros.controller.ts        # POST /mobile/v1/cobros            (idempotent)
                                          # GET  /mobile/v1/cobros/mios?desde=&hasta=
                                          # GET  /mobile/v1/cobros/:id/recibo-data
    caja/
      mobile-caja.controller.ts          # GET  /mobile/v1/caja/actual
                                          # POST /mobile/v1/caja/abrir       (idempotent)
                                          # POST /mobile/v1/caja/cerrar      (idempotent)
                                          # POST /mobile/v1/caja/rendir      (idempotent)
    reportes/
      mobile-reportes.controller.ts      # GET  /mobile/v1/reportes/resumen-dia
                                          # GET  /mobile/v1/reportes/cartera-vencida
    sistema/
      mobile-sistema.controller.ts       # GET  /mobile/v1/version
    common/
      idempotency.interceptor.ts
      idempotency-store.service.ts
      guards/empresa-context.guard.ts    # extrae X-Empresa-Id + valida acceso del usuario
```

Cada controller **reutiliza los services existentes** (`CobrosService`, `ClientesService`, `CajaService`, etc.) y solo compone DTOs delgados pensados para móvil.

### 1.2 Idempotencia

Nueva tabla Prisma:

```prisma
model mobile_idempotency {
  key         String   @id
  empresa_id  String
  usuario_id  String
  endpoint    String
  response    Json
  status_code Int
  created_at  DateTime @default(now())
  @@index([empresa_id, created_at])
}
```

`IdempotencyInterceptor`:
- Si el request trae header `Idempotency-Key`, busca por `(key, endpoint)`.
- Si existe → devuelve la response cacheada sin re-ejecutar el handler.
- Si no existe → ejecuta el handler, persiste `{key, empresa, usuario, endpoint, response, status_code}`, devuelve.
- TTL: 7 días. Cron diario de limpieza (`@Cron('0 3 * * *')`).

Aplicado a:
- `POST /mobile/v1/cobros` (obligatorio)
- `POST /mobile/v1/caja/abrir | /cerrar | /rendir` (obligatorio)

### 1.3 Geolocalización en cobros

Migración:

```sql
ALTER TABLE cobros ADD COLUMN geo_lat NUMERIC(10,7);
ALTER TABLE cobros ADD COLUMN geo_lng NUMERIC(10,7);
ALTER TABLE cobros ADD COLUMN geo_accuracy NUMERIC(8,2);
ALTER TABLE cobros ADD COLUMN origen VARCHAR(20) DEFAULT 'web';  -- 'web' | 'mobile'
```

> El nombre exacto de la tabla de cobros se confirma en la implementación (verificar Prisma schema; el módulo `cobros` ya existe en backend).

El DTO de `POST /mobile/v1/cobros` acepta `geo: { lat, lng, accuracy } | null` opcional (puede venir vacío si el cobrador denegó permiso). El service guarda y marca `origen='mobile'`.

### 1.4 Cartera asignada

Si no existe `cliente.cobrador_asignado_id`:

```sql
ALTER TABLE clientes ADD COLUMN cobrador_asignado_id UUID
  REFERENCES vendedores_cobradores(id);
CREATE INDEX clientes_cobrador_asignado_idx ON clientes(cobrador_asignado_id);
```

Resolución en runtime:
- `GET /mobile/v1/clientes?cartera=true` filtra por `cobrador_asignado_id` que se mapea desde el usuario logueado vía `usuarios.vendedor_cobrador_id`.
- Si el usuario no está atado a un `vendedor_cobrador`, la app muestra mensaje "No tenés cartera asignada, podés buscar libremente".

### 1.5 Endpoint `/version`

```ts
GET /mobile/v1/version
→ {
    latest:           "1.2.0",
    minimo_soportado: "1.0.0",
    apk_url:          "https://<servidor>/apks/novasis-cobros-1.2.0.apk",
    changelog:        "- Cobros multi-moneda\n- Reportes del día"
  }
```

Configurable vía env vars (`MOBILE_LATEST`, `MOBILE_MIN_SUPPORTED`, `MOBILE_APK_URL`, `MOBILE_CHANGELOG`).

Comportamiento en la app:
- `current < minimo_soportado` → pantalla bloqueante con CTA "Descargar actualización".
- `minimo_soportado ≤ current < latest` → banner dismissible.
- `current >= latest` → sin aviso.

### 1.6 Permisos y guards

Sin rol nuevo en v1. El guard `EmpresaContextGuard`:
- Valida JWT.
- Lee `X-Empresa-Id` del header.
- Verifica que el usuario tenga acceso a esa empresa.
- Verifica permisos de módulos Cobros + Cajas (los mismos que la web).

Filtros adicionales (`cartera=true`, `mios`) se aplican por `usuarioId` extraído del JWT, no por rol nuevo.

### 1.7 DTOs principales

**`POST /mobile/v1/cobros`** request:
```ts
{
  cliente_id: string,
  caja_id: string,
  moneda_id: string,
  cotizacion?: number,            // requerido si la moneda del cobro != moneda factura
  imputaciones: [
    { factura_id: string, monto: number }
  ],
  forma_pago: 'efectivo' | 'cheque' | 'transferencia',
  cheque?: {
    banco_id: string, numero: string, titular: string,
    fecha_emision: string, fecha_vencimiento: string, diferido: boolean
  },
  transferencia?: { banco_id: string, numero_operacion: string, fecha: string },
  geo?: { lat: number, lng: number, accuracy: number },
  observacion?: string
}
```

**`POST /mobile/v1/cobros`** response:
```ts
{
  cobro_id: string,
  numero_recibo: string,
  recibo_data: { /* shape consumida por escpos-formatter */ }
}
```

---

## Fase 2 — Auth + selector de empresa (móvil)

`src/auth/`

- **LoginScreen**: form usuario + password → `POST /mobile/v1/auth/login`.
- **EmpresaSelectorScreen**: si la respuesta trae varias empresas (`empresas: [{ id, nombre, ruc }]`), muestra lista; si trae una sola, autoselect transparente.
- **AuthService**:
  - Guarda `accessToken + refreshToken + empresaId + usuarioId` en AsyncStorage.
  - Interceptor de axios agrega `Authorization: Bearer ...` + `X-Empresa-Id`.
  - En 401 intenta `/auth/refresh` una vez; si falla, navega a Login y limpia storage.
- Sesión larga: refresh token TTL 90 días, access token 1 día.

---

## Fase 3 — Caja del cobrador

Reuso del flujo de cajas/rendiciones existente (`RendicionesPanel` en la web es referencia visual).

- **AperturaCajaScreen**: si `GET /mobile/v1/caja/actual` devuelve `null`, pantalla obligatoria al ingresar → monto inicial + observación → `POST /mobile/v1/caja/abrir`.
- **CajaActualScreen**: badge en Home muestra "Caja abierta · Gs. X recaudado · N cobros". Tap → detalle con desglose por forma de pago.
- **RendicionScreen**: al cerrar el día → resumen + monto rendido + diferencia → `POST /mobile/v1/caja/rendir`. Impresión opcional del comprobante de rendición.
- Si hay **rendición pendiente** del día anterior, la pantalla Home la muestra destacada con CTA "Rendir ahora".

---

## Fase 4 — Flujo de cobro

### 4.1 Pantallas

```
BuscarCliente
   └─ ClienteEstadoCuenta (facturas con saldo + último pago + saldo total)
        └─ NuevoCobro
              ├─ selector multi-factura con monto a imputar por cada una
              ├─ forma de pago (efectivo / cheque / transferencia)
              ├─ moneda (PYG / USD) + cotización si difiere de moneda factura
              ├─ datos del cheque (condicional)
              ├─ datos transferencia (condicional)
              └─ Geo capturada en background al entrar a la pantalla
                    └─ ConfirmacionCobro
                          ├─ POST /mobile/v1/cobros con Idempotency-Key (uuid v4 generado al entrar)
                          ├─ Print directo: printQueue.enqueue(formatRecibo(reciboData))
                          └─ Pantalla éxito + acciones: Reimprimir / Volver al inicio
```

### 4.2 Multimoneda

- Si la factura está en USD, el campo monto se muestra en USD por default.
- Se puede cambiar a PYG (la app trae cotización del día desde `/mobile/v1/dashboard` cacheada por sesión).
- El backend valida y guarda el cobro con su moneda + cotización aplicada.

### 4.3 Recibo impreso

- Hook `useImpresion()` invoca `printQueue.enqueue(jobBuffer)` directamente (no HTTP local).
- Si `printerService.status !== 'connected'` al confirmar: registra el cobro igual y muestra toast "Recibo en cola, reconectá la impresora". El job queda persistido en `@smartfact_print_queue` (ya implementado).
- "Reimprimir" disponible en Historial → llama `GET /mobile/v1/cobros/:id/recibo-data` y re-arma el buffer.

### 4.4 Geo

`GeoService`:
- Al entrar a `NuevoCobro`, dispara `getCurrentPosition` con timeout 8s y `enableHighAccuracy: true`.
- Si el usuario denegó permiso → no bloquea, el cobro va sin geo.
- Si la lectura tarda más que el tap "Confirmar" → toma la última posición conocida o envía `null`.

---

## Fase 5 — Cartera + búsqueda + estado de cuenta

### 5.1 ClientesScreen (tab)

- Default: cartera asignada ordenada por `(dias_mora desc, monto_vencido desc)`.
- SearchBar arriba: si query ≥ 2 caracteres, llama `/mobile/v1/clientes?q=...` (sin filtro de cartera) con debounce 350 ms.
- Pull-to-refresh.
- Caché vía React Query con `staleTime: 5 min` + `gcTime: 24 h` para navegación offline.

### 5.2 ClienteDetalleScreen

- Header: razón social, RUC, teléfono (tap → marca), dirección (tap → abre Maps).
- KPIs: saldo total, deuda vencida, último pago.
- Lista de facturas con saldo (badges por estado: vigente / vencida).
- CTA principal: **Cobrar** → entra a `NuevoCobro` con cliente preseleccionado.

---

## Fase 6 — Reportes

`ReportesScreen` (tab) agrupa:

- **Resumen del día** (card): total recaudado hoy + desglose por forma de pago + cantidad recibos + clientes únicos. Endpoint `/mobile/v1/reportes/resumen-dia`.
- **Historial de mis cobros** → `HistorialCobrosScreen`: lista paginada filtrable por fecha. Cada item permite "Reimprimir" o "Ver detalle".
- **Cartera vencida**: reutiliza `ClientesScreen` con filtro `?vencida=true`.
- **Rendición pendiente** del día anterior: card destacada si existe.

---

## Fase 7 — Tema, navegación, i18n

### 7.1 Theme

`src/theme/tokens.ts` — mirror de tokens de la web:

```ts
export const darkTokens = {
  bg: '#0f1419', bg2: '#1a1f2e', bg6: '#232838',
  primary: '#f97316',
  borderColor: '#2a3142',
  textPrimary: '#e2e8f0', textSecondary: '#94a3b8',
  ok: '#22c55e', warn: '#f59e0b', error: '#ef4444', info: '#3b82f6',
};
export const lightTokens = { /* equivalente */ };
```

`ThemeProvider` con context + `useColorScheme()` del SO + override manual guardado en AsyncStorage.

### 7.2 Navegación

```
RootStack
 ├─ AuthStack            (LoginScreen, EmpresaSelectorScreen)
 └─ AppStack
      └─ Tabs (Bottom)
           ├─ Home        (dashboard + caja + accesos rápidos)
           ├─ Cobros      (BuscarCliente → flujo cobro)
           ├─ Clientes    (cartera + search + detalle)
           ├─ Reportes
           └─ Ajustes     (impresora, tema, idioma, logout)
```

### 7.3 i18n

- `i18next` + `react-i18next` + `react-native-localize`.
- Solo `locales/es.json` en v1, estructurado por dominios:

```
locales/es.json
  auth.login.titulo, auth.login.usuario, ...
  cobros.nuevoCobro.titulo, ...
  caja.apertura.montoInicial, ...
  reportes.resumenDia.titulo, ...
  ajustes.impresora.titulo, ...
```

- Listo para agregar `gn.json`, `en.json` sin tocar componentes.

---

## Fase 8 — Caché offline (modo b)

React Query como capa principal:
- Persistencia: `react-query-async-storage-persister` (clientes y estado-cuenta se preservan al cerrar la app).
- TTLs: `staleTime: 5 min`, `gcTime: 24 h`, `refetchOnWindowFocus: true`.
- Mutaciones (POST /cobros, /caja/*) **NO** se encolan offline — si no hay red, error claro "Sin conexión, intentá de nuevo cuando vuelva la señal".
- Detección de red con `@react-native-community/netinfo`; badge global "Sin conexión" en el header.

---

## Fase 9 — Sentry + version check

- `Sentry.init({ dsn })` en `App.tsx` (DSN en `.env` vía `react-native-config`).
- `ErrorBoundary` global con pantalla fallback.
- Tag `release` con `package.json` version.
- Al abrir la app: `GET /mobile/v1/version`:
  - Si `current < minimo_soportado` → pantalla bloqueante con CTA "Descargar".
  - Si `current < latest` → banner dismissible.

---

## Fase 10 — Convivencia con el bridge actual

**Crítico**: el bridge HTTP no debe romperse — clientes lo usan en producción.

- `HttpBridge`, `PrintQueue`, `PrinterService`, `ForegroundService`, módulos nativos Android → **intactos**.
- En `App.tsx`, **solo si hay sesión guardada** se navega al `AppStack`; si no, va al `LoginScreen`.
- El bridge HTTP arranca **independiente del login** (como hoy), siempre que haya MAC de impresora guardada.
- Las pantallas `StatusScreen` y `SetupScreen` actuales se preservan como sub-pantallas dentro de la tab "Ajustes" del nuevo bottom-tabs.

---

## Archivos críticos

### Backend nuevos
```
src/mobile/v1/mobile-v1.module.ts
src/mobile/v1/auth/mobile-auth.controller.ts
src/mobile/v1/auth/dto/*.ts
src/mobile/v1/dashboard/mobile-dashboard.controller.ts
src/mobile/v1/clientes/mobile-clientes.controller.ts
src/mobile/v1/cobros/mobile-cobros.controller.ts
src/mobile/v1/cobros/dto/crear-cobro.dto.ts
src/mobile/v1/caja/mobile-caja.controller.ts
src/mobile/v1/reportes/mobile-reportes.controller.ts
src/mobile/v1/sistema/mobile-sistema.controller.ts
src/mobile/v1/common/idempotency.interceptor.ts
src/mobile/v1/common/idempotency-store.service.ts
src/mobile/v1/common/guards/empresa-context.guard.ts
prisma/migrations/<ts>_mobile_idempotency/migration.sql
prisma/migrations/<ts>_cobros_geo_origen/migration.sql
prisma/migrations/<ts>_cliente_cobrador_asignado/migration.sql
```

### Backend reusar (sin modificar)
```
src/cobros/cobros.service.ts
src/clientes/clientes.service.ts
src/caja/caja.service.ts
src/vendedores-cobradores/*
src/auth/* (JwtAuthGuard, login service)
src/contabilidad/services/integracion.service.ts (los cobros móviles disparan el mismo asiento)
src/utils/utilidades.ts (formatDateResponse)
```

### Móvil nuevos
```
src/app/App.tsx                    (reemplaza el actual; root navigator + providers)
src/app/navigation/RootNavigator.tsx
src/app/navigation/AppTabs.tsx
src/auth/LoginScreen.tsx
src/auth/EmpresaSelectorScreen.tsx
src/auth/AuthService.ts
src/auth/useAuth.ts
src/services/ApiClient.ts
src/services/GeoService.ts
src/services/VersionCheck.ts
src/screens/home/HomeScreen.tsx
src/screens/cobros/{BuscarClienteScreen,EstadoCuentaScreen,NuevoCobroScreen,ConfirmacionCobroScreen}.tsx
src/screens/clientes/{ClientesScreen,ClienteDetalleScreen}.tsx
src/screens/caja/{AperturaCajaScreen,CajaActualScreen,RendicionScreen}.tsx
src/screens/reportes/{ReportesScreen,HistorialCobrosScreen}.tsx
src/screens/ajustes/AjustesScreen.tsx
src/theme/{tokens.ts, ThemeProvider.tsx, useTheme.ts}
src/i18n/{index.ts, locales/es.json}
src/components/{MoneyInput,KpiCard,ListItem,Badge,EmptyState,ScreenGuia}.tsx
src/cache/persister.ts
src/types/{cobro.ts, cliente.ts, caja.ts, recibo.ts}
src/utils/{fecha.ts, money.ts}
src/api/{auth.ts, dashboard.ts, clientes.ts, cobros.ts, caja.ts, reportes.ts, sistema.ts}.ts
```

### Móvil preservar (sin tocar lógica)
```
src/services/HttpBridge.ts
src/services/PrintQueue.ts
src/services/PrinterService.ts
src/printer/escpos-formatter.ts
android/.../httpserver/*
android/.../foreground/*
```

### Móvil modificar mínimamente
```
App.tsx                          → reemplaza por root con NavigationContainer y conditional Auth/App
src/screens/StatusScreen.tsx     → mover como sub-screen de la tab Ajustes
src/screens/SetupScreen.tsx      → mover como sub-screen de la tab Ajustes
app.json                         → display name "Novasis Cobros"
android/.../strings.xml          → app_name "Novasis Cobros"
android/app/build.gradle         → minSdkVersion 26 (verificar)
```

---

## Migraciones de base de datos (resumen)

1. **`<ts>_mobile_idempotency`** — tabla `mobile_idempotency` para idempotencia de POSTs.
2. **`<ts>_cobros_geo_origen`** — agrega `geo_lat`, `geo_lng`, `geo_accuracy`, `origen` a la tabla de cobros.
3. **`<ts>_cliente_cobrador_asignado`** — agrega `cobrador_asignado_id` a la tabla `clientes` con FK e índice (si no existe ya).

Las tres son aditivas y backwards-compatible: la web sigue funcionando sin cambios.

---

## Env vars nuevas (backend)

```
MOBILE_LATEST=1.0.0
MOBILE_MIN_SUPPORTED=1.0.0
MOBILE_APK_URL=https://files.novasispy.com/apks/novasis-cobros-latest.apk
MOBILE_CHANGELOG=Versión inicial
MOBILE_IDEMPOTENCY_TTL_DAYS=7
```

## Env vars nuevas (móvil, vía `react-native-config`)

```
API_BASE_URL=https://api.novasispy.com
SENTRY_DSN=...
```

---

## Verificación end-to-end

1. Backend: migraciones aplican. `GET /mobile/v1/version` responde.
2. APK instala sobre la versión actual sin perder pareo de impresora ni cola pendiente.
3. Sin sesión: abre en `LoginScreen`. El bridge HTTP sigue activo y otra app puede POSTear a `:3005/recibo`.
4. Login válido con usuario multi-empresa → muestra selector → al elegir entra a Home.
5. Si no hay caja abierta → modal obligatorio de apertura.
6. Tab Cobros → buscar cliente → ver estado de cuenta → cobrar 2 facturas con efectivo + 1 con cheque → confirmar → recibo se imprime y aparece toast OK.
7. Verificar en la web (`app.novasispy.com`): el cobro figura, con `origen='mobile'`, lat/lng guardados, asiento contable generado por `ContabilidadIntegracionService`.
8. Apagar la impresora antes de confirmar otro cobro → cobro se registra, recibo queda en cola, al volver la impresora la cola drena sola.
9. Cortar internet → buscar cliente cacheado funciona, intento de cobro muestra error "sin conexión", no se duplica al reintentar (Idempotency-Key).
10. Reintento con misma `Idempotency-Key` después de timeout → backend devuelve la response cacheada, sin duplicar cobro.
11. Rendir caja al final del día → resumen + diferencia + comprobante impreso.
12. Cambiar `MOBILE_LATEST` en el backend → al reabrir la app aparece banner de actualización; cambiar `MOBILE_MIN_SUPPORTED` → pantalla bloqueante.
13. Sentry recibe un crash forzado de test (`Sentry.captureException(new Error('test'))`).
14. Toggle dark/light persiste tras reabrir la app.
15. APK funciona en Android 8 (emulador API 26).
16. Cobro en USD sobre factura USD → asiento se genera con cotización correcta.
17. Usuario sin `vendedor_cobrador_id` ve mensaje "sin cartera asignada" pero puede buscar libremente.

---

## Fuera de alcance v1

- Push notifications (FCM).
- Cobros offline con sync.
- Anulación de cobros desde móvil.
- Recibo a cuenta (cobro no imputado a factura).
- Foto del cheque.
- Mapa de clientes por cercanía.
- Metas / ranking de cobradores.
- Facturación móvil (queda en v2).
- OTA updates (CodePush / Expo Updates).
- Publicación en Play Store.
- iOS.
- Multi-idioma real (estructura i18n queda lista, pero solo `es` se traduce).

---

## Roadmap de implementación sugerido

1. **Backend Fase 1**: módulo `mobile/v1`, migraciones, idempotencia, endpoints mock-funcionales con DTOs cerrados.
2. **Móvil Fase 0**: reestructuración de carpetas + tema + navegación (esqueleto navegable sin lógica de negocio).
3. **Móvil Fase 2**: auth completo end-to-end con backend real.
4. **Flujo de cobro vertical** (Login → Home → Cobros → Cobrar → Imprimir) con un solo cliente de prueba.
5. **Caja + rendición**.
6. **Reportes + historial**.
7. **Sentry + version check + polishing visual**.
8. **Testing en dispositivo real con impresora SAT AF330**.
9. **Build de release + firma + distribución del APK**.

---

## Relación con módulos existentes

- **NO modificar**: módulo `cobros`, `clientes`, `caja`, `auth`, `contabilidad` (los reusamos como librerías internas vía DI).
- **Web** (`pos-ventas`): sin cambios funcionales. Eventualmente se puede agregar una columna "Origen" en la lista de cobros para distinguir web/móvil, pero no es bloqueante en v1.
- **Bridge de impresión** (`novasis-print` actual): preservado tal cual, sigue funcionando para clientes ya instalados.
- Branding diferenciado: "Novasis Cobros" (la app) vs "Novasis ERP" (la web) vs "SmartFactPrintBridge" (nombre interno del package Android, no visible al usuario).

---

## Actualización v1.1 — Paridad con CobrosTemplateV2 (web) + UX antibobo

Tras revisar el flujo web (`CobrosTemplateV2.jsx`), se alinea el módulo móvil a la **misma lógica de distribución** que la web y se rediseña la UX de cobro a una **única pantalla** auto-defaulteada con **impresión Bluetooth automática**.

### Cambios funcionales

| Tema | Decisión actualizada |
|---|---|
| Distribución de pago | **No pagos parciales libres**: cuotas se cubren al 100% en orden. Se usan endpoints `previewDistribucion` + `createFromDistribucion` (reuso del service web). |
| Descuentos | **Solo descuentos autorizados** (`autorizaciones_descuento`): si hay una autorización activa para el cliente/factura del usuario, banner verde + toggle aplicar. No hay descuento libre en móvil. |
| Mora | Si el submódulo `ADM_INTERES_MORA` está activo, aviso informativo en pantalla (la mora se computa automáticamente al confirmar). |
| Medios de pago | **Dinámicos** desde `empresa_medio_pago` (no hardcoded enum). El código del medio mapea a `forma_pago` legacy (1=efectivo, 2=cheque, 5=transferencia). |
| UX de cobro | **Single-screen** (se eliminó `ConfirmacionCobroScreen`). Auto-defaults: monto = total cuotas, medio = primero disponible. Botones de monto rápido (+5k/+10k/+50k/+100k + Total exacto). |
| Impresión | **Auto-print** Bluetooth al confirmar, sin intervención. Tarjeta de estado live (`printerService.onStatusChange`) con CTA "Reimprimir" + "Revisar configuración de impresora" cuando no está conectada. |
| Navegación | CTA "Revisar configuración" navega cross-stack a `AjustesTab → ConfigImpresora` via `CommonActions.navigate`. |

### Backend — implementado

```
src/mobile/v1/cobros/dto/crear-desde-distribucion-mobile.dto.ts   (NUEVO)
src/mobile/v1/cobros/mobile-cobros.service.ts                     (+5 métodos)
src/mobile/v1/cobros/mobile-cobros.controller.ts                  (+4 endpoints)
src/mobile/v1/mobile-v1.module.ts                                 (+CobranzasModule)
src/mobile/v1/dashboard/mobile-dashboard.service.ts               (+submodulos.interes_mora)
```

Nuevos endpoints:
- `POST /mobile/v1/cobros/preview-distribucion` — preview en vivo (reusa `CobrosService.previewDistribucion`).
- `POST /mobile/v1/cobros/crear-desde-distribucion` — crea cobro (idempotent, marca `autorizacion_descuento` usada, setea `origen='mobile'` + geo).
- `GET /mobile/v1/cobros/medios-pago` — medios de pago activos de la empresa.
- `GET /mobile/v1/cobros/factura/:facturaId/descuento-activo` — autorización vigente para el usuario, si existe.

Helper privado `submoduloActivo(empresaId, codigo)` replica la lógica de `SubmoduloGuard` (suscripción Activa/EnGracia + addon/incluido).

### Móvil — implementado

```
src/types/domain.ts                       (+MedioPago, DescuentoAutorizado, PreviewDistribucion*, CrearDesdeDistribucionRequest, DashboardResponse.submodulos)
src/api/cobros.ts                         (+mediosPago, previewDistribucion, crearDesdeDistribucion, getDescuentoActivo)
src/screens/cobros/NuevoCobroScreen.tsx   (REWRITE — single-screen + auto-print + preview debounce 350ms)
src/screens/cobros/ConfirmacionCobroScreen.tsx   (ELIMINADO)
src/app/navigation/AppTabs.tsx            (rama ConfirmacionCobro removida)
src/app/navigation/types.ts               (ConfirmacionCobro removido)
```

### Verificación adicional

18. `POST /mobile/v1/cobros/preview-distribucion` con cuotas seleccionadas devuelve `resumen.total_aplicado` consistente con el cálculo de la web.
19. Cliente con autorización activa → banner verde aparece; al aplicar y confirmar, la autorización queda marcada como `Usada` y no reaparece en el siguiente cobro.
20. Empresa con `ADM_INTERES_MORA` activo → aviso informativo visible; mora se aplica al confirmar.
21. Empresa sin medios de pago configurados en `empresa_medio_pago` → mensaje claro en pantalla y CTA deshabilitado.
22. Impresora apagada al confirmar → cobro registrado, tarjeta amber con "Revisar configuración" → tap navega a `AjustesTab/ConfigImpresora`. Al reconectar, cola drena sola.
23. Reconexión Bluetooth durante la pantalla de éxito → la tarjeta cambia a verde en vivo (suscripción a `printerService.onStatusChange`).

### Fuera de alcance también en v1.1

- Descuento libre sin autorización (queda como flujo web).
- Recibo a cuenta / pagos parciales (sigue como flujo web).
- Edición de cotización USD/PYG en línea (se usa la del dashboard).

---

## Actualización v1.2 — Rendición móvil + pulidos UX

Se habilita el flujo de **rendición de cobranzas desde el teléfono** para que el cobrador cierre el día sin pasar por la web. El administrador (tesorería) solo verifica/aprueba desde el panel web.

### Decisiones

| Tema | Decisión |
|---|---|
| Rendición desde móvil | El cobrador rinde **todos los recibos del día no rendidos** en una sola acción. No se permite rendir recibos sueltos (parity con web). |
| Apertura/cierre de caja en móvil | **Fuera de alcance v1.2** — el backend soporta sesión de caja, pero el endpoint `POST /mobile/v1/caja/rendir` permite rendir sin sesión explícita. Las pantallas `AperturaCajaScreen` / `CajaActualScreen` quedan en el repo pero **no wireadas en navegación**. |
| Confirmación destructiva | `Alert.alert` nativo antes de rendir, con resumen (cantidad de recibos, total declarado) + ⚠ si el monto declarado difiere del calculado por el sistema. |
| Formato de fecha en listas | `aaaa-mm-dd hh:mm:ss` en zona horaria local (no ISO con `Z`). |
| Identidad del cliente | Cada recibo en la lista muestra **razón social + RUC** (datos ya incluidos en el response del backend). |
| Inputs de dinero | Todos los inputs numéricos de moneda usan `MoneyInput` (formato live: PYG → `1.000.000`, otras → `10.000,12`). Se eliminaron `TextInput` con `keyboardType="number-pad"` para montos. |

### Bug fixes en flujo de cobro (`NuevoCobroScreen`)

| Bug | Causa | Fix |
|---|---|---|
| Descuento autorizado no mostraba el monto y no se aplicaba | Mobile leía `descuentoAuth.monto` / `.porcentaje`; backend devuelve `descuento_monto` / `descuento_porcentaje` (Prisma) | Renombrado en `DescuentoAutorizado` (domain.ts) y en 3 lugares de `NuevoCobroScreen.tsx`. |
| CTA "Cobrar" mostraba total bruto sin restar descuento | `montoRecibido` se inicializaba una vez con `totalSeleccionado` y nunca recomputaba al togglear descuento | Reemplazado el `inicializado.current` por memo reactivo `netoSugerido = total − descuento` + `useEffect` que setea `montoRecibido = netoSugerido` mientras el usuario no haya tipeado manualmente (`overrideManual.current`). |
| Resumen confundía "Total a aplicar" (imputado a cuotas) con "Total a cobrar" (efectivo) | Backend trata `montoEfectivoAplicar = monto_recibido + descuento` | El resumen ahora muestra **"Imputado a cuotas"** (solo si hay descuento) y **"Total a cobrar"** = `montoRecibido`. CTA también usa `montoRecibido`. |

### Móvil — implementado en v1.2

```
src/types/domain.ts                       (+ReciboDisponible.clientes.personas, fix DescuentoAutorizado fields)
src/app/navigation/types.ts               (+HomeStackParamList.Rendicion)
src/app/navigation/AppTabs.tsx            (+HomeStack.Screen Rendicion)
src/screens/home/HomeScreen.tsx           (+botón "Rendición" secundario)
src/screens/caja/RendicionScreen.tsx      (PRE-EXISTENTE, ajustada: cliente visible, fecha formateada, MoneyInput, Alert de confirmación)
src/screens/cobros/NuevoCobroScreen.tsx   (fix descuento + resumen + CTA)
```

> Nota: `src/api/caja.ts` (`recibosDisponibles`, `rendir`) y `RendicionScreen.tsx` ya existían pre-v1.2 pero estaban sin entrypoint en la navegación.

### Backend — sin cambios en v1.2

Endpoints reutilizados (ya implementados):

- `GET /mobile/caja/recibos-disponibles` — recibos del cobrador no rendidos (incluye `clientes.personas.razon_social/ruc`, `moneda`, `recibo_cobro_detalle`).
- `POST /mobile/caja/rendir` — idempotente; crea rendición en estado **PENDIENTE** lista para verificación.

### Mejora del ticket impreso (paridad con `recibo_ticket.js` web)

Aunque entró en commits del mismo ciclo, se documenta acá para consistencia:

- `CobrosService.buildReciboPayload` (NUEVO, privado) — fuente única de verdad del payload del recibo. Reutilizado por `generateReciboPdf` (web) y `getReciboPrintData` (mobile).
- `ReciboData` extendido en `src/types/domain.ts`: `sucursal`, `cobrador.nombre`, `formas_pago[]` con banco/cheque/tarjeta, `detalles[]` con `nro_cuota`, `total_cuotas`, `fecha_vencimiento`, `items_factura`, `saldo_factura`, `observacion`, `saldo_cuenta`.
- `src/printer/escpos-formatter.ts` reescrito para emitir la misma estructura visual que el ticket PDF: EMPRESA → SUCURSAL → título → CLIENTE → DETALLE agrupado por factura → FORMAS DE PAGO → TOTAL doble alto → cobrador → observación → footer.

### Verificación adicional v1.2

24. `GET /mobile/caja/recibos-disponibles` con cobrador autenticado devuelve recibos del día con `clientes.personas` poblado.
25. RendicionScreen lista recibos con cliente + RUC + fecha local `aaaa-mm-dd hh:mm:ss`.
26. Editar "Monto rendido" muestra formato en vivo según moneda (PYG sin decimales).
27. Tap en "Rendir" abre Alert nativo con resumen; cancelar no dispara mutación; confirmar dispara `POST /mobile/caja/rendir` con `Idempotency-Key`.
28. Si `total_declarado ≠ total sistema`, el Alert muestra ⚠ con la diferencia.
29. Cobro con descuento autorizado: monto recibido y CTA reflejan `total − descuento`; backend recibe `monto_recibido` neto + `descuento_monto` y crea cobro sin sobrante.
30. Ticket impreso ESC/POS muestra empresa, sucursal, cobrador, agrupación por factura con cuotas, formas de pago con banco/cheque y footer "¡Muchas gracias por su pago!".

### Fuera de alcance v1.2

- Apertura/cierre de sesión de caja desde móvil (pantallas existen pero no wireadas).
- Edición de recibos a rendir (todo-o-nada).
- Vista previa del estado de la rendición tras enviar (solo se muestra "Rendición enviada / pendiente de aprobación").

---

## Actualización v1.3 — Integración con Workflow de cobranza (planeada, 2026-06-22)

> Documenta los gaps detectados entre el app móvil actual y lo implementado en pos-ventas (Submódulos 4, 6, 7 + Mora avanzada + Mesa de Gestión + Promesas enriquecidas Opción A) y el estado de la **rendición de cobranza**. Es el alcance propuesto para la próxima iteración del app del cobrador de ruta.

### Contexto

Tras el cierre del Submódulo 7 (Workflow de cobranza) y la introducción de la Mesa de Gestión web (2026-06-22), el app móvil quedó atrasado en tres ejes:

1. **Trazabilidad de visitas sin cobro** — el cobrador hoy sólo registra recibos; no hay forma de dejar trazado un "no atiende / dirección incorrecta / rechazo a pagar". El reporte de productividad y la Mesa de Gestión de oficina pierden información.
2. **Datos pobres en promesas mobile** — promesa creada desde el campo cae como un `promesas_pago` simple (fecha + monto), sin cuotas asociadas, sin tipo de evidencia, sin pagador. Mientras la web crea promesas ricas (Opción A) con prorrateo por cuota.
3. **Avisos críticos faltantes** — el cobrador no se entera cuando el cliente está en gestión legal (`INFORMCONF`, `DEMANDA`, `INCOBRABLE`) o cuando una factura fue refinanciada. Riesgo operativo y reputacional.

Además, la **rendición de cobranza** quedó manca: el cobrador puede crearla (`POST /mobile/caja/rendir`) pero no puede consultar el estado posterior (BORRADOR → PENDIENTE → APROBADO / OBSERVADO / RECHAZADO). El backend ya expone el ciclo completo en `src/rendiciones/*` (web lo usa), falta espejarlo mobile.

### Backend nuevo (Fase M.B)

```
GET    /api/v1/mobile/rendiciones                     listar mis rendiciones (paginado, filtro por estado)
GET    /api/v1/mobile/rendiciones/:id                 detalle (incluye historial de estados + observaciones tesorería)
GET    /api/v1/mobile/rendiciones/resumen-cobrador    KPIs del cobrador: pendientes_aprobacion, observadas, total_rendido_mes
POST   /api/v1/mobile/gestiones                       registrar resultado de visita (no atiende / promesa / cobrado / rechazo / etc.)
GET    /api/v1/mobile/gestiones/proximas              tareas agendadas del cobrador (gestiones con `proxima_fecha = hoy`)
GET    /api/v1/mobile/clientes/:id/alertas            payload consolidado: mora_avanzada (estado COB_GMR), refinanciacion_activa, promesas_vigentes
```

Reglas:
- Reutilizar `RendicionesService.findAll` con filtro `cobrador_id = me` (vendedores_cobradores del usuario logueado). No exponer rendiciones ajenas.
- `POST /mobile/gestiones` reusa `GestionesService.create` con header `Idempotency-Key` (consistente con el resto de mobile/v1). Para promesa enriquecida acepta `promesa_cuota_ids[]`, `promesa_tipo_evidencia` (default `VERBAL` en móvil), `promesa_pagador_tipo` (default `CLIENTE`).
- `/clientes/:id/alertas` agrupa en una sola request lo que el Estado de cuenta hoy no muestra. Cachear en mobile con `staleTime: 60_000`.

### Móvil — pantallas nuevas (Fase M.M)

| Pantalla | Path nav | Acceso desde |
|---|---|---|
| `MisRendicionesScreen` | `MisRendiciones` | Caja → "Mis rendiciones" |
| `RendicionDetalleScreen` | `RendicionDetalle/:id` | tap en una rendición del listado |
| `RegistrarGestionScreen` | `RegistrarGestion/:clienteId` | botón flotante en `EstadoCuentaScreen` cuando NO hay cobro a registrar |
| `MiAgendaScreen` | `MiAgenda` | Home: badge "N tareas hoy" |
| Banner de alertas | inline en `EstadoCuentaScreen` | bloqueante si mora avanzada activa |

### Móvil — modificaciones

- `RendicionScreen` (existente): después del submit exitoso, navegar a `RendicionDetalle/:id` en vez de mostrar sólo "Rendición enviada". El usuario debe quedar viendo el estado real.
- `EstadoCuentaScreen`:
  - **Banner rojo** "⚠ Cliente en gestión legal (INFORMCONF/DEMANDA) — coordinar con oficina antes de cobrar" si `alertas.mora_avanzada != null`. Bloquea el botón de cobrar (o pide confirmación adicional).
  - **Badge amarillo** "♻ Cuotas refinanciadas — cuotas viejas no se cobran" si `alertas.refinanciacion_activa`. El backend ya filtra de `getCuotasPendientes`, pero el cobrador debe entender por qué hay menos cuotas que las que recuerda.
  - **Botón "Registrar visita sin cobro"** abajo, abre `RegistrarGestionScreen` con `tipo_gestion: VISITA` precargado.
- `NuevoCobroScreen` (en el modal de "Promesa de pago" que reemplazará al input simple):
  - Selector multi-cuota (checkbox con saldo) — la misma UX que la web Mesa de Gestión.
  - Tipo de evidencia: solo Verbal / WhatsApp (no Email / Documento — no aplica al campo).
  - Pagador: solo Cliente / Garante (con `searchClientes` mobile).
  - Una sola request `POST /mobile/gestiones` con `resultado: PROMESA_PAGO + promesa_cuota_ids[] + promesa_tipo_evidencia + promesa_pagador_tipo`.
- `HomeScreen`: agregar badge "📋 N tareas hoy" si `getProximasGestiones().length > 0`.

### Tipos a agregar en `types/domain.ts`

```ts
export type RendicionEstado = 'BORRADOR' | 'PENDIENTE' | 'APROBADO' | 'OBSERVADO' | 'RECHAZADO';

export interface RendicionListItem {
  id: string;
  numero_rendicion: number;
  estado: RendicionEstado;
  total_declarado: number;
  total_recibos: number;
  cantidad_recibos: number;
  moneda?: { codigo: string };
  fecha_envio?: string | null;
  fecha_aprobacion?: string | null;
  observacion_tesoreria?: string | null;
}

export interface RendicionHistorialItem {
  fecha: string;
  estado_anterior: RendicionEstado | null;
  estado_nuevo: RendicionEstado;
  motivo?: string;
  usuario_nombre?: string;
}

export interface RendicionDetalle extends RendicionListItem {
  recibos: ReciboHistorial[];
  historial: RendicionHistorialItem[];
}

export type CobTipoGestion = 'VISITA' | 'LLAMADA' | 'WHATSAPP' | 'EMAIL' | 'SMS' | 'OTRO';
export type CobResultadoGestion =
  | 'COBRADO_TOTAL' | 'COBRADO_PARCIAL' | 'PROMESA_PAGO'
  | 'NO_ATIENDE' | 'CLIENTE_AUSENTE' | 'DIRECCION_INCORRECTA'
  | 'CLIENTE_DISPUTA' | 'RECHAZO_PAGAR' | 'COMPROMISO_LLAMADA_POSTERIOR'
  | 'CLIENTE_FALLECIDO' | 'OTRO';

export interface CrearGestionRequest {
  cliente_id: string;
  factura_cab_id?: string;
  tipo_gestion: CobTipoGestion;
  resultado: CobResultadoGestion;
  observacion?: string;
  proxima_accion?: string;
  proxima_fecha?: string;
  geo?: GeoInput;
  // Solo si resultado === PROMESA_PAGO
  promesa_fecha?: string;
  promesa_monto?: number;
  promesa_cuota_ids?: string[];
  promesa_tipo_evidencia?: 'VERBAL' | 'WHATSAPP';
  promesa_pagador_tipo?: 'CLIENTE' | 'GARANTE';
  promesa_pagador_cliente_id?: string;
}

export interface ClienteAlertas {
  mora_avanzada: { estado: string; fecha_ingreso: string; observacion?: string } | null;
  refinanciacion_activa: { id: string; cuotas_nuevas: number } | null;
  promesas_vigentes: number;
  promesas_incumplidas: number;
}
```

### Roadmap propuesto

1. **Fase M.B.1** — Backend: endpoints `mobile/rendiciones/*` (GET listar + GET :id + GET resumen) reutilizando `RendicionesService` con scope al cobrador del token. Sin migraciones.
2. **Fase M.M.1** — Móvil: `MisRendicionesScreen` + `RendicionDetalleScreen` + redirección desde `RendicionScreen` tras submit. **Cierra el gap funcional inmediato** del usuario.
3. **Fase M.B.2** — Backend: `POST /mobile/gestiones` (wrapper de `GestionesService.create` con idempotencia + scope cobrador) + `GET /mobile/clientes/:id/alertas`.
4. **Fase M.M.2** — Móvil: banner de alertas + botón "Registrar visita sin cobro" → `RegistrarGestionScreen` con quick-buttons por resultado (No atiende, Ausente, Dirección incorrecta, Rechazo, Promesa, Compromiso re-visita).
5. **Fase M.M.3** — Móvil: extender modal de promesa con selector multi-cuota + evidencia + pagador (Opción A en mobile).
6. **Fase M.B.3 + M.M.4** — Backend `GET /mobile/gestiones/proximas` + pantalla `MiAgendaScreen` + badge en Home.

### Fuera de alcance v1.3

- Edición/anulación de rendiciones desde el móvil (la web ya cubre estos casos en Tesorería).
- Adjuntos / fotos en gestiones (`adjuntos: Json`) — postergado hasta tener storage S3 o equivalente configurado.
- Inicio de gestión de mora avanzada (`COB_GMR_*`) desde el campo — sigue siendo prerrogativa de oficina/legales, el cobrador solo lee el estado.
- Mesa de Gestión / worklist priorizada en mobile — el cobrador trabaja por ruta/zona, no por priorización de mora.

### Verificación end-to-end (esperada)

- `MisRendiciones` lista al menos 1 entrada con estado correcto tras crear una rendición.
- Cambio de estado en backend (web Tesorería aprueba) → pull-to-refresh en mobile refleja `APROBADO`.
- Registrar "No atiende" en un cliente desde el campo → aparece en `Mesa de Gestión` web del gestor de oficina con badge "Yo" (si el usuario logueado coincide).
- Cliente con gestión `INFORMCONF` activa → banner rojo bloqueante en `EstadoCuenta`.
- Promesa creada desde mobile con 3 cuotas seleccionadas → en la base hay 3 filas en `promesas_pago` con `monto_prometido` prorrateado y `cuota_id` correcto.

### Principios UX (audiencia: cobradores que nunca usaron un app)

Esta es la condición operativa **más importante** de v1.3 y debe atravesar cada pantalla nueva. Si rompe alguna de estas reglas, no se acepta el PR.

**1. Un solo botón por vista.** Cada pantalla tiene una única acción principal grande (mínimo 56dp, ancho ≥ 70% del viewport, color primario). Acciones secundarias quedan como texto/link discreto debajo. El usuario nunca duda qué tocar primero.

**2. Lenguaje humano, cero jerga.**
- ❌ "Resultado de gestión: COMPROMISO_LLAMADA_POSTERIOR"
- ✅ "¿Cómo te fue con el cliente?" → "Me dijo que llame más tarde"
- ❌ "Estado: OBSERVADO"
- ✅ "⚠ La tesorería pidió aclarar algo" (con el detalle abajo)
- ❌ "INFORMCONF"
- ✅ "🔒 Cliente en gestión legal — avisá a la oficina"

**3. Botones grandes con ícono + texto** para resultados de visita (no dropdowns, no listas). Cada resultado típico = un botón cuadrado con emoji/ícono + 1-2 palabras:
```
 ┌──────────┐ ┌──────────┐
 │   📞     │ │   🚪     │
 │ No atiende│ │  Ausente │
 └──────────┘ └──────────┘
 ┌──────────┐ ┌──────────┐
 │   🤝     │ │   ❌     │
 │ Promesa  │ │ Rechazo  │
 └──────────┘ └──────────┘
```

**4. Confirmación con resumen en español, no JSON.** Antes de cada acción irreversible (cobro, promesa, rendición), modal de confirmación con frases completas en lenguaje natural:
> "Vas a registrar un cobro de **Gs. 150.000** a **Juan Pérez** por la cuota **2 de la factura 001-001-0000123**. ¿Confirmás?"

**5. Sin pantallas vacías sin guía.** Cada estado vacío trae un emoji grande + 1 frase + 1 acción sugerida. Nunca quedarse mirando un fondo blanco.

**6. Feedback inmediato y constante.**
- Toast verde grande tras cada acción OK ("✅ Cobro registrado")
- Vibración corta al confirmar
- Spinner siempre que pase >300 ms entre tap y resultado
- Sound suave opcional (toggle en Ajustes)

**7. "Deshacer" mejor que "Confirmás?".** Cuando se pueda (registrar gestión, marcar tarea hecha): ejecutar la acción + mostrar snackbar "Hecho ✓  [Deshacer]" 4 segundos. Reduce dialogos modales que el usuario no lee.

**8. Onboarding mínimo de 3 pantallas la primera vez.** Después del login inicial, swipe horizontal corto:
1. "Acá vas a ver tus clientes asignados"
2. "Cuando vayas a cobrar, tocá el cliente y elegí las cuotas"
3. "Si no podés cobrar, igual contale a la oficina qué pasó tocando 'Registrar visita sin cobro'"

Guardar `onboarding_visto` en `tokenStore` para no repetir.

**9. Errores en español claro, no en código.**
- ❌ "Error 422: cuota_ids inválido"
- ✅ "Algo no cuadra con las cuotas elegidas. Cerrá y elegí las cuotas de nuevo."
- ❌ "Network error"
- ✅ "📡 Sin señal — guardamos lo que hiciste y se enviará cuando vuelva internet"

**10. Iconografía consistente y sin ambigüedad.**
- 💰 = cobro
- 🤝 = promesa de pago
- 📞 = llamada / no atiende
- 🚪 = visita / ausente
- 🔒 = mora legal (bloqueo)
- ♻️ = refinanciado
- 📋 = mi agenda
- 🧾 = rendición / recibo
- ✅ = aprobado
- ⚠ = observación pendiente
- ❌ = rechazado / no se pudo

Documentar la tabla emoji-función en un único lugar (`src/i18n/icons.ts` o similar) para que cada pantalla la importe.

**11. Tutorial contextual la primera vez que se usa cada feature nueva.** Tooltip pequeño con flecha que apunta al elemento, una sola vez por feature, dismisseable. Usar `react-native-walkthrough-tooltip` o similar. No molestar después.

**12. Soporte rápido desde la app.** Botón "📞 Llamar al supervisor" en Ajustes — `tel:` directo al número configurado por empresa. Más útil que un FAQ.

Aplicación concreta de estos principios a v1.3:

- `MisRendicionesScreen`: chips de estado con color + emoji ("✅ Aprobada", "⏳ Esperando aprobación", "⚠ Tesorería pidió aclarar"). Sin la palabra "BORRADOR/PENDIENTE/OBSERVADO" suelta.
- `RegistrarGestionScreen`: grid 2x3 de botones grandes con emoji + texto en castellano informal. Tras tap, una sola pantalla más para "¿Algo más que quieras anotar?" (opcional, observación libre).
- Banner de mora legal: rojo intenso, ícono candado, texto "🔒 Este cliente está con un caso legal. Hablá con la oficina antes de cobrar". Botón "📞 Llamar al supervisor" grande debajo.
- Promesa enriquecida en mobile: cuotas como tarjetas swipeables seleccionables (no checkbox chico). Tipo evidencia simplificado a 2 opciones grandes: "Me lo dijo cara a cara" / "Me lo dijo por WhatsApp".
