feat(club-001): club core backend: anonymous members, devices, ledger, cashback config
This commit is contained in:
@@ -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",
|
||||
|
||||
20
work/artifacts/CLUB-001/documenter.md
Normal file
20
work/artifacts/CLUB-001/documenter.md
Normal 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.
|
||||
29
work/artifacts/CLUB-001/leader-close.json
Normal file
29
work/artifacts/CLUB-001/leader-close.json
Normal 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"
|
||||
}
|
||||
48
work/artifacts/CLUB-001/qa.json
Normal file
48
work/artifacts/CLUB-001/qa.json
Normal 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"
|
||||
}
|
||||
45
work/artifacts/CLUB-001/reviewer.json
Normal file
45
work/artifacts/CLUB-001/reviewer.json
Normal 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"
|
||||
}
|
||||
32
work/artifacts/CLUB-001/security.json
Normal file
32
work/artifacts/CLUB-001/security.json
Normal 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"
|
||||
}
|
||||
44
work/artifacts/SHIPPING-ZONES/architect.md
Normal file
44
work/artifacts/SHIPPING-ZONES/architect.md
Normal 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.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user