Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"strconv"
"strings"
"unicode/utf8"

"github.com/moov-io/base"
)

// Batch holds the Batch Header and Batch Control and all Entry Records
Expand Down Expand Up @@ -301,6 +303,18 @@ func (batch *Batch) Validate() error {
return errors.New("use an implementation of batch or NewBatch")
}

// ValidateAll checks properties of the ACH batch and returns ALL errors found.
// Unlike Validate which returns the first error, this method accumulates all validation errors.
//
// ValidateAll will never modify the batch.
func (batch *Batch) ValidateAll() base.ErrorList {
// This stub returns nil - specific batch types should override this method.
// If this method is called on the base Batch type, we treat it as an error.
var errors base.ErrorList
errors.Add(batch.Error("Batch", ErrBatchSECType))
return errors
}

// SetValidation stores ValidateOpts on the Batch which are to be used to override
// the default NACHA validation rules.
func (batch *Batch) SetValidation(opts *ValidateOpts) {
Expand Down Expand Up @@ -395,6 +409,98 @@ func (batch *Batch) verify() error {
return nil
}

// verifyAll checks basic valid NACHA batch rules and returns ALL errors found.
// Unlike verify which returns on first error, this method accumulates all validation errors.
func (batch *Batch) verifyAll() base.ErrorList {
var errors base.ErrorList

// No entries in batch
if len(batch.Entries) <= 0 && len(batch.ADVEntries) <= 0 {
errors.Add(batch.Error("entries", ErrBatchNoEntries))
}
// verify field inclusion in all the records of the batch.
if err := batch.isFieldInclusion(); err != nil {
// convert the field error in to a batch error for a consistent api
errors.Add(batch.Error("FieldError", err))
}

if !batch.IsADV() {
// validate batch header and control codes are the same
if (batch.validateOpts == nil || !batch.validateOpts.UnequalServiceClassCode) &&
batch.Header.ServiceClassCode != batch.Control.ServiceClassCode {
errors.Add(batch.Error("ServiceClassCode",
NewErrBatchHeaderControlEquality(batch.Header.ServiceClassCode, batch.Control.ServiceClassCode)))
}
// Company Identification in the batch header and control must match if bypassCompanyIdentificationMatch is not enabled.
if batch.Header.CompanyIdentification != batch.Control.CompanyIdentification &&
!(batch.validateOpts != nil && batch.validateOpts.BypassCompanyIdentificationMatch) {
errors.Add(batch.Error("CompanyIdentification",
NewErrBatchHeaderControlEquality(batch.Header.CompanyIdentification, batch.Control.CompanyIdentification)))
}

// Control ODFIIdentification must be the same as batch header
if batch.Header.ODFIIdentification != batch.Control.ODFIIdentification {
errors.Add(batch.Error("ODFIIdentification",
NewErrBatchHeaderControlEquality(batch.Header.ODFIIdentification, batch.Control.ODFIIdentification)))
}
// batch number header and control must match
if batch.Header.BatchNumber != batch.Control.BatchNumber {
errors.Add(batch.Error("BatchNumber",
NewErrBatchHeaderControlEquality(batch.Header.BatchNumber, batch.Control.BatchNumber)))
}
} else {
if (batch.validateOpts == nil || !batch.validateOpts.UnequalServiceClassCode) &&
batch.Header.ServiceClassCode != batch.ADVControl.ServiceClassCode {
errors.Add(batch.Error("ServiceClassCode",
NewErrBatchHeaderControlEquality(batch.Header.ServiceClassCode, batch.ADVControl.ServiceClassCode)))
}
// Control ODFIIdentification must be the same as batch header
if batch.Header.ODFIIdentification != batch.ADVControl.ODFIIdentification {
errors.Add(batch.Error("ODFIIdentification",
NewErrBatchHeaderControlEquality(batch.Header.ODFIIdentification, batch.ADVControl.ODFIIdentification)))
}
// batch number header and control must match
if batch.Header.BatchNumber != batch.ADVControl.BatchNumber {
errors.Add(batch.Error("BatchNumber",
NewErrBatchHeaderControlEquality(batch.Header.BatchNumber, batch.ADVControl.BatchNumber)))
}
}

if err := batch.isBatchEntryCount(); err != nil {
errors.Add(err)
}
if batch.validateOpts == nil || !batch.validateOpts.CustomTraceNumbers {
if err := batch.isSequenceAscending(); err != nil {
errors.Add(err)
}
}
if err := batch.isBatchAmount(); err != nil {
errors.Add(err)
}
if err := batch.isEntryHash(); err != nil {
errors.Add(err)
}
if err := batch.isOriginatorDNE(); err != nil {
errors.Add(err)
}
if batch.validateOpts == nil || !batch.validateOpts.CustomTraceNumbers {
if err := batch.isTraceNumberODFI(); err != nil {
errors.Add(err)
}
if err := batch.isAddendaSequence(); err != nil {
errors.Add(err)
}
}
if err := batch.isCategory(); err != nil {
errors.Add(err)
}

if errors.Empty() {
return nil
}
return errors
}

// Build creates valid batch by building sequence numbers and batch control. An error is returned if
// the batch being built has invalid records.
func (batch *Batch) build() error {
Expand Down
30 changes: 30 additions & 0 deletions batchACK.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

package ach

import "github.com/moov-io/base"

// BatchACK is a batch file that handles SEC payment type ACK and ACK+.
// Acknowledgement of a Corporate credit by the Receiving Depository Financial Institution (RDFI).
// For commercial accounts only.
Expand Down Expand Up @@ -59,6 +61,34 @@ func (batch *BatchACK) Validate() error {
return nil
}

// ValidateAll checks properties of the ACH batch and returns ALL errors found.
func (batch *BatchACK) ValidateAll() base.ErrorList {
if batch.validateOpts != nil && (batch.validateOpts.SkipAll || batch.validateOpts.BypassBatchValidation) {
return nil
}

var errors base.ErrorList

if verifyErrs := batch.verifyAll(); verifyErrs != nil {
for _, err := range verifyErrs {
errors.Add(err)
}
}
Comment on lines +72 to +76
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This loop to add errors from verifyAll can be simplified by using the variadic Add method on base.ErrorList. This would make the code more concise.

This pattern is repeated across many of the new ValidateAll implementations in other batch*.go files, and the same suggestion applies there.

    if verifyErrs := batch.verifyAll(); verifyErrs != nil {
        errors.Add(verifyErrs...)
    }


if batch.Header.StandardEntryClassCode != ACK {
errors.Add(batch.Error("StandardEntryClassCode", ErrBatchSECType, ACK))
}

for _, inv := range batch.InvalidEntries() {
errors.Add(inv.Error)
}

if errors.Empty() {
return nil
}
return errors
}

// InvalidEntries returns entries with validation errors in the batch
func (batch *BatchACK) InvalidEntries() []InvalidEntry {
var out []InvalidEntry
Expand Down
37 changes: 37 additions & 0 deletions batchADV.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

package ach

import "github.com/moov-io/base"

// BatchADV holds the Batch Header and Batch Control and all Entry Records for ADV Entries
//
// The ADV entry identifies a Non-Monetary Entry that is used by an ACH Operator to provide accounting information
Expand Down Expand Up @@ -66,6 +68,41 @@ func (batch *BatchADV) Validate() error {
return nil
}

// ValidateAll checks properties of the ACH batch and returns ALL errors found.
func (batch *BatchADV) ValidateAll() base.ErrorList {
if batch.validateOpts != nil && (batch.validateOpts.SkipAll || batch.validateOpts.BypassBatchValidation) {
return nil
}

var errors base.ErrorList

if batch.Header.StandardEntryClassCode != ADV {
errors.Add(batch.Error("StandardEntryClassCode", ErrBatchSECType, ADV))
}
if batch.Header.ServiceClassCode != AutomatedAccountingAdvices {
errors.Add(batch.Error("ServiceClassCode", ErrBatchServiceClassCode, batch.Header.ServiceClassCode))
}
if batch.Header.OriginatorStatusCode != 0 {
errors.Add(batch.Error("OriginatorStatusCode", ErrOrigStatusCode, batch.Header.OriginatorStatusCode))
}

// basic verification of the batch
if verifyErrs := batch.verifyAll(); verifyErrs != nil {
for _, err := range verifyErrs {
errors.Add(err)
}
}

for _, inv := range batch.InvalidEntries() {
errors.Add(inv.Error)
}

if errors.Empty() {
return nil
}
return errors
}

// InvalidEntries returns entries with validation errors in the batch
func (batch *BatchADV) InvalidEntries() []InvalidEntry {
var out []InvalidEntry
Expand Down
36 changes: 36 additions & 0 deletions batchARC.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

package ach

import "github.com/moov-io/base"

// BatchARC holds the BatchHeader and BatchControl and all EntryDetail for ARC Entries.
//
// Accounts Receivable Entry (ARC). A consumer check converted to a one-time ACH debit.
Expand Down Expand Up @@ -76,6 +78,40 @@ func (batch *BatchARC) Validate() error {
return nil
}

// ValidateAll checks properties of the ACH batch and returns ALL errors found.
func (batch *BatchARC) ValidateAll() base.ErrorList {
if batch.validateOpts != nil && (batch.validateOpts.SkipAll || batch.validateOpts.BypassBatchValidation) {
return nil
}

var errors base.ErrorList

if verifyErrs := batch.verifyAll(); verifyErrs != nil {
for _, err := range verifyErrs {
errors.Add(err)
}
}

if batch.Header.StandardEntryClassCode != ARC {
errors.Add(batch.Error("StandardEntryClassCode", ErrBatchSECType, ARC))
}

// ARC detail entries can only be a debit, ServiceClassCode must allow debits
switch batch.Header.ServiceClassCode {
case CreditsOnly:
errors.Add(batch.Error("ServiceClassCode", ErrBatchServiceClassCode, batch.Header.ServiceClassCode))
}

for _, inv := range batch.InvalidEntries() {
errors.Add(inv.Error)
}

if errors.Empty() {
return nil
}
return errors
}

// InvalidEntries returns entries with validation errors in the batch
func (batch *BatchARC) InvalidEntries() []InvalidEntry {
var out []InvalidEntry
Expand Down
30 changes: 30 additions & 0 deletions batchATX.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package ach

import (
"strconv"

"github.com/moov-io/base"
)

// BatchATX holds the BatchHeader and BatchControl and all EntryDetail for ATX (Acknowledgment)
Expand Down Expand Up @@ -66,6 +68,34 @@ func (batch *BatchATX) Validate() error {
return nil
}

// ValidateAll checks properties of the ACH batch and returns ALL errors found.
func (batch *BatchATX) ValidateAll() base.ErrorList {
if batch.validateOpts != nil && (batch.validateOpts.SkipAll || batch.validateOpts.BypassBatchValidation) {
return nil
}

var errors base.ErrorList

if verifyErrs := batch.verifyAll(); verifyErrs != nil {
for _, err := range verifyErrs {
errors.Add(err)
}
}

if batch.Header.StandardEntryClassCode != ATX {
errors.Add(batch.Error("StandardEntryClassCode", ErrBatchSECType, ATX))
}

for _, inv := range batch.InvalidEntries() {
errors.Add(inv.Error)
}

if errors.Empty() {
return nil
}
return errors
}

// InvalidEntries returns entries with validation errors in the batch
func (batch *BatchATX) InvalidEntries() []InvalidEntry {
var out []InvalidEntry
Expand Down
35 changes: 35 additions & 0 deletions batchBOC.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

package ach

import "github.com/moov-io/base"

// BatchBOC holds the BatchHeader and BatchControl and all EntryDetail for BOC Entries.
//
// Back Office Conversion (BOC) A single entry debit initiated at the point of purchase
Expand Down Expand Up @@ -81,6 +83,39 @@ func (batch *BatchBOC) Validate() error {
return nil
}

// ValidateAll checks properties of the ACH batch and returns ALL errors found.
func (batch *BatchBOC) ValidateAll() base.ErrorList {
if batch.validateOpts != nil && (batch.validateOpts.SkipAll || batch.validateOpts.BypassBatchValidation) {
return nil
}

var errors base.ErrorList

if verifyErrs := batch.verifyAll(); verifyErrs != nil {
for _, err := range verifyErrs {
errors.Add(err)
}
}

if batch.Header.StandardEntryClassCode != BOC {
errors.Add(batch.Error("StandardEntryClassCode", ErrBatchSECType, BOC))
}

switch batch.Header.ServiceClassCode {
case CreditsOnly:
errors.Add(batch.Error("ServiceClassCode", ErrBatchServiceClassCode, batch.Header.ServiceClassCode))
}

for _, inv := range batch.InvalidEntries() {
errors.Add(inv.Error)
}

if errors.Empty() {
return nil
}
return errors
}

// InvalidEntries returns entries with validation errors in the batch
func (batch *BatchBOC) InvalidEntries() []InvalidEntry {
var out []InvalidEntry
Expand Down
Loading
Loading