feat(club-002): club PWA: join flow, digital card and installable shell

This commit is contained in:
Deploy
2026-08-26 19:21:49 +02:00
parent b2ccc70dc6
commit c263b44355
20 changed files with 1059 additions and 100 deletions

View File

@@ -0,0 +1,87 @@
# Arquitectura — CLUB-002 · PWA Club
## Objetivo
Construir la primera experiencia frontend del Club sobre el backend ya entregado en CLUB-001:
- alta/join del Club
- tarjeta digital móvil
- shell instalable tipo PWA
## Análisis del frontend actual
- La tienda usa Next App Router en `project/frontend/src/app`.
- No existe manifiesto PWA ni rutas `/club/*` todavía.
- Los flujos autenticados usan proxies Next.js (`/api/auth/*`) para preservar cookies same-origin.
- CLUB-001 ya expone backend suficiente para esta fase:
- `GET /club/config`
- `POST /club/join`
- `GET /club/me`
- `GET /club/movements`
## Diseño propuesto
### 1) Proxy Next dedicado para Club
Añadir `project/frontend/src/app/api/club/[...path]/route.ts`.
Motivo:
- preservar la cookie `mdv_club` como same-origin
- reenviar `set-cookie` del backend en `POST /club/join`
- evitar que componentes cliente hablen directo con `NEXT_PUBLIC_API_URL`
### 2) Rutas frontend Club
Crear páginas:
- `/club` → landing/resumen del Club
- `/club/join` → alta anónima o acceso al card si ya existe identidad Club
- `/club/card` → tarjeta digital con member code, saldo y últimos movimientos
### 3) Shell móvil instalable
Añadir:
- `app/manifest.ts`
- iconos PWA a partir del branding existente
- metadatos PWA en `app/layout.tsx` (`themeColor`, `appleWebApp`, manifest)
- CTA de instalación desde cliente (`beforeinstallprompt`) en la superficie Club
### 4) Estado de Club en cliente
No crear aún un provider global complejo.
En CLUB-002 basta con fetch puntual desde páginas/componentes Club:
- `GET /api/club/config`
- `POST /api/club/join`
- `GET /api/club/me`
- `GET /api/club/movements`
Esto minimiza acoplamiento y deja abierta una futura extracción a `ClubContext` si la PWA crece.
### 5) Tarjeta digital
La card mostrará:
- member code
- saldo actual
- estado del Club
- últimos movimientos
- superficie visual tipo credencial móvil
### 6) Código visual de identificación
CLUB-002 mostrará un identificador visual en la tarjeta usando datos ya emitidos por CLUB-001.
Nota de alcance:
- CLUB-003 es la fase de integración TPV/identificación en caja.
- Por tanto, CLUB-002 prioriza la UX de tarjeta digital e instalación móvil.
- La semántica exacta de escaneo en TPV se cerrará en CLUB-003 para no fijar prematuramente un contrato visual incompatible con caja.
## Alcance
### Sí entra
- proxy Next para Club
- landing/join/card frontend
- manifiesto e iconos PWA
- CTA de instalación
- movimientos recientes y saldo en tarjeta
### No entra
- earn/redeem en TPV
- linking con usuario registrado
- recovery codes
- admin UI del Club
- contrato final de lectura en caja
## Validación prevista
- `cd project/frontend && npm run build`
- `cd project && npm run build`
- `cd project && npm run typecheck`
- `./scripts/verify.sh`

View File

@@ -0,0 +1,22 @@
# CLUB-002 — Documentation notes
## New storefront routes
- `/club`
- `/club/join`
- `/club/card`
- `/manifest.webmanifest`
## New same-origin proxy
- `GET/POST/PATCH /api/club/[...path]`
- Forwards Club requests to backend `/club/*`
- Preserves incoming cookies and forwards backend `set-cookie`
## User-facing behavior
- Customers can activate an anonymous Club identity from the storefront without a separate login flow.
- After join, the browser keeps the `mdv_club` cookie and the Club card can be reopened later from the same device.
- The Club card shows member code, current balance and recent ledger movements.
- The storefront now exposes install metadata for a standalone Club PWA shell.
## Scope reminder
- CLUB-002 is frontend/PWA only.
- TPV scan semantics, recoveries and account linking stay in later Club phases.

View File

@@ -0,0 +1,52 @@
# Implementer evidence — CLUB-002
## Resumen
Implementé la primera superficie frontend/PWA del Club sobre el backend de CLUB-001.
## Qué se añadió
### 1) Proxy Next.js para Club
- `project/frontend/src/app/api/club/[...path]/route.ts`
Función:
- reenviar `GET/POST/PATCH` al backend
- propagar cookies entrantes
- reenviar `set-cookie` del backend para conservar `mdv_club` same-origin
### 2) Rutas frontend del Club
- `project/frontend/src/app/club/page.tsx`
- `project/frontend/src/app/club/join/page.tsx`
- `project/frontend/src/app/club/card/page.tsx`
- `project/frontend/src/components/club/ClubExperience.tsx`
Comportamiento:
- landing Club
- alta anónima desde `POST /api/club/join`
- tarjeta digital con `memberCode`, saldo y movimientos recientes
- CTA para abrir la tarjeta cuando ya existe identidad Club en el dispositivo
- bloque de instalación PWA usando `beforeinstallprompt`
### 3) Manifest e iconos PWA
- `project/frontend/src/app/manifest.ts`
- `project/frontend/public/images/club-icon-192.png`
- `project/frontend/public/images/club-icon-512.png`
Además:
- `project/frontend/src/app/layout.tsx` ahora declara manifest + metadata Apple Web App + viewport themeColor.
### 4) Descubribilidad en storefront
- enlace `Club` añadido en header y footer.
## Decisiones de implementación
- Reutilicé un único componente cliente `ClubExperience` para `/club`, `/club/join` y `/club/card`.
- El frontend consume el backend Club vía proxy Next same-origin para que la cookie `mdv_club` quede gestionada correctamente por el navegador.
- La tarjeta muestra una credencial visual móvil basada en `memberCode` y deja el contrato final de lectura en caja para CLUB-003, como estaba previsto en la fase TPV.
## Validación ejecutada
- `cd project/frontend && npm run build`
- `./scripts/verify.sh`
- `git diff --check`
## Riesgos / siguiente paso
- CLUB-003 debe cerrar el contrato exacto de identificación en TPV/caja para usar la tarjeta digital también en escaneo/lectura operativa.
- Conviene hacer smoke manual real en navegador/móvil para comprobar el prompt de instalación y la persistencia de `mdv_club` después de `POST /club/join`.

View File

@@ -0,0 +1,29 @@
{
"feature_id": "CLUB-002",
"agent": "leader",
"stage": "close",
"verdict": "APPROVED",
"summary": "CLUB-002 cerrada: PWA del Club con join, tarjeta digital, proxy same-origin y manifest instalable entregados con gates aprobados.",
"gates_summary": {
"reviewer": "APPROVED",
"security": "APPROVED",
"qa": "APPROVED"
},
"artifacts": [
"architect.md",
"implementer.md",
"reviewer.json",
"security.json",
"qa.json",
"documenter.md",
"leader-close.json"
],
"evidence": [
"cd project/frontend && npm run build",
"cd project && npm run typecheck",
"cd project && npm run build",
"./scripts/verify.sh",
"git diff --check"
],
"timestamp": "2026-08-26T17:21:45Z"
}

View File

@@ -0,0 +1,50 @@
{
"feature_id": "CLUB-002",
"agent": "qa",
"stage": "qa_gate",
"verdict": "APPROVED",
"qa_check": "qa",
"summary": "QA aprobado: CLUB-002 entrega navegación Club, alta/join, tarjeta digital y shell PWA instalable sin romper el build del storefront ni del backend principal.",
"test_results": {
"automated": [
"cd project/frontend && npm run build ✅",
"cd project && npm run typecheck ✅",
"cd project && npm run build ✅",
"./scripts/verify.sh ✅",
"git diff --check ✅"
],
"coverage": [
"ruta proxy /api/club/[...path]",
"páginas /club, /club/join y /club/card",
"manifest.webmanifest e iconos PWA",
"metadata/viewport del layout del storefront",
"descubribilidad del Club desde header y footer"
],
"manual_smoke_recommended": [
"Abrir /club y /club/join en navegador real y comprobar que POST /api/club/join deja cookie mdv_club y redirige a /club/card.",
"Comprobar en móvil/Chrome que aparece beforeinstallprompt y que la instalación abre start_url=/club en modo standalone.",
"Verificar con un miembro ya existente que /club/card recupera saldo y movimientos desde la cookie sin necesidad de login manual."
]
},
"notes": [
"El build de Next lista correctamente /club, /club/join, /club/card y /manifest.webmanifest.",
"No hay todavía tests E2E navegador para el prompt de instalación; queda como smoke manual recomendado."
],
"evidence": [
"work/artifacts/CLUB-002/implementer.md",
"work/artifacts/CLUB-002/reviewer.json",
"work/artifacts/CLUB-002/security.json",
"project/frontend/src/app/api/club/[...path]/route.ts",
"project/frontend/src/app/club/page.tsx",
"project/frontend/src/app/club/join/page.tsx",
"project/frontend/src/app/club/card/page.tsx",
"project/frontend/src/app/manifest.ts",
"project/frontend/src/components/club/ClubExperience.tsx",
"cd project/frontend && npm run build",
"cd project && npm run typecheck",
"cd project && npm run build",
"./scripts/verify.sh",
"git diff --check"
],
"timestamp": "2026-08-26T17:21:18Z"
}

View File

@@ -0,0 +1,47 @@
{
"feature_id": "CLUB-002",
"agent": "reviewer",
"stage": "review_gate",
"verdict": "APPROVED",
"summary": "Revisión técnica aprobada: CLUB-002 añade una primera superficie PWA coherente con CLUB-001 mediante proxy same-origin, rutas /club, tarjeta digital y manifest instalable sin adelantar todavía el contrato TPV de CLUB-003.",
"checks": [
{
"item": "El storefront añade las rutas /club, /club/join y /club/card reutilizando un componente cliente único y sin introducir acoplamiento global innecesario.",
"ok": true
},
{
"item": "El proxy Next /api/club preserva el prefijo backend /club/*, reenvía cookies y propaga set-cookie para mantener la identidad mdv_club same-origin.",
"ok": true
},
{
"item": "La PWA queda preparada con manifest, iconos, metadata Apple Web App y CTA de instalación, respetando el App Router actual.",
"ok": true
},
{
"item": "Los cambios quedaron validados con build del frontend, typecheck/build del backend, verify.sh y git diff --check.",
"ok": true
}
],
"issues": [],
"notes": [
"La tarjeta visual usa un identificador gráfico derivado del memberCode como placeholder UX; el contrato final de lectura/escaneo en caja sigue correctamente diferido a CLUB-003.",
"No se añadió provider global de Club; para esta fase el fetch puntual por página mantiene el alcance contenido y simplifica evolución futura."
],
"evidence": [
"work/artifacts/CLUB-002/architect.md",
"work/artifacts/CLUB-002/implementer.md",
"project/frontend/src/app/api/club/[...path]/route.ts",
"project/frontend/src/app/club/page.tsx",
"project/frontend/src/app/club/join/page.tsx",
"project/frontend/src/app/club/card/page.tsx",
"project/frontend/src/components/club/ClubExperience.tsx",
"project/frontend/src/app/manifest.ts",
"project/frontend/src/app/layout.tsx",
"cd project/frontend && npm run build",
"cd project && npm run typecheck",
"cd project && npm run build",
"./scripts/verify.sh",
"git diff --check"
],
"timestamp": "2026-08-26T17:20:40Z"
}

View File

@@ -0,0 +1,32 @@
{
"feature_id": "CLUB-002",
"agent": "security",
"stage": "security_gate",
"verdict": "APPROVED",
"security_check": "security",
"summary": "Aprobado: CLUB-002 no introduce nuevas superficies privilegiadas y mantiene la identidad Club dentro de un proxy same-origin que reusa la cookie existente sin exponer secretos adicionales.",
"checks": {
"auth_cookie_flow": "OK: el frontend no gestiona tokens Club en localStorage ni query params; el proxy /api/club reenvía la cookie mdv_club y propaga set-cookie del backend para seguir con el modelo httpOnly de CLUB-001.",
"data_exposure": "OK: las vistas Club muestran solo memberCode, saldo y movimientos del propio dispositivo; no se añaden datos administrativos ni personales adicionales.",
"xss_client": "OK: la UI renderiza datos como texto y SVG generado en React; no usa dangerouslySetInnerHTML, innerHTML ni eval.",
"proxy_integrity": "OK: el proxy apunta a rutas concretas del backend preservando el prefijo /club/* y pasa cookies/Content-Type de forma explícita, sin interpolación insegura de cabeceras arbitrarias del usuario.",
"dependencies": "OK: no se añadieron dependencias nuevas de terceros.",
"pwa_scope": "OK: manifest e iconos no amplían permisos del navegador ni añaden service worker propio en esta fase."
},
"notes": [
"El identificador gráfico de la tarjeta es una representación visual UX, no un secreto ni un mecanismo criptográfico; la semántica operativa de lectura queda para CLUB-003.",
"El uso de NEXT_PUBLIC_API_URL queda encapsulado en el proxy servidor; los componentes cliente hablan con /api/club same-origin."
],
"evidence": [
"project/frontend/src/app/api/club/[...path]/route.ts",
"project/frontend/src/components/club/ClubExperience.tsx",
"project/frontend/src/app/manifest.ts",
"project/frontend/src/app/layout.tsx",
"rg -n \"dangerouslySetInnerHTML|eval\\(|new Function|innerHTML|beforeinstallprompt|set-cookie|cookie|NEXT_PUBLIC_API_URL\" project/frontend/src/app/api/club project/frontend/src/components/club project/frontend/src/app/layout.tsx project/frontend/src/app/manifest.ts",
"cd project/frontend && npm run build",
"cd project && npm run typecheck",
"cd project && npm run build",
"./scripts/verify.sh"
],
"timestamp": "2026-08-26T17:20:55Z"
}

View File

@@ -1,104 +1,13 @@
{
"feature_id": "CLUB-001",
"feature_id": "CLUB-002",
"stage": "close",
"agent": "leader",
"action": "Cerrar CLUB-001 con commit/push automático",
"action": "Cerrar CLUB-002 con commit/push automático",
"state": "running",
"next_agent": "leader",
"waiting_for": "close",
"updated_at": "2026-08-26T16:54:57Z",
"updated_at": "2026-08-26T17:21:47Z",
"timeline": [
{
"ts": "2026-08-26T16:00:10Z",
"agent": "implementer",
"stage": "build",
"state": "running",
"message": "Implementar endpoint optimizado y UI paginada de inventario"
},
{
"ts": "2026-08-26T16:06:51Z",
"agent": "implementer",
"stage": "build",
"state": "done",
"message": "Inventario optimizado con paginación y sin N+1"
},
{
"ts": "2026-08-26T16:06:54Z",
"agent": "reviewer",
"stage": "review_gate",
"state": "running",
"message": "Revisión técnica de optimización de inventario"
},
{
"ts": "2026-08-26T16:07:05Z",
"agent": "reviewer",
"stage": "review_gate",
"state": "done",
"message": "Revisión técnica aprobada para inventario"
},
{
"ts": "2026-08-26T16:07:08Z",
"agent": "security",
"stage": "security_gate",
"state": "running",
"message": "Revisión de seguridad de inventory admin overview"
},
{
"ts": "2026-08-26T16:07:19Z",
"agent": "security",
"stage": "security_gate",
"state": "done",
"message": "Revisión de seguridad aprobada para inventario"
},
{
"ts": "2026-08-26T16:07:22Z",
"agent": "qa",
"stage": "qa_gate",
"state": "running",
"message": "QA de inventario paginado"
},
{
"ts": "2026-08-26T16:07:34Z",
"agent": "qa",
"stage": "qa_gate",
"state": "done",
"message": "QA aprobada para inventario optimizado"
},
{
"ts": "2026-08-26T16:07:40Z",
"agent": "leader",
"stage": "close",
"state": "running",
"message": "Cerrar INVENTORY-OPT y commit/push automático"
},
{
"ts": "2026-08-26T16:07:53Z",
"agent": "leader",
"stage": "close",
"state": "done",
"message": "INVENTORY-OPT cerrada; siguiente en cola SHIPPING-ZONES"
},
{
"ts": "2026-08-26T16:09:18Z",
"agent": "architect",
"stage": "design",
"state": "running",
"message": "Analizar restricciones Baleares/Canarias y zonas continentales"
},
{
"ts": "2026-08-26T16:52:12Z",
"agent": "reviewer",
"stage": "review_gate",
"state": "running",
"message": "Retomar CLUB-001 y revisar técnicamente el core backend del Club"
},
{
"ts": "2026-08-26T16:53:28Z",
"agent": "reviewer",
"stage": "review_gate",
"state": "done",
"message": "Revisión técnica aprobada para CLUB-001"
},
{
"ts": "2026-08-26T16:53:35Z",
"agent": "security",
@@ -147,6 +56,97 @@
"stage": "close",
"state": "running",
"message": "Cerrar CLUB-001 con commit/push automático"
},
{
"ts": "2026-08-26T16:55:24Z",
"agent": "architect",
"stage": "design",
"state": "running",
"message": "Diseñar PWA Club: join flow, tarjeta digital e instalación standalone"
},
{
"ts": "2026-08-26T16:57:43Z",
"agent": "architect",
"stage": "design",
"state": "done",
"message": "Diseño de CLUB-002 completado"
},
{
"ts": "2026-08-26T16:57:46Z",
"agent": "implementer",
"stage": "build",
"state": "running",
"message": "Implementar frontend PWA del Club con proxy, páginas y manifest"
},
{
"ts": "2026-08-26T17:02:40Z",
"agent": "implementer",
"stage": "build",
"state": "done",
"message": "Frontend PWA del Club implementado y validado en build"
},
{
"ts": "2026-08-26T17:20:07Z",
"agent": "reviewer",
"stage": "review_gate",
"state": "running",
"message": "Revisión técnica de la PWA Club"
},
{
"ts": "2026-08-26T17:20:37Z",
"agent": "reviewer",
"stage": "review_gate",
"state": "done",
"message": "Revisión técnica aprobada para CLUB-002"
},
{
"ts": "2026-08-26T17:20:42Z",
"agent": "security",
"stage": "security_gate",
"state": "running",
"message": "Revisión de seguridad de la PWA Club"
},
{
"ts": "2026-08-26T17:21:02Z",
"agent": "security",
"stage": "security_gate",
"state": "done",
"message": "Revisión de seguridad aprobada para CLUB-002"
},
{
"ts": "2026-08-26T17:21:07Z",
"agent": "qa",
"stage": "qa_gate",
"state": "running",
"message": "QA de la PWA Club"
},
{
"ts": "2026-08-26T17:21:25Z",
"agent": "qa",
"stage": "qa_gate",
"state": "done",
"message": "QA aprobada para CLUB-002"
},
{
"ts": "2026-08-26T17:21:28Z",
"agent": "documenter",
"stage": "document",
"state": "running",
"message": "Documentar flujos y contrato visible de CLUB-002"
},
{
"ts": "2026-08-26T17:21:37Z",
"agent": "documenter",
"stage": "document",
"state": "done",
"message": "Documentación de CLUB-002 completada"
},
{
"ts": "2026-08-26T17:21:47Z",
"agent": "leader",
"stage": "close",
"state": "running",
"message": "Cerrar CLUB-002 con commit/push automático"
}
]
}