Skip to content

Commit

Permalink
Merge pull request #1470 from adamdecaf/fix-ENR-parsing
Browse files Browse the repository at this point in the history
fix: cleanup ENR parsing and generation, add tests
  • Loading branch information
adamdecaf authored Aug 20, 2024
2 parents 0ba616b + 605754b commit a8f63e0
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 10 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## v1.42.0 (Released 2024-08-20)

BREAKING CHANGES

This release of moov-io/ach adjusts the type of `ENRPaymentInformation.EnrolleeClassificationCode` to be a string (was int). This change is needed to properly support the values "A" (consumer) and "B" (company).

IMPROVEMENTS

- fix: cleanup ENR parsing and generation, add tests

## v1.41.1 (Released 2024-08-14)

IMPROVEMENTS
Expand Down
49 changes: 41 additions & 8 deletions batchENR.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
)

// BatchENR is a non-monetary entry that enrolls a person with an agency of the US government
Expand Down Expand Up @@ -128,12 +129,40 @@ type ENRPaymentInformation struct {
// 1: (yes) - Initiated by someone other than named beneficiary
// A: Enrollee is a consumer
// b: Enrollee is a company
EnrolleeClassificationCode int
EnrolleeClassificationCode string
}

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)
// Stretch the companies name across two fields
var individualName string
if strings.EqualFold(info.EnrolleeClassificationCode, "B") {
// First fifteen characters are added
individualName = strings.TrimSpace(fmt.Sprintf("%15.15s", info.IndividualName)) + "*"

// Add on a second field if needed
runes := utf8.RuneCountInString(info.IndividualName)
if runes > 15 {
individualName += strings.TrimSpace(fmt.Sprintf("%7.7s", info.IndividualName[15:]))
}
} else {
// Format the Individual's name by Surname first
nameParts := strings.Fields(info.IndividualName)

if len(nameParts) > 1 {
// Surname comes fist
nameParts = append(nameParts[len(nameParts)-1:], nameParts[:len(nameParts)-1]...)
}
individualName = strings.Join(nameParts, "*")
}

return fmt.Sprintf(`%v*%v*%v*%v*%v*%v*%v\`,
info.TransactionCode,
info.RDFIIdentification,
info.CheckDigit,
info.DFIAccountNumber,
info.IndividualIdentification,
individualName,
info.EnrolleeClassificationCode)
}

// ParsePaymentInformation returns an ENRPaymentInformation for a given Addenda05 record. The information is parsed from the addenda's
Expand All @@ -150,9 +179,13 @@ func (batch *BatchENR) ParsePaymentInformation(addenda05 *Addenda05) (*ENRPaymen
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)

enrolleeClassificationCode := parts[7]

individualName := fmt.Sprintf("%s %s", parts[6], parts[5])
if strings.EqualFold(enrolleeClassificationCode, "B") {
// Business Names can be fill two field lengths
individualName = fmt.Sprintf("%s%s", parts[5], parts[6])
}

return &ENRPaymentInformation{
Expand All @@ -161,7 +194,7 @@ func (batch *BatchENR) ParsePaymentInformation(addenda05 *Addenda05) (*ENRPaymen
CheckDigit: parts[2],
DFIAccountNumber: parts[3],
IndividualIdentification: parts[4],
IndividualName: fmt.Sprintf("%s %s", parts[6], parts[5]),
EnrolleeClassificationCode: enrolleeCode,
IndividualName: individualName,
EnrolleeClassificationCode: enrolleeClassificationCode,
}, nil
}
81 changes: 79 additions & 2 deletions batchENR_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"testing"

"github.com/moov-io/base"
"github.com/stretchr/testify/require"
)

// mockBatchENRHeader creates a ENR batch header
Expand Down Expand Up @@ -289,9 +290,85 @@ func TestBatchENR__PaymentInformation(t *testing.T) {
if v := info.IndividualName; v != "JOHN DOE" {
t.Errorf("IndividualName: %s", v)
}
if v := info.EnrolleeClassificationCode; v != 1 {
t.Errorf("EnrolleeClassificationCode: %d", v)
if v := info.EnrolleeClassificationCode; v != "1" {
t.Errorf("EnrolleeClassificationCode: %v", v)
}

b := &BatchENR{}
addenda05 = NewAddenda05()

t.Run("Nacha Examples", func(t *testing.T) {
// Consumer
input := `22*12200004*3*123987654321*777777777*DOE*JOHN*0\`
expected := &ENRPaymentInformation{
TransactionCode: 22,
RDFIIdentification: "12200004",
CheckDigit: "3",
DFIAccountNumber: "123987654321",
IndividualIdentification: "777777777",
IndividualName: "JOHN DOE",
EnrolleeClassificationCode: "0",
}
addenda05.PaymentRelatedInformation = input
parsed, err := b.ParsePaymentInformation(addenda05)
require.NoError(t, err)
require.Equal(t, expected, parsed)
require.Equal(t, input, parsed.String())

// Consumer
input = `27*12200004*3*123987654321*777777777*DOE*JOHN*A\`
addenda05.PaymentRelatedInformation = input
parsed, err = b.ParsePaymentInformation(addenda05)
require.NoError(t, err)

expected = &ENRPaymentInformation{
TransactionCode: 27,
RDFIIdentification: "12200004",
CheckDigit: "3",
DFIAccountNumber: "123987654321",
IndividualIdentification: "777777777",
IndividualName: "JOHN DOE",
EnrolleeClassificationCode: "A",
}
require.Equal(t, expected, parsed)
require.Equal(t, input, parsed.String())

// Company
input = `27*12200004*3*987654321123*876543210*ABCELECTRONICIN*DUSTRIE*B\`
addenda05.PaymentRelatedInformation = input
parsed, err = b.ParsePaymentInformation(addenda05)
require.NoError(t, err)

expected = &ENRPaymentInformation{
TransactionCode: 27,
RDFIIdentification: "12200004",
CheckDigit: "3",
DFIAccountNumber: "987654321123",
IndividualIdentification: "876543210",
IndividualName: "ABCELECTRONICINDUSTRIE",
EnrolleeClassificationCode: "B",
}
require.Equal(t, expected, parsed)
require.Equal(t, input, parsed.String())

// Company
input = `27*12200004*3*987654321123*876543210*ELECTRIC**B\`
addenda05.PaymentRelatedInformation = input
parsed, err = b.ParsePaymentInformation(addenda05)
require.NoError(t, err)

expected = &ENRPaymentInformation{
TransactionCode: 27,
RDFIIdentification: "12200004",
CheckDigit: "3",
DFIAccountNumber: "987654321123",
IndividualIdentification: "876543210",
IndividualName: "ELECTRIC",
EnrolleeClassificationCode: "B",
}
require.Equal(t, expected, parsed)
require.Equal(t, input, parsed.String())
})
}

// TestBatchENRValidTranCodeForServiceClassCode validates a transactionCode based on ServiceClassCode
Expand Down

0 comments on commit a8f63e0

Please sign in to comment.