Operación, sesión y configuración¶
Todo lo que rodea al flujo de compra y lo hace operable en un comercio real, sin nadie atendiendo: el reloj de inactividad, el alta de la terminal, el modo supervisor, el branding por comercio, las pantallas de gestión y el manejo de errores.
El código es la fuente de verdad
Reproducimos clases, eventos y estados a la fecha. Donde algo parece stub o pendiente de cablear, lo marcamos.
Inactividad: el reloj que protege la terminal¶
InactividadBloc (lib/viewmodels/inactividad/) es crítico en autoservicio: si un cliente abandona la terminal a mitad de una compra, nadie va a venir a cancelarla. El sistema lo hace solo.
stateDiagram-v2
[*] --> active: StartInactivityTimer
active --> active: ResetInactivityTimer (hubo actividad)
active --> warning: pasaron inactivityDuration s
warning --> active: el cliente responde
warning --> redirect: pasaron dialogResponseTime s
redirect --> [*]: cancela ticket + va a /home
- Dos timers: el de inactividad (ej. 300 s) y, cuando ese vence, el del diálogo de advertencia (ej. 30 s para responder). Los valores vienen de la config de la terminal (
LoadTimeoutInactivity). - Reset por actividad: cada escaneo, tap, scroll o apertura de diálogo en
ScanPagedisparaResetInactivityTimer. - Cancelación: si vence el diálogo,
RedirectToHomedespacha al backend unchangeStatusTicketcon estadoCANCELED_AUTOMATICALLY, limpia elTicketBloc(ClearTicket) y navega a/home.
Por qué dos timers y no uno
El primero da tiempo generoso (el cliente puede estar buscando un producto); el segundo es una última chance explícita ("¿seguís ahí?"). Sin el diálogo, cancelarías compras de gente que solo se distrajo un momento.
Configuración y registro de la terminal¶
La terminal no sirve hasta estar configurada (sabe a qué backend hablar) y registrada (el backend le dio su TerminalConfig). El ConfigurationBloc (lib/viewmodels/configuration/) maneja ese ciclo.
| Evento | Qué hace |
|---|---|
LoadConnectionConfiguration |
Carga server/puerto/SSL/uuid desde Isar. |
LoadTerminalConfiguration |
Carga la TerminalConfig local; si no hay, hace falta registrar. |
FetchConfiguration(...) |
Descarga config fresca del backend. |
RegisterTerminal(...) |
Registra la terminal contra el backend. |
RefreshConfiguration(silent?) |
Re-descarga; en modo silent no interrumpe la UI si ya hay datos. |
UpdateLocalPrinterMacAddress(mac) |
Persiste la MAC Bluetooth local. |
Qué pide el registro¶
La pantalla register (lib/views/register/) toma: codTerminal, server, port, ssl, y opcionalmente apicardServer/apicardPort (el daemon de tarjeta). Al registrarse, el backend devuelve la TerminalConfig completa.
Qué baja el backend: TerminalConfig¶
lib/domain/configuration/terminal_config.dart — la identidad operativa y fiscal de la terminal:
- Identidad:
codSucursal,codNegocio,codComercio,nroPos,nroPVFiscal(el punto de venta AFIP). config: bloque detallado condevice(impresora: modelo, tipo de conexión, path/MAC),bolsa(EAN de bolsa, si se pregunta),timeout(inactividad, conexión), parámetros fiscales ytheme(branding).
Tras registrar, se persiste todo en Isar y se propaga la identidad al HttpCliente (setTerminalIdentity), para que cada request lleve los headers X-Cod-Terminal/X-Terminal-Uuid.
Refresh silencioso¶
Si cambian precios o promos en el backend, RefreshConfiguration(silent: true) re-descarga la config sin spinner ni interrumpir al cliente: si falla, mantiene la UI estable con lo que ya tenía.
Modo supervisor¶
SupervisorBloc (lib/viewmodels/supervisor/) habilita funciones restringidas: ingreso manual de productos, anulación de ítems, cambio de precio. El flujo de autenticación:
graph LR
A["escanea credencial<br/>SUP{legajo}-SUC{suc}"] --> B{formato válido?}
B -->|no| ERR["error: formato inválido"]
B -->|sí| C["keypad shuffled 0-9"]
C --> D["ingresa PIN"]
D --> E{PIN válido?}
E -->|sí| AUTH["sesión: scopes habilitados"]
E -->|no| ERR2["error: PIN incorrecto"]
- Credencial: se escanea un código con formato
SUP{legajo}-SUC{sucursalId}(regex^SUP(\d{1,8})-?SUC(\d{1,4})$). - PIN con teclado mezclado: el keypad se baraja (0-9 en orden aleatorio) para que mirar la pantalla no revele el PIN — defensa contra shoulder-surfing.
- Sesión:
SupervisorSession { supervisorId, supervisorName, legajo, scopes }. Los scopes (manualEntry,authorizeVoid,modifyPrice) gobiernan qué puede hacer.
La validación de PIN está mockeada (pendiente de backend)
Según el código actual, la validación de PIN no consulta al backend: hay un delay simulado y un PIN fijo, con un TODO de re-cablear contra el servidor. Documentamos el flujo real (credencial + keypad shuffled + sesión con scopes), pero el chequeo de PIN todavía no es real. Es lo primero a endurecer antes de habilitar funciones sensibles en producción.
Branding por comercio¶
AppThemeCubit (lib/core/theme/) cambia colores, logo y marca según el comercio. AppTheme.resolve(cuit) busca el tema por CUIT en un mapa predefinido y cae a un tema por defecto si no lo encuentra. Define primary/secondary, fondos, color del botón PAGAR, colores de estado (error/warning/success), logoUrl y datos de marca. Los widgets lo leen con AppTheme.of(context).

El theming en acción: la misma terminal, con el logo, los colores (amarillo SUPER MAMI) y la marca del comercio resueltos a partir de su CUIT. El "Powered by tipre" y los badges de identidad son parte del chrome común.
Pantallas de gestión¶
| Pantalla | Para qué |
|---|---|
management_ticket |
Historial de tickets: búsqueda por fecha/estado/cliente, ver detalle, reimprimir, anular (con autorización de supervisor). |
management_vale |
Vales de envase: búsqueda, movimientos (consumo/devolución), reimpresión. Ver Envases y vales. |
management_printer |
Impresora: estado, tipo de conexión, dispositivo, test page. Ver Subsistema de impresión. |
Manejo de errores y resiliencia¶
- Tipos de falla: jerarquía
Failure(ServerFailure,NetworkFailure,TimeoutFailure,JsonParseFailure,GenericFailure). ElHttpClientetraduce excepciones de transporte (SocketException,TimeoutException,HandshakeException) a estas fallas de dominio. - Estados de error del backend: el
BackendClientBlocdistingue timeout (commandReference == 'TIMEOUT', > ~30 s), HTTP 5xx/red caída, JSON inválido, y pago rechazado (success=false). ErrorPage: pantalla amigable con ícono, mensaje para el cliente y botón "Reintentar".ConnectionRecoveryMixin: en las pantallas de pago, maneja timeout + reintento de la consulta de transacción sin colgar la UI. Ver Pagos.- Logging estructurado:
AppLogger+FlowLoggerregistran cada request con contexto (pantalla, acción, EAN, estado del ticket). Hay captura global de errores (runZonedGuardedenmain). Es la primera parada cuando algo falla en una terminal en el campo.
En autoservicio, el error tiene que ser para el cliente Y para soporte
ErrorPage muestra algo entendible para quien está comprando; el AppLogger deja el detalle técnico para quien después tiene que diagnosticar. Las dos audiencias importan: no mezcles el stack trace en la pantalla ni escondas el detalle del log.