Repository: moov-io/ach
Pull request #1046 (2022-07-14 01:32:34)

Fix entry category assignment in Reader
https://github.com/moov-io/ach/issues/1045


Patch:
diff --git a/reader.go b/reader.go
index 518aeef2..ac8884fb 100644
--- a/reader.go
+++ b/reader.go
@@ -401,14 +401,14 @@ func (r *Reader) parseEntryDetail() error {
 		return ErrFileEntryOutsideBatch
 	}
 	if r.currentBatch.GetHeader().StandardEntryClassCode != ADV {
-		ed := new(EntryDetail)
+		ed := NewEntryDetail()
 		ed.Parse(r.line)
 		if err := ed.Validate(); err != nil {
 			return r.parseError(err)
 		}
 		r.currentBatch.AddEntry(ed)
 	} else {
-		ed := new(ADVEntryDetail)
+		ed := NewADVEntryDetail()
 		ed.Parse(r.line)
 		if err := ed.Validate(); err != nil {
 			return r.parseError(err)
@@ -468,6 +468,7 @@ func (r *Reader) parseAddenda() error {
 						return r.parseError(err)
 					}
 					r.currentBatch.GetEntries()[entryIndex].Addenda99Dishonored = addenda99Dishonored
+					r.currentBatch.GetEntries()[entryIndex].Category = CategoryDishonoredReturn
 
 				case IsContestedReturnCode(r.line[3:6]):
 					addenda99Contested := NewAddenda99Contested()
@@ -477,6 +478,7 @@ func (r *Reader) parseAddenda() error {
 						return r.parseError(err)
 					}
 					r.currentBatch.GetEntries()[entryIndex].Addenda99Contested = addenda99Contested
+					r.currentBatch.GetEntries()[entryIndex].Category = CategoryDishonoredReturnContested
 
 				default:
 					addenda99 := NewAddenda99()
@@ -486,8 +488,8 @@ func (r *Reader) parseAddenda() error {
 						return r.parseError(err)
 					}
 					r.currentBatch.GetEntries()[entryIndex].Addenda99 = addenda99
+					r.currentBatch.GetEntries()[entryIndex].Category = CategoryReturn
 				}
-				r.currentBatch.GetEntries()[entryIndex].Category = CategoryReturn
 			}
 		} else {
 			return r.parseError(r.currentBatch.Error("AddendaRecordIndicator", ErrBatchAddendaIndicator))


Test patch:
diff --git a/reader_test.go b/reader_test.go
index 3a27f48a..c3c7439b 100644
--- a/reader_test.go
+++ b/reader_test.go
@@ -28,6 +28,7 @@ import (
 	"testing"
 
 	"github.com/moov-io/base"
+	"github.com/stretchr/testify/require"
 )
 
 func readACHFilepath(path string) (*File, error) {
@@ -1755,6 +1756,108 @@ func testACHFileTooLongErr(t testing.TB) {
 	maxLines = 2 + 2000000 + 100000000 + 8
 }
 
+// TestCategoryAssignment ensures entry categories are set correctly
+func TestCategoryAssignment(t *testing.T) {
+	// Create a file containing entries of each category.
+	file := NewFile().SetHeader(mockFileHeader())
+
+	// "Forward"
+	forwardEntry := mockEntryDetail()
+	forwardEntry.Category = CategoryForward
+	forwardEntry.DiscretionaryData = "01"
+	forwardBatch := NewBatchWEB(mockBatchWEBHeader())
+	forwardBatch.AddEntry(forwardEntry)
+	if err := forwardBatch.Create(); err != nil {
+		t.Fatal(err)
+	}
+	file.AddBatch(forwardBatch)
+
+	// "Return"
+	returnEntry := mockEntryDetail()
+	returnEntry.Addenda99 = mockAddenda99()
+	returnEntry.AddendaRecordIndicator = 1
+	returnEntry.Category = CategoryReturn
+	returnEntry.DiscretionaryData = "02"
+	returnBatch := NewBatchWEB(mockBatchWEBHeader())
+	returnBatch.AddEntry(returnEntry)
+	if err := returnBatch.Create(); err != nil {
+		t.Fatal(err)
+	}
+	file.AddBatch(returnBatch)
+
+	// "DishonoredReturn"
+	dishonoredReturnEntry := mockEntryDetail()
+	dishonoredReturnEntry.Addenda99Dishonored = mockAddenda99Dishonored()
+	dishonoredReturnEntry.AddendaRecordIndicator = 1
+	dishonoredReturnEntry.Category = CategoryDishonoredReturn
+	dishonoredReturnEntry.DiscretionaryData = "03"
+	dishonoredReturnBatch := NewBatchWEB(mockBatchWEBHeader())
+	dishonoredReturnBatch.AddEntry(dishonoredReturnEntry)
+	if err := dishonoredReturnBatch.Create(); err != nil {
+		t.Fatal(err)
+	}
+	file.AddBatch(dishonoredReturnBatch)
+
+	// "DishonoredReturnContested"
+	contestedDishonoredReturnEntry := mockEntryDetail()
+	contestedDishonoredReturnEntry.Addenda99Contested = mockAddenda99Contested()
+	contestedDishonoredReturnEntry.AddendaRecordIndicator = 1
+	contestedDishonoredReturnEntry.Category = CategoryDishonoredReturnContested
+	contestedDishonoredReturnEntry.DiscretionaryData = "04"
+	contestedDishonoredReturnBatch := NewBatchWEB(mockBatchWEBHeader())
+	contestedDishonoredReturnBatch.AddEntry(contestedDishonoredReturnEntry)
+	if err := contestedDishonoredReturnBatch.Create(); err != nil {
+		t.Fatal(err)
+	}
+	file.AddBatch(contestedDishonoredReturnBatch)
+
+	// "NOC"
+	nocEntry := mockCOREntryDetail()
+	nocEntry.Addenda98 = mockAddenda98()
+	nocEntry.AddendaRecordIndicator = 1
+	nocEntry.Category = CategoryNOC
+	nocEntry.DiscretionaryData = "05"
+	nocBatch := NewBatchCOR(mockBatchCORHeader())
+	nocBatch.AddEntry(nocEntry)
+	if err := nocBatch.Create(); err != nil {
+		t.Fatal(err)
+	}
+	file.AddBatch(nocBatch)
+
+	if err := file.Create(); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+	if err := file.Validate(); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+
+	b := &bytes.Buffer{}
+	f := NewWriter(b)
+
+	if err := f.Write(file); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+
+	r := NewReader(strings.NewReader(b.String()))
+	readFile, err := r.Read()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	categoriesByDiscretionaryData := map[string]string{}
+	for _, batch := range readFile.Batches {
+		for _, entry := range batch.GetEntries() {
+			categoriesByDiscretionaryData[entry.DiscretionaryData] = entry.Category
+		}
+	}
+
+	require.Equal(t, CategoryForward, categoriesByDiscretionaryData["01"])
+	require.Equal(t, CategoryReturn, categoriesByDiscretionaryData["02"])
+	require.Equal(t, CategoryDishonoredReturn, categoriesByDiscretionaryData["03"])
+	require.Equal(t, CategoryDishonoredReturnContested, categoriesByDiscretionaryData["04"])
+	require.Equal(t, CategoryNOC, categoriesByDiscretionaryData["05"])
+}
+
 // TestACHFileTooLongErr checks that it errors on a file that is too long
 func TestACHFileTooLongErr(t *testing.T) {
 	testACHFileTooLongErr(t)