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. Ojo: esa fila no viene sola. Ver el recuadro La transaccion contenedora mas abajo.
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
source_transaction_id La transaccion de Betterez que contiene la devolucion (cancelacion o cambio). Desde 29/09/2026
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
La transaccion contenedora (cambio del 29/09/2026)

En Betterez una devolucion no es una transaccion: vive en el arreglo refunds[] de otra transaccion que la contiene. Hay dos contenedores: la cancelacion (no vende boletos, total negativo; sale_type = 'cancellation') y el cambio (vende el boleto nuevo y devuelve el viejo; su total ya es neto). Nosotros guardamos las dos cosas: la transaccion y la fila R-. Sumarlas cuenta la misma plata dos veces.

Fila en sales
Tipo
Monto
JJAT4S62 — venta original del boleto C2L5PW
venta
+$15.000
MBUT8SQV — cancelacion (contenedor)
cancellation
−$3.499
R-C2L5PW — devolucion, source_transaction_id = MBUT8SQV
R-
−$3.499
Regla para plata: usar fact_sale.net_sale. Vale el total de la transaccion de Betterez (la cancelacion resta, el cambio suma su neto) y 0 en las filas R- con contenedor. SUM(total) de todas las filas resta la devolucion dos veces. La fila R- se conserva: es como liquida SAP y como la recibe el SRT, que la empareja con su cancelacion por source_transaction_id.
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 = 1
~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 = 1
+ 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 = 1
Cuantos cambios de boleto?
effective_ticket_status = 'changed'
No mezclar con cancelados
Cuanto quedo de penalidad?
SUM(cancel_penalty_amount)
Donde is_partial_cancel = 1
Neto de caja del periodo?
SUM(fact_sale.net_sale)
No SUM(sales.total): la cancelacion y su R- son la misma plata
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 = 1 + 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 = 1
Alineado a Betterez web Incluye parciales is_paid_including_partial_cancel = 1