feat(club-001): club core backend: anonymous members, devices, ledger, cashback config

This commit is contained in:
Deploy
2026-08-26 18:55:00 +02:00
parent c19626bea3
commit b2ccc70dc6
8 changed files with 306 additions and 86 deletions

View File

@@ -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.

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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.

View File

@@ -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"
}
]
}