Sección 02 · El caso
Una venta, como punto de partida
Para que el recorrido sea concreto, fijamos un ejemplo y lo seguimos sin soltarlo.
El hecho del mundo real
El 3 de junio de 2026, en la agencia Andesmar Mendoza Centro, un pasajero compra un boleto Mendoza → Buenos Aires para viajar el 10 de junio. Paga $45.000 ARS en efectivo. Betterez registra la transacción TX-9F3A21 con el boleto BR-7781234.
El destino: un número de gestión
Esa venta, sumada a las otras del día, tiene que terminar contribuyendo +1 pasaje pagado y +$45.000 en la celda correcta del tablero "Ventas por agencia y día". El recorrido es cómo ese hecho llega, intacto y bien clasificado, hasta esa celda.
Betterez nos avisa
En el momento de la compra, Betterez emite un webhook: un POST con el detalle de la transacción, firmado criptográficamente.
¿Qué es un webhook?
Como recibir un mensaje en el instante en que pasa algo. No esperamos a un batch nocturno: el dato entra en segundos.
La firma HMAC
La cabecera X-Betterez-Signature prueba que el evento viene realmente de Betterez y no fue alterado. La API la valida antes de aceptar nada.
Guardar el original, antes de interpretar
La regla más sagrada del sistema: el JSON completo se persiste tal cual llegó antes de extraer una sola tabla de negocio. Es el principio raw-first.
Por qué primero el original
Si mañana cambia una regla de BI, reprocesamos desde este JSON. El dato histórico es intocable: nunca se pierde lo que Betterez dijo.
Idempotencia: nunca dos veces
Betterez reintenta los webhooks. Mismo payload → mismo hash → se reconoce como duplicado y se descarta. Un evento jamás se cuenta dos veces.
Trazabilidad total
Desde cualquier número del tablero se puede volver, vía raw_event_id, hasta este archivo exacto.
El evento se descompone en entidades
Recién ahora se interpreta el JSON. El IngestionService extrae cada parte a su tabla, con las claves de cruce que entretejen todo el modelo.
1 venta → N boletos
Acá el boleto es uno. Si fuera ida y vuelta, serían dos filas en tickets bajo la misma sale_id.
Copia fiel
No se "mejora" nada: betterez_status='paid' se guarda como vino. Las reglas de negocio llegan en la próxima capa.
Claves de cruce
TX-9F3A21 y BR-7781234 son los hilos que permitirán, luego, unir esta venta con su manifiesto.
public.sales mezcla ventas, reembolsos e ítems. Falta curarlo. Eso hace el ETL.Se reconstruye el hecho
Dos veces al día, un job recorre los boletos cambiados recientemente y reconstruye su fila en fact_ticket, aplicando reglas de negocio y uniendo dimensiones.
Lookback, no todo
El ETL no reconstruye 1,1 M de boletos cada vez: solo los que cambiaron en la ventana. Eficiente y siempre al día.
Reglas, una sola vez
"Pagado" = status='paid' se decide acá, no en cada reporte. Todos los tableros heredan la misma definición.
Se enriquece con dimensiones
La fila se une a dim_agency (etiqueta canónica), dim_route, dim_date y, por ticket_number, a su manifest_id.
bi_rebuild.fact_ticket| ticket_number | is_paid | ticket_amount | currency | agency_label | route_id | sale_date |
|---|---|---|---|---|---|---|
BR-7781234 | true | 45000.00 | ARS | Andesmar Mendoza Centro | Mendoza_Buenos Aires | 2026-06-03 |
De la fila a la métrica
Un KPI no es una columna: es una agregación con un filtro acordado. Nuestro boleto aporta a dos métricas del tablero de ventas.
is_paid = true (qué cuenta como venta efectiva) y currency = 'ARS' (no se mezclan monedas porque no se convierten). Omitir cualquiera de los dos produce un número que no se puede comparar.El número en el tablero
MicroStrategy lee fact_ticket y sus dimensiones, y presenta las métricas cruzadas por los atributos que pide negocio. Nuestro boleto ya es parte de una celda.
Ventas pagadas — 03/06/2026 (ARS)
| Agencia | Pasajes | Facturación |
|---|---|---|
| Andesmar Mendoza Centro | 128 | $5.760.000 |
| Andesmar Córdoba Terminal | 96 | $4.310.000 |
| Andesmar San Juan | 74 | $3.180.000 |
Dentro de esos 128 pasajes y $5.760.000 de Mendoza Centro está, indistinguible y correcto, nuestro BR-7781234.
Atributos = dimensiones
"Por agencia" sale de dim_agency; "por día" de dim_date; "por ruta" de dim_route.
Métricas = medidas + filtro
"Pasajes" = count con is_paid; "Facturación" = sum(ticket_amount).
Mismo dato, una verdad
Finanzas, operaciones y dirección leen esta misma fila: nadie recalcula a su manera.
Sección 09 · La garantía
¿Y si el webhook nunca llega?
Toda la historia anterior asume que el evento llegó. ¿Qué pasa si Betterez no lo entrega? Para eso existe la cuarta capa: la consulta activa que cierra los huecos.
Paso 7 · La red de seguridad entra en acción
Un job diario consulta la API de Betterez (GET /transactions) y compara contra public.sales. Si TX-9F3A21 está en la API pero falta en OLTP, lo reinyecta respetando raw-first: vuelve a guardar el JSON en GCS y reanuda el recorrido desde la parada 2.
El mismo recorrido, otra puerta de entrada
La pieza clave es que el safety-net no es un atajo: reinyecta por el mismo camino (raw-first → OLTP → ETL → métrica). Por eso el boleto recuperado es idéntico a uno que llegó por webhook. La idempotencia evita que se duplique si el webhook aparece tarde.
Sección 10 · Cierre
Un hecho, siete paradas, un número confiable
Cada celda del tablero es el final de un viaje como este. Entenderlo es saber por qué se puede confiar en el número — y dónde mirar si algo no cuadra.
| Parada | Capa | Qué garantiza |
|---|---|---|
| 1 · Webhook | Captura | Llega en segundos, firmado |
| 2 · Raw-first | Raw | Original intacto, sin duplicados |
| 3 · OLTP | public | Copia fiel, consultable |
| 4 · ETL | bi_rebuild | Reglas y dimensiones, una sola vez |
| 5 · Métrica | BI | Agregación con filtro acordado |
| 6 · Tablero | MSTR | Una verdad para todos |
| 7 · Safety-net | Recuperación | Lo que falte, se reinyecta |
Andesmar API · Recorrido del dato · Junio 2026