Skip to main content
Cinco tablas, todas append-only. Nada se actualiza nunca. Que es lo que hace seguros los acuses de recibo concurrentes y deja que un mensaje re-enrutado mantenga ambos intentos.

Cómo se unen

La vista cdr_pnl hace left-join de submit y final, así que registros sin resultado aún emergen como PENDING. La exposición en vuelo permanece visible en lugar de excluirse silenciosamente.
Cada lector debe tomar la fila cdr_final más temprana para un mensaje. Por eso los acuses de progreso viven en su propia tabla: un ACCEPTD escrito en cdr_final superaría al DELIVRD que le siguió, en cada lista, cada gráfico y el P&L.

cdr_submit

La tabla portadora. La parte 1 es el único registro por mensaje de qué se cobró.

Identidad y forma

El dinero, ambos lados

Todo el dinero es un bigint de diezmilésimas. Divide entre 10 000 para mostrar; nunca guardes un float.
NULL de dinero es desconocido, nunca cero. Una tarifa que no pudo determinarse permanece distinguible de un mensaje genuinamente gratis.Cualquier consulta que colapse estos a cero reporta un proveedor sin precio como beneficio puro. margin_status existe para hacer ese estado explícito en lugar de inferido.
El dinero vive por completo en part_no = 1. Los segmentos posteriores son filas de entrega sin dinero. Escribir importes en cada segmento multiplicaría ambos lados por el número de segmentos. Cada consulta de dinero necesita WHERE part_no = 1.

Dos monedas, y sin conversión

cost_currency y price_currency se registran por lado. FireFlo nunca convierte entre ellas. Un despliegue que compra en una moneda y vende en otra tiene una columna de margen que no puede sumarse sin hacer la conversión tú mismo.

Las tres columnas de TLV

NULL significa que nada registró la columna; {} significa registrado y no había ninguno. tlvs_sent es NULL para cada proveedor no-SMPP.

body, y por qué es habitualmente NULL

Registrado solo si smsg.cdr.body es excerpt o full (V1.31.0). NULL en cada fila en caso contrario, y en cada fila escrita antes de que la columna existiera. Buscado con ILIKE sobre un índice trigrama (V1.32.0), siempre combinado con un rango de fechas. Ese índice necesita pg_trgm; si el usuario de migración no puede crear extensiones, la búsqueda aún funciona, más lentamente.

registered_delivery

El octeto SMPP como lo envió el cliente, crudo en lugar de decodificado. Lleva tres peticiones independientes y solo algunas se ejecutan.
Responde una pregunta que el registro no podía responder antes: ¿le debíamos un acuse a este cliente en absoluto?NULL en filas escritas antes de que la columna existiera y en un ingreso sin tal concepto. Eso no es cero. Cero es una afirmación sobre el cliente, a saber que no pidió nada.

submit_status y vendor_attempt

Desde V1.40.0. Ahora se escribe una fila cdr_submit por cada intento contra un vendor, no solo el que finalmente tuvo éxito o falló de forma terminal — un nack reintentable también escribe una.
NULL es “no registrado”, nunca una afirmación de éxito. Las filas escritas antes de V1.40.0, y cada fila de un vendor no-SMPP, llevan NULL aquí — léalo igual que tlvs_sent, no como 0.
Antes de esta columna, una tormenta de reintentos solo era visible como un conteo de submits inflado en el vendor, sin registro de lo que realmente dijo en los intentos que fallaron — vea Colas y reintentos para el presupuesto de reintentos que estas filas ahora hacen legible. (gateway_msg_id, part_no, retry_count, vendor_attempt) es la clave natural desde V1.40.0, ampliada desde (gateway_msg_id, part_no, retry_count) — retry_count por sí solo no cambia entre reintentos dentro del worker contra un mismo vendor, así que sin vendor_attempt esas filas colisionarían.

cdr_final y cdr_interim

Resultados y progreso, divididos por la razón anterior. También tienen tiempos de vida diferentes: dlr_state guarda el valor SMPP crudo junto al resultado grueso, porque EXPIRED, REJECTD y UNDELIV facturan todos como fallos pero significan cosas diferentes.

cdr_rejected

Una fila por rechazo, con reason y (desde V1.27.0) el código de estado del proveedor donde el proveedor fue quien rechazó. Una fila rechazada no lleva dinero. price, cost y margin son NULL porque el crédito se devolvió en el momento del rechazo. Que es lo que permite que las consultas de ingresos sumen esas columnas sin un filtro de resultado.

usage_daily

Una fila por cuenta, por producto, por día. Diaria en lugar de mensual así que cualquier período es un SUM sobre días, para siempre, sin una segunda tabla derivada con la que discrepar. Las columnas facturables para un día terminado son finales; las columnas de entrega se reformulan re-ejecutando el rollup, que es por lo que es idempotente.
El purge rechaza borrar cualquier día sin fila usage_daily, y rechaza toda la ejecución en lugar de saltar. Borrar un día sin resumir destruye el registro de consumo permanentemente.

Relacionado

Registros de llamada

Para qué es cada tabla, en prosa.

Tablas de configuración

La otra mitad del esquema.