diff --git a/checkDetail.go b/checkDetail.go index b01bb9ba..801df42a 100644 --- a/checkDetail.go +++ b/checkDetail.go @@ -151,6 +151,8 @@ type CheckDetail struct { validator // converters is composed for imagecashletter to golang Converters converters + + validateOpts *ValidateOpts } // NewCheckDetail returns a new CheckDetail with default values for non exported fields @@ -167,6 +169,14 @@ func (cd *CheckDetail) setRecordType() { cd.recordType = "25" } +// SetValidation stores ValidateOpts on the CheckDetail which are to be used to override +func (cd *CheckDetail) SetValidation(opts *ValidateOpts) { + if cd == nil { + return + } + cd.validateOpts = opts +} + // Parse takes the input record string and parses the CheckDetail values func (cd *CheckDetail) Parse(record string) { if utf8.RuneCountInString(record) < 80 { @@ -285,10 +295,13 @@ func (cd *CheckDetail) Validate() error { } // Conditional if cd.ArchiveTypeIndicator != "" { - if err := cd.isArchiveTypeIndicator(cd.ArchiveTypeIndicator); err != nil { - return &FieldError{FieldName: "ArchiveTypeIndicator", Value: cd.ArchiveTypeIndicator, Msg: err.Error()} + if cd.validateOpts == nil || cd.validateOpts.SkipAll || !cd.validateOpts.AllowInvalidArchiveTypeIndicator { + if err := cd.isArchiveTypeIndicator(cd.ArchiveTypeIndicator); err != nil { + return &FieldError{FieldName: "ArchiveTypeIndicator", Value: cd.ArchiveTypeIndicator, Msg: err.Error()} + } } } + return nil } diff --git a/checkDetail_test.go b/checkDetail_test.go index 2a4f6c34..da033272 100644 --- a/checkDetail_test.go +++ b/checkDetail_test.go @@ -294,6 +294,16 @@ func TestCDArchiveTypeIndicator(t *testing.T) { } } +// TestCDArchiveTypeIndicatorWithValidationOption validation +func TestCDArchiveTypeIndicatorWithValidationOption(t *testing.T) { + cd := mockCheckDetail() + cd.ArchiveTypeIndicator = "W" + cd.SetValidation(&ValidateOpts{AllowInvalidArchiveTypeIndicator: true}) + if err := cd.Validate(); err != nil { + t.Errorf("%T: %s", err, err) + } +} + // Field Inclusion // TestCDFIRecordType validation diff --git a/cmd/writeImageCashLetter/main.go b/cmd/writeImageCashLetter/main.go index 023f95f3..ca97b22a 100644 --- a/cmd/writeImageCashLetter/main.go +++ b/cmd/writeImageCashLetter/main.go @@ -20,6 +20,9 @@ var ( // output formats flagJson = flag.Bool("json", false, "Output file in json") + + 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") ) // main creates an ICL File with 2 CashLetters @@ -72,6 +75,12 @@ func write(path string) { file := imagecashletter.NewFile() file.SetHeader(fh) + // Read validation options from the command + validateOpts := readValidationOpts(*flagValidateOpts) + if validateOpts != nil { + file.SetValidation(validateOpts) + } + // Create 4 CashLetters for i := 0; i < 4; i++ { @@ -284,3 +293,32 @@ func write(path string) { fmt.Printf("Wrote %s\n", path) } + +func readValidationOpts(path string) *imagecashletter.ValidateOpts { + if path != "" { + // read config file + bs, readErr := os.ReadFile(path) + if readErr != nil { + fmt.Printf("ERROR: reading validate opts failed: %v\n", readErr) + os.Exit(1) + } + + var opts imagecashletter.ValidateOpts + if err := json.Unmarshal(bs, &opts); err != nil { + fmt.Printf("ERROR: unmarshal of validate opts failed: %v\n", err) + os.Exit(1) + } + if *flagSkipValidation { + opts.SkipAll = true + } + return &opts + } + + if *flagSkipValidation { + var opts imagecashletter.ValidateOpts + opts.SkipAll = true + return &opts + } + + return nil +} diff --git a/cmd/writeImageCashLetter/main_test.go b/cmd/writeImageCashLetter/main_test.go index e423bca6..2cb605ce 100644 --- a/cmd/writeImageCashLetter/main_test.go +++ b/cmd/writeImageCashLetter/main_test.go @@ -1,8 +1,11 @@ package main import ( + "encoding/json" "os" "testing" + + "github.com/moov-io/imagecashletter" ) // TestFileCreate tests creating an ICL File @@ -36,3 +39,35 @@ func testFileWrite(t testing.TB) { t.Fatal("expected non-empty file") } } + +// TestReadValidationOpts +func TestReadValidationOpts(t *testing.T) { + tmp, err := os.CreateTemp("", "config") + if err != nil { + t.Fatal(err.Error()) + } + defer os.Remove(tmp.Name()) + + f, err := os.Create(tmp.Name()) + if err != nil { + t.Fatal(err.Error()) + } + + buf, _ := json.Marshal(&imagecashletter.ValidateOpts{}) + f.Write(buf) + f.Close() + + if opt := readValidationOpts(tmp.Name()); opt == nil { + t.Fatal("unable to create config") + } + + if opt := readValidationOpts(""); opt != nil { + t.Fatal("does not to create any config") + } + + *flagSkipValidation = true + if opt := readValidationOpts(""); opt == nil { + t.Fatal("unable to create config") + } + +} diff --git a/file.go b/file.go index e7c743e5..d8fa2ac8 100644 --- a/file.go +++ b/file.go @@ -91,6 +91,15 @@ var ( msgFileCredit = "Credit outside of cash letter" ) +// ValidateOpts contains specific overrides from the default set of validations +type ValidateOpts struct { + // SkipAll will disable all validation checks of a File. It has no effect when set on records. + SkipAll bool `json:"skipAll"` + + // AllowInvalidArchiveTypeIndicator can be set to disable archiveTypeIndicator validation + AllowInvalidArchiveTypeIndicator bool `json:"allowInvalidArchiveType"` +} + // FileError is an error describing issues validating a file type FileError struct { FieldName string @@ -119,6 +128,8 @@ type File struct { Bundles []Bundle `json:"bundle,omitempty"` // FileControl is an imagecashletter FileControl Control FileControl `json:"fileControl"` + + validateOpts *ValidateOpts } // NewFile constructs a file template with a FileHeader and FileControl. @@ -322,3 +333,17 @@ func (f *File) setRecordTypes() { } f.Control.setRecordType() } + +// SetValidation stores ValidateOpts +func (f *File) SetValidation(opts *ValidateOpts) { + if f == nil || opts == nil { + return + } + + f.validateOpts = opts +} + +// ValidateOpts returns Validation option +func (f *File) ValidateOpts() *ValidateOpts { + return f.validateOpts +} diff --git a/reader.go b/reader.go index cd105cf9..40839ead 100644 --- a/reader.go +++ b/reader.go @@ -383,6 +383,8 @@ func (r *Reader) parseCheckDetail() error { } cd := new(CheckDetail) cd.Parse(r.decodeLine(r.line)) + // setting validate opts based on file + cd.SetValidation(r.File.ValidateOpts()) // Ensure valid CheckDetail if err := cd.Validate(); err != nil { return r.error(err) @@ -454,6 +456,8 @@ func (r *Reader) parseReturnDetail() error { } rd := new(ReturnDetail) rd.Parse(r.decodeLine(r.line)) + // setting validate opts based on file + rd.SetValidation(r.File.ValidateOpts()) if err := rd.Validate(); err != nil { return r.error(err) } diff --git a/returnDetail.go b/returnDetail.go index c574cd5c..c3edcee3 100644 --- a/returnDetail.go +++ b/returnDetail.go @@ -147,6 +147,8 @@ type ReturnDetail struct { validator // converters is composed for image cash letter to golang Converters converters + + validateOpts *ValidateOpts } // CustomerReturnCode are customer return reason codes as defined in Part 6.2 of the ANSI X9.100-188-2018 Return @@ -175,6 +177,14 @@ func (rd *ReturnDetail) setRecordType() { rd.recordType = "31" } +// SetValidation stores ValidateOpts on the CheckDetail which are to be used to override +func (rd *ReturnDetail) SetValidation(opts *ValidateOpts) { + if rd == nil { + return + } + rd.validateOpts = opts +} + // Parse takes the input record string and parses the ReturnDetail values func (rd *ReturnDetail) Parse(record string) { if utf8.RuneCountInString(record) < 72 { @@ -266,6 +276,7 @@ func (rd *ReturnDetail) String() string { // Validate performs image cash letter format rule checks on the record and returns an error if not Validated // The first error encountered is returned and stops the parsing. func (rd *ReturnDetail) Validate() error { + if err := rd.fieldInclusion(); err != nil { return err } @@ -289,8 +300,10 @@ func (rd *ReturnDetail) Validate() error { } } if rd.ArchiveTypeIndicator != "" { - if err := rd.isArchiveTypeIndicator(rd.ArchiveTypeIndicator); err != nil { - return &FieldError{FieldName: "ArchiveTypeIndicator", Value: rd.ArchiveTypeIndicatorField(), Msg: err.Error()} + if rd.validateOpts == nil || rd.validateOpts.SkipAll || !rd.validateOpts.AllowInvalidArchiveTypeIndicator { + if err := rd.isArchiveTypeIndicator(rd.ArchiveTypeIndicator); err != nil { + return &FieldError{FieldName: "ArchiveTypeIndicator", Value: rd.ArchiveTypeIndicatorField(), Msg: err.Error()} + } } } if rd.TimesReturnedField() != " " && rd.TimesReturnedField() != "" { diff --git a/returnDetail_test.go b/returnDetail_test.go index 573d3c9a..f8cb107f 100644 --- a/returnDetail_test.go +++ b/returnDetail_test.go @@ -248,6 +248,16 @@ func TestRDArchiveTypeIndicator(t *testing.T) { } } +// TestRDArchiveTypeIndicatorWithValidationOption validation +func TestRDArchiveTypeIndicatorWithValidationOption(t *testing.T) { + rd := mockReturnDetail() + rd.ArchiveTypeIndicator = "W" + rd.SetValidation(&ValidateOpts{AllowInvalidArchiveTypeIndicator: true}) + if err := rd.Validate(); err != nil { + t.Errorf("%T: %s", err, err) + } +} + // TestRDTimesReturned validation func TestRDTimesReturned(t *testing.T) { rd := mockReturnDetail()