TELSQL/i — Telecom Rating & Billing

SteelFrame X application operation manual  ·  ← back to Operation Manuals  ·  Sign On

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.

Contents

A. Overview & Architecture ↑ top

A.1 What it does

TELSQL/i takes a telecom CDR feed all the way to a finalized invoice:

A.2 Pure-SQL-PL architecture (no RPG, no 5250 screen)

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:

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.

A.3 Component & flow

  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.

A.4 Object inventory

ObjectTypeRole
TELPLANPFRate-plan catalog (bundle, rates, flat fee, tax).
TELTODPFTime-of-day band table (reference; see note in D).
TELCUSTPFCustomer master (plan, status, cycle-to-date bundle).
TELCDRPFRaw mediation CDR feed (the rating input).
TELUSAGEPFRated usage detail, one row per rated CDR.
TELREJPFRejected-CDR log (written by the CONTINUE handler).
TELINVHPFInvoice header (fee, usage, tax, total, status).
TELINVLPFInvoice line detail.
TELAUDPFGeneric audit/trace log (batch summaries).
FN_TODBANDSQL functionTime-of-day band classifier.
FN_CDRFLDSQL functionPipe-delimited field extractor (string scan).
FN_CEILMINSQL functionBilling-increment minute rounding.
SP_RATE_CDRSQL procedureRate one CDR.
SP_RATE_BATCHSQL procedureRating loop over all to-rate CDRs.
SP_MARK_REJECTSQL procedureLog + flip a rejected CDR.
SP_GEN_INVOICESQL procedureGenerate one invoice per customer/period.
SP_FINALIZESQL procedureFinalize a period's open invoices.
TR_TELUSAGE_ACCRTriggerAFTER INSERT: accrue bundle minutes to CYCMIN.
TR_TELCDR_VETOTriggerBEFORE 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.

B. Online & Access ↑ top

B.1 STRSQL CALL — honest: there is no screen

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 thisType in STRSQL (or CALL from a program)
Rate every to-rate CDR in one batchCALL 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 periodCALL TELSQL.SP_GEN_INVOICE('C000001', '202608', ?, ?) (OUT invno, rc)
Finalize a period's open invoicesCALL 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 / invoicesSELECT ... 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 width matters at the access boundary. The engine does not blank-pad 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.

B.2 Controls & audit workflow (no four-eyes maker–checker)

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:

In sum, the control posture is audit + ingest/rating gating + idempotency enforced in the data layer, rather than a segregation-of-duties approval workflow.

C. Batch / Cycle Procedures ↑ top

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

C.1 The rating/billing cycle

Ingest — INSERT TELCDR (screened by TR_TELCDR_VETO)

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.

Rate — SP_RATE_BATCH → SP_RATE_CDR

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:

  1. parses the four packed fields with FN_CDRFLD (origin, dest, duration, call type);
  2. validates the parsed duration is purely numeric and equals the structured CALLDUR (mediation cross-check; mismatch → 75022);
  3. looks up the customer (unknown → 75023, not active → 75024) and their plan;
  4. classifies the band with FN_TODBAND(CALLTS) and computes billing minutes with FN_CEILMIN(CALLDUR);
  5. splits minutes bundle-then-overage against remaining bundle (BUNDMIN−CYCMIN, floored at 0);
  6. prices the overage through the nested rate CASE (see below), floored at the plan's contracted overage rate;
  7. inserts a 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)
The 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.

Invoice — SP_GEN_INVOICE

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.

Finalize — SP_FINALIZE

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).

C.2 Procedure & trigger table

RoutinePurposeCalls / firesInputsOutputsStep
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).

C.3 Ordering & dependencies

D. Data Files (data dictionary) ↑ top

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.

TELPLAN — Rate-plan catalog (PK PLANCD)

FieldTypeMeaning
PLANCDCHAR(4)Plan code (PK) — e.g. 'BAS ', PLUS, UNLM, ZERO. Note the fixed 4-char width.
PDESCVARCHAR(24)Plan description.
BUNDMININTBundle minutes included per cycle.
OVERRTDECIMAL(7,4)Overage $/minute once the bundle is exhausted (also the rate floor).
PEAKRTDECIMAL(7,4)Peak-band $/minute.
OFFRTDECIMAL(7,4)Off-peak-band $/minute.
FLATFEEDECIMAL(9,2)Monthly flat fee.
TAXPCTDECIMAL(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%.

TELTOD — Time-of-day band table (PK BANDCD, HRSTART, WKND)

FieldTypeMeaning
BANDCDCHAR(1)P peak, O off-peak, W weekend.
HRSTART / HRENDINTHalf-open hour range [HRSTART, HREND), 0–24.
WKNDCHAR(1)Y this row applies only Sat/Sun; N weekday.
Honest note: 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.

TELCUST — Customer master (PK CUSTNO)

FieldTypeMeaning
CUSTNOCHAR(7)Customer number (PK), e.g. C000001.
CNAMEVARCHAR(30)Customer name.
PLANCDCHAR(4)The customer's rate plan (references TELPLAN).
CSTATCHAR(1)A active, S suspended, C closed. Only A rates (else 75024).
CYCMINDECIMAL(9,2)Cycle-to-date bundle minutes consumed (accrued by TR_TELUSAGE_ACCR).

TELCDR — Raw mediation CDR feed (PK CDRID)

FieldTypeMeaning
CDRIDCHAR(10)Call-detail-record id (PK).
CUSTNOCHAR(7)Owning customer.
CDRTXTVARCHAR(60)Packed feed string ORIGNO|DESTNO|DUR|CTYPE (CTYPE: L local, D domestic, I international); parsed by FN_CDRFLD.
CALLTSTIMESTAMPStructured call timestamp (drives the band).
CALLDURINT (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.
CSTATCHAR(1)E to-rate, R rated, X rejected.

TELUSAGE — Rated usage detail (PK CDRID)

FieldTypeMeaning
CDRID / CUSTNOCHAR(10) / CHAR(7)The rated CDR + its customer.
BANDCDCHAR(1)Band applied (P/O/W) from FN_TODBAND.
CTYPECHAR(1)Call type (L/D/I) parsed from CDRTXT.
MINUTESINTBilling-increment minutes (ceil of seconds/60).
BUNDMININTMinutes drawn from the bundle (accrued to CYCMIN).
OVERMININTMinutes billed at the overage rate.
CHARGEDECIMAL(9,4)Overage charge for this call (0 if fully bundled).

TELREJ — Rejected-CDR log (identity RSEQ)

FieldTypeMeaning
RSEQINT identityGenerated-always sequence.
CDRIDCHAR(10)The rejected CDR.
RREASONVARCHAR(60)Reason text carried from the SIGNAL message.

TELINVH — Invoice header (PK INVNO)

FieldTypeMeaning
INVNOCHAR(13)Invoice number (PK), 'IV'||CUSTNO||last-4-of-period.
CUSTNOCHAR(7)Billed customer.
PERIODCHAR(6)Billing period YYYYMM.
FLATFEEDECIMAL(9,2)Plan flat fee for the period.
USGCHGDECIMAL(11,2)Total usage/overage charges.
SUBTOTDECIMAL(11,2)FLATFEE + USGCHG.
TAXDECIMAL(11,2)ROUND(SUBTOT×TAXPCT/100, 2).
TOTALDECIMAL(11,2)SUBTOT + TAX.
ISTATCHAR(1)O open, F finalized.

TELINVL — Invoice line detail (PK INVNO, ILINE)

FieldTypeMeaning
INVNO / ILINECHAR(13) / INTInvoice + line number (PK). Line 1 = flat fee; line 2 = usage (when calls > 0).
LDESCVARCHAR(40)Line description (e.g. Usage/overage (6 calls)).
LAMTDECIMAL(11,2)Line amount.

TELAUD — Audit/trace log (identity ASEQ)

FieldTypeMeaning
ASEQINT identityGenerated-always sequence.
OPCHAR(12)Operation, e.g. RATE_BATCH, GEN_INVOICE.
DETAILVARCHAR(80)Free-text detail (batch counts / invoice number + row count).

Relationships

E. Operations Runbook ↑ top

E.1 Run the rating & billing cycle

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.

  1. Ingest. Let mediation feed CDRs into 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.
  2. Rate. CALL TELSQL.SP_RATE_BATCH(?, ?, ?) and read the three OUT counts.
  3. Invoice. For each customer, CALL TELSQL.SP_GEN_INVOICE(cust, period, ?, ?); the RC is GENERATED or SKIPPED.
  4. Finalize. 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:

E.2 Reconciling figures

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';
The window-function cross-check inside 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.

E.3 Failure & re-run rules

The procedures surface outcomes through OUT parameters and application SQLSTATEs (F.5). Every step is built to be safely re-runnable.

SituationBehaviourAction
Bad CDR in the feedRating 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-runOnly 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 existsSP_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-runOnly 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 codeA 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.
Because every rating run writes a 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.

F. Developer Reference ↑ top

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.

F.1 Functions (3)

FN_TODBAND (TS TIMESTAMP) RETURNS CHAR(1)
Time-of-day band classifier. A deeply nested searched CASE: weekend first (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).
FN_CDRFLD (SRC VARCHAR(60), FLDNO INT) RETURNS VARCHAR(30)
Character-in-a-loop pipe-delimited field extractor for the packed CDRTXT. A 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.
FN_CEILMIN (SECS INT) RETURNS INT
Standard telco billing-increment rule: any partial minute bills as a full minute (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).

F.2 Procedures (5)

SP_RATE_CDR (IN CDRIDIN CHAR(10))
Rate one CDR. Reads the CDR (not found → 75020); parses the four fields with FN_CDRFLD (any missing → 75021); validates the parsed duration is digits-only (via a 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'.
SP_MARK_REJECT (IN CDRIDIN CHAR(10), IN REASONIN VARCHAR(60))
Inserts a TELREJ row and flips the CDR to CSTAT='X'. Called by SP_RATE_BATCH's CONTINUE handler, never directly in the happy path.
SP_RATE_BATCH (OUT RATEDOUT INT, OUT REJECTEDOUT INT, OUT SEENOUT INT)
The big rating loop. 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.
SP_GEN_INVOICE (IN CUSTNOIN CHAR(7), IN PERIODIN CHAR(6), OUT INVNOOUT CHAR(13), OUT RCOUT VARCHAR(20))
Generate one invoice. Idempotency guard: existing invoice → return its number with 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'.
SP_FINALIZE (IN PERIODIN CHAR(6), OUT FINALIZEDOUT INT)
Updates all ISTAT='O' invoices for the period to 'F'; returns the count via GET DIAGNOSTICS ... = ROW_COUNT. Idempotent.

F.3 Triggers (2)

TR_TELCDR_VETO — BEFORE INSERT ON TELCDR (FOR EACH ROW, ATOMIC)
Ingest gate. 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.
TR_TELUSAGE_ACCR — AFTER INSERT ON TELUSAGE (FOR EACH ROW, ATOMIC)
Bundle accumulator. 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).

F.4 SQL-PL patterns & the platform workaround

Platform workaround (SQLPL-SIM-FINDINGS.md SQLPL-PLAT-TEL-03). 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.

F.5 Application SQLSTATE table

SQLSTATERaised byMeaning
75020SP_RATE_CDRCDR not found.
75021SP_RATE_CDRMalformed CDRTXT (a parsed field is missing).
75022SP_RATE_CDRParsed duration non-numeric or disagrees with the structured CALLDUR (mediation cross-check).
75023SP_RATE_CDRUnknown customer.
75024SP_RATE_CDRCustomer not active (CSTAT ≠ 'A').
75025SP_GEN_INVOICEUnknown customer.
75026SP_GEN_INVOICEWindow/aggregate usage mismatch (OLAP self-check failed).
75027TR_TELCDR_VETONULL call duration rejected at ingest.
75028TR_TELCDR_VETOImplausible 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.

G. Glossary ↑ top

Band (time-of-day band)
The peak / off-peak / weekend classification of a call from its timestamp, driving the per-minute rate. Computed by FN_TODBAND; weekend overrides the hour band.
Billing increment
The rounding rule that any partial minute bills as a full minute (FN_CEILMIN) — standard telco billing.
Bundle (bundle minutes)
The plan's included minutes per cycle (BUNDMIN). Minutes are drawn from the bundle first; the remainder is overage. Consumption is tracked per customer in CYCMIN.
CDR — Call Detail Record
One raw record per call from the switch/mediation feed (TELCDR), carrying a packed field string plus a structured timestamp and duration.
CDR mediation
Taking a raw switch feed and preparing it for rating — here, parsing the packed CDRTXT, cross-checking the parsed duration against the structured value, and screening bad durations at ingest.
CONTINUE handler
An SQL/PSM exception handler that, after handling, resumes at the statement after the one that raised — used by SP_RATE_BATCH so a single bad CDR is logged and skipped rather than aborting the batch.
Cycle-to-date (CYCMIN)
The running total of bundle minutes a customer has consumed this billing cycle, accrued by TR_TELUSAGE_ACCR.
Finalize
Moving an invoice from open (ISTAT='O') to finalized ('F') so it is locked for the period (SP_FINALIZE).
Idempotent
Safe to run again with the same result. The rate batch (E-rows only), invoice generation (existing-invoice guard) and finalize (open-rows only) are all idempotent.
Overage
Minutes billed at a per-minute rate once the bundle is exhausted, priced by the nested rate CASE and never below the plan's contracted OVERRT floor.
Rating
Turning a raw CDR into a priced usage record: parse, band, minute-round, bundle/overage split, price, journal (SP_RATE_CDR / SP_RATE_BATCH).
SIGNAL / SQLSTATE
The SQL-PL mechanism for raising a business-rule error with a specific state code (F.5) and message text; caught by the batch's CONTINUE handler and logged to TELREJ.
SQL PL
DB2 for i's procedural SQL language (functions, procedures, triggers). TELSQL/i is written entirely in it, with no RPG orchestration layer.
STRSQL
Start SQL — the IBM i interactive SQL session an operator uses to CALL the procedures, since this application has no 5250 screen.
Window function (OLAP)
A SQL function computing over a window of rows (e.g. SUM(...) OVER (...)); used inside SP_GEN_INVOICE as an independent cross-check on the plain aggregate usage total.