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
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.