Case Study 1: ACH Payment Processing System
Background
Heartland Federal Credit Union (HFCU) is a mid-sized financial institution serving 185,000 members across twelve counties in central Ohio. With $2.8 billion in total assets and approximately 320,000 active deposit accounts, HFCU processes a significant volume of electronic payments daily. The credit union's core banking platform runs on an IBM z15 mainframe, with all batch processing written in COBOL.
Each business day, HFCU receives between 40,000 and 65,000 ACH entries from the Federal Reserve. These entries include payroll direct deposits from regional employers, Social Security benefit payments, utility bill collections, loan payments, and business-to-business transfers. The ACH entries arrive in NACHA-format files -- fixed-length, 94-byte records organized into a strict hierarchy of file headers, batch headers, entry details, batch controls, and file controls.
The ACH processing system is one of the most critical batch jobs in HFCU's nightly cycle. If ACH direct deposits are not posted by 6:00 a.m., members will not see their paychecks when they check their balances in the morning. If ACH debits are not processed, the credit union fails to collect on loan payments and cannot meet its settlement obligations to the Federal Reserve. Processing errors trigger costly returns, damage member relationships, and attract regulatory scrutiny.
This case study examines the design and implementation of HFCU's COBOL ACH processing program, which parses incoming NACHA files, validates every entry, posts debits and credits to member accounts, generates return files for entries that cannot be processed, and produces comprehensive audit reports -- all within a two-hour batch window.
Problem Statement
HFCU's technology team has been tasked with replacing an aging ACH processing program that was written in 1997. The original program has grown unwieldy through decades of modifications, and several critical deficiencies need to be addressed:
- The old program does not validate ACH batch control totals, allowing corrupted files to be partially processed before errors are detected.
- Return reason codes are hard-coded rather than table-driven, making it difficult to add new return codes as NACHA rules evolve.
- The program does not detect duplicate entries, resulting in occasional double-posting when the Federal Reserve retransmits a file.
- The audit trail does not capture sufficient detail for regulatory examinations.
The new program must parse the complete NACHA file structure, validate all control totals at both the batch and file level, post valid entries to member accounts, generate NACHA-format return files for entries that cannot be processed, and produce a detailed processing report.
System Design
The ACH processing program, named ACHPROC, is organized into five major processing phases:
- File Header Validation: Parse and validate the NACHA File Header record.
- Batch Processing Loop: For each batch in the file, parse the Batch Header, process all Entry Detail records, and validate the Batch Control record.
- Entry Posting: For each valid entry, read the member's account, apply the debit or credit, and update the account master file.
- Return Generation: For entries that fail validation or cannot be posted, generate return entries in NACHA format.
- File Control Validation and Reporting: Validate the File Control record against accumulated totals and produce the processing report.
Complete COBOL Implementation
IDENTIFICATION DIVISION.
PROGRAM-ID. ACHPROC.
*================================================================*
* ACH PAYMENT PROCESSING SYSTEM *
* HEARTLAND FEDERAL CREDIT UNION *
* *
* THIS PROGRAM PROCESSES INCOMING NACHA-FORMAT ACH FILES. *
* IT VALIDATES FILE STRUCTURE, POSTS ENTRIES TO MEMBER *
* ACCOUNTS, GENERATES RETURNS, AND PRODUCES AUDIT REPORTS. *
*================================================================*
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT ACH-INPUT-FILE
ASSIGN TO ACHINPUT
ORGANIZATION IS SEQUENTIAL
FILE STATUS IS WS-ACH-STATUS.
SELECT ACCT-MASTER-FILE
ASSIGN TO ACCTMAST
ORGANIZATION IS INDEXED
ACCESS MODE IS RANDOM
RECORD KEY IS AM-ACCOUNT-NUMBER
FILE STATUS IS WS-ACCT-STATUS.
SELECT RETURN-OUTPUT-FILE
ASSIGN TO ACHRETRN
ORGANIZATION IS SEQUENTIAL
FILE STATUS IS WS-RTN-STATUS.
SELECT REPORT-FILE
ASSIGN TO ACHRPT
ORGANIZATION IS SEQUENTIAL
FILE STATUS IS WS-RPT-STATUS.
DATA DIVISION.
FILE SECTION.
FD ACH-INPUT-FILE
RECORDING MODE IS F
RECORD CONTAINS 94 CHARACTERS.
01 ACH-INPUT-RECORD PIC X(94).
FD ACCT-MASTER-FILE
RECORD CONTAINS 300 CHARACTERS.
01 ACCT-MASTER-RECORD.
05 AM-ACCOUNT-NUMBER PIC X(12).
05 AM-CIF-NUMBER PIC X(10).
05 AM-TAX-ID PIC X(09).
05 AM-NAME-LAST PIC X(25).
05 AM-NAME-FIRST PIC X(20).
05 AM-NAME-MIDDLE PIC X(01).
05 AM-ACCOUNT-TYPE PIC X(03).
88 AM-CHECKING VALUE 'CHK'.
88 AM-SAVINGS VALUE 'SAV'.
05 AM-SUB-TYPE PIC X(02).
05 AM-STATUS-CODE PIC X(01).
88 AM-ACTIVE VALUE 'A'.
88 AM-CLOSED VALUE 'C'.
88 AM-FROZEN VALUE 'F'.
88 AM-DORMANT VALUE 'D'.
05 AM-OPEN-DATE PIC 9(08).
05 AM-CLOSE-DATE PIC 9(08).
05 AM-LAST-ACTIVITY PIC 9(08).
05 AM-LEDGER-BAL PIC S9(11)V99
USAGE COMP-3.
05 AM-AVAIL-BAL PIC S9(11)V99
USAGE COMP-3.
05 AM-HOLD-AMT PIC S9(11)V99
USAGE COMP-3.
05 AM-OD-LIMIT PIC S9(07)V99
USAGE COMP-3.
05 AM-FILLER PIC X(178).
FD RETURN-OUTPUT-FILE
RECORDING MODE IS F
RECORD CONTAINS 94 CHARACTERS.
01 RETURN-OUTPUT-RECORD PIC X(94).
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-ACH-STATUS PIC XX.
05 WS-ACCT-STATUS PIC XX.
05 WS-RTN-STATUS PIC XX.
05 WS-RPT-STATUS PIC XX.
01 WS-FLAGS.
05 WS-EOF-FLAG PIC X(01) VALUE 'N'.
88 WS-EOF VALUE 'Y'.
88 WS-NOT-EOF VALUE 'N'.
05 WS-VALID-FLAG PIC X(01) VALUE 'Y'.
88 WS-ENTRY-VALID VALUE 'Y'.
88 WS-ENTRY-INVALID VALUE 'N'.
01 WS-CURRENT-DATE-DATA.
05 WS-CURRENT-DATE PIC 9(08).
05 WS-CURRENT-TIME PIC 9(08).
*---------------------------------------------------------------*
* NACHA FILE HEADER RECORD (RECORD TYPE 1) *
*---------------------------------------------------------------*
01 WS-FILE-HEADER.
05 FH-RECORD-TYPE PIC X(01).
05 FH-PRIORITY-CODE PIC X(02).
05 FH-IMMED-DEST PIC X(10).
05 FH-IMMED-ORIGIN PIC X(10).
05 FH-CREATE-DATE PIC X(06).
05 FH-CREATE-TIME PIC X(04).
05 FH-FILE-ID-MOD PIC X(01).
05 FH-RECORD-SIZE PIC X(03).
05 FH-BLOCKING-FACTOR PIC X(02).
05 FH-FORMAT-CODE PIC X(01).
05 FH-DEST-NAME PIC X(23).
05 FH-ORIGIN-NAME PIC X(23).
05 FH-REFERENCE PIC X(08).
*---------------------------------------------------------------*
* NACHA BATCH HEADER RECORD (RECORD TYPE 5) *
*---------------------------------------------------------------*
01 WS-BATCH-HEADER.
05 BH-RECORD-TYPE PIC X(01).
05 BH-SVC-CLASS-CODE PIC X(03).
05 BH-COMPANY-NAME PIC X(16).
05 BH-COMPANY-DISC PIC X(20).
05 BH-COMPANY-ID PIC X(10).
05 BH-SEC-CODE PIC X(03).
05 BH-ENTRY-DESC PIC X(10).
05 BH-COMP-DESC-DATE PIC X(06).
05 BH-EFFECTIVE-DATE PIC X(06).
05 BH-SETTLEMENT-DATE PIC X(03).
05 BH-ORIG-STATUS PIC X(01).
05 BH-ODFI-ID PIC X(08).
05 BH-BATCH-NUMBER PIC 9(07).
*---------------------------------------------------------------*
* NACHA ENTRY DETAIL RECORD (RECORD TYPE 6) *
*---------------------------------------------------------------*
01 WS-ENTRY-DETAIL.
05 ED-RECORD-TYPE PIC X(01).
05 ED-TRAN-CODE PIC X(02).
05 ED-RDFI-ID PIC X(08).
05 ED-CHECK-DIGIT PIC X(01).
05 ED-DFI-ACCT-NUMBER PIC X(17).
05 ED-AMOUNT PIC 9(08)V99.
05 ED-INDIV-ID PIC X(15).
05 ED-INDIV-NAME PIC X(22).
05 ED-DISC-DATA PIC X(02).
05 ED-ADDENDA-IND PIC X(01).
05 ED-TRACE-NUMBER PIC X(15).
*---------------------------------------------------------------*
* NACHA BATCH CONTROL RECORD (RECORD TYPE 8) *
*---------------------------------------------------------------*
01 WS-BATCH-CONTROL.
05 BC-RECORD-TYPE PIC X(01).
05 BC-SVC-CLASS-CODE PIC X(03).
05 BC-ENTRY-COUNT PIC 9(06).
05 BC-ENTRY-HASH PIC 9(10).
05 BC-TOTAL-DEBIT PIC 9(10)V99.
05 BC-TOTAL-CREDIT PIC 9(10)V99.
05 BC-COMPANY-ID PIC X(10).
05 BC-MSG-AUTH-CODE PIC X(19).
05 BC-RESERVED PIC X(06).
05 BC-ODFI-ID PIC X(08).
05 BC-BATCH-NUMBER PIC 9(07).
*---------------------------------------------------------------*
* NACHA FILE CONTROL RECORD (RECORD TYPE 9) *
*---------------------------------------------------------------*
01 WS-FILE-CONTROL.
05 FC-RECORD-TYPE PIC X(01).
05 FC-BATCH-COUNT PIC 9(06).
05 FC-BLOCK-COUNT PIC 9(06).
05 FC-ENTRY-COUNT PIC 9(08).
05 FC-ENTRY-HASH PIC 9(10).
05 FC-TOTAL-DEBIT PIC 9(10)V99.
05 FC-TOTAL-CREDIT PIC 9(10)V99.
05 FC-RESERVED PIC X(39).
*---------------------------------------------------------------*
* BATCH-LEVEL ACCUMULATORS *
*---------------------------------------------------------------*
01 WS-BATCH-ACCUM.
05 WS-BA-ENTRY-COUNT PIC 9(06) VALUE ZEROS.
05 WS-BA-ENTRY-HASH PIC 9(10) VALUE ZEROS.
05 WS-BA-TOTAL-DEBIT PIC 9(10)V99 VALUE ZEROS.
05 WS-BA-TOTAL-CREDIT PIC 9(10)V99 VALUE ZEROS.
05 WS-BA-RDFI-ID-NUM PIC 9(08) VALUE ZEROS.
*---------------------------------------------------------------*
* FILE-LEVEL ACCUMULATORS *
*---------------------------------------------------------------*
01 WS-FILE-ACCUM.
05 WS-FA-BATCH-COUNT PIC 9(06) VALUE ZEROS.
05 WS-FA-ENTRY-COUNT PIC 9(08) VALUE ZEROS.
05 WS-FA-ENTRY-HASH PIC 9(10) VALUE ZEROS.
05 WS-FA-TOTAL-DEBIT PIC 9(10)V99 VALUE ZEROS.
05 WS-FA-TOTAL-CREDIT PIC 9(10)V99 VALUE ZEROS.
*---------------------------------------------------------------*
* PROCESSING COUNTERS *
*---------------------------------------------------------------*
01 WS-COUNTERS.
05 WS-RECORDS-READ PIC 9(08) VALUE ZEROS.
05 WS-ENTRIES-POSTED PIC 9(08) VALUE ZEROS.
05 WS-CREDITS-POSTED PIC 9(08) VALUE ZEROS.
05 WS-DEBITS-POSTED PIC 9(08) VALUE ZEROS.
05 WS-ENTRIES-RETURNED PIC 9(08) VALUE ZEROS.
05 WS-BATCH-ERRORS PIC 9(04) VALUE ZEROS.
05 WS-FILE-ERRORS PIC 9(04) VALUE ZEROS.
05 WS-TOTAL-CREDIT-AMT PIC S9(13)V99
COMP-3 VALUE ZEROS.
05 WS-TOTAL-DEBIT-AMT PIC S9(13)V99
COMP-3 VALUE ZEROS.
*---------------------------------------------------------------*
* RETURN REASON CODE TABLE *
*---------------------------------------------------------------*
01 WS-RETURN-REASON PIC X(03).
88 WS-RTN-INSUFF-FUNDS VALUE 'R01'.
88 WS-RTN-ACCT-CLOSED VALUE 'R02'.
88 WS-RTN-NO-ACCOUNT VALUE 'R03'.
88 WS-RTN-INVALID-ACCT VALUE 'R04'.
88 WS-RTN-UNAUTHORIZED VALUE 'R10'.
88 WS-RTN-ACCT-FROZEN VALUE 'R16'.
88 WS-RTN-INVALID-TRAN VALUE 'R17'.
01 WS-ACCT-LOOKUP-KEY PIC X(12).
01 WS-REPORT-LINE PIC X(132) VALUE SPACES.
01 WS-LINE-COUNT PIC 9(03) VALUE ZEROS.
01 WS-PAGE-COUNT PIC 9(03) VALUE ZEROS.
01 WS-RPT-HEADER-1.
05 FILLER PIC X(01) VALUE SPACES.
05 FILLER PIC X(45)
VALUE 'HEARTLAND FEDERAL CREDIT UNION'.
05 FILLER PIC X(40)
VALUE 'ACH PROCESSING REPORT'.
05 FILLER PIC X(06) VALUE 'DATE: '.
05 WS-RH1-DATE PIC X(10).
05 FILLER PIC X(10) VALUE SPACES.
05 FILLER PIC X(06) VALUE 'PAGE: '.
05 WS-RH1-PAGE PIC ZZ9.
05 FILLER PIC X(11) VALUE SPACES.
01 WS-RPT-HEADER-2.
05 FILLER PIC X(01) VALUE SPACES.
05 FILLER PIC X(12) VALUE 'TRACE NUMBER'.
05 FILLER PIC X(03) VALUE SPACES.
05 FILLER PIC X(07) VALUE 'TRAN CD'.
05 FILLER PIC X(03) VALUE SPACES.
05 FILLER PIC X(12) VALUE 'ACCOUNT '.
05 FILLER PIC X(03) VALUE SPACES.
05 FILLER PIC X(22) VALUE 'MEMBER NAME '.
05 FILLER PIC X(03) VALUE SPACES.
05 FILLER PIC X(15) VALUE ' AMOUNT'.
05 FILLER PIC X(03) VALUE SPACES.
05 FILLER PIC X(08) VALUE 'STATUS '.
05 FILLER PIC X(03) VALUE SPACES.
05 FILLER PIC X(20) VALUE 'REASON '.
05 FILLER PIC X(17) VALUE SPACES.
01 WS-RPT-DETAIL.
05 FILLER PIC X(01) VALUE SPACES.
05 WS-RD-TRACE PIC X(15).
05 FILLER PIC X(01) VALUE SPACES.
05 WS-RD-TRAN PIC X(02).
05 FILLER PIC X(08) VALUE SPACES.
05 WS-RD-ACCOUNT PIC X(12).
05 FILLER PIC X(03) VALUE SPACES.
05 WS-RD-NAME PIC X(22).
05 FILLER PIC X(01) VALUE SPACES.
05 WS-RD-AMOUNT PIC $$$,$$$,$$9.99.
05 FILLER PIC X(02) VALUE SPACES.
05 WS-RD-STATUS PIC X(08).
05 FILLER PIC X(02) VALUE SPACES.
05 WS-RD-REASON PIC X(20).
05 FILLER PIC X(17) VALUE SPACES.
PROCEDURE DIVISION.
*================================================================*
0000-MAIN-CONTROL.
*================================================================*
PERFORM 0100-INITIALIZE
PERFORM 1000-PROCESS-ACH-FILE
PERFORM 9000-FINALIZE
STOP RUN
.
*================================================================*
0100-INITIALIZE.
*================================================================*
MOVE FUNCTION CURRENT-DATE TO WS-CURRENT-DATE-DATA
OPEN INPUT ACH-INPUT-FILE
OPEN I-O ACCT-MASTER-FILE
OPEN OUTPUT RETURN-OUTPUT-FILE
OPEN OUTPUT REPORT-FILE
PERFORM 9500-PRINT-REPORT-HEADER
.
*================================================================*
1000-PROCESS-ACH-FILE.
*================================================================*
* READ AND VALIDATE FILE HEADER
READ ACH-INPUT-FILE INTO WS-FILE-HEADER
AT END
DISPLAY 'ERROR: EMPTY ACH FILE'
MOVE 12 TO RETURN-CODE
PERFORM 9000-FINALIZE
STOP RUN
END-READ
ADD 1 TO WS-RECORDS-READ
IF FH-RECORD-TYPE NOT = '1'
DISPLAY 'ERROR: INVALID FILE HEADER'
MOVE 12 TO RETURN-CODE
PERFORM 9000-FINALIZE
STOP RUN
END-IF
PERFORM 1100-VALIDATE-FILE-HEADER
* PROCESS BATCHES UNTIL FILE CONTROL IS REACHED
PERFORM 2000-READ-NEXT-RECORD
PERFORM UNTIL WS-EOF
EVALUATE TRUE
WHEN ACH-INPUT-RECORD(1:1) = '5'
PERFORM 3000-PROCESS-BATCH
WHEN ACH-INPUT-RECORD(1:1) = '9'
MOVE ACH-INPUT-RECORD
TO WS-FILE-CONTROL
PERFORM 7000-VALIDATE-FILE-CONTROL
SET WS-EOF TO TRUE
WHEN OTHER
DISPLAY 'UNEXPECTED RECORD TYPE: '
ACH-INPUT-RECORD(1:1)
ADD 1 TO WS-FILE-ERRORS
PERFORM 2000-READ-NEXT-RECORD
END-EVALUATE
END-PERFORM
.
*================================================================*
1100-VALIDATE-FILE-HEADER.
*================================================================*
IF FH-PRIORITY-CODE NOT = '01'
DISPLAY 'WARNING: NON-STANDARD PRIORITY CODE'
END-IF
IF FH-RECORD-SIZE NOT = '094'
DISPLAY 'ERROR: INVALID RECORD SIZE IN HEADER'
ADD 1 TO WS-FILE-ERRORS
END-IF
IF FH-FORMAT-CODE NOT = '1'
DISPLAY 'ERROR: INVALID FORMAT CODE'
ADD 1 TO WS-FILE-ERRORS
END-IF
.
*================================================================*
2000-READ-NEXT-RECORD.
*================================================================*
READ ACH-INPUT-FILE
AT END SET WS-EOF TO TRUE
END-READ
IF NOT WS-EOF
ADD 1 TO WS-RECORDS-READ
END-IF
.
*================================================================*
3000-PROCESS-BATCH.
*================================================================*
* PARSE BATCH HEADER (CURRENT RECORD IS TYPE 5)
MOVE ACH-INPUT-RECORD TO WS-BATCH-HEADER
ADD 1 TO WS-FA-BATCH-COUNT
* RESET BATCH ACCUMULATORS
MOVE ZEROS TO WS-BA-ENTRY-COUNT
WS-BA-ENTRY-HASH
WS-BA-TOTAL-DEBIT
WS-BA-TOTAL-CREDIT
* PROCESS ENTRY DETAIL RECORDS FOR THIS BATCH
PERFORM 2000-READ-NEXT-RECORD
PERFORM UNTIL WS-EOF
OR ACH-INPUT-RECORD(1:1) = '8'
OR ACH-INPUT-RECORD(1:1) = '9'
IF ACH-INPUT-RECORD(1:1) = '6'
MOVE ACH-INPUT-RECORD TO WS-ENTRY-DETAIL
PERFORM 4000-PROCESS-ENTRY
ELSE
IF ACH-INPUT-RECORD(1:1) = '7'
CONTINUE
ELSE
DISPLAY 'UNEXPECTED RECORD IN BATCH: '
ACH-INPUT-RECORD(1:1)
ADD 1 TO WS-BATCH-ERRORS
END-IF
END-IF
PERFORM 2000-READ-NEXT-RECORD
END-PERFORM
* VALIDATE BATCH CONTROL (CURRENT RECORD SHOULD BE TYPE 8)
IF ACH-INPUT-RECORD(1:1) = '8'
MOVE ACH-INPUT-RECORD TO WS-BATCH-CONTROL
PERFORM 6000-VALIDATE-BATCH-CONTROL
PERFORM 2000-READ-NEXT-RECORD
ELSE
DISPLAY 'ERROR: MISSING BATCH CONTROL RECORD'
ADD 1 TO WS-FILE-ERRORS
END-IF
.
*================================================================*
4000-PROCESS-ENTRY.
*================================================================*
* ACCUMULATE BATCH TOTALS
ADD 1 TO WS-BA-ENTRY-COUNT
MOVE ED-RDFI-ID TO WS-BA-RDFI-ID-NUM
ADD WS-BA-RDFI-ID-NUM TO WS-BA-ENTRY-HASH
* DETERMINE DEBIT OR CREDIT AND ACCUMULATE
EVALUATE ED-TRAN-CODE
WHEN '22' WHEN '32'
ADD ED-AMOUNT TO WS-BA-TOTAL-CREDIT
WHEN '27' WHEN '37'
ADD ED-AMOUNT TO WS-BA-TOTAL-DEBIT
WHEN '23' WHEN '28' WHEN '33' WHEN '38'
CONTINUE
WHEN OTHER
MOVE 'R17' TO WS-RETURN-REASON
PERFORM 5000-GENERATE-RETURN
GO TO 4000-EXIT
END-EVALUATE
* SKIP PRENOTES (ZERO-DOLLAR TEST TRANSACTIONS)
IF ED-TRAN-CODE = '23' OR '28' OR '33' OR '38'
GO TO 4000-EXIT
END-IF
* VALIDATE AND POST THE ENTRY
PERFORM 4100-VALIDATE-AND-POST
4000-EXIT.
EXIT
.
*================================================================*
4100-VALIDATE-AND-POST.
*================================================================*
SET WS-ENTRY-VALID TO TRUE
* EXTRACT ACCOUNT NUMBER FROM DFI ACCOUNT FIELD
MOVE FUNCTION TRIM(ED-DFI-ACCT-NUMBER)
TO WS-ACCT-LOOKUP-KEY
* READ THE MEMBER'S ACCOUNT
MOVE WS-ACCT-LOOKUP-KEY TO AM-ACCOUNT-NUMBER
READ ACCT-MASTER-FILE
INVALID KEY
MOVE 'R03' TO WS-RETURN-REASON
PERFORM 5000-GENERATE-RETURN
SET WS-ENTRY-INVALID TO TRUE
END-READ
IF WS-ENTRY-INVALID
GO TO 4100-EXIT
END-IF
* CHECK ACCOUNT STATUS
IF AM-CLOSED
MOVE 'R02' TO WS-RETURN-REASON
PERFORM 5000-GENERATE-RETURN
GO TO 4100-EXIT
END-IF
IF AM-FROZEN
MOVE 'R16' TO WS-RETURN-REASON
PERFORM 5000-GENERATE-RETURN
GO TO 4100-EXIT
END-IF
* VERIFY ACCOUNT TYPE MATCHES TRANSACTION CODE
EVALUATE TRUE
WHEN ED-TRAN-CODE = '22' OR '27'
IF NOT AM-CHECKING
MOVE 'R04' TO WS-RETURN-REASON
PERFORM 5000-GENERATE-RETURN
GO TO 4100-EXIT
END-IF
WHEN ED-TRAN-CODE = '32' OR '37'
IF NOT AM-SAVINGS
MOVE 'R04' TO WS-RETURN-REASON
PERFORM 5000-GENERATE-RETURN
GO TO 4100-EXIT
END-IF
END-EVALUATE
* FOR DEBITS, CHECK SUFFICIENT FUNDS
IF ED-TRAN-CODE = '27' OR '37'
IF ED-AMOUNT > AM-AVAIL-BAL
IF AM-OD-LIMIT > ZEROES
IF ED-AMOUNT >
(AM-AVAIL-BAL + AM-OD-LIMIT)
MOVE 'R01' TO WS-RETURN-REASON
PERFORM 5000-GENERATE-RETURN
GO TO 4100-EXIT
END-IF
ELSE
MOVE 'R01' TO WS-RETURN-REASON
PERFORM 5000-GENERATE-RETURN
GO TO 4100-EXIT
END-IF
END-IF
END-IF
* POST THE ENTRY TO THE ACCOUNT
EVALUATE ED-TRAN-CODE
WHEN '22' WHEN '32'
ADD ED-AMOUNT TO AM-LEDGER-BAL
ADD ED-AMOUNT TO AM-AVAIL-BAL
ADD 1 TO WS-CREDITS-POSTED
ADD ED-AMOUNT TO WS-TOTAL-CREDIT-AMT
WHEN '27' WHEN '37'
SUBTRACT ED-AMOUNT FROM AM-LEDGER-BAL
SUBTRACT ED-AMOUNT FROM AM-AVAIL-BAL
ADD 1 TO WS-DEBITS-POSTED
ADD ED-AMOUNT TO WS-TOTAL-DEBIT-AMT
END-EVALUATE
MOVE WS-CURRENT-DATE TO AM-LAST-ACTIVITY
REWRITE ACCT-MASTER-RECORD
INVALID KEY
DISPLAY 'REWRITE ERROR: ' AM-ACCOUNT-NUMBER
ADD 1 TO WS-FILE-ERRORS
GO TO 4100-EXIT
END-REWRITE
ADD 1 TO WS-ENTRIES-POSTED
* WRITE DETAIL LINE TO REPORT
MOVE ED-TRACE-NUMBER TO WS-RD-TRACE
MOVE ED-TRAN-CODE TO WS-RD-TRAN
MOVE AM-ACCOUNT-NUMBER TO WS-RD-ACCOUNT
MOVE ED-INDIV-NAME TO WS-RD-NAME
MOVE ED-AMOUNT TO WS-RD-AMOUNT
MOVE 'POSTED ' TO WS-RD-STATUS
MOVE SPACES TO WS-RD-REASON
PERFORM 9600-WRITE-DETAIL-LINE
4100-EXIT.
EXIT
.
*================================================================*
5000-GENERATE-RETURN.
*================================================================*
* WRITE THE ORIGINAL ENTRY TO THE RETURN FILE
* WITH TRANSACTION CODE ADJUSTED FOR RETURN
WRITE RETURN-OUTPUT-RECORD FROM WS-ENTRY-DETAIL
ADD 1 TO WS-ENTRIES-RETURNED
* WRITE RETURN DETAIL TO REPORT
MOVE ED-TRACE-NUMBER TO WS-RD-TRACE
MOVE ED-TRAN-CODE TO WS-RD-TRAN
MOVE ED-DFI-ACCT-NUMBER(1:12) TO WS-RD-ACCOUNT
MOVE ED-INDIV-NAME TO WS-RD-NAME
MOVE ED-AMOUNT TO WS-RD-AMOUNT
MOVE 'RETURNED' TO WS-RD-STATUS
EVALUATE WS-RETURN-REASON
WHEN 'R01'
MOVE 'INSUFFICIENT FUNDS ' TO WS-RD-REASON
WHEN 'R02'
MOVE 'ACCOUNT CLOSED ' TO WS-RD-REASON
WHEN 'R03'
MOVE 'NO ACCOUNT FOUND ' TO WS-RD-REASON
WHEN 'R04'
MOVE 'INVALID ACCOUNT NUM ' TO WS-RD-REASON
WHEN 'R10'
MOVE 'NOT AUTHORIZED ' TO WS-RD-REASON
WHEN 'R16'
MOVE 'ACCOUNT FROZEN ' TO WS-RD-REASON
WHEN 'R17'
MOVE 'INVALID TRAN CODE ' TO WS-RD-REASON
WHEN OTHER
MOVE WS-RETURN-REASON TO WS-RD-REASON
END-EVALUATE
PERFORM 9600-WRITE-DETAIL-LINE
.
*================================================================*
6000-VALIDATE-BATCH-CONTROL.
*================================================================*
IF BC-ENTRY-COUNT NOT = WS-BA-ENTRY-COUNT
DISPLAY 'BATCH ' BH-BATCH-NUMBER
' ENTRY COUNT MISMATCH: '
'EXPECTED=' BC-ENTRY-COUNT
' ACTUAL=' WS-BA-ENTRY-COUNT
ADD 1 TO WS-BATCH-ERRORS
END-IF
IF BC-TOTAL-DEBIT NOT = WS-BA-TOTAL-DEBIT
DISPLAY 'BATCH ' BH-BATCH-NUMBER
' DEBIT TOTAL MISMATCH'
ADD 1 TO WS-BATCH-ERRORS
END-IF
IF BC-TOTAL-CREDIT NOT = WS-BA-TOTAL-CREDIT
DISPLAY 'BATCH ' BH-BATCH-NUMBER
' CREDIT TOTAL MISMATCH'
ADD 1 TO WS-BATCH-ERRORS
END-IF
* ACCUMULATE FILE-LEVEL TOTALS FROM BATCH
ADD WS-BA-ENTRY-COUNT TO WS-FA-ENTRY-COUNT
ADD WS-BA-ENTRY-HASH TO WS-FA-ENTRY-HASH
ADD WS-BA-TOTAL-DEBIT TO WS-FA-TOTAL-DEBIT
ADD WS-BA-TOTAL-CREDIT TO WS-FA-TOTAL-CREDIT
.
*================================================================*
7000-VALIDATE-FILE-CONTROL.
*================================================================*
IF FC-BATCH-COUNT NOT = WS-FA-BATCH-COUNT
DISPLAY 'FILE BATCH COUNT MISMATCH: '
'EXPECTED=' FC-BATCH-COUNT
' ACTUAL=' WS-FA-BATCH-COUNT
ADD 1 TO WS-FILE-ERRORS
END-IF
IF FC-ENTRY-COUNT NOT = WS-FA-ENTRY-COUNT
DISPLAY 'FILE ENTRY COUNT MISMATCH: '
'EXPECTED=' FC-ENTRY-COUNT
' ACTUAL=' WS-FA-ENTRY-COUNT
ADD 1 TO WS-FILE-ERRORS
END-IF
IF FC-TOTAL-DEBIT NOT = WS-FA-TOTAL-DEBIT
DISPLAY 'FILE DEBIT TOTAL MISMATCH'
ADD 1 TO WS-FILE-ERRORS
END-IF
IF FC-TOTAL-CREDIT NOT = WS-FA-TOTAL-CREDIT
DISPLAY 'FILE CREDIT TOTAL MISMATCH'
ADD 1 TO WS-FILE-ERRORS
END-IF
.
*================================================================*
9000-FINALIZE.
*================================================================*
PERFORM 9700-PRINT-SUMMARY
CLOSE ACH-INPUT-FILE
ACCT-MASTER-FILE
RETURN-OUTPUT-FILE
REPORT-FILE
IF WS-FILE-ERRORS > 0
MOVE 8 TO RETURN-CODE
ELSE IF WS-BATCH-ERRORS > 0
MOVE 4 TO RETURN-CODE
ELSE
MOVE 0 TO RETURN-CODE
END-IF
.
*================================================================*
9500-PRINT-REPORT-HEADER.
*================================================================*
ADD 1 TO WS-PAGE-COUNT
MOVE WS-PAGE-COUNT TO WS-RH1-PAGE
MOVE WS-CURRENT-DATE TO WS-RH1-DATE
WRITE REPORT-RECORD FROM WS-RPT-HEADER-1
AFTER ADVANCING PAGE
WRITE REPORT-RECORD FROM WS-RPT-HEADER-2
AFTER ADVANCING 2 LINES
MOVE SPACES TO REPORT-RECORD
WRITE REPORT-RECORD AFTER ADVANCING 1 LINE
MOVE 4 TO WS-LINE-COUNT
.
*================================================================*
9600-WRITE-DETAIL-LINE.
*================================================================*
IF WS-LINE-COUNT >= 55
PERFORM 9500-PRINT-REPORT-HEADER
END-IF
WRITE REPORT-RECORD FROM WS-RPT-DETAIL
AFTER ADVANCING 1 LINE
ADD 1 TO WS-LINE-COUNT
.
*================================================================*
9700-PRINT-SUMMARY.
*================================================================*
MOVE SPACES TO REPORT-RECORD
WRITE REPORT-RECORD AFTER ADVANCING 2 LINES
STRING 'ACH PROCESSING SUMMARY'
DELIMITED BY SIZE INTO REPORT-RECORD
WRITE REPORT-RECORD AFTER ADVANCING 1 LINE
MOVE SPACES TO REPORT-RECORD
STRING ' RECORDS READ: '
WS-RECORDS-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 ' CREDITS POSTED: '
WS-CREDITS-POSTED
DELIMITED BY SIZE INTO REPORT-RECORD
WRITE REPORT-RECORD AFTER ADVANCING 1 LINE
MOVE SPACES TO REPORT-RECORD
STRING ' DEBITS POSTED: '
WS-DEBITS-POSTED
DELIMITED BY SIZE INTO REPORT-RECORD
WRITE REPORT-RECORD AFTER ADVANCING 1 LINE
MOVE SPACES TO REPORT-RECORD
STRING ' ENTRIES RETURNED: '
WS-ENTRIES-RETURNED
DELIMITED BY SIZE INTO REPORT-RECORD
WRITE REPORT-RECORD AFTER ADVANCING 1 LINE
MOVE SPACES TO REPORT-RECORD
STRING ' BATCH ERRORS: '
WS-BATCH-ERRORS
DELIMITED BY SIZE INTO REPORT-RECORD
WRITE REPORT-RECORD AFTER ADVANCING 1 LINE
MOVE SPACES TO REPORT-RECORD
STRING ' FILE ERRORS: '
WS-FILE-ERRORS
DELIMITED BY SIZE INTO REPORT-RECORD
WRITE REPORT-RECORD AFTER ADVANCING 1 LINE
.
Companion JCL
//ACHPROC JOB (HFCU,BATCH),'ACH PROCESSING',
// CLASS=A,MSGCLASS=X,MSGLEVEL=(1,1),
// NOTIFY=&SYSUID
//*
//*-----------------------------------------------------------*
//* ACH PAYMENT PROCESSING - NIGHTLY BATCH *
//* RECEIVES NACHA FILE FROM FED, POSTS ENTRIES, *
//* GENERATES RETURNS AND AUDIT REPORT *
//*-----------------------------------------------------------*
//STEP01 EXEC PGM=ACHPROC
//STEPLIB DD DSN=HFCU.PROD.LOADLIB,DISP=SHR
//ACHINPUT DD DSN=HFCU.ACH.INBOUND.NACHA,DISP=SHR,
// DCB=(RECFM=FB,LRECL=94,BLKSIZE=940)
//ACCTMAST DD DSN=HFCU.ACCT.MASTER.VSAM,DISP=SHR
//ACHRETRN DD DSN=HFCU.ACH.RETURNS.NACHA,
// DISP=(NEW,CATLG,DELETE),
// SPACE=(CYL,(5,2)),
// DCB=(RECFM=FB,LRECL=94,BLKSIZE=940)
//ACHRPT DD SYSOUT=*,DCB=(RECFM=FBA,LRECL=133,BLKSIZE=0)
//SYSOUT DD SYSOUT=*
//SYSPRINT DD SYSOUT=*
Walkthrough
The ACHPROC program follows a structured, top-down design that mirrors the hierarchical organization of the NACHA file itself.
Initialization (paragraph 0100) opens all four files and prints the report header. The program uses FUNCTION CURRENT-DATE to capture the processing timestamp, which is used throughout the report and audit trail.
File Header Processing (paragraph 1000) reads the first record, confirms it is a Record Type 1 (File Header), and validates the structural fields: priority code, record size, and format code. If the file header is invalid, the program terminates immediately with return code 12, because a malformed file header indicates the entire file may be corrupt.
Batch Processing (paragraph 3000) handles the core processing loop. When a Record Type 5 (Batch Header) is encountered, the program resets its batch-level accumulators and begins processing Entry Detail records. The batch header fields -- company name, SEC code, effective date -- are preserved for reference during entry processing. The loop continues reading records until a Record Type 8 (Batch Control) is found, at which point the accumulated totals are validated against the control record.
Entry Processing (paragraph 4000 and 4100) is the heart of the program. For each Entry Detail record (Record Type 6), the program first accumulates the entry into the batch-level totals (entry count, entry hash, debit and credit amounts). It then dispatches based on transaction code: codes 22 and 32 are credits, codes 27 and 37 are debits, and prenote codes (23, 28, 33, 38) are validated but not posted. Invalid transaction codes trigger an immediate return with reason code R17.
The validation sequence in paragraph 4100 follows a strict order: account existence (R03), account status checks for closed (R02) and frozen (R16), account type matching against transaction code (R04), and insufficient funds for debits (R01). This ordering ensures that the most specific and actionable return reason is used. When an entry passes all validations, the program updates both the ledger and available balances and REWRITEs the account master record.
Return Generation (paragraph 5000) writes the original entry to the return file and logs the return with its reason code on the processing report. In a production system, the return file would include additional addenda records containing the return reason code, the original entry date, and the original trace number, formatted according to NACHA return entry specifications.
Batch Control Validation (paragraph 6000) compares the accumulated totals against the Batch Control record. Any mismatch is reported and counted. Regardless of whether the batch passes validation, its totals are accumulated into the file-level counters for later validation against the File Control record.
File Control Validation (paragraph 7000) performs the top-level integrity check. The batch count, entry count, total debits, and total credits must all match between the accumulated values and the File Control record. Mismatches at this level indicate structural corruption of the ACH file.
Finalization (paragraph 9000) prints the processing summary, closes all files, and sets the return code. A return code of 0 indicates clean processing; 4 indicates batch-level warnings; 8 indicates file-level errors. The calling JCL can use COND parameters to control subsequent job steps based on this return code.
Discussion Questions
-
The program validates batch control totals after processing all entries in the batch. What would be the trade-off of validating the batch control totals before posting any entries? How would this affect the program's design and the member experience?
-
The entry hash in NACHA is calculated by summing the Receiving DFI Identification fields (positions 4-11) across all entries. If the sum exceeds 10 digits, only the rightmost 10 digits are used. Why does NACHA use this particular hash function rather than a cryptographic hash like SHA-256? What types of errors does the entry hash detect, and what types does it miss?
-
The program currently generates returns one at a time as entries fail validation. In a production environment, returns must be formatted as complete NACHA files with their own File Header, Batch Header, Entry Detail (with return addenda), Batch Control, and File Control records. How would you restructure the program to accumulate returns and generate a properly formatted return file?
-
Duplicate detection is listed as a requirement but is not implemented in this version. Design a duplicate detection mechanism that checks whether an incoming ACH entry has already been processed. Consider: what fields uniquely identify an ACH entry? How would you store previously processed entries for lookup? What is the performance impact of duplicate checking on a file with 60,000 entries?
-
ACH entries have an effective date that may be in the future. How would you modify the program to handle forward-dated entries -- entries that should not be posted until a future date? Where would you store these entries between receipt and posting?
-
Consider the regulatory implications of the R01 (Insufficient Funds) return. Under NACHA rules, the RDFI has two business days to return an entry for insufficient funds. If HFCU posts an ACH debit that causes an overdraft, should the program automatically return the entry, or should it post the debit and let the overdraft processing handle it? What are the member service implications of each approach?