Convoca Members
Guía para administradores: gestión de socios, membresías, cuotas, voluntariado y ciclo de vida completo del socio. Documentación fiel al código de Convoca Members 2.8.23.
Manual para la junta directiva (sin conocimientos técnicos)¶
Esta página es la referencia técnica del plugin. Para el día a día hay un manual en tres partes, en castellano y con capturas:
1. Introducción¶
Convoca Members gestiona el ciclo de vida completo de los socios de la asociación: altas, renovaciones, periodos de gracia, bajas automáticas, cuotas, estados de membresía, horas de voluntariado, carnets, certificados y documentación. Ideal para asociaciones, ONGs y entidades sin ánimo de lucro.
Requisitos: convoca-core activo, WordPress 6.4+, PHP 8.1+. Si no está activo convoca-core, el plugin muestra un aviso en el administrador y no se carga.
2. Estructura de datos¶
Convoca Members registra estos Custom Post Types (todos privados, sin show_in_rest) y una taxonomía:
| Tipo | Slug | Uso |
|---|---|---|
CPT miembro |
miembro |
Ficha del socio (todos los datos en meta _convoca_*) |
CPT convoca_documento |
convoca_documento |
Documentos generados (p. ej. acuerdo de incorporación); sin interfaz propia |
CPT registro_hora |
registro_hora |
Registros de horas de voluntariado |
CPT proyecto |
proyecto |
Proyectos de voluntariado (desplegable en el registro de horas) |
Taxonomía tipo_miembro |
tipo_miembro |
Clasificación adicional (heredada del libro de socios) |
Los datos personales de miembro no se exponen por el REST nativo de WordPress (show_in_rest => false); solo se leen a través de las rutas propias autenticadas (ver §17).
3. Configuración inicial¶
Accede a Miembros → Ajustes (slug conv-members-settings; requiere manage_options). La pantalla tiene estas pestañas:
| Pestaña | Contenido |
|---|---|
| General | Email administrador, remitente, IBAN, proveedor de email, RGPD, edad mínima, ciclo de vida de la membresía y tema de documentos |
| Planes de Socio | Editar/añadir planes (precio, horas, modalidad, ventajas) |
| Plantillas de Email | Editar asunto y cuerpo de las plantillas |
| Campos Voluntariado | Texto legal y campos personalizados del formulario de voluntariado |
| Gamificación | Badges por modalidad (solo licencia PRO) |
| Estado | Diagnóstico del sistema (plugins, páginas con shortcode, versión de BD) |
Texto legal del voluntariado (la Declaración Responsable)¶
El ajuste convoca_volunteer_legal_text (pestaña Campos Voluntariado) guarda la
Declaración Responsable: lo que la persona declara y acepta al darse de alta como voluntaria. Es
un solo texto y aparece en los dos sitios a la vez, así que no hay que mantener dos copias:
- el formulario público de voluntariado, en el recuadro gris que va justo encima de la casilla «He leído y acepto la Declaración Responsable» (obligatoria);
- el acuerdo de incorporación en PDF, donde se imprime bajo el epígrafe «Declaración Responsable», antes del sello de aceptación digital.
Admite HTML sencillo (p, strong, ul). Dos cosas que hay que respetar al editarlo:
- Sin emojis: el PDF se dibuja con una tipografía que no tiene sus glifos y saldrían como «?».
- Corto: el acuerdo tiene que caber en una página A4. El texto de 2026 cabe (verificado en de ejemplo y en la demo: 1 página, 595,28 × 841,89 pt); si se alarga, se irá a dos.
El texto vigente (fuente: Programa de Voluntariado 2026-2030 v2, Manual del Voluntariado 2026,
Reglamento Interno 2026 y normativa de referencia):
<p>Declaro que los datos que facilito son ciertos y que reúno los requisitos para ser voluntario/a de [Nombre de la asociación]: actúo de forma voluntaria, altruista y sin contraprestación económica (Ley 45/2015, de 14 de octubre, del Voluntariado) y no figuro en el Registro Central de Delincuentes Sexuales ni tengo antecedentes penales no cancelados. Si soy menor de edad, cuento con la autorización de mis representantes legales.</p>
<p>He leído y acepto el Programa de Voluntariado 2026-2030, el Manual del Voluntariado, el Código Ético y de Conducta y el Reglamento Interno, y me comprometo a cumplir las tareas que se me asignen, a asistir a las reuniones de planificación y a participar en la formación necesaria. Mantendré la confidencialidad de la información de las personas participantes, también después de finalizar mi colaboración, y sé que la asociación dispone de un seguro de accidentes y de responsabilidad civil para la acción voluntaria.</p>
<p>Mis datos se tratan para gestionar el voluntariado (alta, seguro, formación, acreditación y certificación). Responsable: [Nombre de la asociación] ([CIF]), Registro de Asociaciones de [territorio] n.º [número] y Registro de Entidades de Voluntariado n.º [número]. Base: mi consentimiento y la ejecución del acuerdo. No se ceden a terceros salvo obligación legal. Puedo ejercer mis derechos de acceso, rectificación, supresión y los demás escribiendo a [correo de protección de datos]; el detalle completo está en la Política de Privacidad. Puedo revocar esta declaración y darme de baja cuando quiera, sin tener que justificar el motivo.</p>
La baja es libre: la propia declaración dice que se puede revocar sin justificar el motivo, en línea con el artículo 9 de la Ley 45/2015.
Ajustes generales¶
| Ajuste | Clave | Descripción |
|---|---|---|
| Email administrador | convoca_members_settings[admin_email] |
Recibe notificaciones de nuevas altas |
| Nombre remitente | convoca_members_settings[sender_name] |
Nombre en el From: de los emails |
| IBAN | convoca_members_settings[iban] |
Para transferencias |
| Proveedor de email | convoca_members_settings[email_provider] |
wpmail (defecto) o mailgun (API key, dominio y región US/EU) |
| Versión texto legal RGPD | convoca_members_settings[rgpd_version] |
Por defecto 1.0 |
| Edad mínima | convoca_members_settings[min_age] |
0 desactiva la restricción |

4. Planes de membresía¶
Los planes se definen en la opción convoca_members_plans. Si está vacía, se usan los valores por defecto definidos en CPT_Miembro::DEFAULT_PLANS y se pueden personalizar en Ajustes → Planes de Socio.
Planes por defecto (precio € / horas anuales de voluntariado)¶
| Modalidad | Plan | Precio | Horas | Clave |
|---|---|---|---|---|
| Numerario | 🥉 Bronce | 30 € | 15 h | bronze |
| Numerario | 🥈 Plata | 50 € | 25 h | silver |
| Numerario | 🥇 Oro | 100 € | 50 h | gold |
| Familiar | Familiar Bronce | 45 € | 22,5 h | fam-bronze |
| Familiar | Familiar Plata | 75 € | 37,5 h | fam-silver |
| Familiar | Familiar Oro | 150 € | 75 h | fam-gold |
| Juvenil | Juvenil Bronce | 15 € | 7,5 h | juv-bronze |
| Juvenil | Juvenil Plata | 25 € | 12,5 h | juv-silver |
| Juvenil | Juvenil Oro | 50 € | 25 h | juv-gold |
El objetivo de horas es configurable por plan desde Ajustes → Planes de Socio (campo «Horas Voluntariado»). Los valores de la tabla son los defecto: bronze = 15 h, silver = 25 h, gold = 50 h.
Selectores de modalidad¶
| Clave | Modalidad | Uso |
|---|---|---|
familiar |
Virtual | Selector "Familiar" (precio 0): el formulario pide además un sub_plan (fam-bronze, fam-silver o fam-gold) |
juvenil |
Virtual | Selector "Juvenil" (precio 0): el formulario pide además un sub_plan (juv-*) |
Cuando el socio elige el selector familiar o juvenil, la lógica resuelve el plan real desde sub_plan. Los planes aceptan además payment_methods (bizum, tarjeta, transferencia), advantages (una por línea), active, order, emoji y description. Se pueden filtrar por código con convoca_members_plans.
Validaciones en el alta: la modalidad Juvenil exige tener menos de 30 años; un plan desactivado (active = false) se rechaza.
5. Alta de socio¶
Desde el formulario público¶
Coloca el shortcode en una página pública:
El formulario (multi-paso, plantilla templates/form-alta.php) recoge: nombre, DNI/NIE (validado con suma de control), email, teléfono, fecha de nacimiento, dirección, municipio, canal de contacto, plan/modalidad, forma de pago, consentimiento RGPD y comunicaciones, y (opcional) pago recurrente. Si el socio es menor de edad se piden los datos del tutor.
El envío se hace por REST POST /convoca-members/v1/alta (con nonce convoca_alta_nonce) o por el AJAX heredado. Está limitado a 3 registros por hora por IP.
El estado inicial depende de la forma de pago:
| Forma de pago | Estado inicial |
|---|---|
cuota / tarjeta / bizum / transferencia |
pendiente_pago |
voluntariado |
pendiente_documentacion |
Si el plan tiene importe y el gateway está activo, el alta redirige al TPV. Si hay duplicado de DNI o email, el sistema lo detecta atómicamente (ver §7, re-alta).
Desde el panel de administración¶
Miembros → Añadir nuevo (o Añadir voluntario) abre el editor de socio. La aprobación de un socio (botón Aprobar en el listado o vía pago completado) ejecuta CPT_Miembro::approve_member, que:
- Asigna el número de socio secuencial (tabla
convoca_member_sequence). - Establece
fecha_alta(solo la primera vez) yfecha_renovacion(+1 año si no existía). - Pasa el estado a
activoy crea el usuario de WordPress vinculado, enviando credenciales por email (Process_Member::handle_approved).
6. Estados de membresía¶
Cada socio tiene un estado guardado en _convoca_estado_miembro. La máquina de estados (Estados) valida cada transición y registra el historial en _convoca_historial y en el log de auditoría.
| Estado | Significado |
|---|---|
Pendiente documentación (pendiente_documentacion) |
Alta de voluntariado (o recién registrado) a la espera de cumplir requisitos |
Pendiente pago (pendiente_pago) |
Alta con cuota sin pagar |
Activo (activo) |
Membresía vigente |
Suspendido (suspendido) |
Periodo de gracia: pierde beneficios, solo puede renovar |
Baja solicitada (baja_solicitada) |
El socio pidió la baja desde su panel; pendiente de confirmar |
Baja (baja) |
Membresía finalizada (conservada para histórico) |
Transiciones permitidas¶
| Desde | Puede pasar a |
|---|---|
pendiente_documentacion |
pendiente_pago, activo, baja |
pendiente_pago |
activo, suspendido, baja |
activo |
suspendido, baja_solicitada, baja, pendiente_documentacion |
suspendido |
activo, baja_solicitada, baja |
baja_solicitada |
baja, activo |
baja |
pendiente_documentacion, pendiente_pago, activo (re-alta/reactivación) |
Cambiar estado manualmente¶
En la ficha del socio (detalle ?member_id=X) el desplegable Cambiar estado solo ofrece los destinos permitidos según el estado actual; el cambio se aplica por AJAX con su nota de auditoría. En el listado también hay acciones rápidas de aprobar y eliminar (papelera).
7. Ciclo de vida de la membresía¶
El plugin aplica automáticamente un ciclo de vida configurable tras la fecha de renovación (_convoca_fecha_renovacion). Los parámetros se ajustan en Ajustes → General → Ciclo de vida de la membresía:
| Ajuste | Clave | Rango | Defecto | Efecto |
|---|---|---|---|---|
| Días hasta perder beneficios | grace_suspend_days |
0–30 | 1 | Días tras el vencimiento antes de pasar a suspendido |
| Días hasta la baja automática | grace_baja_days |
1–90 | 30 | Días tras el vencimiento antes de pasar a baja |
La evaluación se hace en CPT_Miembro::check_member_status, invocada por el cron diario (Cron_Manager::process_expirations) para todo socio activo con renovación vencida:
- Suspensión (gracia corta): al cumplirse
grace_suspend_daysdesde el vencimiento (con defecto 1, el día siguiente al vencimiento), el socio pasa asuspendido,_convoca_estado_cuotase marcavencida. Pierde los beneficios pero conserva el acceso al panel solo para renovar. - Baja automática (gracia larga): al cumplirse
grace_baja_days, antes de cambiar el estado se cierran todas sus sesiones (Member_Auth::logout_member_sessions), pasa abaja, se guardafecha_bajay se dispara el hookconvoca_members_miembro_dado_de_baja.
Nota: el orden importa. La baja se evalúa primero (es la condición más severa); la suspensión solo se aplica si aún no ha llegado la fecha de baja.
Renovación tardía (siempre desde el vencimiento)¶
Cuando un socio en activo, suspendido o baja_solicitada paga la cuota, Payment_Listener suma siempre 1 año a la fecha de vencimiento anterior (_convoca_fecha_renovacion + 1 año), nunca desde "hoy". Así una renovación tardía no pierde el periodo ya pagado. La primera activación (sin fecha previa) o la re-entrada tras baja sí arrancan un ciclo nuevo desde hoy.
Re-alta tras baja (sin duplicar)¶
Si un socio dado de baja vuelve a rellenar el formulario con el mismo DNI o email, Process_Member::register detecta la ficha existente en estado baja y reactiva la misma ficha: conserva el número de socio, la antigüedad (fecha_alta histórica) y el historial; actualiza plan, forma de pago y datos de contacto. No se crea un registro duplicado.
Ciclo anual de voluntarios (por horas)¶
Los socios con forma_pago = voluntariado no se rigen por fechas de cuota. CPT_Miembro::check_volunteer_cycle evalúa, al vencer su renovación, si completaron las horas anuales del plan:
- Si las horas aprobadas del ciclo ≥ objetivo → renueva
fecha_renovacion+ 1 año y sigueactivo. - Si no cumplen → vuelve a
pendiente_documentacion(solo voluntario, sin beneficios de socio) hasta completar de nuevo las horas.
8. Renovaciones¶
Renovación desde el panel del socio¶
- Botón "Renovar membresía" (pestaña Pagos y Cuotas) y el shortcode
[convoca_renovar](página/renovar/) llaman aPOST /me/renovar, que crea un pago por un año y devuelvepayment_urlpara redirigir al TPV. - La renovación está permitida para socios
activoysuspendido(check_renewable_member). Un sociobajano puede renovar desde el panel: debe re-darse de alta (re-alta, §7).
Renovación automática (recurrente)¶
Los socios con _convoca_pago_recurrente = 1 se renuevan automáticamente por cron (process_automatic_renewals): se genera el cargo con el gateway (cargo real por token vía Redsys REST) y se envía renovacion_automatica.
Política de reintentos y gracia (D1/D2): si el cargo falla, el socio conserva los beneficios mientras haya reintentos activos: se reintenta cada ciclo diario hasta auto_renew_max_attempts (opción de Ajustes → Ciclo de vida, 3 por defecto) dentro de una ventana de auto_renew_retry_days (5 días). Cada intento fallido incrementa _convoca_autorenew_attempts y registra _convoca_autorenew_last_attempt; mientras haya reintentos pendientes, check_member_status no suspende ni da de baja al socio. Agotados los intentos (o sin token guardado), pasa a pendiente_pago y recibe un enlace de pago manual con solicitud de tokenización para futuras renovaciones.
Renovación por horas de voluntariado (recordatorios)¶
Los recordatorios escalados de renovación (30, 15 y 7 días antes del vencimiento; plantillas renovacion, renovacion_15d y renovacion_7d) se envían a todos los socios activos con fecha de renovación, incluidos los que renuevan por horas de voluntariado (forma_pago = voluntariado). Para estos, las plantillas incluyen además:
| Variable | Valor |
|---|---|
{horas_actuales} |
Horas aprobadas acumuladas |
{horas_objetivo} |
Objetivo de horas del plan |
{porcentaje_cumplimiento} |
Porcentaje de cumplimiento |
El objetivo de horas es configurable por plan (ver §4).
9. Bajas¶
Las bajas son lógicas: el registro se conserva para histórico. Pueden producirse por tres vías:
- Automática por impago al agotarse
grace_baja_days(§7), con cierre de sesión. - Solicitada por el socio desde su panel (
POST /me/unsubscribe→ estadobaja_solicitada); un administrador confirma después elbaja. - Manual desde la ficha del socio (transición a
baja, estableciendofecha_baja).
⚠️ No elimines un socio salvo obligación legal: el sistema conserva todo el histórico. La eliminación (papelera) es una acción aparte disponible en el listado.
10. Pagos y cuotas¶
Convoca Members se integra con Convoca Gateway. Payment_Listener escucha convoca_gateway_payment_completed / convoca_gateway_payment_failed y gestiona el estado del socio:
- Pago completado: actualiza
estado_cuota,metodo_pago, calcula la nuevafecha_renovacion(§7), reactiva/activa el socio y envía renovación completada (con el carnet adjunto). - Pago fallido: marca
estado_cuotacomovencida(opendienteen el primer intento) y reenvía un enlace de pago.
El historial de pagos del socio se lee desde el CPT pago del gateway (origen members), visible en la pestaña Pagos y Cuotas del panel y en GET /me/pagos.
11. Panel del socio (área personal)¶
El shortcode [convoca_mi_area] (página Mi Cuenta) muestra el panel del socio autenticado. La autenticación usa las credenciales de WordPress (usuario o email + contraseña) recibidas por email al aprobarse el alta, y una sesión propia en la cookie convoca_member_session (6 horas).
| Pestaña | Funcionalidad |
|---|---|
| Mis Datos | Ver y editar dirección, teléfono, email y cumpleaños; solicitar la baja |
| Carnet Digital | Carnet de socio (HTML imprimible) |
| Inscripciones | Actividades activas y pasadas (vía convoca-enroll) |
| Voluntariado | Registrar y consultar horas |
| Pagos y Cuotas | Historial de pagos + botón Renovar membresía |
| Buscar | Búsqueda de actividades (y socios si hay sesión) |
| Notificaciones | Avisos internos con contador de no leídos |
Un socio suspendido sigue accediendo al panel pero solo en lectura y para renovar (no puede editar perfil ni registrar horas nuevas).
Reglas de edición de datos (perfil)¶
| Dato | Regla |
|---|---|
| Dirección | Editable libremente |
| Teléfono | Editable; verificable con enlace enviado al email |
| Doble opt-in: el cambio queda pendiente hasta confirmar el enlace (caduca a las 24 h) | |
| Cumpleaños | Solo se puede establecer una vez; después no se modifica (se rechaza con error) |
Verificación de teléfono y email¶
- Teléfono: botón Verificar teléfono →
POST /me/verify-phonegenera un enlace de 24 h enviado al email del socio; al pulsarlo (?convoca_confirm_phone=1&member=X&token=Y) se marcatelefono_verificado = 1. - Email: al cambiar el email,
POST /me/profileguardaemail_pendiente+ token y envía el enlace de confirmación;POST /me/confirm-email(o el enlace?convoca_confirm_email=1…) aplica el cambio.
12. Carnet digital¶
El carnet se sirve como HTML impreso (PDF_Card::get_html) con botón "IMPRIMIR / GUARDAR PDF", y como PDF (PDF_Card::generate_pdf) vía Convoca\Core\Signature (Dompdf).
| Característica | Detalle |
|---|---|
| Tema | Claro u oscuro según la opción compartida del core convoca_document_theme (Ajustes → General) |
| QR | Generado localmente con chillerlan/php-qrcode (sin API externa), apunta a la verificación pública del socio |
| Paleta de marca | Púrpura #320028 + naranja #ff8700 |
| Contenido | Nº de socio (4 dígitos), nombre, plan (badge), insignia de antigüedad, fecha de alta y dominio |
La insignia de antigüedad (D3) muestra los años continuados desde la fecha de alta (_convoca_fecha_alta, conservada en las re-activaciones): aparece «N año(s)» junto al plan a partir de 1 año de pertenencia y queda oculta antes de cumplirse el primer año. Es un reconocimiento visual; no conlleva descuentos automáticos.
El carnet también se adjunta como PDF al email de renovación completada.
El PDF es la tarjeta, no un folio con la tarjeta dentro. La página mide lo que la tarjeta
(450×280 px = 119×74 mm) y no lleva el botón de imprimir: Dompdf ignora @media print, así que lo
marcado como no-print —el botón y su texto de ayuda— acababa dentro del PDF. El HTML que ve
el navegador sí lo conserva.
Tres cosas del motor de PDF que costaron una página de más y quedan resueltas en el modo PDF
(get_html($id, $theme, true)): Dompdf no respeta box-sizing: border-box (suma el padding y
el borde a las medidas, así que a la tarjeta se le iban 60 px de más y no cabía), no sabe hacer
flexbox (las tres zonas se apilaban y el QR se salía) y cuenta los adornos decorativos que
sobresalen del folio. En PDF se dan las medidas del contenido y las zonas se colocan con flotantes
y clear. Además, un emoji en la etiqueta del plan no tiene glifo en la tipografía del PDF y
salía como «?»: en el PDF se quita el emoji y se conserva el texto.
La generación usa get_temp_dir() del core y no wp_tempnam() (que vive en wp-admin y no
está cargado fuera del escritorio: un correo de cron que adjuntase la tarjeta rompía).
Y no lleva overflow: hidden en la cabecera ni en el pie: Dompdf calcula esos contenedores con
altura cero cuando tienen flotantes dentro y recorta su contenido, así que el carnet salía
impreso con dos líneas y sin logo, insignias, fecha ni QR (aunque el texto siguiera en el fichero).
El nombre ajusta su tamaño por longitud (20 / 16 / 13 px) para que un nombre largo no se cruce
con el pie.
La modalidad del socio en el carnet¶
El distintivo naranja del carnet muestra la modalidad (el plan del socio). Se resuelve con
CPT_Miembro::get_plan(), que:
- busca la clave exacta (lo normal:
basico,familiar,bronze…); - si no existe, busca por etiqueta del plan y por clave normalizada — sin adornos (los planes llevan emoji delante: la etiqueta de la demo es literalmente «🥉 Bronce»), sin acentos, sin distinguir mayúsculas y sin espacios de sobra.
Ese segundo paso hace falta porque hay socios guardados con la etiqueta en vez de la clave (los
sembrados de la demo tienen «Bronce» donde debería ir bronze). Sin él, el plan no se resolvía y los
tres documentos caían al texto genérico: el carnet salía con «SOCIO/A».
Verificado en vivo: en la demo, tres socios reales pasaron de «SOCIO/A» a BRONCE / PLATA / ORO,
y en la asociación de ejemplo las modalidades basico y familiar resuelven por clave, por etiqueta, con adorno y en
cualquier caja. Límite conocido: solo se compara con la clave o la etiqueta completas; un
valor intermedio guardado en el socio (p. ej. «Familiar Plus» cuando el plan se llame «Modalidad Familiar
Juvenil Plus») no se resuelve. En la instalación de ejemplo no hay ningún plan con ese nombre.
Si la etiqueta es larga (más de 15 caracteres) el distintivo no cabe en la línea del logo y, en el PDF, ocupaba la línea entera metiéndose encima del nombre de la organización. Montar la cabecera como tabla o con posiciones absolutas tampoco servía (las insignias caían en la línea de los datos, o el logo se partía en dos líneas). En el PDF, una modalidad larga ocupa su propia línea y el cuerpo y el pie bajan; en el navegador no hace falta porque lo reparte el flexbox. Las etiquetas cortas (las de uso habitual) se quedan exactamente como estaban.
Base de estilos común¶
Los tres documentos (carnet, acuerdo y certificado) comparten Estilos_PDF: la tipografía y
los colores de la marca en un solo sitio (constantes de PHP, porque Dompdf no soporta variables
CSS) y los resets neutros que tienen en común. La base se antepone a la hoja de cada documento,
así que cada uno manda en lo suyo y añadirla no cambia lo que ya se veía — comprobado antes y
después con los tres documentos generados: mismas páginas, mismo tamaño de folio y el mismo texto.
Y con la base van escritas las trampas de Dompdf, medidas una a una y documentadas al lado de las reglas. Son las que cuestan una tarde cada vez que se olvidan:
| Regla | Qué pasa en Dompdf |
|---|---|
display: flex |
No existe: los hijos se apilan. Se coloca con float y clear. |
overflow: hidden con flotantes dentro |
Recorta el contenido (calcula el contenedor con altura cero). El texto sigue en el fichero, así que pdftotext no lo delata: hay que mirar el PDF. |
box-sizing: border-box |
No se respeta: suma el padding y el borde a las medidas (un bloque de 450×280 con padding de 30 px se va a 510×340). |
@media print |
No existe: lo marcado como no-print acaba dentro del PDF. |
| Emojis | La tipografía del PDF no tiene el glifo: sale un «?». Usar Texto_PDF::sin_emoji(). |
var(--x) |
No funciona: los colores van escritos (de ahí las constantes). |
| Cifras | number_format_i18n() devuelve el formato inglés aunque el locale sea es_ES: usar Texto_PDF::horas() («18,5»). |
| Medir páginas | pdfinfo , nunca file: file cuenta coincidencias de /Type /Page y cuela el /Type /Pages del árbol, así que da un número que no es (sobre un carnet de una página dijo «3 page(s)»). Y pdftotext -f 2 -l 2 sobre un PDF de una página no da error: devuelve vacío. |

13. Certificados de voluntariado¶
Certificate_Generator emite el certificado cuando el socio ha completado las horas aprobadas ≥ objetivo del plan (recalculado al solicitarlo, no solo por meta).
- Generación:
GET /me/certificatedevuelvecertificado_id, horas ydownload_url;GET /me/certificate/downloadsirve el PDF. - Formato del ID:
VOL-AAAA-XXXXX(año + 5 caracteres), p. ej.VOL-2025-A1B2C. - QR local (chillerlan) enlaza a la verificación pública.
De dónde salen las horas¶
Del libro de horas: el certificado suma _convoca_horas de los registros del socio
(registro_hora con _convoca_member_id) cuyo estado es aprobada. Así, un registro anulado o
rechazado no cuenta — verificado: 8 + 6 + 4,5 h aprobadas y una anulada de 10 h dan un certificado
de 18,50 h.
Detalles de texto (medidos sobre el PDF)¶
- El documento sale en una página A4, con el QR abajo a la derecha.
- Las horas van con la convención del castellano: «18,5 horas» y «1.234,5» si algún día hay
muchas. No se sigue el formato del idioma del sitio a propósito: medido en un sitio con locale
es_ESy las traducciones cargadas, WordPress devolvíadecimal_point = '.'ythousands_sep = ','(el formato inglés), así que el certificado salía con las letras en castellano y las cifras en inglés. Un sitio que quiera otra convención tiene el filtroconvoca_documento_horas. - La etiqueta del plan se imprime sin emojis: la tipografía del PDF no tiene el glifo y salía un «?» (los planes de la demo se llaman «🥉 Bronce»).
El flujo completo, y la misma cifra en la web¶
El recorrido entero es: el socio pide el certificado (GET /me/certificate) → generate() dibuja el
PDF y estampa tres metas en el socio (_convoca_certificado_id, _convoca_certificado_emitido,
_convoca_certificado_valido_hasta) → cualquiera verifica el folio en la página pública
/verificar-certificado/, que es un formulario (?id= solo lo rellena; el resultado sale al
enviarlo) → Certificate_Generator::verify() busca al socio por esa meta y devuelve nombre, horas,
emisión, validez y folio, o nada. Verificado en vivo: con un folio real responde con el nombre y las
horas, y con uno inventado dice «Certificado No Encontrado» (nunca un error).
Las cifras son las mismas en la web que en el PDF. La verificación pública imprimía las horas con
number_format( $horas, 1 ), o sea con los separadores de PHP: «12.5 h». Y el importe de
renovación del panel del socio daba «50.00 €» por lo mismo. Los dos van ya con la coma
castellana (Texto_PDF::horas() y separadores explícitos). Ojo con esto al escribir pantallas
nuevas: number_format() sin separadores no vale para algo que lee una persona; y
number_format_i18n() tampoco, medido (el sitio «en castellano» devolvía el formato inglés). Sin
decimales no hay problema: number_format( $x, 0 ) se queda como está.
Lo vigila FormatoCifrasTest, que recorre includes/, public/ y admin/ buscando cifras con
decimales formateadas sin separadores explícitos; comprobado que caza el fallo (se metió a propósito).
Validez del certificado (1 año)¶
El certificado tiene una validez de 1 año desde la emisión, configurable con el filtro convoca_members_certificado_validez_anios. El PDF muestra «Válido hasta:
- Al aprobarse horas nuevas (hook
convoca_members_hora_aprobada), la validez se regenera desde la fecha actual (el certificado cubre las horas completadas). - La validez se guarda en la meta
_convoca_certificado_valido_hasta. - Retrocompatible: los certificados emitidos antes de esta política calculan su caducidad como emisión + 1 año.
Verificación pública¶
Introduciendo el ID se muestra nombre, horas, fecha de emisión y validez, y distingue el estado: vigente (verde ✅) o caducado (ámbar ⚠️ «Certificado Caducado»). No requiere sesión.

14. Verificación pública de membresía¶
Los socios pueden verificar su estado sin iniciar sesión escaneando el QR del carnet:
El QR apunta a /verificar-socio/?id=<ID>&token=<hash>. El shortcode valida el token (HMAC-SHA256 con sal persistente), comprueba que el socio existe y está activo, y muestra nombre, número de socio y plan.
15. Voluntariado¶
Alta de voluntario¶
Formulario de registro (plantilla templates/form-voluntariado.php) con texto legal obligatorio y campos personalizados configurados en Ajustes → Campos Voluntariado (opción convoca_volunteer_fields).
Acuerdo de incorporación (PDF)¶
PDF_Document::generate_volunteer_agreement($user_id) genera el acuerdo de incorporación y lo guarda como convoca_documento, con el PDF en uploads/convoca-documentos/ y un hash de firma. Es idempotente: si esa persona ya tiene un acuerdo publicado, devuelve ese y no crea otro.
| Pieza | Detalle |
|---|---|
| Plantilla | Opción convoca_pdf_templates['acuerdo_incorporacion'], con marcadores {{nombre}}, {{dni}}, {{fecha_incorporacion}}, {{dynamic_fields}}, {{declaracion}} |
| Campos | Nombre, DNI, fecha de incorporación, campos personalizados y la declaración responsable (texto legal de convoca_volunteer_legal_text) |
| Sello digital | Aceptado por, fecha y hora, IP y hash SHA-256; sustituye al comentario marcador de la plantilla |
| Una página | El documento entero cabe en una página; el sello se iba a la segunda y ahora el espaciado se aprieta lo justo |
El marcador del sello se reconoce con el nombre antiguo de la clase (BDV_Signature) y con el actual (Signature): las plantillas guardadas antes decían el viejo y, al no reconocerlo, el sello caía siempre al final del documento en lugar de donde el sitio lo hubiera puesto. El correo de alta lo adjunta.
Registrar horas¶
El voluntario registra horas desde su panel (pestaña Voluntariado) vía POST /me/horas (solo socios activos). El registro se guarda en el CPT registro_hora con estado inicial pendiente.
Aprobación¶
Un administrador aprueba o rechaza desde la gestión de horas. Estados posibles: pendiente, aprobada, rechazada. Al aprobar (convoca_members_hora_aprobada), Voluntariado_Manager suma las horas y, si alcanzan el objetivo del plan, convierte automáticamente al voluntario en socio activo (establece la renovación a un año del inicio del ciclo) y dispara el email objetivo_voluntariado_completado.
Ciclo anual¶
Los voluntarios activos se evalúan anualmente por horas (ver §7): cumplir el objetivo renueva el ciclo; no cumplirlo los devuelve a pendiente_documentacion.
Gamificación (PRO)¶
Con licencia PRO, Voluntariado_Gamification asigna niveles por track según las horas acumuladas. Tracks por defecto (opción convoca_gamification_tracks):
| Track | Clave | Niveles (horas) |
|---|---|---|
| 🌱 Naturaleza | nature |
Semilla (0), Brote (10), Árbol (25), Bosque (50), Ecosistema (100) |
| 🤝 Comunidad | community |
Mano Abierta (0), Vínculo (10), Comunidad (25), Ciudad (50), Red (100) |
| 🚀 Crecimiento | growth |
Nivel 1 (0), Nivel 2 (10), Nivel 3 (25), Nivel 4 (50), Nivel 5 (100) |
Los niveles se personalizan en Ajustes → Gamificación. Datos disponibles en GET /me/gamification.
16. Emails¶
Email_Manager envía plantillas HTML (layout premium de convoca-core, naranja #FF8700 + violeta #320028) con variables dinámicas. Las plantillas se editan en Ajustes → Plantillas de Email.
| Plantilla | Cuándo se envía |
|---|---|
solicitud_recibida |
Al registrarse desde el formulario |
bienvenida |
Al activar a un socio |
credenciales_acceso |
Tras la aprobación, con usuario/contraseña y enlace de acceso |
recordatorio_pago, pago_pendiente_2, pago_pendiente_ultimo |
Recordatorios escalonados de pago pendiente (3, 7 y 14 días) |
renovacion, renovacion_15d, renovacion_7d |
Avisos de renovación 30/15/7 días antes del vencimiento (con botón de pago {link_pago}) |
renovacion_automatica |
Al generar un cargo recurrente |
renovacion_completada |
Al completarse el pago de renovación; adjunta el carnet PDF |
voluntariado_recordatorio |
Recordatorio trimestral de horas |
objetivo_voluntariado_completado |
Al completar las horas del plan |
confirm_email / verify_phone |
Confirmación de cambio de email y verificación de teléfono |
Variables disponibles (entre otras): {nombre}, {email}, {tipo_miembro}, {plan}, {cuota}, {importe}, {numero_socio}, {fecha_alta}, {fecha_renovacion}, {fecha_baja}, {link_pago}, {usuario}, {password}, {login_url}, {link_confirmacion}.
Proveedor de email¶
Email_Verifier resuelve el proveedor activo según email_provider:
| Proveedor | Slug | Requisitos |
|---|---|---|
| WordPress (wp_mail) | wpmail |
Correo configurado en WordPress (defecto) |
| Mailgun | mailgun |
API key, dominio y región US/EU |
Los proveedores se pueden ampliar con el filtro convoca_email_providers.
17. API REST¶
Namespace: convoca-members/v1.
Autenticación: las rutas /me/*, /member/* y /documentos/* exigen la sesión de socio (cookie convoca_member_session, o cabecera X-Convoca-Auth). Las respuestas de estas rutas envían cabeceras no-store para que el navegador nunca sirva datos privados en caché. /login, /alta, /proyectos y /search son públicas (con rate limiting).
| Método | Ruta | Descripción |
|---|---|---|
| POST | /login |
Inicia sesión (credenciales de WordPress: usuario/email + contraseña). Limitado a 10 intentos / 5 min por IP |
| POST | /alta |
Alta de socio desde el formulario (nonce convoca_alta_nonce) |
| GET | /me |
Perfil del socio: id, nombre, email, email_pendiente, direccion, telefono, telefono_verificado, cumpleanos, codigo, estado, plan, forma_pago, fecha_renovacion, fecha_alta, fecha_baja, cuota_vencida, puede_renovar, renovable, tipo |
| POST | /me/profile |
Editar perfil (dirección, teléfono, email con confirmación, cumpleaños una sola vez) — solo activo |
| POST | /me/verify-phone |
Enviar enlace de verificación de teléfono |
| POST | /me/confirm-email |
Confirmar email pendiente con token |
| POST | /me/renovar |
Renovar membresía (genera pago, devuelve payment_url) — activo y suspendido |
| POST | /me/unsubscribe |
Solicitar baja (baja_solicitada) |
| GET | /me/card |
HTML del carnet (admite ?theme=light|dark) |
| GET | /me/certificate |
Datos del certificado de voluntariado |
| GET | /me/certificate/download |
Descargar certificado PDF |
| GET | /me/gamification |
Datos de gamificación (nivel, badges, progreso) |
| GET | /me/pagos |
Historial de pagos |
| GET/POST | /me/horas |
Leer horas (cualquier miembro) / registrar horas (solo activo) |
| GET | /me/inscripciones |
Inscripciones del socio |
| GET | /proyectos |
Proyectos activos (para el desplegable) |
| GET | /search |
Búsqueda unificada ?q=&type=&limit= (actividades públicas; socios solo con sesión) |
| GET | /admin/members/search |
Búsqueda admin de miembros (gestionar_miembros) |
| GET | /documentos/{id} |
Descarga segura de un documento del socio |
| GET | /member/notifications |
Notificaciones del socio |
| POST | /member/notifications/read |
Marcar notificación leída |
| POST | /member/notifications/read-all |
Marcar todas leídas |
18. GDPR y protección de datos¶
GDPR_Tools se integra con las Herramientas de privacidad de WordPress (Herramientas → Exportar/Borrar datos personales), no con un panel propio:
- Exportar: registra un exportador de datos de socio que devuelve perfil, consentimiento, pagos, inscripciones y horas en el archivo ZIP estándar de WordPress.
- Borrar: anonimiza el perfil (los campos PII pasan a
[Eliminado], el título aSocio anonimizado #ID), conserva los pagos anonimizados por obligación fiscal, y elimina inscripciones y registros de horas. - El consentimiento del alta se registra con
GDPR_Tools::log_consent(versión, timestamp e IP).
19. Auditoría y registros¶
Cada cambio de estado genera una entrada en _convoca_historial (de/a/fecha/usuario/nota) y en la tabla convoca_logs (Logger). Consulta:
- Miembros → Registros (slug
conv-members-logs): log global con nivel, contexto y objeto. - Ficha del socio: pestaña de historial de estados y últimos 20 registros de actividad.
20. Shortcodes públicos¶
| Shortcode | Descripción |
|---|---|
[convoca_alta_socio] |
Formulario de alta para nuevos socios |
[convoca_mi_area] |
Panel completo del socio (datos, carnet, inscripciones, voluntariado, pagos) |
[convoca_mi_perfil] |
Perfil del socio (estado, plan, inscripciones) — legado, requiere login de WordPress |
[convoca_renovar] |
Renovación de membresía: muestra plan y genera el pago (página /renovar/) |
[convoca_voluntariado] |
Formulario de alta de voluntariado |
[convoca_verificar_socio] |
Verificación pública de membresía (QR del carnet) |
[convoca_verificar_certificado] |
Verificación pública de certificados |
21. Bloques Gutenberg¶
Además de los shortcodes, se registran cuatro bloques (categoría convoca-members):
| Bloque | Equivalente |
|---|---|
convoca-members/formulario-alta |
[convoca_alta_socio] |
convoca-members/mi-area |
[convoca_mi_area] |
convoca-members/formulario-voluntariado |
[convoca_voluntariado] |
convoca-members/verificar-certificado |
[convoca_verificar_certificado] |
22. Cron y automatizaciones¶
Dos eventos programados (registrados en la activación):
| Evento | Frecuencia | Tareas |
|---|---|---|
convoca_daily_event |
Diaria | Recordatorios de pago (3/7/14 d), avisos de renovación (30/15/7 d), renovaciones automáticas recurrentes, expiración/ciclo de vida (check_member_status), recordatorios trimestrales de voluntariado |
convoca_weekly_event |
Semanal (lunes 08:00) | Resumen semanal para administradores (digest_extra_emails) |
Los locks atómicos (Utils::acquire_lock) evitan ejecuciones concurrentes y envíos duplicados.
Copia de los correos a la administración¶
Los correos de socio (alta, bienvenida, credenciales, recordatorios de pago, renovación,
certificado, tarjeta PDF y voluntariado) se copian al correo administrador mediante
Convoca\Core\Email_Copy, con el mismo canal de envío y marcados con [Copia].
En Ajustes → General hay dos campos relacionados:
- Email administrador: recibe las copias (si se deja vacío se usa el correo del sitio).
- Enviar copia de los correos a la administración: interruptor, marcado por defecto.
El correo administrador debe ser distinto del remitente
Si coincide con el remitente configurado, la copia llega al propio remitente y pasa desapercibida.
23. Problemas comunes¶
| Problema | Solución |
|---|---|
| No se envían emails | Revisa Miembros → Registros; comprueba el proveedor de email en Ajustes → General (wpmail o Mailgun con su API key/dominio) |
| Un socio "desaparece" de la zona privada | Ha pasado a suspendido o baja por el ciclo de vida: revisa fecha_renovacion y los días de gracia en Ajustes |
| Socio que se da de alta de nuevo | No se duplica: si la ficha está en baja se reactiva (mismo número y antigüedad) |
| La renovación no amplía la fecha correcta | Es intencional: la renovación tardía suma 1 año desde el vencimiento anterior, no desde hoy |
| No puedo cambiar mi cumpleaños | Es intencional: solo se puede establecer una vez |
| El email de verificación no llega | Revisa el proveedor de email y la carpeta de spam; el enlace caduca a las 24 h |
| No puedo renovar desde el panel | Solo activo y suspendido pueden renovar; un baja debe re-darse de alta |
| El certificado no se genera | Se requiere haber completado las horas aprobadas ≥ objetivo del plan |
| Error al generar PDF | Verifica Convoca\Core\Signature (Dompdf) en convoca-core y que chillerlan/php-qrcode esté instalado (composer install) |