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.
DENTIS/i runs the revenue life of a dental practice, from a charted procedure to a posted GL entry:
DTCHART) citing an ADA procedure code and a tooth/surface; the daily post
(DTPOST) edits each line, prices it off the fee schedule, and posts it to the patient
ledger as a charge.DTCLGEN); self-pay patients are never
claimed.DTADJUD) against the patient's
plan and (patient, plan-year) accumulator: deductible first, then the per-line coverage-category
rate (the classic dental 100/80/50 design), then a cap at the plan's annual benefit maximum.DTPAYPOST), prints aged statements
(DTSTMTRUN), reports AR aging (DTAGERPT), posts the practice-revenue GL
distribution (DTGLDST), and resets the plan-year accumulators (DTYRRST).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.
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.
| Object | Type | Role |
|---|---|---|
| DTPAT | PF | Patient master. |
| DTPROV | PF | Provider (dentist / hygienist) master. |
| DTFEE | PF | Procedure / fee schedule (ADA-style codes). |
| DTINS | PF | Insurance plan (deductible, annual max, coverage rates). |
| DTAPPT | PF | Appointment. |
| DTCHART | PF | Charted treatment procedure line (the app's spine). |
| DTLEDG | PF | Patient ledger / AR (the durable audit trail). |
| DTCLAIM | PF | Insurance claim header. |
| DTCLAIML | PF | Claim lines (one per charted procedure). |
| DTACCUM | PF | Per-(patient, plan-year) benefit accumulator. |
| DTSTMT | PF | Statement-run aged-balance snapshot. |
| DTGLDIST | SQL table | Practice-revenue GL distribution. |
| DTCHARTP / DTLEDGP / DTCLAIMP | LF (3) | By-PATID access paths over chart / ledger / claim. |
| DTPATD / DTLEDGD / DTMENUD | DSPF (3) | Patient inquiry / ledger subfile / menu display files. |
| DTSTMTP | PRTF | Patient-statement printer file. |
| DTREFLD | RPGLE | Seed reference data. |
| DTPOST / DTCLGEN / DTADJUD | RPGLE (3) | Daily cycle: post, generate, adjudicate. |
| DTPAYPOST / DTSTMTRUN / DTYRRST | RPGLE (3) | Monthly cycle: pay-post, statements, year reset. |
| DTGLDST | SQLRPGLE | Practice-revenue GL distribution (embedded SQL). |
| DTPATIQ / DTLEDGIQ / DTMENU | RPGLE (3) | Interactive: benefit inquiry / ledger subfile / menu. |
| DTAGERPT | CBLLE | ILE COBOL AR-aging audit report. |
| DTSETUP | CLP | Create every object & compile every program. |
| DTDAILY / DTMONTH | CLP (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.
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 this | Type on the command line |
|---|---|
| Open the operator main menu | CALL DENTIS/DTMENU |
| Patient benefit inquiry (direct) | CALL DENTIS/DTPATIQ |
| Patient ledger inquiry (direct) | CALL DENTIS/DTLEDGIQ |
| Run the daily treatment cycle | CALL DENTIS/DTDAILY (or SBMJOB it) |
| Run the monthly close cycle | CALL DENTIS/DTMONTH |
| Build/compile everything, then seed reference data | CALL 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.
A plain menu: option 1 CALLs DTPATIQ, option 2 CALLs DTLEDGIQ,
F3 exits. An invalid option shows "Invalid option." and re-displays.
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."
| Field | Type (DDS) | Shows |
|---|---|---|
| IPAT | 9A input/output | Patient id keyed by the operator. |
| DNAME / DPLAN / DSTAT | output | Patient name, plan id, status. |
| DDED / DDMET / DDREM | 14A output | Plan deductible / met / remaining (derived). |
| DAMAX / DAUSD / DAREM | 14A output | Annual maximum / used / remaining (derived). |
| DMSG | 50A output | Status line (found / not found / self-pay). |
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.
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.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:
ANNUSED; the invariant
(insurance + patient = allowed) is enforced on every line and every claim header.DTPOST only posts CSTAT='E' chart lines; DTCLGEN only groups
CSTAT='G' lines for insured patients; DTADJUD only adjudicates
CSTAT='V' claims; DTPAYPOST only pays CSTAT='A' claims;
DTYRRST only rolls ASTAT='A' accumulators for the current year.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.
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)
| Program | Cycle | Purpose | Reads | Writes / result |
|---|---|---|---|---|
| DTREFLD | setup | Seed reference data (fees, plans, providers, patients, opening accumulators). | — | DTFEE(10) DTINS(2) DTPROV(4) DTPAT(5) DTACCUM(3); DSPLY FEES=/PLANS=/PROVS=/PATIENTS=/ACCUMS=. |
| DTPOST | daily 1 | Edit + price each entered chart line, post it to the ledger as a charge. | DTCHART DTFEE DTPAT DTPROV | DTCHART→G/D, DTLEDG(C); DSPLY POSTED=/REJECT=/SKIP=. |
| DTCLGEN | daily 2 | Group an insured patient's posted lines by appointment into one claim. | DTAPPT DTCHART DTPAT | DTCLAIM DTCLAIML, DTCHART→C; DSPLY GENERATED=/SKIP=. |
| DTADJUD | daily 3 | Adjudicate each validated claim (deductible → coverage rate → annual-max cap). | DTCLAIM DTCLAIML DTPAT DTINS DTFEE DTACCUM | DTCLAIM/DTCLAIML→A, DTACCUM updated, DTLEDG(A); DSPLY ADJUDICATED=/SKIP=. |
| DTPAYPOST | monthly 1 | Post insurance + patient payments against adjudicated claims; close the claim. | DTCLAIM | DTLEDG(I,X), DTCLAIM→X; DSPLY POSTED=/SKIP=. |
| DTSTMTRUN | monthly 2 | Sweep each active patient's ledger, age the balance, print a statement. | DTPAT DTLEDGP | DTSTMT, DTSTMTP print; DSPLY PRINTED=/SKIP=. |
| DTGLDST | monthly 3 | Post the practice-revenue GL distribution (movement since last close). | DTLEDG, DTGLDIST (SQL) | DTGLDIST rows; DSPLY BATCH=/ROWS=/REVENUE=. |
| DTAGERPT | monthly 4 | ILE COBOL AR-aging audit report over the statement file. | DTSTMT | DSPLY TOTALBAL/CURRENT/DAYS30/DAYS60/DAYS90/STMTS. |
| DTYRRST | monthly 5 | Close each current-year accumulator, open a fresh next-year one. | DTACCUM | DTACCUM→C + new year row; DSPLY RESET=/SKIP=. |
DTDAILY = DTPOST → DTCLGEN → DTADJUD. DTMONTH = DTPAYPOST → DTSTMTRUN → DTGLDST → DTAGERPT → DTYRRST.
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
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
G lines are claimable), and DTCLGEN before DTADJUD (only generated
V claims are adjudicable). DTDAILY enforces this order.A) claims, so
the month's dailies must have run; DTSTMTRUN and DTGLDST read the ledger that DTPOST/DTPAYPOST
built, so they must follow payment posting.DTSETUP then DTREFLD must
run before any cycle, to create the objects and seed the fee schedule / plans / providers /
patients / opening accumulators.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%).
| Field | Type | Meaning |
|---|---|---|
| PATID | 9A | Patient id (key), e.g. PT0000001. |
| PNAME | 30A | Patient name. |
| PDOB | 8S 0 | Birth date (YYYYMMDD). |
| PPHONE | 12A | Phone. |
| PINSPLN | 8A | Insurance plan id (FK→DTINS); blank = self-pay. |
| PSUBID | 9A | Subscriber id (a dependent points at the subscriber's id). |
| PSTAT | 1A | A active, I inactive (chart lines against an I patient are denied E2). |
| Field | Type | Meaning |
|---|---|---|
| PROVID | 9A | Provider id (key), e.g. PRV000001. |
| PVNAME | 30A | Provider name. |
| PVTYPE | 1A | D dentist, H hygienist. |
| PVLIC | 12A | Licence number. |
| PVSTAT | 1A | A active, T terminated (chart lines against a T provider are denied E3). |
| Field | Type | Meaning |
|---|---|---|
| PROCCD | 5A | ADA-style procedure code (key), e.g. D1110. |
| PDESC | 30A | Procedure description. |
| FCAT | 1A | Coverage category: P preventive (100%), B basic (80%), M major (50%), O orthodontic (50%). |
| FFEE | 11P 2 | Practice's standard fee (the allowance the plan pays a percentage of). |
| FSTAT | 1A | A 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.
| Field | Type | Meaning |
|---|---|---|
| PLANID | 8A | Plan id (key), e.g. PLAN0001. |
| IDESC | 30A | Plan description. |
| IDEDUCT | 11P 2 | Annual per-patient deductible (applied once per plan year, before coverage rates). |
| IANNMAX | 11P 2 | Annual benefit maximum the plan will pay per patient per year (the cap that protects the plan). |
| ICOVP / ICOVB / ICOVM | 5P 4 | Coverage rates for preventive / basic / major (and ortho uses the major rate). |
| ISTAT | 1A | A 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.
| Field | Type | Meaning |
|---|---|---|
| APPTNO | 9A | Appointment number (key), e.g. APT000001. |
| PATID / PROVID | 9A | Patient / provider (FK). |
| ADT / ATIME | 8S 0 / 4S 0 | Appointment date (YYYYMMDD) / time (HHMM). |
| AREASON | 30A | Reason / description. |
| ASTAT | 1A | S scheduled, K kept/completed, C cancelled, N no-show. |
| Field | Type | Meaning |
|---|---|---|
| CHARTNO | 9A | Chart-line number (key), e.g. CHT000001. |
| APPTNO / PATID / PROVID | 9A | Owning appointment / patient / provider. |
| CDT | 8S 0 | Chart (service) date (YYYYMMDD). |
| PROCCD | 5A | ADA procedure code charted (FK→DTFEE). |
| TOOTH / SURF | 2A / 4A | Chairside references (TOOTH blank for whole-mouth procedures). |
| CFEE | 11P 2 | Priced fee (set from DTFEE by DTPOST). |
| CSTAT | 1A | E entered, G posted-to-ledger, C claimed, D denied. |
| CDENCD | 2A | Denial 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.
| Field | Type | Meaning |
|---|---|---|
| LSEQ | 8S 0 | Ledger sequence (key) — a stable, derived value per posting program (see F.3), so a re-run finds its own row. |
| LTYPE | 1A | C charge, I insurance payment, X patient payment, A adjudication audit row. |
| PATID | 9A | Patient (FK). |
| LDT | 8S 0 | Posting date (YYYYMMDD). |
| LAMT | 11P 2 | Signed amount (charges positive, payments negative — balance is a plain SUM). |
| LREF | 9A | Reference (chart-line or claim number). |
| LMEMO | 30A | Free-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.
| Field | Type | Meaning |
|---|---|---|
| CLMNO | 9A | Claim number (key), derived from the appointment number. |
| PATID / PROVID | 9A | Patient / provider. |
| CSVCDT | 8S 0 | Service date (YYYYMMDD). |
| CFEE | 11P 2 | Billed fee (roll-up of its lines). |
| CALLOWD | 11P 2 | Allowed amount (= billed fee, in this dental model). |
| CDEDAPP | 11P 2 | Deductible applied on this claim. |
| CINSPD | 11P 2 | Insurance paid. |
| CPATRSP | 11P 2 | Patient responsibility. INVARIANT: CINSPD + CPATRSP = CALLOWD. |
| CSTAT | 1A | E entered/generated, V validated, A adjudicated, X closed (paid). |
| CDENCD | 2A | Claim-level denial code (unused by the seeded happy path). |
| CPLANYR | 4S 0 | Plan year (from the service date), the accumulator key. |
| Field | Type | Meaning |
|---|---|---|
| CLMNO / CLINE | 9A / 3S 0 | Claim number + line number (composite key). |
| CHARTNO | 9A | The charted line this claim line carries. |
| PROCCD | 5A | Procedure code (drives the line's coverage rate at adjudication). |
| LFEE | 11P 2 | Line fee. |
| LALLOWD / LINSPD / LPATRSP | 11P 2 | Line allowed / insurance / patient (set at adjudication). |
| LSTAT | 1A | V validated, A adjudicated. |
DTCLAIMP is a logical file over DTCLAIM keyed (PATID, CLMNO) — the claims-by-patient path for a patient-history enquiry.
| Field | Type | Meaning |
|---|---|---|
| PATID / ACCYR | 9A / 4S 0 | Patient + plan year (composite key). |
| DEDMET | 11P 2 | Deductible satisfied so far this plan year. |
| ANNUSED | 11P 2 | Annual benefit paid so far (the value the cap tests against). |
| ACLMCNT | 5P 0 | Claims adjudicated against this accumulator. |
| AINSYTD | 11P 2 | Insurance-paid year-to-date. |
| ASTAT | 1A | A active (current year), C closed (rolled by DTYRRST). |
| Field | Type | Meaning |
|---|---|---|
| STMTNO | 9A | Statement number (key), derived from the patient id. |
| PATID | 9A | Patient. |
| SRUN | 6S 0 | Statement run period (YYYYMM). |
| SBAL | 11P 2 | Total open balance. |
| SCUR / S30 / S60 / S90 | 11P 2 | Aged buckets: CURRENT / 0–30 / 31–60 / 61–90 days. |
| SDT | 8S 0 | Statement date (YYYYMMDD). |
| Field | Type | Meaning |
|---|---|---|
| GDSEQ | DECIMAL(8,0) | GL sequence (PK), continued from MAX(GDSEQ). |
| GDBATCH | DECIMAL(6,0) | Batch period (YYYYMM). |
| GDACCT | CHAR(12) | Account: 1200-AR, 4000-REV, 1010-CASH. |
| GDDRCR | CHAR(1) | D debit, C credit. |
| GDAMT | DECIMAL(11,2) | Posting amount. |
| GDCAT | CHAR(3) | Movement category: REV, INS, PAT. |
| GDREF | CHAR(9) | Reference (REVENUE / INSPAY / PATPAY). |
| GDDT | DECIMAL(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.
The front office charts procedures against appointments during the day; the daily cycle turns those charts into posted, adjudicated revenue.
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.DTCHART (chart lines in state
E), each citing a procedure code and tooth/surface.SBMJOB CMD(CALL PGM(DENTIS/DTDAILY)).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:
DTPOST POSTED=/REJECT= — posted count matches the clean chart lines; any REJECT
count is the E1/E2/E3 denials, and each denied line now carries CSTAT='D' with its
CDENCD. Investigate unexpected denials (retired code, inactive patient, terminated
provider).DTCLGEN GENERATED= — one claim per insured appointment; self-pay
appointments and appointments with only denied lines generate nothing.DTADJUD ADJUDICATED= — equals the validated (V) claims; each claim
now satisfies the invariant CINSPD + CPATRSP = CALLOWD, and the patient accumulator has
advanced.V claims left outstanding).SBMJOB CMD(CALL PGM(DENTIS/DTMONTH)).DTPAYPOST POSTED=, DTSTMTRUN PRINTED=,
DTGLDST BATCH= ROWS= REVENUE=, DTAGERPT TOTALBAL/CURRENT/...,
DTYRRST RESET=.Reconciling figures (the same ones the volume battle checks against hand-derived numbers):
DTPAYPOST writes exactly two negative ledger
credits per adjudicated claim (-CINSPD as I, -CPATRSP as X); a fully-paid
patient's charges and payments net to 0.00.DTSTMTRUN PRINTED= equals the count of active
patients with a non-zero balance; a zero-balance account is skipped. The statement's SBAL and the
four aged buckets are derived excluding the A audit rows.REVENUE= is the sum of the run's charges (e.g. 165.00 = 110.00 + 55.00). A re-run posts
no new rows because DTGLDST compares against what is already in DTGLDIST per account.DTAGERPT TOTALBAL equals the sum of
DTSTMT.SBAL, bucketed the same way (CURRENT/30/60/90).DTYRRST RESET= equals the active current-year
accumulators; each is closed (ASTAT='C', balances preserved) and a fresh zeroed
next-year row opened.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.
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.
| Situation | Behaviour | Action |
|---|---|---|
| Daily cycle fails partway | Posted/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 DTADJUD | Adjudicated 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 DTCLGEN | The claim number is derived from the appointment number. | Second run reports GENERATED=0; no duplicate claim, no chart line re-claimed. Safe. |
| Re-run DTPAYPOST | Two stable ledger keys per claim (30000000+ / 50000000+). | Second run reports POSTED=0; no duplicate payment postings. Safe. |
| Re-run DTSTMTRUN | Statement number derived from the patient id; same hard-coded run number. | Second run reports PRINTED=0; no duplicate statement. Safe. |
| Re-run DTGLDST | Posts the movement since the last close, per account. | Second run posts no new rows. Safe. |
| Re-run DTYRRST | CHAINs 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. |
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.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.
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).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.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.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.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.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).TOTALBAL / CURRENT / DAYS30 / DAYS60 / DAYS90 / STMTS. The report the front
office reconciles the statement run against.A rows). SFLSIZ(20)/SFLPAG(5), roll keys.For every validated claim (CSTAT='V'), in claim-number order, DTADJUD:
20000000 + claim digits and
CHAIN(EN)s DTLEDG; if the row exists, the claim is already adjudicated — skip.ACCKEY); computes remaining deductible and remaining annual maximum, each
floored at zero.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).DEDMET += deductible applied, ANNUSED +=
insurance paid, ACLMCNT += 1, AINSYTD += insurance paid (or writes a
new accumulator row if none existed).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):
| Claim | Lines (fee, category) | Deductible | Insurance | Patient | Annual used after |
|---|---|---|---|---|---|
| 1 | D1110 110.00 prev + D2140 180.00 basic (allowed 290.00) | 50.00 (drawn on line 1) | 60.00 + 144.00 = 204.00 | 50.00 + 36.00 = 86.00 | 204.00 |
| 2 | D2740 1250.00 major (deductible already met) | 0.00 | 1250 × .50 = 625.00 | 625.00 | 829.00 |
| 3 | D6010 2400.00 major (annual max caps) | 0.00 | uncapped 1200.00, but only 671.00 max remained → 671.00 | 2400 − 671 = 1729.00 | 1500.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.
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.
| Program | Stable key | Derived from |
|---|---|---|
| DTPOST | DTLEDG LSEQ = 10000000 + chart digits | the chart-line number |
| DTCLGEN | DTCLAIM CLMNO = appointment digits | the appointment number |
| DTADJUD | DTLEDG LSEQ = 20000000 + claim digits | the claim number |
| DTPAYPOST | DTLEDG 30000000+ (ins) & 50000000+ (patient) | the claim number |
| DTSTMTRUN | DTSTMT STMTNO = 60000000 + patient digits | the patient id |
| DTGLDST | compares SUM(GDAMT) already posted per account | the ledger totals vs. prior GL |
| DTYRRST | CHAIN the (patient, next-year) accumulator | the 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.
| Code | Meaning |
|---|---|
| E1 | Unknown or inactive procedure code (DTFEE CHAIN failed or FSTAT≠'A'). |
| E2 | Unknown or inactive patient (DTPAT CHAIN failed or PSTAT≠'A'). |
| E3 | Unknown 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.
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.
SBMJOB CMD(CALL PGM(DENTIS/DTDAILY))).