Repository: moov-io/ach
Pull request #1421 (2024-05-09 20:23:02)

fix: stop checking .Amount is valid for returns
Fixes https://github.com/moov-io/ach/issues/1332 


Patch:
diff --git a/batch.go b/batch.go
index 9f6dd15d..5dc62eda 100644
--- a/batch.go
+++ b/batch.go
@@ -1038,6 +1038,7 @@ func (batch *Batch) ValidAmountForCodes(entry *EntryDetail) error {
 	if batch.validateOpts != nil && batch.validateOpts.AllowInvalidAmounts {
 		return nil
 	}
+
 	if entry != nil && (entry.Addenda98 != nil || entry.Addenda98Refused != nil) {
 		// NOC entries will have a zero'd amount value
 		if entry.Amount != 0 {
@@ -1045,7 +1046,12 @@ func (batch *Batch) ValidAmountForCodes(entry *EntryDetail) error {
 		}
 		return nil
 	}
+	if entry != nil && (entry.Addenda99 != nil || entry.Addenda99Contested != nil || entry.Addenda99Dishonored != nil) {
+		// Returned prenotes can have a zero amount, so allow returns through
+		return nil
+	}
 
+	// If the entry is a PRENOTE force it's amount to be zero
 	var isPrenoteDesc bool
 	if batch != nil && batch.Header != nil {
 		isPrenoteDesc = strings.EqualFold(batch.Header.CompanyEntryDescription, "PRENOTE")


Test patch:
diff --git a/batch_test.go b/batch_test.go
index 55938ce2..af80f750 100644
--- a/batch_test.go
+++ b/batch_test.go
@@ -1070,15 +1070,55 @@ func TestBatch__ValidAmountForCodes(t *testing.T) {
 	b1.Entries[0].Amount = 0
 	require.NoError(t, b1.Create())
 
-	// NOC entries must have zero amounts
-	file, err := ReadFile(filepath.Join("test", "testdata", "cor-example.ach"))
-	require.NoError(t, err)
-	require.NoError(t, file.Validate())
+	t.Run("corrections", func(t *testing.T) {
+		// NOC entries must have zero amounts
+		file, err := ReadFile(filepath.Join("test", "testdata", "cor-example.ach"))
+		require.NoError(t, err)
+		require.NoError(t, file.Validate())
+
+		batch, ok := file.Batches[0].(*BatchCOR)
+		require.True(t, ok)
+		batch.Entries[0].Amount = 12345
+		require.ErrorContains(t, batch.Create(), ErrBatchAmountNonZero.Error())
+	})
 
-	batch, ok := file.Batches[0].(*BatchCOR)
-	require.True(t, ok)
-	batch.Entries[0].Amount = 12345
-	require.ErrorContains(t, batch.Create(), ErrBatchAmountNonZero.Error())
+	t.Run("returns", func(t *testing.T) {
+		// The TransactionCode of a Returned Prenote does not convey it was a prenote,
+		// so ValidAmountForCodes is forced to pick between:
+		//  - allow returns with zero'd amounts (would allow for invalid non-prenote returns)
+		//  - force AllowInvalidAmounts to be set for zero-amount returns to be accepted
+		file, err := ReadFile(filepath.Join("test", "testdata", "return-WEB.ach"))
+		require.NoError(t, err)
+		require.NoError(t, file.Validate())
+
+		batch, ok := file.Batches[0].(*BatchWEB)
+		require.True(t, ok)
+		batch.Header.CompanyEntryDescription = "PRENOTE"
+		batch.Entries[0].Amount = 0
+		require.NoError(t, batch.Create())
+
+		// Allow zero-amount returns through without CompanyEntryDescription set to PRENOTE
+		batch.Header.CompanyEntryDescription = "OTHER"
+		batch.Entries[0].Addenda99 = mockAddenda99()
+		batch.Entries[0].TransactionCode = CheckingReturnNOCCredit
+		require.NoError(t, batch.Create())
+	})
+
+	t.Run("prenote", func(t *testing.T) {
+		file, err := ReadFile(filepath.Join("test", "testdata", "ppd-debit.ach"))
+		require.NoError(t, err)
+		require.NoError(t, file.Validate())
+
+		batch, ok := file.Batches[0].(*BatchPPD)
+		require.True(t, ok)
+		batch.Header.CompanyEntryDescription = "PRENOTE"
+		batch.Entries[0].Amount = 102
+		batch.Entries[0].TransactionCode = CheckingPrenoteDebit
+		require.ErrorContains(t, batch.Create(), ErrBatchAmountNonZero.Error())
+
+		batch.Entries[0].Amount = 0
+		require.NoError(t, batch.Create())
+	})
 }
 
 func TestBatch_AllowInvalidAmounts(t *testing.T) {
@@ -1094,23 +1134,14 @@ func TestBatch_AllowInvalidAmounts(t *testing.T) {
 		BatchNumber:             63472,
 	}
 	entry := &EntryDetail{
-		TransactionCode:        CheckingReturnNOCDebit,
-		RDFIIdentification:     "98765432",
-		CheckDigit:             "0",
-		DFIAccountNumber:       "4000000013",
-		Amount:                 0,
-		IndividualName:         "Jane Doe",
-		TraceNumber:            "123456781176120",
-		Category:               CategoryReturn,
-		AddendaRecordIndicator: 1,
-		Addenda99: &Addenda99{
-			TypeCode:           "99",
-			ReturnCode:         "R03",
-			OriginalTrace:      "987654327608587",
-			OriginalDFI:        "12345678",
-			AddendaInformation: "",
-			TraceNumber:        "123456781176120",
-		},
+		TransactionCode:    CheckingDebit,
+		RDFIIdentification: "98765432",
+		CheckDigit:         "0",
+		DFIAccountNumber:   "4000000013",
+		Amount:             0,
+		IndividualName:     "Jane Doe",
+		TraceNumber:        "123456781176120",
+		Category:           CategoryForward,
 	}
 	b, err := NewBatch(bh)
 	require.NoError(t, err)