TELSQL/i is a telecom rating & billing application: it ingests raw call-detail records
(CDRs) from a switch/mediation feed, parses each packed CDR string, classifies it into a time-of-day band,
applies a bundle-then-overage minute split against the customer's rate plan, prices every call through a
nested rate rule, journals rated usage, and rolls a period's usage into a taxed invoice that is then
finalized. Every rating and billing rule lives in DB2 for i SQL PL — functions, stored
procedures and table triggers. Unlike the RPG-orchestrated applications, TELSQL/i has no 5250 screen
and no RPG driver: it is a pure SQL-PL application invoked by CALL (from STRSQL, an
embedded-SQL caller, or a scheduled job) and exercised end-to-end by the test/tel_daily.mjs
battle. This manual is the reference for the operator who runs the rating and invoicing cycle and for the
developer maintaining it. It is grounded entirely in the committed source
(sqlpl-app-tel/src/schema.sql, src/routines.sql, and the
test/tel_daily.mjs driver). Everything runs in library TELSQL.
TELSQL/i takes a telecom CDR feed all the way to a finalized invoice:
TELCDR, each carrying a
single packed pipe-delimited string ORIGNO|DESTNO|DUR|CTYPE exactly as a switch feed
delivers it, plus a structured CALLTS timestamp and CALLDUR seconds. A
BEFORE-INSERT trigger vetoes a NULL or implausible duration outright, before the row is ever
stored.CALLDUR (a mediation consistency oracle), classifies the
call into a time-of-day band (peak / off-peak / weekend), rounds duration up to whole
billing-increment minutes, splits those minutes bundle-then-overage against the customer's
remaining cycle bundle, prices the overage through a deeply nested rate rule, and writes a
TELUSAGE row.CONTINUE handler logs the reason to TELREJ,
flips the CDR to rejected, and the loop moves on.AFTER-INSERT trigger on TELUSAGE accrues the
bundle-applied minutes of each rated call into the customer's cycle-to-date bundle counter
(CYCMIN).ISTAT O→F),
idempotently.TELSQL/i is deliberately built as a pure SQL-PL application. There is no SQLRPGLE orchestration
layer and no DDS display file — the entire application is the DB2 for i routines in
routines.sql, over the tables in schema.sql:
CALL statements and reads
OUT parameters or result tables. In the committed repository the driver is the
test/tel_daily.mjs battle, which loads the schema/routines, seeds CDRs, and calls the
procedures exactly as an operator would.The benefit for operations: the rules are one auditable place (the SQL-PL routines), reachable
identically from any caller, and the application is self-contained in library TELSQL. The
cost, documented honestly throughout this manual, is that there is no interactive screen; the
operator works through SQL CALLs.
INGEST RATING (batch) INVOICING (batch)
------ -------------- -----------------
INSERT TELCDR SP_RATE_BATCH ---loop---> SP_GEN_INVOICE (per cust/period)
| TR_TELCDR_VETO FETCH CSTAT='E' rows aggregate TELUSAGE over the period
| (75027/75028) CALL SP_RATE_CDR SUM(CHARGE) + window cross-check
v FN_CDRFLD (parse) + plan FLATFEE, + TAX (TAXPCT)
TELCDR (CSTAT E) FN_TODBAND (band) write TELINVH + TELINVL
| FN_CEILMIN (minutes) |
| bundle/overage split v
| nested-CASE rate SP_FINALIZE (ISTAT O->F)
| INSERT TELUSAGE --TR_TELUSAGE_ACCR--> TELCUST.CYCMIN += bundle
| UPDATE CSTAT='R'
| CONTINUE HANDLER (SQLEXCEPTION)
+--> bad row -----------> SP_MARK_REJECT -> TELREJ + CSTAT='X'
TELAUD <-- batch summary row (idempotency proof)
A single rating run flows: operator seeds/receives CDRs into TELCDR (the veto trigger
screens duration) → calls SP_RATE_BATCH → the procedure cursors every to-rate CDR,
calling SP_RATE_CDR per row, which parses, bands, splits, prices and inserts
TELUSAGE (firing the bundle-accrual trigger) or, on any error, is caught by the batch's
CONTINUE handler and logged to TELREJ → then per customer the operator calls
SP_GEN_INVOICE and finally SP_FINALIZE.
| Object | Type | Role |
|---|---|---|
| TELPLAN | PF | Rate-plan catalog (bundle, rates, flat fee, tax). |
| TELTOD | PF | Time-of-day band table (reference; see note in D). |
| TELCUST | PF | Customer master (plan, status, cycle-to-date bundle). |
| TELCDR | PF | Raw mediation CDR feed (the rating input). |
| TELUSAGE | PF | Rated usage detail, one row per rated CDR. |
| TELREJ | PF | Rejected-CDR log (written by the CONTINUE handler). |
| TELINVH | PF | Invoice header (fee, usage, tax, total, status). |
| TELINVL | PF | Invoice line detail. |
| TELAUD | PF | Generic audit/trace log (batch summaries). |
| FN_TODBAND | SQL function | Time-of-day band classifier. |
| FN_CDRFLD | SQL function | Pipe-delimited field extractor (string scan). |
| FN_CEILMIN | SQL function | Billing-increment minute rounding. |
| SP_RATE_CDR | SQL procedure | Rate one CDR. |
| SP_RATE_BATCH | SQL procedure | Rating loop over all to-rate CDRs. |
| SP_MARK_REJECT | SQL procedure | Log + flip a rejected CDR. |
| SP_GEN_INVOICE | SQL procedure | Generate one invoice per customer/period. |
| SP_FINALIZE | SQL procedure | Finalize a period's open invoices. |
| TR_TELUSAGE_ACCR | Trigger | AFTER INSERT: accrue bundle minutes to CYCMIN. |
| TR_TELCDR_VETO | Trigger | BEFORE INSERT: veto bad call durations. |
The full catalogue is 3 functions + 5 procedures + 2 triggers over 9 physical files, all
in library TELSQL. Sections C, D and F expand each.
Honest statement: TELSQL/i has no 5250 display file and no interactive program. There is
nothing to "open" — no subfile, no function keys, no maintenance panel. The application is reached
entirely by issuing SQL CALL statements against its stored procedures. The operator equivalent
of "run the transaction" is "type a CALL and Enter" from STRSQL (or drive the same
CALL from an embedded-SQL program or a scheduled job). Before invoking anything, the job's
library list must include TELSQL — the tested job runs with
LIBL = QSYS QGPL TELSQL QTEMP and CURLIB = TELSQL.
| To do this | Type in STRSQL (or CALL from a program) |
|---|---|
| Rate every to-rate CDR in one batch | CALL TELSQL.SP_RATE_BATCH(?, ?, ?) (OUT rated, rejected, seen) |
| Rate a single CDR by id (rarely, for diagnosis) | CALL TELSQL.SP_RATE_CDR('CDR0000001') |
| Generate one customer's invoice for a period | CALL TELSQL.SP_GEN_INVOICE('C000001', '202608', ?, ?) (OUT invno, rc) |
| Finalize a period's open invoices | CALL TELSQL.SP_FINALIZE('202608', ?) (OUT finalized) |
| Feed a raw CDR (mediation ingest) | INSERT INTO TELSQL.TELCDR VALUES (...) (the veto trigger screens it) |
| Review rated usage / rejects / invoices | SELECT ... FROM TELSQL.TELUSAGE / TELREJ / TELINVH |
Because there is no screen, all state is inspected with ordinary SELECTs (section E). The
procedures signal their outcome two ways: through OUT parameters (counts, the generated invoice
number, a return-code string such as GENERATED/SKIPPED) and through
application SQLSTATEs raised by SIGNAL (section F.5) when a business rule is
violated.
CHAR(n) literals, so a CHAR(4) plan code must be supplied at its exact declared
width — 'BAS ' (trailing space), not 'BAS' — or a downstream
TELPLAN lookup silently finds nothing. The seed literals in schema.sql and the
test driver are pre-padded for exactly this reason; an operator inserting customers by hand must do the
same. This is a documented engine-compatibility accommodation (finding SQLPL-PLAT-TEL-02), not an
application bug.Honest statement: TELSQL/i does not model a true four-eyes maker–checker /
separate-authorization workflow. No second user approves a rating run or an invoice; a CALL
takes effect immediately. The manual documents the control model the application does have —
audit + integrity + idempotency, all enforced in the data layer:
OP='RATE_BATCH', detail seen=.. rated=.. rejected=..), and every invoice
generation writes a row (OP='GEN_INVOICE', the invoice number + last-DML row count). This
is the after-the-fact accountability and idempotency-proof trail.CDRID and a human-readable reason carried from the SIGNAL message text — a complete,
reconstructable record of what was rejected and why, with the CDR itself flipped to
CSTAT='X' so it is never silently left un-rated.SP_RATE_CDR refuses to rate a CDR that is not found (75020),
malformed (75021), whose parsed duration disagrees with the structured value (75022), whose customer
is unknown (75023), or whose customer is not active (75024).SP_RATE_BATCH only ever selects CSTAT='E' rows and
flips each to R/X, so a re-run rates nothing twice; SP_GEN_INVOICE refuses to re-invoice a
customer/period that already has an invoice (returns SKIPPED); SP_FINALIZE
only touches ISTAT='O' rows.In sum, the control posture is audit + ingest/rating gating + idempotency enforced in the data layer, rather than a segregation-of-duties approval workflow.
TELSQL/i's work is a rating & billing cycle rather than a single monolithic job: an
ingest step (CDRs arrive and are screened), a rating step (one batch call rates the whole
feed), an invoicing step (one call per customer/period), and a finalize step (one call per
period). Every step is a bare SQL CALL — there are no control-row-driven parameters as in
the RPG applications; the period and customer are passed as CALL arguments.
-- 1. ingest: mediation feeds CDRs (veto trigger screens duration) INSERT INTO TELSQL.TELCDR VALUES ('CDR0000001','C000001','5550001|5559001|1800|L', TIMESTAMP('2026-08-03-09.00.00'), 1800, 'E'); -- 2. rate the whole feed in one batch CALL TELSQL.SP_RATE_BATCH(?, ?, ?); -- OUT rated, rejected, seen -- 3. invoice each customer for the period CALL TELSQL.SP_GEN_INVOICE('C000001', '202608', ?, ?); -- OUT invno, rc -- 4. finalize the period CALL TELSQL.SP_FINALIZE('202608', ?); -- OUT finalized count
Raw CDRs land as CSTAT='E' ("to-rate") rows. The BEFORE-INSERT trigger
TR_TELCDR_VETO fires first: a NULL CALLDUR is rejected with SQLSTATE 75027, and a
duration < 0 or > 86400 seconds (24h) with 75028. A vetoed INSERT fails
outright — the row is never written — so implausible mediation data can never reach rating.
SP_RATE_BATCH opens a cursor over every CSTAT='E' CDR (ORDER BY
CDRID) and, per row, calls SP_RATE_CDR. For each CDR the rating procedure:
FN_CDRFLD (origin, dest, duration, call type);CALLDUR
(mediation cross-check; mismatch → 75022);FN_TODBAND(CALLTS) and computes billing minutes with
FN_CEILMIN(CALLDUR);BUNDMIN−CYCMIN, floored at 0);TELUSAGE row (firing TR_TELUSAGE_ACCR, which adds the
bundle-applied minutes to the customer's CYCMIN) and flips the CDR to
CSTAT='R'.The batch's CONTINUE handler for SQLEXCEPTION catches any per-row SIGNAL (or unexpected error),
captures the message with GET DIAGNOSTICS, calls SP_MARK_REJECT to log to
TELREJ and flip the CDR to CSTAT='X', increments the rejected count, and the loop
moves on — one bad record never aborts the batch. An OKFLAG set immediately before each
CALL and cleared by the handler ensures only a surviving call is counted as rated. After the
loop, one TELAUD summary row records seen/rated/rejected.
The nested overage-rate rule (SP_RATE_CDR):
intl (I) -> PEAKRT * 3 (regardless of band)
weekend (W band) -> OFFRT (regardless of call type)
peak (P band) -> local: PEAKRT*0.5, else PEAKRT
off-pk (O band) -> local: OFFRT*0.5, else OFFRT
then floor: RATE = GREATEST(RATE, plan OVERRT)
CHARGE = OVERMIN * RATE (bundled minutes are free)
OVERRT floor is deliberate: some plans quote a contracted overage rate
above their per-band rate, so an overage minute is never billed below the plan's overage floor.
Worked example (customer C000004, ZERO plan): a weekday-peak local call prices at
PEAKRT×0.5 = 0.045, which is below the plan's OVERRT 0.12, so it is floored to
0.12.Per customer/period, SP_GEN_INVOICE first checks for an existing invoice (returns
SKIPPED with the existing invoice number if found — the idempotency guard). Otherwise it
derives the period bounds (PSTART = first-of-month, PEND = PSTART + 1 MONTH),
aggregates SUM(CHARGE) and a call count over the customer's rated usage in that window,
cross-checks that plain aggregate against an independent window-function running total (mismatch
→ 75026), computes SUBTOT = FLATFEE + usage, TAX = ROUND(SUBTOT×TAXPCT/100, 2)
and TOTAL, then writes the TELINVH header (ISTAT='O') and
TELINVL lines (a flat-fee line always; a usage line when the call count > 0). The invoice
number is 'IV' || CUSTNO || last-4-of-period. A TELAUD row records the
generation.
SP_FINALIZE(period) updates every ISTAT='O' invoice for the period to
ISTAT='F' and returns the count finalized via GET DIAGNOSTICS ROW_COUNT.
Idempotent: a re-run touches nothing (no open rows remain).
| Routine | Purpose | Calls / fires | Inputs | Outputs | Step |
|---|---|---|---|---|---|
| SP_RATE_BATCH | Rate every to-rate CDR; log rejects; write batch audit. | SP_RATE_CDR, SP_MARK_REJECT; CONTINUE handler. |
(none) — drives off TELCDR CSTAT='E'. |
OUT rated / rejected / seen; TELUSAGE rows; CDR→R/X; TELREJ; TELAUD row. | Rating. |
| SP_RATE_CDR | Rate one CDR (parse, band, split, price, journal). | FN_CDRFLD, FN_TODBAND, FN_CEILMIN; fires TR_TELUSAGE_ACCR. |
IN CDRID. | TELUSAGE row; CDR→R; SIGNALs 75020–75024 on error. | Rating (per row). |
| SP_MARK_REJECT | Log a rejected CDR and flip it to X. | — (called by the batch handler). | IN CDRID, REASON. | TELREJ row; CDR→X. | Rating (reject path). |
| SP_GEN_INVOICE | Generate (or skip) one invoice for a customer/period. | aggregate + window cross-check over TELUSAGE⨯TELCDR. | IN CUSTNO, PERIOD. | OUT INVNO, RC (GENERATED/SKIPPED); TELINVH + TELINVL; TELAUD; SIGNALs 75025/75026. | Invoicing. |
| SP_FINALIZE | Finalize a period's open invoices. | — | IN PERIOD. | OUT FINALIZED count; TELINVH ISTAT O→F. | Finalize. |
| TR_TELCDR_VETO | BEFORE INSERT: screen call duration at ingest. | fires on INSERT TELCDR. | NEW.CALLDUR. | SIGNALs 75027 (NULL) / 75028 (implausible); blocks the INSERT. | Ingest. |
| TR_TELUSAGE_ACCR | AFTER INSERT: accrue bundle minutes to CYCMIN. | fires on INSERT TELUSAGE. | NEW.CUSTNO, NEW.BUNDMIN. | UPDATE TELCUST SET CYCMIN = CYCMIN + BUNDMIN. | Rating (side effect). |
CSTAT='E' rows are rated; a CDR must be inserted (and pass
the veto trigger) before SP_RATE_BATCH will see it.SP_GEN_INVOICE aggregates TELUSAGE rows, which only
exist after rating. Invoicing a customer before their CDRs are rated produces a flat-fee-only invoice
(usage 0).SP_FINALIZE only moves existing open invoices to finalized;
it never generates one.ORDER BY CDRID, and
the bundle-then-overage split consumes bundle in that order — so a customer's earlier-id calls
draw bundle first and later calls tip into overage once the bundle is exhausted (the C000001 six-call
walk across the 100-minute boundary demonstrates this exactly).All files are in library TELSQL, grounded in schema.sql. Rates and charges are
DECIMAL; the packed CDR string is VARCHAR; timestamps are structured
TIMESTAMP; periods are CHAR(6) YYYYMM.
| Field | Type | Meaning |
|---|---|---|
| PLANCD | CHAR(4) | Plan code (PK) — e.g. 'BAS ', PLUS, UNLM, ZERO. Note the fixed 4-char width. |
| PDESC | VARCHAR(24) | Plan description. |
| BUNDMIN | INT | Bundle minutes included per cycle. |
| OVERRT | DECIMAL(7,4) | Overage $/minute once the bundle is exhausted (also the rate floor). |
| PEAKRT | DECIMAL(7,4) | Peak-band $/minute. |
| OFFRT | DECIMAL(7,4) | Off-peak-band $/minute. |
| FLATFEE | DECIMAL(9,2) | Monthly flat fee. |
| TAXPCT | DECIMAL(5,2) | Tax percent, e.g. 8.25. |
Seeded plans: BAS 100min/.15/.08/.04/19.99/6%; PLUS 500min/.10/.06/.03/39.99/7.5%; UNLM 9999min/0/0/0/69.99/8.25%; ZERO 0min/.12/.09/.05/9.99/5%.
| Field | Type | Meaning |
|---|---|---|
| BANDCD | CHAR(1) | P peak, O off-peak, W weekend. |
| HRSTART / HREND | INT | Half-open hour range [HRSTART, HREND), 0–24. |
| WKND | CHAR(1) | Y this row applies only Sat/Sun; N weekday. |
TELTOD is a documentation/reference table describing the
intended band layout (weekend→W; weekday 08–18→P; else O). The live classifier
FN_TODBAND encodes the same rule directly in a nested CASE (DAYOFWEEK / EXTRACT HOUR)
rather than reading TELTOD at runtime, so the table is seeded but not queried by the rating
path. Keep the two in agreement if either is changed.| Field | Type | Meaning |
|---|---|---|
| CUSTNO | CHAR(7) | Customer number (PK), e.g. C000001. |
| CNAME | VARCHAR(30) | Customer name. |
| PLANCD | CHAR(4) | The customer's rate plan (references TELPLAN). |
| CSTAT | CHAR(1) | A active, S suspended, C closed. Only A rates (else 75024). |
| CYCMIN | DECIMAL(9,2) | Cycle-to-date bundle minutes consumed (accrued by TR_TELUSAGE_ACCR). |
| Field | Type | Meaning |
|---|---|---|
| CDRID | CHAR(10) | Call-detail-record id (PK). |
| CUSTNO | CHAR(7) | Owning customer. |
| CDRTXT | VARCHAR(60) | Packed feed string ORIGNO|DESTNO|DUR|CTYPE (CTYPE: L local, D domestic, I international); parsed by FN_CDRFLD. |
| CALLTS | TIMESTAMP | Structured call timestamp (drives the band). |
| CALLDUR | INT (nullable) | Structured duration in seconds; the mediation-verified value the parsed DUR is cross-checked against. Deliberately nullable so a NULL feed value reaches TR_TELCDR_VETO's 75027 check. |
| CSTAT | CHAR(1) | E to-rate, R rated, X rejected. |
| Field | Type | Meaning |
|---|---|---|
| CDRID / CUSTNO | CHAR(10) / CHAR(7) | The rated CDR + its customer. |
| BANDCD | CHAR(1) | Band applied (P/O/W) from FN_TODBAND. |
| CTYPE | CHAR(1) | Call type (L/D/I) parsed from CDRTXT. |
| MINUTES | INT | Billing-increment minutes (ceil of seconds/60). |
| BUNDMIN | INT | Minutes drawn from the bundle (accrued to CYCMIN). |
| OVERMIN | INT | Minutes billed at the overage rate. |
| CHARGE | DECIMAL(9,4) | Overage charge for this call (0 if fully bundled). |
| Field | Type | Meaning |
|---|---|---|
| RSEQ | INT identity | Generated-always sequence. |
| CDRID | CHAR(10) | The rejected CDR. |
| RREASON | VARCHAR(60) | Reason text carried from the SIGNAL message. |
| Field | Type | Meaning |
|---|---|---|
| INVNO | CHAR(13) | Invoice number (PK), 'IV'||CUSTNO||last-4-of-period. |
| CUSTNO | CHAR(7) | Billed customer. |
| PERIOD | CHAR(6) | Billing period YYYYMM. |
| FLATFEE | DECIMAL(9,2) | Plan flat fee for the period. |
| USGCHG | DECIMAL(11,2) | Total usage/overage charges. |
| SUBTOT | DECIMAL(11,2) | FLATFEE + USGCHG. |
| TAX | DECIMAL(11,2) | ROUND(SUBTOT×TAXPCT/100, 2). |
| TOTAL | DECIMAL(11,2) | SUBTOT + TAX. |
| ISTAT | CHAR(1) | O open, F finalized. |
| Field | Type | Meaning |
|---|---|---|
| INVNO / ILINE | CHAR(13) / INT | Invoice + line number (PK). Line 1 = flat fee; line 2 = usage (when calls > 0). |
| LDESC | VARCHAR(40) | Line description (e.g. Usage/overage (6 calls)). |
| LAMT | DECIMAL(11,2) | Line amount. |
| Field | Type | Meaning |
|---|---|---|
| ASEQ | INT identity | Generated-always sequence. |
| OP | CHAR(12) | Operation, e.g. RATE_BATCH, GEN_INVOICE. |
| DETAIL | VARCHAR(80) | Free-text detail (batch counts / invoice number + row count). |
Pre-checks: confirm the job's library list includes TELSQL; confirm the plan and
customer masters are loaded and that customer plan codes are at the exact CHAR(4) width (B.1). Then run the
four steps in order.
TELCDR (CSTAT='E'). The veto
trigger screens each duration at insert; a rejected insert (75027/75028) means bad feed data —
correct it upstream, it was never stored.CALL TELSQL.SP_RATE_BATCH(?, ?, ?) and read the three OUT counts.CALL TELSQL.SP_GEN_INVOICE(cust, period, ?, ?); the RC
is GENERATED or SKIPPED.CALL TELSQL.SP_FINALIZE(period, ?) and confirm the finalized count.Expected rating outcome on the seeded feed (test/tel_daily.mjs):
SP_RATE_BATCH -> seen=16 rated=12 rejected=4
12 good = 6 (C000001) + 3 (C000002) + 1 (C000003) + 2 (C000004)
4 bad = 1 suspended (75024) + 3 malformed (75021 / 75022 / 75023)
Post-checks after rating:
rated + rejected = seen, and every good CDR is now CSTAT='R', every bad one
CSTAT='X' (never silently left at E).TELREJ holds exactly one row per rejected CDR with a reason matching its SQLSTATE
(e.g. %not active% for 75024, %missing field% for 75021,
%mismatch% for 75022, %unknown customer% for 75023).CYCMIN equals the sum of bundle-applied minutes across their rated calls
(C000001 ends at exactly 100, the bundle cap; a zero-bundle customer stays 0).TELAUD RATE_BATCH row records the run's counts.These are the same figures the volume battle checks against an independent hand-derived oracle
(test/tel_daily.mjs). Reconcile them from the base tables, not from the invoice itself.
SELECT FLATFEE, USGCHG, SUBTOT, TAX, TOTAL FROM TELSQL.TELINVH WHERE CUSTNO = 'C000001' AND PERIOD = '202608';
CHARGE = OVERMIN × rate, rate from the nested CASE
floored at the plan OVERRT. E.g. C000001 CDR0000004 (10 overage min, peak domestic,
floored to .15) = 1.50; CDR0000005 (5 min, weekend, floored to .15) = 0.75; CDR0000006 (1 min, intl,
.24) = 0.24.USGCHG = SUM(TELUSAGE.CHARGE) over the customer's rated calls in
the period. C000001: 1.50 + 0.75 + 0.24 = 2.49.SUBTOT = FLATFEE + USGCHG;
TAX = ROUND(SUBTOT×TAXPCT/100, 2); TOTAL = SUBTOT + TAX. C000001:
19.99 + 2.49 = 22.48; tax 22.48×6% = 1.3488→1.35; total 23.83.USGCHG = 0.00 (C000002 PLUS: 39.99/3.00/42.99; C000003 UNLM: 69.99/5.77/75.76).SUM(TELINVH.TOTAL) for the period equals the
independently derived sum of per-customer totals (seeded feed: 23.83 + 42.99 + 75.76 + 11.12).SP_GEN_INVOICE (a SUM() OVER
running total re-derived over the same rows) means an engine disagreement between the OLAP window and the
plain aggregate SIGNALs 75026 rather than silently invoicing a wrong figure — a self-auditing
reconciliation baked into the procedure.The procedures surface outcomes through OUT parameters and application SQLSTATEs (F.5). Every step is built to be safely re-runnable.
| Situation | Behaviour | Action |
|---|---|---|
| Bad CDR in the feed | Rating loop's CONTINUE handler catches the SIGNAL, logs to TELREJ, flips the CDR to X, continues. | Fix the source CDR data if it should have rated; re-insert as a new CSTAT='E' row and re-run the batch. |
| Rate batch re-run | Only CSTAT='E' rows are selected; all prior rows are R/X. | Safe no-op — rates nothing twice, does not double-accrue CYCMIN, does not grow TELREJ. Idempotent. |
| Invoice already exists | SP_GEN_INVOICE returns SKIPPED with the existing invoice number. | Expected. To re-invoice you must first remove the existing TELINVH/TELINVL rows (deliberate double-invoice guard). |
| Finalize re-run | Only ISTAT='O' rows are updated; none remain after the first run. | Safe no-op — returns 0 finalized. Idempotent. |
| Window/aggregate mismatch (75026) | The OLAP running total disagreed with the plain SUM. | A real engine anomaly — do not force the invoice; investigate the usage rows before proceeding. |
| NULL / implausible duration at ingest (75027 / 75028) | The INSERT into TELCDR fails; the row is never stored. | Correct the mediation value and re-insert. Nothing to clean up. |
| Unpadded plan code | A TELPLAN lookup silently finds nothing (no error). | Ensure CHAR(4) plan codes are padded to width (B.1). Check TELCUST.PLANCD if rating produces unexpected zeroes. |
TELAUD summary and every reject a
TELREJ row (with reason), and each rated call a TELUSAGE row (with its band, minute
split and charge), any cycle's effect is fully reconstructable after the fact for reconciliation and
recovery.The complete SQL-PL surface, from routines.sql and schema.sql. Every function,
procedure and trigger is listed with its signature. All objects are in library TELSQL.
DAYOFWEEK(TS) IN (1,7), DB2 1=Sunday..7=Saturday → 'W'), else the hour
band from EXTRACT(HOUR FROM TS) — 08≤HR<18 → 'P', otherwise
'O'. Encodes the TELTOD layout directly (D notes TELTOD is not read at runtime).WHILE loop
walks the string one '|' at a time with POSITION/SUBSTR,
returning the 1-based FLDNO'th field, or NULL if fewer than FLDNO fields exist; the last
field (no trailing pipe) is handled specially. TRIMs stray whitespace. NULL source
propagates to NULL.WHOLE = SECS/60, +1 if MOD(SECS,60) > 0). Guards
NULL or ≤0 seconds to return 0 (a no-call bills nothing;
NULL never propagates).TRANSLATE
digits-to-spaces + TRIM/LENGTH=0 test, NULLIF-guarded for empty)
and equals the structured CALLDUR (mismatch → 75022); resolves the customer
(unknown → 75023) and requires CSTAT='A' (else 75024); classifies the band and
minutes; performs the bundle-then-overage split; prices via the nested rate CASE floored at OVERRT;
inserts TELUSAGE (fires TR_TELUSAGE_ACCR) and flips the CDR to 'R'.CSTAT='X'. Called by SP_RATE_BATCH's CONTINUE
handler, never directly in the happy path.DECLARE CURSOR + OPEN + WHILE + FETCH over every
CSTAT='E' CDR; a CONTINUE HANDLER FOR SQLEXCEPTION captures the message with
GET DIAGNOSTICS, calls SP_MARK_REJECT, and counts the reject; an OKFLAG
distinguishes a rated row from a caught one. Writes a TELAUD summary. See F.4 for why this uses the
explicit-cursor idiom rather than a FOR loop.RCOUT='SKIPPED'. Else unknown customer → 75025; derives period bounds via a labeled
duration (PSTART + 1 MONTH); aggregates SUM(CHARGE)/count over the period;
window-function cross-check (mismatch → 75026); computes subtotal/tax/total; writes TELINVH
(ISTAT='O') + TELINVL (flat-fee line always, usage line when calls>0); TELAUD row;
RCOUT='GENERATED'.ISTAT='O' invoices for the period to 'F'; returns the count via
GET DIAGNOSTICS ... = ROW_COUNT. Idempotent.N.CALLDUR IS NULL → SIGNAL 75027; N.CALLDUR < 0 OR > 86400
→ SIGNAL 75028. A BEFORE trigger, so a SIGNAL aborts the INSERT before the row is
stored.UPDATE TELCUST SET CYCMIN = CYCMIN + N.BUNDMIN WHERE CUSTNO = N.CUSTNO
— adds only the bundle-applied minutes (not overage) to the customer's cycle-to-date counter.
The customer row is guaranteed to pre-exist (SP_RATE_CDR already validated it).WHILE +
POSITION('|' IN REMAIN) + SUBSTR, walking a packed record one delimiter at a
time rather than assuming a fixed layout.LENGTH(TRIM(TRANSLATE(s, REPEAT(' ',
LENGTH(s)), '0123456789'))) = 0 maps every digit to a space; an empty result means the string
was purely numeric. NULLIF(s,'') guards the empty-string edge.REMAINBUND = MAX(0, BUNDMIN − CYCMIN); minutes up
to the remainder are bundled, the rest overage.RATE = GREATEST(RATE, OVERRT) expressed as a follow-up CASE.SUM(...) OVER (ORDER BY ... ROWS BETWEEN UNBOUNDED PRECEDING AND
UNBOUNDED FOLLOWING) running total cross-checked against a plain aggregate SUM.MESSAGE_TEXT (reject reason capture) and
ROW_COUNT (finalize count / invoice DML count).PSTART + 1 MONTH on a TIMESTAMP expression for period bounds
(DB2 clamps a month-end rollover to the shorter month).SP_RATE_BATCH uses the classic explicit DECLARE CURSOR + OPEN + WHILE + FETCH idiom
rather than a FOR ... CURSOR FOR ... DO ... END FOR loop. The FOR-loop form
surfaced a genuine platform defect: a CONTINUE HANDLER declared in the enclosing procedure scope
does not resume the FOR loop's next iteration when it catches an exception raised inside
the loop body — it silently abandons the cursor and every remaining row. The explicit-cursor idiom
(which the earlier SQL-PL apps also use for their big loops) resumes correctly. The battle keeps isolated
regression probes (SQLPL-PLAT-TEL-01 / -03) that flip to PASS automatically if the engine is ever fixed.| SQLSTATE | Raised by | Meaning |
|---|---|---|
| 75020 | SP_RATE_CDR | CDR not found. |
| 75021 | SP_RATE_CDR | Malformed CDRTXT (a parsed field is missing). |
| 75022 | SP_RATE_CDR | Parsed duration non-numeric or disagrees with the structured CALLDUR (mediation cross-check). |
| 75023 | SP_RATE_CDR | Unknown customer. |
| 75024 | SP_RATE_CDR | Customer not active (CSTAT ≠ 'A'). |
| 75025 | SP_GEN_INVOICE | Unknown customer. |
| 75026 | SP_GEN_INVOICE | Window/aggregate usage mismatch (OLAP self-check failed). |
| 75027 | TR_TELCDR_VETO | NULL call duration rejected at ingest. |
| 75028 | TR_TELCDR_VETO | Implausible call duration (<0 or >86400s) rejected at ingest. |
SQLSTATE 75099 appears only in an isolated in-battle platform probe (SQLPL-PLAT-TEL-03), not in the application routines.
SUM(...) OVER (...)); used inside SP_GEN_INVOICE as an independent cross-check on the plain aggregate usage total.