LEXTIME/i is a law-firm practice-management application: clients and their legal
matters, timekeeper time and expense capture with a daily edit, a billing run
that sweeps unbilled work-in-progress (WIP) into invoices — applying a per-matter write-down to
fees, billing expenses at cost, applying client trust (IOLTA) funds, and posting the remainder to
accounts receivable — and a monthly close (AR aging, trust reconciliation, a balanced GL feed and a
trial balance). The whole app is written in RPG (fixed and free form), ILE COBOL, CL and DDS over DB2 for
i physical/logical files, with one SQL table for the GL distribution. The strict invariant it enforces
everywhere is that a client's trust balance may never go negative. This manual is the reference for
the operator who runs the three job cycles and the two online inquiry screens, and for the developer
maintaining the application. It is grounded entirely in the committed source
(legal-app/src/sources.mjs, src/seed.mjs, the DDS/RPG/COBOL/CL members it
loads into library LEXTIME, FINDINGS.md, and the
test/lt_build.mjs / lt_daily.mjs / lt_cycles.mjs drivers).
LEXTIME/i tracks the full life of billable legal work, from a timekeeper's logged hour to cash and the general ledger:
The application is a single, auditable pipeline. A timekeeper's hour becomes cash and a GL entry in a fixed sequence, each step validated and each money movement journaled:
time/expense --edit--> validated WIP --bill--> invoice --trust--> AR --close--> GL feed (E rows) (V rows, rated) (LTBILH/D) (applied) (LTAR) (LTGLFEED)
The billing model is deliberately hand-derivable — every figure below is stated in the test oracles to the exact cent:
time fee (line) = HOURS x BILLRATE (the timekeeper's own rate)
time fees (matter) = SUM(time fee lines) over the matter's 'V' time WIP
write-down amount = time fees x WDPCT (matter's write-down %, 7P4)
billed fees = time fees - write-down amount
expenses (matter) = SUM(expense 'V' WIP) (AT COST, no write-down)
invoice total = billed fees + expenses
trust applied = MIN(invoice total, available trust balance)
AR posted = invoice total - trust applied
trust balance after= trust balance before - trust applied (must stay >= 0)
The write-down is genuinely per-matter, not a hidden floor: a matter with WDPCT=0.0000
bills billed-fees = time-fees exactly. Trust application is optional: a client with no trust account bills
the full invoice straight to AR. And trust is capped at the balance, not the invoice, so a client
whose funds fall short posts the remainder to AR while the balance lands at exactly zero — never
negative.
REFERENCE / SEED ONLINE BATCH (three cycles)
---------------- ------ --------------------
LTREFLD (clients, LTMENU (5250) LXDAILY -> LTTIMLD (raw capture)
timekeepers, opt 1 -> LTMTRIQ (daily) LTWIPED (edit/validate)
matters) opt 2 -> LTWIPIQ LTWIPRP (reject report)
LTTRDEP (trust LXBILL -> LTBILL (WIP -> invoice)
deposits) (billing) LTBILPRT (invoice register)
LTTIMLD (raw time LTGLRPT (trial balance, COBOL)
& expense cards) LXMONTH -> LTARAGE (AR aging)
(monthly) LTTRREC (trust reconciliation)
\ / LTGLDST (GL feed, SQLRPGLE)
\ / LTGLRPT (trial balance, COBOL)
v v
library LEXTIME: LTMATR/LTTIME/LTEXP/LTTRUST/LTTRLDG/LTBILH/LTBILD/LTAR + logicals
|
v
LTGLFEED (SQL GL distribution table) <-- the monthly close posts balanced DR/CR pairs
A single event — say a day's billing — flows: the daily cycle has already left validated
('V') WIP on LTTIME/LTEXP → LTBILL scans the
active matters, sums each matter's WIP, computes the invoice, applies trust from LTTRUST
(writing an APP row to the trust ledger and reducing the balance), posts the AR remainder to
LTAR, writes the invoice header/detail (LTBILH/LTBILD) and flips
each swept WIP row to billed ('B').
| Object | Type | Role |
|---|---|---|
| LTCLNT | PF | Client master. |
| LTTKPR | PF | Timekeeper master (bill rate + cost rate). |
| LTMATR | PF | Matter master (client + write-down %). |
| LTTIME | PF | Time entry WIP capture. |
| LTEXP | PF | Expense entry WIP capture. |
| LTTRUST | PF | Trust (IOLTA) account — running balance per client. |
| LTTRLDG | PF | Trust ledger — durable deposit/application audit trail. |
| LTBILH / LTBILD | PF | Invoice header / detail lines. |
| LTAR | PF | AR ledger — durable posting audit trail. |
| LTMATLF / LTTIMLF / LTEXPLF / LTTRLLF / LTBILLF | LF (5) | Alternate keyed access paths (client / status). |
| LTGLFEED | SQL table | GL distribution the monthly close posts into. |
| LTMATRD / LTWIPD / LTMENUD | DSPF | Matter-inquiry / WIP-subfile / menu display files. |
| LTBILP | PRTF | Client invoice-register printer file. |
| LTREFLD / LTTRDEP / LTTIMLD | RPG | Reference-data / trust-deposit / raw-card seeders. |
| LTWIPED / LTWIPRP | RPG | Daily edit / reject-report drivers. |
| LTBILL / LTBILPRT | RPG | Billing engine / invoice-register print. |
| LTARAGE / LTTRREC | RPG | AR aging / trust reconciliation. |
| LTGLDST | SQLRPGLE | GL distribution into LTGLFEED. |
| LTGLRPT | ILE COBOL | GL trial balance over the invoice headers. |
| LTMTRIQ / LTWIPIQ / LTMENU | RPG | Matter-inquiry / WIP-inquiry / menu programs. |
| LTSETUP | CL | Create every object & compile every program. |
| LXDAILY / LXBILL / LXMONTH | CL | The three job cycles. |
The full catalogue is 10 physical files + 5 logical files, 1 SQL table, 3 display files and 1 printer
file, driven by 13 RPG programs, 1 ILE COBOL program and 4 CL programs. Sections D and F expand each.
Everything lives in library LEXTIME.
LEXTIME/i has no CICS transaction identifiers. 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 LEXTIME — the tested jobs run with
LIBL = QSYS QGPL LEXTIME QTEMP and CURLIB = LEXTIME (each CL cycle also issues
its own ADDLIBLE LIB(LEXTIME) defensively).
| To do this | Type on the command line |
|---|---|
| Open the main menu (routes to the inquiry screens) | CALL LEXTIME/LTMENU |
| Matter inquiry directly | CALL LEXTIME/LTMTRIQ |
| WIP subfile inquiry directly | CALL LEXTIME/LTWIPIQ |
| Run the daily capture/edit cycle | CALL LEXTIME/LXDAILY (or SBMJOB it) |
| Run the billing cycle | CALL LEXTIME/LXBILL |
| Run the monthly close cycle | CALL LEXTIME/LXMONTH |
| One-time: build/compile the whole app | CALL LEXTIME/LTSETUP |
The batch cycles take no CALL parameters and drive fixed processing dates baked into the
programs (e.g. the billing run dates invoices 20260814, the AR-aging report ages as of
20260913), matching the app's hand-checkable test convention. Only LTMENU,
LTMTRIQ and LTWIPIQ are interactive; the batch programs run to completion and
DSPLY a one-line result per step to the job log.
Three display files drive the online surface: LTMENUD (the menu), LTMATRD
(a plain matter-inquiry screen) and LTWIPD (a WIP subfile). All are read-only inquiry
screens — LEXTIME/i does not capture time or post payments interactively; capture is the
batch daily cycle's job (section C).
Key 1 to CALL 'LTMTRIQ', 2 to CALL 'LTWIPIQ'; any
other non-blank option redisplays with Invalid option. in the message line. F3
(DDS CA03 → indicator *IN03) exits.
A plain (non-subfile) screen. Key a matter number; the program CHAINs
LTMATR, shows the client name (from LTCLNT), the description, the write-down
policy and status, and a derived unbilled WIP total — the sum of validated
('V') time fees plus validated expenses for that matter, read over the status logicals
LTTIMLF/LTEXPLF on the 'V'+matter key. Note this WIP figure is the
raw pre-write-down value (write-down is a billing-time discount, not part of what sits in WIP).
An unknown matter blanks the fields and shows Matter not found.
The app's subfile screen (DDS record WSFL under control record WCTL,
SFLPAG(5) per page / SFLSIZ(20), ROLLUP/ROLLDOWN
paging). Key a matter number; the program clears and reloads the subfile with one row per validated
('V') WIP entry — time lines (with the timekeeper name, hours and fee) then expense
lines (description and amount) — and totals the time fees and expenses in the header. Because it
reads the same 'V' status key the billing run uses, the subfile always reflects the current
unbilled WIP, never a stale billed line.
| Field | Type (DDS) | Shows |
|---|---|---|
| WSEQ | 4Y,0 output | Line sequence within the load (RRN). |
| WTYP | 1A output | Line type: T time, X expense. |
| WDESC | 20A output | Timekeeper name (T) or expense description (X). |
| WHRS | 10A output | Hours (time lines only; blank for expenses). |
| WAMT | 13A output | Line amount (fee or expense, edited). |
SFLCLR via indicator *IN31) is written
after the EXFMT returns the newly-keyed matter number and immediately before the
reload, and the READE cursor over each status logical is re-established with a
SETLL after the timekeeper CHAIN moves it — standard subfile-refresh and
cursor-repositioning discipline. Operationally the screen behaves as described.Honest statement: LEXTIME/i does not model a true four-eyes maker–checker / separate-authorization workflow. There is no "one user enters time, a second user approves the bill" gate in the code: the daily edit validates cards automatically, and the billing run sweeps and bills WIP in one flow. The manual documents the control model the application does have:
LTTRLDG
(DEP deposit / APP application) and every AR posting to LTAR
(INVC), each keyed on a stable business key so a re-run regenerates the same key
and is refused rather than double-posting.LTTRUST.TRBAL is the quickly-readable
running trust balance the billing run checks; the monthly LTTRREC independently
re-sums the ledger (DEP adds, APP subtracts) and asserts the two agree to the cent — the audit
proof that the balance was only ever moved by a properly ledgered posting.'V') WIP and flips it to
'B', so nothing is billed twice.MIN(invoice total, balance)
and clamped non-negative, enforced at the one posting site in LTBILL, so the balance
can never go below zero — a client-funds liability invariant re-verified monthly by
LTTRREC.In sum, the control posture is dual-ledger audit + running/re-derived balance reconciliation + status gating + a strict never-negative trust invariant, all in the data-and-batch layer, rather than a segregation-of-duties approval workflow.
LEXTIME/i runs as three CL-driven cycles, each doing real, distinct work: a daily cycle
(LXDAILY — time/expense capture + edit + reject report), a billing cycle
(LXBILL — WIP→invoice, trust application, AR posting, register print, trial
balance) and a monthly cycle (LXMONTH — AR aging, trust reconciliation, GL
distribution, trial balance). Each CL is a bare sequence of CALL PGM(...) steps with a
closing SNDPGMMSG banner; a scheduled submission is a bare SBMJOB CMD(CALL ...).
Before the very first run, LTSETUP creates every object and compiles every program, and the
seed programs (LTREFLD, LTTRDEP) load reference data and initial trust
deposits.
-- one-time build of the whole application CALL PGM(LEXTIME/LTSETUP) CALL PGM(LEXTIME/LTREFLD) clients, timekeepers, matters CALL PGM(LEXTIME/LTTRDEP) initial trust deposits (C00001 1000.00, C00002 300.00) -- then the three cycles, in order SBMJOB CMD(CALL PGM(LEXTIME/LXDAILY)) JOB(LXDAILY) SBMJOB CMD(CALL PGM(LEXTIME/LXBILL)) JOB(LXBILL) SBMJOB CMD(CALL PGM(LEXTIME/LXMONTH)) JOB(LXMONTH)
| Cycle / CL | Program | Purpose | Reads | Writes / DSPLY |
|---|---|---|---|---|
| LXDAILY | LTTIMLD | Load the day's raw time & expense cards (incl. deliberate exceptions). | — (host-coded cards) | LTTIME/LTEXP 'E' rows; TCARDS=10 XCARDS=5 SKIP=0. |
| LTWIPED | Daily edit: validate or reject each 'E' entry, rate the clean ones. | LTTIME/LTEXP/LTMATR/LTTKPR |
'E'→'V' (rated) or 'R' (+reason); TOK=5 TREJ=5 XOK=3 XREJ=2. | |
| LTWIPRP | Reject exception report over the status logicals. | LTTIMLF/LTEXPLF |
TREJ=5 E1=1 E2=1 E3=1 / E4=1 E5=1 XREJ=2. | |
| LXBILL | LTBILL | Billing engine: WIP→invoice, trust applied, AR posted. | base PFs + LTMATR/LTTKPR/LTTRUST |
LTBILH/LTBILD/LTAR/LTTRLDG; balance reduced; BILLED=4 SKIP=1. |
| LTBILPRT | Print the client invoice register (PRTF, page overflow). | LTBILH/LTBILD/LTCLNT |
spooled register; INVS=4 BILLED=7303. | |
| LTGLRPT | GL trial balance over the invoice headers (ILE COBOL). | LTBILH |
TOTDIFF 0.00, ARDIFF 0.00. | |
| LXMONTH | LTARAGE | AR aging: bucket each open invoice's outstanding balance by age. | LTBILH |
INVS / CUR / B31 / B61 / B90 / TOT. |
| LTTRREC | Trust reconciliation: re-sum the ledger vs. the running balance. | LTTRUST/LTTRLLF |
CLIENTS / BREAKS / NEG + total trust liability. | |
| LTGLDST | GL distribution: balanced DR/CR pairs into the SQL feed (SQLRPGLE). | LTBILH |
LTGLFEED rows; BATCH / ROWS / TOTAL. | |
| LTGLRPT | Trial balance again, closing the month. | LTBILH |
TOTDIFF 0.00, ARDIFF 0.00. |
LTTIMLD writes 10 raw time cards and 5 raw expense cards (all status 'E',
unrated), including deliberate bad cards. LTWIPED then walks every 'E' row:
for a time entry it CHAINs the matter (E1 if missing, E2 if not active), then the timekeeper
(E5 if missing or terminated, else defaulting TRATE from BILLRATE), then checks
hours (E3 over the 24-hour ceiling, E4 zero-or-negative); a clean entry is rated
TFEE = THOURS × TRATE (half-adjusted to exact cents) and flipped to 'V'.
Expenses take the same E1/E2 matter checks (E4/E5 don't apply) and flip to 'V' at cost.
LTWIPRP reports the rejects over the status logicals.
Expected DSPLY: LTTIMLD TCARDS=10 XCARDS=5 SKIP=0 LTWIPED TOK=5 TREJ=5 XOK=3 XREJ=2 5 clean time (fees 7337.50), 3 clean exp (510.50) LTWIPRP TREJ=5 E1=1 E2=1 E3=1 LTWIPRP E4=1 E5=1 XREJ=2 one reject of each kind, both bad expenses
LTBILL scans all active matters; for each one carrying validated WIP it sums time fees and
expenses, applies the matter write-down to fees only, computes the invoice total, applies
MIN(total, trust balance) from LTTRUST (writing an APP ledger row
and reducing the balance, never below zero), posts the AR remainder to LTAR, writes the
invoice header and detail lines (one summarised T line per timekeeper, the X
expense lines, then an informational W write-down line when the write-down is positive), and
flips each swept WIP row to 'B'. Matters bill in matter-number order, so a client's earlier
matter drains the trust first. LTBILPRT prints the register; LTGLRPT proves the
trial balance.
Expected DSPLY / hand-derived invoices: LTBILL BILLED=4 SKIP=1 M00005 closed, no WIP -> skipped M00001 fees 4900.00 wd 490.00 (10%) billed 4410.00 exp 250.00 TOTAL 4660.00 trust MIN(4660,1000)=1000.00 AR 3660.00 balance after 0.00 M00002 fees 437.50 wd 0.00 billed 437.50 exp 0.00 TOTAL 437.50 trust MIN(437.50,0)=0.00 (drained by M00001) AR 437.50 in full M00003 fees 1100.00 wd 55.00 (5%) billed 1045.00 exp 85.50 TOTAL 1130.50 trust MIN(1130.50,300)=300.00 (partial, capped at balance) AR 830.50 M00004 fees 900.00 wd 0.00 billed 900.00 exp 175.00 TOTAL 1075.00 no trust account -> trust 0.00 AR 1075.00 in full TOTALS billed 7303.00 trust applied 1300.00 AR posted 6003.00 (7303 - 1300 = 6003)
LTARAGE buckets each open invoice's outstanding balance (BARAMT - BPAIDAMT)
by age from BILDT to a fixed as-of date 20260913, using a 30-day-month julian
approximation (CURRENT 0–30, B31 31–60, B61 61–90, B90 over 90). LTTRREC
re-sums the trust ledger per client and asserts it matches TRBAL with zero breaks and zero
negatives. LTGLDST sums the invoice headers and posts balanced DR/CR pairs into
LTGLFEED. LTGLRPT closes with the trial balance.
Expected DSPLY (with one injected 74-day-old 500.00 invoice for aging): LTARAGE INVS=5 CUR=6003.00 ... the 4 real invoices are exactly on the 30-day edge LTARAGE ... B61=500.00 ... the injected aged invoice LTTRREC CLIENTS=2 BREAKS=0 NEG=0 LTTRREC TOTAL TRUST LIABILITY=0.00 both accounts fully drained, strict zero LTGLRPT TOTDIFF 0.00 / ARDIFF 0.00 every figure reconciles
LTSETUP then LTREFLD (masters) then
LTTRDEP (trust) must run before any cycle; the billing run has nothing to apply trust
against otherwise.'V') WIP;
LTWIPED is what produces it. Run the daily cycle, then bill.LTBILL bills in
MTNO order, a client's lower-numbered matter drains the shared trust balance before a
higher-numbered one — the reason C00001's M00001 takes the full 1000.00 and M00002 gets 0.00.All files are in library LEXTIME, described in DDS in src/sources.mjs. Dates
are stored as signed numeric YYYYMMDD (8S 0); money is packed
11P 2; rates and the write-down percentage are packed 7P 4 (so 450.0000 is a
rate and 0.1000 is 10%). Business keys are the letter+5-digit spine convention
(C00001, T00001, M00001).
| Field | Type | Meaning |
|---|---|---|
| CLNO | 6A | Client number (key), e.g. C00001. |
| CNAME | 30A | Client name. |
| CADDR | 30A | Address. |
| CTERMS | 2S 0 | Payment terms (days). |
| CSTAT | 1A | A active, C closed (no new matters; existing may still bill). |
| CSTARTDT | 8S 0 | Client start date. |
| Field | Type | Meaning |
|---|---|---|
| TKNO | 6A | Timekeeper number (key), e.g. T00001. |
| TKNAME | 30A | Timekeeper name. |
| TKCLASS | 1A | P partner, A associate, L paralegal. |
| BILLRATE | 7P 4 | Hourly rate charged to the client. |
| COSTRATE | 7P 4 | Internal cost/hour (separate figure, for realization/margin reporting). |
| TKSTAT | 1A | A active, T terminated. (Deliberately named TKSTAT, not TSTAT — see the field-name note below.) |
| HIREDT | 8S 0 | Hire date. |
Seeded set: T00001 partner 450.0000/180.0000, T00002 associate 275.0000/110.0000, T00003 paralegal 125.0000/45.0000, T00004 associate 300.0000/120.0000 (terminated).
LTTIME already carries a field named
TSTAT (time-entry status). This emulator's RPG compiler shares one flat global-storage slot
per field name across every externally-described file a program opens, with no compile-time
conflict check (documented as FINDING 1's field-name class in the app's own notes). A program that opened
both LTTIME and LTTKPR would silently corrupt whichever TSTAT it
didn't mean; naming the timekeeper status TKSTAT avoids the collision by construction.| Field | Type | Meaning |
|---|---|---|
| MTNO | 6A | Matter number (key), e.g. M00001. |
| CLNO | 6A | Owning client. |
| MTDESC | 30A | Matter description. |
| WDPCT | 7P 4 | Write-down % applied to time fees at billing (0.1000 = 10%). |
| MOPENDT / MCLOSDT | 8S 0 | Opened / closed dates. |
| MSTAT | 1A | A active, C closed (closed rejects new capture; existing WIP may still bill). |
Seeded: M00001→C00001 (10%), M00002→C00001 (0%), M00003→C00002 (5%), M00004→C00003 (0%), M00005→C00002 (closed).
| Field | Type | Meaning |
|---|---|---|
| TENTNO | 8S 0 | Time-entry number (key). |
| MTNO / TKNO | 6A / 6A | Matter / timekeeper. |
| TWKDT | 8S 0 | Work date. |
| THOURS | 5P 2 | Hours logged. |
| TRATE | 7P 4 | Rate (defaulted from the timekeeper's BILLRATE at edit time). |
| TFEE | 11P 2 | Line fee = THOURS × TRATE (set by the edit; zero until validated). |
| TSTAT | 1A | E entered, V validated (unbilled WIP), R rejected, B billed. |
| TERRCD | 2A | Reason code on a rejected entry (E1–E5). |
| Field | Type | Meaning |
|---|---|---|
| XENTNO | 8S 0 | Expense-entry number (key). |
| MTNO | 6A | Matter. |
| XWKDT | 8S 0 | Date incurred. |
| XDESC | 20A | Description (e.g. FILING FEE). |
| XAMT | 11P 2 | Cost (billed at cost, no write-down). |
| XSTAT / XERRCD | 1A / 2A | Status (E/V/R/B) / reason code (E1/E2 only). |
| Field | Type | Meaning |
|---|---|---|
| CLNO | 6A | Client (key) — one trust account per client. |
| TRBAL | 11P 2 | Current running trust balance; must never go negative. |
| TROPENDT | 8S 0 | Account open date. |
| Field | Type | Meaning |
|---|---|---|
| TRSEQ | 8S 0 | Stable business key derived from client digits + a sequence offset (idempotency guard). |
| CLNO / MTNO | 6A / 6A | Client / matter (matter blank on a deposit). |
| TRTYP | 3A | DEP deposit, APP applied against an invoice. |
| TRAMT | 11P 2 | Amount. |
| TRDT / TRMEMO | 8S 0 / 25A | Date / free-text memo. |
| Field | Type | Meaning |
|---|---|---|
| BILNO | 8S 0 | Invoice number (key) — a stable key = the matter's 5 digits (one bill per matter per run). |
| MTNO / CLNO | 6A / 6A | Matter / client. |
| BFEES | 11P 2 | Billed fees (time fees NET of write-down). |
| BWDOWN | 11P 2 | Write-down amount taken off gross time fees (informational). |
| BEXP | 11P 2 | Expenses at cost. |
| BTOT | 11P 2 | Invoice total = BFEES + BEXP. |
| BTRUST | 11P 2 | Trust funds applied to this invoice. |
| BARAMT | 11P 2 | Posted to AR = BTOT − BTRUST. |
| BILDT / BDUEDT | 8S 0 | Invoice / due dates. |
| BPAIDAMT | 11P 2 | Amount paid (aging uses BARAMT − BPAIDAMT). |
| BSTAT | 1A | O open, P paid. |
| Field | Type | Meaning |
|---|---|---|
| BILNO / LSEQ | 8S 0 / 3S 0 | Invoice + line sequence (key). |
| LTYPE | 1A | T time line (one per timekeeper), X expense line, W write-down (informational). |
| LDESC | 20A | Line description (timekeeper name / expense text / "WRITE-DOWN TIME FEES"). |
| LHRS / LRATE | 5P 2 / 7P 4 | Hours / rate on a time line (rate carries WDPCT on the W line). |
| LAMT | 11P 2 | Line amount. |
| Field | Type | Meaning |
|---|---|---|
| ARSEQ | 8S 0 | Stable business key (= 20000000 + BILNO) — idempotency guard. |
| CLNO / MTNO | 6A / 6A | Client / matter. |
| ARTYP | 4A | INVC (invoice posted). |
| ARAMT | 11P 2 | Net-of-trust AR amount. |
| ARDT / ARMEMO | 8S 0 / 25A | Date / memo. |
| Field | Type | Meaning |
|---|---|---|
| GLSEQ | DECIMAL(8,0) | Sequence (PK) — the idempotency guard (a re-run's INSERT collides). |
| GLBATCH | DECIMAL(6,0) | Batch (period), e.g. 202608. |
| ACCT / DRCR | CHAR(9) / CHAR(1) | GL account / D debit or C credit. |
| AMT | DECIMAL(13,2) | Amount. |
| GLREF / GLDT | CHAR(8) / DECIMAL(8,0) | Reference (BILLING/WRITEDWN/TRUSTAPP) / date. Index LTGLFACC on (ACCT, DRCR). |
LEXTIME (each CL also issues its own
ADDLIBLE).CALL LEXTIME/LTSETUP, then
CALL LEXTIME/LTREFLD (masters), then CALL LEXTIME/LTTRDEP (trust
deposits).SBMJOB CMD(CALL PGM(LEXTIME/LXDAILY)).CALL LEXTIME/LTMENU.Post-checks after the daily cycle:
LTTIMLD TCARDS=10 XCARDS=5 SKIP=0 on the first load (a re-run shows
TCARDS=0 XCARDS=0 SKIP=15 — the sequential-counter guard refusing every card
again).LTWIPED TOK=5 TREJ=5 XOK=3 XREJ=2; validated time fees total 7337.50 and
validated expenses 510.50.LTWIPRP breaks the rejects out as E1=1 E2=1 E3=1 E4=1 E5=1 (time) and
XREJ=2 (expense), proving the status logicals are genuine keyed access paths.'V') WIP on file.SBMJOB CMD(CALL PGM(LEXTIME/LXBILL)). Confirm
LTBILL BILLED=4 SKIP=1 and review the spooled invoice register
(INVS=4 BILLED=7303).SELECT MTNO, BFEES, BWDOWN, BEXP, BTOT, BTRUST, BARAMT FROM LEXTIME/LTBILH ORDER BY MTNO;
billed − trust = AR exactly (7303.00 − 1300.00 = 6003.00).'V' rows remain (5 time / 3 expense all flipped to
'B'); the AR ledger carries 4 INVC postings, the trust ledger 2 new
APP rows.SBMJOB CMD(CALL PGM(LEXTIME/LXMONTH)). Confirm
LTTRREC CLIENTS=2 BREAKS=0 NEG=0 and TOTAL TRUST LIABILITY=0.00, the
aging buckets, and LTGLRPT TOTDIFF 0.00 / ARDIFF 0.00.SELECT ACCT, DRCR, SUM(AMT) AS T FROM LEXTIME.LTGLFEED GROUP BY ACCT, DRCR ORDER BY ACCT;
Total debits must equal total credits. The BILLING leg debits 1200-AR for the invoice
total and credits 4100-FEES (net billed fees) + 4200-EXP (expenses); the
WRITEDWN memo pair (4900-WDWN / 4950-WDOF) self-balances; the TRUSTAPP leg
moves 2300-TRLB to reduce 1200-AR by the trust applied.
Each program DSPLYs its result to the job log. Every cycle in LEXTIME/i is designed to be
safely re-runnable through a stable-key idempotency guard — a re-run regenerates the same key and
the duplicate write/insert is refused rather than double-posting.
| Situation | Behaviour | Action |
|---|---|---|
| Daily edit re-run | Only 'E' rows are touched; validated/rejected rows are left alone. | Safe no-op: LTWIPED TOK=0 TREJ=0 XOK=0 XREJ=0. Idempotent. |
| Raw-card load re-run | The sequential TENTNO/XENTNO counter has advanced past the seeded range. | Nothing new captured: TCARDS=0 XCARDS=0 SKIP=15. Idempotent. |
| Billing re-run | Every swept entry is now 'B', so no matter has any 'V' WIP left. | LTBILL BILLED=0 SKIP=5: no bill, no AR, no trust movement happens twice. Idempotent off the WIP gate. |
| Trust deposit re-run | LTTRUST is UNIQUE-keyed on CLNO; the CHAIN finds the account exists. | Skips re-depositing (SKIP counter increments). Idempotent. |
| GL distribution re-run | GLSEQ is the SQL PRIMARY KEY; the re-run's INSERTs collide. | The feed is left exactly as it was (not doubled). Idempotent per batch. |
| A card rejected by the edit | Set to 'R' with a reason code (E1–E5); no rate, no fee. | Correct the master data (matter/timekeeper) or the card, reload as a fresh 'E' entry, re-run the edit. Rejected rows never bill. |
| A trust break at reconciliation | LTTRREC DSPLYs a BREAK line and bumps BREAKS. | Investigate: the ledger-derived balance and TRBAL disagree, meaning a balance was moved outside a ledgered posting. A healthy month shows BREAKS=0 NEG=0. |
LTTRLDG and every AR posting to
LTAR, and the running balance is re-derived monthly from the ledger by LTTRREC,
any billing run's effect on client funds is fully reconstructable and independently verifiable after the
fact.The program surface, from src/sources.mjs. All objects are in library
LEXTIME. The source is held as JS string constants (column-exact fixed-form RPG via the
C()/D() helpers, plus free-form /free blocks) and loaded into
library LEXTIME's source physical files by seedLextime() in
src/seed.mjs — the same DEMOLIB convention as ibmi/samples.js.
LTTRUST running-balance row and a DEP LTTRLDG row. Idempotent (CHAIN on the UNIQUE CLNO key).'E'), including the deliberate exception cards. Idempotent (sequential-counter key guard).'E' entry, applies E1–E5 checks, rates a clean time entry TFEE = THOURS × TRATE (half-adjusted) from the timekeeper's bill rate, flips to 'V' or 'R'. Re-CHAINs the entry after the master lookups moved the cursor. Idempotent.LTTIMLF/LTEXPLF status logicals (read-only), breaking rejects out by reason code.'V' WIP, write-down on fees, expenses at cost, invoice total, MIN(total, balance) trust applied (never negative), AR remainder; writes header/detail, ledger rows, flips WIP to 'B'. Scans the base PFs for the sweep, deliberately not the status logicals — see F.4. Idempotent off the WIP gate.LTBILP PRTF with page overflow (OFLIND(*IN90)): a matter block per invoice (header, itemised lines via a partial-key READE on BILNO, AR-posted net), then register totals.BARAMT − BPAIDAMT by age (30-day-month julian day-count) from BILDT to the fixed as-of date 20260913: CURRENT / B31 / B61 / B90.LTTRLDG (DEP adds, APP subtracts) over LTTRLLF and asserts it equals TRBAL to the cent; counts breaks and negatives; totals the trust liability.LTGLFEED with free-form exec sql insert. Idempotent (GLSEQ PK).LTBILH: proves invoice total = billed fees + expenses (TOTDIFF) and AR posted = invoice total − trust applied (ARDIFF), both exactly zero.Every figure below is hand-derivable and asserted to the cent by test/lt_cycles.mjs.
| Matter | Time fees | WDPCT | Write-down | Billed fees | Expenses | Total | Trust | AR |
|---|---|---|---|---|---|---|---|---|
| M00001 | 4900.00 | 0.1000 | 490.00 | 4410.00 | 250.00 | 4660.00 | 1000.00 | 3660.00 |
| M00002 | 437.50 | 0.0000 | 0.00 | 437.50 | 0.00 | 437.50 | 0.00 | 437.50 |
| M00003 | 1100.00 | 0.0500 | 55.00 | 1045.00 | 85.50 | 1130.50 | 300.00 | 830.50 |
| M00004 | 900.00 | 0.0000 | 0.00 | 900.00 | 175.00 | 1075.00 | 0.00 | 1075.00 |
| TOTAL | 7337.50 | — | 545.00 | 6792.50 | 510.50 | 7303.00 | 1300.00 | 6003.00 |
Note how the invariants show up in the totals: billed fees + expenses = total
(6792.50 + 510.50 = 7303.00), total − trust = AR (7303.00 − 1300.00 = 6003.00),
the write-down is per-matter (M00002/M00004 take none), and trust is capped at the balance (M00003 applies
only 300.00 of a 1130.50 invoice), leaving both trust accounts at exactly 0.00.
LTGLDST posts three self-consistent legs, so the batch balances (DR = CR):
| Reference | Debit | Credit | Basis |
|---|---|---|---|
| BILLING | 1200-AR | 4100-FEES + 4200-EXP | AR debited for invoice total; fee revenue at NET billed fees, expense revenue at cost. AR = FEES + EXP by construction, so this leg alone balances. |
| WRITEDWN | 4900-WDWN | 4950-WDOF | Informational memo pair recording the realization loss; never touches AR or revenue (the write-down happened before billing, not as a credit note). |
| TRUSTAPP | 2300-TRLB | 1200-AR | Client funds held (trust liability) move to reduce AR by the trust applied. |
With the test's injected 500.00 orphan invoice included, the feed nets to: 1200-AR debit
7803.00 = 4100-FEES 7292.50 + 4200-EXP 510.50; the 4900/4950 memo
pair 545.00; and 2300-TRLB debit = 1200-AR credit = 1300.00 (trust applied).
What LTBILL does and why. The billing engine deliberately opens only the base physical
files (LTTIME/LTEXP) for its WIP sweep, never their status logicals
(LTTIMLF/LTEXPLF), scanning SETLL *LOVAL / READ
sequentially and filtering MTNO/TSTAT in RPG logic. This is a defensive
redesign around a confirmed platform bug, logged as FINDING 1 in legal-app/FINDINGS.md.
The bug (confirmed). A logical file's keyed index does not observe an UPDATE
posted through a separate, concurrently-open file instance over the same base PF / record format
in the same job. The earlier version of LTBILL swept each matter's unbilled
('V') WIP via the status logicals, and for each row read through the logical it
CHAIN-and-UPDATE-ed the same record through the base PF to flip it to
'B', then re-SETLLed the logical to reposition. The logical's index never saw
the 'V'→'B' flip, so the next READE returned the same
record (with its old key values) forever — an infinite READE loop looping on
TENTNO 90000001 (matter M00001, timekeeper T00001). Left running, the 3-digit line-sequence
counter WSEQ would eventually overflow past 999 and raise a second, misleading
SNX0103 Receiver too small symptom that masked the true infinite loop.
Class & severity. This is the same class as the freight application's NA-02 finding (cross-file-instance index staleness). Severity High: it silently hangs the calling program with no error or escape message. On real IBM i, DB2/400 keeps a logical file's keyed index transactionally consistent with its base PF within a job across separate opens (single-job read-your-writes), so this specific shape either can't arise or the index would be maintained transactionally.
The fix (app-level, not a platform workaround). LTBILL was rewritten to scan the
base PFs sequentially and filter in RPG — the same defensive shape staffing-app's
SPCALC already uses (it opens only the base time PF, never its status logical, for exactly this reason).
The status logicals remain in the app, used correctly (read-only, no same-run cross-instance UPDATE) by
LTWIPRP, LTMTRIQ and LTWIPIQ, and the build battle still proves
they are genuine, distinct keyed access paths. The platform gap itself remains present for any other
program that would combine a keyed-LF sweep with a same-run UPDATE through a sibling file instance.
| Code | Applies to | Meaning |
|---|---|---|
| E1 | time + expense | No such matter on the master. |
| E2 | time + expense | Matter is not active (closed). |
| E3 | time only | Hours exceed the 24.00 one-day ceiling. |
| E4 | time only | Hours are zero or negative. |
| E5 | time only | No such timekeeper, or timekeeper not active (terminated). |
| Field | Values |
|---|---|
| TSTAT / XSTAT | E entered, V validated (WIP), R rejected, B billed. |
| MSTAT / CSTAT | A active, C closed. |
| TKSTAT | A active, T terminated. |
| BSTAT | O open, P paid. |
| TRTYP / ARTYP | DEP / APP (trust); INVC (AR). |
| LTYPE | T time, X expense, W write-down. |
SBMJOB CMD(CALL PGM(LEXTIME/LXBILL))).