Skip to content

Commit

Permalink
Add BatchENR
Browse files Browse the repository at this point in the history
  • Loading branch information
adamdecaf committed Nov 5, 2018
1 parent ad70d15 commit 3b4ea81
Show file tree
Hide file tree
Showing 12 changed files with 661 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ script:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then test -z $(gofmt -s -l $GOFILES); fi
- go test ./...
- misspell -error -locale US $GOFILES
- gocyclo -over 19 $GOFILES
- gocyclo -over 25 $GOFILES
- golint -set_exit_status $GOFILES
- megacheck ./...
after_success:
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ ACH is under active development but already in production for multiple companies
* CIE (Customer-Initiated Entry)
* COR (Automated Notification of Change(NOC))
* CTX (Corporate Trade Exchange)
* DNE (Death Notification Entry)
* DNE (Death Notification Entry)
* ENR (Automatic Enrollment Entry)
* IAT (International ACH Transactions)
* POP (Point of Purchase)
* POS (Point of Sale)
Expand Down
2 changes: 2 additions & 0 deletions batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ func NewBatch(bh *BatchHeader) (Batcher, error) {
return NewBatchCTX(bh), nil
case "DNE":
return NewBatchDNE(bh), nil
case "ENR":
return NewBatchENR(bh), nil
case "IAT":
msg := fmt.Sprintf(msgFileIATSEC, bh.StandardEntryClassCode)
return nil, &FileError{FieldName: "StandardEntryClassCode", Value: bh.StandardEntryClassCode, Msg: msg}
Expand Down
173 changes: 173 additions & 0 deletions batchENR.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Copyright 2018 The Moov Authors
// Use of this source code is governed by an Apache License
// license that can be found in the LICENSE file.

package ach

import (
"fmt"
"strconv"
"strings"
)

// BatchENR is a non-monetary entry that enrolls a person with an agency of the US government
// for a depository financial institution.
//
// Allowed TransactionCode values: 22 Demand Credit, 27 Demand Debit, 32 Savings Credit, 37 Savings Debit
type BatchENR struct {
batch
}

var (
msgBatchENRAddendaType = "%T found where Addenda05 is required for SEC type ENR"
)

func NewBatchENR(bh *BatchHeader) *BatchENR {
batch := new(BatchENR)
batch.SetControl(NewBatchControl())
batch.SetHeader(bh)
return batch
}

// Validate ensures the batch meets NACHA rules specific to this batch type.
func (batch *BatchENR) Validate() error {
if err := batch.verify(); err != nil {
return err
}

// Batch Header checks
if batch.Header.StandardEntryClassCode != "ENR" {
msg := fmt.Sprintf(msgBatchSECType, batch.Header.StandardEntryClassCode, "ENR")
return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "StandardEntryClassCode", Msg: msg}
}
if batch.Header.CompanyEntryDescription != "AUTOENROLL" {
msg := fmt.Sprintf(msgBatchCompanyEntryDescription, batch.Header.CompanyEntryDescription, "ENR, must be AUTOENROLL")
return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "CompanyEntryDescription", Msg: msg}
}

// Range over Entries
for _, entry := range batch.Entries {
if err := entry.Validate(); err != nil {
return err
}

if entry.Amount != 0 {
msg := fmt.Sprintf(msgBatchAmountZero, entry.Amount, "ENR")
return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Amount", Msg: msg}
}

switch entry.TransactionCode {
case 22, 27, 32, 37:
default:
msg := fmt.Sprintf(msgBatchTransactionCode, entry.TransactionCode, "ENR")
return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TransactionCode", Msg: msg}
}

// ENR must have one Addenda05
if len(entry.Addendum) <= 0 || len(entry.Addendum) > 9999 {
msg := fmt.Sprintf(msgBatchAddendaCount, len(entry.Addendum), 1, batch.Header.StandardEntryClassCode)
return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "AddendaCount", Msg: msg}
}
}

// Check Addenda05
return batch.isAddenda05()
}

// Create builds the batch sequence numbers and batch control.
func (batch *BatchENR) Create() error {
// generates sequence numbers and batch control
if err := batch.build(); err != nil {
return err
}
return batch.Validate()
}

// isAddenda05 verifies that a Addenda05 exists for each EntryDetail and is Validated
func (batch *BatchENR) isAddenda05() error {
if len(batch.Entries) != 1 {
return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "entries", Msg: msgBatchEntries}
}

for _, entry := range batch.Entries {
// Addenda type assertion must be Addenda05
addenda05, ok := entry.Addendum[0].(*Addenda05)
if !ok {
msg := fmt.Sprintf(msgBatchENRAddendaType, entry.Addendum[0])
return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addendum", Msg: msg}
}
// Addenda05 must be Validated
if err := addenda05.Validate(); err != nil {
// convert the field error in to a batch error for a consistent api
if e, ok := err.(*FieldError); ok {
return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: e.FieldName, Msg: e.Msg}
}
}
}
return nil
}

type ENRPaymentInformation struct {
// TransactionCode is the Transaction Code of the holder's account
// Values: 22 (Demand Credit), 27 (Demand Debit), 32 (Savings Credit), 37 (Savings Debit)
TransactionCode int

// RDFIIdentification is the Receiving Depository Identification Number. Typically the first 8 of their ABA routing number.
RDFIIdentification string

// CheckDigit is the last digit from an ABA routing number.
CheckDigit string

// DFIAccountNumber contains the holder's account number.
DFIAccountNumber string

// IndividualIdentification contains the customer's Social Security Number (SSN) for automated enrollments and the
// taxpayer ID for companies.
IndividualIdentification string

// IndividualName is the account holders full name.
IndividualName string

// EnrolleeClassificationCode (also called Representative Payee Indicator) returns a code from a specific Addenda05 record.
// These codes represent:
// 0: (no) - Initiated by beneficiary
// 1: (yes) - Initiated by someone other than named beneficiary
// A: Enrollee is a consumer
// b: Enrollee is a company
EnrolleeClassificationCode int
}

func (info *ENRPaymentInformation) String() string {
line := "TransactionCode: %d, RDFIIdentification: %s, CheckDigit: %s, DFIAccountNumber: %s, IndividualIdentification: %v, IndividualName: %s, EnrolleeClassificationCode: %d"
return fmt.Sprintf(line, info.TransactionCode, info.RDFIIdentification, info.CheckDigit, info.DFIAccountNumber, info.IndividualIdentification != "", info.IndividualName, info.EnrolleeClassificationCode)
}

// ParsePaymentInformation returns an ENRPaymentInformation for a given Addenda05 record. The information is parsed from the addenda's
// PaymentRelatedInformation field.
//
// The returned information is not validated for correctness.
func (batch *BatchENR) ParsePaymentInformation(addenda05 *Addenda05) (*ENRPaymentInformation, error) {
parts := strings.Split(strings.TrimSuffix(addenda05.PaymentRelatedInformation, `\`), "*") // PaymentRelatedInformation is terminated by '\'
if len(parts) != 8 {
return nil, fmt.Errorf("ENR: unable to parse Addenda05 (%s) PaymentRelatedInformation", addenda05.ID)
}

txCode, err := strconv.Atoi(parts[0])
if err != nil {
return nil, fmt.Errorf("ENR: unable to parse TransactionCode (%s) from Addenda05.ID=%s", parts[0], addenda05.ID)
}
enrolleeCode, err := strconv.Atoi(parts[7])
if err != nil {
return nil, fmt.Errorf("ENR: unable to parse EnrolleeClassificationCode (%s) from Addenda05.ID=%s", parts[7], addenda05.ID)
}

return &ENRPaymentInformation{
TransactionCode: txCode,
RDFIIdentification: parts[1],
CheckDigit: parts[2],
DFIAccountNumber: parts[3],
IndividualIdentification: parts[4],
IndividualName: fmt.Sprintf("%s %s", parts[6], parts[5]),
EnrolleeClassificationCode: enrolleeCode,
}, nil
}
Loading

0 comments on commit 3b4ea81

Please sign in to comment.