Saltar a contenido

RFC-0001 — Arquitectura y Diseño Técnico de Qaudit

Estado Draft
Autor Diana Alvarenga (tesista, arquitectura/dev)
Fecha 2026-06-16
PRD asociado docs/prds/qaudit.prd.md
Sistema externo QUICK POS — QPWebAPI 1.0.0 (OpenAPI 3.0.1)
Revisores Tribunal de tesis / Ing. Pedro Coronel (tutor)

Este RFC define el cómo. El qué y el por qué viven en el PRD. Donde el PRD listó capacidades, acá se define arquitectura, contratos, modelo de datos y trade-offs. Toda decisión de stack está justificada para poder defenderse ante tribunal.


1. Resumen

Qaudit es una aplicación web full-stack que reemplaza el flujo manual de auditoría de inventario en tienda. Corre on-premise en el servidor central de Zavidoro (Docker), expone la web a los auditores y se integra con el QUICK POS de cada tienda vía su API REST a través de la VPN corporativa. El sistema cubre: SSO corporativo, administración de tiendas/usuarios, ciclo de sesión de auditoría multiusuario, ingesta del Excel RFID, distribución y búsqueda física, conciliación, emisión automática de remitos de ajuste, firma en canvas y reporte de cierre por correo, más un dashboard gerencial.

La decisión arquitectural central: toda lógica de negocio, persistencia y credenciales viven en el backend; el cliente nunca habla con QUICK POS ni ve secretos. Con VPN corporativa confirmada, el backend central alcanza el QUICK POS de cualquier tienda por hostname:puerto, lo que elimina el patrón de "token efímero al cliente" que se barajó bajo el supuesto (descartado) de no tener VPN.


2. Contexto y restricciones

  • Cliente real + tesis individual: requerimientos estables, cliente definido, necesidad de documentación formal → metodología RUP (ver anteproyecto §7). El RFC es un artefacto de la fase de Elaboración.
  • QUICK POS es on-premise por tienda, expuesto solo a la red corporativa. Los servidores de tienda y el central están interconectados por VPN.
  • Web expuesta a Internet: los auditores acceden desde cualquier ubicación. Las llamadas a QUICK POS nunca salen del backend.
  • Identidad corporativa única: tenant Microsoft Entra ID único, dominio @zavidoro.com.py. No hay B2B ni invitados externos.
  • Operación: deploys y actualizaciones los gestiona Diana. Sin equipo de SRE; la solución tiene que ser simple de operar (pocos contenedores, sin dependencias de runtime externas).

2.1 Objetivos del diseño (Goals)

  1. Un solo artefacto desplegable, simple de operar on-premise.
  2. Lógica de negocio testeable y aislada del transporte (HTTP/UI).
  3. Integración con QUICK POS segura (credenciales server-side) e idempotente (un remito no se emite dos veces).
  4. Integridad de datos garantizada en la base, no solo en la app.
  5. Trazabilidad completa: por auditor, por ítem, por sesión, por comprobante.

2.2 No-objetivos (Non-goals)

  • Alta disponibilidad / multi-instancia / autoscaling. Es un equipo de auditoría, una empresa: una instancia basta.
  • Lectura RFID, consulta de stock en tiempo real (ConsultaGral), u otras operaciones de QUICK POS.
  • Firma con validez jurídica ante terceros (la canvas es firma electrónica simple, Ley 6822/2021).

3. Stack tecnológico y justificación

Capa Elección Por qué (y qué se descartó)
App full-stack Next.js (App Router) Frontend + Route Handlers (backend) en un solo proyecto y contenedor. Descartado Expo/React Native Web: la app es primariamente web (panel admin de escritorio + uso en tablet), RN-Web es ciudadano de segunda en CSS/accesibilidad, complica SSO y rompe librerías de Excel. No hay requerimiento de hardware nativo que justifique RN.
Backend Route Handlers de Next.js A la escala del sistema no se justifica un backend separado desplegado aparte. Menos piezas que operar. Si en el futuro hace falta separarlo, la lógica ya está aislada en lib/ (no acoplada a Next).
ORM Prisma Migraciones versionadas, type-safety end-to-end, schema como fuente de verdad única — defendible en tesis por su documentación y tooling. Descartado Drizzle (más liviano y cercano a SQL, pero menos material de respaldo académico y migraciones menos maduras al momento).
Base de datos PostgreSQL 16 Constraints ricos (CHECK, FK compuestas, UNIQUE NULLS NOT DISTINCT ≥ PG15), transacciones, madurez. Es la última línea de defensa de integridad.
Auth Auth.js (NextAuth) + provider Microsoft Entra ID OIDC single-tenant estándar; provider nativo; gestión de sesión integrada. Descartado Azure AD B2B: no hay usuarios externos; el "whitelisting" es autorización a nivel de app, no invitación de identidad.
Empaquetado Docker + Docker Compose Dos/tres servicios (app, postgres), reproducible, sin dependencias de cloud. Encaja con on-premise Linux y con que Diana opere los deploys.
Procesamiento Excel SheetJS (xlsx), server-side Parseo y validación en backend; la lógica de negocio no vive en el cliente.
Firma signature_pad sobre <canvas> Simple, sin PKI. Exporta a imagen/SVG para persistir.
PDF Generación server-side desde plantilla HTML Reporte consistente, no depende del entorno del cliente.

Lenguaje: TypeScript estricto en todo el stack.


4. Arquitectura del sistema

4.1 Topología de despliegue

                       Internet
                          │  HTTPS (reverse proxy / TLS)
                          ▼
┌──────────────────────────────────────────────────────────┐
│  Servidor central on-premise (Linux, Docker Compose)     │
│                                                          │
│   ┌───────────────────────┐     ┌────────────────────┐   │
│   │  app (Next.js)        │     │  postgres:16       │   │
│   │  - UI (RSC + client)  │────▶│  volumen persistido│   │
│   │  - Route Handlers     │     └────────────────────┘   │
│   │  - lib/ (dominio)     │                              │
│   │  - QUICK POS client   │                              │
│   │    (cache de tokens   │                              │
│   │     en memoria)       │                              │
│   └──────────┬────────────┘                              │
└──────────────┼───────────────────────────────────────────┘
               │  HTTP interno por VPN corporativa
               │  (hostname:puerto por tienda)
        ┌──────┴───────┬───────────────┐
        ▼              ▼               ▼
  QUICK POS T01   QUICK POS T02   QUICK POS Tnn   (on-premise por tienda)

  Microsoft Entra ID (OIDC, externo)  ◀── SSO desde el navegador del auditor
  Servidor SMTP corporativo           ◀── envío del reporte de cierre

Decisión: la web se expone a Internet; las llamadas a QUICK POS van solo desde el backend por la VPN. Las credenciales permanentes de QUICK POS jamás llegan al navegador.

4.2 Capas internas de la app

src/
  app/            App Router: UI + layouts + guards de rol (admin / audit)
  app/api/        Route Handlers (HTTP boundary) — validan input, delegan a lib/
  lib/
    domain/       Lógica pura, testeable, sin I/O:
                    - reconciliation.ts  (cálculo de diferencias y tipo de ajuste)
                    - sku.ts             (construcción de codigoItem)
                    - prefijo.ts         (construcción de prefijo 099-0{codsuc})
                    - excel/parser.ts    (Excel RFID → AuditItem[])
    quickpos/     Cliente de integración: token lifecycle + remitos
    db.ts         Prisma singleton
    mail.ts       Envío del reporte

Regla: lib/domain/ no importa Next ni Prisma ni fetch. Recibe datos, devuelve datos. Es lo que se testea con TDD y lo que sustenta la defensa de "lógica auditada y consistente".


5. Modelo de datos

PostgreSQL. Los constraints son parte del contrato, no decoración: garantizan integridad ante bugs, scripts manuales o migraciones. (Notación resumida; el schema canónico vive en Prisma + migración SQL.)

-- Tiendas (gestionadas por admin). Datos de conexión al QUICK POS de cada tienda.
stores
  id        uuid PK default gen_random_uuid()
  nombre    text NOT NULL
  hostname  text NOT NULL
  puerto    int  NOT NULL CHECK (puerto BETWEEN 1 AND 65535)
  codsuc    char(2) NOT NULL UNIQUE         -- "01".. → prefijo "099-0{codsuc}"
  codemp    int  NOT NULL                   -- empresa (header de remitos)
  activa    boolean NOT NULL DEFAULT true
  UNIQUE (hostname, puerto)

-- Whitelist de usuarios habilitados (@zavidoro.com.py)
users
  id      uuid PK default gen_random_uuid()
  email   text NOT NULL UNIQUE CHECK (email LIKE '%@zavidoro.com.py')
  nombre  text NOT NULL
  rol     text NOT NULL CHECK (rol IN ('admin','auditor'))
  activo  boolean NOT NULL DEFAULT true
  ms_oid  text UNIQUE                       -- object id de Entra; NULL hasta el primer login (se vincula ahí)

-- Sesión de auditoría por tienda. Una sola activa por tienda.
audit_sessions
  id            uuid PK default gen_random_uuid()
  store_id      uuid NOT NULL REFERENCES stores(id)
  creado_por    uuid NOT NULL REFERENCES users(id)   -- solo trazabilidad, sin permisos
  fecha_inicio  timestamptz NOT NULL DEFAULT now()
  fecha_cierre  timestamptz NULL
  estado        text NOT NULL DEFAULT 'carga'
                CHECK (estado IN ('carga','asignacion','busqueda','conciliacion','cerrado'))
  CHECK (fecha_cierre IS NULL OR fecha_cierre > fecha_inicio)
  UNIQUE NULLS NOT DISTINCT (store_id, fecha_cierre)  -- una sola abierta por tienda (PG15+)

-- Auditores que participan de la sesión (define quién puede recibir ítems y quién firma)
audit_session_members
  session_id uuid NOT NULL REFERENCES audit_sessions(id)
  user_id    uuid NOT NULL REFERENCES users(id)
  PRIMARY KEY (session_id, user_id)

-- Ítems con diferencia, cargados desde el Excel
audit_items
  id               uuid PK default gen_random_uuid()
  session_id       uuid NOT NULL REFERENCES audit_sessions(id)
  codigo_item      text NOT NULL                      -- SKU para QUICK POS
  descripcion      text NULL
  division         text NOT NULL CHECK (division IN ('apparel','equipment','footwear'))
  stock_bas        numeric NOT NULL CHECK (stock_bas >= 0)
  cantidad_rfid    numeric NOT NULL CHECK (cantidad_rfid >= 0)
  diferencia       numeric NOT NULL CHECK (diferencia = stock_bas - cantidad_rfid)
  cant_encontrada  numeric NOT NULL DEFAULT 0 CHECK (cant_encontrada >= 0)
  diferencia_final numeric NOT NULL                   -- = diferencia - cant_encontrada (app)
  despacho         text NOT NULL DEFAULT 'FAL26'
  mal_entallado    boolean NOT NULL DEFAULT false
  observacion      text NULL
  asignado_a       uuid NULL
  estado           text NOT NULL DEFAULT 'pendiente'
                   CHECK (estado IN ('pendiente','resuelto','ajustado'))
  UNIQUE (session_id, codigo_item)
  -- solo se asigna a un miembro de la sesión (FK compuesta, no FK simple a users):
  FOREIGN KEY (session_id, asignado_a)
     REFERENCES audit_session_members(session_id, user_id)

-- Remitos emitidos en QUICK POS
remitos
  id                 uuid PK default gen_random_uuid()
  session_id         uuid NOT NULL REFERENCES audit_sessions(id)
  tipo               text NOT NULL CHECK (tipo IN ('egreso','ingreso'))
  numero_comprobante int  NULL CHECK (numero_comprobante > 0)   -- devuelto por QUICK POS
  prefijo            text NOT NULL
  fecha_emision      timestamptz NOT NULL DEFAULT now()
  idempotency_key    uuid NOT NULL UNIQUE              -- evita doble emisión (ver §7.4)
  estado             text NOT NULL DEFAULT 'pendiente'
                     CHECK (estado IN ('pendiente','enviado','error'))
  error_msg          text NULL
  CHECK (estado = 'error' OR error_msg IS NULL)
  UNIQUE (session_id, tipo)                            -- un egreso y un ingreso por sesión

-- Trazabilidad ítem → comprobante
remito_items
  remito_id uuid NOT NULL REFERENCES remitos(id)
  item_id   uuid NOT NULL REFERENCES audit_items(id)
  cantidad  numeric NOT NULL CHECK (cantidad > 0)
  PRIMARY KEY (remito_id, item_id)

-- Firmas del cierre (N firmantes presentes)
audit_signatures
  id         uuid PK default gen_random_uuid()
  session_id uuid NOT NULL REFERENCES audit_sessions(id)
  firmante   text NOT NULL                  -- nombre del encargado / presente
  firma_svg  text NOT NULL                  -- canvas exportado (base64/SVG)
  fecha      timestamptz NOT NULL DEFAULT now()

-- Reporte de cierre
audit_reports
  id             uuid PK default gen_random_uuid()
  session_id     uuid NOT NULL UNIQUE REFERENCES audit_sessions(id)
  pdf_path       text NOT NULL
  fecha_gen      timestamptz NOT NULL DEFAULT now()
  correo_enviado boolean NOT NULL DEFAULT false
  fecha_envio    timestamptz NULL
  CHECK (correo_enviado = false OR fecha_envio IS NOT NULL)

Notas de diseño: - UNIQUE NULLS NOT DISTINCT (store_id, fecha_cierre): dos sesiones abiertas comparten (store_id, NULL) y colisionan. Al cerrar, fecha_cierre toma valor y libera la restricción. Fuerza "una sesión activa por tienda" sin trigger. Requiere PG15+ (tenemos 16). - FK compuesta (session_id, asignado_a) → audit_session_members: hace imposible asignar un ítem a alguien que no está en la sesión, a nivel base. - ms_oid es nullable: el admin habilita un correo en la whitelist antes de que la persona inicie sesión por primera vez, así que el object id de Entra todavía no existe en ese momento. Se vincula en el primer login, matcheando por email normalizado (lower/trim). En Postgres UNIQUE admite múltiples NULL (NULLS DISTINCT por defecto), de modo que varios usuarios pendientes de su primer login conviven sin colisionar. - idempotency_key y UNIQUE (session_id, tipo): defensa doble contra emitir el mismo remito dos veces (§7.4). - mal_entallado es ortogonal al signo de la diferencia: va en el mismo egreso que los faltantes (concepto AJU), con su razón en observacion.


6. Autenticación y autorización

  1. Autenticación: Auth.js con provider Microsoft Entra ID (OIDC, single-tenant). El auditor hace login con su cuenta @zavidoro.com.py.
  2. Autorización (whitelist): tras autenticar, la app verifica que el email/ms_oid exista en users y activo = true. Si no está habilitado → acceso denegado, sin importar que la identidad Microsoft sea válida. Esto es autorización de aplicación, no de identidad.
  3. Roles: admin (panel de tiendas/usuarios) vs auditor (flujo operativo). Guards a nivel de layout de ruta ((admin) / (audit)) y revalidados en cada Route Handler — nunca confiar solo en el guard de UI.
  4. Sesión: JWT de Auth.js. El ms_oid del token mapea a users.id.

Trade-off documentado: el tenant único simplifica todo a OIDC estándar. No se usa Microsoft Graph para gestionar invitaciones porque no se invita identidades — solo se habilitan correos ya existentes en el dominio.


7. Integración con QUICK POS

Contrato real (Swagger QPWebAPI 1.0.0). La integración se reduce a tres operaciones: obtener token, emitir RemitoEgreso, emitir RemitoIngreso. No se usa ConsultaGral (el stock de BAS llega en el Excel).

7.1 Autenticación contra QUICK POS

POST /auth/tokenmultipart/form-data:

grant_type=password
client_id=api            # default del swagger
client_secret=secret     # default del swagger
username=<cuenta servicio>
password=<secreto>

Respuesta (AccessTokens): access_token (Bearer JWT), token_type, expires_in, refresh_token. Refresh con el mismo endpoint y grant_type=refresh_token.

  • Es OAuth2 Resource Owner Password Credentials. Requiere una cuenta de servicio por tienda (Windows o SQL Server, según configure cada QUICK POS), o una compartida si todas aceptan las mismas credenciales — a confirmar.
  • Credenciales en variables de entorno del backend, nunca en código ni en cliente.

7.2 Gestión de tokens (server-side)

Cache de tokens en memoria del proceso Node, keyed por store_id:

getToken(storeId):
  si hay token cacheado y no expira en < N seg → usarlo
  si expira pronto y hay refresh_token        → refresh
  si no                                        → password grant
  guardar { access_token, expires_at, refresh_token } en cache[storeId]

Efímero por diseño; no va a BD ni Redis. Limitación asumida: una sola instancia de backend. Si se escalara a múltiples instancias, este cache se mueve a un store compartido — fuera de alcance hoy (Non-goal §2.2).

7.3 Emisión de remitos

Una sola llamada por tipo de ajuste por sesión: un RemitoEgreso con todos los faltantes + mal entallados, un RemitoIngreso con todos los sobrantes. Arrays de items.

Valores fijos confirmados con negocio:

Campo Valor Nota
prefijo 099-0{codsuc} codsuc (2 díg.) por tienda, concatenado
concepto AJU siempre, ajuste de inventario
destino (egreso) C siempre cliente
origen (ingreso) S provisional, a confirmar con quien configura QUICK POS
despacho (por ítem) FAL26 default; lógica por temporada es mejora futura, no bloqueante
numero (ingreso) omitido QUICK POS lo autogenera (confirmado)

codigoItem (SKU) = {códigoProducto} + 0, + {talla} (ej. ABC123-0,42.5). El delimitador exacto se valida contra el Excel crudo (ver Riesgos).

Payload (egreso, forma real del schema):

POST /api/REMITOEGRESO        Authorization: Bearer <token>
{
  "header":      { "etiqueta": "REMITOS DE EGRESO", "codemp": <codemp>, "codsuc": <codsuc>, "fecha": "dd/mm/aaaa hh:mm:ss" },
  "remitoEgreso": {
    "fecha":    "dd/mm/aaaa hh:mm:ss",
    "prefijo":  "099-001",
    "destino":  "C",
    "concepto": "AJU",
    "items": [
      { "codigoItem": "ABC123-0,42.5", "cantidadPrimeraUnidad": 2, "despacho": "FAL26", "observacionItem": "mal entallado" }
    ]
  }
}

RemitoIngreso es análogo con remitoIngreso.origen = "S".

Respuesta (RespuestaAcliente): codigoEstado, cuerpo (de donde se extrae el número de comprobante), pdf (posible comprobante en PDF), pdFerror. Se persiste numero_comprobante, se pasa remitos.estado = enviado, y si codigoEstado/pdFerror indican fallo → estado = error + error_msg.

7.4 Idempotencia y consistencia (no estaba en el relevamiento — crítico)

La emisión de un remito es un efecto colateral externo no transaccional: si la llamada se reintenta tras un timeout que en realidad sí impactó, se duplica un ajuste de stock real. Mitigaciones:

  1. remitos se crea en estado pendiente con idempotency_key antes de llamar a QUICK POS, dentro de la transacción que también marca los audit_items como ajustado.
  2. UNIQUE (session_id, tipo) impide un segundo egreso/ingreso para la misma sesión.
  3. Ante timeout/red caída, no se reintenta a ciegas: el estado queda pendiente y se requiere verificación manual/reconsulta antes de reintentar. El reporte de cierre no se genera con remitos en pendiente o error.

Decisión abierta: QUICK POS no expone (en el Swagger) un mecanismo de idempotencia propio. Hay que validar en pruebas reales su comportamiento ante reintentos y si cuerpo permite reconciliar un comprobante ya emitido.

7.5 Lógica de conciliación y signo del ajuste — corrección al relevamiento

Definición canónica:

diferencia       = stock_bas - cantidad_rfid          (lo que el sistema cree de más/de menos vs. lo leído)
diferencia_final = diferencia - cant_encontrada       (tras la búsqueda física)

diferencia_final > 0  → FALTANTE  → el sistema tiene de más → EGRESO  (dar de baja stock, destino C)
diferencia_final < 0  → SOBRANTE  → existe de más físicamente → INGRESO (origen S)
diferencia_final = 0  → sin ajuste → el ítem sale del listado
mal_entallado = true  → EGRESO con concepto AJU + observacion, sin importar el signo

⚠️ En las notas del chat de relevamiento quedó anotado "negativo → egreso, positivo → ingreso", que contradice la fórmula diferencia = stock_bas - cantidad_rfid definida en el mismo modelo. La convención correcta es la de arriba. Invertir el signo invierte todos los remitos, por lo que esto se confirma con una emisión de prueba contra un QUICK POS real antes de operar (test de "un faltante conocido → debe generar egreso").


8. Flujos principales

8.1 Ciclo de sesión de auditoría

Auditor inicia sesión (SSO) ─▶ verificación de whitelist
   └▶ selecciona tienda
        ├─ ¿hay sesión activa para la tienda?
        │     no → crea sesión (queda como creado_por y primer miembro)
        │     sí → se une como audit_session_members
        ├─ carga del Excel RFID (cualquier miembro) → audit_items (diferencia ≠ 0)
        ├─ distribución: ítems → miembros (segmentación principal por división)
        ├─ búsqueda física (vista por auditor): registra cant_encontrada + observacion
        ├─ conciliación: recalcula diferencia_final, agrupa por tipo de ajuste
        ├─ emisión de remitos en QUICK POS (egreso / ingreso)
        ├─ firma(s) en canvas del/los presente(s)
        └─ generación del PDF de cierre + envío por correo → estado 'cerrado'

estado de la sesión actúa como máquina de estados (carga → asignacion → busqueda → conciliacion → cerrado); las transiciones se validan en backend.

8.2 Distribución de ítems

Segmentación principal por división (un auditor sostiene un rubro: apparel / equipment / footwear) para no mezclar control. Dentro de la división, el reparto entre los auditores asignados a ese rubro. Abierto: si el reparto intra-división es automático (equitativo por cantidad de ítems) o manual. El modelo soporta ambos (asignado_a por ítem); se decide en /plan del milestone correspondiente.

8.3 Conectividad a la tienda

Con VPN corporativa, el backend alcanza el QUICK POS de la tienda por hostname:puerto de stores. El auditor solo necesita Internet para la web. Antes de emitir remitos, el backend hace un probe de reachability al host de la tienda; si falla, mensaje no categórico ("no se pudo alcanzar el servidor de la tienda; podría ser conectividad o el servicio caído"). Nota: la detección de SSID desde el navegador es imposible y, además, innecesaria con este diseño (el cliente no habla con QUICK POS).


9. Seguridad

  • Secretos (credenciales QUICK POS por tienda, secret de Auth.js, client secret de Entra) solo en variables de entorno del backend; nunca en cliente ni en repo. Validación de presencia al arranque.
  • Credenciales de QUICK POS jamás salen del backend. El cliente no recibe tokens de QUICK POS.
  • Inyección: acceso a datos vía Prisma (consultas parametrizadas). No se construye SQL por concatenación. (ConsultaGral, que acepta SQL crudo, está fuera de alcance — buena noticia de seguridad.)
  • Autorización revalidada server-side en cada Route Handler, no solo en UI.
  • Headers/CSP: HSTS, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, CSP con nonce para scripts; TLS terminado en el reverse proxy.
  • Validación de entrada en el borde: el Excel y todo payload se validan con schema (p. ej. Zod) antes de tocar dominio o BD. No se confía en el archivo subido.
  • Firma canvas: evidencia interna, no jurídica. Se guarda vinculada a session_id con timestamp.

10. Operación y despliegue

  • Docker Compose: servicios app (Next.js) y postgres:16-alpine con volumen persistente y healthcheck; app depende del postgres saludable. docker-compose.prod.yml para overrides de producción.
  • Migraciones: Prisma Migrate versionado; se aplican en el arranque controlado del deploy (no auto-migrate silencioso en cada boot).
  • Backups: dump periódico del volumen de Postgres (responsabilidad operativa de Diana; documentar el procedimiento).
  • Logs: estructurados, sin secretos ni PII innecesaria; registrar contexto de error de integración (estado de remito, codigoEstado de QUICK POS).
  • Exposición: reverse proxy con TLS al frente; la app escucha en 3000 interno.

11. Estrategia de pruebas

TDD obligatorio (regla del proyecto), cobertura objetivo ≥ 95%.

Nivel Qué se prueba Notas
Unitario reconciliation (signo y casos borde: 0, mal entallado, found > diferencia), sku, prefijo, excel/parser lib/domain/ puro, sin I/O. El núcleo defendible de la tesis.
Integración Route Handlers + Prisma (BD de test), cliente QUICK POS con mock del Swagger (token lifecycle, emisión, manejo de error/idempotencia) QUICK POS mockeado por contrato; no se pega a un POS real en CI.
E2E Flujo crítico: login → sesión → carga → asignación → búsqueda → conciliación → remitos (mock) → firma → reporte Playwright; waits deterministas.
Piloto Métricas del PRD en ≥2 sesiones reales contra línea base (OE1) Es la validación de la hipótesis, no un test automatizado.

Caso de prueba de integración no negociable: "un faltante conocido genera exactamente un RemitoEgreso con destino C, concepto AJU y la cantidad correcta" — blinda el signo del ajuste (§7.5).


12. Alternativas consideradas

Alternativa Veredicto Razón
Expo / React Native Web Rechazada App primariamente web; RN-Web degrada CSS/accesibilidad, complica SSO y Excel; sin necesidad de hardware nativo.
Backend separado (NestJS/Express) desplegado aparte Rechazada (por ahora) Sobredimensiona la operación. Lógica aislada en lib/ permite extraerlo si algún día hace falta.
Drizzle ORM Rechazada Prisma da migraciones maduras, type-safety y mejor respaldo documental para tesis.
Token efímero de QUICK POS al cliente Rechazada Era solución a "sin VPN". Con VPN, el backend llama directo; el cliente nunca toca QUICK POS.
Azure AD B2B / invitaciones Rechazada Tenant único, sin externos. Whitelist = autorización de app.
Llamar QUICK POS desde el browser Rechazada Expondría credenciales en devtools.

13. Riesgos y cuestiones abiertas

# Riesgo / cuestión Impacto Acción
1 Schema real del Excel RFID no confirmado (columnas, codificación de división, delimitador exacto del SKU) Alto Bloquear el parser hasta tener el archivo crudo.
2 Signo del ajuste (§7.5) — la nota del chat contradice la fórmula Alto Test de emisión real "faltante → egreso" antes de operar.
3 Idempotencia de QUICK POS ante reintentos no documentada Alto Validar contra POS real; política de no-reintento ciego ya en diseño (§7.4).
4 origen = S en RemitoIngreso provisional Medio Confirmar con quien configura QUICK POS.
5 Cuenta(s) de servicio QUICK POS: ¿una por tienda o compartida?, ¿Windows o SQL? Medio Definir con soporte/infra antes del milestone de integración.
6 Línea base son estimaciones (heredado del PRD) Alto Ejecutar OE1 + encuesta previa antes del piloto.
7 despacho por temporada sin regla de negocio Bajo Default FAL26; mejora posterior (droplist/auto).
8 Acceso al entorno on-premise de la tienda piloto fuera del control del proyecto Alto Coordinar temprano; tener tienda alternativa.

14. Mapa a milestones del PRD

Milestone (PRD) Componentes de este RFC
1 — Acceso y administración §6 Auth/authz, stores/users (§5), guards de rol
2 — Sesión y carga del Excel §5 sesiones, §8.1 ciclo, lib/domain/excel, §13.1
3 — Distribución y búsqueda §8.2, audit_items.asignado_a, FK compuesta a miembros
4 — Conciliación e integración QUICK POS §7 completo, §7.4 idempotencia, §7.5 signo
5 — Firma y reporte §5 firmas/reportes, §3 (signature_pad, PDF, SMTP)
6 — Dashboard métricas por tienda/sesión (consultas de lectura)
7 — Piloto §11 (piloto) + métricas del PRD

Status: DRAFT — diseño técnico para revisión. Próximo paso: /plan docs/prds/qaudit.prd.md por milestone, tomando este RFC como blueprint.