From b2ccc70dc6d742bad5bb3a13d8fc75d89390c324 Mon Sep 17 00:00:00 2001 From: Deploy Date: Wed, 26 Aug 2026 18:55:00 +0200 Subject: [PATCH] feat(club-001): club core backend: anonymous members, devices, ledger, cashback config --- backlog/features.json | 14 +- work/artifacts/CLUB-001/documenter.md | 20 +++ work/artifacts/CLUB-001/leader-close.json | 29 ++++ work/artifacts/CLUB-001/qa.json | 48 +++++++ work/artifacts/CLUB-001/reviewer.json | 45 ++++++ work/artifacts/CLUB-001/security.json | 32 +++++ work/artifacts/SHIPPING-ZONES/architect.md | 44 ++++++ work/runtime-status.json | 160 ++++++++++----------- 8 files changed, 306 insertions(+), 86 deletions(-) create mode 100644 work/artifacts/CLUB-001/documenter.md create mode 100644 work/artifacts/CLUB-001/leader-close.json create mode 100644 work/artifacts/CLUB-001/qa.json create mode 100644 work/artifacts/CLUB-001/reviewer.json create mode 100644 work/artifacts/CLUB-001/security.json create mode 100644 work/artifacts/SHIPPING-ZONES/architect.md diff --git a/backlog/features.json b/backlog/features.json index 2fcb3a1..2acfee6 100644 --- a/backlog/features.json +++ b/backlog/features.json @@ -7896,7 +7896,7 @@ "description": "Need change", "priority": "med", "risk": "low", - "status": "pending", + "status": "blocked", "created_at": "2026-08-24", "gates": { "reviewer": false, @@ -8008,14 +8008,16 @@ "description": "Phase 1. Add club members, device tokens, recovery-ready identity model, transaction ledger as source of truth, cashback config, basic backend endpoints, migrations and tests.", "priority": "high", "risk": "high", - "status": "blocked", + "status": "done", "created_at": "2026-08-26", "gates": { - "reviewer": false, - "security": false, - "qa": false + "reviewer": true, + "security": true, + "qa": true, + "close": true }, - "phase": "backend" + "phase": "backend", + "completed_at": "2026-08-26T16:54:59Z" }, { "id": "CLUB-002", diff --git a/work/artifacts/CLUB-001/documenter.md b/work/artifacts/CLUB-001/documenter.md new file mode 100644 index 0000000..04ef3ec --- /dev/null +++ b/work/artifacts/CLUB-001/documenter.md @@ -0,0 +1,20 @@ +# CLUB-001 — Documentation notes + +## New backend endpoints +- `GET /club/config` +- `POST /club/join` +- `GET /club/me` +- `GET /club/movements` +- `GET /admin/club/settings` +- `PATCH /admin/club/settings` + +## Contract notes +- Anonymous Club identity is device-based for now. +- Client receives the raw `deviceToken` once; database stores only `device_token_hash`. +- The backend also persists the token in cookie `mdv_club` and accepts it again via cookie or `x-club-device-token` header. +- `club_transactions` is the source of truth; `club_members.current_balance_cents` is a transactional cache. +- Cashback is configurable through `store_settings` (`club_cashback_bps`) instead of hardcoded constants. + +## Scope boundary +- CLUB-001 ships backend foundations only. +- PWA screens, digital card UI, TPV integration, recovery flow and account linking remain for later CLUB phases. diff --git a/work/artifacts/CLUB-001/leader-close.json b/work/artifacts/CLUB-001/leader-close.json new file mode 100644 index 0000000..a075357 --- /dev/null +++ b/work/artifacts/CLUB-001/leader-close.json @@ -0,0 +1,29 @@ +{ + "feature_id": "CLUB-001", + "agent": "leader", + "stage": "close", + "verdict": "APPROVED", + "summary": "CLUB-001 cerrada: fase 1 backend del Club entregada con ledger, identidad anónima por dispositivo, settings admin, tests y 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": [ + "./scripts/verify.sh", + "work/artifacts/CLUB-001/reviewer.json", + "work/artifacts/CLUB-001/security.json", + "work/artifacts/CLUB-001/qa.json", + "work/artifacts/CLUB-001/documenter.md" + ], + "timestamp": "2026-08-26T16:54:55Z" +} diff --git a/work/artifacts/CLUB-001/qa.json b/work/artifacts/CLUB-001/qa.json new file mode 100644 index 0000000..9ecc1c5 --- /dev/null +++ b/work/artifacts/CLUB-001/qa.json @@ -0,0 +1,48 @@ +{ + "feature_id": "CLUB-001", + "agent": "qa", + "stage": "qa_gate", + "verdict": "APPROVED", + "qa_check": "qa", + "summary": "QA aprobado: CLUB-001 cubre el alta anónima, persistencia del device token hasheado, lectura de socio/movimientos y settings admin con tests reales e idempotencia del ledger.", + "test_results": { + "automated": [ + "./scripts/verify.sh ✅", + "cd project && npm run typecheck ✅", + "cd project && npm run build ✅", + "cd project && npx vitest run src/modules/club/tests/token-and-code.test.ts src/modules/club/tests/boundary.test.ts ✅", + "cd project && TEST_DATABASE_URL=postgres://mdv:mdv_dev_only@localhost:5432/mercadodevida_test npx vitest run src/app/tests/club.itest.ts --no-file-parallelism ✅", + "git diff --check ✅" + ], + "coverage": [ + "GET /club/config", + "POST /club/join con creación inicial y reuse por cookie/header", + "GET /club/me", + "GET /club/movements", + "GET/PATCH /admin/club/settings", + "idempotencia de club.recordTransaction" + ], + "manual_smoke_recommended": [ + "Cuando exista frontend/PWA, comprobar persistencia real de la cookie mdv_club entre navegación y reinstalación en móvil.", + "En fases TPV futuras, validar earn/redeem/refund contra el ledger usando la misma idempotencyKey por evento de negocio." + ] + }, + "notes": [ + "La itest muestra un warning de SMTP no configurado durante el registro del admin de prueba; no rompe la suite y viene del flujo de identidad preexistente.", + "No se observaron regresiones del harness al revalidar verify.sh después de retomar CLUB-001." + ], + "evidence": [ + "work/artifacts/CLUB-001/implementer.md", + "work/artifacts/CLUB-001/reviewer.json", + "work/artifacts/CLUB-001/security.json", + "project/src/app/tests/club.itest.ts", + "project/src/modules/club/tests/token-and-code.test.ts", + "project/src/modules/club/tests/boundary.test.ts", + "./scripts/verify.sh", + "cd project && npm run typecheck", + "cd project && npm run build", + "cd project && TEST_DATABASE_URL=postgres://mdv:mdv_dev_only@localhost:5432/mercadodevida_test npx vitest run src/app/tests/club.itest.ts --no-file-parallelism", + "git diff --check" + ], + "timestamp": "2026-08-26T16:54:20Z" +} diff --git a/work/artifacts/CLUB-001/reviewer.json b/work/artifacts/CLUB-001/reviewer.json new file mode 100644 index 0000000..5d0d27f --- /dev/null +++ b/work/artifacts/CLUB-001/reviewer.json @@ -0,0 +1,45 @@ +{ + "feature_id": "CLUB-001", + "agent": "reviewer", + "stage": "review_gate", + "verdict": "APPROVED", + "summary": "Revisión técnica aprobada: CLUB-001 entrega el core backend del Club con módulo aislado, ledger idempotente, settings reutilizando store_settings y cobertura de tests real sobre PostgreSQL.", + "checks": [ + { + "item": "El módulo club sigue la arquitectura del proyecto (api/application/domain/infrastructure/index) y queda registrado en build-app.ts sin acoplarse a internals de otros módulos.", + "ok": true + }, + { + "item": "La persistencia usa club_transactions como source of truth y mantiene current_balance_cents como cache transaccional con control de saldo negativo e idempotency_key.", + "ok": true + }, + { + "item": "Los endpoints de Fase 1 cubren config, join, me, movements y settings admin con validación zod/parseJson y mapeo explícito de errores AppError.", + "ok": true + }, + { + "item": "La feature quedó validada con typecheck, build, tests unitarios/boundary y una itest real PostgreSQL para join, reuse, movements y settings admin.", + "ok": true + } + ], + "issues": [], + "notes": [ + "Las migraciones 057 y 058 corregidas por el implementer son necesarias para que las itest reales del Club migren hasta 066_club_core; no cambian la intención funcional previa.", + "La semántica de bloqueo/merge de miembros queda modelada en schema pero la aplicación operativa de esos estados se puede profundizar en fases posteriores cuando exista linking/admin completo." + ], + "evidence": [ + "work/artifacts/CLUB-001/architect.md", + "work/artifacts/CLUB-001/implementer.md", + "project/src/modules/club/api/club.routes.ts", + "project/src/modules/club/application/club-service.ts", + "project/src/modules/club/infrastructure/pg-club-repository.ts", + "project/migrations/066_club_core.js", + "project/src/app/tests/club.itest.ts", + "cd project && npm run typecheck", + "cd project && npm run build", + "cd project && npx vitest run src/modules/club/tests/token-and-code.test.ts src/modules/club/tests/boundary.test.ts", + "cd project && TEST_DATABASE_URL=postgres://mdv:mdv_dev_only@localhost:5432/mercadodevida_test npx vitest run src/app/tests/club.itest.ts --no-file-parallelism", + "git diff --check" + ], + "timestamp": "2026-08-26T16:52:50Z" +} diff --git a/work/artifacts/CLUB-001/security.json b/work/artifacts/CLUB-001/security.json new file mode 100644 index 0000000..120e4c7 --- /dev/null +++ b/work/artifacts/CLUB-001/security.json @@ -0,0 +1,32 @@ +{ + "feature_id": "CLUB-001", + "agent": "security", + "stage": "security_gate", + "verdict": "APPROVED", + "security_check": "security", + "summary": "Aprobado: CLUB-001 mantiene autenticación, validación e integridad adecuadas; los tokens de dispositivo no se almacenan en claro y el ledger usa claves de idempotencia para evitar duplicados.", + "checks": { + "auth": "OK: /admin/club/settings exige authenticate + requireRole(admin); /club/me y /club/movements solo resuelven miembro con sesión customer o token de dispositivo válido; /club/join no expone privilegios administrativos.", + "secrets": "OK: el raw device token solo se entrega al cliente y en base de datos se persiste exclusivamente device_token_hash (SHA-256). No se introducen secretos hardcoded ni credenciales nuevas.", + "input_validation": "OK: settings admin y query de movimientos usan zod + parseJson; los errores de dominio se traducen a AppError controlados.", + "integrity": "OK: club_transactions tiene idempotency_key único; recordTransaction bloquea la fila de club_members con FOR UPDATE y rechaza balances negativos.", + "injection_xss": "OK: las consultas SQL del módulo están parametrizadas y no se introduce renderizado HTML crudo ni eval/dangerouslySetInnerHTML.", + "dependencies": "OK: no se añadieron dependencias nuevas para el módulo Club." + }, + "notes": [ + "La cookie mdv_club se emite como httpOnly + sameSite=lax y marca secure cuando la petición llega por HTTPS o x-forwarded-proto=https.", + "El warning de envío de email no configurado visto en la itest proviene del flujo preexistente de registro de identidad y no de la superficie nueva del Club." + ], + "evidence": [ + "project/src/modules/club/api/club.routes.ts", + "project/src/modules/club/application/club-service.ts", + "project/src/modules/club/infrastructure/device-token.ts", + "project/src/modules/club/infrastructure/pg-club-repository.ts", + "project/migrations/066_club_core.js", + "rg -n \"device_token_hash|x-club-device-token|mdv_club|club_enabled|club_cashback_bps|idempotency_key\" project/src/modules/club project/migrations/066_club_core.js", + "cd project && npm run typecheck", + "cd project && npm run build", + "cd project && TEST_DATABASE_URL=postgres://mdv:mdv_dev_only@localhost:5432/mercadodevida_test npx vitest run src/app/tests/club.itest.ts --no-file-parallelism" + ], + "timestamp": "2026-08-26T16:53:40Z" +} diff --git a/work/artifacts/SHIPPING-ZONES/architect.md b/work/artifacts/SHIPPING-ZONES/architect.md new file mode 100644 index 0000000..96975c7 --- /dev/null +++ b/work/artifacts/SHIPPING-ZONES/architect.md @@ -0,0 +1,44 @@ +# Arquitectura — SHIPPING-ZONES + +## Problema detectado +El sistema actual de envíos solo soporta zonas por `country + postal_code_prefix` y tiene dos limitaciones: + +1. Una zona genérica de España con `postal_code_prefix = null` también matchea Baleares/Canarias. +2. El endpoint público `/shipping/methods` devuelve métodos de todas las zonas compatibles en vez de resolver la **mejor zona** como sí hace `ShippingService.calculate()`. +3. El checkout frontend carga métodos sin contexto de `country/postalCode`, así que la UI puede enseñar opciones no válidas para la dirección real. + +## Diseño propuesto + +### 1) Extender zonas con exclusiones +Añadir a `shipping_zones` un campo: +- `excluded_postal_code_prefixes text[]` + +Con esto una zona "España peninsular" puede excluir: +- `07` (Baleares) +- `35`, `38` (Canarias) +- opcionalmente `51`, `52` si se quiere mantener también fuera Ceuta/Melilla + +### 2) Unificar matching de zona +Hacer que el listado público de métodos use la misma resolución de mejor zona que el cálculo de shipping: +- misma lógica de prefijo más específico +- respetando exclusiones +- evitando devolver simultáneamente métodos de la zona genérica y de una zona específica + +### 3) Admin shipping UI +Actualizar admin shipping para editar el nuevo campo como CSV legible. + +### 4) Frontend checkout +Actualizar la obtención de métodos de envío para que use `country` y `postalCode` reales del formulario/dirección seleccionada. + +## Alcance +- Sí entra: + - exclusiones por prefijo postal + - mejor matching de zona en backend + - checkout contextual por país/código postal + - soporte admin para editar exclusiones +- No entra: + - rediseño completo del motor de campañas/logística + - nuevas tarifas complejas por operador + +## Nota +Con este enfoque el sistema ya soporta zonas continentales por país y además permite que una zona genérica excluya archipiélagos/territorios especiales sin romper la arquitectura actual. diff --git a/work/runtime-status.json b/work/runtime-status.json index ca83fea..5c2ad0c 100644 --- a/work/runtime-status.json +++ b/work/runtime-status.json @@ -1,90 +1,13 @@ { - "feature_id": "INVENTORY-OPT", + "feature_id": "CLUB-001", "stage": "close", "agent": "leader", - "action": "Cerrar INVENTORY-OPT y commit/push automático", + "action": "Cerrar CLUB-001 con commit/push automático", "state": "running", "next_agent": "leader", "waiting_for": "close", - "updated_at": "2026-08-26T16:07:40Z", + "updated_at": "2026-08-26T16:54:57Z", "timeline": [ - { - "ts": "2026-08-26T10:41:58Z", - "agent": "qa", - "stage": "qa_gate", - "state": "done", - "message": "QA aprobada para IVA POS y checkout sync" - }, - { - "ts": "2026-08-26T10:42:36Z", - "agent": "leader", - "stage": "close", - "state": "blocked", - "message": "Cierre bloqueado: POS-RECEIPT-QUICK-FIXES no existe en backlog/features.json" - }, - { - "ts": "2026-08-26T15:23:01Z", - "agent": "leader", - "stage": "intake", - "state": "running", - "message": "Crear tickets solicitados por el usuario" - }, - { - "ts": "2026-08-26T15:23:29Z", - "agent": "leader", - "stage": "intake", - "state": "done", - "message": "Creados 6 tickets nuevos en backlog" - }, - { - "ts": "2026-08-26T15:37:29Z", - "agent": "architect", - "stage": "design", - "state": "running", - "message": "Analizar arquitectura y diseñar fase 1 backend de Club de Clientes" - }, - { - "ts": "2026-08-26T15:44:37Z", - "agent": "architect", - "stage": "design", - "state": "done", - "message": "Diseño de fase 1 backend de Club completado" - }, - { - "ts": "2026-08-26T15:44:37Z", - "agent": "implementer", - "stage": "build", - "state": "running", - "message": "Implementar Club fase 1 backend: módulo, migración, endpoints y tests" - }, - { - "ts": "2026-08-26T15:52:21Z", - "agent": "implementer", - "stage": "build", - "state": "done", - "message": "Club fase 1 backend implementado y validado" - }, - { - "ts": "2026-08-26T15:59:00Z", - "agent": "implementer", - "stage": "build", - "state": "blocked", - "message": "CLUB-001 pausada: priorizar INVENTORY-OPT, SHIPPING-ZONES y batch de fixes solicitado por usuario" - }, - { - "ts": "2026-08-26T15:59:14Z", - "agent": "architect", - "stage": "design", - "state": "running", - "message": "Analizar inventario para optimización con 10k+ productos" - }, - { - "ts": "2026-08-26T16:00:10Z", - "agent": "architect", - "stage": "design", - "state": "done", - "message": "Diseño completado para inventario paginado y sin N+1" - }, { "ts": "2026-08-26T16:00:10Z", "agent": "implementer", @@ -147,6 +70,83 @@ "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", + "stage": "security_gate", + "state": "running", + "message": "Revisión de seguridad del core backend del Club" + }, + { + "ts": "2026-08-26T16:53:52Z", + "agent": "security", + "stage": "security_gate", + "state": "done", + "message": "Revisión de seguridad aprobada para CLUB-001" + }, + { + "ts": "2026-08-26T16:53:56Z", + "agent": "qa", + "stage": "qa_gate", + "state": "running", + "message": "QA del core backend del Club" + }, + { + "ts": "2026-08-26T16:54:14Z", + "agent": "qa", + "stage": "qa_gate", + "state": "done", + "message": "QA aprobada para CLUB-001" + }, + { + "ts": "2026-08-26T16:54:39Z", + "agent": "documenter", + "stage": "document", + "state": "running", + "message": "Documentar endpoints y contratos de CLUB-001" + }, + { + "ts": "2026-08-26T16:54:48Z", + "agent": "documenter", + "stage": "document", + "state": "done", + "message": "Documentación de contratos de CLUB-001 completada" + }, + { + "ts": "2026-08-26T16:54:57Z", + "agent": "leader", + "stage": "close", + "state": "running", + "message": "Cerrar CLUB-001 con commit/push automático" } ] }