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:
-
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.
-
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.
-
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.
-
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
-
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.
-
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?
-
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?
-
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?
-
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?
-
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?