AMPCOOP/i is a rural electric-cooperative billing application: member and meter
master maintenance, rollover-safe meter-read import, tiered kWh + demand (kVA) consumption billing,
cash posting into a member ledger, delinquency / disconnect & reconnect processing, late-fee and
interest assessment, the coop-distinctive capital-credits (patronage) allocation, and a balanced
GL posting under commitment control. It is a native IBM i application: RPG (fixed-form and totally
free-form **FREE), one ILE COBOL program, CL drivers, and DDS physical / logical / display /
printer files — no SQL-PL business-logic layer; the rules live in the RPG. Every batch program is a
parameterless SBMJOB step that reads its own arrival files. This manual is the reference for
the operator who runs the online menu and the night batch chain, and for the developer maintaining the
application. It is grounded entirely in the committed source (electric-app/src/sources.mjs,
src/seed.mjs, and the test/ampcoop_build.mjs / ampcoop_daycycle.mjs
drivers). Everything runs in library AMPCOOP.
AMPCOOP/i services the monthly billing cycle of a member-owned electric cooperative:
AMMEMB) and
meter master (AMMETER) through one callable program (AMMNT), and load the
tiered rate plan (AMRATLD).AMREADIM).AMBLGEN).AMCSHPST).AMDELINQ), assess a flat late fee + monthly interest
(AMFEECAL), and reconnect a paid-up disconnected location (AMRECON).AMCCALLOC).AMGLPST), print the bills (AMBILLPR), age the AR
(AMAGE), and cross-check the GL in ILE COBOL (AMGLRPT).Honest scope. AMPCOOP/i is a deliberately compact but genuine cooperative estate.
It models the forward billing cycle: meter read (rollover-safe) → tiered + demand consumption
bill → member ledger → payment / delinquency → capital-credits patronage → balanced
GL. It does not model capital-credit retirement (board-declared cash payout of a
prior vintage): AMCCALC.CCRETIRE and the member's MCCBAL exist as running
ledger fields so a future retirement program has somewhere to post, but this system's batch cycles only
allocate. There is no prorated final-bill, budget-billing, or tax engine. The value is the honest,
end-to-end, GL-balanced core — verified against an independent JS oracle in the day-cycle test.
Unlike a two-layer SQL-PL application, AMPCOOP/i keeps all business logic in the RPG programs
themselves, working directly against externally-described DDS files through native record-level I/O
(CHAIN / READ / READE / SETLL / WRITE /
UPDATE). SQL appears in exactly two places, both deliberate:
AMGLF is created by SQL DDL through
RUNSQLSTM, and AMGLPST is a **FREE SQLRPGLE program that reads
control totals with embedded SELECT ... SUM(...) INTO, inserts balanced DR/CR rows, and
COMMITs or ROLLBACKs under commitment control.AMGLRPT (ILE COBOL) reads
AMBILL through a COMP-3 FD and cross-checks its total against an embedded
SQL SUM over the GL feed.Everything else — the tiered billing walk, rollover math, payment waterfall, delinquency,
fees, capital credits — is plain RPG. Several D-spec-heavy calc programs are built column-exact
from fixed-form helpers (C()/D() in the source), and the interactive and
maintenance programs are totally-free-form /free. The night chain is a set of separate
SBMJOBs sequenced on a *JOBQ so each program fully closes its output files
before the next opens them.
MAINTENANCE ONLINE (5250) BATCH (night *JOBQ chain)
----------- ------------- ------------------------
AMRATLD (rate plan) AMMENU ---1---> AMACCTIQ AMREADIM read import (rollover-safe)
AMMNT (memb/meter) ---2---> AMBILLIQ AMBLGEN tiered + demand billing
(subfile) AMCSHPST cash posting
AMDELINQ delinquency / disconnect
AMFEECAL late fee + interest
AMRECON reconnect (paid-up)
AMCCALLOC capital-credits allocation
AMGLPST GL post (commit ctl)
AMBILLPR bill print (spool)
AMAGE aged-AR report
AMGLRPT COBOL GL cross-check
data files (library AMPCOOP)
AMMEMB member master (MCCBAL running capital acct)
| AMSVCLOC service location (SLBAL, contracted SLKVA)
| AMMETER meter master (MPRVRD dial, MRLOVR modulus)
| AMRATE tiered rate plan (UNITPRC/BASECHG/DEMANDPRC)
v
AMREAD meter reads (RUSAGE rollover-safe) --AMBLGEN--> AMBILL + AMBLIN(tier lines)
|
AMCASH receipts --AMCSHPST--> ledger AMLEDG <---------+ every posting program
AMCCALC capital-credit allocations (B/P/R/F/C/A audit rows, disjoint key bands)
AMGLF DB2 GL feed <--AMGLPST-- balanced DR/CR
aging: AMAGELF (LF over AMBILL, keyed DUEDT) detail LF: AMBLINL (over AMBLIN, keyed BILLNO)
A single billing event flows: AMREADIM computes rollover-safe RUSAGE and
WRITEs an AMREAD row → AMBLGEN chains the service location for its rate
class + kVA, walks AMRATE tiers, and writes an AMBILL header, N
AMBLIN tier lines, an AMLEDG 'B' audit row, and raises SLBAL
→ later programs post payments, fees, capital credits and the GL against those same files.
| Object | Type | Role |
|---|---|---|
| AMMEMB | PF | Member master (member-owner; MCCBAL running capital-credit balance). |
| AMSVCLOC | PF | Service location (one metered point; SLBAL, contracted SLKVA). |
| AMMETER | PF | Meter master (dial MPRVRD, rollover modulus MRLOVR). |
| AMRATE | PF | Tiered rate plan (UNITPRC per tier, BASECHG, DEMANDPRC). |
| AMREAD | PF | Meter reads, arrival; rollover-safe RUSAGE (journaled). |
| AMBILL | PF | Bill header (energy/demand/total charge, balance, status). |
| AMBLIN | PF | Per-bill tier detail lines. |
| AMCASH | PF | Cash receipts, arrival. |
| AMLEDG | PF | Member ledger / AR audit trail (journaled; UNIQUE-keyed append). |
| AMCCALC | PF | Capital-credits allocation, one row/member/year. |
| AMBLINL | LF | AMBLIN keyed by BILLNO (bill's tier lines). |
| AMAGELF | LF | AMBILL keyed by DUEDT (aging / delinquency scan). |
| AMGLF | DB2 table | GL posting feed (SQL DDL via RUNSQLSTM). |
| AMACCTD / AMBILLD / AMMENUD | DSPF | Member inquiry / bill-inquiry subfile / operator menu. |
| AMBILLP | PRTF | Printed member electric bill. |
| AMRATLD | RPGLE | Rate-plan seed loader. |
| AMMNT | RPGLE | Member + meter master maintenance (callable). |
| AMREADIM | RPGLE | Rollover-safe meter-read import. |
| AMBLGEN | RPGLE | Tiered + demand billing engine. |
| AMCSHPST | RPGLE | Cash posting (partial-pay aware). |
| AMDELINQ | RPGLE | Delinquency / disconnect. |
| AMFEECAL | RPGLE | Late fee + interest accrual. |
| AMRECON | RPGLE | Reconnect (paid-up disconnected locations). |
| AMCCALLOC | RPGLE | Capital-credits (patronage) allocation. |
| AMGLPST | SQLRPGLE | GL posting under commitment control (**FREE + embedded SQL). |
| AMBILLPR | RPGLE | Bill print (PRTF spool). |
| AMAGE | RPGLE | Aged-AR report. |
| AMACCTIQ / AMBILLIQ / AMMENU | RPGLE | Member inquiry / bill-inquiry subfile / menu. |
| AMGLRPT | CBLLE | ILE COBOL GL cross-check (COMP-3 FD + embedded SQL). |
| AMSETUP / AMNIGHT | CLP | Build-everything setup / night-batch entry point. |
The full catalogue is 10 PFs, 2 LFs, 1 DB2 table, 3 DSPFs, 1 PRTF, 14 RPG programs, 1 ILE COBOL
program and 2 CL programs, over the single library AMPCOOP. Sections D and F expand each.
AMPCOOP/i has no CICS transaction identifiers. On IBM i, each program is reached by name from a
5250 command-entry line (or a SBMJOB/*JOBQ for the batch jobs). 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 AMPCOOP — the tested jobs run
with LIBL = QSYS QGPL AMPCOOP QTEMP and CURLIB = AMPCOOP.
| To do this | Type on the command line |
|---|---|
| Open the operator menu (routes to the two inquiries) | CALL AMPCOOP/AMMENU |
| Open member/account inquiry directly | CALL AMPCOOP/AMACCTIQ |
| Open the bill-inquiry subfile directly | CALL AMPCOOP/AMBILLIQ |
| Maintain a member or meter (callable, parameterized) | CALL AMPCOOP/AMMNT PARM(...) |
| Load the tiered rate plan (reference data) | CALL AMPCOOP/AMRATLD |
| Run one night-batch step | SBMJOB CMD(CALL AMPCOOP/AMBLGEN) JOBQ(AMPCOOP/NITEQ) |
| Run the whole night chain (single CL entry point) | CALL AMPCOOP/AMNIGHT |
The night-batch programs take no CALL parameters — each does one pass over its arrival /
aging file, so a scheduled submission is a bare CALL. Only AMMENU,
AMACCTIQ and AMBILLIQ are interactive; AMMNT is callable but
parameter-driven (no screen of its own).
AMMENU is a small /free WORKSTN program over the AMMENUD menu
format. It EXFMTs the menu, reads MOPT, and routes to one of the two inquiry programs by a
classic indicator-conditioned fixed-form dynamic CALL (option 1 lights
*IN81 → CALL 'AMACCTIQ'; option 2 lights *IN82 →
CALL 'AMBILLIQ'). It loops until F3.
if/if (not elseif) to set the
routing indicators, and the two CALLs are fixed-form indicator-conditioned C-specs —
documented engine-compatibility accommodations. An option other than 1/2 sets DMSG='Invalid
option' and re-displays.AMACCTIQ is a plain (non-subfile) WORKSTN inquiry. The operator keys a service
location number (ISLNO) and presses Enter; the program
CHAINs AMSVCLOC, then CHAINs AMMEMB for the member name
and the running capital-credit balance, and displays member / rate class / meter / status / balance /
capital credits. It loops until F3.
| Field | Type (DDS) | Shows |
|---|---|---|
| ISLNO | 8A input/output | Service-location number keyed by the operator. |
| DMEMB | 25A output | Member name (from AMMEMB via SLNO→MEMBNO). |
| DRATE | 3A output | Rate class (RATECD). |
| DMETER | 8A output | Meter number (METERNO). |
| DSLSTAT | 1A output | Service-location status (A active, D disconnected). |
| DBAL | 14A output | Service-location balance (SLBAL, rendered). |
| DCCBAL | 14A output | Member's running capital-credit balance (MCCBAL). |
| DMSG | 50A output | Result/error message (HI). |
Not-found and blank-key cases each set DMSG and re-display without a chain
error. The balance shown reflects current state — after the night cycle it is the
post-payment, post-late-fee SLBAL.
AMBILLIQ is the app's one subfile screen. The operator keys a bill number
(IBILLNO); the program CHAINs AMBILL for the header (svc location,
cycle, usage, status, energy / demand / total charge), then clears and reloads the BSFL
subfile with the bill's tier detail lines by SETLL/READE over
AMBLINL (the LF keyed by BILLNO), and EXFMTs BCTL. Subfile control indicators:
41=SFLDSP, 42=SFLDSPCTL, 43=SFLCLR, 44=SFLEND(*MORE),
with SFLPAG(10) / SFLSIZ(20).
| Field | Type (DDS) | Shows |
|---|---|---|
| SLINENO | 2Y0 output | Bill line number. |
| STIER | 2Y0 output | Rate tier number for the line (LTIERNO). |
| SUSE | 9Y0 output | kWh billed in this tier band (TIERUSE). |
| SPRC | 14A output | Per-kWh tier price (TIERPRC). |
| SAMT | 14A output | Tier line amount (LINEAMT). |
| IBILLNO | 8A input/output | Bill number keyed by the operator. |
| DUSAGE / DBSTAT | 10A / 1A output | Total metered usage / bill status. |
| DENERGY / DDEMAND / DCHG | 14A output | Energy charge / demand charge / total charge. |
A not-found bill clears the subfile (SFLCLR) and sets DMSG='Bill not
found'. The header energy/demand split is exactly the AMBLGEN output: for the tested COM bill,
demand 170.00 = 20 kVA × 8.5000, total 226.50.
AMMNT is a callable (dcl-pi) maintenance program — no screen of its own;
it is driven by parameters (from a menu shell, another program, or a command). One program covers both the
member (AMMEMB) and the meter (AMMETER) masters, selected by
pAction:
| pAction | Does | Behaviour |
|---|---|---|
| CC | Create/change member | CHAIN AMMEMB on the 6-char member key; if found, UPDATE name/city/state (msg MEMBER CHANGED); else WRITE a fresh active member with MCCBAL 0 (MEMBER CREATED). |
| MC | Create/change meter | CHAIN AMMETER on METERNO; if found, UPDATE svc-loc + multiplier (METER CHANGED); else WRITE a new active meter (MRLOVR 99999, MPRVRD 0) (METER CREATED). |
| CI | Inquire member | CHAIN AMMEMB; returns name/city/state and MEMBER FOUND/NOT FOUND via OUT parms. |
| MI | Inquire meter | CHAIN AMMETER; returns svc-loc + multiplier and METER FOUND/NOT FOUND. |
Create/change is idempotent and validate-before-write: it re-CHAINs (full key)
immediately before each UPDATE so a stale read record is never double-UPDATEd, and uses
%found() to decide create vs. change. Any other action returns BAD ACTION.
Honest statement: AMPCOOP/i does not model a four-eyes maker–checker /
separate-authorization workflow. A cash receipt posted by AMCSHPST is applied immediately; a
bill generated by AMBLGEN is written in one pass. The control model the application
does have is data-layer audit + status gating + integrity:
AMSETUP starts journaling (STRJRNPF IMAGES(*BOTH)) on the
two audit-critical files — the meter reads AMREAD and the ledger
AMLEDG — to AMJRN/AMRCV, so both before/after images are
captured for recovery and DSPJRN review.BSTAT 'O'/'D' with a positive balance; fees only to 'D' bills with a positive balance;
reconnect only to 'D' service locations with SLBAL≤0; capital credits only to members
who actually patronized (patronage > 0).AMGLPST posts under commitment control and only
COMMITs if debits equal credits; otherwise it ROLLBACKs (see F.5).AMSETUP revokes *PUBLIC *ALL on the ledger and
re-grants *CHANGE — the audit file cannot be casually deleted/cleared.In sum the control posture is ledger audit + journaling + status/state gating + GL-balance enforcement, all in the data/RPG layer, rather than a segregation-of-duties approval workflow.
AMPCOOP/i's billing cycle runs as an ordered night chain. Each step is a separate program that
takes no CALL parameters — each does one forward pass over an arrival file
(AMREAD, AMCASH) or an aging LF (AMAGELF), or over a master. The
AMNIGHT CL is a documented single entry point that CALLs the core steps in order; the
day-cycle test drives the same steps as separate SBMJOBs on a *JOBQ
(NITEQ) so each program fully closes its output files before the next opens them — the
safe idiom because COBOL/CL OPEN OUTPUT semantics truncate a bare arrival PF, so overlapping
opens must be avoided.
-- create the night job queue once CRTJOBQ JOBQ(AMPCOOP/NITEQ) TEXT('AMPCOOP night batch queue') -- submit the steps in order (each parameterless) SBMJOB CMD(CALL AMPCOOP/AMBLGEN) JOB(AMBLGEN) JOBQ(AMPCOOP/NITEQ) HOLD(*NO) SBMJOB CMD(CALL AMPCOOP/AMCSHPST) JOB(AMCASH) JOBQ(AMPCOOP/NITEQ) HOLD(*NO) SBMJOB CMD(CALL AMPCOOP/AMDELINQ) JOB(AMDLQ) JOBQ(AMPCOOP/NITEQ) HOLD(*NO) SBMJOB CMD(CALL AMPCOOP/AMFEECAL) JOB(AMFEE) JOBQ(AMPCOOP/NITEQ) HOLD(*NO) SBMJOB CMD(CALL AMPCOOP/AMCCALLOC) JOB(AMCCALC) JOBQ(AMPCOOP/NITEQ) HOLD(*NO) SBMJOB CMD(CALL AMPCOOP/AMGLPST) JOB(AMGLP) JOBQ(AMPCOOP/NITEQ) HOLD(*NO) SBMJOB CMD(CALL AMPCOOP/AMBILLPR) JOB(AMPRT) JOBQ(AMPCOOP/NITEQ) HOLD(*NO) -- or the whole chain via the CL entry point CALL AMPCOOP/AMNIGHT
Meter-read import (AMREADIM) is a callable, parameterized program
(it takes a meter, service location, cycle, current reading and read-id) — it is the point where the
coop's AMI / manual-read upload feeds the system, run before billing to populate
AMREAD. It is not one of the parameterless night-chain steps. The AMNIGHT CL
sequences AMBLGEN → AMCSHPST → AMDELINQ → AMFEECAL → AMCCALLOC → AMGLPST
→ AMBILLPR.
| Program | Purpose | Reads / writes | Output (DSPLY) | Frequency |
|---|---|---|---|---|
| AMREADIM | Rollover-safe meter-read import. | CHAIN AMMETER; WRITE AMREAD; UPDATE AMMETER (roll MPRVRD forward). | AMREADIM meter USAGE=nnn ROLL=Y/N |
Per read upload (callable). |
| AMBLGEN | Tiered + demand billing engine. | READ AMREAD; CHAIN AMSVCLOC/AMRATE; WRITE AMBILL/AMBLIN/AMLEDG('B'); UPDATE SLBAL. | AMBLGEN GENERATED=g SKIPPED=s REJECTED=r |
Nightly. |
| AMCSHPST | Cash posting (partial-pay aware). | READ AMCASH; scan AMBILL for oldest open; UPDATE AMBILL/AMSVCLOC; WRITE AMLEDG('P'). | AMCSHPST FULL=f PART=p NOBILL=n |
Nightly. |
| AMDELINQ | Delinquency / disconnect. | READ AMAGELF; UPDATE AMBILL('D')/AMSVCLOC('D')/AMMEMB('D'); WRITE AMLEDG('A'). | AMDELINQ FLAGGED=d |
Nightly. |
| AMFEECAL | Late fee + monthly interest. | READ AMAGELF; UPDATE AMBILL/AMSVCLOC balances; WRITE AMLEDG('F'). | AMFEECAL ASSESSED=a TOTFEE=t |
Nightly. |
| AMRECON | Reconnect paid-up disconnected locations. | Scan AMSVCLOC; UPDATE AMSVCLOC('A')/AMMEMB('A'); WRITE AMLEDG('R'). | AMRECON RECONNECTED=r |
As needed (post-payment). |
| AMCCALLOC | Capital-credits (patronage) allocation. | Scan AMMEMB; sum CHARGE over AMBILL/AMSVCLOC; WRITE AMCCALC; UPDATE MCCBAL; WRITE AMLEDG('C'). | AMCCALLOC YEAR=y ALLOCATED=a SKIPPED=s TOTALLOC=t |
Annually (allocation year). |
| AMGLPST | GL posting under commitment control. | Embedded SQL SUM over AMBILL/AMLEDG; INSERT AMGLF; COMMIT/ROLLBACK. | AMGLPST IN BALANCE DR=.. CR=.. COMMITTED |
Nightly (close). |
| AMBILLPR | Bill print (PRTF spool). | READ AMBILL; READE AMBLINL; WRITE AMBILLP bands. | AMBILLPR PRINTED=p |
Nightly. |
| AMAGE | Aged-AR report. | READ AMAGELF (keyed DUEDT); accumulate open vs. delinquent. | AMAGE BILLS=n TOTBAL=t OPEN=o DLQ=d |
On demand. |
| AMGLRPT | ILE COBOL GL cross-check. | READ AMBILL (COMP-3 FD); embedded SQL SUM over AMGLF/AMLEDG. | AMGLRPT CROSS-CHECK: IN BALANCE |
On demand. |
Given a raw dial pair, AMREADIM CHAINs the meter master for its own last reading
(MPRVRD) and rollover modulus (MRLOVR), computes the rollover-safe consumption
(F.2), WRITEs the AMREAD row (setting RUSAGE and the RROLLOVR flag),
and UPDATEs MPRVRD forward to the current reading so the next cycle's baseline is correct.
Expected DSPLY (COM meter wrapped 99800 -> 00150, modulus 99999): AMREADIM MT000102 USAGE=350 ROLL=Y (99999-99800)+150+1 = 350
One pass over AMREAD. For each read it derives a deterministic BILLNO
('B' + last 7 of READID); a CHAIN(EN) that finds an existing bill skips it (idempotent
re-run). It CHAINs AMSVCLOC for the rate class + contracted SLKVA, then walks
AMRATE tiers in TIERNO order via a bounded full-key CHAIN(E) loop, billing
MIN(remaining, band) × UNITPRC per tier, adding BASECHG and the demand
charge (SLKVA × DEMANDPRC) on tier 1. It uses a two-phase compute-then-persist
pattern (F.1) to avoid a torn write, then writes the header, N tier lines, an AMLEDG 'B' row,
and raises SLBAL.
Expected DSPLY (RES 1200 kWh + COM 350 kWh, both first-time bills): AMBLGEN GENERATED=2 SKIPPED=0 REJECTED=0 Tested bill totals (verified against the JS oracle): RES 1200 kWh: energy 173.00 (12.00 base + 55+70+36), demand 0, total 173.00 COM 350 kWh: energy 56.50 (25.00 base + 31.50), demand 170.00, total 226.50
One pass over AMCASH. Each receipt finds the oldest open bill for its service
location by a forward scan of AMBILL selecting the first 'O'/'D' bill matching
SLNO with a positive balance (AMBILL is keyed by BILLNO, not SLNO, so a bounded key CHAIN
isn't available). Partial pay → BSTAT='D'; full/over → BSTAT='P'.
SLBAL is decremented by the amount applied and an AMLEDG 'P' row is written.
Expected DSPLY (one 100.00 receipt, partial-paying the 173.00 RES bill): AMCSHPST FULL=0 PART=1 NOBILL=0 RES BSTAT=D, BILLBAL 73.00
One pass over the aging LF AMAGELF (keyed by DUEDT). Any bill still 'O'/'D'
with a positive balance is flagged delinquent: bill → BSTAT='D', its service location
→ SLSTAT='D' (disconnected) and its member → MSTAT='D', plus an
AMLEDG 'A' memo row. LEDGSEQ is assigned from a high-water-mark scan of the
program's reserved 90000000–90999999 band (F.4) so a second same-day run never collides.
Expected DSPLY (both bills past due after the partial payment):
AMDELINQ FLAGGED=2
One pass over AMAGELF. For every 'D' bill with a positive balance it assesses a flat late
fee (5.00) plus monthly interest at 1.50% on the bill's current balance,
computed in wide packed working storage (15P4/13P2) and rounded half-up to cents (the + 0.005
idiom). The fee is added to BILLBAL and SLBAL; an AMLEDG 'F' row is
written. Idempotency guard: before assessing, it scans AMLEDG for an existing 'F' row
already carrying this bill's memo tag — so a bill delinquent across multiple same-day runs is
fee'd at most once.
Expected DSPLY (both delinquent bills fee'd; interest on each current balance):
AMFEECAL ASSESSED=2 TOTFEE=15.15
RES: 5.00 + interest(73.00 x 1.5%)=1.10 = 6.10 -> BILLBAL 79.10
COM: 5.00 + interest(226.50 x 1.5%)=3.40 = 8.40 -> BILLBAL 234.90
Completes the disconnect→reconnect signature. PASS 1 scans AMSVCLOC: any
disconnected location ('D') whose balance is fully cleared (SLBAL≤0) is reconnected
(SLSTAT→'A') with an AMLEDG 'R' row. PASS 2 re-scans the master and
restores a member to active only if it has no other still-delinquent service location — a
genuine nested full-table scan (its reserved ledger band is 60000000–60999999).
The coop-distinctive step. It derives the allocation year from the bills' cycle
(CCYEAR = cycle / 100), then for each member sums patronage = total
CHARGE across all of that member's service locations' in-year bills, allocates a flat
8% (ALLOCPCT), writes one AMCCALC row (patronage, allocated,
retire 0), credits the member's running MCCBAL, and writes an AMLEDG 'C' row. A
CHAIN(EN) on the CCYEAR+MEMBNO key skips a member already allocated this year
(idempotent). Patronage is the original billed charge — unaffected by later fees or
payments — matching a real coop's "patronage = business done" basis.
Expected DSPLY (2 members, 8% of each member's billed patronage):
AMCCALLOC YEAR=2026 ALLOCATED=2 SKIPPED=0 TOTALLOC=31.96
RES member patronage 173.00 -> alloc 13.84
COM member patronage 226.50 -> alloc 18.12
Under commitment control (F.5): SUM billed charges, cash applied, fees/interest and capital credits
from AMBILL/AMLEDG, INSERT the four balanced DR/CR pairs into
AMGLF, re-SUM the batch, and COMMIT only if debits equal credits (else
ROLLBACK).
Expected DSPLY:
AMGLPST IN BALANCE DR=.. CR=.. COMMITTED
AMBLGEN bills from AMREAD.RUSAGE, so
AMREADIM must have imported the cycle's reads first.AMBILL — there must be bills to act on. Run
AMBLGEN first.AMFEECAL only fees bills already marked
'D'; delinquency must run first or nothing is fee'd.AMGLPST sums the final billed / cash / fee / capital
totals, so it runs last of the money steps; the balance check would otherwise be premature.All files are in library AMPCOOP, grounded in the DDS in src/sources.mjs.
Dates are stored as signed 8S 0 in YYYYMMDD form (cycle as 6S 0
YYYYMM); money and rates are packed decimal (P). Every DDS record format uses
distinct field names across files (e.g. RUSAGE/BUSAGE not
USAGE; MSTAT vs SLSTAT) — a deliberate platform-quirk guard
(F.6).
| Field | Type | Meaning |
|---|---|---|
| MEMBNO | 6A | Member number (PK), e.g. M00001. |
| MNAME / MADDR | 25A | Member name / street address. |
| MCITY / MSTATE / MZIP | 15A / 2A / 5A | City / state / ZIP. |
| MSTAT | 1A | Member status: A active, D delinquent (disconnected). |
| MJOINDT | 8S 0 | Join date (drives capital-credits vintage/eligibility). |
| MCCBAL | 9P 2 | Running lifetime capital-credit balance (allocated up, retired down). |
| Field | Type | Meaning |
|---|---|---|
| SLNO | 8A | Service-location number (PK), e.g. SL000101. |
| MEMBNO | 6A | Owning member. |
| RATECD | 3A | Rate class (RES / COM) → AMRATE. |
| METERNO | 8A | Serving meter → AMMETER. |
| SLSTAT | 1A | A active, D disconnected. |
| SLBAL | 9P 2 | Outstanding balance (raised by billing/fees, lowered by cash). |
| SLKVA | 5P 0 | Contracted / measured demand capacity (kVA) for the demand charge. |
| Field | Type | Meaning |
|---|---|---|
| METERNO | 8A | Meter number (PK). |
| MSLNO | 8A | Service location this meter serves. |
| MINSTDT | 8S 0 | Install date. |
| MMULT | 5P 2 | Dial multiplier (scaled-unit meters). |
| MPRVRD | 9P 0 | Meter's own last-recorded dial reading (rolled forward by AMREADIM). |
| MRLOVR | 9P 0 | Rollover modulus / dial capacity (e.g. 99999); 0 = no fixed dial wheel. |
| MSTAT | 1A | Meter status (A active). |
| Field | Type | Meaning |
|---|---|---|
| RATECD / TIERNO | 3A / 2S 0 | Rate class + tier number (PK). |
| TIERUB | 9P 0 | Tier upper bound (cumulative kWh); the top tier uses a sentinel-large bound to absorb the rest. |
| UNITPRC | 7P 4 | Per-kWh price for this tier's band. |
| BASECHG | 7P 2 | Fixed monthly service charge (carried on tier 1). |
| DEMANDPRC | 7P 4 | Per-kVA demand charge price (carried on tier 1; 0 for RES). |
Seeded plan (AMRATLD): RES 3 tiers — base 12.00, 500 kWh @0.1100, next 500 @0.1400, rest @0.1800, no demand; COM 2 tiers — base 25.00, 2000 kWh @0.0900, rest @0.1200, demand 8.5000/kVA.
| Field | Type | Meaning |
|---|---|---|
| READID | 8S 0 | Read identifier (PK). |
| SLNO / CYCLE | 8A / 6S 0 | Service location / billing cycle (YYYYMM). |
| PRIORRD / CURRRD | 9P 0 | Prior / current raw dial readings. |
| RUSAGE | 9P 0 | Already-computed, rollover-safe consumption for this read. |
| RROLLOVR | 1A | Y if a dial rollover was detected on this read. |
| Field | Type | Meaning |
|---|---|---|
| BILLNO | 8A | Bill number (PK); deterministic 'B' + last 7 of READID. |
| SLNO / CYCLE | 8A / 6S 0 | Service location / cycle. |
| BUSAGE | 9P 0 | Total metered usage billed (kWh). |
| ENERGYCHG / DEMANDCHG / CHARGE | 9P 2 | Energy charge / demand charge / total. |
| DUEDT | 8S 0 | Due date (cycle×100 + 20). |
| BSTAT | 1A | O open, D delinquent, P paid. |
| BILLBAL | 9P 2 | Remaining balance on this bill. |
| Field | Type | Meaning |
|---|---|---|
| BILLNO / LINENO | 8A / 2S 0 | Bill + line number (PK). |
| LTIERNO | 2S 0 | Rate tier billed on this line. |
| TIERUSE | 9P 0 | kWh billed in this tier band. |
| TIERPRC | 7P 4 | Per-kWh price applied. |
| LINEAMT | 9P 2 | Line amount (TIERUSE×TIERPRC). |
| Field | Type | Meaning |
|---|---|---|
| RCPTNO | 8S 0 | Receipt number (PK). |
| SLNO / RCPTDT | 8A / 8S 0 | Service location paid / receipt date. |
| PAYAMT | 9P 2 | Amount received. |
| Field | Type | Meaning |
|---|---|---|
| LEDGSEQ | 8S 0 | Ledger sequence (PK, UNIQUE); each posting program uses a disjoint key band (F.4). |
| SLNO | 8A | Service location (blank for a member-level capital-credit row). |
| LTYPE | 1A | B bill, P payment, R reconnect, F fee/interest, C capital credit, A delinquency adj. |
| LDT | 8S 0 | Posting date. |
| LAMT | 9P 2 | Amount posted. |
| LMEMO | 20A | Free-text memo (also the AMFEECAL idempotency tag). |
| Field | Type | Meaning |
|---|---|---|
| CCYEAR / MEMBNO | 4S 0 / 6A | Allocation year + member (PK). |
| CCPATRON | 9P 2 | Total energy+demand charges billed to the member in-year (patronage base). |
| CCALLOC | 9P 2 | Capital credit allocated (ALLOCPCT of patronage). |
| CCRETIRE | 9P 2 | Amount retired/paid out against this vintage (0 — retirement not modelled). |
| Field | Type | Meaning |
|---|---|---|
| BATCHID | CHAR(8) | Posting batch id (e.g. GLB00001). |
| ACCT | CHAR(6) | GL account (120000 A/R, 400000 Revenue, 100000 Cash, 410000 Fee Revenue, 390000 Retained Margin, 290000 Members' Capital). |
| DRCR | CHAR(1) | D debit / C credit. |
| AMT | DECIMAL(11,2) | Posting amount. |
| SRCPGM | CHAR(8) | Source program (AMGLPST). |
| File | Over | Key | Used by |
|---|---|---|---|
| AMBLINL | AMBLIN | BILLNO | Bill print / bill-inquiry subfile (the bill's tier lines). |
| AMAGELF | AMBILL | DUEDT | Aging / delinquency / fee scans (SETLL *LOVAL forward READ by due date). |
The whole estate stands up from one CL. AMSETUP deletes then re-creates every PF/LF/DSPF/
PRTF, runs the SQL DDL for the GL feed (RUNSQLSTM), creates the journal
AMJRN/AMRCV and starts journaling on AMREAD + AMLEDG,
compiles all 14 RPG + 1 COBOL programs (CRTBNDRPG/CRTBNDCBL), sets ledger
authority, and CALLs AMRATLD to seed the rate plan.
CRTLIB LIB(AMPCOOP), then seed the source members (harness seedAmpcoop()).CRTCLPGM PGM(AMPCOOP/AMSETUP) SRCFILE(AMPCOOP/QCLSRC) SRCMBR(AMSETUP) (and AMNIGHT).CALL AMPCOOP/AMSETUP — expect the banner AMPCOOP setup complete. Rate plan loaded.Post-checks: all 10 PFs + 2 LFs + 3 DSPFs + PRTF exist; AMGLF is queryable
(SELECT COUNT(*) FROM AMPCOOP.AMGLF); AMJRN/AMRCV present; the rate
plan holds 5 tiers (RES 3, COM 2) with COM tier 1 DEMANDPRC = 8.5.
AMREADIM per meter (AMI/manual-read feed);
confirm each USAGE=/ROLL= line and that AMREAD got a row per
read with a correct rollover-safe RUSAGE.CRTJOBQ JOBQ(AMPCOOP/NITEQ).AMBLGEN → AMCSHPST → AMDELINQ →
AMFEECAL → AMCCALLOC → AMGLPST → AMBILLPR — or CALL
AMPCOOP/AMNIGHT. Wait for each job to reach OUTQ before relying on its output.SELECT COALESCE(SUM(CASE WHEN DRCR='D' THEN AMT ELSE 0 END),0) AS DR,
COALESCE(SUM(CASE WHEN DRCR='C' THEN AMT ELSE 0 END),0) AS CR
FROM AMPCOOP.AMGLF; -- DR must equal CR, and be non-zero
Reconciling figures (the same ones the day-cycle test checks against an independent JS oracle):
SELECT SUM(AMT) FROM AMPCOOP.AMGLF WHERE ACCT='120000' AND DRCR='D'.ACCT='100000') = total cash applied by AMCSHPST.ACCT='290000') = total capital credits allocated by
AMCCALLOC.CALL AMPCOOP/AMGLRPT reads AMBILL (COMP-3) and confirms
CROSS-CHECK: IN BALANCE (bill total + fees = GL A/R debit).CALL AMPCOOP/AMAGE gives BILLS=/TOTBAL=/OPEN=/DLQ=.Reconnects (as needed): after a disconnected member pays their balance to zero, run
CALL AMPCOOP/AMRECON and confirm RECONNECTED= and the location's
SLSTAT back to 'A' (member 'A' too if no other location is still delinquent).
Each program DSPLYs its result counts. The night chain is designed so most steps are safe to re-run:
| Situation | Behaviour | Action |
|---|---|---|
| Re-run AMBLGEN | Deterministic BILLNO + CHAIN(EN) skips already-billed reads. | Idempotent. Re-submit safely; SKIPPED= rises, GENERATED=0, no duplicate bills. |
| Re-run AMFEECAL same day | Ledger 'F' memo-tag scan finds the prior fee. | Idempotent. ASSESSED=0; no double fee. |
| Re-run AMCCALLOC same year | CHAIN(EN) on CCYEAR+MEMBNO skips allocated members. | Idempotent. SKIPPED= rises, no double allocation. |
| Re-run AMDELINQ same day | LEDGSEQ high-water-mark scan continues its 90M band; already-'D' bills stay 'D'. | Safe — no duplicate-key abend; new flags only for newly-past-due bills. |
| AMGLPST out of balance | Program ROLLBACKs the whole batch (commitment control). | No partial GL rows persist. Investigate source data, fix, re-run. |
| AMREADIM on unknown meter | METER NOT FOUND, no AMREAD row. | Create the meter (AMMNT 'MC') then re-import. |
| Cash for a service location with no open bill | NOBILL= increments; receipt not applied. | Confirm a bill exists / was generated; re-post after billing. |
| Overlapping batch opens | A bare arrival PF opened OUTPUT truncates on OPEN. | Run steps sequentially on the *JOBQ (wait for OUTQ) — never two output-file writers at once. |
STRJRNPF IMAGES(*BOTH), any night's effect is fully reconstructable after the fact
via DSPJRN for reconciliation and recovery.The business logic lives in the RPG. All objects are in library AMPCOOP; source in
electric-app/src/sources.mjs.
For each read, energy is billed by walking the rate tiers cumulatively. Tier t bills
MIN(remaining, band) × UNITPRC, where band = TIERUBt -
TIERUBt-1; the sentinel-topped last tier absorbs any remaining usage.
BASECHG and the demand charge (SLKVA × DEMANDPRC) are added on tier 1 only,
so demand is assessed once per bill regardless of how many kWh tiers the usage spans.
RES 1200 kWh (base 12.00; 500@0.11, 500@0.14, rest@0.18): tier1 500 x 0.11 = 55.00 tier2 500 x 0.14 = 70.00 tier3 200 x 0.18 = 36.00 energy = 12.00 + 55 + 70 + 36 = 173.00 ; demand 0 ; total 173.00 COM 350 kWh (base 25.00; 2000@0.09, rest@0.12; demand 8.5/kVA, 20 kVA): tier1 350 x 0.09 = 31.50 (all usage fits tier 1) energy = 25.00 + 31.50 = 56.50 ; demand 20 x 8.5 = 170.00 ; total 226.50
Two-phase pattern. The platform has a confirmed silent-decimal-overflow quirk on ADD-family ops
(POLARIS PG-011). AMBLGEN therefore computes and validates the whole bill in wide (11P2/13P2)
working accumulators in PHASE 1, writing nothing; if any line, the demand charge, or the whole-bill
charge would overflow the persistable 9P2 columns (> 9999999.99) it flags
WBADRD='Y' and increments REJECTED. Only a validated bill enters PHASE 2,
which re-walks the tiers (deterministic full-key CHAINs) to write the detail rows, then the header,
balance and ledger — so a would-be torn write is caught before any row is persisted.
Meter-read import is kept a separate concern from billing so metering and rate math can be
tested independently. Given a prior dial reading (MPRVRD from the meter master), a current
reading, and the meter's rollover modulus (MRLOVR):
if CURRRD >= PRIORRD: consumption = CURRRD - PRIORRD (normal) else: consumption = (MRLOVR - PRIORRD) + CURRRD + 1 (dial wrapped)
The wrap case is the distance from PRIORRD up to the dial's max, plus one for the wrap to
0, plus the distance from 0 to CURRRD. Tested directly: a 5-digit dial wrapping
99800 → 00150 (modulus 99999) yields (99999-99800)+150+1 = 350 kWh and
sets RROLLOVR='Y' — not a huge negative delta. AMREADIM also rolls
MPRVRD forward to the current reading so the next cycle's baseline is correct.
A cooperative is member-owned: margins are allocated back to members in proportion to their
patronage (the business they did with the coop), as retained-equity "capital credits" rather than
immediate cash. AMCCALLOC, for the allocation year (cycle / 100), sums each member's
patronage = SUM(CHARGE) across all their service locations' in-year bills, allocates a flat
ALLOCPCT = 8%, posts one AMCCALC row (patronage, allocated, retire 0), credits
the member's running MCCBAL, and writes an AMLEDG 'C' row.
RES patronage 173.00 x 8% = 13.84 COM patronage 226.50 x 8% = 18.12 -> TOTALLOC 31.96
Patronage is the original billed CHARGE — unaffected by later payments or fees —
matching a real coop's "patronage = business done" basis. Allocation is idempotent
(CHAIN(EN) on the year+member key skips an already-allocated member); a member with zero
patronage is skipped. The whole allocation is native RPG I/O — nested full-table scans keyed
differently per file, no SQL.
AMLEDG is a UNIQUE-keyed PF on LEDGSEQ, not a bare arrival file. A non-keyed output
PF opened OUTPUT truncates on each program's OPEN (COBOL/CL OPEN OUTPUT semantics), so a live
arrival ledger would only ever hold the last posting program's rows. A UNIQUE key routes writes through
the keyed WRITE path, which appends. Every posting program draws LEDGSEQ from a
disjoint numeric band so keys never collide across programs:
| Program | LTYPE | LEDGSEQ band |
|---|---|---|
| AMBLGEN | B (bill) | 10000000 + READID |
| AMCSHPST | P (payment) | 20000000 + RCPTNO |
| AMRECON | R (reconnect) | 60000000 + seq |
| AMFEECAL | F (fee/interest) | 70000000 + seq |
| AMCCALLOC | C (capital credit) | 80000000 + seq |
| AMDELINQ | A (delinquency adj) | 90000000 + seq |
The three seq-based programs (AMDELINQ/AMFEECAL/AMRECON/AMCCALLOC) compute a
high-water mark at start — a SETLL to the band floor then a forward scan capped
at the band ceiling — so a second run in the same business day continues from the current max
instead of restarting at the band floor and duplicating a key.
AMGLPST is a **FREE SQLRPGLE program under commitment control
(ctl-opt actgrp('AMPCOOP') commit(*yes)). It reads four control totals by embedded SQL,
inserts four balanced DR/CR pairs, re-sums the batch, and commits only in balance:
| Event | Debit | Credit |
|---|---|---|
| Billing | A/R (120000) | Revenue (400000) |
| Cash applied | Cash (100000) | A/R (120000) |
| Fees / interest | A/R (120000) | Fee Revenue (410000) |
| Capital credits | Retained Margin (390000) | Members' Capital (290000) |
The capital-credits entry is the coop-distinctive one: allocating capital credits moves margin
out of retained margin and into members' capital (an equity account on a coop balance sheet) —
it is not a revenue or cash event. If SUM(DR) ≠ SUM(CR) the program ROLLBACKs
the batch and DSPLYs OUT OF BALANCE ... ROLLED BACK; otherwise COMMIT and
IN BALANCE ... COMMITTED. The ILE COBOL AMGLRPT independently re-derives the
A/R target (bill total + fees, from a COMP-3 FD read + embedded SQL SUM) and prints
IN BALANCE/OUT OF BALANCE.
The source was written against a set of confirmed engine/transpile quirks, applied from the start rather than re-chased:
RUSAGE/BUSAGE not USAGE;
MSTAT vs SLSTAT). A field literally named USAGE also breaks
the RPG→COBOL transpile (COBOL reserved word); never used.CRTBNDRPG; DDS conditioning indicators use
exactly one space before Nxx; PRTF field-position digits end at DDS column 44; free-form uses nested
if not elseif where an elseif/END-IF mis-scope was
confirmed.CHARGE (energy + demand), the base for the capital-credits allocation.CCRETIRE and MCCBAL exist as running fields only.SLKVA kVA × DEMANDPRC),
assessed once per bill on tier 1, independent of kWh consumed. RES has none; COM is demand-billed.(MRLOVR - PRIORRD) + CURRRD + 1 (AMREADIM).SETLL *LOVAL / forward-READ for
delinquency, fees and the aged-AR report.