nacha-report.ps1
<#PSScriptInfo
.VERSION 1.0.8 .GUID 2687ebd5-b9f5-403a-bf2b-13fed20fd6cd .AUTHOR Lawrence Billinghurst larry@trifused.com .COMPANYNAME TriFused .COPYRIGHT 2024 .TAGS NACHA ACH BANKING FINTECH .LICENSEURI https://github.com/Trifused/nacha-report/blob/main/LICENSE .PROJECTURI https://github.com/Trifused/nacha-report .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES .PRIVATEDATA #> <# .DESCRIPTION NACHA - NACHA (National Automated Clearing House Association) is the organization that manages the development, administration, and governance of the ACH Network in the United States. The ACH Network is a payment system that allows for the electronic transfer of funds between banks and credit unions. The NACHA file format adheres to a structure where each line, referred to as a **record**, contains exactly 94 characters, making also know as fixed-width ASCII file format. These records are organized into various **fields**, each occupying a predetermined position within the line. A NACHA file contains 6 different types of records: Type 1. **File Header**: Starts the file, with details like company name and file creation date. Type 5. **Batch Header**: Begins a group of transactions, indicating the payment type and originator. Type 6. **Entry Detail**: Represents individual financial transactions, detailing account numbers and amounts. Type 7. **Addenda**: Optional, provides extra information for a transaction. Type 8. **Batch Control**: Ends a batch, summarizing its transactions and total amount. Type 9. **File Control**: Concludes the file, summarizing all batches and entries. #> ############################################################################# # TriFused - Nacha-Report # # NAME: Nacha-Report.ps1 # # AUTHOR: Lawrence Billinghurst # DATE: 3/20/2024 # EMAIL: larry@trifused.com # # VERSION HISTORY # 1.0 2024-03-20 Initial Version. # Current: 1.0.7 # > used field mapping from Joshua Nasiatka - Verify-ACH.ps1 # Ref: https://github.com/jossryan/ACH-Verify-Tool # # ACH test data generator - https://yawetse.github.io/nachie/ # ############################################################################# <# .SYNOPSIS This script reads a NACH file and outputs a summeary report with out any sensitive account informaion .DESCRIPTION NACHA - NACHA (National Automated Clearing House Association) is the organization that manages the development, administration, and governance of the ACH Network in the United States. The ACH Network is a payment system that allows for the electronic transfer of funds between banks and credit unions. The NACHA file format adheres to a structure where each line, referred to as a **record**, contains exactly 94 characters, making also know as fixed-width ASCII file format. These records are organized into various **fields**, each occupying a predetermined position within the line. A NACHA file contains 6 different types of records: Type 1. **File Header**: Starts the file, with details like company name and file creation date. Type 5. **Batch Header**: Begins a group of transactions, indicating the payment type and originator. Type 6. **Entry Detail**: Represents individual financial transactions, detailing account numbers and amounts. Type 7. **Addenda**: Optional, provides extra information for a transaction. Type 8. **Batch Control**: Ends a batch, summarizing its transactions and total amount. Type 9. **File Control**: Concludes the file, summarizing all batches and entries. .PARAMETER ParameterName -nachaFilePath C:\FolderA\FolderB\mynachafile.txt -- path to nacha file (Any extension will work) -testdata -- Will auto download some test data -showTrace6 -- Will show the Trace codes for type 6 -no ### -- remove record types from report .EXAMPLE .\nacha-neport.ps1 -nachaFielPath .\nacha-report.ps1 -testdata -- Use Test data - will prompt to download .\nacha-report.ps1 -no 67 -- Remove type 6 and 7 from report .\nacha-report.ps1 -no 5678 -- Remove type 5, 6, 7 and 8 from report .NOTES Additional information about the script, like its version, author, or history. # ############################################################################# # VERSION HISTORY # 1.0 2024-03-20 Initial Version. # > used ACH field mapping from Joshua Nasiatka - Verify-ACH.ps1 // Thanks!! # Ref: https://github.com/jossryan/ACH-Verify-Tool # # NACH test data generator - https://yawetse.github.io/nachie/ # ############################################################################# Report Format --------->>> NACHA File Report <<<--------- NACHA File Name: ach-test-file.txt NACHA File Date: January 06, 2015 NACHA File Time: 12:13 PM 1, [File Date-YYMMDD], [File Time-HHmm], [Destination Name], [Org Name] 5, [Batch Number], [Transaction Description], [Effective Date] 6, [Trans Code], {[Trace Number]}, [Reciver Name], [Amount] 6, [Trans Code], {[Trace Number]}, [Reciver Name], [(Amount) <--debit] 7, [Addenda Type Code], [Payment Related Information], [Addenda Sequence Number], [Entry Detail Sequence Number] 8, [Batch Number], [Lines in Batch], [(Debit Total)],[Credit Total] 9, [Batch Count],[Block Count], [Entry Count], [(Debit Total)],[Credit Total] --------->>> NACHA File Report End <<<--------- .LINK A link to more information or documentation related to the script. #> # Script logic starts here param ( [string]$nachaFilePath="" ,[switch]$showTrace6 ,[string]$no ,[switch]$testdata ) # Get the directory where the script is located $scriptDirectory = $PSScriptRoot # Output the directory path Write-Host "`This Script is running from: $scriptDirectory" $defultTestDataFileName = $scriptDirectory + "\test-nacha-file.txt" # Test data and file processing if (-not ($testdata) ){ if (-not $nachaFilePath) { Write-Host "Usage: ./nacha-report.ps1 -nachaFilePath <Path to NACHA file>" Write-Host "Usage: ./nacha-report.ps1 -testdata" exit 1 } } else{ if ($testdata) { if (-not (Test-Path $defultTestDataFileName )) { #Write-Host "Test data file not found downloading." Write-Host "Test data file not found." # Ask the user if they want to download the test nacha file $testfileuri = "https://drive.google.com/file/d/1-tEJ6Y_KMvUIuL55DG1oddekG9cD2WMN" Write-Host "View the test nacha data file at: $($testfileuri)/view" $userInput = Read-Host "Do you want to download the test nacha data file? (Y/N)" if ($userInput -eq 'Y' -or $userInput -eq 'y') { # Assuming $defultTestDataFileName is defined earlier in the script $outputPath = $defultTestDataFileName # The direct download URL should be different from the view URL # Convert Google Drive view link to download link (this may require a different approach for actual downloading) $url = $testfileuri.Replace("/file/d/", "/uc?export=download&id=").Replace("/view", "") # Create a web client object $client = New-Object System.Net.WebClient try { $client.DownloadFile($url, $outputPath) Write-Host "File downloaded successfully to: $outputPath" } catch { Write-Host "An error occurred during file download: $_" } } else { Write-Host "Download canceled by the user." exit 1 } } $nachaFilePath $nachaFilePath = $defultTestDataFileName $nachaFilePath } #$nachaFilePath = $defultTestDataFileName if (-not (Test-Path $nachaFilePath) ) { Write-Host "Error: File not found." exit 1 } } Function ReadACHLine ($line) { # Get record type # 1 File Header Record # 5 Company/Batch Header Record # 6 Entry Detail Record (CCD/PPD Entries) # 7 Addenda Record # 8 Batch Control Record # 9 File Control Record # used field mapping from Joshua Nasiatka - Verify-ACH.ps1 script $record_type = $line.substring(0,1).trim() if ($line -ne '9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999') { } # 1 - FILE HEADER RECORD if ($record_type -eq '1') { $record_details = [PSCustomObject]@{ 'record_type' = $line.substring(0,1).trim() 'priority_code' = $line.substring(1,2).trim() 'immediate_destination' = $line.substring(3,10).trim() 'immediate_origin' = $line.substring(13,10).trim() 'file_creation_date' = $line.substring(23,6).trim() 'file_creation_time' = $line.substring(29,4).trim() 'file_id_modifier' = $line.substring(33,1).trim() 'record_size' = $line.substring(34,3).trim() 'blocking_factor' = $line.substring(37,2).trim() 'format_code' = $line.substring(39,1).trim() 'immediate_destination_name' = $line.substring(40,23).trim() 'immediate_origin_name' = $line.substring(63,23).trim() 'reference_code' = $line.substring(86,8).trim() } # ##### Add Line to Output Record $ACHContents.Add($record_details) |Out-Null # ##### # 5 - COMPANY/BATCH HEADER RECORD } elseif ($record_type -eq '5') { $record_details = [PSCustomObject]@{ 'record_type' = $line.substring(0,1).trim() 'service_class_code' = $line.substring(1,3).trim() 'company_name' = $line.substring(4,16).trim() 'company_discretionary_data_5' = $line.substring(20,20).trim() 'company_identification' = $line.substring(40,10).trim() 'standard_entry_class_code' = $line.substring(50,3).trim() 'company_entry_description' = $line.substring(53,10).trim() 'company_descriptive_date' = $line.substring(63,6).trim() 'effective_entry_date' = $line.substring(69,6).trim() 'settlement_date' = $line.substring(75,3).trim() 'originator_status_code' = $line.substring(78,1).trim() 'originating_dfi_identification' = $line.substring(79,8).trim() 'batch_number' = $line.substring(87,7).trim() } # ##### Add Line to Output Record $ACHContents.Add($record_details) |Out-Null # ##### # 6 - ENTRY DETAIL RECORD (CCD/PPD ENTRIES) } elseif ($record_type -eq '6') { $record_details = [PSCustomObject]@{ 'record_type' = $line.substring(0,1).trim() 'transaction_code' = $line.substring(1,2).trim() 'receiving_dfi_identification' = $line.substring(3,8).trim() 'check_digit' = $line.substring(11,1).trim() 'dfi_account_number' = $line.substring(12,17).trim() 'amount' = $(try{($line.substring(29,10).trim())/100}catch{$line.substring(29,10).trim()}) 'individual_identification_number' = $line.substring(39,15).trim() 'individual_name' = $line.substring(54,22).trim() 'company_discretionary_data_6' = $line.substring(76,2).trim() 'addenda_record_indicator' = $line.substring(78,1).trim() 'trace_number' = $line.substring(79,15).trim() } # ##### Add Line to Output Record $ACHContents.Add($record_details) |Out-Null # ##### # 7 - ADDENDA RECORD } elseif ($record_type -eq '7') { $record_details = [PSCustomObject]@{ 'record_type' = $line.substring(0,1).trim() 'addenda_type_code' = $line.substring(1,2).trim() 'addenda_related' = $line.substring(3,80).trim() 'addenda_sequence_number' = $line.substring(83,4).trim() 'entry_detail_sequence_number' = $line.substring(87,7).trim() } # ##### Add Line to Output Record $ACHContents.Add($record_details) |Out-Null # ##### # 8 - BATCH CONTROL RECORD } elseif ($record_type -eq '8') { $record_details = [PSCustomObject]@{ 'record_type' = $line.substring(0,1).trim() 'service_class_code' = $line.substring(1,3).trim() 'entry_addenda_count_8' = $line.substring(4,6).trim() 'entry_hash' = $line.substring(10,10).trim() 'total_debit_entry' = $(try{($line.substring(20,12).trim())/100}catch{$line.substring(20,12).trim()}) 'total_credit_entry' = $(try{($line.substring(32,12).trim())/100}catch{$line.substring(32,12).trim()}) 'company_identification' = $line.substring(44,10).trim() 'message_authorization_code' = $line.substring(54,19).trim() 'reserved_8' = $line.substring(73,6).trim() 'originating_dfi_identification' = $line.substring(79,8).trim() 'batch_number' = $line.substring(87,7).trim() } # ##### Add Line to Output Record $ACHContents.Add($record_details) |Out-Null # ##### # 9 - FILE CONTROL RECORD } elseif ($record_type -eq '9') { if ($line -ne '9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999') { $record_details = [PSCustomObject]@{ 'record_type' = $line.substring(0,1).trim() 'batch_count' = $line.substring(1,6).trim() 'block_count' = $line.substring(7,6).trim() 'entry_addenda_count_9' = $line.substring(13,8).trim() 'entry_hash' = $line.substring(21,10).trim() 'total_debit_entry_in_file' = $(try{($line.substring(31,12).trim())/100}catch{$line.substring(31,12).trim()}) 'total_credit_entry_in_file' = $(try{($line.substring(43,12).trim())/100}catch{$line.substring(43,12).trim()}) 'reserved_9' = $line.substring(55,39).trim() } # ##### Add Line to Output Record $ACHContents.Add($record_details) |Out-Null # ##### } else { #Write-Output ">>>End of File" } # NO OTHER RECORD TYPES } else { Write-Warning "Invalid record, skipping..." return } } ############################################################################################################################################ # Load the nacha file in memory Write-Host $nachaFilePath $nachaFileContent = Get-Content $nachaFilePath #clear Output varable [System.Collections.ArrayList]$ACHContents = @() # Loop through each line and parse the contents foreach ($line in $nachaFileContent) { ReadACHLine($line) } Write-Host "NACHA File Parsing completed.`r`n" # Build the Report $NachaFileTime ="" $FinalReport = "`r`n--------->>> NACHA File Report <<<---------`r`n`n" # Clear and Start building report $NachafileName = Split-Path -Path $nachaFilePath -Leaf $FinalReport = $FinalReport + "NACHA File Name: " + $NachafileName + "`r`n" $ACHContents | ForEach-Object { $currentObject = $_ switch ($currentObject.record_type) { "1" { $NachaFileDate=$($currentObject.file_creation_date) $NachaFileTime=$($currentObject.file_creation_time) # Parse the date string into a DateTime object $parsedDate = [DateTime]::ParseExact($NachaFileDate, "yymmdd", $null) $parsedTime = [DateTime]::ParseExact($NachaFileTime, "HHmm", $null) # Convert the DateTime object into a long date format string and add to report $FinalReport += "NACHA File Date: " + $parsedDate.ToString("MMMM dd, yyyy") + "`r`n" # Add Date to Report $FinalReport += "NACHA File Time: " + $parsedTime.ToString("hh:mm tt") + "`r`n" # Add Time to Report if (-not($no.Contains($currentObject.record_type))) { $ReportOut = "$($currentObject.record_type), $($currentObject.file_creation_date), $($currentObject.file_creation_time), $($currentObject.immediate_destination_name), $($currentObject.immediate_origin_name)" $FinalReport += "`n"+$ReportOut # Add data to report break } } "5" { if (-not($no.Contains($currentObject.record_type))) { $ReportOut = " $($currentObject.record_type), Batch: $($currentObject.batch_number), $($currentObject.company_entry_description), $($currentObject.effective_entry_date)" $FinalReport += "`n"+$ReportOut # Add data to report break } } "6" { if (-not($no.Contains($currentObject.record_type))) { $TransactionCode = $($currentObject.transaction_code) switch ($TransactionCode){ { $_ -in "22", "32" } {$formattedamount = "{0:N2}" -f $($currentObject.amount)} { $_ -in "27", "37" } {$formattedamount = "({0:N2})" -f $($currentObject.amount)} } #ShowTrace6 Switch Logic $ReportOut = " $($currentObject.record_type), $TransactionCode" if ($ShowTrace6) { $ReportOut += ", $($currentObject.trace_number)" } $ReportOut += ", $($currentObject.individual_name), $formattedamount" $FinalReport += "`n"+$ReportOut # Add data to report break } } "7" { if ((-not($no.Contains($currentObject.record_type)))) { $ReportOut = " $($currentObject.record_type), $($currentObject.addenda_type_code), $($currentObject.entry_detail_sequence_number), $($currentObject.addenda_related)" $FinalReport += "`n"+$ReportOut # Add data to report $FinalReport += "`n"+$ReportOut # Add data to report break } } "8" { if (-not($no.Contains($currentObject.record_type))) { $formattedDebitTotal = "({0:N2})" -f $($currentObject.total_debit_entry) $formattedCreditTotal = "{0:N2}" -f $($currentObject.total_credit_entry) $ReportOut = " $($currentObject.record_type), Batch: $($currentObject.batch_number), $($currentObject.entry_addenda_count_8), $formattedDebitTotal,$formattedCreditTotal" $FinalReport += "`n"+$ReportOut # Add data to report break } } "9" { if (-not($no.Contains($currentObject.record_type))) { $formattedDebitTotal = "({0:N2})" -f $($currentObject.total_debit_entry_in_file) $formattedCreditTotal = "{0:N2}" -f $($currentObject.total_credit_entry_in_file) $ReportOut = "$($currentObject.record_type), $($currentObject.batch_count),$($currentObject.block_count), $($currentObject.entry_addenda_count_9), $formattedDebitTotal,$formattedCreditTotal"+"`n" $FinalReport += "`r`n`n"+$ReportOut # Add data to report - pushed 9 down 1 line break } } } } $FinalReport += "`n--------->>> NACHA File Report End <<<---------`n" # Add end to report return $FinalReport |