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)¶
- Un solo artefacto desplegable, simple de operar on-premise.
- Lógica de negocio testeable y aislada del transporte (HTTP/UI).
- Integración con QUICK POS segura (credenciales server-side) e idempotente (un remito no se emite dos veces).
- Integridad de datos garantizada en la base, no solo en la app.
- 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. |
| 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¶
- Autenticación: Auth.js con provider Microsoft Entra ID (OIDC, single-tenant). El auditor hace login con su cuenta
@zavidoro.com.py. - Autorización (whitelist): tras autenticar, la app verifica que el
email/ms_oidexista enusersyactivo = 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. - Roles:
admin(panel de tiendas/usuarios) vsauditor(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. - Sesión: JWT de Auth.js. El
ms_oiddel token mapea ausers.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/token — multipart/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:
remitosse crea en estadopendienteconidempotency_keyantes de llamar a QUICK POS, dentro de la transacción que también marca losaudit_itemscomoajustado.UNIQUE (session_id, tipo)impide un segundo egreso/ingreso para la misma sesión.- Ante timeout/red caída, no se reintenta a ciegas: el estado queda
pendientey se requiere verificación manual/reconsulta antes de reintentar. El reporte de cierre no se genera con remitos enpendienteoerror.
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
cuerpopermite 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_idcon timestamp.
10. Operación y despliegue¶
- Docker Compose: servicios
app(Next.js) ypostgres:16-alpinecon volumen persistente y healthcheck;appdepende delpostgressaludable.docker-compose.prod.ymlpara 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
3000interno.
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.