AMORTIS/i is a term-loan origination and servicing application: it originates a loan by
generating a full constant-principal (straight-line) amortisation schedule, accrues daily interest
on a 30/360 basis, bills instalments monthly, applies borrower payments on an interest-first waterfall,
ages the book into delinquency buckets, prices early-settlement payoff quotations, charges servicing fees
and escrow, posts an interest-income GL distribution, and at year end re-prices, reports and closes
matured loans. It is a program-driven estate written in RPG, ILE COBOL, CL, DDS and embedded SQL
— the business logic lives in the RPG/COBOL programs themselves, orchestrated by four CL job cycles.
This manual is the reference for the operator who runs the online screens and the daily/weekly/monthly/
annual cycles, and for the developer maintaining the application. It is grounded entirely in the committed
source (loans-app/src/sources.mjs, src/seed.mjs, and the
test/ln_*.mjs drivers).
AMORTIS), its own file/program set, and its own design: constant-principal term
loans with the logic in RPG/COBOL programs rather than DB2 SQL PL.
AMORTIS/i services the full life of a term loan, in library AMORTIS:
LNSCHD row per instalment), then activate the loan —
set the balance to the advance, status A, first due date.LNACRD) and a ledger transaction
(LNTXN type I), and add to the loan's unbilled accrual bucket.LNDELQ.LNGLDIST.Every loan in AMORTIS is a constant-principal (straight-line) term loan, not a level-annuity
loan — a deliberate modelling choice. The principal component of every instalment is fixed
(original principal / term), the interest component declines with the balance, and the
instalment therefore declines too:
principal component = original principal / term (fixed) interest component_n = opening balance_n * periodic rate (declining) instalment_n = principal + interest_n (declining) closing_n = opening_n - principal (declining)
The canonical loan LN000001 is 12,000.00 at 12.0000% over 12 monthly instalments. Periodic
rate = 12.0000 / 12 / 100 = 0.010000; principal per instalment = 12000 / 12 = 1000.00.
n=1 open 12000.00 int 120.00 prin 1000.00 inst 1120.00 close 11000.00 n=2 open 11000.00 int 110.00 prin 1000.00 inst 1110.00 close 10000.00 ... n=12 open 1000.00 int 10.00 prin 1000.00 inst 1010.00 close 0.00 total interest = 0.01 * 1000 * (12+11+...+1) = 0.01 * 1000 * 78 = 780.00
When the advance does not divide evenly by the term, the schedule would never reach zero (30,000.00 over
36 left 0.12 outstanding), so the final instalment repays whatever is actually left — a real
amortisation plug. The daily accrual uses the same 30/360 convention, so a full 30-day month accrues
exactly the schedule's interest: a day on 12,000.00 at 12% is
12000 × 0.01 / 30 = 4.00, and 30 days is 120.00 — the interest the monthly billing
bills. That 30/360 identity between accrual and schedule is the design's central invariant.
ORIGINATION ONLINE BATCH (the four CL cycles)
----------- ------ -------------------------
LNREFLD (seed) LNMENU (5250 menu) LNDAILY -> LNACCRU accrual
LNORIG builds opt 1 -> LNLOANIQ LNCASH payments
LNSCHD schedule opt 2 -> LNSCHDIQ LNDELQP re-bucket
activates loan (subfile) LNWEEK -> LNARRWL worklist
LNQUOTE payoff quote
LNPORTF (COBOL report) LNARRPR print worklist
LNMONTH -> LNBILLP billing
LNRUNDT (*DTAARA: accrual / cut-off / LNFEEPG fees+escrow
as-at run date, set by CHGDTAARA) --------. LNGLPST GL distribution
| | LNPORTF portfolio
v v LNYEAR -> LNYRSTM interest stmt
LNLOAN <--writes--- every posting program LNRATRV rate review
| \ LNCLOSE matured closure
| +--> LNTXN (durable ledger: I/B/C/F/S/X, one stable key per event)
| +--> LNACRD (daily accrual detail, keyed LOANNO+ACDT)
| +--> LNDELQ (delinquency movement history)
+------> LNQUOT (payoff quotations) LNGLDIST (SQL GL) LNSCHD (schedule)
A single processing event (say, daily accrual) flows: operator sets the run date on the
LNRUNDT data area → the LNDAILY CL cycle CALLs LNACCRU
→ it reads each active loan, computes the day's interest, writes an LNACRD detail row and
an LNTXN ledger row (each guarded by a stable idempotency key), and advances the loan's
LNACCR bucket.
| Object | Type | Role |
|---|---|---|
| LNBRW | PF | Borrower master. |
| LNLOAN | PF | Loan master — the servicing spine. |
| LNSCHD | PF | Amortisation schedule (composite key LOANNO+SCHINS). |
| LNSCHDL | LF | Schedule keyed by due date (billing access path). |
| LNLOANL | LF | Loans keyed by bucket, omitting bucket 0 (collections path). |
| LNPAY | PF | Payment receipts as banked. |
| LNTXN | PF | Durable loan transaction ledger. |
| LNACRD | PF | Daily interest-accrual detail. |
| LNDELQ | PF | Delinquency-movement history. |
| LNQUOT | PF | Payoff / early-settlement quotations. |
| LNFEE | PF | Fee & escrow catalogue. |
| LNGLDIST | SQL table | Interest-income GL distribution (+index LNGLDACC). |
| LNLOAND / LNSCHDD / LNMENUD | DSPF | Inquiry, subfile-schedule, and menu display files. |
| LNARRP | PRTF | Arrears / collections worklist printer file. |
| LNREFLD / LNORIG | RPGLE | Reference-data seed / origination. |
| LNACCRU / LNCASH / LNDELQP | RPGLE | Daily-cycle programs. |
| LNBILLP / LNFEEPG / LNGLPST | RPGLE / SQLRPGLE | Monthly-cycle programs. |
| LNQUOTE / LNARRWL / LNARRPR | RPGLE | Weekly-cycle programs. |
| LNRATRV / LNCLOSE / LNYRSTM | RPGLE | Annual-cycle programs. |
| LNLOANIQ / LNSCHDIQ / LNMENU | RPGLE | Interactive inquiry programs. |
| LNPORTF | CBLLE | COBOL portfolio-position report. |
| LNSETUP | CLP | Create/compile every object. |
| LNDAILY / LNWEEK / LNMONTH / LNYEAR | CLP | The four job cycles. |
| LNRUNDT | *DTAARA | Run-date control (CHAR(10)) read by every dated batch program. |
The full catalogue is 11 physical/logical DDS files + 1 SQL table (+index), 4 display/printer files,
17 RPG programs, 1 COBOL program and 5 CL programs, plus the LNRUNDT data area. Sections D and
F expand each.
AMORTIS/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 via CL/JOBQ 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 AMORTIS; every CL cycle does its own
ADDLIBLE LIB(AMORTIS) first.
| To do this | Type on the command line |
|---|---|
| Build/rebuild the whole application (once) | CALL AMORTIS/LNSETUP then CALL AMORTIS/LNREFLD |
| Originate the pending loans | CALL AMORTIS/LNORIG |
| Open the online menu | CALL AMORTIS/LNMENU |
| Open loan inquiry directly | CALL AMORTIS/LNLOANIQ |
| Open the amortisation-schedule subfile directly | CALL AMORTIS/LNSCHDIQ |
| Set the batch run date (before a dated cycle) | CHGDTAARA DTAARA(AMORTIS/LNRUNDT) VALUE(' YYYYMMDD') |
| Run a cycle | CALL AMORTIS/LNDAILY (or LNWEEK / LNMONTH / LNYEAR; or SBMJOB it) |
The batch cycles take no CALL parameters; the dated programs read their processing date from the
LNRUNDT data area (section F.3), so a scheduled submission is a bare CALL. Only
LNMENU / LNLOANIQ / LNSCHDIQ are interactive; the batch programs run to completion and DSPLY a one-line
result apiece.
A plain option menu. Key 1 for Loan Inquiry (CALLs LNLOANIQ) or 2 for the
Amortisation Schedule (CALLs LNSCHDIQ); any other non-blank option shows
Invalid option. F3 exits.
A plain (non-subfile) screen. Key a loan number and Enter; the program CHAINs
LNLOAN, looks the borrower name up in LNBRW, and shows the servicing position plus
the derived payoff total — computed on the screen with the same formula LNQUOTE prices with
(balance + accrued + billed-unpaid-interest + fees - escrow).
The application's natural subfile screen (record SSFL under control record
SCTL, SFLPAG(6), SFLSIZ(60)). Key a loan number and
Enter: the subfile is cleared and re-loaded with that loan's instalments before it
is displayed, so each enquiry shows the schedule just keyed rather than the previous one. A 12-instalment
schedule fills two pages, so Roll Up/Roll Down
paging is genuinely exercised. The header shows the advance and the total interest over the life, summed
from the schedule rows themselves (780.00 for LN000001).
%EDITC(...:'L') (not code X, which emits the full unedited 15-digit picture),
the due date is composed as yyyymmdd from %char substrings, and the subfile is
cleared in the same pass as the load so every WRITE lands on a free RRN rather than a
duplicate-record 01021. Operationally the screens behave exactly as shown.Honest statement: AMORTIS/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 receipt in
LNPAY is applied by LNCASH when the daily cycle runs, and origination through
LNORIG builds-and-activates in one pass. The control model the application does have:
I),
billing (B), cash (C), fee/escrow (F), settlement
(S), closure (X). The whole book is reconstructable from it.LNORIG only schedules a loan
in P; accrual, billing, fees, quoting and repricing act only on A loans;
LNCLOSE settles only a zero-balance A loan and skips one already
S.S belongs to
LNCLOSE alone. (The source records that when both programs claimed settlement, the escrow
was never refunded — a real bug, now fixed by that split.)CHAIN(EN) (section F.2). This is the
integrity backbone in place of a DB-trigger layer.In sum, the control posture is durable ledger + accrual/delinquency detail + status gating + key-based idempotency, all enforced in the programs, rather than a segregation-of-duties approval workflow.
AMORTIS/i's servicing runs as four periodic CL cycles rather than one monolithic nightly job: a
daily cycle (accrual + cash + re-bucket), a weekly cycle (worklist + payoff quotes), a
monthly cycle (billing + fees + GL + portfolio), and an annual cycle (statement + rate review
+ closure). Each CL program does its own ADDLIBLE LIB(AMORTIS), CALLs its member programs in
order, and sends a completion message. The dated programs take no parameters; each reads its
processing date from the single LNRUNDT data area — so a scheduled/JOBQ submission sets
the date, then simply calls the cycle.
-- set the processing date the next dated cycle runs for (CHAR(10), right-justified) CHGDTAARA DTAARA(AMORTIS/LNRUNDT) VALUE(' 20260116') -- then submit the (parameterless) cycle SBMJOB CMD(CALL PGM(AMORTIS/LNDAILY)) JOB(LNDAILY) -- blank the data area to fall back to each program's built-in default date CHGDTAARA DTAARA(AMORTIS/LNRUNDT) VALUE(' ')
| Program | Cycle | Purpose | Reads / writes | DSPLY result |
|---|---|---|---|---|
| LNREFLD | setup | Seed borrowers, fee catalogue, pending loans. | writes LNBRW/LNFEE/LNLOAN | LNREFLD BORROWERS=4 FEES=3 LOANS=4 |
| LNORIG | setup | Build each pending loan's schedule, activate it. | writes LNSCHD; updates LNLOAN | LNORIG ORIGINATED=n SKIP=n |
| LNACCRU | daily | Accrue one day's 30/360 interest per active loan. | LNRUNDT; LNLOAN/LNACRD/LNTXN(I) | LNACCRU ACCRUED=n SKIP=n TOT=x |
| LNCASH | daily | Apply unapplied receipts on the waterfall. | LNPAY/LNLOAN/LNTXN(C) | LNCASH APPLIED=n SKIP=n |
| LNDELQP | daily | Age loans into buckets 0–4; log movement. | LNRUNDT; LNSCHD/LNLOAN/LNDELQ | LNDELQP CHECKED=n MOVED=n |
| LNARRWL | weekly | Total arrears by bucket for the collections desk. | LNLOANL/LNBRW | LNARRWL DELQ=n B1..B4=x TOT=x |
| LNQUOTE | weekly | Price a payoff quotation per active loan. | LNRUNDT; LNLOAN/LNQUOT | LNQUOTE QUOTED=n SKIP=n |
| LNARRPR | weekly | Print the arrears worklist (PRTF, page overflow). | LNLOANL/LNBRW → LNARRP | LNARRPR LINES=n TOT=x |
| LNBILLP | monthly | Bill instalments due (due-date order); relieve accrual. | LNRUNDT; LNSCHDL/LNLOAN/LNTXN(B) | LNBILLP BILLED=n SKIP=n TOT=x |
| LNFEEPG | monthly | Charge servicing fees & escrow on billed loans. | LNRUNDT; LNFEE/LNLOAN/LNTXN(F) | LNFEEPG CHARGED=n SKIP=n SVC=x ESC=x |
| LNGLPST | monthly | Sum ledger by type; write balanced DR/CR pairs. | LNTXN → LNGLDIST (SQL) | LNGLPST BATCH=202602 ROWS=n |
| LNYRSTM | annual | Total year-end interest / accrual / fees from ledger. | LNTXN/LNLOAN | LNYRSTM BILLED=n INT=x ACCR=x FEE=x |
| LNRATRV | annual | Re-price active loans against credit score (anchored). | LNLOAN/LNBRW/LNSCHD | LNRATRV CHECKED=n REPRICED=n |
| LNCLOSE | annual | Close zero-balance loans; refund escrow. | LNLOAN/LNTXN(X) | LNCLOSE CLOSED=n SKIP=n |
| LNPORTF | monthly/annual | COBOL portfolio position & delinquent count. | reads LNLOAN | LNPORTF PRINCIPAL/ACCRUED/ARREARS/ESCROW/LOANS/DELINQUENT |
LNACCRU reads the run date from LNRUNDT (default 20260115), and for each active loan
with a positive balance accrues balance × rate/1200 / 30 for the day — 4.00 on
LN000001 — writing an LNACRD detail row (keyed LOANNO+ACDT) and, guarded by
a stable ledger key, an LNTXN type-I row, then adding to LNACCR.
LNCASH applies each unapplied receipt fees→interest→principal, surplus as a principal
prepayment; a partial receipt clears interest before principal. LNDELQP re-ages each active loan by
the days-past-due of its oldest billed-unpaid instalment (30/360 calendar) into buckets 0–4 and logs
any movement to LNDELQ.
Expected DSPLY (one active 5000/12000-class loan accruing 4.00/day): LNACCRU ACCRUED= 1 SKIP= 3 TOT= 4.00 LNCASH APPLIED= 1 SKIP= 0 one receipt applied LNDELQP CHECKED= 4 MOVED= 0 book aged, nothing moved yet
LNARRWL reads the delinquency logical file LNLOANL (which omits bucket-0 loans),
totalling arrears (LNINTDU+LNPRNDU+LNFEE) by bucket. LNQUOTE prices a payoff per active
loan: balance + (accrued + billed-unpaid interest) + fees - escrow, netting escrow off as a
credit back to the borrower, and writes one LNQUOT row keyed (LOANNO, QTDT).
LNARRPR prints the same worklist with borrower names and the payoff figure.
Expected payoff (LN000001 after month-1 billing, per the cycles suite):
payoff = 12000.00 + (0.00 accrued + 120.00 billed) + 30.00 fees - 25.00 escrow = 12125.00
LNBILLP walks the schedule in due-date order via LNSCHDL, bills every
S instalment due on or before the cut-off (default 20260201), moving its interest/principal
into LNINTDU/LNPRNDU, relieving LNACCR by the interest billed (floored
at 0), flipping the schedule row to B and posting an LNTXN type-B
row. LNFEEPG totals the active fee catalogue by type and charges every billed loan the servicing fee
(into LNFEE) and escrow (into LNFEE and LNESCRW both — a
two-sided movement). LNGLPST sums the ledger and books the double entries. LNPORTF reports the
book.
Expected DSPLY (the 4-loan seed book, month 1): LNBILLP BILLED= 4 SKIP=... TOT= 4480.00 4 x 1120.00-class instalments LNFEEPG CHARGED= 4 SKIP=... SVC= 5.00 ESC= 25.00 per-loan; book: fees 120.00, escrow 100.00 LNGLPST BATCH=202602 ROWS= 6 DR/CR pairs: 480.00 interest, 120.00 fee+escrow LNPORTF LOANS ... 4 DELINQUENT ...
1210-INTR / CR
4100-INTI (480.00); cash applied — DR 1010-CASH / CR 1200-LNRC;
fees & escrow — DR 1220-FEER / CR 4200-FEEI (120.00). Each pair is
balanced; the credit side is what the income accounts recognise.LNYRSTM totals interest billed (ledger B rows), accrued (I) and fees
(F) for the year-end certificate (INT=480.00 across the seed book). LNRATRV
re-prices active loans against credit score: score ≥750 → original rate −1.0000 (floor
6.0000); score <600 → original rate +2.0000 (cap 30.0000); otherwise unchanged. The adjustment
anchors on the original contracted rate (recovered from schedule instalment 1: interest/opening
×1200), never on the current rate, so the first run moves the rate and every re-run is a no-op.
LNCLOSE settles any active zero-balance loan (all buckets zero): refunds the escrow via an
LNTXN type-X row and flips status to S.
Expected DSPLY (per the cycles suite): LNYRSTM BILLED= 4 INT= 480.00 ACCR= 16.00 FEE=120.00 LNRATRV CHECKED= 4 REPRICED= 2 780->11.0000 (down), 590->26.0000 (up) LNCLOSE CLOSED= 0 SKIP= 4 none fully repaid yet
LNORIG must build the schedule and activate the
loan before accrual/billing have anything to act on.LNDELQP ages the oldest
billed-unpaid instalment, so a loan only becomes delinquent after LNBILLP has
billed instalments that then go unpaid.LNFEEPG charges only loans that have been billed (non-zero
due buckets), so it must follow LNBILLP in the monthly CL.LNCASH drives the balance to zero but does not settle;
LNCLOSE (annual) does the settlement + escrow refund. Run the servicing that zeroes a
loan before expecting the annual close to settle it.LNRUNDT to process a new period — see F.3.All files are in library AMORTIS, grounded in the DDS/SQL members in
sources.mjs. Dates are stored as signed yyyymmdd numerics; money is packed
11P2; the nominal annual rate is packed 7P4 (12.0000 = 12%), wide enough that
dividing by 12 keeps the periodic factor exact.
| Field | Type | Meaning |
|---|---|---|
| BRWID | 7A | Borrower id (key), e.g. BR00001. |
| BRNAME | 30A | Borrower name. |
| BRADDR | 30A | Address. |
| BRSCORE | 4S 0 | Credit score (read by the annual rate review). |
| BRSTAT | 1A | A active, C closed. |
| BROPNDT | 8S 0 | Relationship-open date (yyyymmdd). |
| Field | Type | Meaning |
|---|---|---|
| LOANNO | 8A | Loan number (key), e.g. LN000001. |
| BRWID | 7A | Owning borrower. |
| LNPRIN | 11P 2 | Original advanced principal. |
| LNRATE | 7P 4 | Nominal annual rate percent (12.0000 = 12%). |
| LNTERM | 3S 0 | Number of monthly instalments. |
| LNBAL | 11P 2 | Current principal outstanding. |
| LNACCR | 11P 2 | Interest accrued but not yet billed. |
| LNINTDU | 11P 2 | Interest billed and still unpaid. |
| LNPRNDU | 11P 2 | Principal billed and still unpaid. |
| LNESCRW | 11P 2 | Escrow balance held on the loan. |
| LNFEE | 11P 2 | Fees outstanding. |
| LNBUCK | 1S 0 | Delinquency bucket 0–4 (0 current, 4 = 90+). |
| LNDPD | 5S 0 | Days past due. |
| LNSTAT | 1A | P pending, A active, S settled, W written off. |
| LNOPNDT / LNMATDT / LNNXTDT | 8S 0 | Opened / matures / next-due dates (yyyymmdd). |
| Field | Type | Meaning |
|---|---|---|
| LOANNO / SCHINS | 8A / 3S 0 | Loan + instalment number (composite key). |
| SCHDUEDT | 8S 0 | Instalment due date (yyyymmdd). |
| SCHOPN | 11P 2 | Opening balance for the instalment. |
| SCHINT | 11P 2 | Interest component. |
| SCHPRN | 11P 2 | Principal component (constant, plug on the last). |
| SCHAMT | 11P 2 | Instalment = SCHINT + SCHPRN. |
| SCHCLS | 11P 2 | Closing balance = SCHOPN − SCHPRN. |
| SCHSTAT | 1A | S scheduled, B billed, P paid. |
Logical views: LNSCHDL keys the same records by SCHDUEDT, LOANNO
(the monthly billing's due-date access path). LNLOANL keys LNLOAN by
LNBUCK, LOANNO and omits bucket 0 (COMP(GT 0)) — the collections worklist
is the file.
| Field | Type | Meaning |
|---|---|---|
| PYREF | 8A | Receipt reference (key), e.g. PY000001. |
| LOANNO | 8A | Loan the receipt is for. |
| PYDT / PYAMT | 8S 0 / 11P 2 | Receipt date / amount. |
| PYMETH | 1A | Payment method. |
| PYSTAT | 1A | U unapplied, A applied (applied once). |
| Field | Type | Meaning |
|---|---|---|
| TXSEQ | 8S 0 | Stable business-derived sequence (key; see F.2). |
| LOANNO | 8A | Loan. |
| TXTYPE | 1A | I accrual, B billing, C cash, F fee/escrow, S payoff, X closure. |
| TXDT / TXAMT | 8S 0 / 11P 2 | Transaction date / amount. |
| TXINT / TXPRN | 11P 2 | Interest / principal split of the amount. |
| TXBAL | 11P 2 | Balance snapshot after the txn (where applicable). |
| TXREF / TXMEMO | 8A / 25A | Source reference / free-text memo. |
| Field | Type | Meaning |
|---|---|---|
| LOANNO / ACDT | 8A / 8S 0 | Loan + accrual date (composite key — the accrual idempotency guard). |
| ACBAL | 11P 2 | Balance the accrual was computed on. |
| ACRATE | 9P 6 | Monthly factor applied (rate/1200). |
| ACAMT | 11P 2 | Interest accrued that day. |
| Field | Type | Meaning |
|---|---|---|
| DQSEQ | 8S 0 | Stable per (loan, resulting bucket) (key; range 70000000). |
| LOANNO / DQDT | 8A / 8S 0 | Loan / re-bucketing date. |
| DQOLD / DQNEW | 1S 0 | Bucket before / after. |
| DQDPD / DQAMT | 5S 0 / 11P 2 | Days past due / arrears at the move. |
| Field | Type | Meaning |
|---|---|---|
| LOANNO / QTDT | 8A / 8S 0 | Loan + quote date (composite key; one quote per day). |
| QTPRIN / QTINT / QTFEE / QTESCR | 11P 2 | Principal / interest / fees / escrow components. |
| QTTOT | 11P 2 | Payoff total = prin + int + fee − escrow. |
| QTSTAT | 1A | O open quotation. |
| Field | Type | Meaning |
|---|---|---|
| FECODE | 4A | Fee code (key): SVC1, ESC1, LAT1. |
| FEDESC | 25A | Description. |
| FETYPE | 1A | S servicing fee (per instalment), E escrow contribution. |
| FEAMT | 11P 2 | Amount (SVC1 5.00, ESC1 25.00, LAT1 15.00). |
| FESTAT | 1A | A active, I inactive (LAT1 is seeded inactive). |
| Field | Type | Meaning |
|---|---|---|
| GLSEQ | DECIMAL(8,0) | Distribution row sequence (PK). |
| GLBATCH | DECIMAL(6,0) | Posting batch (yyyymm). |
| GLACCT / GLDRCR | CHAR(9) / CHAR(1) | Account (e.g. 4100-INTI) / D or C. |
| GLAMT | DECIMAL(11,2) | Posted amount. |
| GLLOAN / GLDT | CHAR(8) / DECIMAL(8,0) | Loan (or *ALL) / GL date. |
Indexed by (GLACCT, GLDRCR) (LNGLDACC) so income by account is a keyed read.
CALL AMORTIS/LNSETUP, then
CALL AMORTIS/LNREFLD (seeds 4 borrowers, 3 fees, 4 pending loans), then
CALL AMORTIS/LNORIG to build schedules and activate the loans.CHGDTAARA DTAARA(AMORTIS/LNRUNDT) VALUE(' <YYYYMMDD>')
(the value is CHAR(10), right-justified).SBMJOB CMD(CALL PGM(AMORTIS/LNDAILY)).CALL AMORTIS/LNMENU (option 1 loan inquiry, option 2
the amortisation subfile) as needed.Pre-checks: confirm the job's library list includes AMORTIS; confirm
LNRUNDT holds the intended date (or is blank to accept the program default).
Post-checks after the daily cycle:
LNACCRU ACCRUED= equals the count of active positive-balance loans; TOT=
reconciles to their per-day interest (4.00 for each 12000@12% loan). Exactly one LNACRD
row per loan per date; one LNTXN type-I row per loan per date.LNCASH APPLIED= equals the unapplied receipts; each waterfall obeyed
fees→interest→principal (a 500.00 receipt on a loan owing 120.00 interest + 1000.00
principal retires the 120.00 first, then 380.00 principal).LNDELQP CHECKED= equals the active loans; MOVED= equals those that changed
bucket, each with a matching LNDELQ row.CHGDTAARA DTAARA(AMORTIS/LNRUNDT) VALUE(' <month-end>').LNMONTH. Confirm LNBILLP BILLED=, LNFEEPG CHARGED=,
LNGLPST ROWS=, and the LNPORTF lines.LNYEAR and confirm the statement, rate review and closure lines.Reconciling figures (the same ones the volume suites check against a hand-derived oracle, for the 4-loan seed book after month-1 billing):
SUM(SCHINT) billed = 480.00; LNYRSTM INT=480.00
and GL 4100-INTI credited 480.00.4200-FEEI credited with the 120.00 of fee+escrow the ledger recorded.LNACCR is 0.00 (the 4.00 accrued was
relieved by the 120.00 billed, floored at 0).LNRATRV REPRICED=2, and a re-run reprices 0.LNPORTF LOANS 4, with PRINCIPAL / ACCRUED / ARREARS / ESCROW
totals summed straight off LNLOAN.Ad-hoc portfolio & GL review: CALL AMORTIS/LNPORTF DSPLYs the book totals;
STRSQL → SELECT GLACCT,GLDRCR,SUM(GLAMT) FROM AMORTIS.LNGLDIST GROUP BY GLACCT,GLDRCR
proves the DR/CR pairs balance.
Each program DSPLYs a one-line result. Re-run safety is built on the stable idempotency keys
(F.2): a re-run regenerates the same key, the CHAIN(EN) guard finds the existing row, and the
program refuses to double-post.
| Situation | Behaviour | Action |
|---|---|---|
| Re-run the daily accrual, same date | Each loan already has its LNACRD (LOANNO+ACDT) row and its ledger row. | Safe no-op — nothing double-accrues. Idempotent per accrual date. Advance LNRUNDT to accrue a new day. |
| Re-run cash application | The receipt is flipped to A; its ledger key (range 30000000) already exists. | Safe no-op — a receipt is applied exactly once. |
| Re-run monthly billing, same cut-off | Billed instalments are B; their ledger key (20000000) already exists. | Safe no-op — BILLED=0, the due bucket stays 120.00 (not 240.00). |
| Re-run fees | The fee ledger key (40000000) per loop already exists. | Safe no-op — CHARGED=0. |
| Re-run delinquency re-bucket | History key (70000000 + loan + resulting bucket) is stable per move. | No second history row for the same bucket; the loan is simply recomputed to the same value. |
| Re-run the annual rate review | The target is anchored to the original rate. | First run reprices, every re-run is REPRICED=0 — never walks the rate down repeatedly. |
| Re-run closure | An S loan is skipped; its closure ledger key (60000000) exists. | Safe no-op; a loan is never closed twice, and its escrow refunds exactly once. |
| Run a dated cycle without advancing LNRUNDT | The run date is unchanged, so the idempotency key is unchanged. | The cycle processes the same period again as a no-op. Always CHGDTAARA to a new date to process a new period (see F.3). |
LNTXN (with its interest/
principal split and, where relevant, a post-txn balance), every accrual to LNACRD, and every
bucket move to LNDELQ, any cycle's effect is fully reconstructable after the fact for
reconciliation and recovery.The complete program surface, from sources.mjs. All objects are in library
AMORTIS. The RPG is written in the fixed-form C-spec / free-form hybrid the emulator's
ibmi/samples.js convention uses; helpers C() and D() in
sources.mjs emit column-exact C-spec and D-spec source.
LNREFLD BORROWERS=4 FEES=3 LOANS=4.P loan, generates the constant-principal schedule (periodic factor
rate/1200, principal advance/term, last-instalment plug), then re-CHAINs the
loan and activates it (LNBAL=LNPRIN, LNSTAT='A'). Idempotent via the
composite (LOANNO, SCHINS) KLIST guard.LNACCR += balance × rate/1200 / 30, one
LNACRD (LOANNO+ACDT) row and one LNTXN type-I row (key range
10000000 + loan digits ×100 + day slot).LNBAL) → surplus
principal prepayment. Ledger key range 30000000 + receipt digits; receipt flipped to A.
Drives balance to zero but does not settle.Billed-unpaid instalment, computes 30/360 days-past-due, maps to bucket
0–4, updates LNBUCK/LNDPD, and (on a move) writes LNDELQ.LNSCHDL; bills each S instalment due
≤ cut-off, moves interest/principal to the due buckets, relieves LNACCR (floored 0),
flips the row to B, posts LNTXN type-B (key 20000000).LNFEE) and escrow (into LNFEE and LNESCRW), posts type-F
(key 40000000).LNTXN by type and inserts balanced DR/CR pairs into LNGLDIST with
embedded EXEC SQL INSERT: interest (1210-INTR/4100-INTI), cash (1010-CASH/1200-LNRC),
fee+escrow (1220-FEER/4200-FEEI).balance + (accrued + billed-unpaid interest) + fees - escrow, one
LNQUOT row keyed (LOANNO, QTDT).LNLOANL (bucket-0 omitted), total arrears by bucket / print with borrower names and
payoff. LNARRPR uses printer overflow (OFLIND(*IN90)) to re-print the header per page.SCHINT×1200/SCHOPN)
and applies the score policy to that anchor. Idempotent by anchoring, not delta.LNESCRW via type-X (key
60000000), sets LNSTAT='S'. Skips already-S loans.B), accrual (I) and fees (F) for the
tax certificate.LNLOAN, DSPLYing PRINCIPAL / ACCRUED / ARREARS / ESCROW /
LOANS / DELINQUENT totals.ADDLIBLE LIB(AMORTIS) then CALLing its
members in the fixed order (section C).AMORTIS/i has no DB trigger layer; re-run safety is engineered into the ledger/detail keys. Each
posting program derives a stable key from a business key plus a disjoint numeric range, so a re-run
regenerates the SAME key, the CHAIN(EN) guard finds it, and the program refuses rather than
double-posting.
| Program | TXTYPE | Key formula |
|---|---|---|
| LNACCRU | I | 10000000 + loan digits × 100 + accrual day-of-month slot |
| LNBILLP | B | 20000000 + loan digits × 100 + instalment number |
| LNCASH | C | 30000000 + receipt-reference digits |
| LNFEEPG | F | 40000000 + loan digits × 100 + instalment number |
| LNCLOSE | X | 60000000 + loan digits |
| LNDELQP | — | 70000000 + loan digits × 10 + resulting bucket (into LNDELQ) |
LNSCHD (composite LOANNO+SCHINS) and LNQUOT / LNACRD
(composite LOANNO+date) are guarded through KLISTs rather than a single range — guarding
on LOANNO alone would match instalment 1 and skip the whole loan.
Every dated batch program (LNACCRU, LNDELQP, LNBILLP,
LNFEEPG, LNQUOTE) reads its processing date from a *CHAR(10) data
area AMORTIS/LNRUNDT. If it holds a non-blank all-digit value, that is the date; otherwise the
program falls back to a built-in default literal. The operator sets it with
CHGDTAARA DTAARA(AMORTIS/LNRUNDT) VALUE(' YYYYMMDD') and blanks it to restore defaults.
LNRUNDT is what lets
the cycle advance day by day. A related fix: the accrual ledger's day slot (WDAY) must track
the real accrual date, not a fixed literal, or the loan-level totals look right while the LNTXN
audit trail silently stops growing after day one.Every batch program ends with a single DSPLY line the operator (and the test harness) reads
as its result — the AMORTIS/i equivalent of a job-completion message. The full set:
| Program | DSPLY line |
|---|---|
| LNREFLD | LNREFLD BORROWERS=n FEES=n LOANS=n |
| LNORIG | LNORIG ORIGINATED=n SKIP=n |
| LNACCRU | LNACCRU ACCRUED=n SKIP=n TOT=x |
| LNCASH | LNCASH APPLIED=n SKIP=n |
| LNDELQP | LNDELQP CHECKED=n MOVED=n |
| LNBILLP | LNBILLP BILLED=n SKIP=n TOT=x |
| LNFEEPG | LNFEEPG CHARGED=n SKIP=n SVC=x ESC=x |
| LNGLPST | LNGLPST BATCH=202602 ROWS=n |
| LNQUOTE | LNQUOTE QUOTED=n SKIP=n |
| LNARRWL | LNARRWL DELQ=n B1=x B2=x B3=x B4=x TOT=x |
| LNARRPR | LNARRPR LINES=n TOT=x |
| LNRATRV | LNRATRV CHECKED=n REPRICED=n |
| LNCLOSE | LNCLOSE CLOSED=n SKIP=n |
| LNYRSTM | LNYRSTM BILLED=n INT=x ACCR=x FEE=x |
| LNPORTF | LNPORTF PRINCIPAL/ACCRUED/ARREARS/ESCROW/LOANS/DELINQUENT <value> (six lines) |
balance × rate/1200 / 30; a 30-day month accrues exactly the schedule's interest.CHAIN(EN); and, for the rate review, by
anchoring on the original rate.*CHAR(10) data area holding the processing date the dated batch programs read, set by
CHGDTAARA, so a parameterless cycle can be driven for any period.