Andesmar BI · Documentación interna

Devoluciones: como las modelamos

Cancelaciones, reembolsos y cambios de boleto — de la caja al tablero, explicado para negocio con el detalle tecnico debajo.

Junio 2026 OLTP: sales + refund_details BI: bi_rebuild.fact_ticket
💰
La regla de oro: Betterez no borra ni modifica la venta original. Cuando hay una devolucion, deja el boleto como estaba y agrega un evento nuevo: una fila R- en caja (monto negativo) y un registro en refund_details con el detalle. Nosotros copiamos esa logica tal cual — asi se puede auditar que se vendio y que se devolvio.
1

Tres capas de datos

De afuera hacia adentro: del evento crudo al tablero

Origen
Betterez
Webhooks / API
JSON completo
raw-first
Capa operativa
public.*
sales · tickets · refund_details
ETL diario
logica de negocio
Capa BI
fact_ticket
1 fila = 1 boleto
Capa Tablas clave Para que sirve
Raw raw_events + GCS Auditoria, reprocesar si algo falla. JSON completo de Betterez.
OLTP sales, tickets, refund_details Caja, reconciliacion, operacion diaria. Fuente del ETL.
BI bi_rebuild.fact_ticket MicroStrategy, KPIs, tableros. Estado curado por boleto.
2

Caja: la venta no se toca, se suma una fila R-

Modelo de libro contable validado con Andesmar

Cada devolucion genera una nueva fila en sales con prefijo R- y monto negativo. El neto de caja es simplemente SUM(total) de todas las filas.
Transaccion
Tipo
Monto
TX-ABC123 — venta boleto ZK6M6Q
venta
+$16.200
R-ZK6M6Q — cancelacion con penalidad
R-
−$14.580
Neto de caja
+$1.620
Campos de vinculacion
CampoQué guarda
original_transaction_id La venta que se devolvio
refund_id Codigo Betterez (ej. R-KNJ8CZ)
ticket_number El boleto afectado
sale_type refund · is_refund = true
Detalle en refund_details

La fila R- en sales maneja el balance de caja. El detalle fino vive en refund_details (relacion 1:1):

penalty / display_penalty type amount / refunded taxes_detail shift_location
3

Tres tipos de devolucion

No son lo mismo para negocio ni para reportes

Cancelación

refund_details.type = cancel

El pasajero no viaja. Puede haber penalidad retenida por Andesmar. Betterez saca el boleto del manifiesto.

effective = cancelled

Reembolso

refund_details.type = refund

Devolucion de dinero con flujo contable distinto a la cancelacion. Tambien genera fila R- negativa.

effective = refunded

Cambio de boleto

refund_details.type = change

Se anula el boleto viejo y se emite uno nuevo (misma u otra venta). A veces no hay fila en refunds — ver seccion 6.

effective = changed
Prioridad en el ETL (primera coincidencia gana)
1. cancel
2. refund
3. change
4. status OLTP

Resultado: columna effective_ticket_status en fact_ticket.

4

El truco que confunde a todos

El boleto puede seguir diciendo “paid” despues de cancelar

En Betterez, cuando cancelas un boleto, el campo tickets.status muchas veces sigue en paid. La senal real de la baja esta en refund_details, no en el status del ticket.
LO QUE VE BETTEREZ EN EL BOLETO
paid

Status crudo — puede estar desactualizado

LO QUE LEE NUESTRO ETL
cancelled

Desde refund_details.type = cancel

Columna BI Pregunta que responde Uso
ticket_status Qué dejó Betterez en el boleto Solo auditoria
effective_ticket_status Estado real para reportes Hojas por estado
is_paid Cuenta como pasaje vendido (tablero oficial)? KPI BI
is_paid_including_partial_cancel Cuenta como Betterez web por agencia? Cruce Betterez
refund_amount Cuanto se devolvio al pasajero Metricas de monto
cancel_penalty_amount Cuanto retuvo Andesmar (solo parciales) Ingreso por penalidad
5

Cancelación parcial vs total

El caso de negocio que explica las diferencias con Betterez web

~3.036
cancelaciones parciales globales
is_partial_cancel = true
~36.140
cancelados totales
effective = cancelled
2
diferencia tipica agencia/mes
BI neto vs Betterez web
Tipo Condicion Betterez web is_paid BI is_paid_incl_partial
Sin cancel Sin registro en refund_details Cuenta true true
Parcial Reembolso < precio del boleto Cuenta false true
Total Reembolso precio del boleto No cuenta false false
Ejemplo real — agencia L78, abril 2026: is_paid = 10.933 · is_paid_including_partial_cancel = 10.935 (= Betterez web) · 2 boletos parciales explican la diferencia.
Boleto Precio Reembolsado Penalidad retenida
ZK6M6Q $16.200 $14.580 $1.620
GDTSAT $19.000 $17.099 $1.901
6

Cambios de boleto

No es cancelacion pura — hay un boleto sucesor

Boleto original
ABC123
changed
refund type=change
o intra-tx
Boleto sucesor
XYZ789
paid

Patron 1: refund type = change en una transaccion + venta nueva en otra TX.

En BI: successor_match_method = heuristic_unique cuando el match es unico.

Patron 2: mismo sale_id, par changed / paid misma ruta, sin fila en refund_details.

En BI: successor_match_method = same_transaction (Fase 2b).

Patron 3: solo una pierna del viaje cambio; el sucesor no es obvio.

successor_ticket_number queda null. Contar cambios con effective_ticket_status = 'changed'.

Columnas de enlace: successor_ticket_number, successor_transaction_id, successor_match_method. Ver presentacion complementaria: estados-boleto.html seccion cambios.
7

Qué filtro usar según la pregunta

Cheat sheet para negocio y modeladores

Cuantos pasajes vendimos? (tablero oficial)
is_paid = true
+ filtro currency
Por que no coincide con Betterez web?
is_paid_including_partial_cancel
Incluye cancelaciones parciales
Cuantos cancelaron?
effective_ticket_status = 'cancelled'
No usar solo ticket_status
Cuantas devoluciones refund?
effective_ticket_status = 'refunded'
O is_refunded = true
Cuantos cambios de boleto?
effective_ticket_status = 'changed'
No mezclar con cancelados
Cuanto quedo de penalidad?
SUM(cancel_penalty_amount)
Donde is_partial_cancel = true
Neto de caja del periodo?
SUM(sales.total)
Incluye filas R- negativas
Auditoria del payload crudo?
raw_events + GCS
JSON completo de Betterez
8

La analogia del libro y el manifiesto

Para cerrar la charla con audiencia no tecnica

📖

El libro contable

sales + filas R- registran cada movimiento de plata: venta +, devolucion . El neto es la caja real.

🚌

El manifiesto del colectivo

tickets + manifest_entries listan quien sube al bus. Cuando alguien cancela, Betterez lo saca del manifiesto — pero el boleto a veces sigue marcado como pagado.

Nosotros leemos la nota de devolucion (refund_details) para saber la verdad operativa y armar el tablero con reglas claras: venta neta, parcial, total o cambio. Las diferencias de 2–20 boletos por agencia/mes entre Betterez web y el tablero suelen ser reglas de negocio, no errores de datos.

9

Checklist rapido

Antes de hablar de devoluciones en un reporte

  • Para caja: sumar sales.total incluyendo filas R- (negativas)
  • Para boletos vendidos: is_paid (BI) o is_paid_including_partial_cancel (cruce Betterez)
  • Para cancelados: effective_ticket_status = 'cancelled', no solo ticket_status
  • Para penalidades: is_partial_cancel = true + SUM(cancel_penalty_amount)
  • Para cambios: effective_ticket_status = 'changed' + columnas successor_*
  • Montos de dinero: siempre filtrar currency; nunca mezclar monedas
  • Detalle fino de devoluciones: fact_ticket, no solo fact_ticket_mstr
  • Auditoria tecnica: refund_details + JSON en GCS via raw_events

Resumen en una tabla
Concepto de negocio Donde vive (tecnico) Qué mirar en BI
Venta original sales (monto +) fact_sale / ticket_amount
Devolucion en caja sales fila R- (monto −) SUM(sales.total) neto
Detalle de la devolucion refund_details refund_amount, refund_type
Cancelación con penalidad type = cancel, reembolso < precio is_partial_cancel, cancel_penalty_amount
Cancelación total type = cancel, reembolso ≥ precio is_full_cancel
Cambio de boleto type = change o intra-tx effective = changed, successor_*
Pasaje vendido (oficial) Sin cancel/refund en refunds is_paid = true
Alineado a Betterez web Incluye parciales is_paid_including_partial_cancel = true