--- type: DC asset_id: DC-REB-RClub-SistemaPuntosMembresias-v01 version: v01 status: Draft owner: Victor Heredia sherpa_owner: Jay fecha_creacion: 2026-06-11 fecha_ultima_actualizacion: 2026-06-11 intellbank: IB-REB-Rebelocity subbank: — tipo_largo: Documento Canónico — Documentación técnica completa del motor de Puntos y Membresías (Points v2) de la Plataforma de Comunidades Colaborativas audiencia: Equipo de desarrollo + producto (EmpowerLabs) · referencia para diseño de capabilities (afiliación, tarjeta, puntos, tracking) proposito: Documentar el flujo completo del sistema Points v2 — asignación de puntos, membresías, tiers, recompensas, referidos, tarjetas digitales/QR, organizaciones afiliadas y anti-fraude. Es la fuente técnica de verdad sobre la que se diseña el esquema de numeración de tarjetas (ver SPEC-REB-RClub-NumeracionTarjetas-v01). nota_scope: El motor es multi-organización (MPB · TRIBU · CLUB) y soporta todas las comunidades colaborativas; se archiva en IB-REB por instrucción de Victor, dado que Rebelocity Club es la comunidad piloto. Activo de alcance transversal a la plataforma. referencias_canonicas: - SPEC-REB-RClub-NumeracionTarjetas-v01 (esquema de numeración de tarjetas derivado de este sistema) - RFI-EL-PlataformaComunidades-AfiliacionTarjetaPuntos-v01 (requerimientos de implementación) - PLAN-EL-DesarrolloPlataformaComunidades-v01 (sprint dev base) - SPEC-REB-RClub-ArquitecturaDatos-RegistroPerfil-v01 --- ## Asset Header - **Asset ID:** DC-REB-RClub-SistemaPuntosMembresias-v01 - **Version:** v01 - **Status:** Draft - **Owner:** Victor Heredia - **Sherpa:** Jay - **Ratificador:** — - **IntellBank:** IB-REB-Rebelocity - **Tipo:** DC — Documento Canónico - **Propósito:** Documentación técnica completa del motor Points v2 (puntos, membresías, tiers, tarjetas digitales). - **Última actualización:** 2026-06-11 --- # Sistema de Puntos y Membresías — Documentación Completa > **Propósito:** Documentar el flujo completo de asignación de puntos, membresías, tiers, recompensas, referidos y operaciones del sistema Points v2. > > **Última actualización:** Junio 2026 --- ## Contexto del Sistema El **Sistema de Puntos (Points v2)** es el motor de lealtad y engagement de la plataforma. Permite asignar puntos a los usuarios por realizar acciones clave (registro, lectura, compras, referidos, etc.), canjear esos puntos por recompensas, y progresar a través de niveles (tiers) que desbloquean beneficios exclusivos. ### Principios base del diseño - **Append-only ledger**: Cada transacción de puntos es inmutable. El saldo se calcula siempre como la suma de todas las transacciones. - **Idempotencia**: Cada operación tiene una clave única que previene duplicados, incluso con reintentos del cliente. - **Niveles que nunca bajan**: El tier del usuario se determina por sus puntos de vida (*lifetime*), que nunca decrecen. Esto evita la frustración de perder beneficios. - **Multi-organización**: El sistema soporta múltiples comunidades/marcas (MPB, TRIBU, CLUB) con configuraciones de puntos, niveles y recompensas independientes. - **Cooldowns y topes**: Cada acción tiene reglas de frecuencia (diario, semanal, mensual, único) y topes máximos para prevenir abuso. - **Sin expiración de puntos**: Los puntos acumulados no expiran, a diferencia del sistema legacy de créditos. ### Diferencias con el sistema legacy de créditos | Aspecto | Points v2 (nuevo) | Créditos (legacy) | |---|---|---| | Almacenamiento | MySQL (tablas propias) | MySQL (tablas pb_*) | | Expiración | No expiran | Expiran mensualmente | | Niveles | Tiers basados en lifetime | Planes de suscripción (Gratis/Básica/Pro) | | Canje | Recompensas del catálogo | Desbloqueo de resúmenes | | Multi-org | Sí (organizaciones afiliadas) | No (sistema global) | --- ## Índice 1. [Arquitectura General](#1-arquitectura-general) 2. [Modelo de Datos](#2-modelo-de-datos) 3. [Sistema de Membresías (Planes de Suscripción)](#3-sistema-de-membresías) 4. [Sistema de Tiers (Puntos de Lealtad)](#4-sistema-de-tiers) 5. [Flujo de Asignación de Puntos](#5-flujo-de-asignación-de-puntos) 6. [Catálogo de Acciones y Puntos](#6-catálogo-de-acciones) 7. [Sistema de Recompensas (Canje)](#7-sistema-de-recompensas) 8. [Sistema de Referidos](#8-sistema-de-referidos) 9. [Tarjetas Digitales y QR](#9-tarjetas-digitales-y-qr) 10. [Organizaciones Afiliadas](#10-organizaciones-afiliadas) 11. [Tareas Programadas (Cron)](#11-tareas-programadas) 12. [Seguridad y Anti-Fraude](#12-seguridad-y-anti-fraude) 13. [APIs](#13-apis) 14. [Integración con KatIA (AI Sherpa)](#14-integración-con-katia) --- ## 1. Arquitectura General ``` ┌──────────────┐ ┌──────────────────────┐ ┌─────────────────┐ │ Frontend │────▶│ Node.js (Express) │────▶│ MySQL (Points)│ │ (Web/Móvil) │ │ API REST │ │ MySQL (Points) │ └──────────────┘ └──────────────────────┘ └─────────────────┘ │ ┌──────┴──────┐ │ PHP API │ │ (validate- │ │ membership)│ └─────────────┘ ``` ### Stack tecnológico | Componente | Tecnología | |---|---| | API Principal | Node.js + Express | | Base de datos principal | MySQL (Points v2 + créditos legacy) | | Base de datos | MySQL (todo el sistema de puntos y membresías) | | Autenticación | Firebase Auth + JWT | | Pasarela de pago | Stripe (webhooks entrantes) | | Proxy API | PHP (validación de membresía getUser) | ### Ubicación de archivos clave ``` routes/points/index.js → Enrutamiento Points v2 (/api/points) routes/credits/index.js → Enrutamiento membresías legacy (/api/membership) controllers/points/*.js → Controladores Points v2 services/points/*.js → Lógica de negocio Points v2 services/membership/*.js → Lógica de membresías/planes sql/points/*.sql → Migraciones y esquemas tests/points/points-v2.http → Tests HTTP ``` --- ## 2. Modelo de Datos ### 2.1 Tablas del sistema Points v2 (MySQL) #### `point_actions` — Catálogo de acciones que otorgan puntos | Columna | Tipo | Descripción | |---|---|---| | `id` | INT PK | Auto-incremental | | `organization_id` | INT NULL | FK a `affiliated_organizations`. NULL = global | | `action_key` | VARCHAR(50) | Identificador único por organización | | `name` | VARCHAR(100) | Nombre descriptivo | | `description` | VARCHAR(255) | Descripción | | `points` | INT | Puntos que otorga (positivo) | | `cooldown` | VARCHAR(20) | `once` \| `daily` \| `weekly` \| `monthly` \| NULL | | `max_per_period` | INT NULL | Tope mensual (NULL = sin tope) | | `requires_card` | BOOLEAN | Requiere tarjeta digital existente | | `is_global` | BOOLEAN | Aplica a todas las organizaciones | | `enabled` | BOOLEAN | Activo/inactivo | **Unique:** `(organization_id, action_key)` --- #### `user_points` — Saldos de puntos por usuario/organización | Columna | Tipo | Descripción | |---|---|---| | `id` | INT PK | Auto-incremental | | `user_id` | INT | FK a `puesto(id)` | | `organization_id` | INT | FK a `affiliated_organizations` | | `balance` | INT | Puntos disponibles para canjear | | `lifetime` | INT | Puntos acumulados históricos (nunca decrece) | | `tier_id` | INT NULL | FK a `membership_tiers` (NULL = NORMAL) | **Unique:** `(user_id, organization_id)` --- #### `point_transactions` — Libro contable (append-only) | Columna | Tipo | Descripción | |---|---|---| | `id` | BIGINT PK | Auto-incremental | | `user_id` | INT | FK a `puesto(id)` | | `organization_id` | INT | FK a `affiliated_organizations` | | `action_key` | VARCHAR(50) | `signup`, `read_summary`, `spend`, etc. | | `points` | INT | Positivo = ganados, negativo = gastados | | `balance_before` | INT | Saldo antes de la transacción | | `balance_after` | INT | Saldo después de la transacción | | `reference_type` | VARCHAR(50) | Tipo de referencia externa | | `reference_id` | VARCHAR(100) | ID de referencia externa | | `idempotency_key` | VARCHAR(64) | **UNIQUE** — Clave de idempotencia | | `metadata` | JSON | Metadatos adicionales | **Unique:** `(idempotency_key)` — Garantiza que ninguna transacción se duplique. --- #### `membership_tiers` — Definición de niveles (tiers) | Columna | Tipo | Descripción | |---|---|---| | `id` | INT PK | Auto-incremental | | `organization_id` | INT | FK a `affiliated_organizations` | | `code` | VARCHAR(30) | `NORMAL`, `CLASICA`, `ORO`, `PLATINO` | | `name` | VARCHAR(50) | Nombre del nivel | | `min_lifetime` | INT | Puntos de vida requeridos para alcanzar | | `color` | VARCHAR(7) | Color hexadecimal | | `icon` | VARCHAR(255) | Icono | | `benefits` | JSON | Beneficios: `discount_pct`, `free_playbook`, `priority_support`, etc. | | `sort_order` | INT | Orden de jerarquía (0-3) | | `enabled` | BOOLEAN | Activo/inactivo | **Unique:** `(organization_id, code)` --- #### `user_cards` — Tarjetas digitales | Columna | Tipo | Descripción | |---|---|---| | `user_id` | INT | FK a `puesto(id)` | | `organization_id` | INT | FK a `affiliated_organizations` | | `card_id` | VARCHAR(50) | UUID opaco, **UNIQUE** | | `card_number` | VARCHAR(12) | 12 dígitos numéricos, **UNIQUE** | | `tier_id` | INT NULL | FK a `membership_tiers` | | `tier_code` | VARCHAR(30) | Código del tier actual | | `badges` | JSON | Insignias | | `card_style` | JSON | Configuración visual | | `qr_secret` | VARCHAR(64) | HMAC-SHA256( `cardId`, `POINTS_QR_SECRET`) | | `is_public` | BOOLEAN | Perfil público visible | --- #### `referrals` — Seguimiento de referidos | Columna | Tipo | Descripción | |---|---|---| | `id` | INT PK | Auto-incremental | | `referrer_id` | INT | FK a `puesto(id)` — quien refiere | | `referred_id` | INT NULL | FK a `puesto(id)` — referido | | `organization_id` | INT | FK a `affiliated_organizations` | | `code` | VARCHAR(20) | **UNIQUE** — código alfanumérico de 8 caracteres | | `status` | ENUM | `pending` \| `completed` \| `rewarded` | | `referrer_points` | INT | Puntos otorgados al referente (200) | | `referred_points` | INT | Puntos otorgados al referido (100) | --- #### `point_rewards` — Catálogo de recompensas | Columna | Tipo | Descripción | |---|---|---| | `id` | INT PK | Auto-incremental | | `organization_id` | INT | FK a `affiliated_organizations` | | `reward_key` | VARCHAR(50) | Identificador único por organización | | `name` | VARCHAR(100) | Nombre | | `description` | VARCHAR(255) | Descripción | | `cost` | INT | Puntos requeridos (positivo) | | `category` | VARCHAR(50) | `membership` \| `playbook` \| `discount` \| `event` \| `merch` \| `other` | | `min_tier_code` | VARCHAR(30) NULL | Tier mínimo requerido (NULL = cualquiera) | | `stock` | INT NULL | Stock total (NULL = ilimitado) | | `stock_remaining` | INT NULL | Stock restante | | `valid_from` / `valid_until` | TIMESTAMP | Ventana de validez | --- #### `affiliated_organizations` — Comunidades/marcas afiliadas | Columna | Tipo | Descripción | |---|---|---| | `id` | INT PK | Auto-incremental | | `pap_eje_id` | INT | FK a `pap_eje` (sistema externo) | | `org_code` | VARCHAR(20) | `MPB`, `TRIBU`, `CLUB` — **UNIQUE** | | `brand_name` | VARCHAR(100) | Nombre de marca | | `brand_color` | VARCHAR(7) | Color hexadecimal | | `contact_email` / `contact_phone` | — | Datos de contacto | --- ### 2.2 Tablas del sistema de créditos legacy | Tabla | Propósito | |---|---| | `pb_membresia` | Planes de suscripción (Gratis, Básica, Básica Pro) | | `pb_monthly_usage` | Uso mensual de base (cupo + consumido) | | `pb_credit_balance_monthly` | Créditos mensuales (ganados, consumidos, expirados, disponibles) | | `pb_credit_ledger` | Libro contable de créditos | --- ## 3. Sistema de Membresías ### 3.1 Planes de Suscripción | Plan | Código | Cupo Base | Gana Créditos | AI Sherpa | Descuento | |---|---|---|---|---|---| | **Gratis** | `FREE` | 5 resúmenes/mes | Sí (hasta 5 extra) | No | 0% | | **Básica** | `BASIC` | Ilimitado | No | No | 10% | | **Básica Pro** | `BASIC_PRO` | Ilimitado | No | Sí (50 msgs/mes) | 15% | ### 3.2 Resolución de membresía El flujo `MembershipService.resolveMembership(userId)`: 1. Consulta a la API PHP: `POST /api/playbook/membresia/get/user` 2. Mapea nombres → códigos internos: - `'gratis'` → `FREE` - `'basica'` / `'básica'` → `BASIC` - `'basica pro'` / `'básica pro'` → `BASIC_PRO` - Cualquier otro → `FREE` (default) 3. Si el plan es `BASIC_PRO`, no consume créditos. 4. Solo `FREE` puede ganar créditos (`canEarnCredits`). ### 3.3 Upgrade sugerido `MembershipService.getSuggestedPlan(currentPlanCode, reason)`: - Cuando se alcanza el límite mensual (`MONTHLY_LIMIT_REACHED`) sugiere upgrade a `BASIC_PRO`. --- ## 4. Sistema de Tiers ### 4.1 Niveles de lealtad (basados en puntos de vida) | Código | Nombre | Puntos de Vida | Beneficios | |---|---|---|---| | `NORMAL` | Normal | 0 | Sin beneficios | | `CLASICA` | Clásica | 500 | 5% de descuento | | `ORO` | Oro | 2,000 | 10% descuento + playbook gratis | | `PLATINO` | Platino | 5,000 | 15% descuento + playbook gratis + soporte prioritario + AI Sherpa + contenido exclusivo | ### 4.2 Reglas del sistema de tiers - **Solo ascienden, nunca bajan**: El tier se calcula sobre `lifetime` (puntos acumulados históricos), no sobre `balance`. - **Cálculo automático**: Cada vez que se otorgan puntos (`awardPoints`), se invoca `TierService.recalculateTier()`. - **Batch**: `TierService.recalculateAllTiers(orgId)` permite recalcular todos los usuarios de una organización. - **Los beneficios son acumulativos**: Oro incluye los beneficios de Clásica; Platino incluye todos los anteriores. ### 4.3 Progresión visual La tarjeta digital muestra: - Barra de progreso hacia el siguiente tier - Porcentaje completado - Puntos restantes para subir de nivel - Insignias desbloqueadas --- ## 5. Flujo de Asignación de Puntos ### 5.1 Flujo completo (`POST /api/points/award`) ``` Cliente API Node.js MySQL │ │ │ │ POST /api/points/award │ │ │ { userId, actionKey, │ │ │ organizationId } │ │ │──────────────────────────────▶│ │ │ │ │ │ │ ¿actionKey = 'signup'? │ │ │ ├─ Sí → CardService │ │ │ │ .getOrCreateCard() │ │ │ │ (crea user_points │ │ │ │ + tarjeta si no existe)│ │ │ │ │ │ │ ¿Requiere card? │ │ │ ├─ Sí → verificar que │ │ │ │ user_points exista │ │ │ │ │ │ │ Validar acción │ │ │ (busca action_key + org, │ │ │ fallback a is_global) │ │ │ │ │ │ Generar idempotency_key │ │ │ SHA256(userId:actionKey: │ │ │ referenceId:date) │ │ │ │ │ │ Verificar cooldown │ │ │ (once/daily/weekly/monthly) │ │ │ │ │ │ BEGIN TRANSACTION │ │ │ SELECT ... FOR UPDATE │ │ │ (lock row en user_points) │ │ │ │ │ │ Validar tope mensual │ │ │ (max_per_period) │ │ │ │ │ │ Actualizar saldo │ │ │ balance += points │ │ │ lifetime += points │ │ │ │ │ │ Insertar transacción │ │ │ (con idempotency_key) │ │ │ │ │ │ Recalcular tier │ │ │ TierService.recalculateTier() │ │ │ │ │ │ COMMIT │ │ │ │ │ { success, transactionId, │ │ │ pointsAwarded, balance*, │ │ │ tier, cardId } │ │ │◀──────────────────────────────│ │ ``` ### 5.2 Paso a paso detallado 1. **Bootstrap en signup**: Si `actionKey === 'signup'`, se llama a `CardService.getOrCreateCard()` que: - Crea el registro en `user_points` si no existe (balance = 0, lifetime = 0) - Genera una tarjeta digital (`user_cards`) con `card_id` UUID, `card_number` de 12 dígitos, `qr_secret` HMAC - Asigna tier inicial (NORMAL) 2. **Validación de acción**: `PointsService._validateAction(actionKey, orgId)`: - Busca en `point_actions` donde `organization_id = orgId` y `action_key = actionKey` - Si no encuentra, busca donde `is_global = TRUE` y `action_key = actionKey` - Retorna la configuración (puntos, cooldown, max_per_period, requires_card, enabled) 3. **Generación de clave de idempotencia**: - `SHA256( userId + ':' + actionKey + ':' + referenceId + ':' + date )` - La constraint `UNIQUE(idempotency_key)` en la BD es la última línea de defensa 4. **Verificación de cooldown**: - `once`: Se busca cualquier transacción previa con ese `action_key` para el usuario - `daily`: Se busca transacción en el mismo día calendario - `weekly`: Se busca en la misma semana (ISO) - `monthly`: Se busca en el mismo mes calendario - `NULL`: Sin restricción 5. **Lock de fila**: `SELECT ... FROM user_points WHERE ... FOR UPDATE` dentro de una transacción para evitar condiciones de carrera. 6. **Validación de tope mensual**: Si `max_per_period` está definido, se cuenta cuántas transacciones de ese `action_key` lleva el usuario en el mes actual. 7. **Actualización de saldo**: `balance` se incrementa, `lifetime` se incrementa. 8. **Registro de transacción**: Se inserta en `point_transactions` como registro inmutable. 9. **Recálculo de tier**: Se evalúa si el nuevo `lifetime` cruza algún umbral de `membership_tiers`. Si es así, se actualiza `tier_id` en `user_points` y `tier_code` en `user_cards`. ### 5.3 Manejo de errores | Error | Código HTTP | Causa | |---|---|---| | Acción no encontrada | 404 | `action_key` + `organization_id` no existe | | Cooldown activo | 429 | El usuario ya realizó esta acción en el período | | Tope mensual alcanzado | 429 | `max_per_period` alcanzado | | Transacción duplicada | 409 | `idempotency_key` ya existe (reintento seguro) | | Sin tarjeta | 400 | `requires_card = true` pero no hay `user_points` | | Acción deshabilitada | 400 | `enabled = false` | --- ## 6. Catálogo de Acciones ### 6.1 Acciones y puntos asignados | `action_key` | Pts | Cooldown | Tope/mes | ¿Requiere tarjeta? | |---|---|---|---|---| | `signup` | 100 | once | — | No (crea tarjeta) | | `verify_email` | 50 | once | — | No | | `complete_profile` | 50 | once | — | No | | `read_summary` | 10 | daily | 50 | No | | `read_playbook` | 50 | once | — | No | | `purchase_playbook` | 200 | once | — | No | | `purchase_membership` | 500 | monthly | 1 | Sí | | `write_review` | 30 | weekly | 4 | Sí | | `daily_visit` | 5 | daily | 1 | Sí | | `social_share` | 15 | daily | 3 | Sí | | `referral_signup` (refierente) | 200 | once | — | Sí | | `referral_signup` (referido) | 100 | once | — | Sí | | `referral_complete` | 100 | once | — | Sí | | `survey` | 100 | monthly | 1 | Sí | | `ticket_purchase` | 150 | NULL | — | Sí | | `event_checkin` | 50 | NULL | — | Sí | | `event_complete` | 100 | NULL | — | Sí | ### 6.2 Políticas de cooldown | Tipo | Ventana | Reset | |---|---|---| | `once` | Toda la vida del usuario | Nunca | | `daily` | 1 día | 00:00 hora local | | `weekly` | 7 días (lun-dom) | Domingo 00:00 | | `monthly` | 1 mes calendario | Día 1 00:00 | | `NULL` | Sin límite | N/A | --- ## 7. Sistema de Recompensas ### 7.1 Flujo de canje (`POST /api/points/rewards/redeem`) ``` 1. Validar que la recompensa exista y esté enabled = true 2. Validar ventana de validez (valid_from / valid_until) 3. Validar stock (si stock_remaining != NULL y > 0) 4. Obtener user_points del usuario 5. Validar tier mínimo (min_tier_code): - Se compara sort_order del tier del usuario vs. el requerido 6. Validar balance >= cost 7. BEGIN TRANSACTION a. user_points.balance -= cost (lifetime NO cambia) b. Decrementar stock_remaining si aplica c. Insertar transacción con action_key = 'spend', points = -cost 8. COMMIT ``` ### 7.2 Catálogo de recompensas | `reward_key` | Costo | Tier Mínimo | Categoría | Stock | |---|---|---|---|---| | `playbook_discount_10` | 100 | NORMAL | discount | ∞ | | `playbook_discount_25` | 250 | CLASICA | discount | ∞ | | `playbook_free` | 2,000 | ORO | playbook | ∞ | | `premium_month` | 500 | NORMAL | membership | ∞ | | `premium_year` | 5,000 | ORO | membership | ∞ | | `event_priority` | 1,500 | PLATINO | event | 50 | | `swag_pack` | 800 | CLASICA | merch | 100 | ### 7.3 Reglas de redención - Solo `balance` se debita; `lifetime` permanece intacto (el tier no baja) - Se usa `idempotency_key` para prevenir doble canje - El stock se decrementa atómicamente dentro de la transacción - Las recompensas pueden tener fecha de expiración (`valid_until`) --- ## 8. Sistema de Referidos ### 8.1 Flujo completo ``` Referente API Nuevo Usuario │ │ │ │ POST /referrals/code │ │ │──────────────────────────────▶│ │ │◀─────── { code: "A3B9X2K1" } │ │ │ │ │ │ │ POST /referrals/register│ │ │◀──────────────────────────────│ │ │ { code: "A3B9X2K1", userId } │ │ │ │ │ │ Valida: │ │ │ · código existe │ │ │ · no autoreferencia │ │ │ · no duplicado │ │ │ · < 10 referidos/mes │ │ │ │ │ │ Updates status → completed │ │ │ │ │ │ Award points: │ │ │ · referente: +200 pts │ │ │ · referido: +100 pts │ │ │ │ │ │ POST /referrals/reward-complete│ │ │◀──────────────────────────────│ │ │ { code, referredUserId } │ │ │ │ │ │ Award: │ │ │ · referente: +100 pts │ ``` ### 8.2 Reglas del sistema de referidos | Regla | Valor | |---|---| | Formato del código | 8 caracteres alfanuméricos (A-Z, 2-9; excluye 0/1/O/I) | | Máximo de referidos/mes | 10 por referente | | Autoreferencia | Bloqueada | | Duplicados | Bloqueados por UNIQUE KEY en `referred_id` | | Puntos al referente (registro) | 200 (`referral_signup`) | | Puntos al referido (registro) | 100 (`referral_signup`) | | Puntos al referente (completo) | 100 (`referral_complete`) | --- ## 9. Tarjetas Digitales y QR ### 9.1 Estructura de la tarjeta ``` ┌─────────────────────────────────┐ │ [Logo Marca] │ │ │ │ 🥇 PLATINO │ │ │ │ Juan Pérez │ │ ═══ 1234 5678 9012 ═══ │ │ │ │ ▓▓▓▓▓▓▓▓▓░░░░░░░ 65% │ │ Próximo nivel: 2,000 pts │ │ │ │ [●] Tarjeta activa │ │ │ └─────────────────────────────────┘ ``` ### 9.2 QR y perfil público - `qr_secret = HMAC-SHA256(cardId, POINTS_QR_SECRET)` - El QR escanea a: `GET /api/points/profile/:wikiname` - El endpoint de perfil público tiene **rate limiting** (RateLimiter.js) - La tarjeta puede marcarse como `is_public = true/false` ### 9.3 Validación de QR `POST /api/points/card/validate-qr`: 1. Recibe `{ cardId, qrData }` 2. Recalcula HMAC y compara 3. Retorna datos públicos del usuario + tier 4. Rate limited (público, sin auth) --- ## 10. Organizaciones Afiliadas ### 10.1 Organizaciones registradas | Código | Nombre | ID | |---|---|---| | `MPB` | Mi Playbook | 1 | | `TRIBU` | Tribu | 2 | | `CLUB` | Club | 3 | ### 10.2 Membresías afiliadas La tabla `affiliated_memberships` vincula un usuario a una organización con: | Columna | Descripción | |---|---| | `membership_tier` | `BASICA`, `ORO`, `PLATINUM` | | `subsidy_type` | `full`, `partial`, `none` | | `status` | `active`, `inactive`, `pending` | | `valid_from` / `valid_until` | Vigencia | **Unique:** `(user_id, organization_id)` ### 10.3 APIs de afiliación | Método | Ruta | Propósito | |---|---|---| | POST | `/api/points/orgs` | Crear organización | | GET | `/api/points/orgs` | Listar organizaciones | | GET | `/api/points/orgs/:code` | Obtener organización | | PUT | `/api/points/orgs/:code` | Actualizar organización | | POST | `/api/points/orgs/:code/members` | Agregar miembro | | GET | `/api/points/orgs/:code/members` | Listar miembros | | DELETE | `/api/points/orgs/:code/members/:userId` | Remover miembro | --- ## 11. Tareas Programadas ### 11.1 Scheduler Sistema basado en la tabla `scheduler`: ```sql id, name, type (once|daily|weekly|monthly|yearly), time, date, dayOfWeek, dayOfMonth, model, target, isActive, lastRun ``` ### 11.2 Tarea `creditos-pb` (reseteo mensual de créditos) Ejecutada por `CreditsService.resetCredits()`: 1. **Calcular mes anterior** (ej: si es junio, expira mayo) 2. **Expirar créditos no usados**: - Busca `pb_credit_balance_monthly` con `credits_available > 0` - Mueve `credits_available` → `credits_expired` - Registra evento `EXPIRE` en `pb_credit_ledger` 3. **Resetear usuarios válidos**: - Obtiene usuarios con `privilege = 'membresia-pb'` - Resuelve membresía y estado de créditos ### 11.3 Inicialización En `index.js:148-151`, el sistema carga todas las tareas activas de la BD y las programa con `node-schedule`. --- ## 12. Seguridad y Anti-Fraude ### 12.1 Mecanismos de protección | Mecanismo | Dónde | Descripción | |---|---|---| | **Idempotency Key** | `point_transactions.idempotency_key` (UNIQUE) | Garantiza que cada transacción ocurra una sola vez, incluso con retry del cliente | | **Row-level locks** | `SELECT ... FOR UPDATE` dentro de transacciones | Serializa operaciones concurrentes sobre el mismo `user_points` | | **Cooldowns** | A nivel aplicación + BD | Previene farming de puntos | | **Topes mensuales** | `point_actions.max_per_period` | Limita abuso de acciones repetitivas | | **Validación dual** | App + constraints UNIQUE | Doble capa de seguridad | | **Rate limiting** | `RateLimiter.js` (in-memory) | Endpoints públicos/profile tienen límite de requests | | **QR secret** | HMAC-SHA256 con secret del servidor | QR no puede ser falsificado | | **No autoreferencia** | `ReferralService` | Previene autoreferidos | | **Stock tracking** | `point_rewards.stock_remaining` | Control de inventario en canjes | ### 12.2 Flujo de reintento seguro El cliente puede reintentar `POST /api/points/award` de forma segura porque: ``` Request 1: awardPoints(userId, "signup", ...) → INSERT falla (timeout) Request 2 (retry): awardPoints(userId, "signup", ...) → Genera el mismo idempotency_key → INSERT encuentra el UNIQUE KEY (si ya se insertó) → Retorna error 409 → Si no se insertó aún, procede normalmente ``` --- ## 13. APIs ### 13.1 Endpoints de Puntos | Método | Ruta | Controlador | Propósito | |---|---|---|---| | POST | `/api/points/award` | `PointsController.awardPoints` | Otorgar puntos | | GET | `/api/points/balance` | `PointsController.getBalance` | Saldo del usuario | | GET | `/api/points/history` | `PointsController.getHistory` | Historial paginado | | GET | `/api/points/leaderboard` | `PointsController.getLeaderboard` | Top usuarios | ### 13.2 Endpoints de Tarjetas | Método | Ruta | Propósito | |---|---|---| | GET | `/api/points/card` | Obtener o crear tarjeta | | PUT | `/api/points/card` | Actualizar configuración | | GET | `/api/points/card/public/:cardId` | Vista pública (rate limited) | | POST | `/api/points/card/validate-qr` | Validar QR (rate limited) | | GET | `/api/points/profile/:wikiname` | Perfil público desde QR (rate limited) | ### 13.3 Endpoints de Tiers | Método | Ruta | Propósito | |---|---|---| | GET | `/api/points/tiers` | Listar tiers disponibles | | GET | `/api/points/tiers/progress` | Progreso del usuario | ### 13.4 Endpoints de Recompensas | Método | Ruta | Propósito | |---|---|---| | GET | `/api/points/rewards` | Catálogo de recompensas | | GET | `/api/points/rewards/:key` | Detalle de recompensa | | POST | `/api/points/rewards/redeem` | Canjear recompensa | | GET | `/api/points/rewards/redemptions` | Historial de canjes | ### 13.5 Endpoints de Referidos | Método | Ruta | Propósito | |---|---|---| | POST | `/api/points/referrals/code` | Obtener/crear código | | POST | `/api/points/referrals/register` | Registrar referido | | GET | `/api/points/referrals/stats` | Estadísticas de referidos | | POST | `/api/points/referrals/reward-complete` | Recompensa por onboarding completo | ### 13.6 Endpoints de Membresías (Legacy) | Método | Ruta | Propósito | |---|---|---| | GET | `/api/membership/credits/status` | Estado de créditos del usuario | | POST | `/api/membership/credits/unlock` | Desbloquear resumen con crédito | | GET | `/api/membership/credits/ledger` | Libro contable de créditos | --- ## 14. Integración con KatIA ### 14.1 Detección de intención El archivo `llm/sherpa/measureGenerator.js:137-139` reconoce patrones de membresía: ```regex \b(membresia|membresía|suscripcion|suscripción|precio|costo| cuánto cuesta|tier|cada plan|los planes|qué incluye|que incluye| qué plan|que plan|cuanto vale|cuánto vale|plan anual|hay plan)\b ``` ### 14.2 Contexto para el AI En `controllers/ai-sherpa/build.context.js:291-353`: 1. Se obtiene `membership_tier` del usuario desde `validateUserMembership()` 2. Se obtienen todos los planes desde `PbMembershipService.getAllMemberships()` 3. Se inyectan ambos al contexto de KatIA ### 14.3 Membership Advisor `llm/sherpa/membershipAdvisor.js`: - Usa `membership_tier` del contexto - Lee `membresias_activas` flag del canal - Explica los 3 tiers: Gratis, Básica ($10/mes), Básica Pro ($19.99/mes) --- ## Apéndice A: Estructura de directorios ``` services/points/ ├── PointsService.js # Core: award, spend, balance, history, leaderboard (575 líneas) ├── TierService.js # Tier calculation, progress, batch recalc (334 líneas) ├── RewardsService.js # Reward catalog, redeem logic (412 líneas) ├── CardService.js # Card CRUD, QR generation/validation (595 líneas) ├── ReferralService.js # Referral codes, registration, rewards (404 líneas) ├── AffiliationService.js # Org/member management (222 líneas) └── RateLimiter.js # In-memory rate limiter (59 líneas) controllers/points/ ├── PointsController.js ├── RewardsController.js ├── CardController.js ├── ReferralsController.js └── AffiliationController.js services/membership/ ├── MembershipService.js # Plan resolution, mapping, eligibility (169 líneas) └── PbMembershipService.js # CRUD pb_membresia (278 líneas) sql/points/ ├── points_v2_tables.sql # Core schema (8 tablas) ├── points_v2_rewards.sql # Recompensas + seed data ├── points_v2_affiliations.sql # Organizaciones afiliadas ├── points_v2_hardening.sql # Unique key migrations ├── points_v2_is_global.sql # Flag is_global ├── points_v2_rename_memberships.sql # Renombrar tabla └── points_v2_signup_bootstrap.sql # requires_card + bootstrap tests/points/ └── points-v2.http # Suite de tests HTTP (433 líneas) ``` --- ## Apéndice B: Diagrama de relaciones entre tablas ``` affiliated_organizations │ ├──< point_actions (organization_id) ├──< user_points (organization_id) ├──< point_transactions (organization_id) ├──< membership_tiers (organization_id) ├──< user_cards (organization_id) ├──< referrals (organization_id) ├──< point_rewards (organization_id) ├──< affiliated_memberships (organization_id) └──< card_sequences (organization_id) puesto (external) │ ├──< user_points (user_id) ├──< point_transactions (user_id) ├──< user_cards (user_id) ├──< referrals (referrer_id) ├──< referrals (referred_id) └──< affiliated_memberships (user_id) membership_tiers │ ├──< user_points (tier_id) └──< user_cards (tier_id) ``` --- ## Apéndice C: Transacciones y consistencia Todas las operaciones críticas (award, redeem, referral) usan **transacciones MySQL** con: ```sql START TRANSACTION; SELECT ... FROM user_points WHERE ... FOR UPDATE; -- (operaciones de negocio) UPDATE user_points SET balance = ..., lifetime = ... WHERE ...; INSERT INTO point_transactions (...) VALUES (...); UPDATE user_cards SET tier_code = ... WHERE ...; COMMIT; ``` Esto garantiza: - **Atomicidad**: Todo ocurre o nada ocurre - **Aislamiento**: `FOR UPDATE` evita condiciones de carrera - **Consistencia**: El balance siempre refleja la suma de transacciones --- ## Apéndice D: Variables de entorno requeridas | Variable | Propósito | |---|---| | `POINTS_QR_SECRET` | Secreto HMAC para QR codes | | `DB_HOST`, `DB_USER`, `DB_PASS`, `DB_NAME` | Conexión MySQL | | `SERVER_MAIN` | URL base de la API PHP para validación de membresía |