Repository: moov-io/ach
Pull request #1347 (2023-12-21 22:33:13)

Upstream column fixes
I was browsing the forks of this project and found the [@column folks have made several bug fixes](https://github.com/column/moov-ach) so we're picking them into the mainline. 

- add CheckingReturnNOCCredit to list of valid ATX txn codes
- fix BatchHeader.EffectiveEntryDateField
- feat: improve addenda99 for dishonored returns
- fix: Don't truncate Addenda99.AddendaInformation
- fix: trim "R" in dishonored and contested setter
- feat: Parse Addenda99.AddendaInformation
- Addenda99.SetOriginalEntryReturnDate


Patch:
diff --git a/addenda99.go b/addenda99.go
index 533cdf31..3e20f1a1 100644
--- a/addenda99.go
+++ b/addenda99.go
@@ -18,6 +18,7 @@
 package ach
 
 import (
+	"fmt"
 	"strings"
 	"unicode/utf8"
 )
@@ -136,7 +137,7 @@ func (Addenda99 *Addenda99) Parse(record string) {
 			Addenda99.OriginalDFI = Addenda99.parseStringField(reset())
 		case 79:
 			// 36-79
-			Addenda99.AddendaInformation = strings.TrimSpace(reset())
+			Addenda99.AddendaInformation = reset()
 		case 94:
 			// 80-94
 			Addenda99.TraceNumber = strings.TrimSpace(reset())
@@ -244,6 +245,75 @@ func (Addenda99 *Addenda99) TraceNumberField() string {
 	return Addenda99.stringField(Addenda99.TraceNumber, 15)
 }
 
+// format: reserved (3), return trace number (15), return settlement date (3), return reason (2), addenda (21)
+// Ref: https://www.nachaoperatingrulesonline.org/2.16334/s020
+func (Addenda99 *Addenda99) SetDishonoredAddendaInformation(
+	returnTraceNumber string,
+	returnSettlementDate string,
+	returnReasonCode string,
+	addenda string,
+) {
+	// This record drops the "R"
+	returnReasonCode = strings.TrimPrefix(returnReasonCode, "R")
+
+	addendaInformation := fmt.Sprintf("   %s%s%s%s",
+		Addenda99.alphaField(returnTraceNumber, 15),
+		Addenda99.alphaField(returnSettlementDate, 3),
+		Addenda99.alphaField(returnReasonCode, 2),
+		Addenda99.alphaField(addenda, 21),
+	)
+	Addenda99.AddendaInformation = Addenda99.alphaField(addendaInformation, 44)
+}
+
+// Ref: https://www.nachaoperatingrulesonline.org/2.16334/s020
+func (Addenda99 *Addenda99) SetContestedAddendaInformation(
+	originalSettlementDate string,
+	returnTraceNumber string,
+	returnSettlementDate string,
+	returnReasonCode string,
+	dishonoredReturnTraceNumber string,
+	dishonoredReturnSettlementDate string,
+	dishonoredReturnReasonCode string,
+) {
+	// This record drops the "R"
+	returnReasonCode = strings.TrimPrefix(returnReasonCode, "R")
+
+	addendaInformation := fmt.Sprintf("%s%s%s%s%s%s%s ",
+		Addenda99.alphaField(originalSettlementDate, 3),
+		Addenda99.alphaField(returnTraceNumber, 15),
+		Addenda99.alphaField(returnSettlementDate, 3),
+		Addenda99.alphaField(returnReasonCode, 2),
+		Addenda99.alphaField(dishonoredReturnTraceNumber, 15),
+		Addenda99.alphaField(dishonoredReturnSettlementDate, 3),
+		Addenda99.alphaField(dishonoredReturnReasonCode, 2),
+	)
+	Addenda99.AddendaInformation = Addenda99.alphaField(addendaInformation, 44)
+}
+
+func (Addenda99 *Addenda99) AddendaInformationReturnTraceNumber() string {
+	return Addenda99.AddendaInformation[3:18]
+}
+
+func (Addenda99 *Addenda99) AddendaInformationReturnSettlementDate() string {
+	return Addenda99.AddendaInformation[18:21]
+}
+
+func (Addenda99 *Addenda99) AddendaInformationReturnReasonCode() string {
+	return fmt.Sprintf("R%s", Addenda99.AddendaInformation[21:23])
+}
+
+func (Addenda99 *Addenda99) AddendaInformationExtra() string {
+	return Addenda99.AddendaInformation[23:]
+}
+
+func (Addenda99 *Addenda99) SetOriginalEntryReturnDate(date string) {
+	Addenda99.DateOfDeath = Addenda99.stringField(date, 6)
+}
+
+func (Addenda99 *Addenda99) OriginalEntryReturnDate() string {
+	return Addenda99.DateOfDeath
+}
+
 // ReturnCodeField gives the ReturnCode struct for the given Addenda99 record
 func (Addenda99 *Addenda99) ReturnCodeField() *ReturnCode {
 	code, ok := returnCodeDict[Addenda99.ReturnCode]
diff --git a/batchATX.go b/batchATX.go
index 5f7b6eb8..fa7229db 100644
--- a/batchATX.go
+++ b/batchATX.go
@@ -60,7 +60,7 @@ func (batch *BatchATX) Validate() error {
 			return batch.Error("Amount", ErrBatchAmountNonZero, entry.Amount)
 		}
 		switch entry.TransactionCode {
-		case CheckingZeroDollarRemittanceCredit, SavingsZeroDollarRemittanceCredit:
+		case CheckingZeroDollarRemittanceCredit, SavingsZeroDollarRemittanceCredit, CheckingReturnNOCCredit:
 		default:
 			return batch.Error("TransactionCode", ErrBatchTransactionCode, entry.TransactionCode)
 		}
diff --git a/batchHeader.go b/batchHeader.go
index de9e372a..4b7840df 100644
--- a/batchHeader.go
+++ b/batchHeader.go
@@ -376,7 +376,7 @@ func (bh *BatchHeader) CompanyDescriptiveDateField() string {
 // EffectiveEntryDateField get the EffectiveEntryDate in YYMMDD format
 func (bh *BatchHeader) EffectiveEntryDateField() string {
 	// ENR records require EffectiveEntryDate to be space filled. NACHA Page OR108
-	if bh.CompanyEntryDescription == "AUTOENROLL" {
+	if bh.CompanyEntryDescription == "AUTOENROLL" && bh.StandardEntryClassCode == ENR {
 		return bh.alphaField("", 6)
 	}
 	return bh.stringField(bh.EffectiveEntryDate, 6) // YYMMDD


Test patch:
diff --git a/addenda99_dishonored_return_test.go b/addenda99_dishonored_return_test.go
index 33189098..925b698e 100644
--- a/addenda99_dishonored_return_test.go
+++ b/addenda99_dishonored_return_test.go
@@ -36,6 +36,31 @@ func mockAddenda99Dishonored() *Addenda99Dishonored {
 	return addenda99
 }
 
+func TestAddenda99Dishonored(t *testing.T) {
+	addenda99 := NewAddenda99()
+	line := "799R6909100001137143222042712114530   1211453000251201170506                   091000011371432"
+	addenda99.Parse(line)
+
+	require.Equal(t, "12114530", addenda99.OriginalDFI)
+	require.Equal(t, "   1211453000251201170506                   ", addenda99.AddendaInformation)
+	require.Equal(t, "091000011371432", addenda99.TraceNumber)
+	require.Equal(t, line, addenda99.String())
+
+	addenda99 = NewAddenda99()
+	addenda99.ReturnCode = "R69"
+	addenda99.OriginalTrace = "091000011371432"
+	addenda99.DateOfDeath = "220427"
+	addenda99.OriginalDFI = "12114530"
+	addenda99.SetDishonoredAddendaInformation("121145300025120", "117", "R05", "06")
+	addenda99.TraceNumber = "091000011371432"
+	require.Equal(t, line, addenda99.String())
+
+	require.Equal(t, "121145300025120", addenda99.AddendaInformationReturnTraceNumber())
+	require.Equal(t, "117", addenda99.AddendaInformationReturnSettlementDate())
+	require.Equal(t, "R05", addenda99.AddendaInformationReturnReasonCode())
+	require.Equal(t, "06                   ", addenda99.AddendaInformationExtra())
+}
+
 func TestAddenda99Dishonored__Fields(t *testing.T) {
 	addenda99 := mockAddenda99Dishonored()
 
diff --git a/addenda99_test.go b/addenda99_test.go
index 390ec445..2b2c5156 100644
--- a/addenda99_test.go
+++ b/addenda99_test.go
@@ -58,7 +58,7 @@ func testAddenda99Parse(t testing.TB) {
 	if addenda99.OriginalDFI != "09101298" {
 		t.Errorf("expected: %s got: %s", "09101298", addenda99.OriginalDFI)
 	}
-	if addenda99.AddendaInformation != "Authorization revoked" {
+	if addenda99.AddendaInformation != "Authorization revoked                       " {
 		t.Errorf("expected: %v got: %v", "Authorization revoked", addenda99.AddendaInformation)
 	}
 	if addenda99.TraceNumber != "091012980000066" {
diff --git a/batchHeader_test.go b/batchHeader_test.go
index ce6ed2e3..7b3c2241 100644
--- a/batchHeader_test.go
+++ b/batchHeader_test.go
@@ -511,6 +511,7 @@ func TestBatchHeaderENR__EffectiveEntryDateField(t *testing.T) {
 	bh := mockBatchHeader()
 
 	// ENR batches require EffectiveEntryDate to be space filled
+	bh.StandardEntryClassCode = ENR
 	bh.CompanyEntryDescription = "AUTOENROLL"
 	if v, ans := bh.EffectiveEntryDateField(), "      "; v != ans {
 		t.Errorf("got %q (len=%d), expected space filled (len=6)", v, len(ans))