Repository: moov-io/ach
Pull request #1154 (2022-12-29 16:51:45)

skip validate option
- SkipAllValidate into ValidateOpts
- skip all validate logic of reader when the option is enable 

Fixes: https://github.com/moov-io/ach/issues/1147 

codecov-commenter: # [Codecov](https://codecov.io/gh/moov-io/ach/pull/1154?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=moov-io) Report
> Merging [#1154](https://codecov.io/gh/moov-io/ach/pull/1154?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=moov-io) (fb288d5) into [master](https://codecov.io/gh/moov-io/ach/commit/035a0106036b6c71bc545f12c744cd202921cd4f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=moov-io) (035a010) will **decrease** coverage by `0.07%`.
> The diff coverage is `66.66%`.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1154      +/-   ##
==========================================
- Coverage   90.07%   90.00%   -0.08%     
==========================================
  Files          71       71              
  Lines        6732     6743      +11     
==========================================
+ Hits         6064     6069       +5     
- Misses        401      404       +3     
- Partials      267      270       +3     
```

</details>
adamdecaf: Can we add some tests? This is a pretty major change. 
mfdeveloper508: > Can we add some tests? This is a pretty major change.

sure, I will add soon
mfdeveloper508: office is off in this week? I don't see any coworker in slack channel 
adamdecaf: Can we rebase off `origin/master` when the tests are added? Thanks 
adamdecaf: Why does this PR include changes that are already on `master`? 
mfdeveloper508: skip-validation option will be run on file/reader's level, not record's level

Patch:
diff --git a/cmd/achcli/main.go b/cmd/achcli/main.go
index 69a3b87c..4d961466 100644
--- a/cmd/achcli/main.go
+++ b/cmd/achcli/main.go
@@ -31,7 +31,8 @@ var (
 	flagPretty        = flag.Bool("pretty", false, "Display all values in their human readable format")
 	flagPrettyAmounts = flag.Bool("pretty.amounts", false, "Display human readable amounts instead of exact values")
 
-	flagValidateOpts = flag.String("validate", "", "Path to config file in json format to enable validation opts")
+	flagSkipValidation = flag.Bool("skip-validation", false, "Skip all validation checks")
+	flagValidateOpts   = flag.String("validate", "", "Path to config file in json format to enable validation opts")
 )
 
 func main() {
@@ -105,6 +106,9 @@ func readValidationOpts(path string) *ach.ValidateOpts {
 			fmt.Printf("ERROR: unmarshal of validate opts failed: %v\n", err)
 			os.Exit(1)
 		}
+		if *flagSkipValidation {
+			opts.SkipAll = true
+		}
 		return &opts
 	}
 	return nil
diff --git a/docs/create-file.md b/docs/create-file.md
index fff3703e..07efb36f 100644
--- a/docs/create-file.md
+++ b/docs/create-file.md
@@ -44,6 +44,7 @@ Example: `POST /files/create?requireABAOrigin=true&bypassDestination=true`
 
 | Query Param                        | Validation Option                  |
 |------------------------------------|------------------------------------|
+| `allowInvalidCheckDigit`           | `AllowInvalidCheckDigit`           |
 | `allowMissingFileControl`          | `AllowMissingFileControl`          |
 | `allowMissingFileHeader`           | `AllowMissingFileHeader`           |
 | `allowZeroBatches`                 | `AllowZeroBatches`                 |
@@ -53,10 +54,10 @@ Example: `POST /files/create?requireABAOrigin=true&bypassDestination=true`
 | `customReturnCodes`                | `CustomReturnCodes`                |
 | `customTraceNumbers`               | `CustomTraceNumbers`               |
 | `requireABAOrigin`                 | `RequireABAOrigin`                 |
+| `skipAll`                          | `SkipAll`                          |
+| `unequalAddendaCounts`             | `UnequalAddendaCounts`             |
 | `unequalServiceClassCode`          | `UnequalServiceClassCode`          |
 | `unorderedBatchNumbers`            | `AllowUnorderedBatchNumbers`       |
-| `allowInvalidCheckDigit`           | `AllowInvalidCheckDigit`           |
-| `unequalAddendaCounts`             | `UnequalAddendaCounts`             |
 
 ## Upload a raw ACH file
 
diff --git a/file.go b/file.go
index 46a45ba2..bfaa77f4 100644
--- a/file.go
+++ b/file.go
@@ -619,6 +619,9 @@ func (f *File) SetValidation(opts *ValidateOpts) {
 // ValidateOpts contains specific overrides from the default set of validations
 // performed on a NACHA file, records and various fields within.
 type ValidateOpts struct {
+	// SkipAll will disable all validation checks of a File. It has no effect when set on records.
+	SkipAll bool `json:"skipAll"`
+
 	// RequireABAOrigin can be set to enable routing number validation
 	// over the ImmediateOrigin file header field.
 	RequireABAOrigin bool `json:"requireABAOrigin"`
@@ -693,6 +696,10 @@ func (f *File) ValidateWith(opts *ValidateOpts) error {
 		opts = &ValidateOpts{}
 	}
 
+	if opts.SkipAll {
+		return nil
+	}
+
 	if !opts.AllowMissingFileHeader {
 		if err := f.Header.ValidateWith(opts); err != nil {
 			return err
diff --git a/openapi.yaml b/openapi.yaml
index cdb77347..548974bb 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -74,6 +74,11 @@ paths:
           schema:
             type: string
             example: "3f2d23ee214"
+        - name: skipAll
+          in: query
+          description: Optional parameter to disable all validation checks for a File
+          schema:
+            type: boolean
         - name: requireABAOrigin
           in: query
           description: Optional parameter to configure ImmediateOrigin validation
diff --git a/reader.go b/reader.go
index 90029a7a..eb983731 100644
--- a/reader.go
+++ b/reader.go
@@ -290,7 +290,7 @@ func (r *Reader) parseLine() error {
 			r.currentBatch = nil
 			batch.SetValidation(r.File.validateOpts)
 			r.File.AddBatch(batch)
-			if err := batch.Validate(); err != nil {
+			if err := maybeValidate(batch, r.File.validateOpts); err != nil {
 				r.recordName = "Batches"
 				return r.parseError(err)
 			}
@@ -299,7 +299,7 @@ func (r *Reader) parseLine() error {
 			r.IATCurrentBatch = IATBatch{}
 			batch.SetValidation(r.File.validateOpts)
 			r.File.AddIATBatch(batch)
-			if err := batch.Validate(); err != nil {
+			if err := maybeValidate(&batch, r.File.validateOpts); err != nil {
 				r.recordName = "Batches"
 				return r.parseError(err)
 			}
@@ -371,7 +371,7 @@ func (r *Reader) parseFileHeader() error {
 	}
 	r.File.Header.Parse(r.line)
 
-	if err := r.File.Header.Validate(); err != nil {
+	if err := maybeValidate(&r.File.Header, r.File.validateOpts); err != nil {
 		return r.parseError(err)
 	}
 	return nil
@@ -388,7 +388,7 @@ func (r *Reader) parseBatchHeader() error {
 	// Ensure we have a valid batch header before building a batch.
 	bh := NewBatchHeader()
 	bh.Parse(r.line)
-	if err := bh.Validate(); err != nil {
+	if err := maybeValidate(bh, r.File.validateOpts); err != nil {
 		return r.parseError(err)
 	}
 
@@ -413,14 +413,14 @@ func (r *Reader) parseEntryDetail() error {
 		ed := NewEntryDetail()
 		ed.Parse(r.line)
 		ed.SetValidation(r.File.validateOpts)
-		if err := ed.Validate(); err != nil {
+		if err := maybeValidate(ed, r.File.validateOpts); err != nil {
 			return r.parseError(err)
 		}
 		r.currentBatch.AddEntry(ed)
 	} else {
 		ed := NewADVEntryDetail()
 		ed.Parse(r.line)
-		if err := ed.Validate(); err != nil {
+		if err := maybeValidate(ed, r.File.validateOpts); err != nil {
 			return r.parseError(err)
 		}
 		r.currentBatch.AddADVEntry(ed)
@@ -447,21 +447,21 @@ func (r *Reader) parseAddenda() error {
 			case "02":
 				addenda02 := NewAddenda02()
 				addenda02.Parse(r.line)
-				if err := addenda02.Validate(); err != nil {
+				if err := maybeValidate(addenda02, r.File.validateOpts); err != nil {
 					return r.parseError(err)
 				}
 				r.currentBatch.GetEntries()[entryIndex].Addenda02 = addenda02
 			case "05":
 				addenda05 := NewAddenda05()
 				addenda05.Parse(r.line)
-				if err := addenda05.Validate(); err != nil {
+				if err := maybeValidate(addenda05, r.File.validateOpts); err != nil {
 					return r.parseError(err)
 				}
 				r.currentBatch.GetEntries()[entryIndex].AddAddenda05(addenda05)
 			case "98":
 				addenda98 := NewAddenda98()
 				addenda98.Parse(r.line)
-				if err := addenda98.Validate(); err != nil {
+				if err := maybeValidate(addenda98, r.File.validateOpts); err != nil {
 					return r.parseError(err)
 				}
 				r.currentBatch.GetEntries()[entryIndex].Category = CategoryNOC
@@ -474,7 +474,7 @@ func (r *Reader) parseAddenda() error {
 					addenda99Dishonored := NewAddenda99Dishonored()
 					addenda99Dishonored.Parse(r.line)
 					addenda99Dishonored.SetValidation(r.File.validateOpts)
-					if err := addenda99Dishonored.Validate(); err != nil {
+					if err := maybeValidate(addenda99Dishonored, r.File.validateOpts); err != nil {
 						return r.parseError(err)
 					}
 					r.currentBatch.GetEntries()[entryIndex].Addenda99Dishonored = addenda99Dishonored
@@ -484,7 +484,7 @@ func (r *Reader) parseAddenda() error {
 					addenda99Contested := NewAddenda99Contested()
 					addenda99Contested.Parse(r.line)
 					addenda99Contested.SetValidation(r.File.validateOpts)
-					if err := addenda99Contested.Validate(); err != nil {
+					if err := maybeValidate(addenda99Contested, r.File.validateOpts); err != nil {
 						return r.parseError(err)
 					}
 					r.currentBatch.GetEntries()[entryIndex].Addenda99Contested = addenda99Contested
@@ -494,7 +494,7 @@ func (r *Reader) parseAddenda() error {
 					addenda99 := NewAddenda99()
 					addenda99.Parse(r.line)
 					addenda99.SetValidation(r.File.validateOpts)
-					if err := addenda99.Validate(); err != nil {
+					if err := maybeValidate(addenda99, r.File.validateOpts); err != nil {
 						return r.parseError(err)
 					}
 					r.currentBatch.GetEntries()[entryIndex].Addenda99 = addenda99
@@ -525,7 +525,7 @@ func (r *Reader) parseADVAddenda() error {
 	}
 	addenda99 := NewAddenda99()
 	addenda99.Parse(r.line)
-	if err := addenda99.Validate(); err != nil {
+	if err := maybeValidate(addenda99, r.File.validateOpts); err != nil {
 		return r.parseError(err)
 	}
 	r.currentBatch.GetADVEntries()[entryIndex].Category = CategoryReturn
@@ -543,18 +543,18 @@ func (r *Reader) parseBatchControl() error {
 	if r.currentBatch != nil {
 		if r.currentBatch.GetHeader().StandardEntryClassCode == ADV {
 			r.currentBatch.GetADVControl().Parse(r.line)
-			if err := r.currentBatch.GetADVControl().Validate(); err != nil {
+			if err := maybeValidate(r.currentBatch.GetADVControl(), r.File.validateOpts); err != nil {
 				return r.parseError(err)
 			}
 		} else {
 			r.currentBatch.GetControl().Parse(r.line)
-			if err := r.currentBatch.GetControl().Validate(); err != nil {
+			if err := maybeValidate(r.currentBatch.GetControl(), r.File.validateOpts); err != nil {
 				return r.parseError(err)
 			}
 		}
 	} else {
 		r.IATCurrentBatch.GetControl().Parse(r.line)
-		if err := r.IATCurrentBatch.GetControl().Validate(); err != nil {
+		if err := maybeValidate(r.IATCurrentBatch.GetControl(), r.File.validateOpts); err != nil {
 			return r.parseError(err)
 		}
 
@@ -572,7 +572,7 @@ func (r *Reader) parseFileControl() error {
 			return ErrFileControl
 		}
 		r.File.Control.Parse(r.line)
-		if err := r.File.Control.Validate(); err != nil {
+		if err := maybeValidate(&r.File.Control, r.File.validateOpts); err != nil {
 			return r.parseError(err)
 		}
 	} else {
@@ -581,7 +581,7 @@ func (r *Reader) parseFileControl() error {
 			return ErrFileControl
 		}
 		r.File.ADVControl.Parse(r.line)
-		if err := r.File.ADVControl.Validate(); err != nil {
+		if err := maybeValidate(&r.File.ADVControl, r.File.validateOpts); err != nil {
 			return r.parseError(err)
 		}
 	}
@@ -601,7 +601,7 @@ func (r *Reader) parseIATBatchHeader() error {
 	// Ensure we have a valid IAT BatchHeader before building a batch.
 	bh := NewIATBatchHeader()
 	bh.Parse(r.line)
-	if err := bh.Validate(); err != nil {
+	if err := maybeValidate(bh, r.File.validateOpts); err != nil {
 		return r.parseError(err)
 	}
 
@@ -622,7 +622,7 @@ func (r *Reader) parseIATEntryDetail() error {
 
 	ed := new(IATEntryDetail)
 	ed.Parse(r.line)
-	if err := ed.Validate(); err != nil {
+	if err := maybeValidate(ed, r.File.validateOpts); err != nil {
 		return r.parseError(err)
 	}
 	r.IATCurrentBatch.AddEntry(ed)
@@ -681,63 +681,63 @@ func (r *Reader) mandatoryOptionalIATAddenda(entryIndex int) error {
 	case "10":
 		addenda10 := NewAddenda10()
 		addenda10.Parse(r.line)
-		if err := addenda10.Validate(); err != nil {
+		if err := maybeValidate(addenda10, r.File.validateOpts); err != nil {
 			return err
 		}
 		r.IATCurrentBatch.Entries[entryIndex].Addenda10 = addenda10
 	case "11":
 		addenda11 := NewAddenda11()
 		addenda11.Parse(r.line)
-		if err := addenda11.Validate(); err != nil {
+		if err := maybeValidate(addenda11, r.File.validateOpts); err != nil {
 			return err
 		}
 		r.IATCurrentBatch.Entries[entryIndex].Addenda11 = addenda11
 	case "12":
 		addenda12 := NewAddenda12()
 		addenda12.Parse(r.line)
-		if err := addenda12.Validate(); err != nil {
+		if err := maybeValidate(addenda12, r.File.validateOpts); err != nil {
 			return err
 		}
 		r.IATCurrentBatch.Entries[entryIndex].Addenda12 = addenda12
 	case "13":
 		addenda13 := NewAddenda13()
 		addenda13.Parse(r.line)
-		if err := addenda13.Validate(); err != nil {
+		if err := maybeValidate(addenda13, r.File.validateOpts); err != nil {
 			return err
 		}
 		r.IATCurrentBatch.Entries[entryIndex].Addenda13 = addenda13
 	case "14":
 		addenda14 := NewAddenda14()
 		addenda14.Parse(r.line)
-		if err := addenda14.Validate(); err != nil {
+		if err := maybeValidate(addenda14, r.File.validateOpts); err != nil {
 			return err
 		}
 		r.IATCurrentBatch.Entries[entryIndex].Addenda14 = addenda14
 	case "15":
 		addenda15 := NewAddenda15()
 		addenda15.Parse(r.line)
-		if err := addenda15.Validate(); err != nil {
+		if err := maybeValidate(addenda15, r.File.validateOpts); err != nil {
 			return err
 		}
 		r.IATCurrentBatch.Entries[entryIndex].Addenda15 = addenda15
 	case "16":
 		addenda16 := NewAddenda16()
 		addenda16.Parse(r.line)
-		if err := addenda16.Validate(); err != nil {
+		if err := maybeValidate(addenda16, r.File.validateOpts); err != nil {
 			return err
 		}
 		r.IATCurrentBatch.Entries[entryIndex].Addenda16 = addenda16
 	case "17":
 		addenda17 := NewAddenda17()
 		addenda17.Parse(r.line)
-		if err := addenda17.Validate(); err != nil {
+		if err := maybeValidate(addenda17, r.File.validateOpts); err != nil {
 			return err
 		}
 		r.IATCurrentBatch.Entries[entryIndex].AddAddenda17(addenda17)
 	case "18":
 		addenda18 := NewAddenda18()
 		addenda18.Parse(r.line)
-		if err := addenda18.Validate(); err != nil {
+		if err := maybeValidate(addenda18, r.File.validateOpts); err != nil {
 			return err
 		}
 		r.IATCurrentBatch.Entries[entryIndex].AddAddenda18(addenda18)
@@ -749,7 +749,7 @@ func (r *Reader) mandatoryOptionalIATAddenda(entryIndex int) error {
 func (r *Reader) nocIATAddenda(entryIndex int) error {
 	addenda98 := NewAddenda98()
 	addenda98.Parse(r.line)
-	if err := addenda98.Validate(); err != nil {
+	if err := maybeValidate(addenda98, r.File.validateOpts); err != nil {
 		return err
 	}
 	r.IATCurrentBatch.Entries[entryIndex].Addenda98 = addenda98
@@ -760,9 +760,20 @@ func (r *Reader) nocIATAddenda(entryIndex int) error {
 func (r *Reader) returnIATAddenda(entryIndex int) error {
 	addenda99 := NewAddenda99()
 	addenda99.Parse(r.line)
-	if err := addenda99.Validate(); err != nil {
+	if err := maybeValidate(addenda99, r.File.validateOpts); err != nil {
 		return err
 	}
 	r.IATCurrentBatch.Entries[entryIndex].Addenda99 = addenda99
 	return nil
 }
+
+type canValidate interface {
+	Validate() error
+}
+
+func maybeValidate[T canValidate](rec T, opts *ValidateOpts) error {
+	if opts != nil && opts.SkipAll {
+		return nil
+	}
+	return rec.Validate()
+}
diff --git a/server/files.go b/server/files.go
index 5c2c7760..12a456ae 100644
--- a/server/files.go
+++ b/server/files.go
@@ -123,6 +123,7 @@ func decodeCreateFileRequest(_ context.Context, request *http.Request) (interfac
 	}
 
 	const (
+		skipAll                          = "skipAll"
 		requireABAOrigin                 = "requireABAOrigin"
 		bypassOrigin                     = "bypassOrigin"
 		bypassDestination                = "bypassDestination"
@@ -139,6 +140,7 @@ func decodeCreateFileRequest(_ context.Context, request *http.Request) (interfac
 	)
 
 	validationNames := []string{
+		skipAll,
 		requireABAOrigin,
 		bypassOrigin,
 		bypassDestination,
@@ -169,6 +171,8 @@ func decodeCreateFileRequest(_ context.Context, request *http.Request) (interfac
 		}
 
 		switch name {
+		case skipAll:
+			req.validateOpts.SkipAll = true
 		case requireABAOrigin:
 			req.validateOpts.RequireABAOrigin = true
 		case bypassOrigin:


Test patch:
diff --git a/file_test.go b/file_test.go
index ea2ede9b..0d06d20b 100644
--- a/file_test.go
+++ b/file_test.go
@@ -2098,3 +2098,20 @@ func TestFile__AscendingBatchSequence(t *testing.T) {
 		})
 	}
 }
+
+func TestFile_SkipValidation(t *testing.T) {
+	file := mockFilePPD()
+	file.validateOpts = &ValidateOpts{
+		SkipAll: true,
+	}
+
+	file.Control.TotalDebitEntryDollarAmountInFile = 22
+	err := file.Validate()
+	require.NoError(t, err)
+
+	file.validateOpts = nil
+	err = file.ValidateWith(&ValidateOpts{
+		SkipAll: true,
+	})
+	require.NoError(t, err)
+}
diff --git a/reader_test.go b/reader_test.go
index b0a5561f..c439b0a7 100644
--- a/reader_test.go
+++ b/reader_test.go
@@ -2012,3 +2012,39 @@ func TestJSONReader__IncludeFieldName(t *testing.T) {
 		}
 	}
 }
+
+func TestReadFile_SkipValidation(t *testing.T) {
+	bs, err := os.ReadFile(filepath.Join("test", "testdata", "skip-validation.ach"))
+	require.NoError(t, err)
+	bs = bytes.TrimSpace(bs)
+
+	reader := bytes.NewReader(bs)
+	r := NewReader(reader)
+	r.SetValidation(&ValidateOpts{SkipAll: true})
+	f, err := r.Read()
+	require.NoError(t, err)
+
+	// Validate of file is true
+	require.NoError(t, f.Validate())
+
+	// Validate of record is false
+	if err := f.Header.Validate(); err == nil {
+		t.Errorf("got: %v, want: not nil", err)
+	}
+
+	if len(f.Batches) != 3 {
+		t.Errorf("got: %v, want: %v (batched length)", len(f.Batches), 3)
+	} else {
+		for _, batch := range f.Batches {
+			require.Error(t, batch.Validate())
+		}
+	}
+
+	// checking output
+	var buf bytes.Buffer
+	if err := NewWriter(&buf).Write(&f); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+	output := strings.TrimSpace(buf.String())
+	require.Equal(t, string(bs), output)
+}
diff --git a/server/files_test.go b/server/files_test.go
index 0cadb299..2d825d46 100644
--- a/server/files_test.go
+++ b/server/files_test.go
@@ -183,6 +183,12 @@ func TestFiles__decodeCreateFileRequest__validateOpts(t *testing.T) {
 		query  string
 		expect ach.ValidateOpts
 	}{
+		{
+			query: "?skipAll=true",
+			expect: ach.ValidateOpts{
+				SkipAll: true,
+			},
+		},
 		{
 			query: "?bypassCompanyIdentificationMatch=true",
 			expect: ach.ValidateOpts{
diff --git a/test/testdata/skip-validation.ach b/test/testdata/skip-validation.ach
new file mode 100644
index 00000000..6d00e71c
--- /dev/null
+++ b/test/testdata/skip-validation.ach
@@ -0,0 +1,20 @@
+101           2313801041503042207A094101Some Bank              Your Company Inc       A0000001
+5220Your Company Inc                    0231380104WEBTrnsNicknaMar 5 150305   0000000000000000
+622081000210123456789012345670000003521RAj##23920rjf31John Doe                0000000000000000
+6220810002105654221          0000002300RAj##32b1kn1bb3Bob Dole                0000000000000000
+6220810002105654221          0000002499RAj##765kn4    Adam Something          0000000000000000
+6220810002105654221          0000001000RAj##3j43kj4   James Bond              0000000000000000
+822000000400324000840000000000000000000093200231380104                         000000000000000
+5220Your Company Inc                    0231380104WEBTrnsNicknaMar 16150316   0000000000000000
+6220810002105654221          0000017500RAj##8k765j4k32Luke Skywalker         00000000000000000
+822000000100081000210000000000000000000175000231380104                         000000000000000
+5225Your Company Inc                    0231380104PPDTrnsNicknaMar 6 150306   0000000000000000
+627101000019923698412584     0000015000RAj##765432hj  Jane Doe              000000000000000000
+822500000100101000010000000150000000000000000231380104                         000000000000000
+9000003000002000000060050600106000000015000000000026820                                       
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999