Skip to main content
Cinq tables, toutes en append-only. Rien n’est jamais mis à jour — c’est ce qui rend les accusés de livraison concurrents sûrs et permet à un message re-routé de conserver les deux tentatives.

Comment elles se joignent

La vue cdr_pnl fait un left join submit-final, donc les enregistrements sans résultat pour l’instant apparaissent comme PENDING — l’exposition en vol reste visible plutôt que d’être exclue silencieusement.
Chaque lecteur doit prendre la ligne cdr_final la plus ancienne pour un message. C’est pourquoi les accusés de progression vivent dans leur propre table : un ACCEPTD écrit dans cdr_final surpasserait le DELIVRD qui a suivi, dans chaque liste, chaque graphique et le P&L.

cdr_submit

La table porteuse. La partie 1 est le seul enregistrement par message de ce qui a été facturé.

Identité et forme

L’argent, des deux côtés

Tout l’argent est un bigint en dix-millièmes. Divisez par 10 000 pour afficher ; ne stockez jamais un float.
NULL d’argent est inconnu, jamais zéro. Un tarif qui n’a pu être déterminé reste distinct d’un message vraiment gratuit.Toute requête qui coalesce cela à zéro rapporte un fournisseur non tarifé comme pur profit. margin_status existe pour rendre cet état explicite plutôt qu’inféré.
L’argent vit entièrement sur part_no = 1. Les segments ultérieurs sont des lignes de livraison sans argent — écrire des montants sur chaque segment multiplierait les deux côtés par le nombre de segments. Chaque requête d’argent a besoin de WHERE part_no = 1.

Deux devises, et aucune conversion

cost_currency et price_currency sont enregistrées par côté. FireFlo ne convertit jamais entre elles. Un déploiement qui achète dans une devise et vend dans une autre a une colonne de marge qui ne peut être sommée sans faire la conversion vous-même.

Les trois colonnes TLV

NULL signifie que rien n’a enregistré la colonne ; {} signifie enregistré et il n’y en avait pas. tlvs_sent est NULL pour chaque fournisseur non-SMPP.

body, et pourquoi il est habituellement NULL

Enregistré seulement si smsg.cdr.body est excerpt ou full (V1.31.0). NULL sur chaque ligne sinon, et sur chaque ligne écrite avant que la colonne n’existe. Cherché avec ILIKE sur un index de trigrammes (V1.32.0), toujours combiné à une plage de dates. Cet index a besoin de pg_trgm ; si l’utilisateur de migration ne peut créer d’extensions, la recherche fonctionne encore, plus lentement.

registered_delivery

L’octet SMPP tel que le client l’a envoyé, brut plutôt que décodé — il porte trois requêtes indépendantes et seules certaines sont honorées.
Il répond à une question à laquelle l’enregistrement ne pouvait répondre auparavant : devions-nous un accusé à ce client du tout.NULL sur les lignes écrites avant l’existence de la colonne et sur une entrée sans un tel concept. Ce n’est pas zéro — zéro est une affirmation sur le client, à savoir qu’il n’a rien demandé.

submit_status et vendor_attempt

Depuis V1.40.0. Une ligne cdr_submit est désormais écrite pour chaque tentative auprès d’un fournisseur, pas seulement celle qui a finalement réussi ou échoué de manière terminale — un nack réessayable en écrit une aussi.
NULL signifie “non enregistré”, jamais une affirmation de succès. Les lignes écrites avant V1.40.0, et chaque ligne d’un fournisseur non-SMPP, portent NULL ici — lisez-le de la même façon que tlvs_sent, pas comme 0.
Avant cette colonne, une tempête de nouvelles tentatives n’était visible que comme un compte de soumissions gonflé chez le fournisseur, sans trace de ce qu’il avait réellement répondu sur les tentatives qui ont échoué — voir Files et nouvelles tentatives pour le budget de nouvelles tentatives que ces lignes rendent maintenant lisible. (gateway_msg_id, part_no, retry_count, vendor_attempt) est la clé naturelle depuis V1.40.0, élargie depuis (gateway_msg_id, part_no, retry_count) — retry_count seul ne change pas à travers les nouvelles tentatives dans le worker chez un même fournisseur, donc sans vendor_attempt ces lignes entreraient en collision.

cdr_final et cdr_interim

Résultats et progression, séparés pour la raison ci-dessus. Ils ont aussi des durées de vie différentes : dlr_state garde la valeur SMPP brute à côté du résultat grossier, car EXPIRED, REJECTD et UNDELIV facturent tous comme des échecs mais signifient des choses différentes.

cdr_rejected

Une ligne par refus, avec reason et — depuis V1.27.0 — le code de statut du fournisseur là où le fournisseur était celui qui refusait. Une ligne refusée ne porte pas d’argent. price, cost et margin sont NULL car le crédit a été retourné au moment du refus, ce qui permet aux requêtes de revenu de sommer ces colonnes sans filtre de résultat.

usage_daily

Une ligne par compte, par produit, par jour. Quotidienne plutôt que mensuelle pour que toute période soit une SUM sur des jours, pour toujours, sans seconde table dérivée qui pourrait être en désaccord avec elle. Les colonnes facturables pour un jour terminé sont finales ; les colonnes de livraison sont recalculées en relançant le rollup, ce qui la rend idempotente.
La purge refuse de supprimer tout jour sans ligne usage_daily, et refuse tout le run plutôt que de sauter. Supprimer un jour non résumé détruit l’enregistrement de consommation de façon permanente.

Voir aussi

Enregistrements d'appel

À quoi sert chaque table, en prose.

Tables de configuration

L’autre moitié du schéma.