Saltar a contenido

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 ScanPage dispara ResetInactivityTimer.
  • Cancelación: si vence el diálogo, RedirectToHome despacha al backend un changeStatusTicket con estado CANCELED_AUTOMATICALLY, limpia el TicketBloc (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 con device (impresora: modelo, tipo de conexión, path/MAC), bolsa (EAN de bolsa, si se pregunta), timeout (inactividad, conexión), parámetros fiscales y theme (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).

Branding por comercio aplicado en la terminal

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). El HttpCliente traduce excepciones de transporte (SocketException, TimeoutException, HandshakeException) a estas fallas de dominio.
  • Estados de error del backend: el BackendClientBloc distingue 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 + FlowLogger registran cada request con contexto (pantalla, acción, EAN, estado del ticket). Hay captura global de errores (runZonedGuarded en main). 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.