Repository: moov-io/ach
Pull request #1109 (2022-10-21 20:15:59)

feat: add Reversal() to files
Closes: https://github.com/moov-io/ach/issues/756


Patch:
diff --git a/docs/_data/docs-menu.yml b/docs/_data/docs-menu.yml
index d5cd6b1f..e2b4e11e 100644
--- a/docs/_data/docs-menu.yml
+++ b/docs/_data/docs-menu.yml
@@ -38,10 +38,12 @@
 
 - label: File operations
   items:
-    - name: Custom validation
-      link: /custom-validation/
     - name: Balanced offset
       link: /balanced-offset/
+    - name: Change files
+      link: /changes/
+    - name: Custom validation
+      link: /custom-validation/
     - name: Flatten batches
       link: /flatten-batches/
     - name: Merging files
@@ -50,8 +52,8 @@
       link: /segment-file/
     - name: Return files
       link: /returns/
-    - name: Change files
-      link: /changes/
+    - name: Reversal Files
+      link: /reversals/
 
 - label: Production and monitoring
   items:
diff --git a/docs/reversals.md b/docs/reversals.md
new file mode 100644
index 00000000..30d54b9d
--- /dev/null
+++ b/docs/reversals.md
@@ -0,0 +1,42 @@
+---
+layout: page
+title: Reversal Files
+hide_hero: true
+show_sidebar: false
+menubar: docs-menu
+---
+
+# Reversal Files
+
+Often ACH files are sent in error and need to be undone. Nacha has a process for creating a "reversal file" which needs to follow a few steps.
+All other fields should be left untouched so the receiver of a reversal can match the Entry Detail records.
+
+- Replace the value in Company Entry Description with `REVERSAL`
+- Update Effective Entry Date in the future depending on same-day or standard ACH.
+- Update each Transaction Code to undo fund movement.
+
+## Creation
+
+The Moov ACH library offers a `Reversal(..)` function on files.
+
+```go
+func (f *File) Reversal(effectiveEntryDate time.Time) error
+    Reversal will transform a File into a Nacha compliant reversal which can be
+    transmitted to undo fund movement.
+```
+
+```go
+file, err := ach.ReadFile(..) // read from audit storage
+if err != nil {
+    // handle error
+}
+
+err = file.Reversal()
+if err != nil {
+    // handle error
+}
+
+// Now `file` has all Batches and Entries converted into Reversals
+```
+
+See an [example in Go code](https://github.com/moov-io/ach/tree/master/examples/reversals/) of reversing files.
diff --git a/examples/reversal/main.go b/examples/reversal/main.go
new file mode 100644
index 00000000..46ae1bd6
--- /dev/null
+++ b/examples/reversal/main.go
@@ -0,0 +1,49 @@
+// Licensed to The Moov Authors under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. The Moov Authors licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/moov-io/ach"
+	"github.com/moov-io/ach/cmd/achcli/describe"
+)
+
+func main() {
+	file, err := ach.ReadJSONFile(filepath.Join("test", "testdata", "ppd-valid.json"))
+	if err != nil {
+		log.Fatalf("ERROR: unable to read file: %v", err)
+	}
+
+	fmt.Println("Original:")
+	describe.File(os.Stdout, file, nil)
+	fmt.Printf("\n\n")
+
+	effectiveEntryDate := time.Now().In(time.UTC)
+	err = file.Reversal(effectiveEntryDate)
+	if err != nil {
+		log.Fatalf("ERROR: problem reversing file: %v", err)
+	}
+
+	fmt.Println("Reversal:")
+	describe.File(os.Stdout, file, nil)
+}
diff --git a/reversal.go b/reversal.go
new file mode 100644
index 00000000..5da295f6
--- /dev/null
+++ b/reversal.go
@@ -0,0 +1,69 @@
+// Licensed to The Moov Authors under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. The Moov Authors licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package ach
+
+import (
+	"time"
+)
+
+// Reversal will transform a File into a Nacha compliant reversal which can be transmitted to undo fund movement.
+func (f *File) Reversal(effectiveEntryDate time.Time) error {
+	for i := range f.Batches {
+		bh := f.Batches[i].GetHeader()
+
+		// Must submit a Reversing Entry within a batch that includes the word "REVERSAL" in the
+		// Company Entry Description field of the Company/Batch Header Record.
+		//
+		// The description “REVERSAL” must replace the original content of the Company Entry Description
+		// field transmitted in the original batch, including content that may otherwise have been required by The Rules.
+		bh.CompanyEntryDescription = "REVERSAL"
+
+		// For each Reversing Entry, the content on the following fields must remain unchanged from the original,
+		// erroneous Entry to which the Reversal relates:
+		//  - Standard Entry Class Code
+		//  - Company Identification/Originator Identification
+		//  - Amount
+		//  - Update the following records according to the fund flow
+
+		// Adjust Effective Entry Date for same-day vs standard
+		bh.EffectiveEntryDate = effectiveEntryDate.Format("060102")
+
+		f.Batches[i].SetHeader(bh)
+
+		// In EntryDetail records we need to update the TransactionCode fields to undo fund movement.
+		entries := f.Batches[i].GetEntries()
+		for j := range entries {
+			switch entries[j].TransactionCode {
+			case
+				CheckingCredit, CheckingReturnNOCCredit, CheckingPrenoteCredit, CheckingZeroDollarRemittanceCredit,
+				GLCredit, GLPrenoteCredit, GLReturnNOCCredit, GLZeroDollarRemittanceCredit,
+				LoanCredit, LoanPrenoteCredit, LoanReturnNOCCredit, LoanZeroDollarRemittanceCredit,
+				SavingsCredit, SavingsPrenoteCredit, SavingsReturnNOCCredit, SavingsZeroDollarRemittanceCredit:
+				entries[j].TransactionCode += 5
+
+			case
+				CheckingDebit, CheckingPrenoteDebit, CheckingReturnNOCDebit, CheckingZeroDollarRemittanceDebit,
+				GLDebit, GLPrenoteDebit, GLReturnNOCDebit, GLZeroDollarRemittanceDebit,
+				LoanDebit, LoanReturnNOCDebit,
+				SavingsDebit, SavingsPrenoteDebit, SavingsReturnNOCDebit, SavingsZeroDollarRemittanceDebit:
+				entries[j].TransactionCode -= 5
+			}
+		}
+	}
+	return nil
+}


Test patch:
diff --git a/reversal_test.go b/reversal_test.go
new file mode 100644
index 00000000..de81d05e
--- /dev/null
+++ b/reversal_test.go
@@ -0,0 +1,58 @@
+// Licensed to The Moov Authors under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. The Moov Authors licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package ach
+
+import (
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestFileCreditReversal(t *testing.T) {
+	file, err := ReadJSONFile(filepath.Join("test", "testdata", "ppd-valid.json"))
+	require.NoError(t, err)
+
+	effectiveEntryDate := time.Now().In(time.UTC)
+	err = file.Reversal(effectiveEntryDate)
+	require.NoError(t, err)
+
+	b1 := file.Batches[0]
+	require.Equal(t, "REVERSAL", b1.GetHeader().CompanyEntryDescription)
+
+	entries := b1.GetEntries()
+	require.Len(t, entries, 1)
+	require.Equal(t, CheckingDebit, entries[0].TransactionCode)
+}
+
+func TestFileDebitReversal(t *testing.T) {
+	file, err := ReadFile(filepath.Join("test", "testdata", "ppd-debit.ach"))
+	require.NoError(t, err)
+
+	effectiveEntryDate := time.Now().In(time.UTC)
+	err = file.Reversal(effectiveEntryDate)
+	require.NoError(t, err)
+
+	b1 := file.Batches[0]
+	require.Equal(t, "REVERSAL", b1.GetHeader().CompanyEntryDescription)
+
+	entries := b1.GetEntries()
+	require.Len(t, entries, 1)
+	require.Equal(t, CheckingCredit, entries[0].TransactionCode)
+}