Modelo de datos canónico
Referencia del modelo de datos de CDC para escribir reportes y consultas. Está pensada para alguien
que sabe SQL pero no conoce este esquema, y también para asistentes que traducen preguntas en
lenguaje natural a consultas. Todas las consultas corren sobre la base MySQLCDC.
Principio: todo lo reportable vive en tablas centrales
La base es un WordPress multisitio. WordPress guarda el contenido de cada casa en tablas propias
por sitio (wp_3_posts, wp_10_postmeta, wp_17_options, …). Para reportar, ignoralas.
CDC copia y denormaliza todo lo que hace falta en tablas centrales (network-wide), para que un reporte:
- no tenga que unir catorce tablas por casa, y
- no choque con el problema de collation entre tablas de distintos sitios (un
UNIONsobre tablas por sitio falla con Illegal mix of collations).
Cada fila central lleva un blog_id (id numérico de la casa) y, en inscripciones, además un
site_slug (nombre corto de la casa, como elcascosv). Para aislar por casa, filtrá por esas
columnas, nunca eligiendo una tabla por sitio.
Las cuatro tablas que resuelven casi todo:
| Tabla | Alcance | Rol |
|---|---|---|
wp_cdc_registrations | network | Inscripciones. Una fila = una persona inscripta en una actividad. |
wp_users | network | Cuentas de usuario. |
wp_usermeta | network | Datos extra del usuario (documento, país del documento, …). |
wp_cdc_notes | network | Notas / bitácora de auditoría. Todavía sin usar en reportes. |
1. wp_cdc_registrations — inscripciones
El corazón de casi todo reporte. Una fila representa una persona inscripta en un producto (actividad). No es “una compra”: una orden con dos actividades genera dos filas, y el staff que no pasa por el checkout también tiene su fila.
Muchas columnas son snapshots tomados al momento de inscribir (nombre del producto, fechas, tipo, importes). No se actualizan si después se edita el producto: reflejan “lo que estaba vigente cuando la persona se inscribió”.
Identificación y sitio
| Columna | Tipo | Notas |
|---|---|---|
id | BIGINT | Clave primaria de la inscripción. |
blog_id | BIGINT | Id numérico de la casa. Usalo para joins con otros espejos centrales. |
site_slug | VARCHAR | Nombre corto de la casa (elcascosv, lagranjasf). Identificador estable para agrupar/filtrar por casa. NULL en filas muy viejas. |
product_id | BIGINT | Id del producto (actividad) en su sitio. (blog_id, product_id) identifica una actividad. |
order_id | BIGINT | Id de la orden de WooCommerce. NULL en waitlist/staff. |
subscription_id | BIGINT | Id de suscripción, si aplica. Normalmente NULL. |
registered_id | VARCHAR | Clave sintética interna de idempotencia. No aporta a reportes. |
Fechas
| Columna | Tipo | Notas |
|---|---|---|
created_at | DATETIME (UTC) | Cuándo se hizo la reserva. Usala para “inscripciones por mes”. |
updated_at | DATETIME (UTC) | Última modificación de la fila. |
start_datetime | DATETIME | Cuándo empieza la actividad (snapshot). Usala para “año que asistió”. |
end_datetime | DATETIME | Cuándo termina la actividad (snapshot). |
Distinción clave para no confundir números: created_at = cuándo se anotó; start_datetime =
cuándo va/fue a la actividad.
La persona inscripta (columnas attendee_*)
| Columna | Tipo | Notas |
|---|---|---|
attendee_name | VARCHAR NOT NULL | Nombre de quien asiste. Siempre presente: es el respaldo cuando no hay cuenta. |
attendee_email | VARCHAR | Email de contacto de la inscripción. |
attendee_national_id | VARCHAR | Documento de quien asiste (snapshot). Ya no es el vínculo con la cuenta —ese rol es de attendee_user_id—. Completarlo ancla la fila sola (ver más abajo). Puede ser NULL (waitlist, pago pendiente, tercero sin documento). |
attendee_national_id_country | VARCHAR(5) | País emisor del documento (AR, UY, PY, …). Muchas filas NULL. |
attendee_birthday | VARCHAR | Fecha de nacimiento. Sin usar en reportes. |
attendee_phone | VARCHAR | Teléfono. Sin usar en reportes. |
attendee_origin | VARCHAR | Procedencia. Sin usar en reportes. |
attendee_meal_restrictions | VARCHAR | Restricciones alimentarias (texto libre). Sin usar. |
attendee_meal_exceptions | JSON | Excepciones de comidas por día. Sin usar. |
attendee_room_slug | VARCHAR | Habitación asignada. Sin usar en reportes. |
attendee_bed_number | VARCHAR | Cama asignada. Sin usar en reportes. |
attendee_user_id | BIGINT | La cuenta de quien asiste — el ancla de identidad. Es la columna con la que unís a wp_users para saber quién es la persona. NULL hasta que la identidad se confirma (lista de espera nunca la setea; un tercero la recibe por match de documento, al iniciar sesión, o al reclamar “soy yo”). Cuando el comprador es el asistente, attendee_user_id = user_id. |
user_id | BIGINT | El comprador (cliente de la orden de WooCommerce), no necesariamente quien asiste. Puede ser NULL. Para saber quién asiste usá attendee_user_id, no esta columna. |
Tipo de actividad
| Columna | Tipo | Notas |
|---|---|---|
product_name | VARCHAR | Nombre de la actividad (snapshot). |
product_type | VARCHAR | Lista separada por comas de códigos del vocabulario cerrado de red, ordenada alfabéticamente: retiro, convivencia,retiro, retiro,retiro-de-impacto. Códigos válidos: retiro, convivencia, retiro-de-impacto, retiro-de-jovenes, curso-anual. NULL = tipo desconocido (productos históricos sin atributo). |
es_retiro | TINYINT(1) | Columna generada: 1 si product_type contiene la subcadena retiro, si no 0. |
es_convivencia | TINYINT(1) | Columna generada: 1 si product_type contiene convivencia, si no 0. |
es_promocion | TINYINT(1) | 1 si el producto generaba cupones (combo/promoción) al inscribir. |
Ver Criterios de tipo de actividad: usá siempre los flags es_*, no compares product_type
con =. Los códigos de tipo son de red (no se inventan por casa); el snapshot en la inscripción
conserva el valor al momento de anotar.
Estado
| Columna | Tipo | Notas |
|---|---|---|
attendee_type | VARCHAR | participant (asistentes/compradores/lista de espera) u on-site-staff (coordinadores/ayudantes). |
status | VARCHAR | confirmed, awaiting_payment, waitlisted, cancelled. |
order_status | VARCHAR | Estado de la orden de WooCommerce, si aplica. |
payment_method | VARCHAR | Método de pago. |
Dinero
| Columna | Tipo | Notas |
|---|---|---|
amount | DECIMAL(18,4) | Neto cobrado de la línea, después de cupones, sin impuestos. Cubre toda la línea: si quantity > 1, no es precio por persona. NULL = no hubo venta (waitlist, staff) o fila vieja sin backfill. |
amount_subtotal | DECIMAL(18,4) | Lo mismo antes de cupones. amount_subtotal - amount = descuento. |
quantity | INT | Unidades de la línea. Casi siempre 1. |
currency | VARCHAR(3) | ISO 4217. Hoy uniformemente ARS. |
Al sumar importes conviene convertir: SUM(CAST(amount AS DECIMAL(18,4))).
2. wp_users — cuentas
wp_users es la tabla estándar de WordPress. Interesan ID, display_name, user_email y
user_registered.
3. wp_usermeta — datos del usuario
wp_usermeta guarda pares (user_id, meta_key, meta_value). Las claves relevantes son las que se
editan desde el detalle de un usuario en el panel admin, así que son las que vas a querer traer a un
reporte. Los ejemplos de valor son los que hoy ofrece el panel.
meta_key | Qué guarda | Ejemplos de valor |
|---|---|---|
national_id | El documento (DNI) del usuario. Credencial de acceso. Ya no se usa para unir inscripciones con cuentas (eso es attendee_user_id); completar este documento ancla automáticamente las inscripciones huérfanas que lo tengan. | 30123456 |
national_id_country_of_issue | País emisor del documento del usuario. | AR, UY, PY |
_pa_centro | Centro de la persona en esa sede. Guarda el term_id local del atributo WooCommerce pa_centro (por blog). La API lo expone como array de un nombre para mostrar. Cada sede tiene sus propios centros; no es un vocabulario de red. | 23 (id); en UI: Cebil |
relationship | Relación de la persona con la casa. Categoría libre por sede: no hay un catálogo único de red. Guarda el código de la lista de ese sitio, no la etiqueta. | códigos de GET /cdc/v1/vocabularies → relationships (varían por sede) |
gender | Género. | Hombre, Mujer |
birthday | Fecha de nacimiento (texto). | 1990-05-17 |
phone | Teléfono móvil. | 11 5555-1234 |
Ids de cliente Debi
El id de cliente en Debi no vive en
wp_usermeta(eraid_customer_debi, eliminado: en multisite cada casa tiene su propia cuenta Debi y un id de otra casa falla). Queda en la meta del pedido:_debipro_customer_id. Para conciliar pagos, uní por pedido /_debipro_subscription_id, no por usuario.
Vocabularios: tipo de actividad, relación y centro
| Vocabulario | Dónde se guarda | Notas para reportes |
|---|---|---|
| Relación | wp_usermeta.relationship | Por sede, no es una lista fija de red. Cada casa define sus códigos y etiquetas (WP Admin → Vocabularios del sitio). La meta guarda el código; la lista vigente sale de GET /cdc/v1/vocabularies → relationships. No asumas etiquetas concretas en un reporte multi-sede. |
| Tipo de actividad | snapshot wp_cdc_registrations.product_type (y atributo WC pa_tipo_de_actividad en el producto) | Códigos de red habituales: retiro, convivencia, retiro-de-impacto, retiro-de-jovenes, curso-anual (también se pueden sembrar/editar por sede). Preferí los flags es_* (ver abajo). |
| Centro | wp_usermeta._pa_centro | Por sede. Es un term_id del atributo pa_centro de esa sede. El mismo número en otro blog es otro centro. Para mostrar el nombre, uní a wp_N_terms del blog correspondiente (o usá el array de nombres que ya resuelve la API). |
Ejemplos de filtro en SQL:
-- Personas por relación (código de esa sede; no hay lista única de red)
WHERE relationship = 'otro'
-- Inscripciones por tipo (preferí flags; si necesitás un código puntual)
WHERE FIND_IN_SET('curso-anual', product_type) > 0Etiquetas vs códigos (relación / tipo)
Las etiquetas del panel salen de
GET /cdc/v1/vocabulariesde esa sede. Si un reporte necesita el texto legible, mapeá código → etiqueta en la consulta o en Metabase; la meta guarda el código.
_pa_centroy term idsLa meta del usuario es un
term_idpor blog (o vacío). Un breve experimento guardó slugs de red; la API los resuelve a nombre si el término existe en la sede. Los cupones y atributos de producto también usanterm_idpor sede.
4. wp_cdc_notes — notas y bitácora (sin explotar aún)
Tabla de notas/auditoría genérica. Una fila es una nota sobre alguna entidad. Todavía no se usa en ningún reporte, así que es terreno fértil para paneles nuevos (actividad administrativa, cuántos emails se enviaron a clientes, etc.).
| Columna | Notas |
|---|---|
blog_id | Casa. Toda consulta debe scopear por acá. |
entity_type | A qué apunta la nota: registration, order, product, customer. |
entity_id | Id de esa entidad (no es FK: las notas sobreviven al borrado del objeto). |
author_id / author_name | Quién escribió la nota; NULL/sistema cuando la generó el sistema. |
is_system | 1 = generada automáticamente (transiciones de estado, etc.). |
content | Cuerpo de la nota. |
visible_to_customer | 1 = la vio el cliente y (si se envió email) la recibió. |
email_subject / email_to / email_cc | Datos del email, capturados al enviar. |
email_sent_at | Cuándo se envió con éxito. NULL si no se envió o falló. |
created_at | Cuándo se creó la nota (UTC). |
Cómo se vincula una inscripción con la persona
El vínculo es directo y numérico: attendee_user_id. Es la cuenta de quien asiste, confirmada al
inscribir, al iniciar sesión o al reclamar la inscripción. Uniéndola a wp_users.ID pasás de la
inscripción a la cuenta, y desde wp_users + wp_usermeta traés el nombre, el email, el documento y
cualquier otro dato del perfil — sin los problemas de collation que trae un join de texto por
documento.
attendee_user_ides la persona;user_ides quien pagóNo confundas las dos columnas.
user_ides el comprador de la orden; si alguien anota a un tercero, el comprador no es la persona que asiste. Para “quién asistió” usá siempreattendee_user_id.
Lo único a tener presente: no toda inscripción tiene cuenta de asistente. Cuando attendee_user_id
es NULL (identidad todavía sin confirmar, o invitado que nunca se registró) no hay perfil que mirar y
para agrupar/mostrar caés a lo que quedó guardado en la propia inscripción: attendee_email y
attendee_name, que dan más precisión que juntar todos los NULL en un solo grupo.
En una frase
Uní por
attendee_user_id↔wp_users.IDpara traer los datos de la cuenta; si es NULL, caé aattendee_email/attendee_namede la inscripción.
En SQL es un LEFT JOIN con un COALESCE que elige la cuenta cuando existe y cae al dato del
invitado cuando no:
SELECT
r.id AS `Inscripción`,
r.site_slug AS `Sitio`,
r.product_name AS `Actividad`,
COALESCE(u.display_name, r.attendee_name) AS `Persona`,
COALESCE(u.user_email, r.attendee_email) AS `Email`,
CASE WHEN u.ID IS NULL THEN 'Invitado' ELSE 'Con cuenta' END AS `Vínculo`
FROM wp_cdc_registrations r
LEFT JOIN wp_users u ON u.ID = r.attendee_user_id
WHERE r.attendee_type = 'participant';Con eso ya cubrís el 90% de los reportes. Los detalles de abajo solo hacen falta en casos puntuales.
Los NULL se curan solos
No unas por documento para recuperar identidad. La forma de bajar la cantidad de NULL es operativa: al completar el documento —en la cuenta (perfil, alta o checkout) o en la propia inscripción (corrección del admin)— el sistema ancla automáticamente las inscripciones huérfanas que tengan ese documento, y
attendee_user_iddeja de ser NULL. Un reporte que hoy muestra invitados sin cuenta va mejorando solo a medida que se cargan documentos, sin tocar el SQL.
La columna
user_id, el comprador
wp_cdc_registrations.user_ides la cuenta que pagó, no la que asiste. Solo coincide conattendee_user_idcuando el comprador se anotó a sí mismo. No la uses como identidad del asistente.
Criterios de tipo de actividad
Una actividad puede ser un retiro, una convivencia, ambas a la vez, o ninguna. Los
códigos pertenecen al vocabulario cerrado de red (retiro, convivencia, retiro-de-impacto,
retiro-de-jovenes, curso-anual). En la inscripción el tipo vive en product_type como lista
separada por comas, pero no lo consultes con = ni con IN: te perderías los productos que
llevan dos tipos (convivencia,retiro).
Usá siempre las columnas generadas:
es_retiro = 1→ la actividad es (algún tipo de) retiro. Matchea por subcadena, así queretiro-de-jovenes,retiro-de-impacto, etc. cuentan como retiro.es_convivencia = 1→ la actividad es convivencia.- Ambas en 1 → es retiro y convivencia (existe, y es legítimo).
- Ambas en 0 → ninguna de las dos o tipo desconocido (p. ej. solo
curso-anual).
NULL no es 0
Cuando
product_typees NULL (tipo desconocido), ambos flags dan 0. Es decir, “ninguna de las dos” mezcla desconocido con realmente ninguna. Si esa diferencia importa, distinguíproduct_type IS NULLaparte.
es_promocion = 1 marca los combos que reparten cupones; útil para separar promociones de ventas
normales.
SELECT
YEAR(start_datetime) AS `Año`,
SUM(es_retiro = 1) AS `Retiros`,
SUM(es_convivencia = 1) AS `Convivencias`,
SUM(es_retiro = 1 AND es_convivencia = 1) AS `Ambos`,
SUM(es_retiro = 0 AND es_convivencia = 0 AND product_type IS NOT NULL) AS `Ninguna`,
SUM(product_type IS NULL) AS `Tipo desconocido`
FROM wp_cdc_registrations
WHERE attendee_type = 'participant'
AND status NOT IN ('cancelled', 'waitlisted')
GROUP BY YEAR(start_datetime)
ORDER BY `Año` DESC;Estados y su semántica
status toma cuatro valores: confirmed, awaiting_payment, waitlisted, cancelled.
attendee_type distingue participant de on-site-staff.
La forma canónica de contar, tal como la usan los tableros:
| Concepto | Definición |
|---|---|
| Vendidas | attendee_type = 'participant' AND status NOT IN ('cancelled', 'waitlisted', 'awaiting_payment') |
| Por pagar | attendee_type = 'participant' AND status = 'awaiting_payment' |
| Lista de espera | status = 'waitlisted' |
| Canceladas | status = 'cancelled' |
| Staff | attendee_type = 'on-site-staff' AND status NOT IN ('cancelled', 'waitlisted') |
Por qué Vendidas se define por exclusión y no listando los estados buenos: hubo estados que se
retiraron el 10/06/2026 (bed_assigned, arrived, attended, todos migrados a confirmed) que
todavía aparecen en filas históricas. Listar status = 'confirmed' dejaría afuera esas filas viejas;
excluir los malos las incluye.
Estado Pago Debi (meta de la orden)
Las órdenes pagadas con tarjeta (Debi) guardan un resumen del plan de cobro en meta de la orden WooCommerce. En el panel se muestra como Estado Pago (ver Órdenes → Estado Pago).
No vive en las tablas centrales
Estos campos no están en
wp_cdc_registrations. Viven en las tablas por sitio de WooCommerce HPOS (wp_<blog_id>_wc_orders+wp_<blog_id>_wc_orders_meta). Un reporte de red que sumeamountde inscripciones no ve deuda de cuotas: para eso hay que mirar la meta Debi (casa por casa, o unUNIONporblog_id).
Los valores los escribe el plugin Debi a partir de los webhooks de pago (y un reconcile puntual). No los edites a mano en SQL.
Claves de meta (meta_key)
meta_key | Qué es | Valores / tipo |
|---|---|---|
_debipro_payment_status | Estado Pago del plan | current (Al día), past_due (Con deuda), paid (Pagado), cancelled (Cancelado) |
_debipro_amount_paid | Suma acreditada por Debi | decimal (string en meta) |
_debipro_amount_overdue | Suma de cobros fallidos/cancelados que cuentan como deuda | decimal |
_debipro_amount_remaining | final_price − amount_paid (piso 0) | decimal |
_debipro_final_price | Total del plan con tarjeta (puede incluir recargo por cuotas) | decimal |
_debipro_installment_count | Cantidad de cuotas | entero |
_debipro_installment_amount | Monto de cada cuota | decimal |
_debipro_subscription_id | Id del plan/suscripción en Debi | string |
_debipro_subscription_status | Estado crudo del plan en Debi (active, finished, cancelled, …) | string |
_debipro_origin | Cómo nació el vínculo con Debi | installment_plan (checkout en cuotas); otros orígenes de pagos entrantes |
_debipro_payments_synced_at | Última vez que se recalculó la proyección | datetime ISO |
_debipro_customer_id / _debipro_payment_id | Ids Debi auxiliares | string |
Cómo se calcula el estado
Para un plan de checkout (_debipro_origin = installment_plan):
| Condición | _debipro_payment_status | Suele dejar la orden WC en… |
|---|---|---|
amount_paid ≥ final_price | paid | completed |
| Plan cancelado en Debi y aún no está pago | cancelled | cancelled |
amount_overdue > 0 | past_due | sin cambio forzado (sigue processing si ya lo estaba) |
| Resto | current | processing |
Dos “estados” distintos
wp_cdc_registrations.order_status/ estado WC = flujo de la compra (¿confirmó inscripción?)._debipro_payment_status= salud del plan de cuotas.Una inscripción puede estar
confirmedcon ordenprocessingy a la vezpast_due.
Para reportes de cobranza de tarjeta, el filtro útil es
_debipro_payment_status = 'past_due' (y, si hace falta, sumar _debipro_amount_overdue).
Consultas de ejemplo
Todas corren sobre MySQLCDC. Adaptá los WHERE a tu caso.
Inscripciones por casa y estado
SELECT
site_slug AS `Sitio`,
SUM(status = 'confirmed') AS `Confirmadas`,
SUM(status = 'awaiting_payment') AS `Por pagar`,
SUM(status = 'waitlisted') AS `Lista de espera`,
SUM(status = 'cancelled') AS `Canceladas`
FROM wp_cdc_registrations
WHERE attendee_type = 'participant'
GROUP BY site_slug
ORDER BY `Confirmadas` DESC;Inscripciones por mes de reserva
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS `Periodo`,
COUNT(*) AS `Inscripciones`
FROM wp_cdc_registrations
WHERE created_at IS NOT NULL
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ORDER BY `Periodo` DESC;Ingresos por casa (neto de cupones)
SELECT
site_slug AS `Sitio`,
SUM(CAST(amount AS DECIMAL(18,4))) AS `Vendido`,
SUM(amount IS NULL) AS `Sin importe`,
GROUP_CONCAT(DISTINCT currency) AS `Moneda`
FROM wp_cdc_registrations
WHERE attendee_type = 'participant'
AND status NOT IN ('cancelled', 'waitlisted')
GROUP BY site_slug
ORDER BY `Vendido` DESC;Sin importe debería ser 0 en ventas confirmadas; si no lo es, hay filas vendidas sin importe
cargado y el total de Vendido las está dejando afuera silenciosamente.
Personas que más repiten (por asistente)
Agrupá por el ancla attendee_user_id; para las filas de invitado sin cuenta, caé al email (o al
nombre) del snapshot para no juntar a todos los invitados en un mismo grupo NULL. No hace falta el
documento: esos NULL se van curando solos a medida que se cargan documentos.
SELECT
COALESCE(MAX(u.display_name), MAX(r.attendee_name)) AS `Persona`,
COALESCE(MAX(u.user_email), MAX(r.attendee_email)) AS `Email`,
COUNT(*) AS `Inscripciones`,
COUNT(DISTINCT r.site_slug) AS `Casas`
FROM wp_cdc_registrations r
LEFT JOIN wp_users u ON u.ID = r.attendee_user_id
WHERE r.attendee_type = 'participant'
AND r.status NOT IN ('cancelled', 'waitlisted')
GROUP BY COALESCE(
CONCAT('u:', r.attendee_user_id),
CONCAT('e:', NULLIF(LOWER(TRIM(r.attendee_email)), '')),
CONCAT('n:', NULLIF(TRIM(r.attendee_name), ''))
)
ORDER BY `Inscripciones` DESC
LIMIT 50;Retiros vs convivencias por año
Ver el ejemplo en Criterios de tipo de actividad.
Órdenes con deuda de cuotas Debi (una casa)
HPOS: reemplazá 3 por el blog_id de la casa. Los importes vienen como texto en meta; casteá a
decimal al sumar.
SELECT
o.id AS `Orden`,
o.status AS `Estado WC`,
MAX(CASE WHEN m.meta_key = '_debipro_payment_status'
THEN m.meta_value END) AS `Estado Pago`,
CAST(MAX(CASE WHEN m.meta_key = '_debipro_amount_paid'
THEN m.meta_value END) AS DECIMAL(18,4)) AS `Pagado`,
CAST(MAX(CASE WHEN m.meta_key = '_debipro_amount_overdue'
THEN m.meta_value END) AS DECIMAL(18,4)) AS `Adeudado`,
CAST(MAX(CASE WHEN m.meta_key = '_debipro_amount_remaining'
THEN m.meta_value END) AS DECIMAL(18,4)) AS `Restante`,
CAST(MAX(CASE WHEN m.meta_key = '_debipro_final_price'
THEN m.meta_value END) AS DECIMAL(18,4)) AS `Total plan`
FROM wp_3_wc_orders o
INNER JOIN wp_3_wc_orders_meta m ON m.order_id = o.id
WHERE o.type = 'shop_order'
AND o.id IN (
SELECT order_id
FROM wp_3_wc_orders_meta
WHERE meta_key = '_debipro_payment_status'
AND meta_value = 'past_due'
)
GROUP BY o.id, o.status
ORDER BY `Adeudado` DESC;Para otra casa, cambiá el prefijo wp_3_ (p. ej. wp_10_). No hay tabla central de esta proyección.
Glosario rápido (para traducir preguntas a columnas)
Pensado para resolver preguntas en lenguaje natural.
Reglas que un asistente no debe olvidar:
- No toques tablas por sitio (
wp_N_*). Todo lo reportable está en las cuatro tablas centrales. - Filtrá por
site_slug/blog_idpara aislar una casa, no por nombre de tabla. es_retiro/es_convivencia, noproduct_type = '...'.- Vínculo persona↔inscripción por
attendee_user_id(↔wp_users.ID); cuando es NULL, caé aattendee_email/attendee_namedel snapshot (no unas por documento).user_ides el comprador, no el asistente. Los NULL se curan solos al completar documentos. - Pre-agrupá
wp_usermetaantes de unir, para no inflar los conteos. - NULL ≠ 0 en
product_typey en los importes. relationship/ códigos de tipo: filtrá por código, no por etiqueta.relationshipy_pa_centroson por sede (ver Vocabularios: tipo de actividad, relación y centro).