VETCARE/i is a veterinary-clinic management application: owners and their animals
(patients), the visits those animals make, the services and vaccinations performed on a visit priced
off a shared fee schedule, invoices raised one-per-closed-visit, payments applied against invoices,
vaccination due-date RECALL tracking that drives a weekly reminder run, and a monthly AR-aging / GL /
close cycle. The spine of the system is the visit: an animal's visit accumulates service and
vaccine lines priced off VCSVC, closes (rolling its lines into a billed total), and becomes
one invoice; a payment posts against that invoice and moves the owner's AR balance; a vaccine line
administered on a visit generates or refreshes that animal's next-due recall row. Every posting program
is deliberately re-runnable off a ledger keyed by a stable business key. 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 (vet-app/src/sources.mjs,
src/seed.mjs, and the test/vc_*.mjs drivers). Everything runs in library
VETCARE.
VETCARE/i runs the operational and billing life of a veterinary clinic:
VCOWNR and an animal
(patient) master VCANML with the owner as FK; one owner may keep many pets, and a
deceased patient (ANSTAT='D') is retained so the owner keeps history.VCVISIT) for an animal accumulates
service/vaccine lines (VCVSTL) priced off the fee schedule (VCSVC); each
line amount is SVPRICE × VLQTY.VSBILL and closes it, then raises exactly one invoice (VCINV) per closed
visit, copying the lines down verbatim into invoice lines (VCINVL).VCRECALL), RCDUE = visit date + SVRECDY; a weekly reminder run
(VCRCWRK) sweeps the due and overdue recalls.VCPYMT) decrements its invoice balance
(overpayment rejected, never negative); the owner's AR balance is the sum of that owner's open
invoice balances.VCAR), a balanced
GL revenue/cash distribution (VCGLDIST), and a COBOL close-summary report.Every program in the application obeys one small set of billing identities, stated at the top of the source and asserted by the test drivers:
VLAMT in VCVSTL).VSBILL).VSBILL at generation time (one invoice per closed visit).These are not decorative: the daily suite checks that invoice line amounts sum exactly to the invoice total, and that a rejected overpayment never drives a balance negative.
REFERENCE ONLINE BATCH (periodic)
--------- ------ ----------------
VCREFLD (seed) VCMENU (5250) DAILY VCDAILY
owners opt 1 -> VCPATIQ VCVSTCLS visit close (roll-up)
animals opt 2 -> VCVLIQ VCINVGEN invoice generation
providers VCRECGEN recall due-date gen
fee schedule VCPYPOST payment posting
WEEKLY VCWEEK
VCRECRUN recall reminder run
MONTHLY VCMONTH
VCARAGE AR aging
VCGLDST GL distribution (SQL)
VCMCLOSE close summary (COBOL)
VCVISIT --close--> VSBILL --invoice--> VCINV --pay--> IBAL --> owner AR
| |
+--> VCVSTL (lines) --copy-down--> VCINVL (invoice lines, verbatim)
+--> vaccine line --> VCRECALL (RCDUE = VSDT + SVRECDY) --> VCRCWRK (weekly)
Every posting program writes VCLEDG (durable audit + re-runnability guard).
A single event — say a visit being invoiced — flows: VCVSTCLS rolls the
visit's lines into VSBILL and sets VSTAT='C' → VCINVGEN raises
VCINV/VCINVL, sets VSTAT='I' and carries the invoice number back
→ VCRECGEN reads the invoiced visit's vaccine lines and generates/refreshes
VCRECALL → each step first CHAINs its stable ledger key in VCLEDG and
refuses to double-post if the row is already there.
| Object | Type | Role |
|---|---|---|
| VCOWNR | PF | Owner / client master (spine OWNID). |
| VCANML | PF | Animal / patient master (spine ANMID, owner FK). |
| VCANMLO | LF | Animals keyed by OWNER (patients-by-owner path). |
| VCPROV | PF | Provider / staff (vet, technician). |
| VCSVC | PF | Fee schedule — services AND vaccines share one list. |
| VCVISIT | PF | Visit header (the spine of the app). |
| VCVISITO | LF | Visits keyed by ANIMAL (patient-history path). |
| VCVSTL | PF | Visit lines (composite key VISITNO, VLINE). |
| VCRECALL | PF | Vaccination recall (one row per ANMID, SVCCD). |
| VCRCWRK | PF | Recall reminder worklist (weekly output). |
| VCINV | PF | Invoice header (one per closed visit). |
| VCINVO | LF | Invoices keyed by OWNER (AR-aging path). |
| VCINVL | PF | Invoice lines (verbatim copy-down of visit lines). |
| VCPYMT | PF | Payment / receipt applied to an invoice. |
| VCAR | PF | AR aging snapshot (monthly, per owner/run). |
| VCLEDG | PF | Durable audit trail + re-runnability ledger. |
| VCGLDIST | SQL table | GL distribution the monthly close posts into. |
| VCPATD / VCVLD / VCMENUD | DSPF | Patient inquiry / visit-subfile inquiry / menu. |
| VCINVP | PRTF | Client-invoice / statement printer file. |
| VCREFLD | RPG | Reference-data seeder. |
| VCVSTCLS / VCINVGEN / VCRECGEN / VCPYPOST | RPG | Daily posting programs. |
| VCRECRUN | RPG | Weekly recall reminder run. |
| VCARAGE | RPG | Monthly AR aging. |
| VCGLDST | SQLRPGLE | Monthly GL distribution (embedded SQL). |
| VCPATIQ / VCVLIQ / VCMENU | RPG | Interactive inquiry / menu programs. |
| VCMCLOSE | ILE COBOL | Monthly close-summary report. |
| VCSETUP | CL | Create every object, compile every program. |
| VCDAILY / VCWEEK / VCMONTH | CL | The three job cycles. |
The full catalogue is 13 PFs, 2 LFs, 3 DSPFs, 1 PRTF and 1 SQL table (+ its index), driven by 11 RPG programs, 1 ILE COBOL program and 4 CL programs. Sections D and F expand each.
VETCARE/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 through the VCMENU menu driver for
the interactive programs, 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 VETCARE — the tested jobs run with
LIBL = QSYS QGPL VETCARE QTEMP and CURLIB = VETCARE.
| To do this | Type on the command line |
|---|---|
| Open the operator menu (routes to the inquiry screens) | CALL VETCARE/VCMENU |
| Open the patient inquiry screen directly | CALL VETCARE/VCPATIQ |
| Open the visit-line subfile inquiry directly | CALL VETCARE/VCVLIQ |
| Run the daily visit/billing cycle | CALL VETCARE/VCDAILY (or SBMJOB it) |
| Run the weekly recall reminder cycle | CALL VETCARE/VCWEEK |
| Run the monthly close cycle | CALL VETCARE/VCMONTH |
| Build/rebuild every object & program | CALL VETCARE/VCSETUP, then CALL VETCARE/VCREFLD |
The batch cycles and their component programs take no CALL parameters: each is a bare
CALL, so a scheduled submission is trivial. Only the three inquiry programs
(VCPATIQ, VCVLIQ, VCMENU) are interactive; the batch programs run
to completion and DSPLY a one-line result per step.
VCPATIQ and VCVLIQ display an animal or a visit but do not update anything. In
the tests, visits, lines and payments are written directly to the files (VCVISIT,
VCVSTL, VCPYMT) before the daily cycle rolls them forward. Operationally,
transaction rows arrive from whatever feeds these files (a data-entry front end, a load, or STRSQL); the
batch programs then close, invoice, recall and post them.A plain menu screen. Key an option in MOPT and press Enter:
1 program-to-program CALLs VCPATIQ, 2 CALLs VCVLIQ; any other
non-blank value shows Invalid option. and leaves the menu usable.
F3 ends the program.
Key an animal id in IANM and press Enter. The program CHAINs the
animal, resolves the owner name through the animal-to-owner lookup, and renders two derived
figures computed on the fly, not stored: the animal's next-due vaccine (the earliest
RCDUE among that animal's non-done open recalls) and the owner's live AR balance (the
sum of that owner's open invoice balances, walked through VCINVO). An unknown animal reports
Animal not found. and clears every detail field. F3 exits.
The app's subfile screen. Key a visit number in IVIS and press
Enter; the header renders the animal, visit date, status
(O/C/I), billed total and invoice number, and the
subfile (DDS record LSFL under control record LCTL,
SFLPAG(5)/SFLSIZ(20)) loads the visit's service/vaccine lines. It follows the
classic subfile discipline: clear (SFLCLR under indicator 31), load for the key just typed,
then display — so a miss shows an empty subfile and Visit not found. rather than stale
rows. Roll Up/Roll Down page the subfile;
F3 exits.
SDESC as blanks) —
the fee-schedule description is filled in only on the invoice lines (VCINVL.ILDESC)
and the printed statement. This is an honest note about the shipped screen, not a defect.A printer file (not a screen): the client invoice / statement. It carries an owner/invoice/animal
header block (INVOWN), one detail line per invoice line (INVDTL: service code,
description, quantity, amount), and a totals block (INVTOT: invoice total and balance due).
It is the printable, fixed record of a raised invoice.
Honest statement: VETCARE/i does not model a true four-eyes maker–checker / separate-authorization workflow. There is no "one user posts, a second user approves" step: the daily cycle closes, invoices, recalls and posts in one flow, and the interactive screens are inquiry-only. The control model the application does have is enforced in the posting programs and the ledger:
VCLEDG row with a stable key derived from a business key (never a per-run
counter). Before doing any work it CHAINs that key with (EN) and refuses if the row is
already present. This is both the durable audit trail and the guard that makes every cycle safe to
re-run. See F.3 for the disjoint key ranges.VCVSTCLS closes only VSTAT='O' visits (and only if they have lines);
VCINVGEN invoices only VSTAT='C'; VCRECGEN recalls only
VSTAT='I' vaccine lines; VCPYPOST posts only unposted
(PYSTAT='E') payments; VCRECRUN reminds only open
(RCSTAT='O') recalls.VCPYPOST caps the applied amount at the invoice's
outstanding balance: a payment larger than the balance is rejected outright
(PYSTAT='X', balance unchanged) rather than silently driving the balance negative. A
balance reaching zero moves the invoice to paid (ISTAT='P').NOLINES in the close program's DSPLY.VCGLDST re-derives the already-posted totals from the
distribution table itself before posting, so it feeds only the movement since the last
close and its debit/credit pairs always balance.In sum, the control posture is a durable ledger + state gating + a cap-and-reject rule on money, all enforced in the batch layer, rather than a segregation-of-duties approval workflow.
VETCARE/i's work runs as three periodic cycles, each a CL driver that CALLs its component programs in
a fixed order: a daily cycle (VCDAILY: close → invoice → recall → pay),
a weekly cycle (VCWEEK: the recall reminder run), and a monthly cycle
(VCMONTH: AR aging → GL distribution → close summary). All programs take
no CALL parameters. Where a program needs a "run date" it currently carries it as a literal in the
source (e.g. VCRECRUN uses run date 20260810 / run number 202632;
VCARAGE uses 20260831 / 202608; VCGLDST uses batch
202608) — a deliberately simple, deterministic model for the emulator, not a control
row. This is the SBMJOB idiom: a scheduled/JOBQ submission is a bare CALL.
-- submit the (parameterless) daily cycle as a batch job SBMJOB CMD(CALL PGM(VETCARE/VCDAILY)) JOB(VCDAILY) -- or run a single step interactively CALL VETCARE/VCVSTCLS
| Program | Cycle | Purpose | Reads | Writes | DSPLY result |
|---|---|---|---|---|---|
| VCVSTCLS | Daily 1 | Close every open visit with ≥1 line; roll lines into VSBILL. | VCVISIT, VCVSTL | VCVISIT (VSTAT='C'), VCLEDG ('V') | CLOSED=n NOLINES=n SKIP=n |
| VCINVGEN | Daily 2 | Raise one invoice per closed visit; copy lines down verbatim. | VCVISIT, VCVSTL, VCANML, VCSVC | VCINV, VCINVL, VCVISIT (VSTAT='I'), VCLEDG ('I') | GENERATED=n SKIP=n |
| VCRECGEN | Daily 3 | Generate/roll-forward the recall for each vaccine line of an invoiced visit. | VCVISIT, VCVSTL, VCSVC | VCRECALL, VCLEDG ('R') | GENERATED=n SKIP=n |
| VCPYPOST | Daily 4 | Apply unposted payments; cap overpayments; mark invoice paid at zero. | VCPYMT, VCINV | VCINV (IBAL/ISTAT), VCPYMT (PYSTAT), VCLEDG ('P') | POSTED=n REJECTED=n SKIP=n |
| VCRECRUN | Weekly | Sweep open recalls due/overdue at the run date; write worklist; mark reminded. | VCRECALL | VCRCWRK, VCRECALL (RCSTAT='R') | REMINDED=n SKIP=n |
| VCARAGE | Monthly 1 | Age each owner's open-invoice balances into current/30/60/90 buckets. | VCINV | VCAR, VCLEDG ('A') | OWNERS=n TOTAL=nnn |
| VCGLDST | Monthly 2 | Post balanced DR/CR pairs for revenue and cash movement (embedded SQL). | VCINV, VCPYMT, VCGLDIST | VCGLDIST | BATCH=nnn ROWS=n REVENUE=nnn |
| VCMCLOSE | Monthly 3 | ILE COBOL close summary: counts/totals by invoice status, grand outstanding. | VCINV | (report only) | BILLED / OUTSTAND / INVOICES / PAID / OPEN |
(1) VCVSTCLS reads every visit; for each VSTAT='O' visit it sums the visit's lines
(VLAMT) into VSBILL and sets VSTAT='C'. A lineless visit cannot
close (counted NOLINES). (2) VCINVGEN turns each closed visit into one invoice: the
invoice number is derived from the visit ('INV'+%subst(visit:4:6)), the total is copied from
VSBILL, the owner is pulled off the animal master, every visit line is copied down verbatim
into an invoice line with the fee-schedule description, and the visit moves to VSTAT='I'
carrying INVNO back. (3) VCRECGEN reads each invoiced visit's vaccine lines
(SVTYPE='V', SVRECDY>0) and, per (ANMID, SVCCD), either rolls
the existing recall forward or opens a new one with RCDUE = visit date + SVRECDY. (4)
VCPYPOST applies each unposted payment, capping at the invoice balance.
Expected DSPLY (4 lined visits + 1 empty; 3 vaccine lines; 3 payments, 1 overpay): VCVSTCLS CLOSED=4 NOLINES=1 SKIP=0 VCINVGEN GENERATED=4 SKIP=0 VCRECGEN GENERATED=3 SKIP=... 2x VACRAB rolled to one row + 1x VACBOR; BLOOD1 none VCPYPOST POSTED=2 REJECTED=1 SKIP=0 two paid off, one overpayment rejected
(AN00001, VACRAB) row, its RCLASTDT and RCDUE advanced to
the later visit (the daily test seeds visit 1 on 20260801 → recall due 20261166, then visit 2 on
20260805 rolls the same row to due 20261170, then a third dose on 20260901 rolls it to 20261266 —
still one row). A bordetella line uses its own 180-day interval (20260802 + 180 = 20260982), proving the
interval comes from the fee-schedule row, not a constant.VCRECRUN sweeps every open recall (RCSTAT='O') and, for those due at or before
the run date (RCDUE ≤ run date, including overdue), writes a VCRCWRK worklist
row and flips the recall to reminded (RCSTAT='R') so it is not re-flagged next week. A recall
strictly past the run date is flagged overdue (WKOVER='Y'); one due exactly today is
WKOVER='N' (due today is not yet overdue). Recalls not yet due, and already-reminded ones,
are skipped.
Expected DSPLY (run date 20260810; overdue + due-today + not-due + already-reminded): VCRECRUN REMINDED=2 SKIP=2 overdue(Y) + due-today(N) reminded; 2 skipped
(1) VCARAGE walks every open invoice (ISTAT='O'), buckets its balance by age
against the run date using 30/360 day arithmetic (see F.4) into current
(0–29), 30–59, 60–89 and 90+, and writes one VCAR row per owner with a
balance. Paid invoices contribute nothing. (2) VCGLDST totals invoiced revenue over all
invoices and applied cash over payments, then posts only the movement since the last close as two balanced
pairs: DR 1200-ARSVC / CR 4100-SVCREV for revenue, DR 1010-CASH /
CR 1200-ARSVC for cash collected. (3) VCMCLOSE (ILE COBOL) reports billed, outstanding,
and invoice counts by status.
Expected DSPLY (2 owners aged; 5 invoices incl. one paid; one applied payment): VCARAGE OWNERS=2 TOTAL=425.00 OW00001 225.00 + OW00002 200.00 VCGLDST BATCH=202608 ROWS=4 REVENUE=485.00 2 balanced pairs; DR/CR each 485.00 & 60.00 VCMCLOSE BILLED 485.00 grand billed over all invoices VCMCLOSE OUTSTAND 425.00 total open balances
YYYYMMDD integers precisely because a naive subtraction across a month
boundary would be wildly wrong (20260831 − 20260731 "=" 100, not 31).VSTAT='C'), and it must be
invoiced before its vaccine lines generate recalls (VCRECGEN acts only on
VSTAT='I'). Payment posting runs last so it applies against freshly generated invoices.VCRECGEN has generated; run the dailies first so the recall rows exist.Every field below is taken verbatim from the DDS/DDL in vet-app/src/sources.mjs. Numeric
types follow the DDS convention: nP m = packed decimal, n digits, m
decimal places; nS m = zoned decimal; nA = character; nY =
numeric-display. Dates are plain YYYYMMDD integers (see F.4). All physical
files are UNIQUE-keyed; the two logical files are non-unique by design.
| Field | Type | Meaning |
|---|---|---|
| OWNID | 7A | Owner id — the spine key (e.g. OW00001). |
| OWNAME | 30A | Owner name. |
| OWADDR | 30A | Address. |
| OWPHONE | 12A | Phone. |
| OWEMAIL | 30A | Email. |
| OWSTAT | 1A | Status: A=active, I=inactive. |
Key: OWNID.
| Field | Type | Meaning |
|---|---|---|
| ANMID | 7A | Animal id — the patient spine key (e.g. AN00001). |
| OWNID | 7A | Owner FK (one owner keeps many pets). |
| ANNAME | 20A | Animal name. |
| ANSPEC | 3A | Species: CAN/FEL/AVI/EXO/EQU… |
| ANBREED | 20A | Breed. |
| ANSEX | 1A | M/F. |
| ANFIXED | 1A | Y=spayed/neutered, N=intact. |
| ANDOB | 8S 0 | Birth date YYYYMMDD (0 if unknown). |
| ANWGT | 5P 1 | Weight. |
| ANSTAT | 1A | A=active, D=deceased (retained so the owner keeps history). |
Key: ANMID. Logical VCANMLO re-keys the same record on
(OWNID, ANMID) — the patients-by-owner path.
| Field | Type | Meaning |
|---|---|---|
| PROVID | 6A | Provider id (e.g. DR0001, TC0001). |
| PVNAME | 30A | Provider name. |
| PVTYPE | 1A | V=veterinarian, T=technician. |
| PVLIC | 12A | Licence number. |
| PVSTAT | 1A | A=active, I=inactive. |
Key: PROVID.
| Field | Type | Meaning |
|---|---|---|
| SVCCD | 6A | Service / vaccine code (e.g. EXAM01, VACRAB). |
| SVDESC | 30A | Description (copied down onto invoice lines). |
| SVTYPE | 1A | S=service, V=vaccine. |
| SVPRICE | 9P 2 | Unit price. |
| SVRECDY | 4S 0 | Recall interval in days for a vaccine (e.g. 365 rabies, 180 bordetella); 0 for a plain service, which never recalls. |
| SVSTAT | 1A | Status A/I. |
Key: SVCCD.
| Field | Type | Meaning |
|---|---|---|
| VISITNO | 9A | Visit number (e.g. VIS000301). |
| ANMID | 7A | Animal FK. |
| PROVID | 6A | Attending provider. |
| VSDT | 8S 0 | Visit date YYYYMMDD. |
| VSREASN | 30A | Reason for visit. |
| VSBILL | 9P 2 | Rolled-up billed total (written by VCVSTCLS at close). |
| VSTAT | 1A | Workflow: O=open, C=closed, I=invoiced. |
| INVNO | 9A | Invoice number carried back after invoicing. |
Key: VISITNO. Logical VCVISITO re-keys on (ANMID, VISITNO) —
the patient-history path both the patient inquiry and the recall run walk.
| Field | Type | Meaning |
|---|---|---|
| VISITNO | 9A | Visit FK (composite key part 1). |
| VLINE | 3S 0 | Line number (composite key part 2). |
| SVCCD | 6A | Fee-schedule code performed. |
| VLQTY | 5P 1 | Quantity. |
| VLAMT | 9P 2 | Priced extension = SVPRICE × VLQTY as of the line. |
| VLSTAT | 1A | Line status. |
Key: (VISITNO, VLINE).
| Field | Type | Meaning |
|---|---|---|
| ANMID | 7A | Animal (composite key part 1). |
| SVCCD | 6A | Vaccine code (composite key part 2) — one row per pair. |
| RCDUE | 8S 0 | Next-due date = visit date + SVRECDY. |
| RCLASTDT | 8S 0 | Date of the last dose (the visit date that generated/rolled this row). |
| RCSTAT | 1A | O=open (not yet reminded), R=reminded, D=done. |
Key: (ANMID, SVCCD). A second dose of the same vaccine rolls this one row forward
rather than adding a new one.
| Field | Type | Meaning |
|---|---|---|
| WKSEQ | 8S 0 | Stable per-(animal, vaccine, run) sequence key. |
| ANMID | 7A | Animal. |
| SVCCD | 6A | Vaccine. |
| WKRUN | 6S 0 | Run number (e.g. 202632). |
| WKDUE | 8S 0 | The recall due date at run time. |
| WKOVER | 1A | Y=overdue (due < run date), N=due today. |
| WKSTAT | 1A | Worklist status. |
Key: WKSEQ.
| Field | Type | Meaning |
|---|---|---|
| INVNO | 9A | Invoice number = 'INV'+%subst(visit:4:6). |
| VISITNO | 9A | Source visit. |
| OWNID | 7A | Owner (pulled off the animal master). |
| ANMID | 7A | Animal. |
| IDT | 8S 0 | Invoice date (= visit date). |
| ITOTAL | 9P 2 | Invoice total (= the visit's VSBILL). |
| IBAL | 9P 2 | Live balance, decremented by payments. |
| ISTAT | 1A | O=open (owed), P=paid in full, V=void. |
Key: INVNO. Logical VCINVO re-keys on (OWNID, INVNO) —
the AR-aging path.
| Field | Type | Meaning |
|---|---|---|
| INVNO | 9A | Invoice FK (composite key part 1). |
| ILINE | 3S 0 | Line number (composite key part 2). |
| SVCCD | 6A | Fee-schedule code. |
| ILDESC | 30A | Fee-schedule description snapshotted at generation (blank if the code is not found). |
| ILQTY | 5P 1 | Quantity. |
| ILAMT | 9P 2 | Line amount (copied from the visit line). |
Key: (INVNO, ILINE). The copy-down makes the invoice a fixed, printable record even if the
fee schedule later changes.
| Field | Type | Meaning |
|---|---|---|
| PAYNO | 9A | Payment number (its own unique business key). |
| INVNO | 9A | Invoice the payment is applied to. |
| OWNID | 7A | Owner. |
| PYDT | 8S 0 | Payment date. |
| PYAMT | 9P 2 | Amount tendered. |
| PYMTHD | 2A | CA=cash, CK=check, CC=card. |
| PYSTAT | 1A | E=entered (unposted), A=applied, X=rejected (overpayment). |
Key: PAYNO.
| Field | Type | Meaning |
|---|---|---|
| OWNID | 7A | Owner (composite key part 1). |
| ARRUN | 6S 0 | Aging run (e.g. 202608) (composite key part 2). |
| ARCUR | 9P 2 | Current bucket (0–29 days). |
| AR30 | 9P 2 | 30–59 days. |
| AR60 | 9P 2 | 60–89 days. |
| AR90 | 9P 2 | 90+ days. |
| ARTOT | 9P 2 | Row total (= sum of the four buckets). |
Key: (OWNID, ARRUN).
| Field | Type | Meaning |
|---|---|---|
| LSEQ | 8S 0 | Stable ledger key, disjoint numeric range per program (see F.3). |
| LTYPE | 1A | Program tag: V/I/R/P/W/A/G. |
| LKEY | 9A | The business key posted (visit, invoice, animal, payment, owner…). |
| LDT | 8S 0 | Effective date. |
| LAMT | 11P 2 | Primary amount (billed, applied, due date, aged total…). |
| LAMT2 | 11P 2 | Secondary amount (e.g. remaining balance after a payment). |
| LREF | 9A | Cross-reference (invoice, visit, owner…). |
| LMEMO | 30A | Free-text memo (VISIT CLOSED, INVOICE GENERATED…). |
Key: LSEQ.
| Column | Type | Meaning |
|---|---|---|
| GDSEQ | DECIMAL(8,0) | Sequence (primary key; allocated from MAX(GDSEQ)). |
| GDBATCH | DECIMAL(6,0) | Close batch (e.g. 202608). |
| GDACCT | CHAR(12) | Account (1200-ARSVC, 4100-SVCREV, 1010-CASH). |
| GDDRCR | CHAR(1) | D=debit, C=credit. |
| GDAMT | DECIMAL(9,2) | Posted amount. |
| GDCAT | CHAR(3) | Category (SVC, CSH). |
| GDREF | CHAR(9) | Reference (REVENUE, CASHRCPT). |
| GDDT | DECIMAL(8,0) | Posting date. |
Primary key GDSEQ; index VCGLDACC on (GDACCT, GDDRCR) so the
movement re-derivation (SUM(GDAMT) by account/side) is keyed.
This runbook walks the three cycles as an operator runs them, and gives the exact reconciling figures the
test drivers (test/vc_daily.mjs, vc_weekly.mjs, vc_monthly.mjs) assert.
Those figures are the reconciliation oracle: if a run's DSPLY lines do not match them for the seeded data,
something is wrong. The library list and one-time build are as in B.1: run
CALL VETCARE/VCSETUP then CALL VETCARE/VCREFLD once before anything else.
Transaction rows (visits, lines, payments) arrive into VCVISIT/VCVSTL/
VCPYMT from whatever feeds them (there is no on-line data-entry screen — see
B). Then run the daily cycle:
-- interactively CALL VETCARE/VCDAILY -- or as a batch job SBMJOB CMD(CALL PGM(VETCARE/VCDAILY)) JOB(VCDAILY)
The daily driver runs VCVSTCLS → VCINVGEN → VCRECGEN → VCPYPOST in that fixed
order (see C.3). For the seeded daily scenario — four visits with lines plus one
empty visit; a rabies dose on two visits for the same animal plus one bordetella dose; three payments one of
which overpays — the expected DSPLY is:
VCVSTCLS CLOSED=4 NOLINES=1 SKIP=0 VCINVGEN GENERATED=4 SKIP=0 VCRECGEN GENERATED=3 SKIP=... VCPYPOST POSTED=2 REJECTED=1 SKIP=0
What each figure reconciles to (all hand-derived in vc_daily.mjs):
| Object | Figure | Why |
|---|---|---|
| INV000001 | total 93.00, balance 0.00 | EXAM01 65.00 + VACRAB 28.00; PAY000001 pays 93.00 → paid off, ISTAT='P'. |
| INV000002 | total 28.00, balance 28.00 | VACRAB 28.00; PAY000002 tenders 500.00 > balance — overpayment REJECTED, balance unchanged, payment marked X. |
| INV000003 | total 24.00, balance 0.00 | VACBOR 24.00; PAY000003 pays 24.00 exactly. |
| INV000004 | total 145.00, balance 145.00 | BLOOD1 145.00, no payment; contributes to owner AR. |
The overpayment case is the load-bearing control: a payment larger than the balance is rejected outright
(never a negative balance). The invoice-line copy-down invariant is asserted too — the invoice lines of
INV000001 sum exactly to its total.
(AN00001, VACRAB) recall row, its RCDUE/RCLASTDT advanced to the later
visit; the bordetella dose uses its own 180-day interval from VCSVC, not a constant. That is
why VCRECGEN reports GENERATED=3, not one row per vaccine line.CALL VETCARE/VCWEEK runs the single program VCRECRUN against run date
20260810 / run number 202632 (literals in the source, see C). It
sweeps open recalls (RCSTAT='O') due at or before the run date, writes a VCRCWRK
worklist row per hit, and flips each hit to reminded (RCSTAT='R'). For the seeded weekly
scenario — one overdue, one due-today, one not-yet-due, one already-reminded — the expected DSPLY
is:
VCRECRUN REMINDED=2 SKIP=2
The overdue recall (WKOVER='Y') and the due-today recall (WKOVER='N' — due
today is not yet overdue) are reminded; the not-yet-due and the already-reminded rows are skipped. A re-run
of the same week finds every worklist key already on file and reports REMINDED=0
(idempotent by the stable per-(animal, vaccine, run) key).
CALL VETCARE/VCMONTH runs VCARAGE → VCGLDST → VCMCLOSE against run
month 202608 / run date 20260831. For the seeded monthly scenario — five
invoices, four open across two owners and one (INV000005, 60.00) fully paid, plus the one applied 60.00
payment — the expected DSPLY is:
VCARAGE OWNERS=2 TOTAL=425.00 VCGLDST BATCH=202608 ROWS=4 REVENUE=485.00 VCMCLOSE BILLED 485.00 VCMCLOSE OUTSTAND 425.00
The reconciliation, straight from vc_monthly.mjs:
| Line | Figure | Derivation |
|---|---|---|
| VCARAGE OWNERS | 2 | Only owners with an open balance get a VCAR row; the paid invoice is excluded from aging entirely. |
| VCARAGE TOTAL | 425.00 | OW00001 225.00 (INV000001 100.00 current + INV000002 50.00 30–59 + INV000003 75.00 90+) + OW00002 200.00 (INV000004 60–89). |
| VCGLDST REVENUE | 485.00 | Sum of ITOTAL over all 5 invoices (100+50+75+200+60), incl. the paid one. |
| VCGLDST ROWS | 4 | Two balanced pairs: revenue DR 1200-ARSVC 485.00 / CR 4100-SVCREV 485.00, and cash DR 1010-CASH 60.00 / CR 1200-ARSVC 60.00. |
| VCMCLOSE BILLED | 485.00 | Grand billed (ITOTAL) over all invoices. |
| VCMCLOSE OUTSTAND | 425.00 | Total open balances (485.00 billed − 60.00 paid). |
VCGLDST re-derives what it
has already posted from VCGLDIST and posts only the movement since. If the drivers then seed a
further 40.00 open invoice (INV000006, OW00001) and re-run, the second GL run posts just that movement
— ROWS=2 REVENUE=525.00 — and the COBOL close then reports
BILLED 525.00 / OUTSTAND 465.00. A re-run with no new movement posts
nothing. An aging re-run over data with no open invoices reports OWNERS=0.VCLEDG (or, for the weekly
run, VCRCWRK) key with (EN) before doing any work and skips anything already
posted — a re-run of already-processed rows shows the same net counts with the redundant rows
counted as SKIP and no double-posting.20260810/202632, monthly
20260831/202608). To process a different period the source literal is
changed and the program recompiled; there is no control row or CALL parameter.All source is held as JS string constants in vet-app/src/sources.mjs and loaded into library
VETCARE as source members by src/seed.mjs (seedVetcare()), mirroring
the ibmi/samples.js DEMOLIB convention. The RPG is fixed-form C-specs and D-specs emitted by two
column-exact helpers (C(), D()) interleaved with /free blocks;
VCMCLOSE is hand-written ILE COBOL; the GL table is SQL DDL; the cycles and build are CL.
VCSETUP creates every object (13 PFs, 2 LFs, 3 DSPFs, 1 PRTF, 1 SQL table + index) and compiles
every program — note it uses CRTBNDRPG for the SQLRPGLE program VCGLDST (a
platform quirk: CRTBNDRPG, never CRTSQLRPGI).
Files: VCVISIT (UF), VCVSTL (IF), VCLEDG (UF A). Reads every visit
*LOVAL-forward; for a VSTAT='O' visit it sums the visit's VLAMT lines
into WSUM, then re-CHAINs the header (the line READEs moved the cursor), sets
VSBILL=WSUM, VSTAT='C', and writes the 'V' ledger row. A lineless
visit is counted NOLINES and left open. Ledger key 10000000 + visit digits.
Files add VCANML/VCSVC (IF) and VCINV/VCINVL (UF A).
For each VSTAT='C' visit: derive WINV = 'INV'+%subst(visit:4:6), pull the owner off
VCANML, write the VCINV header (ITOTAL=IBAL=VSBILL,
ISTAT='O'), copy every visit line down into a VCINVL line snapshotting
SVDESC (blank if the code is not found), then move the visit to VSTAT='I' carrying
INVNO back and write the 'I' ledger row. Ledger key
20000000 + visit digits.
Files: VCVISIT/VCVSTL/VCSVC (IF), VCRECALL/
VCLEDG (UF A). For each VSTAT='I' visit it reads the visit's lines; a line whose
VCSVC row is SVTYPE='V' with SVRECDY>0 yields
WDUE = visit date + SVRECDY. It CHAINs the composite (ANMID, SVCCD) recall key: if
present it rolls the row forward (RCLASTDT/RCDUE advanced), else it writes a new
open recall. A SETGT restores the line cursor after the recall I/O. Ledger key
30000000 + (visit digits mod 100000)×10 + (VLINE mod 10), bounded to fit the 8S0 key.
Files: VCPYMT/VCINV (UF), VCLEDG (UF A). For each
PYSTAT='E' payment it CHAINs the invoice, then caps the applied amount at the balance:
WAMT > WBAL → WAPPLY=0 (rejected, PYSTAT='X', balance
unchanged, counted REJECTED); otherwise it decrements IBAL, marks the payment
applied (PYSTAT='A'), and moves the invoice to ISTAT='P' when the balance hits
zero. The ledger row carries the applied amount (LAMT) and the remaining balance
(LAMT2). Ledger key 40000000 + payment digits.
Files: VCRECALL (UF), VCRCWRK (UF A). Run date/number are literals
(20260810/202632). Skips non-open recalls and those with RCDUE > run
date; for a hit it writes a VCRCWRK row (WKOVER='Y' if RCDUE < run
date, else 'N') and flips the recall to RCSTAT='R'. The re-runnability guard is on
the worklist file itself (key 50000000 + animal digits×1000 + vaccine digits), not
VCLEDG — the only posting program that guards on its own output file.
Files: VCINV (IF), VCAR/VCLEDG (UF A). Run month/date literals
202608/20260831. It sweeps VCINV keyed on INVNO and
accumulates per-owner buckets, posting the previous owner's row via the POST subroutine on an
owner break and once more at end. Each open invoice is aged in 30/360 days against the run date (see
F.4) into current/30/60/90. Paid invoices contribute nothing. Ledger key
60000000 + owner digits×100 + (run − 202600).
VCINV and VCAR share the field name
OWNID (both are owner-keyed), so AGE1 captures the invoice's fields into work
variables before calling POST — because POST WRITEs a
VCAR record and would otherwise overwrite the shared OWNID buffer mid-owner. This is
a deliberate, commented ordering in the source.Files: VCINV/VCPYMT (IF); embedded SQL against VETCARE.VCGLDIST.
Batch literal 202608. It totals invoiced revenue (SUM(ITOTAL) over all invoices)
and applied cash (PYSTAT='A' payments), re-derives what it has already posted
(SELECT COALESCE(SUM(GDAMT),0) for the revenue-credit and cash-debit sides), and posts only the
positive movement as two balanced pairs via the GDROW subroutine, allocating
GDSEQ = MAX(GDSEQ)+1. Because it re-derives before posting, a re-run with no new movement writes
nothing.
Sequentially reads the indexed VCINV file, accumulating count and INV-TOTAL,
plus paid-count (P) and open-count/outstanding-balance (O), then DISPLAYs five
VCMCLOSE lines: BILLED, OUTSTAND, INVOICES,
PAID, OPEN. Report-only; it writes no files.
Files: VCPATD (CF WORKSTN), VCANML/VCOWNR/VCRECALL/
VCINVO (IF). On an animal id it CHAINs the animal, resolves the owner name, then derives
two live figures: the earliest RCDUE among the animal's non-done open recalls (walking
VCRECALL by animal), and the owner's live AR balance = sum of open (ISTAT='O')
invoice balances walked through VCINVO. Neither is stored. An unknown animal clears every field
and shows Animal not found..
Files: VCVLD (CF WORKSTN, SFILE(LSFL:RRN)), VCVISIT/
VCVSTL (IF). Classic subfile discipline: set *IN31 and WRITE the control record to
clear (SFLCLR), zero the RRN, then on a found visit load the lines into LSFL and
EXFMT the control record. It stays fixed-form around the SETLL/READE load loop on purpose so a free-form
IF cannot let the loop run on a miss (which would show stale rows). The subfile leaves
SDESC blank — descriptions live only on invoice lines and the printed statement.
EXFMTs the menu; MOPT='1' program-CALLs 'VCPATIQ', '2' CALLs
'VCVLIQ', any other non-blank shows Invalid option.. F3
(*IN03) ends the loop.
Every posting program derives a stable guard key from a business key (never a per-run counter),
CHAINs it with (EN) before doing any work, and refuses if the row is already present. The ranges
are disjoint so the keys never collide across programs:
| Program | Tag | Guard file | Key formula |
|---|---|---|---|
| VCVSTCLS | V | VCLEDG | 10000000 + visit digits |
| VCINVGEN | I | VCLEDG | 20000000 + visit digits |
| VCRECGEN | R | VCLEDG | 30000000 + (visit digits mod 100000)×10 + (VLINE mod 10) |
| VCPYPOST | P | VCLEDG | 40000000 + payment digits |
| VCRECRUN | W | VCRCWRK | 50000000 + animal digits×1000 + vaccine digits |
| VCARAGE | A | VCLEDG | 60000000 + owner digits×100 + (run − 202600) |
| VCGLDST | G | VCGLDIST | MAX(GDSEQ)+1; movement re-derived from the table before posting |
Note VCRECRUN and VCGLDST guard on their own output file rather than
VCLEDG: the weekly run cannot re-flag the same recall for the same run, and the GL feed cannot
re-post the same movement.
Every date in the app is a plain YYYYMMDD integer. Two conventions matter:
RCDUE = visit date + SVRECDY is a raw integer
addition (e.g. 20260801 + 365 = 20261166). It is a deterministic, monotonic due-date token
for the emulator, not a real calendar date — the weekly run compares it by integer ordering, which
is all it needs.VCARAGE converts each date to a day-count
(YYYY)×360 + (MM)×30 + DD and ages on the difference. This is deliberate: a
naive subtraction of two YYYYMMDD integers across a month boundary is wildly wrong
(20260831 − 20260731 "=" 100, not 31). Buckets are current (<30), 30–59,
60–89, 90+; a negative age is clamped to 0.VCGLDST re-derives its
already-posted revenue and cash from VCGLDIST and posts only the delta as balanced DR/CR
pairs: revenue DR 1200-ARSVC / CR 4100-SVCREV, cash DR 1010-CASH
/ CR 1200-ARSVC. Each pair posts two VCGLDIST rows of equal amount and
opposite side, so the distribution always balances.SVTYPE='S') and vaccines
(SVTYPE='V'). A vaccine's SVRECDY drives its recall interval.(EN) before working and skips anything already posted, so a re-run does not double-post.VCPYPOST rule that a payment larger than the invoice balance is rejected outright
(PYSTAT='X', balance unchanged) rather than driving the balance negative.VCANML, the patient) and its client (VCOWNR, the owner). One owner
keeps many pets; a deceased patient is retained so the owner keeps history.(animal, vaccine) holding that animal's next-due date for that vaccine
(RCDUE = visit date + SVRECDY). A repeat dose rolls the same row forward rather than
duplicating it.RCDUE/RCLASTDT instead of creating a new row — a due-date applied
to a composite-key accumulator.VCRCWRK worklist
and marking each hit reminded so it is not re-flagged next week.SBMJOB CMD(CALL PGM(VETCARE/VCDAILY))). The parameterless cycles make a scheduled
submission trivial.SFL/SFLCTL).
VCVLIQ's visit-line list is the app's subfile (SFLPAG(5)/SFLSIZ(20)).VSBILL), and becomes exactly one invoice. Workflow O→C→I.YYYY×360 + MM×30 + DD, so ages are
meaningful across month boundaries (a naive YYYYMMDD subtraction is not).