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:

  1. The old program does not validate ACH batch control totals, allowing corrupted files to be partially processed before errors are detected.
  2. Return reason codes are hard-coded rather than table-driven, making it difficult to add new return codes as NACHA rules evolve.
  3. The program does not detect duplicate entries, resulting in occasional double-posting when the Federal Reserve retransmits a file.
  4. 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:

  1. File Header Validation: Parse and validate the NACHA File Header record.
  2. Batch Processing Loop: For each batch in the file, parse the Batch Header, process all Entry Detail records, and validate the Batch Control record.
  3. Entry Posting: For each valid entry, read the member's account, apply the debit or credit, and update the account master file.
  4. Return Generation: For entries that fail validation or cannot be posted, generate return entries in NACHA format.
  5. 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

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

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

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

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

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

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