# Arquitectura — CLUB-003 · Integración TPV del Club ## Objetivo Conectar la caja TPV con el módulo Club para que el dependiente pueda identificar a un socio y acumular cashback en cada venta. El ledger registra la transacción de forma idempotente. ## Análisis existente ### Lo que ya existe - Módulo Club con `club_members`, `club_devices`, `club_transactions` (CLUB-001). - PWA Club con join y tarjeta digital (CLUB-002). - Terminal POS en `project/apps/pos/` con búsqueda de cliente por email. - Flujo de venta: añadir artículos → cobrar → `POST /pos/sales`. ### Patrones a reutilizar - La misma transacción de base de datos que usa el POS para ventas. - El ledger idempotente de CLUB-001 con `idempotencyKey`. - Configuración de Club en `store_settings` (`club_cashback_bps`). ## Diseño propuesto ### 1) Endpoint de resolución de socio por código `GET /club/resolve?memberCode=MDV-XXXXXXXX` Devuelve: `{ member, config }` si existe y está activo, o 404. Esto permite al TPV resolver un socio desde el código que el cliente presenta en la tarjeta digital. ### 2) Extender creación de venta POS `POST /pos/sales` acepta un campo opcional: ```ts clubMemberId?: string; ``` Si está presente y el Club está habilitado: 1. Resolver el socio por ID. 2. Consultar `club_cashback_bps` de settings. 3. Calcular `cashbackCents = round(totalCents * cashbackBps / 10_000)`. 4. Crear transacción de ledger idempotente dentro de la misma transacción DB que la venta: ``` idempotencyKey: `club-earn-${orderId}` type: 'earn' amountCents: totalCents balanceDeltaCents: cashbackCents saleId: orderId ``` 5. Devolver en la respuesta de venta: `{ ..., clubEarnedCents: cashbackCents }`. Si la venta es `PENDING` (no se cobra aún), no se acumula cashback. Solo cuando `orderState === 'COMPLETED'`. ### 3) Receipt del TPV incluye cashback El receipt ya incluye campos libres. Se añade: ``` clubMemberCode: string (si hay socio) clubEarnedCents: number ``` ### 4) UI del TPV: búsqueda de socio Club En la barra superior del terminal, рядом con la info de cajero: - Campo de texto para código de socio Club. - Botón "Club" que abre un diálogo simple de búsqueda. - Muestra: código, saldo actual. - Al confirmar, asocia el socio a la venta en curso. - Al borrar/limpiar caja, también se limpia el socio. ## Alcance ### Sí entra - `GET /club/resolve` - integración de `clubMemberId` en `POST /pos/sales` - cálculo de cashback configurable desde settings - ledger idempotente por orderId - UI simple en TPV para buscar socio Club - receipt con cashback ganado ### No entra - UI de reintegro de cashback en devolución TPV (CLUB-004) - canjeo de saldo Club en TPV - vínculo con usuario registrado en TPV - lógica de recovery codes - admin UI del Club ## Estrategia de tests - Test unitario del cálculo de cashback en `create-pos-sale.ts`. - Test de integración del endpoint `/club/resolve`. - Test del flujo completo: venta POS + Club earn ledger (con itest real si el pool de test lo soporta). ## Validación prevista - `cd project && npm run typecheck` - `cd project && npm run build` - `cd project/apps/pos && npm run build` - `cd project && npx vitest run src/modules/club/tests/boundary.test.ts` - `./scripts/verify.sh`