# 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`
