is_paid = true) — KPI oficial BI (corte 2026-06-13)is_paid=false)refund_details (webhook refund.created), pero el campo status del boleto a veces no se actualiza. El ETL cruza ambas fuentes.is_paid. Cruce con Betterez web → is_paid_including_partial_cancel. Cancelados → effective_ticket_status.Para quién es este documento
Presentación en vivo + referencia standalone para consultar después
Vocabulario de negocio
Términos que aparecen en Betterez, en los reportes y en esta plataforma
- Transacción / venta
- Operación de compra completa en Betterez. Puede incluir uno o varios boletos, seguros y otros ítems. En nuestra base: tabla
sales.
- Boleto / pasaje / ticket
- Un pasaje individual con número propio (ej.
ZK6M6Q). Es la unidad de análisis del tablero BI. En nuestra base: tablatickets, hechofact_ticket.
- Cancelación
- El pasajero devuelve el boleto. Betterez calcula penalidad y monto a reembolsar en 3 pasos (ver § ciclo de vida). Webhook:
refund.createdcontype = cancel.
- Cancelación parcial
- Cancelación donde Andesmar retiene parte del importe como penalidad. El pasajero recibe menos de lo que pagó. Betterez web sí cuenta ese boleto; el tablero BI neto no.
- Cancelación total
- Reembolso completo (≥ importe del boleto). Ningún sistema lo cuenta como venta vigente.
- Cambio / reemisión
- Modificación de fecha, horario o servicio. Puede generar un boleto nuevo y marcar el anterior como
changed. Webhook:ticket.updatedy a vecesrefund.createdcontype = change.
- Devolución (refund)
- Reembolso distinto de cancelación estándar. Tipo
refundenrefund_details.
- Penalidad / cargo por cancelación
- Monto que Andesmar retiene. En Betterez API: campo
penaltydel cancel set. En BI:cancel_penalty_amount = ticket_amount − reembolso.
- OLTP
- Capa operacional: copia fiel de lo que Betterez nos envía (PostgreSQL GCP, schema
public).
- BI / bi_rebuild
- Capa analítica curada para reportes y MicroStrategy. Una fila = un boleto con flags de negocio calculados.
- ETL
- Proceso diario que transforma OLTP → BI. Recalcula estados mirando
refund_details, no solo el status crudo.
- Grano
- Qué representa una fila. En
fact_ticket: 1 fila = 1 boleto. Enfact_sale: 1 fila = 1 venta.
Qué es Betterez y qué nos manda
La plataforma de ticketing que opera las ventas de Andesmar
| Acción en Betterez | Webhook | Qué guardamos | Impacto en BI |
|---|---|---|---|
| Compra de boleto | ticket.created |
Fila en tickets con status = paid |
Venta neta |
| Cancelación ejecutada | refund.created (type = cancel) + a veces ticket.updated |
Fila en refund_details; tickets.status puede quedar en paid |
Excluido de is_paid |
| Cambio de boleto | ticket.updated + a veces refund.created (type = change) |
Status changed; boleto nuevo en misma u otra venta |
No cuenta como venta vigente |
| Devolución (refund) | refund.created (type = refund) |
Fila en refund_details |
effective = refunded |
| Pago pendiente / expirado | ticket.updated |
waiting for payment, payment expired |
No operable |
refund.created incluye campos como penalty, amountToRefund, refunded y type.
Esos datos son la fuente de verdad para saber si hubo cancelación parcial; el campo status del ticket no siempre refleja el resultado final.
Ciclo de vida de un boleto
Desde la venta hasta la cancelación o el cambio
- Consultar ítems cancelables — qué boletos se pueden devolver y bajo qué reglas de canal.
- Crear cancel set — preview con penalidad, fees y monto a reembolsar por medio de pago.
- Ejecutar cancelación — procesa el reembolso, actualiza ítems y emite webhooks.
- Consultar datos del cambio (
trip-change-info). - Buscar inventario disponible para el nuevo viaje.
- Crear carrito con el cambio (puede ser “zero pay” si el precio es igual).
refund_details como la nota de crédito.
A veces el recibo original sigue diciendo “pagado” aunque ya exista la nota de crédito.
Nuestro tablero BI lee ambos documentos antes de decidir si el boleto sigue contando como venta.
Tablas involucradas y sus relaciones
Modelo conceptual: qué es cada tabla, qué representa una fila y cómo se conectan
paid, confirmed…). Puede incluir varios boletos y productos ancillaries.transaction.* → guardamos en public.salestransaction_id, sale_date, total, currency, agency_code, status (mapeado: paid→confirmed).tickets.sale_id)total), impuestos, fecha de viaje, status crudo de Betterez (paid, cancelled, changed…).type (cancel/change/refund), refunded, amount, penalty, ticket_number.refund.created · Fuente de verdad para cancelacionesis_paid, effective_ticket_status, flags de cancel parcial/total, montos de refund, dimensiones (agencia, canal, fecha).has_nacional, etc.) y columnas básicas de estado. No trae flags finos como is_partial_cancel.ticket_id si se necesitan ambosdim_agency (punto de venta),
dim_channel (web, agencia, backoffice),
dim_date / dim_mes_anio (fecha de venta),
dim_route, dim_station (origen/destino).
Permiten cortar los KPI por agencia, mes, canal, etc.
raw_events.
Si hay duda sobre un boleto, siempre podemos auditar qué mandó Betterez originalmente.
Tres niveles de lectura del estado
De la vista ejecutiva al detalle de auditoría
is_paid vs is_paid_including_partial_canceleffective_ticket_statusis_partial_cancel, montos, ticket_statuseffective_ticket_status = 'cancelled'effective_ticket_status = 'refunded'effective_ticket_status = 'changed'tickets.status (paid, changed, waiting for payment…) o unknownrefund_details de forma más confiable que en tickets.status.
Priorizamos el evento postventa sobre el campo que a veces no se actualiza.
El camino del dato: de Betterez al reporte
Tres capas, dos saltos
refunds.type
transaction.status
de negocio
ticket_status, refund_type, refund_amount).
Llegan intactas, pero pueden estar mal en origen.
is_paid, effective_ticket_status, is_partial_cancel…).
Las arma el ETL mirando refund_details.
Columnas que vienen directo de Betterez
Copias del OLTP — útiles para auditoría, no para KPI ejecutivos
| Columna en BI | Tabla OLTP origen | Origen en Betterez | Uso recomendado |
|---|---|---|---|
ticket_status |
public.tickets.status |
tickets[].status del webhookValores: paid cancelled changed waiting for payment payment expired |
Solo auditoría Puede decir “paid” aunque el boleto ya esté cancelado |
current_status |
public.tickets.status |
Espejo sincronizado puntualmente | No usar en KPI Puede quedar desactualizado |
refund_type |
public.refund_details.type |
refunds[].type — cancel, change, refund |
Complementario Agrupado por coma si hay varios eventos |
refund_amount |
public.refund_details |
Suma de refunded / amount del webhook |
Métricas de monto |
fact_sale.status |
public.sales.status |
data.transaction.status (mapeado: paid→confirmed) |
Nivel venta, no boleto |
dim_manifest.manifest_status |
public.manifest_entries |
Status del manifiesto (re-fetch API) | Dimensión operativa |
Columnas calculadas en el ETL
No existen en Betterez — las construye nuestro proceso diario con reglas de negocio acordadas
| Valor | Cuando aparece | Uso tipico |
|---|---|---|
| paid | Sin evento cancel/refund/change en refund_details |
Venta neta standard |
| cancelled | Hay type = cancel en refund_details (aunque ticket_status diga paid) |
Hoja de cancelados |
| refunded | Hay type = refund en refund_details |
Hoja de devoluciones |
| changed | type = change en refunds, o ticket_status = changed sin refund |
Hoja de cambios/reemision |
| unknown | Estado no reconocido | Solo auditoria |
| Flag | Logica resumida | Para que sirve |
|---|---|---|
is_paid |
ticket_status = 'paid' Y sin cancel ni refund en refund_details |
KPI oficial BI Tablero y manifiesto |
is_paid_including_partial_cancel |
is_paid = true O is_partial_cancel = true |
Cruce con Betterez web Ranking de agencias |
is_partial_cancel |
Cancel en refunds con reembolso menor que el importe del boleto | El cliente pago una penalidad — Betterez lo cuenta, BI neto no |
is_full_cancel |
Cancel y reembolso total (≥ importe boleto) | Cancelacion sin costo para el pasajero |
is_cancelled |
ticket_status = 'cancelled' O cancel en refunds |
Flag compuesto general de cancelacion |
is_refunded |
ticket_status = 'refunded' O refund en refunds |
Flag compuesto de devolucion |
has_refund_record |
Existe al menos una fila en refund_details |
Detectar si el boleto tuvo algun evento postventa |
| Columna | Calculo | Cuando tiene valor |
|---|---|---|
refund_amount |
Suma total reembolsada al pasajero | Boletos con cualquier refund/cancel |
cancel_penalty_amount |
ticket_amount - cancel_refund_amount |
Solo en cancelaciones parciales — es el ingreso retenido por Andesmar |
currency al sumar montos. Nunca mezclar ARS con otras monedas en la misma metrica.
Aplicar cap de $500.000 ARS para KPIs ejecutivos (evita outliers historicos).
| Columna | Por que evitarla |
|---|---|
is_final_paid |
Solo mira ticket_status = 'paid' sin revisar refund_details. Sobrecontabiliza boletos cancelados. |
current_status |
Espejo OLTP de sync puntual. Igual que ticket_status, puede estar desactualizado. |
ticket_status (como KPI) |
Valor crudo Betterez. Betterez no lo actualiza cuando hay cancelacion con penalidad — puede quedar paid cuando el boleto ya esta cancelado. |
"Directo de Betterez" no significa "confiable"
El campo status de Betterez puede decir "paid" cuando el boleto ya esta cancelado
tickets.status de forma confiable cuando hay una cancelacion o devolucion.
El evento de cancelacion queda en refund_details pero el status del boleto puede seguir figurando como paid.
Por eso el ETL prioriza refund_details sobre el status crudo.
ticket_status = 'paid' pero effective_ticket_status = 'cancelled'ticket_status = 'paid' y cancelacion parcial activaticket_status dice "paid" pero is_paid es falseis_paid = true (venta neta real) — corte 2026-06-13
Si usaras solo ticket_status = 'paid' para contar boletos vendidos, estarias contando ~3.717 boletos de mas que en realidad estan cancelados.
El campo is_paid corrige ese problema mirando refund_details.
Dicho de otra forma: ticket_status es el dato crudo de Betterez, pero
is_paid es la interpretacion correcta de ese dato.
Cancelación parcial: el corazón de la diferencia BI vs Betterez
Cuando el pasajero paga penalidad y Betterez sigue contando el boleto
| Tipo de cancelación | Condición | Betterez web | is_paid (BI neto) | is_paid_including_partial_cancel |
|---|---|---|---|---|
| Sin cancelación | No hay evento en refund_details |
Cuenta | true | true |
| Parcial | Reembolso < importe del boleto (penalidad retenida) | Cuenta | false | true |
| Total | Reembolso ≥ importe del boleto | No cuenta | false | false |
penalty, penaltyReason).
El monto reembolsado llega en refunded / amountToRefund del webhook refund.created.
cancel_penalty_amount = ticket_amount − reembolso.
Ese ingreso retenido es revenue para Andesmar aunque el boleto ya no sea una “venta vigente” en el sentido neto.
Que columna usar segun la pregunta
Guia rapida para reportes, dashboards y reconciliacion
is_paid = trueis_paid_including_partial_cancel = trueeffective_ticket_status = 'cancelled'ticket_status = 'cancelled' solo.effective_ticket_status = 'changed'refund_details.effective_ticket_status = 'refunded'is_refunded = true.is_partial_cancel = truecancel_penalty_amount.SUM(ticket_amount)currency = 'ARS'. Cap $500k para KPI ejecutivo.ticket_statusTabla de verdad: combinaciones frecuentes en produccion
Como se leen los flags juntos en casos reales
| ticket_status | effective_ticket_status | is_paid | is_partial_cancel | is_full_cancel | is_paid_incl_parcial | Interpretacion |
|---|---|---|---|---|---|---|
| paid | paid | true | false | false | true | Venta neta standard Ambos sistemas coinciden. |
| paid | cancelled | false | true | false | true | Cancel parcial Betterez lo cuenta, BI neto no. La diferencia entre ambos sistemas. |
| paid | cancelled | false | false | true | false | Cancel total Betterez no actualizo el status. Ambos sistemas excluyen. |
| changed | changed | false | false | false | false | Cambio de boleto Ni el BI ni Betterez lo cuentan como venta vigente. |
| cancelled | cancelled | false | false | false | false | Cancel explicito OLTP Betterez actualizo el status correctamente. |
ticket_status = 'paid' pero hay una cancelacion parcial en refund_details,
Betterez lo cuenta como boleto vigente (cobro penalidad), pero el tablero BI oficial no (is_paid = false).
Esto genera diferencias de 2-20 boletos por agencia/mes que son reglas de negocio, no errores.
Ejemplo real: agencia L78, abril 2026
Como se traduce la diferencia entre columnas a numeros concretos
is_paid = true |
10.933 |
is_paid_including_partial_cancel |
10.935 = Betterez web |
is_partial_cancel = true |
2 |
is_full_cancel (con status=paid) |
12 |
| N° boleto | Importe | Reembolso | Penalidad |
|---|---|---|---|
ZK6M6Q |
$16.200 | $14.580 | $1.620 |
GDTSAT |
$19.000 | $17.099 | $1.901 |
En abril 2026, la agencia L78 vendió 10.933 boletos netos según el tablero BI. Betterez web mostraba 10.935. La diferencia son exactamente 2 cancelaciones parciales: pasajeros que devolvieron el boleto pero pagaron penalidad. Betterez los sigue contando; nuestro KPI neto no. No hay error de carga — es la regla acordada para medir venta real vs alinear con el ranking de agencias.
Cambios de boleto y sucesores
Cómo enlazamos un boleto changed con su reemplazo
refund_details.type = change en la venta original y aparece un boleto paid
candidato en otra transacción (misma persona y misma ruta, dentro de la ventana de tiempo).Columna:
successor_match_method = 'heuristic_unique'
changed y su reemplazo paid quedan en la misma venta
(sale_id), con misma ruta. Suele ocurrir sin registro de refund.Columna:
successor_match_method = 'same_transaction'
changedFromTicketId
desde API de Betterez para confirmar el reemplazo correcto.Columna:
successor_match_method = 'api'
successor_ticket_number y successor_transaction_id permiten hacer drill-down de cambios en dashboards.
Si quedan NULL, ese boleto changed no tiene reemplazo identificado o no tiene reemplazo real
(por ejemplo, cambio parcial de tramo).
Como modelar esto en MicroStrategy
Que atributos y metricas configurar, y con que tabla
| Atributo MSTR | Columna |
|---|---|
| Estado efectivo | effective_ticket_status |
| Pagado neto BI | is_paid |
| Pagado estilo Betterez | is_paid_including_partial_cancel |
| Cancel parcial | is_partial_cancel |
| Cancel total | is_full_cancel |
| Status OLTP (auditoria) | ticket_status — solo usuarios avanzados |
| Necesidad | Tabla |
|---|---|
| Estados finos, cancel parcial, montos refund | fact_ticket |
Etiquetas manifiesto (has_*), vista wide MSTR |
fact_ticket_mstr |
| Ambas necesidades | JOIN por ticket_id |
fact_ticket_mstr solo trae is_paid y effective_ticket_status.
Para is_partial_cancel u otros flags finos, usar fact_ticket.
Count(ticket_id)
donde is_paid = True
y currency = 'ARS'
Count(ticket_id)
donde is_paid_including_partial_cancel = True
y currency = 'ARS'
Count(ticket_id)
donde effective_ticket_status = 'cancelled'
Sum(ticket_amount)
donde is_paid = True
y currency = 'ARS'
y product_type = 'reservation'
Preguntas frecuentes (negocio)
Respuestas directas para la reunión y para consultar después
refund_details) y a veces no actualiza el campo status del boleto. Si contáramos solo “status = paid”, incluiríamos ~3.717 boletos cancelados de más. Construimos reglas propias para dar un número neto confiable.is_paid) vs contar también cancelaciones parciales con penalidad (is_paid_including_partial_cancel). Son reglas de negocio distintas, no fallos de carga de datos.is_paid = true para boletos vendidos netos y el mismo filtro para revenue. Es el KPI oficial acordado con operaciones y manifiesto.cancel_penalty_amount. El pasajero recuperó menos de lo que pagó; la diferencia es ingreso para la empresa aunque el boleto no cuente como venta vigente en el KPI neto.changed y no cuenta como venta vigente. Si hay reemplazo, las columnas successor_* enlazan al boleto nuevo con método heuristic_unique, same_transaction o api. Si quedan en NULL, el reemplazo no pudo identificarse o el cambio no generó un boleto sucesor directo.bi_rebuild. Para comparaciones históricas, usar los informes de reconciliación específicos.Checklist antes de publicar un reporte con boletos
Verificacion rapida para evitar los errores mas comunes
- El conteo de boletos usa
is_paid(tablero BI) ois_paid_including_partial_cancel(cruce Betterez) segun el KPI - El revenue usa el mismo filtro que el conteo de boletos
- Hay filtro de
currencyen todas las metricas de dinero - Solo interurbano:
product_type = 'reservation'aplicado - Los cancelados usan
effective_ticket_status = 'cancelled'y no soloticket_status - No se usa
is_final_paidnicurrent_statusen KPI ejecutivos - Si hay montos ARS: cap de $500.000 aplicado para outliers historicos
- Los estados finos (cancel parcial, refund) vienen de
fact_ticket, no solo defact_ticket_mstr - El cubo MSTR fue refrescado si se deployaron columnas nuevas recientemente
Imprimible / consulta rápida post-presentación
| Pregunta | Filtro correcto | No usar |
|---|---|---|
| Boletos vendidos (tablero oficial BI) | is_paid = true |
ticket_status, is_final_paid |
| Boletos segun Betterez web / agencias | is_paid_including_partial_cancel = true |
Solo effective_ticket_status = 'paid' |
| Revenue de boletos | Mismo que conteo + SUM(ticket_amount) + currency |
Mezclar monedas sin filtro |
| Cancelados del periodo | effective_ticket_status = 'cancelled' |
Solo ticket_status = 'cancelled' |
| Cambios / reemision | effective_ticket_status = 'changed' |
— |
| Penalidades retenidas | is_partial_cancel = true + SUM(cancel_penalty_amount) |
— |
| Auditoria payload crudo | ticket_status |
— (solo diagnostico tecnico) |