Repository: moov-io/ach
Pull request #1451 (2024-07-25 00:13:29)

feat: Added validation option to allow $0 entry amount
Details in issue https://github.com/moov-io/ach/issues/1450


Patch:
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 25ae7da0..f83984c2 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -36,7 +36,7 @@ jobs:
           persist-credentials: false
 
       - name: "Run analysis"
-        uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
+        uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3
         with:
           results_file: results.sarif
           results_format: sarif
diff --git a/batch.go b/batch.go
index 5dc62eda..160cfbea 100644
--- a/batch.go
+++ b/batch.go
@@ -1064,6 +1064,9 @@ func (batch *Batch) ValidAmountForCodes(entry *EntryDetail) error {
 		return fieldError("Amount", ErrBatchAmountNonZero, entry.Amount)
 	} else {
 		if entry.Amount == 0 {
+			if batch.validateOpts != nil && batch.validateOpts.AllowZeroEntryAmount {
+				return nil
+			}
 			return fieldError("Amount", ErrBatchAmountZero, entry.Amount)
 		}
 	}
diff --git a/docs/custom-validation.md b/docs/custom-validation.md
index df18fce8..010cfb09 100644
--- a/docs/custom-validation.md
+++ b/docs/custom-validation.md
@@ -92,6 +92,9 @@ UnequalAddendaCounts bool `json:"unequalAddendaCounts"`
 ```
 // AllowInvalidAmounts will skip verifying the Amount is valid for the TransactionCode and entry type.
 AllowInvalidAmounts bool `json:"allowInvalidAmounts"`
+
+// AllowZeroEntryAmount will skip enforcing the entry Amount to be non-zero.
+AllowZeroEntryAmount bool `json:"allowZeroEntryAmount"`
 ```
 
 ### File Header
diff --git a/file.go b/file.go
index 426c1f75..600996f3 100644
--- a/file.go
+++ b/file.go
@@ -712,6 +712,9 @@ type ValidateOpts struct {
 
 	// AllowInvalidAmounts will skip verifying the Amount is valid for the TransactionCode and entry type.
 	AllowInvalidAmounts bool `json:"allowInvalidAmounts"`
+
+	// AllowZeroEntryAmount will skip enforcing the entry Amount to be non-zero
+	AllowZeroEntryAmount bool `json:"allowZeroEntryAmount"`
 }
 
 // merge will combine two ValidateOpts structs and keep any non-zero field values.
diff --git a/server/validate.go b/server/validate.go
index 069f9b49..b9c52785 100644
--- a/server/validate.go
+++ b/server/validate.go
@@ -48,6 +48,7 @@ const (
 	unequalAddendaCounts             = "unequalAddendaCounts"
 	preserveSpaces                   = "preserveSpaces"
 	allowInvalidAmounts              = "allowInvalidAmounts"
+	allowZeroEntryAmount             = "allowZeroEntryAmount"
 )
 
 // readValidateOpts parses ValidateOpts from the URL query parameters and from the request body.
@@ -75,6 +76,7 @@ func readValidateOpts(request *http.Request) (io.Reader, *ach.ValidateOpts, erro
 		unequalAddendaCounts,
 		preserveSpaces,
 		allowInvalidAmounts,
+		allowZeroEntryAmount,
 	}
 
 	var buf bytes.Buffer
@@ -130,6 +132,8 @@ func readValidateOpts(request *http.Request) (io.Reader, *ach.ValidateOpts, erro
 			opts.PreserveSpaces = yes
 		case allowInvalidAmounts:
 			opts.AllowInvalidAmounts = yes
+		case allowZeroEntryAmount:
+			opts.AllowZeroEntryAmount = yes
 		}
 	}
 


Test patch:
diff --git a/batch_test.go b/batch_test.go
index af80f750..0272333e 100644
--- a/batch_test.go
+++ b/batch_test.go
@@ -1121,6 +1121,23 @@ func TestBatch__ValidAmountForCodes(t *testing.T) {
 	})
 }
 
+func TestBatch_ValidAmountForCodes_AllowZeroEntryAmount(t *testing.T) {
+	b1 := mockBatchWEB(t)
+
+	// Standard EntryDetails should pass
+	require.NoError(t, b1.Create())
+
+	// Verify a zero amount fails
+	b1.Entries[0].Amount = 0
+	require.ErrorContains(t, b1.Create(), ErrBatchAmountZero.Error())
+
+	// Bypass zero amount check
+	b1.SetValidation(&ValidateOpts{
+		AllowZeroEntryAmount: true,
+	})
+	require.NoError(t, b1.Create())
+}
+
 func TestBatch_AllowInvalidAmounts(t *testing.T) {
 	bh := &BatchHeader{
 		OriginatorStatusCode:    1,