DENTIS/i — Dental Practice Management

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

DENTIS/i is a dental-practice management application: patient and provider masters, an ADA-style procedure/fee schedule, appointment scheduling, chairside treatment charting, insurance claim generation and adjudication against a dual-coverage annual-benefit accumulator, a patient ledger/AR with statements and aging, and a practice-revenue GL feed. The spine of the application is the charted procedure line — an appointment becomes one or more procedure lines against the fee schedule — and the accumulator it drives is a per-patient annual benefit maximum (plus a plan-year deductible), not a medical out-of-pocket cap. All logic is coded in ILE RPG, one embedded-SQL RPG program, an ILE COBOL report, and CL; there is no separate rules engine — each batch program reads and writes the files directly. This manual is the reference for the operator who runs the daily and monthly cycles and the two inquiry screens, and for the developer maintaining the application. It is grounded entirely in the committed source (dental-app/src/sources.mjs, src/seed.mjs, and the test/dt_build.mjs / dt_daily.mjs / dt_monthly.mjs drivers). Everything runs in library DENTIS.

Contents

A. Overview & Architecture ↑ top

A.1 What it does

DENTIS/i runs the revenue life of a dental practice, from a charted procedure to a posted GL entry:

A.2 The billing arithmetic — the one formula every program obeys

The entire application is organised around one adjudication formula, applied per claim line because a dental claim's lines can straddle preventive / basic / major categories with different coverage rates:

  fee             = the practice's fee-schedule amount for the procedure (DTFEE.FFEE)
  allowed         = fee            (the practice fee IS the allowance; a dental PPO pays a
                                    percentage of it, there is no separate re-priced "allowed")
  deductible part = min(remaining plan deductible, line fee)      -> patient
  remainder       = fee - deductible part
  ins share (raw) = remainder * category coverage rate
                    (preventive 100% / basic 80% / major 50% -- classic dental 100/80/50)
  ins share       = ins share (raw), CAPPED so (annual benefit used + ins share)
                    never exceeds the plan's annual maximum; the excess moves to the patient
  patient resp    = fee - ins share
  INVARIANT:        ins share + patient resp = allowed, ALWAYS

The annual-maximum cap is the mirror image of a medical out-of-pocket cap: a medical cap protects the member; this cap protects the plan — once the plan has paid its annual maximum, it pays nothing more and the patient owes the rest. The accumulator (DTACCUM, one row per patient per plan year) is what makes adjudication stateful: each claim reads the deductible already met and the annual benefit already used, applies against them, and writes them back, so the next claim pays differently because of the last one.

A.3 Component & flow

  APPOINTMENT -> CHART -> CLAIM              DAILY (DTDAILY)          MONTHLY (DTMONTH)
  ----------------------------              ---------------          -----------------
  DTAPPT   (scheduled visit)                DTPOST   charge-post      DTPAYPOST  ins+pat payments
    |                                       DTCLGEN  claim per appt   DTSTMTRUN  statements + aging
    v                                       DTADJUD  adjudicate       DTGLDST    GL distribution
  DTCHART  (one row per procedure) --edit-->   reads DTPAT/DTINS/     DTAGERPT   COBOL AR aging report
    |         priced off DTFEE                 DTFEE, reads+writes    DTYRRST    plan-year reset
    v                                          DTACCUM
  DTLEDG  (charge C)  <----writes---- DTPOST                          \                    /
    ^                                                                  \                  /
    |   DTCLGEN groups posted (G) chart lines by APPTNO -> DTCLAIM      v                v
    |   + DTCLAIML   (insured patients only)                          DTLEDG  (ins pmt I / pat pmt X)
    |                                                                        + DTSTMT (aged snapshot)
    +-- DTADJUD adjudicates V claims -> stamps DTCLAIM/DTCLAIML,              + DTGLDIST (GL rows)
        updates DTACCUM, writes a 'A' audit row to DTLEDG

A single treatment event flows: an appointment is charted → DTPOST prices and posts the chart line to DTLEDG as a charge and marks it posted (CSTAT='G') → DTCLGEN groups the day's posted lines for an insured patient into one DTCLAIM per appointment and marks them claimed (CSTAT='C') → DTADJUD adjudicates the claim against DTACCUM and stamps the split onto the claim and its lines. Money is only ever moved on the ledger; the accumulator carries the running benefit position.

A.4 Object inventory

ObjectTypeRole
DTPATPFPatient master.
DTPROVPFProvider (dentist / hygienist) master.
DTFEEPFProcedure / fee schedule (ADA-style codes).
DTINSPFInsurance plan (deductible, annual max, coverage rates).
DTAPPTPFAppointment.
DTCHARTPFCharted treatment procedure line (the app's spine).
DTLEDGPFPatient ledger / AR (the durable audit trail).
DTCLAIMPFInsurance claim header.
DTCLAIMLPFClaim lines (one per charted procedure).
DTACCUMPFPer-(patient, plan-year) benefit accumulator.
DTSTMTPFStatement-run aged-balance snapshot.
DTGLDISTSQL tablePractice-revenue GL distribution.
DTCHARTP / DTLEDGP / DTCLAIMPLF (3)By-PATID access paths over chart / ledger / claim.
DTPATD / DTLEDGD / DTMENUDDSPF (3)Patient inquiry / ledger subfile / menu display files.
DTSTMTPPRTFPatient-statement printer file.
DTREFLDRPGLESeed reference data.
DTPOST / DTCLGEN / DTADJUDRPGLE (3)Daily cycle: post, generate, adjudicate.
DTPAYPOST / DTSTMTRUN / DTYRRSTRPGLE (3)Monthly cycle: pay-post, statements, year reset.
DTGLDSTSQLRPGLEPractice-revenue GL distribution (embedded SQL).
DTPATIQ / DTLEDGIQ / DTMENURPGLE (3)Interactive: benefit inquiry / ledger subfile / menu.
DTAGERPTCBLLEILE COBOL AR-aging audit report.
DTSETUPCLPCreate every object & compile every program.
DTDAILY / DTMONTHCLP (2)The two job cycles.

The full catalogue is 11 PFs + 1 SQL table, 3 LFs, 3 DSPFs + 1 PRTF, driven by 10 ILE RPG programs + 1 SQLRPGLE + 1 ILE COBOL program, plus 3 CL programs (setup + two cycles). Sections D and F expand each.

B. Online Transactions & Screens ↑ top

B.1 The command/entry line

DENTIS/i has no CICS transaction identifiers and no menu-driven transid switch. On IBM i, each program is reached by name from a 5250 command-entry line (or a JOBQ/scheduler for the batch cycles). The operator equivalent of "type a transid and Enter" is "type a CALL command and Enter". Before invoking anything, the job's library list must include DENTIS — the tested jobs run with LIBL = QSYS QGPL DENTIS QTEMP and CURLIB = DENTIS. Note the two CL cycles do this for you with an ADDLIBLE LIB(DENTIS) as their first step.

To do thisType on the command line
Open the operator main menuCALL DENTIS/DTMENU
Patient benefit inquiry (direct)CALL DENTIS/DTPATIQ
Patient ledger inquiry (direct)CALL DENTIS/DTLEDGIQ
Run the daily treatment cycleCALL DENTIS/DTDAILY (or SBMJOB it)
Run the monthly close cycleCALL DENTIS/DTMONTH
Build/compile everything, then seed reference dataCALL DENTIS/DTSETUP then CALL DENTIS/DTREFLD

The batch programs take no CALL parameters. There is no batch-control table the way LOANSVC/i has an LNCTL row — each program sweeps its input file (chart lines, claims, the ledger, the accumulator) top to bottom. Several programs carry a hard-coded processing date / run number in their source (e.g. DTPAYPOST posts payments dated 20260901, DTSTMTRUN runs statement period 202609 as of 20260901, DTGLDST posts GL batch 202609 dated 20260930, DTYRRST resets from current year 2026); these are constants in the committed source, so a production deployment would edit and recompile the program to advance the period. Only DTMENU, DTPATIQ and DTLEDGIQ are interactive; the batch programs run to completion and DSPLY a one-line result.

B.2 The menu & the two inquiry screens

Main menu (DTMENU / DTMENUD)

A plain menu: option 1 CALLs DTPATIQ, option 2 CALLs DTLEDGIQ, F3 exits. An invalid option shows "Invalid option." and re-displays.

DENTIS/i Main Menu 1. Patient Inquiry 2. Ledger Inquiry Option . . . . : _ F3=Exit Enter=Select

Patient benefit inquiry (DTPATIQ / DTPATD)

A non-subfile screen. Key a patient id and Enter; DTPATIQ CHAINs the patient (DTPAT), their plan (DTINS) and their (patient, current plan year) accumulator (DTACCUM, key year 2026), then displays the derived benefit position — deductible remaining and annual remaining are computed on the screen, not stored. A self-pay patient (blank PINSPLN) shows N/A — SELF-PAY in the benefit fields; an unknown id shows "Patient not found."

Patient Inquiry - DENTIS/i Patient id: PT0000001 Name . . . . . : ELIJAH R MARCHETTI Plan . . . . . : PLAN0001 Status . . . . : A Deductible . . : 50.00 Ded met . . . : 50.00 Ded remaining : 0.00 Annual max . . : 1500.00 Annual used . : 1500.00 Annual remaining: 0.00 Patient found. F3=Exit Enter=Inquire
FieldType (DDS)Shows
IPAT9A input/outputPatient id keyed by the operator.
DNAME / DPLAN / DSTAToutputPatient name, plan id, status.
DDED / DDMET / DDREM14A outputPlan deductible / met / remaining (derived).
DAMAX / DAUSD / DAREM14A outputAnnual maximum / used / remaining (derived).
DMSG50A outputStatus line (found / not found / self-pay).

Ledger inquiry (DTLEDGIQ / DTLEDGD)

The app's subfile screen. Key a patient id and Enter; DTLEDGIQ clears the subfile, then loads it from the by-patient ledger path (DTLEDGP, SFLPAG(5) per page, SFLSIZ(20)), and only then displays — the classic clear-load-display discipline so the operator never sees a previous enquiry's rows. Each ledger line shows as a subfile row; a running open balance is displayed above the list. Roll pages; F3 exits.

Ledger Inquiry - DENTIS/i Patient id: PT0000004 Name . . . . . : NOAH B KRISTIANSEN Balance . . . : 55.00 Lin Ty Date Amount Memo 1 C 20260815 55.00 PROCEDURE CHARGE D0120 Patient found. F3=Exit Roll=Page Enter=Inquire
Both inquiry screens treat an LTYPE='A' ledger row (the adjudication audit row DTADJUD writes, carrying the insurance-paid amount for reference) as display-only: DTLEDGIQ shows it in the subfile but excludes it from the running balance, because the real money movements are the charge (C), the insurance payment (I) and the patient payment (X). Including the A row would double-count the insurance portion. The same exclusion is applied by DTSTMTRUN when it computes the statement balance.

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

Honest statement: DENTIS/i does not model a true four-eyes maker–checker / separate-authorization workflow. There is no "one user posts, a second user approves" step: a chart line is edited and posted by DTPOST in one pass, a claim is generated and then adjudicated by the batch programs with no approval gate, and payments are posted in full at posting time by DTPAYPOST. The control model the application does have is data-layer discipline:

In sum, the control posture is structural edit + durable ledger audit + accumulator/state gating, all enforced in the programs and the data model, rather than a segregation-of-duties approval workflow.

C. Batch Jobs & the Periodic Cycle ↑ top

DENTIS/i's processing runs as two CL-driven cycles: a daily cycle (DTDAILY) that turns charted procedures into posted, claimed, adjudicated revenue, and a monthly close cycle (DTMONTH) that posts payments, prints statements, reports aging, posts the GL, and rolls the plan year. Each cycle is a CL program that CALLs its RPG/COBOL steps in order; every step takes no parameters and DSPLYs a one-line result banner naming its counts.

-- the daily cycle: post charges, generate claims, adjudicate
CALL DENTIS/DTDAILY
-- or scheduled:
SBMJOB CMD(CALL PGM(DENTIS/DTDAILY)) JOB(DTDAILY)

-- the monthly close
SBMJOB CMD(CALL PGM(DENTIS/DTMONTH)) JOB(DTMONTH)

C.1 Full batch program set

ProgramCyclePurposeReadsWrites / result
DTREFLDsetupSeed reference data (fees, plans, providers, patients, opening accumulators).DTFEE(10) DTINS(2) DTPROV(4) DTPAT(5) DTACCUM(3); DSPLY FEES=/PLANS=/PROVS=/PATIENTS=/ACCUMS=.
DTPOSTdaily 1Edit + price each entered chart line, post it to the ledger as a charge.DTCHART DTFEE DTPAT DTPROVDTCHART→G/D, DTLEDG(C); DSPLY POSTED=/REJECT=/SKIP=.
DTCLGENdaily 2Group an insured patient's posted lines by appointment into one claim.DTAPPT DTCHART DTPATDTCLAIM DTCLAIML, DTCHART→C; DSPLY GENERATED=/SKIP=.
DTADJUDdaily 3Adjudicate each validated claim (deductible → coverage rate → annual-max cap).DTCLAIM DTCLAIML DTPAT DTINS DTFEE DTACCUMDTCLAIM/DTCLAIML→A, DTACCUM updated, DTLEDG(A); DSPLY ADJUDICATED=/SKIP=.
DTPAYPOSTmonthly 1Post insurance + patient payments against adjudicated claims; close the claim.DTCLAIMDTLEDG(I,X), DTCLAIM→X; DSPLY POSTED=/SKIP=.
DTSTMTRUNmonthly 2Sweep each active patient's ledger, age the balance, print a statement.DTPAT DTLEDGPDTSTMT, DTSTMTP print; DSPLY PRINTED=/SKIP=.
DTGLDSTmonthly 3Post the practice-revenue GL distribution (movement since last close).DTLEDG, DTGLDIST (SQL)DTGLDIST rows; DSPLY BATCH=/ROWS=/REVENUE=.
DTAGERPTmonthly 4ILE COBOL AR-aging audit report over the statement file.DTSTMTDSPLY TOTALBAL/CURRENT/DAYS30/DAYS60/DAYS90/STMTS.
DTYRRSTmonthly 5Close each current-year accumulator, open a fresh next-year one.DTACCUMDTACCUM→C + new year row; DSPLY RESET=/SKIP=.

DTDAILY = DTPOST → DTCLGEN → DTADJUD.   DTMONTH = DTPAYPOST → DTSTMTRUN → DTGLDST → DTAGERPT → DTYRRST.

C.2 Daily / monthly detail

Daily — DTDAILY (DTPOST → DTCLGEN → DTADJUD)

DTPOST reads every chart line; for each in state E it CHAINs the fee schedule (denial E1 on unknown/inactive code), the patient (E2 on unknown/inactive) and the provider (E3 on unknown/terminated). A clean line is priced at DTFEE.FFEE, posted to DTLEDG as a charge (LTYPE='C'), and moved to posted (CSTAT='G'); a broken line is denied (CSTAT='D', CDENCD set). Note the charge is posted for every posted line including self-pay patients — self-pay simply never becomes a claim. DTCLGEN reads each appointment, skips self-pay (blank PINSPLN), derives the claim number from the appointment number and, if that claim does not already exist, totals the appointment's posted lines into one DTCLAIM (status V) with one DTCLAIML per line, marking each chart line claimed (CSTAT='C'). DTADJUD is the engine (section F.2).

Expected DSPLY (dt_daily scenario: 8 chart lines, 3 insured claims):
  DTPOST POSTED=5 REJECT=3        5 clean lines posted, 3 broken denied E1/E2/E3
  DTCLGEN GENERATED=3 SKIP=...    one claim per insured appointment; self-pay skipped
  DTADJUD ADJUDICATED=3 SKIP=0    all 3 validated claims adjudicated

Monthly — DTMONTH (DTPAYPOST → DTSTMTRUN → DTGLDST → DTAGERPT → DTYRRST)

DTPAYPOST reads each adjudicated claim (CSTAT='A') and writes two negative ledger credits — the insurer's remittance (LTYPE='I', -CINSPD) and the patient's payment (LTYPE='X', -CPATRSP, assumed collected in full) — then closes the claim (CSTAT='X'). DTSTMTRUN sweeps each active patient's ledger (excluding the A audit rows), sums a running balance, ages it into four buckets (CURRENT / 0–30 / 31–60 / 61–90) using a 30-day-month day count, and, for any patient with a non-zero balance, writes a DTSTMT snapshot and prints DTSTMTP; a clean (zero-balance) account is skipped. DTGLDST totals the ledger by posting type and posts the movement since the last close as three balanced debit/credit pairs (revenue, insurance cash, patient cash). DTAGERPT (COBOL) re-totals the statement file for the front-office reconciliation. DTYRRST closes each current-year accumulator (ASTAT='C') and opens a fresh zeroed next-year row.

Expected DSPLY (dt_monthly scenario: one insured 110.00 claim + one self-pay 55.00 charge):
  DTPAYPOST POSTED=1 SKIP=0                  insured claim paid; self-pay has no claim
  DTSTMTRUN PRINTED=1                         only the self-pay 55.00 balance statements
  DTGLDST BATCH=202609 ROWS=6 REVENUE=165     110 + 55 charged; 6 GL rows (3 pairs)
  DTAGERPT TOTALBAL 55.00 / CURRENT 55.00     COBOL AR aging total
  DTYRRST RESET=3                             3 accumulators rolled 2026 -> 2027
The insured patient in that scenario nets to a 0.00 balance (110.00 charge − 60.00 insurance − 50.00 patient) and so is correctly not statemented; the self-pay patient's 55.00 is CURRENT because it was posted the same day as the run date. The GL revenue figure (165.00) is the sum of the two charges, independent of who pays.

C.3 Ordering & dependencies

D. Data Files (data dictionary) ↑ top

All files are in library DENTIS, grounded in the DDS/SQL source in src/sources.mjs. Dates are stored as signed numeric YYYYMMDD (or YYYYMM for a plan year / run period). Money is packed 11P 2; coverage rates are packed 5P 4 (e.g. 0.8000 = 80%).

DTPAT — Patient master (key PATID)

FieldTypeMeaning
PATID9APatient id (key), e.g. PT0000001.
PNAME30APatient name.
PDOB8S 0Birth date (YYYYMMDD).
PPHONE12APhone.
PINSPLN8AInsurance plan id (FK→DTINS); blank = self-pay.
PSUBID9ASubscriber id (a dependent points at the subscriber's id).
PSTAT1AA active, I inactive (chart lines against an I patient are denied E2).

DTPROV — Provider master (key PROVID)

FieldTypeMeaning
PROVID9AProvider id (key), e.g. PRV000001.
PVNAME30AProvider name.
PVTYPE1AD dentist, H hygienist.
PVLIC12ALicence number.
PVSTAT1AA active, T terminated (chart lines against a T provider are denied E3).

DTFEE — Procedure / fee schedule (key PROCCD)

FieldTypeMeaning
PROCCD5AADA-style procedure code (key), e.g. D1110.
PDESC30AProcedure description.
FCAT1ACoverage category: P preventive (100%), B basic (80%), M major (50%), O orthodontic (50%).
FFEE11P 2Practice's standard fee (the allowance the plan pays a percentage of).
FSTAT1AA active, I inactive/retired (denied E1).

Seeded schedule (10 codes): preventive D0120 55.00 / D1110 110.00 / D0274 65.00; basic D2140 180.00 / D2330 195.00 / D3310 900.00; major D2740 1250.00 / D6010 2400.00; ortho D8080 5200.00; and one deliberately INACTIVE code D9999 (100.00, FSTAT='I') to exercise the E1 edit.

DTINS — Insurance plan (key PLANID)

FieldTypeMeaning
PLANID8APlan id (key), e.g. PLAN0001.
IDESC30APlan description.
IDEDUCT11P 2Annual per-patient deductible (applied once per plan year, before coverage rates).
IANNMAX11P 2Annual benefit maximum the plan will pay per patient per year (the cap that protects the plan).
ICOVP / ICOVB / ICOVM5P 4Coverage rates for preventive / basic / major (and ortho uses the major rate).
ISTAT1AA active.

Seeded plans: PLAN0001 STANDARD PPO — deductible 50.00, 100/80/50 rates, annual max 1500.00; PLAN0002 PREMIER PPO — deductible 25.00, richer 100/90/60 rates, annual max 2500.00.

DTAPPT — Appointment (key APPTNO)

FieldTypeMeaning
APPTNO9AAppointment number (key), e.g. APT000001.
PATID / PROVID9APatient / provider (FK).
ADT / ATIME8S 0 / 4S 0Appointment date (YYYYMMDD) / time (HHMM).
AREASON30AReason / description.
ASTAT1AS scheduled, K kept/completed, C cancelled, N no-show.

DTCHART — Charted procedure line (key CHARTNO)

FieldTypeMeaning
CHARTNO9AChart-line number (key), e.g. CHT000001.
APPTNO / PATID / PROVID9AOwning appointment / patient / provider.
CDT8S 0Chart (service) date (YYYYMMDD).
PROCCD5AADA procedure code charted (FK→DTFEE).
TOOTH / SURF2A / 4AChairside references (TOOTH blank for whole-mouth procedures).
CFEE11P 2Priced fee (set from DTFEE by DTPOST).
CSTAT1AE entered, G posted-to-ledger, C claimed, D denied.
CDENCD2ADenial edit code when CSTAT=D (E1/E2/E3).

DTCHARTP is a logical file over DTCHART keyed (PATID, CHARTNO) — the chart-lines-by-patient access path.

DTLEDG — Patient ledger / AR (key LSEQ)

FieldTypeMeaning
LSEQ8S 0Ledger sequence (key) — a stable, derived value per posting program (see F.3), so a re-run finds its own row.
LTYPE1AC charge, I insurance payment, X patient payment, A adjudication audit row.
PATID9APatient (FK).
LDT8S 0Posting date (YYYYMMDD).
LAMT11P 2Signed amount (charges positive, payments negative — balance is a plain SUM).
LREF9AReference (chart-line or claim number).
LMEMO30AFree-text memo, e.g. PROCEDURE CHARGE D1110.

DTLEDGP is a logical file over DTLEDG keyed (PATID, LSEQ) — the ledger-by-patient path the statement run and the ledger inquiry walk. Note LTYPE='A' rows are reference-only (the insurance-paid amount at adjudication) and are excluded from balance sums.

DTCLAIM — Claim header (key CLMNO)

FieldTypeMeaning
CLMNO9AClaim number (key), derived from the appointment number.
PATID / PROVID9APatient / provider.
CSVCDT8S 0Service date (YYYYMMDD).
CFEE11P 2Billed fee (roll-up of its lines).
CALLOWD11P 2Allowed amount (= billed fee, in this dental model).
CDEDAPP11P 2Deductible applied on this claim.
CINSPD11P 2Insurance paid.
CPATRSP11P 2Patient responsibility. INVARIANT: CINSPD + CPATRSP = CALLOWD.
CSTAT1AE entered/generated, V validated, A adjudicated, X closed (paid).
CDENCD2AClaim-level denial code (unused by the seeded happy path).
CPLANYR4S 0Plan year (from the service date), the accumulator key.

DTCLAIML — Claim lines (composite key CLMNO, CLINE)

FieldTypeMeaning
CLMNO / CLINE9A / 3S 0Claim number + line number (composite key).
CHARTNO9AThe charted line this claim line carries.
PROCCD5AProcedure code (drives the line's coverage rate at adjudication).
LFEE11P 2Line fee.
LALLOWD / LINSPD / LPATRSP11P 2Line allowed / insurance / patient (set at adjudication).
LSTAT1AV validated, A adjudicated.

DTCLAIMP is a logical file over DTCLAIM keyed (PATID, CLMNO) — the claims-by-patient path for a patient-history enquiry.

DTACCUM — Benefit accumulator (composite key PATID, ACCYR)

FieldTypeMeaning
PATID / ACCYR9A / 4S 0Patient + plan year (composite key).
DEDMET11P 2Deductible satisfied so far this plan year.
ANNUSED11P 2Annual benefit paid so far (the value the cap tests against).
ACLMCNT5P 0Claims adjudicated against this accumulator.
AINSYTD11P 2Insurance-paid year-to-date.
ASTAT1AA active (current year), C closed (rolled by DTYRRST).

DTSTMT — Statement-run snapshot (key STMTNO)

FieldTypeMeaning
STMTNO9AStatement number (key), derived from the patient id.
PATID9APatient.
SRUN6S 0Statement run period (YYYYMM).
SBAL11P 2Total open balance.
SCUR / S30 / S60 / S9011P 2Aged buckets: CURRENT / 0–30 / 31–60 / 61–90 days.
SDT8S 0Statement date (YYYYMMDD).

DTGLDIST — Practice-revenue GL distribution (SQL table, PK GDSEQ)

FieldTypeMeaning
GDSEQDECIMAL(8,0)GL sequence (PK), continued from MAX(GDSEQ).
GDBATCHDECIMAL(6,0)Batch period (YYYYMM).
GDACCTCHAR(12)Account: 1200-AR, 4000-REV, 1010-CASH.
GDDRCRCHAR(1)D debit, C credit.
GDAMTDECIMAL(11,2)Posting amount.
GDCATCHAR(3)Movement category: REV, INS, PAT.
GDREFCHAR(9)Reference (REVENUE / INSPAY / PATPAY).
GDDTDECIMAL(8,0)Posting date (YYYYMMDD).

Index DTGLDACC on (GDACCT, GDDRCR). The distribution posts three balanced pairs: revenue DR 1200-AR / CR 4000-REV; insurance cash DR 1010-CASH / CR 1200-AR; patient cash DR 1010-CASH / CR 1200-AR.

Relationships

E. Operations Runbook ↑ top

E.1 Day-in-the-life

The front office charts procedures against appointments during the day; the daily cycle turns those charts into posted, adjudicated revenue.

  1. Pre-check. Confirm the job's library list includes DENTIS (or rely on the cycle's own ADDLIBLE). On a fresh library, run CALL DENTIS/DTSETUP then CALL DENTIS/DTREFLD once to create objects and seed reference data.
  2. Charting for the day's kept appointments is captured into DTCHART (chart lines in state E), each citing a procedure code and tooth/surface.
  3. Submit the daily cycle: SBMJOB CMD(CALL PGM(DENTIS/DTDAILY)).
  4. Post-check the three DSPLY banners (see below).
  5. Handle interactive enquiries as they arise: CALL DENTIS/DTMENU → option 1 for a patient's live benefit position, option 2 for a patient's ledger.

Post-checks after the daily cycle:

E.2 Month-end close

  1. Confirm every business day's daily cycle for the month has run (all claims adjudicated, no V claims left outstanding).
  2. Submit the monthly cycle: SBMJOB CMD(CALL PGM(DENTIS/DTMONTH)).
  3. Confirm the five DSPLY banners: DTPAYPOST POSTED=, DTSTMTRUN PRINTED=, DTGLDST BATCH= ROWS= REVENUE=, DTAGERPT TOTALBAL/CURRENT/..., DTYRRST RESET=.
  4. Reconcile the figures (below).

Reconciling figures (the same ones the volume battle checks against hand-derived numbers):

Ad-hoc GL review: SELECT GDACCT, GDDRCR, GDCAT, SUM(GDAMT) FROM DENTIS.DTGLDIST GROUP BY GDACCT, GDDRCR, GDCAT lists the three balanced pairs the close posted.

E.3 Failure & re-run rules

Every program in DENTIS/i is designed to be safely re-runnable: each derives a stable key for the row it would write and refuses to double-post if that row already exists (section F.3). This is the single most important operational property — a cycle that failed partway can simply be re-submitted.

SituationBehaviourAction
Daily cycle fails partwayPosted/claimed/adjudicated rows are stamped and keyed stably; un-processed ones are untouched.Re-submit DTDAILY: already-processed lines/claims are no-ops (guards find their stable rows), the rest complete. Idempotent.
Re-run DTADJUDAdjudicated claims have a 20000000+claim ledger row already.Second run reports ADJUDICATED=0; the deductible is not applied twice and annual used is not doubled. Safe.
Re-run DTCLGENThe claim number is derived from the appointment number.Second run reports GENERATED=0; no duplicate claim, no chart line re-claimed. Safe.
Re-run DTPAYPOSTTwo stable ledger keys per claim (30000000+ / 50000000+).Second run reports POSTED=0; no duplicate payment postings. Safe.
Re-run DTSTMTRUNStatement number derived from the patient id; same hard-coded run number.Second run reports PRINTED=0; no duplicate statement. Safe.
Re-run DTGLDSTPosts the movement since the last close, per account.Second run posts no new rows. Safe.
Re-run DTYRRSTCHAINs the (patient, next-year) accumulator.Second run reports RESET=0; no third accumulator row. Safe.
Chart line denied (E1/E2/E3)Line is CSTAT='D' with its CDENCD; no charge posted.Correct the reference data (activate code/patient/provider) or the chart line, reset it to E, re-run DTPOST.
Because every money movement is journaled to DTLEDG (charge / insurance payment / patient payment) and every adjudication leaves an A audit row, any cycle's effect is fully reconstructable after the fact for reconciliation and recovery. The stable-key discipline means a partial failure is recovered by re-running, not by manual clean-up.

F. Developer Reference ↑ top

The complete program surface, from src/sources.mjs. All objects are in library DENTIS. The RPG programs are ILE RPG (mixed fixed-form C-specs and /free blocks); DTGLDST adds embedded SQL; DTAGERPT is ILE COBOL. Sources are held as JS string constants and loaded into DENTIS's source physical files by src/seed.mjs (seedDentis()), mirroring the emulator's DEMOLIB sample convention; the app subtree is never auto-run by the engine.

F.1 Programs (16)

DTREFLD (RPGLE) — seed reference data
Writes the fee schedule (10 codes), 2 insurance plans, 4 providers, 5 patients and 3 opening accumulators (plan year 2026, all zero). DSPLYs the counts. Includes the deliberate edge data: an inactive procedure code (D9999), a terminated provider (PRV000004), an inactive patient (PT0000005), a self-pay patient (PT0000004, blank plan), and a subscriber/dependent pair (PT0000002→PT0000001).
DTPOST (RPGLE) — daily step 1, chart-line edit + ledger charge posting
Reads each chart line; for CSTAT='E' lines, CHAINs DTFEE (E1), DTPAT (E2), DTPROV (E3). A clean line is priced at FFEE, written to DTLEDG as a C charge keyed 10000000 + chart-line digits, and moved to G; a broken line is denied (D + CDENCD). Re-runnable via the stable ledger key (CHAIN(EN) guard).
DTCLGEN (RPGLE) — daily step 2, claim generation by appointment
For each appointment, skips self-pay patients, derives the claim number from the appointment number, and if that claim does not exist, two-passes DTCHART: first to total the appointment's G lines, then to write one DTCLAIM (status V) plus one DTCLAIML per line, marking each chart line C. Re-runnable: the derived claim number CHAIN(EN)s and LEAVESRs if already present.
DTADJUD (SQLRPGLE-shaped RPGLE) — daily step 3, the adjudication engine
The program the whole application exists for — see F.2.
DTPAYPOST (RPGLE) — monthly step 1, payment posting
For each A claim, writes two negative ledger credits: insurance (I, -CINSPD, keyed 30000000+) and patient (X, -CPATRSP, keyed 50000000+), then closes the claim (X). Both postings share one guard (the insurance key); a re-run finds it and skips.
DTSTMTRUN (RPGLE) — monthly step 2, statement / aging run
For each active patient, sweeps DTLEDGP (excluding A rows), sums the balance, ages it with a 30-day-month day count, and for a non-zero balance writes DTSTMT + prints DTSTMTP. Statement number derived from the patient id (60000000+); re-run finds it and prints nothing.
DTYRRST (RPGLE) — monthly step 3, plan-year accumulator reset
Closes each ASTAT='A' current-year accumulator (→C) and opens a fresh zeroed next-year row. Guards against re-rolling a row it just wrote (skips ACCYR > current year) and against a second run (CHAINs the next-year composite key). KLISTs declared at top level.
DTGLDST (SQLRPGLE) — monthly, practice-revenue GL distribution
Totals the ledger by posting type, then posts the movement since the last close (comparing against SUM(GDAMT) already in DTGLDIST per account) as three balanced DR/CR pairs. Uses free-form embedded EXEC SQL SELECT ... INTO / INSERT; only counts a row when SQLCOD = 0. Compiled with CRTBNDRPG (see F.5).
DTAGERPT (CBLLE) — monthly, COBOL AR-aging audit report
Hand-written ILE COBOL: sequentially reads DTSTMT, accumulates the four aged buckets and the total, and DISPLAYs TOTALBAL / CURRENT / DAYS30 / DAYS60 / DAYS90 / STMTS. The report the front office reconciles the statement run against.
DTPATIQ (RPGLE, WORKSTN) — interactive benefit inquiry
Reads a patient id, CHAINs DTPAT / DTINS / (patient, 2026) DTACCUM, and displays the derived deductible-remaining and annual-remaining position; self-pay shows N/A.
DTLEDGIQ (RPGLE, WORKSTN + subfile) — interactive ledger inquiry
Clear-load-display subfile over DTLEDGP; shows each ledger line and a running balance (excluding A rows). SFLSIZ(20)/SFLPAG(5), roll keys.
DTMENU (RPGLE, WORKSTN) — operator main menu
Option 1 CALLs DTPATIQ, option 2 CALLs DTLEDGIQ.
DTSETUP / DTDAILY / DTMONTH (CLP)
DTSETUP DLTFs then CRTPFs/CRTLFs/CRTDSPFs/CRTPRTF every file, RUNSQLSTMs the GL DDL + index, and CRTBNDRPGs / CRTBNDCBLs every program. DTDAILY and DTMONTH ADDLIBLE DENTIS and CALL their steps in order.

F.2 The adjudication engine (DTADJUD)

For every validated claim (CSTAT='V'), in claim-number order, DTADJUD:

  1. Re-runnability guard first. Derives ledger key 20000000 + claim digits and CHAIN(EN)s DTLEDG; if the row exists, the claim is already adjudicated — skip.
  2. Reads the plan & accumulator. CHAINs the patient (skip if self-pay), the plan (deductible, annual max, the three coverage rates), and the (patient, plan-year) accumulator (composite ACCKEY); computes remaining deductible and remaining annual maximum, each floored at zero.
  3. Line by line (READE over DTCLAIML by CLMNO): pick the coverage rate from the line's procedure category (P→preventive, M/O→major, else basic); draw the line's deductible from the running remaining-deductible pool (min(pool, line fee)); insurance = remainder × rate, then capped by the running remaining-annual-max pool; patient = fee − insurance. Each line's split is UPDATEd onto DTCLAIML (re-CHAINed by its composite LINKEY so a partial key can't match line 1 every time; SETGT restores the sequential position).
  4. Writes the accumulator back: DEDMET += deductible applied, ANNUSED += insurance paid, ACLMCNT += 1, AINSYTD += insurance paid (or writes a new accumulator row if none existed).
  5. Writes the durable A ledger row (the key that makes the claim un-repeatable) and stamps the claim header (CALLOWD / CDEDAPP / CINSPD / CPATRSP, CSTAT='A').

Worked example from dt_daily.mjs (patient PT0000001, PLAN0001: deductible 50.00, rates 100/80/50, annual max 1500.00):

ClaimLines (fee, category)DeductibleInsurancePatientAnnual used after
1D1110 110.00 prev + D2140 180.00 basic (allowed 290.00)50.00 (drawn on line 1)60.00 + 144.00 = 204.0050.00 + 36.00 = 86.00204.00
2D2740 1250.00 major (deductible already met)0.001250 × .50 = 625.00625.00829.00
3D6010 2400.00 major (annual max caps)0.00uncapped 1200.00, but only 671.00 max remained → 671.002400 − 671 = 1729.001500.00 (maxed)

Claim 3 proves the cap: 1500.00 − 829.00 = 671.00 of annual benefit remained, so the 671.00 figure only works out if the accumulator genuinely carried 829.00 forward from claims 1+2 — and the invariant still holds (671.00 + 1729.00 = 2400.00). The accumulator ends at exactly 1500.00, never over.

F.3 Re-runnability & stable ledger keys

Every posting program derives a stable key from the business object it processes and refuses to double-post if the row already exists (CHAIN(EN) — the indicator is set ON when the row is not found, so "indicator ON = safe to write"). This is what makes the whole application re-runnable end to end.

ProgramStable keyDerived from
DTPOSTDTLEDG LSEQ = 10000000 + chart digitsthe chart-line number
DTCLGENDTCLAIM CLMNO = appointment digitsthe appointment number
DTADJUDDTLEDG LSEQ = 20000000 + claim digitsthe claim number
DTPAYPOSTDTLEDG 30000000+ (ins) & 50000000+ (patient)the claim number
DTSTMTRUNDTSTMT STMTNO = 60000000 + patient digitsthe patient id
DTGLDSTcompares SUM(GDAMT) already posted per accountthe ledger totals vs. prior GL
DTYRRSTCHAIN the (patient, next-year) accumulatorthe composite accumulator key

The disjoint numeric ranges (10M / 20M / 30M / 50M / 60M) keep each program's ledger keys from colliding with another's, so all ledger sequence numbers stay unique across the app.

F.4 Edit / denial codes (DTPOST)

CodeMeaning
E1Unknown or inactive procedure code (DTFEE CHAIN failed or FSTAT≠'A').
E2Unknown or inactive patient (DTPAT CHAIN failed or PSTAT≠'A').
E3Unknown or terminated provider (DTPROV CHAIN failed or PVSTAT≠'A').

A denied line is stamped CSTAT='D' with the code in CDENCD and never becomes a charge or a claim line. The seeded data deliberately includes one of each (D9999 retired → E1, PT0000005 inactive → E2, PRV000004 terminated → E3) so the edit path is exercised by the daily battle.

F.5 Developer notes & platform finding

Self-documented app-level fixes (in code comments, not platform bugs). Three mistakes made and fixed during development are documented at the fix site in src/sources.mjs rather than as platform findings: an RPG free-form/fixed-form mixing issue (a /free IF cannot bracket a fixed-form CHAIN — see the fixed-form guards in DTPOST/DTLEDGIQ), an infinite accumulator-roll loop in DTYRRST (fixed by skipping ACCYR > current year so the sequential READ never re-rolls a row it just wrote), and a YYYYMMDD-subtraction aging bug in DTSTMTRUN (fixed with a 30-day-month day count so aging is consistent across a month boundary).

Platform finding PG-DENTIS-001 (honest reference). While building the app, one genuine emulator issue was reproduced and reported (dental-app/FINDINGS.md): the CRTSQLRPGI CL command — a real, standard IBM i "SQL RPG precompile + compile" command — is not implemented as a distinct command here, yet instead of failing loudly with SFF0006 (command not found) the way a genuinely unknown command does, it silently falls through to a generic path: it still compiles the RPG, but ignores the PGM(lib/name) parameter, creates the program object under the literal name UNDEFINED in the current library, and reports success. The net effect is broken automation with no error — a later CALL of the intended program fails far from the real cause. Severity is medium (silent wrong behaviour, not a crash). How this app avoids it: the one program with embedded SQL, DTGLDST, is compiled with CRTBNDRPG (which handles the embedded SQL correctly), matching the working precedent in the claims-app HCGLDST program — so DENTIS/i never invokes CRTSQLRPGI. The finding is reported per the read-only discipline (no engine code was modified); the fix belongs in the emulator's command dispatch, which should raise SFF0006 for the unimplemented command.

G. Glossary ↑ top

ADA procedure code
A standardised 5-character dental procedure code (e.g. D1110 cleaning, D2740 crown). The fee schedule (DTFEE) is keyed by it, and each code carries a coverage category.
Accumulator (DTACCUM)
The per-(patient, plan-year) benefit position: deductible met, annual benefit used, claim count, insurance-paid year-to-date. Adjudication reads it, applies against it, and writes it back, so each claim pays in light of the prior ones. The heart of dental benefit administration.
Adjudication
Splitting a claim's allowed amount into insurance-paid and patient-responsibility, applying the deductible, per-line coverage rates, and the annual-maximum cap (DTADJUD).
Annual benefit maximum
The most a plan will pay a patient in a plan year (DTINS.IANNMAX). Once reached, the plan pays 0.00 more and the patient owes the rest — a cap that protects the plan (the mirror image of a medical out-of-pocket cap, which protects the member).
Coverage category / 100/80/50
The classic dental benefit design: preventive covered 100%, basic 80%, major 50%. Each procedure's category (DTFEE.FCAT) selects the plan rate (DTINS.ICOVP/ICOVB/ICOVM) applied to its line.
Deductible
The annual amount a patient pays before coverage rates apply (DTINS.IDEDUCT), tracked once per plan year in DTACCUM.DEDMET.
Chart line (DTCHART)
One row per procedure performed at a visit, citing an ADA code and tooth/surface. The application's spine: an appointment becomes one or more chart lines, which become charges and (for insured patients) claim lines.
Claim / claim line (DTCLAIM / DTCLAIML)
An insurance claim is one header (roll-up amounts) plus one line per charted procedure. DTCLGEN generates one claim per insured appointment; DTADJUD adjudicates it.
Idempotent / re-runnable
Safe to run again with the same result. Every posting program derives a stable key and refuses to double-post (section F.3), so a failed cycle is recovered by re-submitting it.
Ledger (DTLEDG)
The durable patient AR trail: charges (C), insurance payments (I), patient payments (X) and adjudication audit rows (A). The account balance is a plain SUM of the signed amounts, excluding the reference-only A rows.
Self-pay
A patient with no insurance plan (blank DTPAT.PINSPLN). Their charges post straight to the ledger and are never grouped into a claim.
SBMJOB
Submit Job — the IBM i command that queues a program to run as a batch job (e.g. SBMJOB CMD(CALL PGM(DENTIS/DTDAILY))).
Stable / derived key
A ledger/statement/claim key computed deterministically from a business object (chart line, appointment, claim, patient) so a re-run finds its own row and does not duplicate it.
Subfile
A 5250 display construct listing many rows on one screen (DDS SFL/SFLCTL). DTLEDGIQ's ledger list is a subfile; it is cleared, loaded, then displayed on each enquiry.
WORKSTN / EXFMT
The RPG device file and operation for an interactive 5250 screen: EXFMT writes a record format and reads the operator's response in one step (used by DTMENU, DTPATIQ, DTLEDGIQ).