Repository: moov-io/ach
Pull request #1078 (2022-09-01 14:29:24)

fix: calculate EntryHash from ABA8, properly format string value
Issue: https://github.com/moov-io/ach/discussions/1077


Patch:
diff --git a/batch.go b/batch.go
index 9f7c1a9c..f33b989f 100644
--- a/batch.go
+++ b/batch.go
@@ -767,21 +767,16 @@ func (batch *Batch) calculateEntryHash() int {
 
 	if !batch.IsADV() {
 		for _, entry := range batch.Entries {
-
-			entryRDFI, _ := strconv.Atoi(entry.RDFIIdentification)
-
-			hash = hash + entryRDFI
+			entryRDFI, _ := strconv.Atoi(aba8(entry.RDFIIdentification))
+			hash += entryRDFI
 		}
 	} else {
 		for _, entry := range batch.ADVEntries {
-
-			entryRDFI, _ := strconv.Atoi(entry.RDFIIdentification)
-
-			hash = hash + entryRDFI
+			entryRDFI, _ := strconv.Atoi(aba8(entry.RDFIIdentification))
+			hash += entryRDFI
 		}
 	}
 
-	// This follows what is done in BatchControl.Parse
 	// EntryHash is essentially the sum of all the RDFI routing numbers in the batch. If the sum exceeds 10 digits
 	// (because you have lots of Entry Detail Records), lop off the most significant digits of the sum until there
 	// are only 10.
diff --git a/iatBatch.go b/iatBatch.go
index 8e08ced7..25fe7d04 100644
--- a/iatBatch.go
+++ b/iatBatch.go
@@ -206,7 +206,7 @@ func (iatBatch *IATBatch) build() error {
 	bc.ODFIIdentification = iatBatch.Header.ODFIIdentification
 	bc.BatchNumber = iatBatch.Header.BatchNumber
 	bc.EntryAddendaCount = entryCount
-	bc.EntryHash = iatBatch.parseNumField(iatBatch.calculateEntryHash())
+	bc.EntryHash = iatBatch.calculateEntryHash()
 	bc.TotalCreditEntryDollarAmount, bc.TotalDebitEntryDollarAmount = iatBatch.calculateBatchAmounts()
 	iatBatch.Control = bc
 
@@ -389,7 +389,7 @@ func (iatBatch *IATBatch) isSequenceAscending() error {
 // isEntryHash validates the hash by recalculating the result
 func (iatBatch *IATBatch) isEntryHash() error {
 	hashField := iatBatch.calculateEntryHash()
-	if hashField != iatBatch.Control.EntryHashField() {
+	if hashField != iatBatch.Control.EntryHash {
 		return iatBatch.Error("EntryHash",
 			NewErrBatchCalculatedControlEquality(hashField, iatBatch.Control.EntryHash))
 	}
@@ -398,15 +398,17 @@ func (iatBatch *IATBatch) isEntryHash() error {
 
 // calculateEntryHash This field is prepared by hashing the 8-digit Routing Number in each entry.
 // The Entry Hash provides a check against inadvertent alteration of data
-func (iatBatch *IATBatch) calculateEntryHash() string {
+func (iatBatch *IATBatch) calculateEntryHash() int {
 	hash := 0
 	for _, entry := range iatBatch.Entries {
-
-		entryRDFI, _ := strconv.Atoi(entry.RDFIIdentification)
-
-		hash = hash + entryRDFI
+		entryRDFI, _ := strconv.Atoi(aba8(entry.RDFIIdentification))
+		hash += entryRDFI
 	}
-	return iatBatch.numericField(hash, 10)
+
+	// EntryHash is essentially the sum of all the RDFI routing numbers in the batch. If the sum exceeds 10 digits
+	// (because you have lots of Entry Detail Records), lop off the most significant digits of the sum until there
+	// are only 10.
+	return iatBatch.leastSignificantDigits(hash, 10)
 }
 
 // isTraceNumberODFI checks if the first 8 positions of the entry detail trace number


Test patch:
diff --git a/batch_test.go b/batch_test.go
index bbe6ce82..b61ae1b2 100644
--- a/batch_test.go
+++ b/batch_test.go
@@ -1528,3 +1528,23 @@ func TestBatch__CompanyIdentificationMismatch(t *testing.T) {
 	}
 
 }
+
+func TestBatch_calculateEntryHash(t *testing.T) {
+	b := &Batch{}
+	b.SetHeader(mockBatchHeader())
+
+	ed1 := mockEntryDetail()
+	ed1.RDFIIdentification = "05600507"
+	b.AddEntry(ed1)
+
+	ed2 := mockEntryDetail()
+	ed2.RDFIIdentification = "05140225"
+	b.AddEntry(ed2)
+
+	ed3 := mockEntryDetail()
+	ed3.RDFIIdentification = "11400065"
+	b.AddEntry(ed3)
+
+	hash := b.calculateEntryHash()
+	require.Equal(t, 22140797, hash)
+}
diff --git a/iatBatch_test.go b/iatBatch_test.go
index bf4d5b06..5bdb1c2a 100644
--- a/iatBatch_test.go
+++ b/iatBatch_test.go
@@ -25,26 +25,32 @@ import (
 	"testing"
 
 	"github.com/moov-io/base"
+	"github.com/stretchr/testify/require"
 )
 
 // mockIATBatch
 func mockIATBatch(t testing.TB) IATBatch {
 	mockBatch := IATBatch{}
 	mockBatch.SetHeader(mockIATBatchHeaderFF())
-	mockBatch.AddEntry(mockIATEntryDetail())
-	mockBatch.Entries[0].Addenda10 = mockAddenda10()
-	mockBatch.Entries[0].Addenda11 = mockAddenda11()
-	mockBatch.Entries[0].Addenda12 = mockAddenda12()
-	mockBatch.Entries[0].Addenda13 = mockAddenda13()
-	mockBatch.Entries[0].Addenda14 = mockAddenda14()
-	mockBatch.Entries[0].Addenda15 = mockAddenda15()
-	mockBatch.Entries[0].Addenda16 = mockAddenda16()
+	mockBatch.AddEntry(mockIATEntryDetailWithAddendas())
 	if err := mockBatch.build(); err != nil {
 		t.Fatal(err)
 	}
 	return mockBatch
 }
 
+func mockIATEntryDetailWithAddendas() *IATEntryDetail {
+	ed := mockIATEntryDetail()
+	ed.Addenda10 = mockAddenda10()
+	ed.Addenda11 = mockAddenda11()
+	ed.Addenda12 = mockAddenda12()
+	ed.Addenda13 = mockAddenda13()
+	ed.Addenda14 = mockAddenda14()
+	ed.Addenda15 = mockAddenda15()
+	ed.Addenda16 = mockAddenda16()
+	return ed
+}
+
 // mockIATBatchManyEntries
 func mockIATBatchManyEntries(t testing.TB) IATBatch {
 	mockBatch := IATBatch{}
@@ -1938,3 +1944,53 @@ func TestParseRuneCountIATBatchHeader(t *testing.T) {
 		t.Errorf("%T: %s", err, err)
 	}
 }
+
+func TestIATBatch_calculateEntryHash(t *testing.T) {
+	b := &IATBatch{}
+	b.SetHeader(mockIATBatchHeaderFF())
+
+	ed1 := mockIATEntryDetailWithAddendas()
+	ed1.RDFIIdentification = "05600507"
+	b.AddEntry(ed1)
+
+	ed2 := mockIATEntryDetailWithAddendas()
+	ed2.RDFIIdentification = "05140225"
+	b.AddEntry(ed2)
+
+	ed3 := mockIATEntryDetailWithAddendas()
+	ed3.RDFIIdentification = "11400065"
+	b.AddEntry(ed3)
+
+	require.NoError(t, b.build())
+
+	hash := b.calculateEntryHash()
+	require.Equal(t, 22140797, hash)
+}
+
+func TestDiscussion1077(t *testing.T) {
+	b := IATBatch{}
+	b.SetHeader(mockIATBatchHeaderFF())
+
+	ed1 := mockIATEntryDetailWithAddendas()
+	ed1.RDFIIdentification = "101201863"
+	b.AddEntry(ed1)
+
+	ed2 := mockIATEntryDetailWithAddendas()
+	ed2.RDFIIdentification = "274973183"
+	b.AddEntry(ed2)
+
+	ed3 := mockIATEntryDetailWithAddendas()
+	ed3.RDFIIdentification = "111900659"
+	b.AddEntry(ed3)
+
+	ed4 := mockIATEntryDetailWithAddendas()
+	ed4.RDFIIdentification = "101918101"
+	b.AddEntry(ed4)
+
+	ed5 := mockIATEntryDetailWithAddendas()
+	ed5.RDFIIdentification = "044000037"
+	b.AddEntry(ed5)
+
+	require.NoError(t, b.build())
+	require.Equal(t, 63399382, b.calculateEntryHash())
+}