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.
Tres capas de datos
De afuera hacia adentro: del evento crudo al tablero
| 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. |
Caja: la venta no se toca, se suma una fila R-
Modelo de libro contable validado con Andesmar
sales con prefijo R- y monto negativo.
El neto de caja es simplemente SUM(total) de todas las filas.
TX-ABC123 — venta boleto ZK6M6QR-ZK6M6Q — cancelacion con penalidad| Campo | Qué 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 |
La fila R- en sales maneja el balance de caja.
El detalle fino vive en refund_details (relacion 1:1):
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.
Reembolso
refund_details.type = refund
Devolucion de dinero con flujo contable distinto a la cancelacion. Tambien genera fila R- negativa.
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.
Resultado: columna effective_ticket_status en fact_ticket.
El truco que confunde a todos
El boleto puede seguir diciendo “paid” despues de cancelar
tickets.status muchas veces sigue en paid.
La senal real de la baja esta en refund_details, no en el status del ticket.
Status crudo — puede estar desactualizado
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 |
Cancelación parcial vs total
El caso de negocio que explica las diferencias con Betterez web
is_partial_cancel = trueeffective = cancelledBI 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 |
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 |
Cambios de boleto
No es cancelacion pura — hay un boleto sucesor
o intra-tx
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'.
successor_ticket_number, successor_transaction_id, successor_match_method.
Ver presentacion complementaria: estados-boleto.html seccion cambios.
Qué filtro usar según la pregunta
Cheat sheet para negocio y modeladores
is_paid = truecurrencyis_paid_including_partial_canceleffective_ticket_status = 'cancelled'ticket_statuseffective_ticket_status = 'refunded'is_refunded = trueeffective_ticket_status = 'changed'SUM(cancel_penalty_amount)is_partial_cancel = trueSUM(sales.total)R- negativasraw_events + GCSLa 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.
Checklist rapido
Antes de hablar de devoluciones en un reporte
- Para caja: sumar
sales.totalincluyendo filasR-(negativas) - Para boletos vendidos:
is_paid(BI) ois_paid_including_partial_cancel(cruce Betterez) - Para cancelados:
effective_ticket_status = 'cancelled', no soloticket_status - Para penalidades:
is_partial_cancel = true+SUM(cancel_penalty_amount) - Para cambios:
effective_ticket_status = 'changed'+ columnassuccessor_* - Montos de dinero: siempre filtrar
currency; nunca mezclar monedas - Detalle fino de devoluciones:
fact_ticket, no solofact_ticket_mstr - Auditoria tecnica:
refund_details+ JSON en GCS viaraw_events
| 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 |