Una moneda de reporting a partir de muchas monedas de tienda
Una tienda que cambia sus precios a la moneda local del visitante es buena para la conversión y terrible para el reporting. El mismo producto se vende por 99 en tres sitios y aterriza en tus plataformas de anuncios como tres cifras distintas. Beaconry reescribe value y cada items[].price a una moneda objetivo antes de que el evento se abra en abanico, usando los tipos diarios del Banco Central Europeo, conservando la moneda original en el evento y recurriendo a una caché duradera cuando el feed del BCE no responde.
El problema: un producto, tres cifras
Multi-moneda es hoy lo mínimo imprescindible para cualquier tienda que venda a través de fronteras. Una tienda WooCommerce con un plugin de cambio de moneda, o un storefront SureCart que geolocaliza al visitante, le muestra EUR a un comprador alemán, GBP a uno británico y USD a uno estadounidense. Cada checkout dispara un evento purchase con el precio en la moneda en la que pagó ese comprador.
Eso es correcto para el cliente y está roto para el reporting. Tres compras completadas del mismo producto de 99 llegan a tus plataformas de anuncios como:
- Comprador EUR:
value: 99, currency: "EUR" - Comprador GBP:
value: 99, currency: "GBP" - Comprador USD:
value: 99, currency: "USD"
Algunas plataformas convierten internamente para mostrar la cifra, otras no, y las que lo hacen usan su propio tipo en su propio momento, que tú no puedes ver ni auditar. Peor aún, cuando una sola campaña atrae compradores a través de las tres monedas, al optimizador de la plataforma se le dice que una conversión vale "99" en cada caso, aunque 99 GBP equivalgan a aproximadamente 1,18 veces el valor de 99 EUR y 99 USD a aproximadamente 0,93 veces. Tu columna de ROAS mezcla tres escalas de valor. La puja basada en valor optimiza contra ruido.
La solución es elegir una moneda de reporting y convertir cada campo monetario a ella antes de que el evento salga de tu servidor. Eso es exactamente lo que hace la función Multi-Currency de Beaconry, y como Beaconry despacha del lado del servidor, la conversión ocurre en un único lugar que alimenta cada canal de forma idéntica.
Dónde engancha: un filtro, por delante de cada canal
Beaconry abre en abanico un único evento canónico hacia hasta diez canales del lado del servidor (GA4, Meta, TikTok, LinkedIn, Google Ads, Microsoft Ads, Pinterest, X Ads, Snapchat, Reddit). El fan-out vive en BCNR_Forwarder::dispatch(), y justo antes de dividir el evento por canal ejecuta un filtro:
apply_filters( 'bcnr_pre_dispatch_event', $event )La clase Multi-Currency registra un único listener en ese filtro:
// class-bcnr-currency.php
add_filter( 'bcnr_pre_dispatch_event', [ self::class, 'normalize_event' ], 10, 1 );Esta ubicación es el quid de la cuestión. La conversión de moneda ocurre una vez, antes de que el evento se copie en diez payloads por canal. No hay ningún código de moneda por canal, ningún riesgo de que Meta reciba EUR mientras Google Ads recibe el USD original. Cada método dispatch_*() aguas abajo lee el mismo value, currency e items[].price ya normalizados. Añade un undécimo canal mañana y heredará el valor normalizado gratis, sin ningún código de moneda que escribir.
Qué se reescribe y qué se deja en paz
El listener lee el ajuste currency_target del cliente (un código ISO de 3 letras, o el override de la constante BCNR_CURRENCY_TARGET) y reescribe tres cosas en un evento monetario:
$event['value'], multiplicado por el tipo de origen a objetivo.- Cada
$event['items'][n]['price'], la misma multiplicación por línea de pedido. Los eventos del funnel llevan arraysitems[]completos (item_id, item_name, price, quantity), así que el precio por ítem también tiene que moverse o tu ingreso a nivel de ítem en GA4 no cuadraría con el total del evento. $event['currency'], fijado al objetivo para que cada canal aguas abajo reporte el mismo código.
Es deliberadamente conservador sobre cuándo no hace nada. El evento pasa completamente sin cambios en todos estos casos:
- La función está desactivada (
currency_targetvacío). Estado por defecto. - El evento no tiene
value(unpage_viewo unview_item_listsin precio no es dinero y no se toca). - Falta la moneda de origen, o ya coincide con el objetivo. No-op, salvo que a un evento con valor pero con moneda vacía se le estampa el código objetivo para que un canal aguas abajo no recurra a su propio "EUR" por defecto.
- La moneda de origen no está en la tabla del BCE (un código exótico), en cuyo caso Beaconry deja el valor en paz en lugar de adivinar un tipo.
- La tabla FX está vacía tras un pull fallido sin fallback en caché. Paso directo más un aviso de administración, nunca un crash.
"Dejarlo en paz en lugar de adivinar" es la regla rectora. Un tipo erróneo corrompe en silencio el reporting de ingresos de una forma muy difícil de notar; un valor sin convertir está al menos obviamente en su moneda original y se explica por sí solo.
De dónde vienen los tipos: BCE, basado en EUR
La fuente de los tipos es el feed de referencia diario del Banco Central Europeo, el mismo XML sobre el que media Europa financiera funciona discretamente:
https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xmlEs público, gratuito, no requiere clave de API y publica unas 30 monedas principales una vez por día hábil hacia las 16:00 CET. Beaconry parsea los nodos <Cube currency="USD" rate="1.08"/> a una tabla plana de [ 'USD' => 1.08, 'GBP' => 0.84, ... ].
Un detalle importa para la corrección: el BCE publica solo tipos EUR a X. No hay ninguna cifra directa USD a GBP en el feed. Así que una conversión USD a GBP puentea a través de EUR:
// USD -> EUR is 1 / rate[USD], then EUR -> GBP is x rate[GBP]
$to_eur = ( $from === 'EUR' ) ? 1.0 : 1.0 / $rates[ $from ];
$from_eur = ( $to === 'EUR' ) ? 1.0 : $rates[ $to ];
return $to_eur * $from_eur;Si cualquiera de los dos lados es desconocido, la función devuelve 0.0, que quien la llama lee como "no convertir", y el evento pasa intacto. Los importes convertidos se redondean a dos decimales con PHP_ROUND_HALF_UP para que un valor como 1.005 redondee de forma predecible en lugar de depender de la suerte del coma flotante.
La propia moneda objetivo se valida contra la tabla en vivo antes de que se convierta nada. EUR siempre se acepta (es la base del BCE). Cualquier otra cosa tiene que existir realmente en el conjunto de tipos descargado, de modo que un currency_target mal tecleado como "USB" se resuelve a vacío y toda la función hace un no-op en lugar de romper cada evento de compra.
El fallback duradero: las caídas del BCE no detienen el dispatch
Un seguimiento de conversiones que se rompe cuando un feed aguas arriba tiene una tarde mala es peor que ninguna normalización, porque se lleva consigo todo el evento de compra. Así que la tabla de tipos tiene dos capas de caché y un suelo duro de paso directo.
- Caché transient (TTL de 24 h). El camino feliz. El feed del BCE se descarga como mucho una vez al día mediante el cron
bcnr_currency_refresh, se parsea y se almacena en el transientbcnr_fx_rates. Cada evento durante el día lo lee con coste de red cero. - Fallback duradero en option. La misma tabla parseada también se escribe en una opción de WP con
autoload=false,bcnr_fx_rates_fallback. Los transients pueden ser desalojados antes de tiempo por una caché de objetos agresiva; esta opción no. Si el transient ha desaparecido y el pull del BCE falla, Beaconry sirve los últimos tipos buenos desde esta opción y reescribe el transient durante una hora para no martillear al BCE en cada evento durante una caída. - Suelo de tabla vacía. Solo si no hay transient, ni fallback duradero, y el pull en vivo falla (una instalación recién creada que nunca ha llegado ni una vez al BCE) la función se rinde. Registra un mensaje legible por humanos en
bcnr_fx_last_error, lo muestra como aviso de administración y deja pasar cada evento en su moneda original. El dispatch sigue funcionando. Nadie pierde una conversión.
El propio pull HTTP es defensivo: un timeout de 5 segundos, verificación SSL activada, una respuesta no-200 o un body vacío tratados como fallo, y el XML parseado con LIBXML_NONET de modo que ni siquiera la fuente de confianza del BCE pueda traer una entidad externa. Cualquier error de parseo devuelve una tabla vacía, que va directa a la lógica de fallback de arriba.
La moneda original nunca se descarta
Normalizar in situ destruiría información. Si un comprador alemán pagó 99 EUR y tú solo almacenas "92,59 USD", has perdido el hecho de que la transacción real fue en EUR. Beaconry conserva ambas. Tras una conversión anota el evento con dos campos internos:
$event['_bcnr_fx_source'] = $source; // e.g. 'USD'
$event['_bcnr_fx_rate'] = $rate; // e.g. 0.9259Estos dos campos son el rastro de auditoría. La tabla de eventos recientes del Live-Dashboard puede renderizar "92,59 EUR (USD)" en lugar de una cifra pelada, y el log operativo puede mostrar el tipo exacto que se aplicó a un evento dado, de modo que una discrepancia de ingresos es depurable a posteriori en lugar de una caja negra.
Crucialmente, el prefijo _bcnr_ significa que estos son campos solo internos. Cada método dispatch_*() reenvía únicamente las claves documentadas que su canal espera (el conjunto content_* de Meta, el conjunto items[] de GA4, etcétera). Las anotaciones _bcnr_fx_source y _bcnr_fx_rate se leen dentro de WordPress para mostrarlas y registrarlas, y se eliminan antes de que nada salga por el cable. Meta nunca las ve, GA4 nunca las ve, existen puramente para que un humano pueda responder más tarde "¿qué tipo aplicamos al pedido 4815?".
Qué arregla realmente entre Meta, Google y GA4
En concreto, con currency_target fijado a EUR y una campaña que trajo a los tres compradores del principio de este artículo:
- Meta recibe tres eventos
Purchaseque leen todosvalue: ~92.59, currency: "EUR"(el de GBP en su propio equivalente en EUR, el de EUR sin cambios). Los valores por ítem decontent_*cuadran con el total del evento porqueitems[].pricese movió con él. La puja basada en valor y el ROAS en el Ads Manager ahora comparan lo comparable. - Google Ads obtiene el mismo valor normalizado a través de la Conversions API, así que la columna de valor de conversión en Google Ads está en una moneda, en lugar de mezclar en silencio tres que Google luego reconvierte según su propio calendario.
- GA4 registra cada
purchaseen EUR, lo que significa que los informes de Monetización suman correctamente sin que configures una conversión de moneda en los propios ajustes de GA4 (y sin el redondeo que GA4 aplica cuando convierte por ti). El ingreso a nivel de ítem en GA4 coincide con el ingreso del evento porque ambos se convirtieron con el mismo tipo en el mismo instante.
La propiedad compartida entre los tres es que el valor se convirtió una vez, del lado del servidor, con un tipo que puedes ver, antes de que ninguna plataforma se involucrara. Ya no confías en tres conversiones de caja negra distintas; estás enviando a tres plataformas una cifra consistente y guardando el recibo.
Activarlo
Desactivado por defecto. Actívalo desde la tarjeta Multi-Currency en la pestaña Advanced, o fija la constante en wp-config.php para un override de usuario avanzado:
define( 'BCNR_CURRENCY_TARGET', 'EUR' );Elige la moneda en la que realmente reportas y pujas, que suele ser tu moneda contable, no necesariamente tu moneda de checkout más común. Después de eso no hay nada por canal que configurar. El cron diario mantiene la tabla de tipos caliente, el fallback duradero cubre los días malos del BCE, y cada evento monetario de WooCommerce, EDD o SureCart aterriza en tus plataformas de anuncios en una sola moneda.
Para verificar: completa una compra de prueba en una moneda que no sea la objetivo y revisa la tabla de eventos recientes del Live-Dashboard. Un evento normalizado renderiza el valor objetivo con la moneda de origen entre paréntesis, por ejemplo "92,59 EUR (USD)". Si ves la moneda original sin cambios, revisa la zona de avisos de administración por si está el mensaje de fallback del BCE, y confirma que tu currency_target es un código que realmente aparece en el feed del BCE.
Para llevar
Las tiendas multi-moneda no tienen un problema de precios, tienen un problema de reporting: el mismo producto llega a tus plataformas de anuncios como varias cifras incompatibles. Beaconry lo resuelve en el único lugar que alimenta cada canal, el filtro bcnr_pre_dispatch_event, convirtiendo value y cada items[].price a una moneda objetivo con los tipos diarios del BCE, antes del fan-out. Conserva la moneda original y el tipo aplicado en el evento para que las cuentas sean auditables, puentea los pares de divisas a través de EUR porque es todo lo que el BCE publica, y tiene una caché de dos capas con un suelo duro de paso directo de modo que una caída del BCE te cuesta precisión por un día, nunca una sola conversión rastreada. Una moneda de reporting, un tipo que puedes ver, ninguna conversión de caja negra en tres plataformas distintas.