LINKS/i runs the operations and member billing of a golf course / country club:
membership classes and recurring dues, tee-time booking and played rounds (green fees with a per-class
discount), pro-shop inventory and sales, F&B chits with per-class minimum-spend tracking, cart rentals,
member payments, and a chargeable-to-account member ledger (the receivables book) that ages into late
fees and closes into a balanced GL feed. Every business rule lives in RPG (with one ILE COBOL control
report and one SQLRPGLE GL close); the three CL job cycles — GCDAILY, GCMONTH,
GCPERIOD — chain the programs. This manual is the reference for the operator who runs the
online screens and the periodic batch cycles, and for the developer maintaining the application. It is
grounded entirely in the committed source (golf-app/src/sources.mjs, src/seed.mjs,
and the test/gc_*.mjs drivers).
LINKS/i runs a club's member-facing operations and the receivables billing behind them:
The application is organised around one central book: GCMEMB.MBAL, the live
chargeable-to-account member balance, mirrored by the GCLEDG member ledger (one immutable row
per posting). The invariant every test asserts is that MBAL moves only by postings
(up) and payments (down), and always reconstructs exactly as SUM(GCLEDG.LEDAMT) over the
member's own rows. Charges raise the balance and write a positive ledger row; a receipt lowers it and
writes a negative one.
G), pro-shop
sale (P), F&B chit (F), cart rental (C), dues
(D), minimum-spend shortfall (M), late fee (L) and receipt
(R) all become GCLEDG rows, keyed (MEMNO, LEDSEQ) so a
member's entries sit together for a keyed partial read.GCARRPT control report
and the GCGLPST GL close.MBAL by exactly the
amount received.Everything runs in library LINKS. There is no SQL-PL layer here (unlike LOANSVC/i): the
logic is hand-written RPG over keyed physical files, with a single SQLRPGLE program (GCGLPST)
for the GL feed and one ILE COBOL program (GCARRPT) for the control report.
ONLINE BATCH (three job cycles)
------ ------------------------
GCMENU (5250) GCDAILY --> GCRNDPST (green fee/cart/guest -> ledger 'G')
opt 1 -> GCMEMBIQ GCSALEPST (pro-shop sale, relieve stock -> 'P')
opt 2 -> GCLEDIQ GCCHITPST (F&B chit -> 'F', accumulate MFBSPND)
(subfile over GCLEDG) GCCARTPST (standalone cart rental -> 'C')
GCRCPTPST (receipt -> 'R', negative amount)
GCMONTH --> GCDUES (recurring dues -> 'D')
GCMINCHK (F&B min-spend shortfall -> 'M', reset MFBSPND)
GCAGE (AR ageing report, PRTF)
GCPERIOD --> GCLATE (ageing -> late fee -> 'L')
GCGLPST (balanced GL close -> GCGLDIST)
GCARRPT (COBOL member-ledger control report)
\ /
v v
GCMEMB (MBAL: the live member balance) <-- moved ONLY by postings / payments
|
+--> GCLEDG (one immutable row per posting; balance = sum(LEDAMT))
+--> GCROUND / GCITEM / GCGUEST ... (per-stream durable records)
A single event — say a played round — flows: GCRNDPST reads the played
(TSTAT='P') tee-time, prices the green fee against the member's class discount, sums cart and
guest fees, writes a GCROUND row, flips the tee-time to 'G', raises
GCMEMB.MBAL and writes one GCLEDG 'G' row. Every posting program derives its
ledger sequence number from stable source digits so a re-run lands on the identical key and the
CHAIN(EN) guard refuses it — each cycle is idempotent by construction.
| Object | Type | Role |
|---|---|---|
| GCCLASS | PF | Membership-class tariff (dues / min-spend / green-fee discount). |
| GCMEMB | PF | Member master — the live ledger balance MBAL. |
| GCGUEST | PF | Guest register (sponsored guests, full-rate fee). |
| GCTEE | PF | Tee-time bookings (lifecycle B/P/X/N/G). |
| GCROUND | PF | Played-round history (one per posted green fee). |
| GCITEM | PF | Pro-shop inventory master (ONHAND). |
| GCSALE | PF | Pro-shop sales. |
| GCCHIT | PF | F&B chits. |
| GCCART | PF | Standalone cart rentals. |
| GCLEDG | PF | Member ledger (the receivables book). |
| GCLEDLF | LF | Ledger alternate access path, keyed by LEDTYPE. |
| GCRCPT | PF | Member payments / receipts. |
| GCGLDIST | SQL table | GL distribution feed (+ index GCGLDACC). |
| GCMEMBD / GCLEDD / GCMENUD | DSPF | Member inquiry / ledger subfile / menu display files. |
| GCAGEP | PRTF | AR ageing printer file. |
| GCREFLD | RPGLE | Seed reference & transactional data. |
| GCRNDPST / GCSALEPST / GCCHITPST / GCCARTPST / GCRCPTPST | RPGLE | Daily posting programs. |
| GCDUES / GCMINCHK / GCAGE | RPGLE | Monthly billing / ageing programs. |
| GCLATE | RPGLE | Period ageing → late fee. |
| GCGLPST | SQLRPGLE | Period balanced GL close. |
| GCARRPT | ILE COBOL | Member-ledger control report. |
| GCMEMBIQ / GCLEDIQ / GCMENU | RPGLE | Interactive inquiry & menu programs. |
| GCSETUP / GCDAILY / GCMONTH / GCPERIOD | CLP | Build + the three job cycles. |
The full catalogue is 11 PFs + 1 LF + 1 SQL table (with index), 3 DSPFs and 1 PRTF, driven by 14 RPG programs, 1 ILE COBOL program and 4 CL programs. Sections D and F expand each.
LINKS/i has no CICS transaction identifiers and no transid switch. On IBM i, each program is
reached by name from a 5250 command-entry line (or a JOBQ/scheduler for the batch cycles). The
operator equivalent of "type a transid and Enter" is "type a CALL command and Enter". Before
invoking anything, the job's library list must include LINKS — the tested jobs run with
LIBL = QSYS QGPL LINKS QTEMP and CURLIB = LINKS.
| To do this | Type on the command line |
|---|---|
| Open the operator menu | CALL LINKS/GCMENU |
| Member inquiry directly | CALL LINKS/GCMEMBIQ |
| Ledger subfile inquiry directly | CALL LINKS/GCLEDIQ |
| Run the daily posting cycle | CALL LINKS/GCDAILY (or SBMJOB it) |
| Run the monthly dues/min-spend cycle | CALL LINKS/GCMONTH |
| Run the period ageing/GL cycle | CALL LINKS/GCPERIOD |
| Build/rebuild all objects | CALL LINKS/GCSETUP then CALL LINKS/GCREFLD |
The batch programs take no CALL parameters. There is no LOANSVC-style single control row; instead
each program reads its processing/billing date and period from hard-coded working-storage defaults
in the source (e.g. GCDUES.WDT INZ(20260901), GCDUES.WPERIOD INZ(9),
GCLATE.WRUNDT INZ(20260930)). A scheduled submission is therefore a bare CALL; to
process a different date/period the source constant is changed and the program recompiled. This is honest
to the source — the app models the business rules, not a production date-parameter surface.
GCMENU is the operator entry point: a plain menu (GCMENUD record
GCMENU) offering 1. Member Inquiry and 2. Ledger Inquiry. It program-to-program
CALLs the chosen inquiry, redisplays on return, reports Invalid option for anything
else, and exits on F3.
GCMEMBIQ is a plain (non-subfile) inquiry over GCMEMBD record
MEMINQ. The operator keys a member number; the program CHAINs
GCMEMB then GCCLASS and renders the master record plus the class tariff and the
live F&B-spend figure. An unknown member shows Member not found. with the detail cleared.
| Field | Type (DDS) | Shows |
|---|---|---|
| IMEMB | 6A input/output | Member number to inquire. |
| DNAME | 30A output | Member name (MNAME). |
| DCLASS | 2A output | Membership class (MCLASS). |
| DDUES | 14A output | Class monthly dues (MCDUES, edited). |
| DMINSP | 14A output | Class F&B minimum (MCMINSP, edited). |
| DFBSP | 14A output | Period F&B spend (MFBSPND, edited). |
| DBAL | 14A output | Live ledger balance (MBAL, edited). |
| DHOLD | 1A output | Manual-hold flag (MHOLD). |
| DMSG | 50A output | Result message (Member found / not found). |
GCLEDIQ is the app's subfile screen. It presents a member header plus a subfile
(DDS record LSFL under control record LCTL, SFLPAG(5) per page,
SFLSIZ(0050), ROLLUP/ROLLDOWN) of that member's ledger entries. The
operator keys a member number; the program clears the subfile, CHAINs GCMEMB for
the header, then does a keyed partial read of GCLEDG (SETLL +
READE on MEMNO) loading one subfile row per ledger entry, and redisplays.
| Field | Type (DDS) | Shows |
|---|---|---|
| SSEQ | 8Y,0 output | Ledger sequence (LEDSEQ). |
| SDT | 10A output | Posting date (LEDDT). |
| STYPE | 1A output | Ledger type (LEDTYPE): D/G/P/F/C/M/L/R. |
| SAMT | 12A output | Signed amount (LEDAMT, edited). |
| SREF | 8A output | Source reference (LEDREF). |
| SDESC | 25A output | Description (LEDDESC). |
CA03).Both inquiry screens are strictly read-only: they display the member master, class tariff and ledger. No balance or ledger row is edited from the screen — every money movement happens through the batch posting programs, so the ledger stays an immutable audit trail.
GCLEDIQ reloads for a member
with zero ledger rows, the previously-displayed subfile rows can still render (the header
name/balance updates correctly to the new member's own 0.00 figures). This is a re-confirmation of the
same already-documented ORDERIS/i behaviour, not a new finding: the DDS parser reads a two-digit
conditioning indicator (N31/31 on SFLDSP/SFLDSPCTL/SFLCLR)
as its first digit only, so the intended "clear then don't display" split collapses to one indicator, and
the renderer recomposes the last-displayed detail area on a genuinely empty pass. It is
cosmetic/interactive-only with no data-integrity impact; test/gc_interactive.mjs asserts only
the correct parts (the header update) and documents the stale-subfile behaviour inline rather than masking
it. This is the same NA-05/NA-07 estate area recorded in golf-app/FINDINGS.md.Honest statement: LINKS/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 booking that is marked played, a chit, a sale, a receipt are all posted by the next batch cycle with no interactive approval gate. The manual documents the control model the application does have:
SUM(LEDAMT). The
tests assert this reconstruction for every member after every cycle.GCRNDPST processes only TSTAT='P' (played) tee-times; the posting programs
process only 'O' (open/unposted) sales/chits/carts and 'U' (unapplied)
receipts, flipping each to its posted status so it cannot be picked up twice.MHOLD='Y' parks a member
for new privileges but does not stop dues, min-spend or postings — a held member still
owes. The tests verify M00004 (held) is still billed and never has its status changed by a cycle.ONHAND falls by exactly what a sale ships
(floored at zero), and a receipt lowers MBAL by exactly the amount received — the
two invariants the daily suite checks.CHAIN(EN) guard refuses a duplicate, so re-running a cycle never double-posts (see F.3).In sum, the control posture is immutable-ledger audit + record-lifecycle/scope gating + conservation invariants + derived-key idempotency, rather than a segregation-of-duties approval workflow.
LINKS/i's processing runs as three CL-driven cycles rather than one monolithic nightly job: a
daily posting cycle (GCDAILY), a monthly billing cycle (GCMONTH),
and a period ageing/close cycle (GCPERIOD). Each CL ADDLIBLE LIB(LINKS),
chains its member programs by bare CALL, and ends with a completion banner. All programs take
no CALL parameters; dates/periods are working-storage constants in the source (B.1).
-- the three cycles are parameterless SBMJOBs:
SBMJOB CMD(CALL PGM(LINKS/GCDAILY)) JOB(GCDAILY)
SBMJOB CMD(CALL PGM(LINKS/GCMONTH)) JOB(GCMONTH)
SBMJOB CMD(CALL PGM(LINKS/GCPERIOD)) JOB(GCPERIOD)
| Program | Cycle | Purpose | Reads / writes | DSPLY line |
|---|---|---|---|---|
| GCREFLD | setup | Seed classes, members, pro-shop items (build only). | writes GCCLASS/GCMEMB/GCITEM. | GCREFLD CLASS=4 MEMB=4 ITEM=4 |
| GCRNDPST | daily | Post green fee/cart/guest for played tee-times. | GCTEE→'G', GCROUND, GCGUEST→'P', GCMEMB, GCLEDG 'G'. | GCRNDPST POSTED=n SKIP=n |
| GCSALEPST | daily | Post pro-shop sales, relieve inventory. | GCSALE→'P', GCITEM ONHAND, GCMEMB, GCLEDG 'P'. | GCSALEPST POSTED=n SKIP=n |
| GCCHITPST | daily | Post F&B chits, accumulate MFBSPND. | GCCHIT→'P', GCMEMB (MBAL+MFBSPND), GCLEDG 'F'. | GCCHITPST POSTED=n SKIP=n |
| GCCARTPST | daily | Post standalone cart rentals. | GCCART→'P', GCMEMB, GCLEDG 'C'. | GCCARTPST POSTED=n SKIP=n |
| GCRCPTPST | daily | Apply member payments (lower balance). | GCRCPT→'A', GCMEMB (MBAL−), GCLEDG 'R' (negative). | GCRCPTPST APPLIED=n SKIP=n |
| GCDUES | monthly | Bill recurring class dues to active members. | GCMEMB, GCLEDG 'D'. | GCDUES BILLED=n SKIP=n |
| GCMINCHK | monthly | Charge F&B minimum-spend shortfall; reset MFBSPND. | GCMEMB (MBAL, MFBSPND→0), GCLEDG 'M'. | GCMINCHK CHARGED=n MET=n SKIP=n |
| GCAGE | monthly | Print AR ageing report (buckets by days since last pay). | reads GCMEMB/GCLEDG, writes GCAGEP. | GCAGE MEMBERS=n TOTAL=n |
| GCLATE | period | Age each account; charge a flat late fee past grace. | GCMEMB, GCLEDG 'L'. | GCLATE CHARGED=n OK=n SKIP=n |
| GCGLPST | period | Post a balanced GL feed of the ledger movement. | reads GCLEDG, writes GCGLDIST (SQL). | GCGLPST BATCH=n ROWS=n |
| GCARRPT | period | ILE COBOL member-ledger control report. | reads GCMEMB. | GCARRPT OWED/MEMBERS/OUTSTND/ONHOLD |
The CL chains, in order: GCRNDPST → GCSALEPST →
GCCHITPST → GCCARTPST → GCRCPTPST. Green fees price the
standard 45.00 fee against the member's class discount, add the standard 20.00 cart charge
(if CARTYN='Y') and every sponsored guest's flat 45.00 fee (never discounted), write a
GCROUND row and one GCLEDG 'G' row. Pro-shop sales relieve ONHAND by
exactly the quantity sold (floored at zero). Chits post and add to MFBSPND. Receipts post as a
negative amount and lower MBAL.
Expected DSPLY (gc_daily oracle: 3 played tee-times, 1 sale, 2 chits, 1 cart, 1 receipt): GCRNDPST POSTED=3 SKIP=1 the 4th tee-time is BOOKED, not played -> skipped GCSALEPST POSTED=1 SKIP=0 GCCHITPST POSTED=2 SKIP=0 GCCARTPST POSTED=1 SKIP=0 GCRCPTPST APPLIED=1 SKIP=0 Resulting member balances: M00001=201.00 (65 round + 36 shop + 200 chit - 100 receipt), M00002=121.00 (81 round + 40 chit), M00003=62.50 (42.50 round + 20 cart), M00004=0.00. Pro-shop glove on-hand 40 -> 38. Every member's balance = sum of their own ledger rows.
The CL chains GCDUES → GCMINCHK → GCAGE.
GCDUES bills every MSTAT='A' member their class dues (a manual hold does
not skip billing). GCMINCHK compares each member's accumulated MFBSPND
against the class minimum: a member below the minimum is charged the shortfall (ledger 'M', a
positive amount); one who met it gets a zero-amount 'M' marker row (so the idempotency
guard has something to find on a re-run); either way MFBSPND resets to zero for the next
period. GCAGE prints the AR ageing report.
Expected DSPLY (gc_monthly oracle: FL/SR/JR/FL-held members; M00002 pre-set MFBSPND=80): GCDUES BILLED=4 SKIP=0 GCMINCHK CHARGED=3 MET=1 SKIP=0 M00002 met 75.00 minimum (spent 80) -> no charge GCAGE MEMBERS=4 TOTAL=... Balances: M00001=400.00 (250 dues + 150 shortfall), M00002=150.00 (dues only, met), M00003=85.00 (60 + 25), M00004=400.00 (250 + 150; held still billed). MFBSPND all reset to 0.
GCMINCHK writes an 'M' row even
when the minimum was met (amount 0.00). Without that durable row, a re-run would find no idempotency guard,
re-read MFBSPND (already reset to zero by the first run) and wrongly charge the full
minimum a second time to every member who originally met it. The zero-amount marker is the guard.The CL chains GCLATE → GCGLPST → GCARRPT.
GCLATE ages each member from their last payment (the most recent 'R'
ledger row; a member with no payment history is judged from their join date), on a 30/360 convention with a
30-day grace window, and charges a flat 25.00 late fee (ledger 'L') only while
MBAL > 0. GCGLPST posts a balanced two-leg GL close (F.4). GCARRPT
prints the COBOL control report.
Expected DSPLY (gc_period oracle, run date 20260930): GCLATE CHARGED=2 OK=1 M00002 (ancient join) + M00004 (89d) late; M00001 (29d) within grace; M00003 zero-bal skipped GCGLPST BATCH=202609 ROWS=... GCARRPT OWED 190.00 / MEMBERS 4.00 / OUTSTND 3.00 / ONHOLD 1.00 GL: DR total = CR total = 270.00 (230.00 charges leg + 40.00 receipts leg).
MFBSPND, which the monthly
GCMINCHK reads to compute the shortfall — run the month's dailies first or the
minimum-spend charge is over-assessed against an empty tracker.GCLATE ages off the
posted ledger and GCGLPST nets the ledger movement into the GL, so the ledger must be
fully posted first. Within PERIOD: late fees, then GL close (so the fees are in the movement), then
the control report.All files are in library LINKS, grounded in the DDS/SQL of golf-app/src/sources.mjs.
Dates are stored as signed 8S,0 in YYYYMMDD form; money is packed decimal
(9P,2 or 11P,2); the green-fee discount is a packed percentage (5P,2,
e.g. 20.00 = 20%).
| Field | Type | Meaning |
|---|---|---|
| MCCLASS | 2A | Class code (PK): FL / SR / JR / SO. |
| MCDESC | 20A | Class description. |
| MCDUES | 9P,2 | Monthly dues for the class. |
| MCMINSP | 9P,2 | Period F&B minimum spend. |
| MCGFDISC | 5P,2 | Green-fee discount percentage. |
| MCSTAT | 1A | Class status (A active). |
Seeded classes: FL 250.00/150.00/0%, SR 150.00/75.00/20%, JR 60.00/25.00/50%, SO 40.00/50.00/0%.
| Field | Type | Meaning |
|---|---|---|
| MEMNO | 6A | Member number (PK), e.g. M00001. |
| MNAME | 30A | Member name. |
| MCLASS | 2A | Membership class (→ GCCLASS). |
| MJOINDT | 8S,0 | Join date (YYYYMMDD); the ageing fallback for a member with no payment history. |
| MBAL | 11P,2 | Live ledger balance — moved ONLY by postings (up) and payments (down). |
| MFBSPND | 9P,2 | Period F&B-spend accumulator; read then reset by GCMINCHK. |
| MSTAT | 1A | Member status (A active). |
| MHOLD | 1A | Manual hold (Y). Blocks new privileges only, not billing. |
Seeded members: M00001 HARRIET NORTHGATE (FL), M00002 DEREK PENNYFEATHER (SR), M00003 CASSIE OAKMOOR (JR), M00004 REGINALD ASHCOMBE (FL, MHOLD=Y). All start MBAL=0, MFBSPND=0.
| Field | Type | Meaning |
|---|---|---|
| MEMNO / GSTNO | 6A / 8A | Sponsoring member + guest number (composite key). |
| GSTNAME | 30A | Guest name. |
| GSTDT | 8S,0 | Play date. |
| GSTFEE | 9P,2 | Guest green fee (stamped at the flat rate when the round posts). |
| GSTSTAT | 1A | O open, P posted (charged to sponsor's round). |
| Field | Type | Meaning |
|---|---|---|
| TEENO | 8A | Tee-time number (PK). |
| TEEDT / TEETIME | 8S,0 / 4S,0 | Date (YYYYMMDD) / time (HHMM). |
| MEMNO | 6A | Booking member. |
| PLAYERS / GUESTS | 2S,0 / 2S,0 | Player and guest headcounts. |
| CARTYN | 1A | Y requests a cart at booking. |
| CARTAMT | 9P,2 | Cart charge, stamped when the round posts. |
| TSTAT | 1A | B booked, P played, X cancelled, N no-show, G green-fee posted. |
| GFAMT | 9P,2 | Green fee, stamped when the round posts. |
| Field | Type | Meaning |
|---|---|---|
| RNDNO | 8A | Round number (PK), derived from the tee-time (RN0 + digits). |
| MEMNO | 6A | Member who played. |
| RNDDT / TEENO | 8S,0 / 8A | Round date / source tee-time. |
| GFAMT / CARTAMT | 9P,2 / 9P,2 | Green fee / cart charge on this round. |
| RNDSTAT | 1A | Round status (P posted). |
| Field | Type | Meaning |
|---|---|---|
| ITEMNO | 8A | Item number (PK). |
| IDESC | 30A | Item description. |
| IPRICE | 9P,2 | Unit price. |
| ONHAND | 9P,2 | On-hand quantity; the conservation invariant (falls only by what ships). |
| ISTAT | 1A | Item status (A active). |
Seeded items: PS100001 glove 18.00/40, PS100002 balls 32.50/60, PS100003 head-cover set 45.00/3 (critically low), PS100004 polo 55.00/25.
| Field | Type | Meaning |
|---|---|---|
| SALENO | 8A | Sale number (PK). |
| MEMNO | 6A | Member (blank = walk-in, out of scope for billing). |
| ITEMNO / SQTY | 8A / 7P,2 | Item sold / quantity. |
| SAMT | 9P,2 | Sale amount posted to the ledger. |
| SALEDT / SSTAT | 8S,0 / 1A | Sale date / status (O open, P posted). |
| Field | Type | Meaning |
|---|---|---|
| CHITNO | 8A | Chit number (PK). |
| MEMNO / CHITDT | 6A / 8S,0 | Member / date. |
| CHITAMT | 9P,2 | Chit amount; accumulates into MFBSPND on posting. |
| CDESC / CSTAT | 20A / 1A | Description / status (O open, P posted). |
| Field | Type | Meaning |
|---|---|---|
| CARTNO | 8A | Rental number (PK). |
| MEMNO / CARTDT | 6A / 8S,0 | Member / date. |
| CARTAMT | 9P,2 | Rental amount posted to the ledger. |
| CSTAT | 1A | Status (O open, P posted). |
This is the genuinely separate rental stream (e.g. a non-golf outing); tee-time-attached cart charges live on GCTEE/GCROUND instead.
| Field | Type | Meaning |
|---|---|---|
| MEMNO / LEDSEQ | 6A / 8S,0 | Member + derived sequence (composite key). |
| LEDDT | 8S,0 | Posting date (YYYYMMDD). |
| LEDTYPE | 1A | D dues, G green fee, P pro-shop, F F&B, C cart, M min-spend, L late fee, R receipt. |
| LEDAMT | 11P,2 | Signed amount (charges positive, receipts negative). |
| LEDREF / LEDDESC | 8A / 25A | Source reference / description. |
Never updated once written; a member's balance is always SUM(LEDAMT) over
their own rows.
PFILE(GCLEDG) alternate access path leading on LEDTYPE — the
"everything of this kind this period" access the GL close wants without a scan. Same record format, a
different leading key from the base PF (which leads on MEMNO).
| Field | Type | Meaning |
|---|---|---|
| RCPTNO | 8A | Receipt number (PK). |
| MEMNO / RCPTDT | 6A / 8S,0 | Member / date. |
| RCPTAMT | 11P,2 | Amount received; applied in full (balance may run credit). |
| RSTAT | 1A | U unposted, A applied. |
| Field | Type | Meaning |
|---|---|---|
| GLSEQ | DECIMAL(8,0) | GL sequence (PK). |
| GLBATCH | DECIMAL(6,0) | Batch (period, e.g. 202609). |
| ACCT | CHAR(9) | GL account (see F.4). |
| DRCR | CHAR(1) | D debit / C credit. |
| AMT | DECIMAL(11,2) | Posting amount. |
| GLREF / GLDT | CHAR(8) / DECIMAL(8,0) | Reference / date. |
Index GCGLDACC on (ACCT, DRCR) supports the "already posted per
account" net-off query the close runs to stay reconcilable across repeated period closes.
LINKS (LIBL = QSYS QGPL LINKS QTEMP, CURLIB = LINKS).TSTAT='P', guests with GSTSTAT='O', open sales/chits/carts ('O'), unapplied receipts ('U').SBMJOB CMD(CALL PGM(LINKS/GCDAILY)).CALL LINKS/GCMENU (option 1 member, option 2 ledger).Post-checks after the daily cycle:
GCRNDPST POSTED= equals the played tee-times; SKIP= counts booked/cancelled/already-posted ones. Each posted tee-time is now TSTAT='G' with a GCROUND row.GCSALEPST: ONHAND fell by exactly the quantity sold; no walk-in (blank-member) sale posted.GCCHITPST: each member's MFBSPND rose by their chit total (the figure GCMINCHK will read).GCRCPTPST: each receipt is now RSTAT='A' and posted a negative 'R' ledger row; MBAL fell by exactly the amount received.MBAL equals the sum of their own GCLEDG rows.GCDAILY has run for the month (so MFBSPND is complete).SBMJOB CMD(CALL PGM(LINKS/GCMONTH)). Confirm GCDUES BILLED=, GCMINCHK CHARGED=/MET=, and the GCAGE report. Verify every member's MFBSPND reset to 0.00.SBMJOB CMD(CALL PGM(LINKS/GCPERIOD)). Confirm GCLATE CHARGED=/OK=, GCGLPST BATCH=/ROWS=, and the GCARRPT control lines.SELECT DRCR, SUM(AMT) FROM LINKS.GCGLDIST GROUP BY DRCR;
-- total debits must equal total credits (the balanced-legs invariant)
SELECT ACCT, DRCR, SUM(AMT) FROM LINKS.GCGLDIST GROUP BY ACCT, DRCR ORDER BY ACCT;
Reconciling figures (the same ones the period volume test checks against a hand-derived oracle):
SUM(AMT WHERE DRCR='D') = SUM(AMT WHERE DRCR='C'), always, because the charges leg and receipts leg each balance independently.1300-MBAR = the gross of every new charge type; matched krona-for-krona by the per-type revenue CR lines (Dues/Green-fee/Pro-shop/F&B/Cart/Min-spend/Late-fee).1000-CASH = cash received this period; matched by CR 1300-MBAR.GCARRPT OWED = Σ MBAL over members with a positive balance; OUTSTND = that member count; ONHOLD = held-member count.Each program DSPLYs a POSTED/BILLED/CHARGED/APPLIED count and a SKIP count. A
healthy re-run shows the work count at 0 (everything already posted) and the skip count carrying the
already-done rows — the derived-key idempotency guard (F.3) makes every cycle safe to re-submit.
| Situation | Behaviour | Action |
|---|---|---|
| Daily cycle fails partway | Posted rows are flipped to their posted status and have a ledger row; unposted ones do not. | Re-submit GCDAILY: already-posted rows are refused by the CHAIN(EN) guard, the rest catch up. Idempotent. |
| Re-run the whole daily chain | Every posting reports POSTED=0; balances unchanged. | Safe no-op — the daily suite asserts M00001 stays 201.00 and no duplicate ledger rows appear. |
| Re-run GCDUES same period | BILLED=0; no double-bill. | Safe. The key encodes the period (WPERIOD), so a different period bills afresh. |
| Re-run GCMINCHK same period | CHARGED=0 MET=0; MFBSPND already reset. | Safe — the zero-amount MET marker row is the guard that stops a re-charge from an emptied tracker. |
| Re-run GCLATE same period | CHARGED=0; no second late fee. | Safe (period-keyed guard). A member within grace, or with MBAL≤0, is never charged. |
| Re-run GCGLPST | Nets off already-posted amounts; posts only new movement. | A no-op re-run adds no GL rows and the GL stays balanced (DR=CR). |
| Walk-in / blank-member sale or chit | Counted in SKIP, never posted to a ledger. | Expected — cash-and-carry is out of scope for member billing. |
| Held member (MHOLD=Y) | Still billed dues, min-spend and late fees; status unchanged. | Expected — the hold blocks new privileges, not billing. |
GCLEDG row and MBAL
always reconstructs as the sum of those rows, any cycle's effect is fully reconstructable after the fact
for reconciliation and recovery — the invariant every gc_* test re-checks per member.The program surface, from golf-app/src/sources.mjs. All objects are in library
LINKS. The RPG programs are column-exact fixed-form (C/D-spec helpers) with
/free blocks; the pattern throughout is a *LOVAL SETLL + READ loop
over the driving file, an EXSR per row, and a DSPLY summary line.
TSTAT='P'): derive RNDNO from the tee-time number
('RN0' + %subst(TEENO:4:5)); guard with CHAIN(EN) on GCROUND;
price the green fee WBASE(45) − discount% from the member's class; add the cart
(20) if CARTYN='Y'; walk every sponsored guest (READE on MEMNO,
matching date and GSTSTAT='O') at the flat WGSTFEE(45); write GCROUND,
flip the tee-time to 'G', raise MBAL, write one GCLEDG 'G' row
keyed 20000000 + digits. Note the explicit wgst = 0 reset per round — RPG
working storage is global, so without it a prior round's guest total would survive into a later
guest-less round.SSTAT='O') with a non-blank member: ledger key 30000000 +
digits, CHAIN(EN) guard; relieve ONHAND by SQTY (floored at
zero); flip the sale to 'P'; raise MBAL; write GCLEDG 'P'.
Blank-member walk-ins are skipped.40000000 + digits; flip to 'P'; raise both
MBAL and MFBSPND; write GCLEDG 'F'.50000000 + digits; flip to 'P'; raise
MBAL; write GCLEDG 'C'.RSTAT='U'): key 80000000 + digits; flip to
'A'; lower MBAL by exactly RCPTAMT; write GCLEDG 'R'
with a negative amount. The full amount always applies — a club account is a running balance,
so it may run credit.10000000 + (member-digits × 100) + WPERIOD,
CHAIN(EN) guard; look up class dues; raise MBAL; write GCLEDG 'D'.
A held member is still billed.60000000 + (digits × 100) + WPERIOD; shortfall =
max(0, MCMINSP − MFBSPND); raise MBAL by the shortfall; reset
MFBSPND to 0 regardless; always write a GCLEDG 'M' row (positive shortfall
→ CHARGED, zero → MET marker). The zero-amount MET row is the re-run guard.MBAL > 0: find the most recent 'R' ledger row (or the
join date), compute days on a 30/360 serial, bucket 0/1/2/3 (≤30 / ≤60 / ≤90 / >90 days), and
print AGEDTL on GCAGEP with an AGETOT footer. Overflow re-prints
AGEHDR via OFLIND(*IN90).MBAL > 0: serialise the run date and last-payment date on a
30/360 convention; if the gap is within the 30-day grace → OK; else key 70000000 + (digits ×
100) + WPERIOD, guard, raise MBAL by the flat WFEE(25), write
GCLEDG 'L'. A member with no payment history is judged from their join date.EXEC SQL SELECT SUM(AMT)
per account) so only new movement posts; then writes two independently balanced legs (F.4) via a
glrow subroutine that INSERTs into GCGLDIST and only counts the row
when SQLCOD=0.GCMEMB sequentially; totals owed over positive balances, counts members,
outstanding accounts and held members; DISPLAYs OWED / MEMBERS / OUTSTND / ONHOLD.LINKS/i has no database sequence and no control-row cursor; instead every posting program derives
its GCLEDG.LEDSEQ from stable source digits, so a re-run regenerates the identical key and the
CHAIN(EN) guard on that key refuses the duplicate. The high digit namespaces the source
stream; period-scoped charges fold in the period so a new period lands on a fresh key.
| Program | LEDSEQ formula | Namespace |
|---|---|---|
| GCDUES | 10000000 + (memdigits × 100) + period | dues, per member per period |
| GCRNDPST | 20000000 + teedigits | round, per tee-time |
| GCSALEPST | 30000000 + saledigits | pro-shop, per sale |
| GCCHITPST | 40000000 + chitdigits | F&B, per chit |
| GCCARTPST | 50000000 + cartdigits | cart, per rental |
| GCMINCHK | 60000000 + (memdigits × 100) + period | min-spend, per member per period |
| GCLATE | 70000000 + (memdigits × 100) + period | late fee, per member per period |
| GCRCPTPST | 80000000 + rcptdigits | receipt, per receipt |
Several programs re-CHAIN the driving record after the guard/lookup reads (which move the
file cursor) before updating it — e.g. GCRNDPST re-CHAINs the tee-time after walking the
class and guest files. This is a deliberate cursor-restore, noted inline in the source.
The GL close maps each ledger type to a revenue/asset account. The charges leg debits AR
(1300-MBAR) for the gross and credits each type's own revenue account; the receipts leg debits
cash and credits AR — two legs, each balanced by construction.
| LEDTYPE | Meaning | GL account (CR unless noted) |
|---|---|---|
| D | Monthly dues | 4100-DUES |
| G | Green fee / cart / guest | 4200-GRNF |
| P | Pro-shop sale | 4300-SHOP |
| F | F&B chit | 4400-FOOD |
| C | Cart rental | 4500-CART |
| M | Min-spend shortfall | 4600-MINS |
| L | Late fee | 4700-LATE |
| R | Receipt (payment) | 1000-CASH (DR), 1300-MBAR (CR) |
| — | Charges AR control | 1300-MBAR (DR, = Σ D/G/P/F/C/M/L) |
Worked GL (gc_period oracle): DR 1300-MBAR 230.00 = CR 4100-DUES 180.00 + CR 4700-LATE 50.00; plus DR 1000-CASH 40.00 = CR 1300-MBAR 40.00. Grand total DR = CR = 270.00.
Grounded in golf-app/FINDINGS.md. LINKS/i re-confirmed — but did not newly introduce
— two already-documented ORDERIS/i engine behaviours in the DDS/subfile renderer (the NA-05/NA-07
estate area):
N31/31 on
SFLDSP/SFLDSPCTL/SFLCLR, the DDS parser reads only the first digit,
so N31 and 31 collapse onto indicator 3 instead of two independent
ones — the intended "clear then don't display" vs "display" split is not honoured as designed.GCLEDIQ reloads for a real member
with no ledger rows, the prior pass's rows can still render even though the program issued
SFLCLR; the header (name/balance) correctly updates to the new member's 0.00 figures.Both are cosmetic/interactive-only with no data-integrity impact. No workaround was applied
silently: test/gc_interactive.mjs asserts only the correct parts (the header update) and
documents the stale-subfile behaviour inline rather than asserting a false pass or masking it with
different DDS. The batch posting logic and every balance/ledger/GL invariant are unaffected.
GCMEMB.MBAL) always reconstructs as the sum of their own ledger rows.MFBSPND) is charged the shortfall (ledger type M); the tracker then resets.ONHAND falls by exactly the quantity a sale ships (floored at
zero), never for an out-of-scope walk-in.CHAIN(EN) guard refuses the duplicate (F.3).SBMJOB
CMD(CALL PGM(LINKS/GCDAILY))). The LINKS/i cycles take no parameters.GCLEDIQ's
ledger list is a subfile with paging (SFLPAG 5 / SFLSIZ 50) and roll keys.GCGLPST uses it, to sum the ledger and insert the GL
distribution rows.