AMPCOOP/i — Electric Cooperative Billing

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

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.

Contents

A. Overview & Architecture ↑ top

A.1 What it does

AMPCOOP/i services the monthly billing cycle of a member-owned electric cooperative:

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.

A.2 Native-RPG architecture (no SQL-PL logic layer)

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:

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.

A.3 Component & flow

  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.

A.4 Object inventory

ObjectTypeRole
AMMEMBPFMember master (member-owner; MCCBAL running capital-credit balance).
AMSVCLOCPFService location (one metered point; SLBAL, contracted SLKVA).
AMMETERPFMeter master (dial MPRVRD, rollover modulus MRLOVR).
AMRATEPFTiered rate plan (UNITPRC per tier, BASECHG, DEMANDPRC).
AMREADPFMeter reads, arrival; rollover-safe RUSAGE (journaled).
AMBILLPFBill header (energy/demand/total charge, balance, status).
AMBLINPFPer-bill tier detail lines.
AMCASHPFCash receipts, arrival.
AMLEDGPFMember ledger / AR audit trail (journaled; UNIQUE-keyed append).
AMCCALCPFCapital-credits allocation, one row/member/year.
AMBLINLLFAMBLIN keyed by BILLNO (bill's tier lines).
AMAGELFLFAMBILL keyed by DUEDT (aging / delinquency scan).
AMGLFDB2 tableGL posting feed (SQL DDL via RUNSQLSTM).
AMACCTD / AMBILLD / AMMENUDDSPFMember inquiry / bill-inquiry subfile / operator menu.
AMBILLPPRTFPrinted member electric bill.
AMRATLDRPGLERate-plan seed loader.
AMMNTRPGLEMember + meter master maintenance (callable).
AMREADIMRPGLERollover-safe meter-read import.
AMBLGENRPGLETiered + demand billing engine.
AMCSHPSTRPGLECash posting (partial-pay aware).
AMDELINQRPGLEDelinquency / disconnect.
AMFEECALRPGLELate fee + interest accrual.
AMRECONRPGLEReconnect (paid-up disconnected locations).
AMCCALLOCRPGLECapital-credits (patronage) allocation.
AMGLPSTSQLRPGLEGL posting under commitment control (**FREE + embedded SQL).
AMBILLPRRPGLEBill print (PRTF spool).
AMAGERPGLEAged-AR report.
AMACCTIQ / AMBILLIQ / AMMENURPGLEMember inquiry / bill-inquiry subfile / menu.
AMGLRPTCBLLEILE COBOL GL cross-check (COMP-3 FD + embedded SQL).
AMSETUP / AMNIGHTCLPBuild-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.

B. Online Transactions & Screens ↑ top

B.1 The command/entry line

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 thisType on the command line
Open the operator menu (routes to the two inquiries)CALL AMPCOOP/AMMENU
Open member/account inquiry directlyCALL AMPCOOP/AMACCTIQ
Open the bill-inquiry subfile directlyCALL 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 stepSBMJOB 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).

B.2 The operator menu (AMMENU / AMMENUD)

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 *IN81CALL 'AMACCTIQ'; option 2 lights *IN82CALL 'AMBILLIQ'). It loops until F3.

AMPCOOP/i Main Menu 1. Member Inquiry 2. Bill Inquiry Option . . . . : _ F3=Exit Enter=Select
The menu uses nested 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.

B.3 Member inquiry (AMACCTIQ / AMACCTD)

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.

Member Inquiry - AMPCOOP/i Service location: SL000101 Member . . . . : JANET HOLLOWAY Rate class . . : RES Meter number . : MT000101 Status . . . . : A Balance . . . : 68.15 Cap. credits . : 13.84 Service location found. F3=Exit Enter=Inquire

Fields (ACCTINQ)

FieldType (DDS)Shows
ISLNO8A input/outputService-location number keyed by the operator.
DMEMB25A outputMember name (from AMMEMB via SLNO→MEMBNO).
DRATE3A outputRate class (RATECD).
DMETER8A outputMeter number (METERNO).
DSLSTAT1A outputService-location status (A active, D disconnected).
DBAL14A outputService-location balance (SLBAL, rendered).
DCCBAL14A outputMember's running capital-credit balance (MCCBAL).
DMSG50A outputResult/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.

B.4 Bill inquiry subfile (AMBILLIQ / AMBILLD)

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).

Bill Inquiry - AMPCOOP/i Bill number: B0005002 Svc location . : SL000102 Cycle . . . . : 202608 Usage . . . . : 350 Status . . . . : O Line Tier Usage Price Amount 1 1 350 0.0900 31.50 energy charge 56.50 demand charge 170.00 total charge 226.50 Bill found. F3=Exit Enter=Inquire

Subfile columns (BSFL) and header fields

FieldType (DDS)Shows
SLINENO2Y0 outputBill line number.
STIER2Y0 outputRate tier number for the line (LTIERNO).
SUSE9Y0 outputkWh billed in this tier band (TIERUSE).
SPRC14A outputPer-kWh tier price (TIERPRC).
SAMT14A outputTier line amount (LINEAMT).
IBILLNO8A input/outputBill number keyed by the operator.
DUSAGE / DBSTAT10A / 1A outputTotal metered usage / bill status.
DENERGY / DDEMAND / DCHG14A outputEnergy 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.

B.5 Master maintenance (AMMNT)

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:

pActionDoesBehaviour
CCCreate/change memberCHAIN 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).
MCCreate/change meterCHAIN AMMETER on METERNO; if found, UPDATE svc-loc + multiplier (METER CHANGED); else WRITE a new active meter (MRLOVR 99999, MPRVRD 0) (METER CREATED).
CIInquire memberCHAIN AMMEMB; returns name/city/state and MEMBER FOUND/NOT FOUND via OUT parms.
MIInquire meterCHAIN 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.

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

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:

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.

C. Batch Jobs & the Night Cycle ↑ top

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.

C.1 Full batch program set

ProgramPurposeReads / writesOutput (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.

C.2 Step-by-step detail

Meter-read import — AMREADIM

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

Billing — AMBLGEN

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

Cash posting — AMCSHPST

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

Delinquency / disconnect — AMDELINQ

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

Late fee + interest — AMFEECAL

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

Reconnect — AMRECON

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).

Capital-credits allocation — AMCCALLOC

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

GL post — AMGLPST

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

C.3 Ordering & dependencies

D. Data Files (data dictionary) ↑ top

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).

AMMEMB — Member master (PK MEMBNO)

FieldTypeMeaning
MEMBNO6AMember number (PK), e.g. M00001.
MNAME / MADDR25AMember name / street address.
MCITY / MSTATE / MZIP15A / 2A / 5ACity / state / ZIP.
MSTAT1AMember status: A active, D delinquent (disconnected).
MJOINDT8S 0Join date (drives capital-credits vintage/eligibility).
MCCBAL9P 2Running lifetime capital-credit balance (allocated up, retired down).

AMSVCLOC — Service location (PK SLNO; owned by MEMBNO)

FieldTypeMeaning
SLNO8AService-location number (PK), e.g. SL000101.
MEMBNO6AOwning member.
RATECD3ARate class (RES / COM) → AMRATE.
METERNO8AServing meter → AMMETER.
SLSTAT1AA active, D disconnected.
SLBAL9P 2Outstanding balance (raised by billing/fees, lowered by cash).
SLKVA5P 0Contracted / measured demand capacity (kVA) for the demand charge.

AMMETER — Meter master (PK METERNO)

FieldTypeMeaning
METERNO8AMeter number (PK).
MSLNO8AService location this meter serves.
MINSTDT8S 0Install date.
MMULT5P 2Dial multiplier (scaled-unit meters).
MPRVRD9P 0Meter's own last-recorded dial reading (rolled forward by AMREADIM).
MRLOVR9P 0Rollover modulus / dial capacity (e.g. 99999); 0 = no fixed dial wheel.
MSTAT1AMeter status (A active).

AMRATE — Tiered rate plan (PK RATECD, TIERNO)

FieldTypeMeaning
RATECD / TIERNO3A / 2S 0Rate class + tier number (PK).
TIERUB9P 0Tier upper bound (cumulative kWh); the top tier uses a sentinel-large bound to absorb the rest.
UNITPRC7P 4Per-kWh price for this tier's band.
BASECHG7P 2Fixed monthly service charge (carried on tier 1).
DEMANDPRC7P 4Per-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.

AMREAD — Meter reads, arrival (PK READID; journaled)

FieldTypeMeaning
READID8S 0Read identifier (PK).
SLNO / CYCLE8A / 6S 0Service location / billing cycle (YYYYMM).
PRIORRD / CURRRD9P 0Prior / current raw dial readings.
RUSAGE9P 0Already-computed, rollover-safe consumption for this read.
RROLLOVR1AY if a dial rollover was detected on this read.

AMBILL — Bill header (PK BILLNO)

FieldTypeMeaning
BILLNO8ABill number (PK); deterministic 'B' + last 7 of READID.
SLNO / CYCLE8A / 6S 0Service location / cycle.
BUSAGE9P 0Total metered usage billed (kWh).
ENERGYCHG / DEMANDCHG / CHARGE9P 2Energy charge / demand charge / total.
DUEDT8S 0Due date (cycle×100 + 20).
BSTAT1AO open, D delinquent, P paid.
BILLBAL9P 2Remaining balance on this bill.

AMBLIN — Bill tier detail lines (PK BILLNO, LINENO)

FieldTypeMeaning
BILLNO / LINENO8A / 2S 0Bill + line number (PK).
LTIERNO2S 0Rate tier billed on this line.
TIERUSE9P 0kWh billed in this tier band.
TIERPRC7P 4Per-kWh price applied.
LINEAMT9P 2Line amount (TIERUSE×TIERPRC).

AMCASH — Cash receipts, arrival (PK RCPTNO)

FieldTypeMeaning
RCPTNO8S 0Receipt number (PK).
SLNO / RCPTDT8A / 8S 0Service location paid / receipt date.
PAYAMT9P 2Amount received.

AMLEDG — Member ledger / AR audit trail (PK LEDGSEQ; journaled)

FieldTypeMeaning
LEDGSEQ8S 0Ledger sequence (PK, UNIQUE); each posting program uses a disjoint key band (F.4).
SLNO8AService location (blank for a member-level capital-credit row).
LTYPE1AB bill, P payment, R reconnect, F fee/interest, C capital credit, A delinquency adj.
LDT8S 0Posting date.
LAMT9P 2Amount posted.
LMEMO20AFree-text memo (also the AMFEECAL idempotency tag).

AMCCALC — Capital-credits allocation (PK CCYEAR, MEMBNO)

FieldTypeMeaning
CCYEAR / MEMBNO4S 0 / 6AAllocation year + member (PK).
CCPATRON9P 2Total energy+demand charges billed to the member in-year (patronage base).
CCALLOC9P 2Capital credit allocated (ALLOCPCT of patronage).
CCRETIRE9P 2Amount retired/paid out against this vintage (0 — retirement not modelled).

AMGLF — GL posting feed (DB2 table, SQL DDL)

FieldTypeMeaning
BATCHIDCHAR(8)Posting batch id (e.g. GLB00001).
ACCTCHAR(6)GL account (120000 A/R, 400000 Revenue, 100000 Cash, 410000 Fee Revenue, 390000 Retained Margin, 290000 Members' Capital).
DRCRCHAR(1)D debit / C credit.
AMTDECIMAL(11,2)Posting amount.
SRCPGMCHAR(8)Source program (AMGLPST).

Logical files

FileOverKeyUsed by
AMBLINLAMBLINBILLNOBill print / bill-inquiry subfile (the bill's tier lines).
AMAGELFAMBILLDUEDTAging / delinquency / fee scans (SETLL *LOVAL forward READ by due date).

Relationships

E. Operations Runbook ↑ top

E.1 Build & setup

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.

  1. CRTLIB LIB(AMPCOOP), then seed the source members (harness seedAmpcoop()).
  2. CRTCLPGM PGM(AMPCOOP/AMSETUP) SRCFILE(AMPCOOP/QCLSRC) SRCMBR(AMSETUP) (and AMNIGHT).
  3. 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.

E.2 Night-in-the-life

  1. Import the cycle's meter reads. Drive 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.
  2. Create the job queue once: CRTJOBQ JOBQ(AMPCOOP/NITEQ).
  3. Submit the night chain in order (C above): AMBLGEN → AMCSHPST → AMDELINQ → AMFEECAL → AMCCALLOC → AMGLPST → AMBILLPR — or CALL AMPCOOP/AMNIGHT. Wait for each job to reach OUTQ before relying on its output.
  4. Post-check each step's DSPLY (counts/totals) against expectations (C.2).
  5. Verify the GL is balanced:
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):

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).

E.3 Failure & re-run rules

Each program DSPLYs its result counts. The night chain is designed so most steps are safe to re-run:

SituationBehaviourAction
Re-run AMBLGENDeterministic BILLNO + CHAIN(EN) skips already-billed reads.Idempotent. Re-submit safely; SKIPPED= rises, GENERATED=0, no duplicate bills.
Re-run AMFEECAL same dayLedger 'F' memo-tag scan finds the prior fee.Idempotent. ASSESSED=0; no double fee.
Re-run AMCCALLOC same yearCHAIN(EN) on CCYEAR+MEMBNO skips allocated members.Idempotent. SKIPPED= rises, no double allocation.
Re-run AMDELINQ same dayLEDGSEQ 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 balanceProgram ROLLBACKs the whole batch (commitment control).No partial GL rows persist. Investigate source data, fix, re-run.
AMREADIM on unknown meterMETER NOT FOUND, no AMREAD row.Create the meter (AMMNT 'MC') then re-import.
Cash for a service location with no open billNOBILL= increments; receipt not applied.Confirm a bill exists / was generated; re-post after billing.
Overlapping batch opensA bare arrival PF opened OUTPUT truncates on OPEN.Run steps sequentially on the *JOBQ (wait for OUTQ) — never two output-file writers at once.
Because every money movement is journaled to AMLEDG (typed row + memo) and AMREAD/AMLEDG are under STRJRNPF IMAGES(*BOTH), any night's effect is fully reconstructable after the fact via DSPJRN for reconciliation and recovery.

F. Developer Reference ↑ top

The business logic lives in the RPG. All objects are in library AMPCOOP; source in electric-app/src/sources.mjs.

F.1 The billing math (AMBLGEN) — two-phase, torn-write-safe

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.

F.2 Rollover-safe consumption (AMREADIM)

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.

F.3 Capital credits (AMCCALLOC) — the coop-distinctive workflow

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.

F.4 The ledger & disjoint key bands (AMLEDG)

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:

ProgramLTYPELEDGSEQ band
AMBLGENB (bill)10000000 + READID
AMCSHPSTP (payment)20000000 + RCPTNO
AMRECONR (reconnect)60000000 + seq
AMFEECALF (fee/interest)70000000 + seq
AMCCALLOCC (capital credit)80000000 + seq
AMDELINQA (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.

F.5 GL posting & balance (AMGLPST)

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:

EventDebitCredit
BillingA/R (120000)Revenue (400000)
Cash appliedCash (100000)A/R (120000)
Fees / interestA/R (120000)Fee Revenue (410000)
Capital creditsRetained 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.

F.6 Platform quirks handled (from the source header)

The source was written against a set of confirmed engine/transpile quirks, applied from the start rather than re-chased:

Distinct field names across files
Same-named fields across externally-described files share one storage slot — so every record format uses distinct names (RUSAGE/BUSAGE not USAGE; MSTAT vs SLSTAT). A field literally named USAGE also breaks the RPG→COBOL transpile (COBOL reserved word); never used.
Base PF before its LF in F-specs
When UPDATEing a shared format, declare the base PF before its LF; and after a PF-side key UPDATE, re-CHAIN the base PF rather than re-reading through a keyed LF the same run.
UNIQUE-keyed output PFs for append tables
Arrival-style tables that must append across runs (AMLEDG) are UNIQUE-keyed so the keyed WRITE path appends rather than the bare-PF OPEN OUTPUT truncating them (F.4).
Silent decimal overflow
The compute-then-validate-before-write two-phase pattern (F.1) guards the confirmed silent decimal-overflow-on-ADD quirk with wide (11P2+) accumulators.
Full-key CHAIN over partial-key SETLL/READE on unique files
Rate-tier and bill lookups use bounded full composite-key CHAIN loops, not partial-key positioned reads.
CRTBNDRPG, DDS conditioning, PRTF columns
Plain and embedded-SQL RPG compile with 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.

G. Glossary ↑ top

Capital credits (patronage capital)
A cooperative's member-owned equity: a share of annual margin allocated back to each member in proportion to their patronage (billed charges), credited to a running capital account (MCCBAL) rather than paid in cash. AMCCALLOC allocates 8% of each member's in-year patronage.
Patronage
The business a member did with the coop in the year — here their total original billed CHARGE (energy + demand), the base for the capital-credits allocation.
Retirement (of capital credits)
A later, board-declared cash payout of a prior allocation vintage. AMPCOOP/i does not model retirement; CCRETIRE and MCCBAL exist as running fields only.
Demand charge
A charge on contracted/measured capacity (SLKVA kVA × DEMANDPRC), assessed once per bill on tier 1, independent of kWh consumed. RES has none; COM is demand-billed.
Tiered (stepped) rate
A rate that prices successive bands of kWh differently (AMRATE tiers). The top tier carries a sentinel-large upper bound so it absorbs all remaining usage.
Rollover-safe consumption
Consumption computed so a meter dial that wrapped past its modulus (e.g. 99999→00000) mid-cycle still yields a correct positive kWh: (MRLOVR - PRIORRD) + CURRRD + 1 (AMREADIM).
Service location
One metered point of delivery for a member (AMSVCLOC) — the unit that is billed, carries a balance, and is connected/disconnected. A member may own several.
Member (member-owner)
A cooperative's customer is a member-owner (AMMEMB), not a "customer" — the ownership is what makes capital credits meaningful.
Delinquency / disconnect / reconnect
Past-due bills flag the bill, service location and member delinquent and disconnect the location (AMDELINQ); once the balance is cleared the location (and member, if no other location is delinquent) is reconnected (AMRECON).
Member ledger (AMLEDG)
The typed AR audit trail — one row per money movement (B/P/R/F/C/A), UNIQUE-keyed so it appends, journaled for recovery.
Aging LF (AMAGELF)
A logical file over AMBILL keyed by due date, scanned SETLL *LOVAL / forward-READ for delinquency, fees and the aged-AR report.
Two-phase (compute-then-persist)
Computing and validating a whole bill in wide working storage before writing any row, to guard the platform's silent-decimal-overflow quirk against a torn write (AMBLGEN).
Disjoint key band
The non-overlapping LEDGSEQ ranges each posting program writes into, so appends never collide (F.4).
Commitment control
DB2 transaction boundary. AMGLPST posts the GL under commit(*yes) and only COMMITs a balanced batch (DR = CR), otherwise ROLLBACKs.
Subfile
A 5250 display construct listing many rows on one screen (DDS SFL/SFLCTL). AMBILLIQ's tier-line list is the app's one subfile.
SBMJOB / *JOBQ
Submit Job to a job queue — the IBM i idiom for the sequenced night chain, so each step closes its files before the next opens them.
SQLRPGLE / **FREE / EXEC SQL
Totally free-form RPG with embedded SQL. Here only AMGLPST (GL posting) and the COBOL AMGLRPT use embedded SQL; all other logic is native RPG record-level I/O.