Skip to content

Commit

Permalink
achx: fix ABA8 panic, generate correction/return trace numbers from R…
Browse files Browse the repository at this point in the history
…DFIIdentification
adamdecaf committed Nov 27, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 38ab602 commit 531465a
Showing 3 changed files with 59 additions and 30 deletions.
39 changes: 26 additions & 13 deletions internal/achx/trace.go
Original file line number Diff line number Diff line change
@@ -5,34 +5,43 @@
package achx

import (
"crypto/rand"
"fmt"
"math/rand"
"time"
"math/big"
"strconv"
"unicode/utf8"
)

var (
traceNumberSource = rand.NewSource(time.Now().Unix())
"github.com/moov-io/ach"
"github.com/pkg/errors"
)

// TraceNumber returns a trace number from a given routing number
// and uses a hidden random generator. These values are not expected
// to be cryptographically secure.
func TraceNumber(routingNumber string) string {
v := fmt.Sprintf("%s%d", ABA8(routingNumber), traceNumberSource.Int63())
//
// We decided not to increment trace numbers from a shared counter because
// it will create a lot of "NNNNN00001" trace numbers that are duplicated
// and hard to uniquely identify a Transfer.
func TraceNumber(routingNumber string) (string, error) {
n, err := rand.Int(rand.Reader, big.NewInt(1e15))
if err != nil {
return "", errors.Wrap(err, "ERROR creating trace number")
}
v := fmt.Sprintf("%s%s", ABA8(routingNumber), n.String())
if utf8.RuneCountInString(v) > 15 {
return v[:15]
return v[:15], nil
}
return v
return v, nil
}

// ABA8 returns the first 8 digits of an ABA routing number.
// If the input is invalid then an empty string is returned.
func ABA8(rtn string) string {
if n := utf8.RuneCountInString(rtn); n == 10 {
n := utf8.RuneCountInString(rtn)
if n == 10 {
return rtn[1:9] // ACH server will prefix with space, 0, or 1
}
if n := utf8.RuneCountInString(rtn); n != 8 && n != 9 {
if n != 8 && n != 9 {
return ""
}
return rtn[:8]
@@ -41,10 +50,14 @@ func ABA8(rtn string) string {
// ABACheckDigit returns the last digit of an ABA routing number.
// If the input is invalid then an empty string is returned.
func ABACheckDigit(rtn string) string {
if n := utf8.RuneCountInString(rtn); n == 10 {
n := utf8.RuneCountInString(rtn)
if n == 10 {
return rtn[9:] // ACH server will prefix with space, 0, or 1
}
if n := utf8.RuneCountInString(rtn); n != 8 && n != 9 {
if n == 8 {
return strconv.Itoa(ach.CalculateCheckDigit(rtn))
}
if n != 8 && n != 9 {
return ""
}
return rtn[8:9]
36 changes: 21 additions & 15 deletions internal/achx/trace_test.go
Original file line number Diff line number Diff line change
@@ -6,28 +6,34 @@ package achx

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestTrace__ABA(t *testing.T) {
routingNumber := "231380104"
if v := ABA8(routingNumber); v != "23138010" {
t.Errorf("got %s", v)
}
if v := ABACheckDigit(routingNumber); v != "4" {
t.Errorf("got %s", v)
}
// 9 digits
require.Equal(t, "23138010", ABA8("231380104"))
require.Equal(t, "4", ABACheckDigit("231380104"))

// 10 digit from ACH server
if v := ABA8("0123456789"); v != "12345678" {
t.Errorf("got %s", v)
}
if v := ABACheckDigit("0123456789"); v != "9" {
t.Errorf("got %s", v)
}
require.Equal(t, "12345678", ABA8("0123456789"))
require.Equal(t, "9", ABACheckDigit("0123456789"))

// 8 digits
require.Equal(t, "12345678", ABA8("12345678"))
require.Equal(t, "0", ABACheckDigit("12345678"))

// short
require.Equal(t, "", ABA8("1234"))
require.Equal(t, "", ABACheckDigit("1234"))
require.Equal(t, "", ABA8(""))
require.Equal(t, "", ABACheckDigit(""))
}

func TestTraceNumber(t *testing.T) {
if v := TraceNumber("121042882"); v == "" {
t.Error("empty trace number")
for i := 0; i < 10000; i++ {
trace, err := TraceNumber("121042882")
require.NoError(t, err)
require.NotEmpty(t, trace)
}
}
14 changes: 12 additions & 2 deletions pkg/response/entry_transformer.go
Original file line number Diff line number Diff line change
@@ -56,9 +56,14 @@ func (t *CorrectionTransformer) MorphEntry(fh ach.FileHeader, ed *ach.EntryDetai
out.IndividualName = ed.IndividualName
out.DiscretionaryData = ed.DiscretionaryData
out.AddendaRecordIndicator = 1
out.TraceNumber = achx.TraceNumber(fh.ImmediateDestination)
out.Category = ach.CategoryNOC

if trace, err := achx.TraceNumber(ed.RDFIIdentificationField()); err != nil {
return out, fmt.Errorf("generating trace number: %w", err)
} else {
out.TraceNumber = trace
}

// Create the NOC addenda
addenda98 := ach.NewAddenda98()
addenda98.ChangeCode = action.Correction.Code
@@ -115,9 +120,14 @@ func (t *ReturnTransformer) MorphEntry(fh ach.FileHeader, ed *ach.EntryDetail, a
out.IndividualName = ed.IndividualName
out.DiscretionaryData = ed.DiscretionaryData
out.AddendaRecordIndicator = 1
out.TraceNumber = achx.TraceNumber(fh.ImmediateDestination)
out.Category = ach.CategoryReturn

if trace, err := achx.TraceNumber(ed.RDFIIdentificationField()); err != nil {
return out, fmt.Errorf("generating trace number: %w", err)
} else {
out.TraceNumber = trace
}

// Create the Return addenda
addenda99 := ach.NewAddenda99()
addenda99.ReturnCode = action.Return.Code

0 comments on commit 531465a

Please sign in to comment.