How they join
cdr_pnl view left-joins submit and final, so records with no outcome yet surface as
PENDING — in-flight exposure stays visible rather than being silently excluded.
cdr_submit
The load-bearing table. Part 1 is the only per-message record of what was charged.
Identity and shape
The money, both sides
All money is a
bigint of ten-thousandths. Divide by 10 000 to display; never store a float.
The money lives entirely on part_no = 1. Later segments are delivery rows with no money —
writing amounts on every segment would multiply both sides by the segment count. Every money query
needs WHERE part_no = 1.
Two currencies, and no conversion
cost_currency and price_currency are recorded per side. FireFlo never converts between them.
A deployment buying in one currency and selling in another has a margin column that cannot be summed
without doing the conversion yourself.
The three TLV columns
NULL means nothing recorded the column; {} means recorded and there were none. tlvs_sent is
NULL for every non-SMPP vendor.
body, and why it is usually NULL
Recorded only if smsg.cdr.body is excerpt or full (V1.31.0). NULL on every row otherwise, and
on every row written before the column existed.
Searched with ILIKE over a trigram index (V1.32.0), always combined with a date range. That index
needs pg_trgm; if the migration user may not create extensions, search still works, more slowly.
registered_delivery
The SMPP octet as the customer sent it, raw rather than decoded — it carries three independent
requests and only some are acted on.
submit_status and vendor_attempt
Since V1.40.0. A cdr_submit row is now written for every attempt against a vendor, not only the
one that finally succeeded or terminally failed — a retryable nack writes one too.
Before this column, a retry storm was visible only as an inflated submit count at the vendor, with no
record of what it actually said on the attempts that failed — see
Queues and retries for the retry budget these rows now make
legible.
(gateway_msg_id, part_no, retry_count, vendor_attempt) is the natural key as of V1.40.0, widened from
(gateway_msg_id, part_no, retry_count) — retry_count alone does not change across in-worker retries
at one vendor, so without vendor_attempt those rows would collide.
cdr_final and cdr_interim
Outcomes and progress, split for the reason above. They also have different lifetimes:
dlr_state keeps the raw SMPP value alongside the coarse outcome, because EXPIRED, REJECTD and
UNDELIV all bill as failures but mean different things.
cdr_rejected
One row per refusal, with reason and — since V1.27.0 — the vendor’s status code where the vendor was
the one refusing.
A refused row carries no money. price, cost and margin are NULL because credit was
returned at the moment of refusal, which is what lets revenue queries sum those columns without an
outcome filter.
usage_daily
One row per account, per product, per day. Daily rather than monthly so any period is a SUM over
days, forever, with no second derived table to disagree with it.
Billable columns for a finished day are final; delivery columns are restated by re-running the
rollup, which is why it is idempotent.
Related
Call records
What each table is for, in prose.
Configuration tables
The other half of the schema.