HARBOR/i runs the day-to-day operation of a marina: a slip inventory by size-class and
rate, a boat/owner registry, slip rental agreements (monthly, seasonal and transient), fuel-dock sales
with inventory relief, service work orders, a customer ledger / accounts-receivable with open-item
discipline, cash-receipt application, AR aging with late fees, and a balanced GL close. It is a
classic IBM i application: DDS physical/logical files, fixed-form and free-form RPG batch programs,
two interactive 5250 screens (one plain, one subfile), an ILE COBOL control report, an embedded-SQL GL
post, and CL job cycles that string the batch programs together. This manual is the reference for the
operator who runs the online screens and the three periodic cycles, and for the developer maintaining
the application. It is grounded entirely in the committed source
(marina-app/src/sources.mjs, src/seed.mjs, and the
test/hb_*.mjs drivers). Everything runs in library HARBOR.
HARBOR/i covers the operational life of a boat's stay at the marina:
S/M/L) and a status
(V vacant, O occupied, M maintenance). The availability
screen answers "have we got a 35ft slip free?" with live counts.M monthly, S seasonal, T
transient) and a rate locked at signing (SLRATE), so a later change to the
standard rate table never silently reprices an existing renter.Two invariants run through the whole application and are the operator's and the developer's north star:
HBSLIPAG.RBAL (the live ledger balance of an
agreement) rises only via a charge (slip rent, fuel, service, or late fee) and falls only
via cash application. Every charge/payment test asserts the balance moved by exactly
the transaction amount — no rounding drift, no double-posting.HBFUEL.FQOH (gallons on hand) falls only via a
fuel sale (HBFUELPOST relief) and rises only via a delivery
(HBFUELRCV receipt), each by exactly the ticket quantity. A cash sale still relieves
inventory but posts no ledger charge (paid on the spot at the dock terminal, out of this system's
scope).Each posting batch is also idempotent: a re-run never double-charges, double-relieves or
double-pays. Idempotency is enforced either by a per-row posted-flag (FPOST/RPOST/WPOST),
by a derived charge key (a charge number computed from the agreement plus the run period, so a
same-period re-run regenerates the same key and is refused), or, for the GL, by a
watermark-since-last-post subtraction.
SETUP / SEED ONLINE BATCH (three cycles)
------------ ------ --------------------
HBSETUP (CL) HBMENU (5250) DAILY (HBDAILY CL):
CRTPF/CRTLF/... opt 1 -> HBAVAILQ HBFUELRCV (delivery in)
CRTBNDRPG/... opt 2 -> HBLEDGQ HBFUELPOST (sales + relief + 'U' charge)
HBREFLD (RPG) HBWOPOST (service 'S' charge)
seeds MAR/SZCL/ HBCASHAP (cash apply, oldest-due-first)
SLIP/BOAT/AGR/FUEL HBAVAILQ (plain DSPF) MONTHLY (HBMONTH CL):
slips by size class HBRENTRL (rent-roll 'R' charge)
HBLEDGQ (subfile) HBAGE (AR aging -> HBDELQ)
boat's ledger history HBLATEFEE ('L' late fee)
HBGLPST (balanced GL post)
HBAGERPT (COBOL control report)
PERIOD (HBPERIOD CL):
HBTRNBILL (transient checkout billing)
Every charge/payment writes an HBCHG ledger row and moves HBSLIPAG.RBAL.
Fuel sales/deliveries move HBFUEL.FQOH. The GL close reads HBCHG and writes
balanced DR/CR rows into HBGLDIST (SQL table).
A single event — say a charged fuel sale — flows: the dock captures an HBFSALE
ticket (FPOST='N') → the daily HBFUELPOST relieves
HBFUEL.FQOH by the gallons sold, writes a 'U' ledger debit to
HBCHG, raises the boat's HBSLIPAG.RBAL, and stamps the ticket
FPOST='Y' so it is never posted twice.
| Object | Type | Role |
|---|---|---|
| HBMAR | PF | Marina/basin master. |
| HBSZCL | PF | Slip size-class rate table (monthly/seasonal/transient rates). |
| HBSLIP | PF | Slip master (size class + status). |
| HBSLPLF | LF | Slips keyed by (MARCD, SSTAT, SZCLCD) for availability. |
| HBBOAT | PF | Boat/owner registry. |
| HBSLIPAG | PF | Slip rental agreement (locked rate, live balance). |
| HBCHG | PF | Customer ledger / charge journal (the heart of the app). |
| HBCHGLF | LF | Ledger keyed by (BOATNO, CHGNO) for the subfile inquiry. |
| HBCHGDLF | LF | Ledger keyed by (AGRNO, CDUEDT, CHGNO) for aging. |
| HBRCPT | PF | Cash receipts. |
| HBDELQ | PF | AR aging / late-fee detail (one row per agreement, upserted). |
| HBFUEL | PF | Fuel-dock inventory (QOH, cost, price). |
| HBFSALE | PF | Fuel sale tickets (POS captures). |
| HBFUELRC | PF | Fuel deliveries/receipts. |
| HBWO | PF | Service work orders. |
| HBGLDIST | SQL table | GL distribution (balanced DR/CR rows); index HBGLDACC. |
| HBAVAILD / HBLEDGD / HBMENUD | DSPF | Availability / ledger-subfile / menu display files. |
| HBAGEP | PRTF | AR aging printer file. |
| HBREFLD | RPG | Reference-data seed loader. |
| HBRENTRL / HBTRNBILL | RPG | Monthly/seasonal rent-roll / transient checkout billing. |
| HBFUELPOST / HBFUELRCV | RPG | Fuel sale posting+relief / delivery receiving. |
| HBWOPOST / HBCASHAP | RPG | Work-order posting / cash application. |
| HBAGE / HBLATEFEE | RPG | AR aging / late-fee assessment. |
| HBGLPST | SQLRPGLE | GL post (embedded SQL over HBGLDIST). |
| HBAVAILQ / HBLEDGQ / HBMENU | RPG | Interactive availability / ledger / menu drivers. |
| HBAGERPT | ILE COBOL | AR aging control report over the ledger. |
| HBSETUP / HBDAILY / HBMONTH / HBPERIOD | CLP | Build/seed CL + the three job-cycle CLs. |
The catalogue is 15 database files (12 PF + 3 LF), 1 SQL table + index, 4 DSPF/PRTF, 13 RPG/SQLRPGLE programs, 1 COBOL program and 4 CL programs. Sections D and F expand each.
HARBOR/i has no CICS transaction identifiers. On IBM i each program is reached by name from a
5250 command-entry line (or a JOBQ/scheduler for the batch cycles). Before invoking anything, the
job's library list must include HARBOR — the tested jobs run with
LIBL = QSYS QGPL HARBOR QTEMP and CURLIB = HARBOR. The everyday entry point is
the menu; from there the operator picks the two inquiry screens.
| To do this | Type on the command line |
|---|---|
| Open the operator main menu | CALL HARBOR/HBMENU |
| Open the slip availability inquiry directly | CALL HARBOR/HBAVAILQ |
| Open the customer ledger inquiry directly | CALL HARBOR/HBLEDGQ |
| Build/compile every object, first time | CALL HARBOR/HBSETUP |
| Seed the reference data | CALL HARBOR/HBREFLD |
| Run the daily cycle | CALL HARBOR/HBDAILY (or SBMJOB it) |
| Run the monthly cycle | CALL HARBOR/HBMONTH |
| Run the period (transient) cycle | CALL HARBOR/HBPERIOD |
The batch drivers take no CALL parameters: run dates and periods are compiled into the RPG as
named constants (e.g. WRUNDT INZ(20260901), WPERIOD INZ('09')), so a scheduled
submission is a bare CALL. Only the three HB*Q/HBMENU programs are
interactive; the batch programs run to completion and DSPLY a one-line result summary each.
HBMENU is the front door. It shows a two-option menu and program-to-program CALLs the chosen inquiry,
returning to the menu when the operator presses F3 in the inquiry. An option
other than 1/2/blank sets the "Invalid option." message.
A plain (non-subfile) screen answering the everyday "have we got a slip of this class free?"
question. The operator keys a marina code and a size class; the program CHAINs
HBSZCL to render that class's description and its three rate tiers, then walks every slip at
the marina (READE on HBSLIP by MARCD) and tallies live counts of
vacant / occupied / maintenance slips of that class. Re-key and press
Enter to inquire again; F3 exits.
M figures: monthly
400.00, season 1800.00, transient 65.00; MAR1 has two M slips (01A occupied, 01E vacant),
so vacant=1, occupied=1. An unknown class leaves the fields blank with "Size class not found."HBLEDGQ is the app's subfile screen (DDS record LSFL under control record
LCTL, SFLPAG(5) per page, SFLSIZ(30), ROLLUP/ROLLDOWN
paging). The operator keys a boat number; the program CHAINs HBBOAT for the boat
name, scans HBSLIPAG for that boat's agreement (header agreement number and live balance),
then loads the boat's ledger rows from HBCHGLF (keyed by BOATNO) into the
subfile in posting order. The subfile is cleared and reloaded each pass, so a fresh inquiry always
reflects current data.
| Field | Type (DDS) | Shows |
|---|---|---|
| SCHGNO | 8A output | Charge number (ledger row key). |
| SCDT | 10A output | Transaction date. |
| SCTYPE | 1A output | Type: R rent, U fuel, S service, L late fee, P payment. |
| SCAMT | 12A output | Transaction amount (%EDITC 'K'). |
| SCBAL | 12A output | Agreement balance AFTER this row (CBALAFT). |
| SCMEMO | 30A output | Free-text memo. |
HBSLIPAG's own key is AGRNO,
not BOATNO, so finding a boat's agreement for the header is a filtered full scan
(SETLL *LOVAL + sequential READ, iter when
BOATNO ≠ wboat) rather than a keyed read — the same honest choice the sibling
storage-app's SVLEDGQ makes. The subfile load itself does use a keyed access path
(HBCHGLF by BOATNO).HARBOR/i's processing runs as three CL-driven cycles rather than one nightly monolith: a
daily cycle (fuel receiving + fuel sale posting + service posting + cash application), a
monthly cycle (slip rent-roll → AR aging → late fees → GL post → aging report),
and a period cycle (transient-slip checkout billing). Each CL simply ADDLIBLE HARBOR
then CALLs its member programs in order. None takes parameters.
-- run the three cycles (bare, parameterless CALLs / SBMJOBs) CALL PGM(HARBOR/HBDAILY) CALL PGM(HARBOR/HBMONTH) CALL PGM(HARBOR/HBPERIOD) -- or scheduled: SBMJOB CMD(CALL PGM(HARBOR/HBDAILY)) JOB(HBDAILY)
| Program | Cycle | Purpose | Reads / writes | DSPLY summary |
|---|---|---|---|---|
| HBFUELRCV | Daily | Receive fuel deliveries; raise QOH; replace standing cost. | HBFUELRC (RPOST) → HBFUEL.FQOH/FCOST. | HBFUELRCV RECEIVED=n SKIP=n |
| HBFUELPOST | Daily | Post fuel sale tickets: relieve inventory; charge charged sales to the ledger. | HBFSALE (FPOST) → HBFUEL.FQOH, HBCHG('U'), HBSLIPAG.RBAL. | HBFUELPOST POSTED=n SKIP=n |
| HBWOPOST | Daily | Post completed service work orders to the ledger. | HBWO (WSTAT='C', WPOST) → HBCHG('S'), HBSLIPAG.RBAL. | HBWOPOST POSTED=n SKIP=n |
| HBCASHAP | Daily | Apply cash receipts oldest-due-first; write payment rows. | HBRCPT (RCSTAT='U') → HBCHG('P')/COPEN, HBSLIPAG.RBAL. | HBCASHAP APPLIED=n SKIP=n |
| HBRENTRL | Monthly | Rent-roll: charge active M/S agreements their locked rate. | HBSLIPAG → HBCHG('R'), HBSLIPAG.RBAL. | HBRENTRL BILLED=n SKIP=n |
| HBAGE | Monthly | Age each active agreement on its oldest open charge. | HBSLIPAG + HBCHG → HBDELQ.DDAYS (upsert). | HBAGE CURRENT=n LATE=n |
| HBLATEFEE | Monthly | Charge one flat late fee past the grace period. | HBDELQ (DDAYS>grace) → HBCHG('L'), HBSLIPAG.RBAL. | HBLATEFEE CHARGED=n SKIP=n |
| HBGLPST | Monthly | Sum the ledger since last post; write balanced DR/CR rows. | HBCHG → HBGLDIST (SQL). | HBGLPST BATCH=nnnnnn ROWS=n |
| HBAGERPT | Monthly | COBOL control report: reconcile the receivables sub-ledger. | HBCHG (sequential). | HBAGERPT BILLED/OPEN/CHARGES/STILLOPEN |
| HBTRNBILL | Period | Bill checked-out transient stays per night; terminate agreement. | HBSLIPAG (T, RCKOUTDT set) → HBCHG('R'), RSTAT='T'. | HBTRNBILL BILLED=n SKIP=n |
The daily cycle moves money and inventory captured during the day: (1) HBFUELRCV raises
HBFUEL.FQOH by each unposted delivery's RQTY and replaces the standing cost;
(2) HBFUELPOST relieves FQOH by each unposted sale's FQTY and, for a
FPAY='A' (charged) sale, writes a 'U' ledger debit and raises the agreement
balance — a FPAY='C' (cash) sale relieves inventory only; (3) HBWOPOST
posts each complete-and-unposted work order as an 'S' debit; (4) HBCASHAP walks
each unapplied receipt against its agreement's open charges oldest-due-first.
Expected DSPLY (the hb_cycle oracle: 1 delivery, 2 sales, 1 WO, 2 receipts): HBFUELRCV RECEIVED= 1 SKIP= 0 MAR1 gas 800.0 -> 1000.0 (+200.0) HBFUELPOST POSTED= 2 SKIP= 0 1000.0 -> 920.0 (-50 cash -30 charged) HBWOPOST POSTED= 1 SKIP= 0 AG000002 +75.00 service HBCASHAP APPLIED= 2 SKIP= 0 RC000001 full, RC000002 part
HBCASHAP and
HBAGE want an agreement's open charges in due-date order, but HBCHG's own key is
CHGNO. Because a logical file with an alternate key is read/update-only here (a WRITE through
it is refused) and a program cannot open a PF and an LF sharing one record format at once, both programs
scan HBCHG (SETLL *LOVAL + sequential READ) to find the
current agreement's oldest open CDUEDT and settle it one charge at a time — the same
honest choice storage-app's SVCASHAP/SVAGE make. A receipt larger than what is
owed applies only what is owed; the remainder stays unapplied on the receipt (RCSTAT='P').(1) HBRENTRL writes one 'R' slip-rent charge per active M/S
agreement for its locked SLRATE and raises the balance by exactly that amount —
transient (T) agreements are skipped. (2) HBAGE finds each active agreement's
oldest open charge and stamps HBDELQ.DDAYS = days past due on the 30/360 serial convention.
(3) HBLATEFEE charges a flat 25.00 fee to any agreement whose
DDAYS > 10 (the grace period), once per period. (4) HBGLPST sums the ledger by
type and posts balanced GL rows. (5) HBAGERPT (COBOL) prints/DSPLYs the receivables control
totals.
Expected DSPLY (the hb_cycle oracle): HBRENTRL BILLED= 4 SKIP= 1 4 monthly/seasonal billed, 1 transient skipped HBAGE CURRENT= 1 LATE= 4 only the un-charged transient is current HBLATEFEE CHARGED= 4 SKIP= 1 HBGLPST BATCH=202609 ROWS=n DR total = CR total
The GL post writes a balanced set for the movement since the last close:
DR 1200-AR against CR 4100-RENT / 4300-FUEL / 4400-SVC / 4200-FEES for the
billed charges, and DR 1000-CASH / CR 1200-AR for cash applied. In the seeded run the credit
totals reconcile to slip-rent 4585.00, fuel 112.50, service 75.00,
late fees 100.00 (4×25.00), and cash 500.00 — and total debits equal
total credits.
For every active transient (T) agreement whose RCKOUTDT is set (checkout
recorded) and not yet billed, HBTRNBILL computes the number of nights between
RSTRTDT and RCKOUTDT on the 30/360 serial convention (minimum one night),
charges nights × SLRATE as an 'R' ledger row, raises the balance, and
terminates the agreement (RSTAT='T'). An agreement not yet checked out
(RCKOUTDT=0) is simply skipped until it is.
Expected DSPLY (AG000005 checks in 09-10, out 09-13 = 3 nights @ 45.00): HBTRNBILL BILLED= 1 SKIP= 4 3 x 45.00 = 135.00, RSTAT -> T
HBDELQ.DDAYS; the GL post must see the late-fee rows so its balanced set is complete.All files are in library HARBOR, grounded in the DDS in src/sources.mjs.
Dates are stored as signed 8S0 in YYYYMMDD form; money is packed
DECIMAL; fuel quantities are packed one-decimal gallons; fuel cost/price are packed
three-decimal per-gallon.
| Field | Type | Meaning |
|---|---|---|
| MARCD | 4A | Marina code (key), e.g. MAR1. |
| MARNM | 30A | Marina name. |
| MARCITY / MARST | 20A / 2A | City / state-or-region code. |
| MARSTAT | 1A | Marina status (A active). |
| Field | Type | Meaning |
|---|---|---|
| SZCLCD | 2A | Size-class code (key): S/M/L. |
| SZDESC | 20A | Description, e.g. "MEDIUM SLIP TO 35FT". |
| SZMORATE | 9P2 | Standard MONTHLY rate. |
| SZSSNRATE | 9P2 | Standard SEASONAL (May–Sep lump) rate. |
| SZTRNRATE | 9P2 | Standard TRANSIENT day-rate. |
Seeded classes: S 250/1100/45, M 400/1800/65, L
650/2900/95. These are the standard rates a new rental picks up at signing; the agreement then
carries its own locked SLRATE, so a later table change never reprices an existing
renter.
| Field | Type | Meaning |
|---|---|---|
| MARCD / SLIPNO | 4A / 6A | Marina + slip number (key). |
| SZCLCD | 2A | Size class of this slip. |
| SSHORE | 1A | Shore power Y/N (tracked; standard, no premium). |
| SSTAT | 1A | V vacant, O occupied, M maintenance. |
Alternate access path HBSLPLF keys the same records by (MARCD, SSTAT, SZCLCD)
for the availability inquiry without a scan.
| Field | Type | Meaning |
|---|---|---|
| BOATNO | 6A | Boat number (key), e.g. B00001. |
| BNAME | 30A | Boat name. |
| OWNNM / OADDR / OPHONE | 30A / 30A / 12A | Owner name / address / phone. |
| BLOA | 5P1 | Length overall, feet (one decimal). |
| BSTAT | 1A | A active, X removed. |
| Field | Type | Meaning |
|---|---|---|
| AGRNO | 8A | Agreement number (key), e.g. AG000001. |
| MARCD / SLIPNO / BOATNO | 4A / 6A / 6A | The slip and boat this agreement covers. |
| RTYPE | 1A | M monthly, S seasonal, T transient. |
| RSTRTDT / RCKOUTDT | 8S0 / 8S0 | Start date / checkout date (0 until checked out). |
| SLRATE | 9P2 | Rate LOCKED at signing (monthly amount / season lump / day-rate). |
| RBAL | 11P2 | Live ledger balance (rises via charge, falls via cash — the invariant). |
| RSTAT | 1A | A active, T terminated (checked out). |
| RLASTCH | 8S0 | Date of last charge posted to this agreement. |
| Field | Type | Meaning |
|---|---|---|
| CHGNO | 8A | Charge number (key); for posting programs it is DERIVED so re-runs collide. |
| AGRNO / BOATNO | 8A / 6A | Owning agreement / boat. |
| CDT | 8S0 | Transaction date. |
| CTYPE | 1A | R slip rent, U fuel, S service, L late fee, P payment (credit). |
| CAMT | 11P2 | Amount (always POSITIVE; sign implied by CTYPE). |
| CDUEDT | 8S0 | Due date for a debit (0 for a payment row). |
| COPEN | 11P2 | Open-item remaining unpaid on THIS debit (drives cash application). |
| CSTAT | 1A | O open / P fully paid (blank on a payment row). |
| CBALAFT | 11P2 | Agreement balance immediately AFTER this row (point-in-time snapshot). |
| CMEMO | 30A | Free-text detail. |
Two alternate access paths: HBCHGLF (BOATNO, CHGNO) for the ledger subfile,
and HBCHGDLF (AGRNO, CDUEDT, CHGNO) for the oldest-due-first walk aging/cash want.
| Field | Type | Meaning |
|---|---|---|
| RCPTNO | 8A | Receipt number (key). |
| AGRNO / BOATNO | 8A / 6A | Agreement / boat the receipt is for. |
| RCDT | 8S0 | Receipt date. |
| RCAMT | 11P2 | Amount received. |
| RCAPPL | 11P2 | Amount actually applied against open charges. |
| RCSTAT | 1A | U unapplied, P part-applied, A fully applied. |
| Field | Type | Meaning |
|---|---|---|
| AGRNO / BOATNO | 8A / 6A | Agreement / boat (one row per agreement, upserted in place). |
| DDAYS | 5S0 | Days past due on the OLDEST unpaid charge at the last aging run. |
| DFEEDT | 8S0 | When the late fee was first assessed for the current delinquency. |
| DFEETOT | 11P2 | Late fees accumulated on this agreement. |
| Field | Type | Meaning |
|---|---|---|
| MARCD / FGRADE | 4A / 1A | Marina + grade (G gasoline, D diesel) (key). |
| FQOH | 11P1 | Quantity on hand, gallons (the conservation invariant). |
| FCOST / FPRICE | 7P3 / 7P3 | Standing cost / retail price per gallon. |
| Field | Type | Meaning |
|---|---|---|
| SALENO | 8A | Sale number (key). |
| MARCD / AGRNO / BOATNO | 4A / 8A / 6A | Marina / (charged) agreement / boat. |
| SDT / FGRADE | 8S0 / 1A | Sale date / fuel grade. |
| FQTY / FAMT | 11P1 / 11P2 | Gallons sold / extended amount at price. |
| FPAY | 1A | C cash (no ledger charge) / A charged to agreement. |
| FPOST | 1A | N unposted / Y posted (the idempotency guard). |
| Field | Type | Meaning |
|---|---|---|
| RCPTNO | 8A | Delivery receipt number (key). |
| MARCD / FGRADE | 4A / 1A | Marina / grade delivered. |
| RDT / RQTY / RCOST | 8S0 / 11P1 / 7P3 | Delivery date / gallons / cost per gallon. |
| RPOST | 1A | N unposted / Y posted (guard). |
| Field | Type | Meaning |
|---|---|---|
| WONO | 8A | Work-order number (key). |
| AGRNO / BOATNO | 8A / 6A | Agreement / boat. |
| WDT / WDESC / WAMT | 8S0 / 30A / 11P2 | Date / description / amount. |
| WSTAT | 1A | O open / C complete (chargeable). |
| WPOST | 1A | N unposted / Y posted (guard). |
| Field | Type | Meaning |
|---|---|---|
| GLSEQ | DECIMAL(8,0) | Sequence (PK); HBGLPST continues it from MAX(GLSEQ). |
| GLBATCH | DECIMAL(6,0) | Batch (the close year-month, e.g. 202609). |
| ACCT | CHAR(9) | Account: 1000-CASH, 1200-AR, 4100-RENT, 4200-FEES, 4300-FUEL, 4400-SVC. |
| DRCR | CHAR(1) | D debit / C credit. |
| AMT / GLREF / GLDT | DECIMAL(11,2) / CHAR(8) / DECIMAL(8,0) | Amount / reference / date. Index HBGLDACC on (ACCT, DRCR). |
HARBOR (ADDLIBLE HARBOR).CALL HARBOR/HBSETUP — deletes any prior files, then CRTPF/CRTLF
the database, CRTDSPF/CRTPRTF the screens/report, RUNSQLSTM the
GL table + index, and CRTBNDRPG/CRTBNDCBL every program. It ends with the
"HARBOR setup complete." message.CALL HARBOR/HBREFLD — seeds 2 marinas, 3 size classes, 8 slips, 5 boats, 5 rental
agreements and 4 fuel-inventory rows. Confirm the DSPLY:
HBREFLD MAR=2 SZCL=3 SLIP=8 BOAT=5 AGR=5 FUEL=4.HBFUELRC, RPOST='N'), fuel sale tickets
(HBFSALE, FPOST='N'), completed work orders (HBWO,
WSTAT='C', WPOST='N') and cash receipts (HBRCPT,
RCSTAT='U') — captured at the dock/desk during the day.SBMJOB CMD(CALL PGM(HARBOR/HBDAILY)).CALL HARBOR/HBMENU (availability, ledger) as
needed.HBSLIPAG.RCKOUTDT and run
CALL HARBOR/HBPERIOD to bill the stay.Post-checks after the daily cycle:
HBFUELRCV RECEIVED= equals the deliveries staged; QOH rose by exactly the delivered
gallons (conservation).HBFUELPOST POSTED= equals the tickets staged; QOH fell by exactly the total gallons sold;
each charged (A) sale wrote one 'U' ledger debit and raised its agreement
balance; cash (C) sales wrote no ledger row.HBWOPOST POSTED= equals the completed work orders; each raised its balance by exactly
WAMT.HBCASHAP APPLIED= equals the unapplied receipts; balances fell by exactly the amount
applied; over-payment left a remainder unapplied (RCSTAT='P').SBMJOB CMD(CALL PGM(HARBOR/HBMONTH)).HBRENTRL BILLED=, HBAGE CURRENT=/LATE=,
HBLATEFEE CHARGED=, HBGLPST BATCH=/ROWS=, and the
HBAGERPT control totals.SELECT DRCR, SUM(AMT) FROM HARBOR.HBGLDIST GROUP BY DRCR;
-- and by account:
SELECT ACCT, DRCR, SUM(AMT) FROM HARBOR.HBGLDIST GROUP BY ACCT, DRCR ORDER BY ACCT;
Reconciling figures (the same ones the hb_cycle simulation checks against a hand-derived JS oracle):
4100-RENT credit) = the regular monthly/seasonal charges +
any backdated + the transient billing, e.g. 400+250+400+2900 + 500 + 135 = 4585.00.4300-FUEL) = the charged fuel sales only (cash sales bypass the
ledger), e.g. 112.50.4400-SVC) = the posted work orders, e.g. 75.00.4200-FEES) = 25.00 × delinquent agreements, e.g.
100.00.1000-CASH debit) = the payments applied, e.g. 500.00.1200-AR) nets the billed charges (debit) against cash applied (credit); the
DR and CR totals across all accounts are equal.Aging control: HBAGERPT (COBOL) DSPLYs BILLED (sum of debit charge amounts), OPEN
(sum still unpaid), CHARGES (debit row count) and STILLOPEN (count of open debits) so the receivables
sub-ledger can be reconciled against the ledger.
Every posting batch is safely re-runnable; each DSPLYs a POSTED/BILLED/APPLIED= plus a
SKIP= count, and on a clean re-run the posted count drops to 0 while everything is skipped.
| Situation | Behaviour | Action |
|---|---|---|
| Daily fuel/service/cash re-run | Posted rows carry FPOST/RPOST/WPOST='Y' and receipts RCSTAT≠'U'; the derived payment key already exists. | Safe: HBDAILY re-runs with everything skipped. Idempotent. |
| Rent-roll re-run, same period | Charge number is derived from (agreement, period); the same key already exists. | Safe no-op — the agreement is skipped, no double charge. A NEW period (different WPERIOD) bills again. |
| Aging re-run | HBAGE upserts one HBDELQ row per agreement (CHAIN then UPDATE-or-WRITE). | Safe: recomputes cleanly, no duplicate rows. |
| Late-fee re-run, same period | Fee charge number is derived from (agreement, period). | Safe no-op — never double-fees within a period. |
| GL post re-run | HBGLPST subtracts what is already posted per account before writing (watermark). | Safe: a second run posts only new movement; the first month's figures are never re-posted. |
| Transient billing re-run | Charge key is derived from the agreement; a billed stay already exists and the agreement is now RSTAT='T'. | Safe: skipped, never double-billed. |
| Over-payment on a receipt | Only what is owed is applied; the remainder stays unapplied. | Receipt shows RCSTAT='P'; the surplus is never lost. Apply it later against new charges. |
HBCHG with a
post-transaction balance snapshot (CBALAFT) and an open-item remaining
(COPEN), any cycle's effect is fully reconstructable for reconciliation and recovery.The programs are held as JS string constants in marina-app/src/sources.mjs and loaded into
library HARBOR's source physical files by src/seed.mjs (seedHarbor()),
mirroring the emulator's ibmi/samples.js DEMOLIB convention. The RPG is mostly fixed-form
C-specs with /free blocks; two programs (HBGLPST embedded SQL, and the
interactive drivers) lean more on free-form.
HBSLIPAG; for each active M/S agreement builds
WCHGNO = 'R' + %subst(agr:4:5) + period, guards with CHAIN(EN) +
IF *IN95=*OFF, then writes a 'R' charge and raises RBAL.
Transient agreements are skipped.T agreements with RCKOUTDT≠0: computes nights on the 30/360
serial (min 1), charges nights×SLRATE, sets RSTAT='T'. Guarded by a
derived 'T' charge key.FPOST='N' sale: CHAIN(E) HBFUEL and relieve
FQOH -= FQTY; for FPAY='A', write a 'U' ledger debit and raise
RBAL; stamp FPOST='Y'. Its idempotency guard uses free-form
CHAIN + %FOUND() (not a fixed-form indicator) — see F.4.RPOST='N' delivery: FQOH += RQTY, replace FCOST
(simple replace-cost, not weighted-average), stamp RPOST='Y'.'S' debit and raise
RBAL. Work variables are prefixed WK* deliberately — see F.3.RCSTAT='U' receipt, walks the agreement's open charges oldest-due-first by
scanning HBCHG, settling COPEN one charge at a time until the receipt is
exhausted; reduces RBAL by the amount applied; writes a 'P' payment row
keyed by a derived receipt key; stamps RCSTAT='A'/'P'.HBDELQ.DDAYS =
run-date minus due-date on the 30/360 serial. Upserts the HBDELQ row.HBDELQ row with DDAYS>10 (grace), charges one flat
25.00 'L' fee (derived-key guarded) and raises RBAL.HBCHG by CTYPE, reads the already-posted totals per account
(the watermark), subtracts them, and INSERTs balanced DR/CR rows into
HBGLDIST continuing GLSEQ from MAX(GLSEQ). Only a row whose
insert returned SQLCOD=0 is counted.HBCHG, sums debit charge amounts and open amounts, counts charges and
still-open debits; DSPLYs BILLED / OPEN / CHARGES / STILLOPEN.EXFMT HBMENU loop; option 1 CALL 'HBAVAILQ', option 2 CALL 'HBLEDGQ',
anything else sets "Invalid option."; *IN03 (F3) exits.CHAIN(E) HBSZCL for the rate tiers, then READE
HBSLIP by marina tallying vacant/occupied/maintenance for the keyed class; renders via
%EDITC.LSFL/LCTL over HBLEDGD (SFILE(LSFL:RRN)).
Clears the subfile (*IN31 → WRITE LCTL) each pass, CHAINs
HBBOAT, scans HBSLIPAG for the header, then loads HBCHGLF rows
by boat.'R'+%subst(agr:4:5)+period (rent),
'L'+...+period (late fee), 'U'+%subst(sale:2:7) (fuel),
'S'+%subst(wono:2:7) (service), 'P'+%subst(rcpt:4:5)+'00' (payment).YYYYMMDD to a serial
((yy*12)+mm)*30+dd and subtract — a simple, deterministic day count matching the
sibling storage-app.FPOST/RPOST/WPOST gate the
one-time inventory/ledger effect of a captured ticket, splitting capture (at the dock/desk) from
posting (a separate idempotent batch step).HBCHG's own key (CHGNO) does not match the
access order a routine needs, HARBOR/i scans rather than pretend a keyed read — documented in
the source and mirrored from storage-app.HBWO's externally-described fields already
include WAMT/WSTAT/WPOST; a standalone D-spec sharing one of
those names would share its storage slot on this platform (a known quirk), so HBWOPOST names
its work variables WKAMT/WKPOST/... to avoid aliasing the real ledger
fields.Finding. During the build of HARBOR/i, the fixed-form RPG CHAIN(xN) FILE key nn
not-found resulting indicator was found to unreliably report FOUND for a key proven absent
from the file, while the free-form %FOUND() built-in on the identical operation is reliable.
The behavior is 100% reproducible in a minimal single-file repro (a freshly created UNIQUE-keyed PF with
zero records: WKEY CHAIN(EN) TESTPFR nn then IF *INnn=*ON prints "FOUND"
when the file is empty), yet the exact trigger could not be reduced below "a hand-written program
of this shape" — the sibling storage-app's real SVCASHAP, using the identical idiom,
correctly reports NOT FOUND on an empty file. Logged as Confirmed (behavior) / Suspected (exact
trigger) in marina-app/FINDINGS.md; locus ibmi/rpg.js emitChain()'s
indicator-assignment codegen path (the %FOUND() path is unaffected).
Where it bit HARBOR/i and the fix. In HBFUELPOST, the charged-fuel-sale idempotency
guard WCHGNO CHAIN(EN) HBCHGR ... 95 reported FOUND on the first run against an empty
HBCHG, so the 'U' fuel debit was silently never written and the boat's balance
was never raised for the charge — while inventory relief and the "POSTED=n" counter were still
correct, making the bug easy to miss. The fix, in HARBOR/i's own source (not a platform workaround that
hides the finding), switched that guard to the free-form CHAIN + %FOUND()
form, which is reliable in every repro. See S.HBFUELPOST, the block commented
PG-HB-001.
CHAIN(EN) ... nn idiom
(HBRENTRL, HBTRNBILL, HBCASHAP, HBLATEFEE) all pass
their own idempotency tests in test/hb_cycle.mjs (correctly NOT FOUND on first run, FOUND on
re-run), so this is not a blanket "never use CHAIN(EN)" situation — it appears to depend
on some program-shape/size property not fully characterized. Those four were left as-is (proven working by
their passing tests) rather than speculatively rewritten.SLRATE) and a live ledger balance (RBAL).HBDELQ.DDAYS on the 30/360 serial convention.COPEN) and a status (O/P).FQOH) falls only via a sale and rises only via a delivery, each by exactly
the ticket quantity.25.00 fee per period.HBSZCL rate table, so an existing renter is never silently repriced.RCSTAT='P').COPEN>0, CSTAT='O');
cash application and aging both walk open items.SBMJOB CMD(CALL PGM(HARBOR/HBDAILY))).HBTRNBILL,
which then terminates the agreement (RSTAT='T').((yy*12)+mm)*30+dd, and day counts are differences of these serials.