Case Study 1: General Ledger Posting System

Background

Pacific Northwest Healthcare Partners (PNHP) is a regional healthcare network operating six hospitals, twenty-three outpatient clinics, and four long-term care facilities across Oregon and Washington. With annual revenues exceeding $3.2 billion and more than 18,000 employees, PNHP's financial operations are complex, spanning patient billing, insurance reimbursement, physician compensation, pharmaceutical procurement, capital equipment management, and regulatory compliance with both federal healthcare standards and state financial reporting requirements.

PNHP's financial systems run on an IBM z16 mainframe. The General Ledger system, implemented entirely in COBOL, serves as the single authoritative source for all financial data. Every dollar that flows through the organization -- from a $12 co-pay at a walk-in clinic to a $45 million bond issuance for a new surgical wing -- is ultimately recorded in the GL through journal entries.

The GL system processes journal entries from seven feeder systems (subledgers): Accounts Payable, Accounts Receivable (patient billing), Payroll, Fixed Assets, Inventory, Cash Management, and Revenue Cycle Management. Each subledger generates summary journal entries that are batched and transmitted to the GL for posting during the nightly batch cycle. In addition, the finance team creates approximately 200-400 manual journal entries per month for accruals, reclassifications, corrections, and non-routine transactions.

On an average day, the GL posting program processes approximately 8,500 journal entry lines across 1,200 journal entries, updating balances for 4,800 active GL accounts organized across six company codes (one for each hospital entity). The entire posting run must complete within a 90-minute batch window to allow subsequent programs (trial balance generation, financial statement production, regulatory reporting) to run before the 6:00 a.m. start of the next business day.

Problem Statement

PNHP's existing GL posting program was written in 1998 and has served reliably for over two decades. However, a series of events has exposed limitations that require a rewrite:

  1. SOX compliance gap: During the most recent external audit, the auditors flagged that the posting program does not enforce segregation of duties. The same user who creates a journal entry can also post it, violating a fundamental SOX internal control.

  2. Missing validation: The program validates that each entry balances (debits equal credits) but does not verify that the referenced accounts exist in the chart of accounts, are active, or are postable. This has resulted in entries being posted to inactive accounts, requiring manual corrections.

  3. Insufficient audit trail: The program updates GL balances but does not write a detailed audit trail record for each posted line. Auditors must reconstruct the posting history from the journal entry file, which is time-consuming and error-prone.

  4. No period validation: The program does not check whether the target period is open or closed. On two occasions in the past year, entries were accidentally posted to closed periods, corrupting previously finalized financial statements.

The new program, GLPOST, must address all four deficiencies while maintaining the performance characteristics needed to complete within the 90-minute batch window.

System Design

The GLPOST program follows a sequential processing model that reads journal entries in order, validates each entry against a comprehensive set of rules, posts valid entries to the GL master file, writes rejected entries to an error file, generates a complete audit trail, and produces a posting summary report.

The validation pipeline for each journal entry consists of six checks: 1. Balance check: Total debits must equal total credits. 2. Account validation: Every referenced account must exist, be active, and be postable. 3. Period validation: The target posting period must be open. 4. SOX controls: Segregation of duties and approval thresholds must be satisfied. 5. Amount validation: No zero-amount lines are permitted. 6. Cross-reference validation: The journal code must be valid for the originating subledger.

Complete COBOL Implementation

       IDENTIFICATION DIVISION.
       PROGRAM-ID. GLPOST.
      *================================================================*
      * GENERAL LEDGER POSTING SYSTEM                                  *
      * PACIFIC NORTHWEST HEALTHCARE PARTNERS                          *
      *                                                                *
      * READS JOURNAL ENTRIES, VALIDATES, POSTS TO GL MASTER,          *
      * GENERATES AUDIT TRAIL AND POSTING REPORT.                      *
      *================================================================*

       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT JOURNAL-INPUT-FILE
               ASSIGN TO JEINPUT
               ORGANIZATION IS SEQUENTIAL
               FILE STATUS IS WS-JE-FS.

           SELECT GL-MASTER-FILE
               ASSIGN TO GLMAST
               ORGANIZATION IS INDEXED
               ACCESS MODE IS RANDOM
               RECORD KEY IS GL-FULL-KEY
               FILE STATUS IS WS-GL-FS.

           SELECT COA-FILE
               ASSIGN TO COAFILE
               ORGANIZATION IS INDEXED
               ACCESS MODE IS RANDOM
               RECORD KEY IS COA-ACCOUNT-KEY
               FILE STATUS IS WS-COA-FS.

           SELECT PERIOD-CONTROL-FILE
               ASSIGN TO PRDCTRL
               ORGANIZATION IS INDEXED
               ACCESS MODE IS RANDOM
               RECORD KEY IS PC-KEY
               FILE STATUS IS WS-PC-FS.

           SELECT AUDIT-TRAIL-FILE
               ASSIGN TO AUDTRL
               ORGANIZATION IS SEQUENTIAL
               FILE STATUS IS WS-AT-FS.

           SELECT REJECT-FILE
               ASSIGN TO REJECTS
               ORGANIZATION IS SEQUENTIAL
               FILE STATUS IS WS-RJ-FS.

           SELECT REPORT-FILE
               ASSIGN TO POSTRPT
               ORGANIZATION IS SEQUENTIAL
               FILE STATUS IS WS-RPT-FS.

       DATA DIVISION.
       FILE SECTION.

       FD  JOURNAL-INPUT-FILE
           RECORDING MODE IS F
           RECORD CONTAINS 300 CHARACTERS.
       01  JE-INPUT-RECORD             PIC X(300).

       FD  GL-MASTER-FILE
           RECORD CONTAINS 600 CHARACTERS.
       01  GL-MASTER-RECORD.
           05  GL-FULL-KEY.
               10  GL-COMPANY          PIC X(04).
               10  GL-ACCOUNT-NUM      PIC X(10).
               10  GL-FISCAL-YEAR      PIC 9(04).
           05  GL-DESCRIPTION          PIC X(40).
           05  GL-ACCOUNT-TYPE         PIC X(01).
           05  GL-NORMAL-BALANCE       PIC X(01).
           05  GL-CURRENCY-CODE        PIC X(03).
           05  GL-BEG-BALANCE          PIC S9(13)V99
                                       COMP-3.
           05  GL-PERIOD-TABLE.
               10  GL-PERIOD-ENTRY OCCURS 13 TIMES
                                   INDEXED BY GL-PRD-IDX.
                   15  GL-PRD-DEBITS   PIC S9(13)V99
                                       COMP-3.
                   15  GL-PRD-CREDITS  PIC S9(13)V99
                                       COMP-3.
                   15  GL-PRD-NET      PIC S9(13)V99
                                       COMP-3.
                   15  GL-PRD-BUDGET   PIC S9(13)V99
                                       COMP-3.
           05  GL-YTD-DEBITS           PIC S9(13)V99
                                       COMP-3.
           05  GL-YTD-CREDITS          PIC S9(13)V99
                                       COMP-3.
           05  GL-YTD-NET              PIC S9(13)V99
                                       COMP-3.
           05  GL-END-BALANCE          PIC S9(13)V99
                                       COMP-3.
           05  GL-LAST-POST-DATE       PIC 9(08).
           05  GL-LAST-POST-PERIOD     PIC 9(02).
           05  GL-ENTRY-COUNT-YTD      PIC 9(06).
           05  GL-FILLER               PIC X(50).

       FD  COA-FILE
           RECORD CONTAINS 150 CHARACTERS.
       01  COA-RECORD.
           05  COA-ACCOUNT-KEY.
               10  COA-COMPANY         PIC X(04).
               10  COA-NATURAL-ACCT    PIC X(04).
               10  COA-COST-CENTER     PIC X(04).
               10  COA-SUB-ACCOUNT     PIC X(02).
           05  COA-DESCRIPTION         PIC X(40).
           05  COA-ACCOUNT-TYPE        PIC X(01).
               88  COA-ASSET           VALUE 'A'.
               88  COA-LIABILITY       VALUE 'L'.
               88  COA-EQUITY          VALUE 'E'.
               88  COA-REVENUE         VALUE 'R'.
               88  COA-EXPENSE         VALUE 'X'.
           05  COA-NORMAL-BALANCE      PIC X(01).
           05  COA-STATUS              PIC X(01).
               88  COA-ACTIVE          VALUE 'A'.
               88  COA-INACTIVE        VALUE 'I'.
           05  COA-POSTING-ALLOWED     PIC X(01).
               88  COA-POSTABLE        VALUE 'Y'.
               88  COA-SUMMARY-ONLY    VALUE 'N'.
           05  COA-FILLER              PIC X(79).

       FD  PERIOD-CONTROL-FILE
           RECORD CONTAINS 50 CHARACTERS.
       01  PERIOD-CONTROL-RECORD.
           05  PC-KEY.
               10  PC-COMPANY          PIC X(04).
               10  PC-FISCAL-YEAR      PIC 9(04).
               10  PC-PERIOD           PIC 9(02).
           05  PC-STATUS               PIC X(01).
               88  PC-OPEN             VALUE 'O'.
               88  PC-CLOSED           VALUE 'C'.
               88  PC-LOCKED           VALUE 'L'.
           05  PC-FILLER               PIC X(39).

       FD  AUDIT-TRAIL-FILE
           RECORDING MODE IS F
           RECORD CONTAINS 250 CHARACTERS.
       01  AUDIT-TRAIL-RECORD          PIC X(250).

       FD  REJECT-FILE
           RECORDING MODE IS F
           RECORD CONTAINS 350 CHARACTERS.
       01  REJECT-RECORD               PIC X(350).

       FD  REPORT-FILE
           RECORDING MODE IS F
           RECORD CONTAINS 132 CHARACTERS.
       01  REPORT-RECORD               PIC X(132).

       WORKING-STORAGE SECTION.

       01  WS-FILE-STATUSES.
           05  WS-JE-FS               PIC XX.
           05  WS-GL-FS               PIC XX.
           05  WS-COA-FS              PIC XX.
           05  WS-PC-FS               PIC XX.
           05  WS-AT-FS               PIC XX.
           05  WS-RJ-FS               PIC XX.
           05  WS-RPT-FS              PIC XX.

       01  WS-FLAGS.
           05  WS-EOF-FLAG            PIC X VALUE 'N'.
               88  WS-EOF             VALUE 'Y'.
           05  WS-VALID-FLAG          PIC X VALUE 'Y'.
               88  WS-ENTRY-VALID     VALUE 'Y'.
               88  WS-ENTRY-INVALID   VALUE 'N'.
           05  WS-NEW-RECORD-FLAG     PIC X VALUE 'N'.
               88  WS-NEW-GL-RECORD   VALUE 'Y'.

       01  WS-CURRENT-TIMESTAMP        PIC X(26).
       01  WS-CURRENT-DATE             PIC 9(08).
       01  WS-PROGRAM-NAME             PIC X(08) VALUE 'GLPOST'.

      *---------------------------------------------------------------*
      *  JOURNAL ENTRY WORKING STORAGE                                *
      *---------------------------------------------------------------*
       01  WS-JE-HEADER.
           05  WS-JH-REC-TYPE         PIC X(01).
           05  WS-JH-ENTRY-NUM        PIC 9(10).
           05  WS-JH-COMPANY          PIC X(04).
           05  WS-JH-JOURNAL-CODE     PIC X(03).
           05  WS-JH-ENTRY-DATE       PIC 9(08).
           05  WS-JH-PERIOD           PIC 9(02).
           05  WS-JH-FISCAL-YEAR      PIC 9(04).
           05  WS-JH-DESCRIPTION      PIC X(40).
           05  WS-JH-SOURCE-REF       PIC X(20).
           05  WS-JH-ENTRY-TYPE       PIC X(01).
           05  WS-JH-STATUS           PIC X(01).
           05  WS-JH-LINE-COUNT       PIC 9(04).
           05  WS-JH-TOTAL-DEBITS     PIC S9(13)V99.
           05  WS-JH-TOTAL-CREDITS    PIC S9(13)V99.
           05  WS-JH-ENTERED-BY       PIC X(08).
           05  WS-JH-APPROVED-BY      PIC X(08).
           05  WS-JH-FILLER           PIC X(130).

       01  WS-JE-LINE.
           05  WS-JL-REC-TYPE         PIC X(01).
           05  WS-JL-ENTRY-NUM        PIC 9(10).
           05  WS-JL-LINE-NUM         PIC 9(04).
           05  WS-JL-ACCOUNT          PIC X(14).
           05  WS-JL-DC-IND           PIC X(01).
               88  WS-JL-DEBIT        VALUE 'D'.
               88  WS-JL-CREDIT       VALUE 'C'.
           05  WS-JL-AMOUNT           PIC S9(13)V99.
           05  WS-JL-DESCRIPTION      PIC X(30).
           05  WS-JL-REFERENCE        PIC X(20).
           05  WS-JL-FILLER           PIC X(205).

      *---------------------------------------------------------------*
      *  LINE ACCUMULATOR TABLE                                       *
      *---------------------------------------------------------------*
       01  WS-LINE-TABLE.
           05  WS-LINE-ENTRY OCCURS 100 TIMES.
               10  WS-LT-ACCOUNT      PIC X(14).
               10  WS-LT-DC-IND       PIC X(01).
               10  WS-LT-AMOUNT       PIC S9(13)V99.
               10  WS-LT-DESC         PIC X(30).
               10  WS-LT-REF          PIC X(20).
       01  WS-LINE-COUNT              PIC 9(04) VALUE ZEROS.

      *---------------------------------------------------------------*
      *  PROCESSING COUNTERS                                          *
      *---------------------------------------------------------------*
       01  WS-COUNTERS.
           05  WS-ENTRIES-READ        PIC 9(06) VALUE ZEROS.
           05  WS-ENTRIES-POSTED      PIC 9(06) VALUE ZEROS.
           05  WS-ENTRIES-REJECTED    PIC 9(06) VALUE ZEROS.
           05  WS-LINES-POSTED        PIC 9(08) VALUE ZEROS.
           05  WS-TOTAL-DR-POSTED     PIC S9(15)V99
                                      COMP-3 VALUE ZEROS.
           05  WS-TOTAL-CR-POSTED     PIC S9(15)V99
                                      COMP-3 VALUE ZEROS.
           05  WS-ERRORS-THIS-ENTRY   PIC 9(03) VALUE ZEROS.

      *---------------------------------------------------------------*
      *  SOX CONTROL THRESHOLDS                                       *
      *---------------------------------------------------------------*
       01  WS-SOX-APPROVAL-LIMIT      PIC S9(13)V99
                                      VALUE 50000.00.

      *---------------------------------------------------------------*
      *  AUDIT TRAIL WORK RECORD                                      *
      *---------------------------------------------------------------*
       01  WS-AUDIT-RECORD.
           05  WS-AR-TIMESTAMP        PIC X(26).
           05  WS-AR-PROGRAM          PIC X(08).
           05  WS-AR-ENTRY-NUM        PIC 9(10).
           05  WS-AR-LINE-NUM         PIC 9(04).
           05  WS-AR-ACCOUNT          PIC X(14).
           05  WS-AR-DC-IND           PIC X(01).
           05  WS-AR-AMOUNT           PIC S9(13)V99.
           05  WS-AR-PERIOD           PIC 9(02).
           05  WS-AR-FISCAL-YEAR      PIC 9(04).
           05  WS-AR-COMPANY          PIC X(04).
           05  WS-AR-JOURNAL-CODE     PIC X(03).
           05  WS-AR-ENTERED-BY       PIC X(08).
           05  WS-AR-APPROVED-BY      PIC X(08).
           05  WS-AR-SOURCE-REF       PIC X(20).
           05  WS-AR-DESCRIPTION      PIC X(30).
           05  WS-AR-RESULT           PIC X(08).
           05  WS-AR-FILLER           PIC X(90).

      *---------------------------------------------------------------*
      *  REJECT WORK RECORD                                           *
      *---------------------------------------------------------------*
       01  WS-REJECT-RECORD.
           05  WS-RR-ENTRY-NUM        PIC 9(10).
           05  WS-RR-LINE-NUM         PIC 9(04).
           05  WS-RR-REASON           PIC X(50).
           05  WS-RR-ORIGINAL-REC     PIC X(286).

      *---------------------------------------------------------------*
      *  REPORT WORK AREAS                                            *
      *---------------------------------------------------------------*
       01  WS-RPT-HEADER.
           05  FILLER          PIC X(01) VALUE SPACES.
           05  FILLER          PIC X(45)
               VALUE 'PACIFIC NORTHWEST HEALTHCARE PARTNERS'.
           05  FILLER          PIC X(40)
               VALUE 'GL POSTING REPORT'.
           05  FILLER          PIC X(06) VALUE 'DATE: '.
           05  WS-RH-DATE     PIC X(10).
           05  FILLER          PIC X(30) VALUE SPACES.

       01  WS-RPT-DETAIL.
           05  FILLER          PIC X(02) VALUE SPACES.
           05  WS-RD-ENTRY    PIC 9(10).
           05  FILLER          PIC X(01) VALUE SPACES.
           05  WS-RD-LINE     PIC 9(04).
           05  FILLER          PIC X(01) VALUE SPACES.
           05  WS-RD-ACCT     PIC X(14).
           05  FILLER          PIC X(01) VALUE SPACES.
           05  WS-RD-DC       PIC X(01).
           05  FILLER          PIC X(01) VALUE SPACES.
           05  WS-RD-AMT      PIC $$$,$$$,$$$,$$9.99.
           05  FILLER          PIC X(01) VALUE SPACES.
           05  WS-RD-DESC     PIC X(30).
           05  FILLER          PIC X(01) VALUE SPACES.
           05  WS-RD-RESULT   PIC X(08).
           05  FILLER          PIC X(37) VALUE SPACES.

       01  WS-IDX                      PIC 9(04).
       01  WS-CALC-BALANCE             PIC S9(15)V99 COMP-3.

       PROCEDURE DIVISION.
      *================================================================*
       0000-MAIN-CONTROL.
      *================================================================*
           PERFORM 0100-INITIALIZE
           PERFORM 1000-PROCESS-ENTRIES UNTIL WS-EOF
           PERFORM 9000-FINALIZE
           STOP RUN
           .

      *================================================================*
       0100-INITIALIZE.
      *================================================================*
           MOVE FUNCTION CURRENT-DATE(1:26)
               TO WS-CURRENT-TIMESTAMP
           MOVE FUNCTION CURRENT-DATE(1:8)
               TO WS-CURRENT-DATE
           MOVE WS-CURRENT-DATE TO WS-RH-DATE

           OPEN INPUT  JOURNAL-INPUT-FILE
           OPEN I-O    GL-MASTER-FILE
           OPEN INPUT  COA-FILE
           OPEN INPUT  PERIOD-CONTROL-FILE
           OPEN OUTPUT AUDIT-TRAIL-FILE
           OPEN OUTPUT REJECT-FILE
           OPEN OUTPUT REPORT-FILE

           WRITE REPORT-RECORD FROM WS-RPT-HEADER
               AFTER ADVANCING PAGE
           MOVE SPACES TO REPORT-RECORD
           WRITE REPORT-RECORD AFTER ADVANCING 1 LINE

           PERFORM 1100-READ-NEXT-RECORD
           .

      *================================================================*
       1000-PROCESS-ENTRIES.
      *================================================================*
      *    READ HEADER RECORD
           IF JE-INPUT-RECORD(1:1) = 'H'
               MOVE JE-INPUT-RECORD TO WS-JE-HEADER
               ADD 1 TO WS-ENTRIES-READ
               MOVE ZEROS TO WS-LINE-COUNT
                              WS-ERRORS-THIS-ENTRY

      *        READ ALL LINES FOR THIS ENTRY
               PERFORM 1100-READ-NEXT-RECORD
               PERFORM UNTIL WS-EOF
                          OR JE-INPUT-RECORD(1:1) = 'H'
                   IF JE-INPUT-RECORD(1:1) = 'L'
                       MOVE JE-INPUT-RECORD TO WS-JE-LINE
                       ADD 1 TO WS-LINE-COUNT
                       IF WS-LINE-COUNT <= 100
                           MOVE WS-JL-ACCOUNT
                               TO WS-LT-ACCOUNT(WS-LINE-COUNT)
                           MOVE WS-JL-DC-IND
                               TO WS-LT-DC-IND(WS-LINE-COUNT)
                           MOVE WS-JL-AMOUNT
                               TO WS-LT-AMOUNT(WS-LINE-COUNT)
                           MOVE WS-JL-DESCRIPTION
                               TO WS-LT-DESC(WS-LINE-COUNT)
                           MOVE WS-JL-REFERENCE
                               TO WS-LT-REF(WS-LINE-COUNT)
                       END-IF
                   END-IF
                   PERFORM 1100-READ-NEXT-RECORD
               END-PERFORM

      *        VALIDATE AND POST THE COMPLETE ENTRY
               PERFORM 2000-VALIDATE-ENTRY
               IF WS-ENTRY-VALID
                   PERFORM 3000-POST-ENTRY
               ELSE
                   PERFORM 4000-REJECT-ENTRY
               END-IF
           ELSE
               PERFORM 1100-READ-NEXT-RECORD
           END-IF
           .

      *================================================================*
       1100-READ-NEXT-RECORD.
      *================================================================*
           READ JOURNAL-INPUT-FILE
               AT END SET WS-EOF TO TRUE
           END-READ
           .

      *================================================================*
       2000-VALIDATE-ENTRY.
      *================================================================*
           SET WS-ENTRY-VALID TO TRUE
           MOVE ZEROS TO WS-ERRORS-THIS-ENTRY

      *    CHECK 1: BALANCE CHECK
           IF WS-JH-TOTAL-DEBITS NOT = WS-JH-TOTAL-CREDITS
               ADD 1 TO WS-ERRORS-THIS-ENTRY
               MOVE 'ENTRY OUT OF BALANCE'
                   TO WS-RR-REASON
               PERFORM 4100-WRITE-REJECT-DETAIL
               SET WS-ENTRY-INVALID TO TRUE
           END-IF

      *    CHECK 2: PERIOD VALIDATION
           MOVE WS-JH-COMPANY     TO PC-COMPANY
           MOVE WS-JH-FISCAL-YEAR TO PC-FISCAL-YEAR
           MOVE WS-JH-PERIOD      TO PC-PERIOD
           READ PERIOD-CONTROL-FILE
               KEY IS PC-KEY
               INVALID KEY
                   ADD 1 TO WS-ERRORS-THIS-ENTRY
                   MOVE 'INVALID PERIOD/YEAR/COMPANY'
                       TO WS-RR-REASON
                   PERFORM 4100-WRITE-REJECT-DETAIL
                   SET WS-ENTRY-INVALID TO TRUE
           END-READ
           IF WS-ENTRY-VALID
               IF NOT PC-OPEN
                   ADD 1 TO WS-ERRORS-THIS-ENTRY
                   MOVE 'POSTING PERIOD IS NOT OPEN'
                       TO WS-RR-REASON
                   PERFORM 4100-WRITE-REJECT-DETAIL
                   SET WS-ENTRY-INVALID TO TRUE
               END-IF
           END-IF

      *    CHECK 3: SOX CONTROLS
           IF WS-JH-ENTERED-BY = WS-JH-APPROVED-BY
              AND WS-JH-APPROVED-BY NOT = SPACES
               ADD 1 TO WS-ERRORS-THIS-ENTRY
               MOVE 'SOX: SAME USER ENTERED AND APPROVED'
                   TO WS-RR-REASON
               PERFORM 4100-WRITE-REJECT-DETAIL
               SET WS-ENTRY-INVALID TO TRUE
           END-IF

           IF WS-JH-TOTAL-DEBITS > WS-SOX-APPROVAL-LIMIT
               IF WS-JH-APPROVED-BY = SPACES
                   ADD 1 TO WS-ERRORS-THIS-ENTRY
                   MOVE 'SOX: ENTRY > $50K WITHOUT APPROVAL'
                       TO WS-RR-REASON
                   PERFORM 4100-WRITE-REJECT-DETAIL
                   SET WS-ENTRY-INVALID TO TRUE
               END-IF
           END-IF

      *    CHECK 4: VALIDATE EACH LINE
           PERFORM VARYING WS-IDX FROM 1 BY 1
               UNTIL WS-IDX > WS-LINE-COUNT
                  OR WS-IDX > 100

      *        ZERO AMOUNT CHECK
               IF WS-LT-AMOUNT(WS-IDX) = ZEROS
                   ADD 1 TO WS-ERRORS-THIS-ENTRY
                   MOVE 'LINE HAS ZERO AMOUNT'
                       TO WS-RR-REASON
                   PERFORM 4100-WRITE-REJECT-DETAIL
                   SET WS-ENTRY-INVALID TO TRUE
               END-IF

      *        ACCOUNT VALIDATION
               MOVE WS-LT-ACCOUNT(WS-IDX)
                   TO COA-ACCOUNT-KEY
               READ COA-FILE
                   KEY IS COA-ACCOUNT-KEY
                   INVALID KEY
                       ADD 1 TO WS-ERRORS-THIS-ENTRY
                       MOVE 'ACCOUNT NOT IN CHART OF ACCOUNTS'
                           TO WS-RR-REASON
                       PERFORM 4100-WRITE-REJECT-DETAIL
                       SET WS-ENTRY-INVALID TO TRUE
               END-READ

               IF WS-ENTRY-VALID OR WS-ERRORS-THIS-ENTRY < 10
                   IF WS-COA-FS = '00'
                       IF NOT COA-ACTIVE
                           ADD 1 TO WS-ERRORS-THIS-ENTRY
                           MOVE 'ACCOUNT IS INACTIVE'
                               TO WS-RR-REASON
                           PERFORM 4100-WRITE-REJECT-DETAIL
                           SET WS-ENTRY-INVALID TO TRUE
                       END-IF
                       IF COA-SUMMARY-ONLY
                           ADD 1 TO WS-ERRORS-THIS-ENTRY
                           MOVE 'ACCOUNT IS SUMMARY-ONLY'
                               TO WS-RR-REASON
                           PERFORM 4100-WRITE-REJECT-DETAIL
                           SET WS-ENTRY-INVALID TO TRUE
                       END-IF
                   END-IF
               END-IF
           END-PERFORM
           .

      *================================================================*
       3000-POST-ENTRY.
      *================================================================*
           PERFORM VARYING WS-IDX FROM 1 BY 1
               UNTIL WS-IDX > WS-LINE-COUNT

               MOVE WS-JH-COMPANY TO GL-COMPANY
               MOVE WS-LT-ACCOUNT(WS-IDX) TO GL-ACCOUNT-NUM
               MOVE WS-JH-FISCAL-YEAR TO GL-FISCAL-YEAR
               MOVE 'N' TO WS-NEW-RECORD-FLAG

               READ GL-MASTER-FILE
                   KEY IS GL-FULL-KEY
                   INVALID KEY
                       PERFORM 3100-INIT-NEW-GL-RECORD
                       SET WS-NEW-GL-RECORD TO TRUE
               END-READ

      *        UPDATE PERIOD ACTIVITY
               IF WS-LT-DC-IND(WS-IDX) = 'D'
                   ADD WS-LT-AMOUNT(WS-IDX)
                       TO GL-PRD-DEBITS(WS-JH-PERIOD)
                   ADD WS-LT-AMOUNT(WS-IDX)
                       TO GL-YTD-DEBITS
                   ADD WS-LT-AMOUNT(WS-IDX)
                       TO WS-TOTAL-DR-POSTED
               ELSE
                   ADD WS-LT-AMOUNT(WS-IDX)
                       TO GL-PRD-CREDITS(WS-JH-PERIOD)
                   ADD WS-LT-AMOUNT(WS-IDX)
                       TO GL-YTD-CREDITS
                   ADD WS-LT-AMOUNT(WS-IDX)
                       TO WS-TOTAL-CR-POSTED
               END-IF

      *        RECALCULATE DERIVED FIELDS
               COMPUTE GL-PRD-NET(WS-JH-PERIOD) =
                   GL-PRD-DEBITS(WS-JH-PERIOD) -
                   GL-PRD-CREDITS(WS-JH-PERIOD)

               COMPUTE GL-YTD-NET =
                   GL-YTD-DEBITS - GL-YTD-CREDITS

      *        RECALCULATE ENDING BALANCE
               MOVE GL-BEG-BALANCE TO WS-CALC-BALANCE
               PERFORM VARYING GL-PRD-IDX FROM 1 BY 1
                   UNTIL GL-PRD-IDX > 13
                   ADD GL-PRD-NET(GL-PRD-IDX)
                       TO WS-CALC-BALANCE
               END-PERFORM
               MOVE WS-CALC-BALANCE TO GL-END-BALANCE

      *        UPDATE TRACKING FIELDS
               MOVE WS-CURRENT-DATE TO GL-LAST-POST-DATE
               MOVE WS-JH-PERIOD TO GL-LAST-POST-PERIOD
               ADD 1 TO GL-ENTRY-COUNT-YTD

      *        WRITE OR REWRITE THE GL RECORD
               IF WS-NEW-GL-RECORD
                   WRITE GL-MASTER-RECORD
                       INVALID KEY
                           DISPLAY 'GL WRITE ERROR: '
                                   GL-FULL-KEY
                   END-WRITE
               ELSE
                   REWRITE GL-MASTER-RECORD
                       INVALID KEY
                           DISPLAY 'GL REWRITE ERROR: '
                                   GL-FULL-KEY
                   END-REWRITE
               END-IF

      *        WRITE AUDIT TRAIL RECORD
               PERFORM 3200-WRITE-AUDIT-TRAIL

      *        WRITE REPORT DETAIL
               MOVE WS-JH-ENTRY-NUM  TO WS-RD-ENTRY
               MOVE WS-IDX           TO WS-RD-LINE
               MOVE WS-LT-ACCOUNT(WS-IDX)
                                      TO WS-RD-ACCT
               MOVE WS-LT-DC-IND(WS-IDX)
                                      TO WS-RD-DC
               MOVE WS-LT-AMOUNT(WS-IDX)
                                      TO WS-RD-AMT
               MOVE WS-LT-DESC(WS-IDX)
                                      TO WS-RD-DESC
               MOVE 'POSTED  '        TO WS-RD-RESULT
               WRITE REPORT-RECORD FROM WS-RPT-DETAIL
                   AFTER ADVANCING 1 LINE

               ADD 1 TO WS-LINES-POSTED
           END-PERFORM

           ADD 1 TO WS-ENTRIES-POSTED
           .

      *================================================================*
       3100-INIT-NEW-GL-RECORD.
      *================================================================*
           INITIALIZE GL-MASTER-RECORD
           MOVE WS-JH-COMPANY     TO GL-COMPANY
           MOVE WS-LT-ACCOUNT(WS-IDX)
                                   TO GL-ACCOUNT-NUM
           MOVE WS-JH-FISCAL-YEAR TO GL-FISCAL-YEAR

      *    COPY ATTRIBUTES FROM CHART OF ACCOUNTS
           MOVE COA-DESCRIPTION    TO GL-DESCRIPTION
           MOVE COA-ACCOUNT-TYPE   TO GL-ACCOUNT-TYPE
           MOVE COA-NORMAL-BALANCE TO GL-NORMAL-BALANCE
           MOVE 'USD'              TO GL-CURRENCY-CODE
           MOVE ZEROS              TO GL-BEG-BALANCE
           .

      *================================================================*
       3200-WRITE-AUDIT-TRAIL.
      *================================================================*
           INITIALIZE WS-AUDIT-RECORD
           MOVE WS-CURRENT-TIMESTAMP  TO WS-AR-TIMESTAMP
           MOVE WS-PROGRAM-NAME       TO WS-AR-PROGRAM
           MOVE WS-JH-ENTRY-NUM       TO WS-AR-ENTRY-NUM
           MOVE WS-IDX                TO WS-AR-LINE-NUM
           MOVE WS-LT-ACCOUNT(WS-IDX) TO WS-AR-ACCOUNT
           MOVE WS-LT-DC-IND(WS-IDX)  TO WS-AR-DC-IND
           MOVE WS-LT-AMOUNT(WS-IDX)  TO WS-AR-AMOUNT
           MOVE WS-JH-PERIOD          TO WS-AR-PERIOD
           MOVE WS-JH-FISCAL-YEAR     TO WS-AR-FISCAL-YEAR
           MOVE WS-JH-COMPANY         TO WS-AR-COMPANY
           MOVE WS-JH-JOURNAL-CODE    TO WS-AR-JOURNAL-CODE
           MOVE WS-JH-ENTERED-BY      TO WS-AR-ENTERED-BY
           MOVE WS-JH-APPROVED-BY     TO WS-AR-APPROVED-BY
           MOVE WS-JH-SOURCE-REF      TO WS-AR-SOURCE-REF
           MOVE WS-LT-DESC(WS-IDX)    TO WS-AR-DESCRIPTION
           MOVE 'POSTED  '            TO WS-AR-RESULT

           WRITE AUDIT-TRAIL-RECORD FROM WS-AUDIT-RECORD
           .

      *================================================================*
       4000-REJECT-ENTRY.
      *================================================================*
           ADD 1 TO WS-ENTRIES-REJECTED

      *    WRITE REPORT LINES FOR REJECTED ENTRY
           PERFORM VARYING WS-IDX FROM 1 BY 1
               UNTIL WS-IDX > WS-LINE-COUNT
               MOVE WS-JH-ENTRY-NUM  TO WS-RD-ENTRY
               MOVE WS-IDX           TO WS-RD-LINE
               MOVE WS-LT-ACCOUNT(WS-IDX)
                                      TO WS-RD-ACCT
               MOVE WS-LT-DC-IND(WS-IDX)
                                      TO WS-RD-DC
               MOVE WS-LT-AMOUNT(WS-IDX)
                                      TO WS-RD-AMT
               MOVE WS-LT-DESC(WS-IDX)
                                      TO WS-RD-DESC
               MOVE 'REJECTED'        TO WS-RD-RESULT
               WRITE REPORT-RECORD FROM WS-RPT-DETAIL
                   AFTER ADVANCING 1 LINE
           END-PERFORM
           .

      *================================================================*
       4100-WRITE-REJECT-DETAIL.
      *================================================================*
           MOVE WS-JH-ENTRY-NUM TO WS-RR-ENTRY-NUM
           MOVE WS-IDX          TO WS-RR-LINE-NUM
           WRITE REJECT-RECORD FROM WS-REJECT-RECORD
           .

      *================================================================*
       9000-FINALIZE.
      *================================================================*
           PERFORM 9100-PRINT-SUMMARY

           CLOSE JOURNAL-INPUT-FILE
                 GL-MASTER-FILE
                 COA-FILE
                 PERIOD-CONTROL-FILE
                 AUDIT-TRAIL-FILE
                 REJECT-FILE
                 REPORT-FILE

           IF WS-ENTRIES-REJECTED > 0
               MOVE 4 TO RETURN-CODE
           ELSE
               MOVE 0 TO RETURN-CODE
           END-IF

           DISPLAY 'GLPOST COMPLETE. RC=' RETURN-CODE
           .

      *================================================================*
       9100-PRINT-SUMMARY.
      *================================================================*
           MOVE SPACES TO REPORT-RECORD
           WRITE REPORT-RECORD AFTER ADVANCING 2 LINES

           STRING '  GL POSTING SUMMARY'
               DELIMITED BY SIZE INTO REPORT-RECORD
           WRITE REPORT-RECORD AFTER ADVANCING 1 LINE

           MOVE SPACES TO REPORT-RECORD
           STRING '    ENTRIES READ:     ' WS-ENTRIES-READ
               DELIMITED BY SIZE INTO REPORT-RECORD
           WRITE REPORT-RECORD AFTER ADVANCING 1 LINE

           MOVE SPACES TO REPORT-RECORD
           STRING '    ENTRIES POSTED:   ' WS-ENTRIES-POSTED
               DELIMITED BY SIZE INTO REPORT-RECORD
           WRITE REPORT-RECORD AFTER ADVANCING 1 LINE

           MOVE SPACES TO REPORT-RECORD
           STRING '    ENTRIES REJECTED: ' WS-ENTRIES-REJECTED
               DELIMITED BY SIZE INTO REPORT-RECORD
           WRITE REPORT-RECORD AFTER ADVANCING 1 LINE

           MOVE SPACES TO REPORT-RECORD
           STRING '    LINES POSTED:     ' WS-LINES-POSTED
               DELIMITED BY SIZE INTO REPORT-RECORD
           WRITE REPORT-RECORD AFTER ADVANCING 1 LINE

           IF WS-TOTAL-DR-POSTED = WS-TOTAL-CR-POSTED
               MOVE SPACES TO REPORT-RECORD
               STRING '    POSTING BALANCED: YES'
                   DELIMITED BY SIZE INTO REPORT-RECORD
           ELSE
               MOVE SPACES TO REPORT-RECORD
               STRING '    POSTING BALANCED: *** NO ***'
                   DELIMITED BY SIZE INTO REPORT-RECORD
           END-IF
           WRITE REPORT-RECORD AFTER ADVANCING 1 LINE
           .

Companion JCL

//GLPOST   JOB (PNHP,ACCTG),'GL DAILY POSTING',
//             CLASS=A,MSGCLASS=X,MSGLEVEL=(1,1),
//             NOTIFY=&SYSUID
//*
//*-----------------------------------------------------------*
//*  GENERAL LEDGER DAILY POSTING                             *
//*  VALIDATES AND POSTS JOURNAL ENTRIES TO GL MASTER          *
//*-----------------------------------------------------------*
//STEP01   EXEC PGM=GLPOST
//STEPLIB  DD DSN=PNHP.PROD.LOADLIB,DISP=SHR
//JEINPUT  DD DSN=PNHP.JOURNAL.ENTRIES.PENDING,DISP=SHR,
//            DCB=(RECFM=FB,LRECL=300,BLKSIZE=0)
//GLMAST   DD DSN=PNHP.GL.MASTER.VSAM,DISP=SHR
//COAFILE  DD DSN=PNHP.CHART.OF.ACCOUNTS.VSAM,DISP=SHR
//PRDCTRL  DD DSN=PNHP.PERIOD.CONTROL.VSAM,DISP=SHR
//AUDTRL   DD DSN=PNHP.GL.AUDIT.TRAIL,DISP=MOD,
//            DCB=(RECFM=FB,LRECL=250,BLKSIZE=0)
//REJECTS  DD DSN=PNHP.GL.POSTING.REJECTS,
//            DISP=(NEW,CATLG,DELETE),
//            SPACE=(CYL,(1,1)),
//            DCB=(RECFM=FB,LRECL=350,BLKSIZE=0)
//POSTRPT  DD SYSOUT=*,DCB=(RECFM=FBA,LRECL=133,BLKSIZE=0)
//SYSOUT   DD SYSOUT=*
//SYSPRINT DD SYSOUT=*

Walkthrough

The GLPOST program processes a journal entry file that contains two record types: Header records (type 'H') and Line records (type 'L'). Each journal entry consists of one header followed by one or more lines. The header contains entry-level information (entry number, company, period, descriptions, user IDs), while each line specifies one account, debit/credit indicator, and amount.

Record Accumulation (paragraph 1000): When the program encounters a header record, it stores the header fields and begins accumulating line records into the working-storage line table (up to 100 lines per entry). This continues until the next header record is encountered or end-of-file is reached. This design ensures that the complete entry is available in memory before validation begins, allowing all-or-nothing posting.

Validation Pipeline (paragraph 2000): The validation checks are performed in a specific order, chosen to catch the most fundamental errors first:

The balance check comes first because an unbalanced entry violates the most basic accounting principle. If debits do not equal credits, there is no point checking individual line accounts.

The period validation comes second because posting to a closed period would corrupt finalized financial statements. The program reads the period control file to verify that the target period is open for the specified company and fiscal year.

The SOX controls come third. Two checks are performed: segregation of duties (the entered-by and approved-by user IDs must differ) and approval threshold (entries exceeding $50,000 in total debits must have an approver). These controls are mandated by PNHP's SOX compliance framework.

The line-level validations come last. For each line, the program checks that the amount is not zero, then reads the chart of accounts to verify that the account exists, is active, and is marked as postable. If the COA lookup fails (INVALID KEY), the line is flagged. If the account is found but is inactive or summary-only, it is also flagged.

Posting (paragraph 3000): For each line of a validated entry, the program reads the GL master record using the full key (company + account + fiscal year). If the record does not exist (first-time posting to this account in this fiscal year), a new record is initialized from the chart of accounts attributes.

The posting logic adds the line amount to either the period debits or period credits, depending on the debit/credit indicator. After updating the period activity, the program recalculates three derived fields: the period net (debits minus credits), the YTD net, and the ending balance. The ending balance is recalculated from scratch by summing the beginning balance and all 13 period nets, ensuring it is always accurate regardless of the posting sequence.

The tracking fields (last post date, last post period, YTD entry count) are updated, and the record is written (for new records) or rewritten (for existing records).

Audit Trail (paragraph 3200): After each line is successfully posted, a comprehensive audit trail record is written. The record captures the timestamp, program name, entry number, line number, account, amount, period, company, journal code, the users who entered and approved the entry, the source reference, and the posting result. This audit trail enables complete traceability from any GL balance back to its source transaction, satisfying SOX and regulatory requirements.

Rejection Handling (paragraph 4000): When an entry fails validation, all of its lines are written to the report with a "REJECTED" status. The specific rejection reasons are written to the reject file. The entire entry is rejected -- individual lines are not posted selectively, because posting partial entries would violate the double-entry balance requirement.

Finalization (paragraph 9000): The summary report shows the total entries read, posted, and rejected, plus the total lines posted. A critical final check verifies that the total debits posted equal the total credits posted. This is a batch-level balance verification that catches any internal logic errors in the posting process itself.

Discussion Questions

  1. The program validates the entire journal entry before posting any lines. What would be the consequences of a design that posts each line individually as it is validated? Consider both the performance implications and the accounting integrity implications.

  2. The audit trail records the user who entered and approved each entry, but it does not record the user who initiated the posting batch. Why might you want to capture this additional information? What SOX control would it support?

  3. The program initializes new GL master records from chart of accounts attributes when an account is posted for the first time in a fiscal year. What happens if the COA attributes change between the initial GL record creation and a subsequent posting? Should the GL record be updated to reflect COA changes, or should it retain the attributes from the time of creation?

  4. The validation pipeline rejects the entire entry if any line fails validation. In practice, finance teams sometimes want partial posting: post the valid lines and reject only the invalid ones. What are the accounting arguments for and against partial posting? How would you modify the program to support configurable partial posting?

  5. The program processes approximately 8,500 journal entry lines in a 90-minute batch window. Calculate the average processing time per line. If PNHP acquires two additional hospitals and the volume increases to 15,000 lines per night, will the program still complete within the batch window? What performance optimizations could you apply?

  6. The period control file determines whether a period is open or closed. What controls should govern who can reopen a closed period? What audit trail entries should be generated when a period is reopened? How does this interact with SOX compliance?