feat(club-003): club POS integration: identify member and register cashback from sales

This commit is contained in:
Deploy
2026-08-26 20:16:13 +02:00
parent c263b44355
commit 66290a08ae
18 changed files with 553 additions and 99 deletions

View File

@@ -0,0 +1,91 @@
# 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`