Saltar a contenido

Convoca SDK

Guía para desarrollar extensiones del ecosistema Convoca sin leer el código.

Arquitectura

Convoca es una plataforma: core provee la infraestructura (registro de módulos, utilidades, licencias, logs, rate limiter) y los plugins (members, enroll, gateway, shifts, publisher, assistant) son módulos registrados. Las extensiones externas se registran igual.

┌─────────────────────────────────────────────┐
│  Tu extensión (plugin independiente)        │
│  ┌───────────────────────────────────────┐  │
│  │  convoca_register_module (filtro)     │  │
│  └──────────────┬────────────────────────┘  │
└─────────────────┼───────────────────────────┘
                  ▼
┌─────────────────────────────────────────────┐
│  Convoca Core (Module_Registry)             │
│  - valida core_min / features / capabilities│
│  - llama al bootstrap                       │
│  - expone Utils, Logger, License_Manager    │
└─────────────────────────────────────────────┘

Registro de módulo

// En tu plugin principal:
add_filter( 'convoca_register_module', 'mi_ext_register' );

function mi_ext_register() {
    if ( ! class_exists( 'Convoca\\Core\\Module_Registry' ) ) {
        return array(); // core no activo — funcionas como plugin normal
    }
    return array(
        'mi-extension' => array(
            'name'         => __( 'Mi Extensión', 'mi-extension' ),
            'version'      => '1.0.0',
            'core_min'     => '2.1.0',
            'features'     => array(),      // features PRO requeridas
            'capabilities' => array(),      // capacidades requeridas
            'bootstrap'    => 'mi_ext_bootstrap',
        ),
    );
}

El core valida el contrato y llama bootstrap solo si es compatible.

API disponible para extensiones

Recurso Acceso
\Convoca\Core\Utils:: 34 helpers: DNI, fechas, rate limit, locks, notices, PDF
\Convoca\Core\Features:: detectar plugins activos (is_members_active(), etc.)
\Convoca\Core\Logger:: logging unificado
\Convoca\Core\License_Manager:: licencias y features PRO
\Convoca\Core\Module_Registry:: registro de módulos

Contratos entre plugins: el vocabulario y las personas

Regla del ecosistema, y la que más se incumple al escribir una extensión: no averigües quién es quién.

Qué De quién es Cómo se usa
Roles y capacidades (socio, voluntario_aprobado, monitor_actividad) Convoca Core Convoca\Core\Capabilities. No los crees: add_role() no pisa un rol existente, así que quien llega segundo no añade nada y el rol se queda a medias según quién activara primero.
Quién es voluntario, quién está aprobado, quién es monitor Convoca Members Pregunta por el contrato, no por el rol
Quién se asigna a un turno Shifts, con las personas que le den convoca_shifts_personas_asignables
Quién acredita horas El libro de horas de Core convoca_can_credit_hours

convoca_shifts_personas_asignables

/**
 * @param array<int, array{id:int, nombre:string}> $personas Lista; por defecto VACÍA.
 * @param string                                   $contexto 'voluntario' | 'monitor'
 */
$personas = apply_filters( 'convoca_shifts_personas_asignables', array(), $contexto );

Si eres el dueño de las personas, contesta con array( array( 'id' => 12, 'nombre' => 'Ana' ), … ). Si no lo eres, no contestes: el valor por defecto vacío es el correcto, y significa «aquí no hay a quién asignar». Shifts pinta la pantalla con la nota del motivo y no deja asignar.

convoca_can_credit_hours

/**
 * @param bool $puede   Por defecto FALSE.
 * @param int  $user_id Usuario de WordPress.
 */
$puede = apply_filters( 'convoca_can_credit_hours', false, $user_id );

El valor por defecto es no a propósito: sin gestión de personas no se acreditan horas de voluntariado ni de monitorado. El turno se marca realizado igual; lo que no hay son horas.

convoca_member_identity y convoca_member_priority

Quién es el socio con la sesión abierta, y si su inscripción tiene prioridad:

/**
 * @param array{autenticado: bool, member_id: int, datos: array} $identidad Por defecto, nadie.
 */
$identidad = apply_filters( 'convoca_member_identity', array(
    'autenticado' => false,
    'member_id'   => 0,
    'datos'       => array(),
) );

/**
 * @param bool $prioridad      Por defecto SÍ (sin gestión de socios no hay cuota que verificar).
 * @param int  $inscripcion_id Inscripción.
 */
$prioridad = apply_filters( 'convoca_member_priority', true, $inscripcion_id );

Se preguntan a través de Convoca\Core\Member_Identity, que guarda la respuesta sólo durante la petición: la sesión del socio vive en un transient y caduca. Tras un login o un logout en la misma carga, quien contesta llama a Member_Identity::olvidar().

Ojo con la asimetría del defecto: la identidad falla en «nadie» (seguro) y la prioridad falla en «sí» (porque es lo que hacía el código antes de existir el contrato). No es incoherencia: es no cambiar el comportamiento de nadie al refactorizar.

Por qué el defecto es «no»

Los dos contratos fallan cerrados. Si nadie contesta, el sistema no inventa personas ni horas. Antes lo hacía: cada plugin leía roles por su cuenta y, en una instalación sin Convoca Members, aparecían personas asignables y se acreditaban horas que nadie podía justificar.

Patrones recomendados

CPT

add_action( 'init', function () {
    register_post_type( 'mi_cpt', array(
        'label' => __( 'Mi CPT', 'mi-extension' ),
        'public' => true,
        'supports' => array( 'title', 'editor' ),
    ) );
} );

Shortcode

add_shortcode( 'mi_shortcode', function ( $atts ) {
    $atts = shortcode_atts( array( 'x' => '' ), $atts );
    return '<div>' . esc_html( $atts['x'] ) . '</div>';
} );

REST endpoint

add_action( 'rest_api_init', function () {
    register_rest_route( 'mi-extension/v1', '/dato', array(
        'methods'             => 'GET',
        'permission_callback' => '__return_true',
        'callback'            => function () {
            return rest_ensure_response( array( 'ok' => true ) );
        },
    ) );
} );

Hook público

do_action( 'mi_extension_hecho', $dato, $extra );
// Documentar en HOOKS: nombre, args, @since

Plugin de ejemplo

Hello Convoca — plantilla completa y funcional:

Convenciones

  1. Prefijos: mi_extension_* en funciones, mi-extension/ en namespaces REST
  2. Namespaces: register_rest_route( 'mi-extension/v1', ... )
  3. Escaping: nunca imprimir sin esc_html()/esc_attr()
  4. Sanitización: sanitize_text_field() en entradas
  5. Nonces: wp_verify_nonce() en POST admin
  6. Versiones: sincronizar header, constante y Stable tag

API congelada

La API pública v3.0 está congelada — los hooks, REST endpoints y shortcodes no cambian sin deprecación. El CI verifica la congelación en cada push. Referencia: docs/openapi.yaml (OpenAPI) y docs/hooks-inventario.md del repo convoca-health-check.