Repository: moov-io/ach
Pull request #729 (2020-06-03 17:58:14)

entries: allow custom trace numbers
Allowing custom TraceNumbers allows extra features to be written using ACH files. This is commonly used for products created in the past few years. 

There's also a branch with https://github.com/moov-io/ach/pull/728 -- https://github.com/moov-io/ach/tree/custom-entry-fields 


Patch:
diff --git a/batch.go b/batch.go
index 6ae447ff..b0dbb608 100644
--- a/batch.go
+++ b/batch.go
@@ -353,8 +353,10 @@ func (batch *Batch) verify() error {
 	if err := batch.isBatchEntryCount(); err != nil {
 		return err
 	}
-	if err := batch.isSequenceAscending(); err != nil {
-		return err
+	if batch.validateOpts == nil || !batch.validateOpts.CustomTraceNumbers {
+		if err := batch.isSequenceAscending(); err != nil {
+			return err
+		}
 	}
 	if err := batch.isBatchAmount(); err != nil {
 		return err
@@ -365,11 +367,13 @@ func (batch *Batch) verify() error {
 	if err := batch.isOriginatorDNE(); err != nil {
 		return err
 	}
-	if err := batch.isTraceNumberODFI(); err != nil {
-		return err
-	}
-	if err := batch.isAddendaSequence(); err != nil {
-		return err
+	if batch.validateOpts == nil || !batch.validateOpts.CustomTraceNumbers {
+		if err := batch.isTraceNumberODFI(); err != nil {
+			return err
+		}
+		if err := batch.isAddendaSequence(); err != nil {
+			return err
+		}
 	}
 	if err := batch.isCategory(); err != nil {
 		return err
@@ -407,8 +411,13 @@ func (batch *Batch) build() error {
 
 			// Add a sequenced TraceNumber if one is not already set. Have to keep original trance number Return and NOC entries
 			if currentTraceNumberODFI != batchHeaderODFI {
-				if batch.validateOpts == nil || !batch.validateOpts.BypassOriginValidation {
+				if opts := batch.validateOpts; opts == nil {
 					entry.SetTraceNumber(batch.Header.ODFIIdentification, seq)
+				} else {
+					// Automatically set the TraceNumber if we are validating Origin and don't have custom trace numbers
+					if !opts.BypassOriginValidation && !opts.CustomTraceNumbers {
+						entry.SetTraceNumber(batch.Header.ODFIIdentification, seq)
+					}
 				}
 			}
 			seq++
diff --git a/file.go b/file.go
index 64a2a4ed..c17f3b9c 100644
--- a/file.go
+++ b/file.go
@@ -552,6 +552,9 @@ type ValidateOpts struct {
 
 	// CheckTransactionCode allows for custom validation of TransactionCode values
 	CheckTransactionCode func(code int) error
+
+	// CustomTraceNumbers disables validation of TraceNumbers
+	CustomTraceNumbers bool `json:"customTraceNumbers"`
 }
 
 // ValidateWith performs NACHA format rule checks on each record according to their specification


Test patch:
diff --git a/batch_test.go b/batch_test.go
index ffb23139..fb3989d0 100644
--- a/batch_test.go
+++ b/batch_test.go
@@ -1438,3 +1438,29 @@ func TestBatch__isTraceNumberODFI(t *testing.T) {
 		t.Errorf("unexpected error: %v", err)
 	}
 }
+
+func TestBatch__CustomTraceNumbers(t *testing.T) {
+	file := mockFilePPD()
+
+	file.Batches[0].SetValidation(&ValidateOpts{
+		BypassOriginValidation: false,
+		CustomTraceNumbers:     false,
+	})
+
+	batch, ok := file.Batches[0].(*BatchPPD)
+	if !ok {
+		t.Fatalf("unexpected batch: %T", file.Batches[0])
+	}
+
+	for i := range batch.Entries {
+		batch.Entries[i].TraceNumber = "333344445"
+	}
+
+	if err := batch.build(); err != nil {
+		t.Fatal(err)
+	}
+
+	if batch.Entries[0].TraceNumber != "121042880000001" {
+		t.Errorf("unexpected trace number: %v", batch.Entries[0].TraceNumber)
+	}
+}
diff --git a/examples/example_custom_trace_number_test.go b/examples/example_custom_trace_number_test.go
new file mode 100644
index 00000000..c942b20b
--- /dev/null
+++ b/examples/example_custom_trace_number_test.go
@@ -0,0 +1,81 @@
+// Licensed to The Moov Authors under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. The Moov Authors licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package examples
+
+import (
+	"fmt"
+	"log"
+
+	"github.com/moov-io/ach"
+)
+
+// Example_customTraceNumber writes an ACH file with a non-standard NACHA TraceNumber
+func Example_customTraceNumber() {
+	fh := mockFileHeader()
+
+	bh := ach.NewBatchHeader()
+	bh.ServiceClassCode = ach.CreditsOnly
+	bh.CompanyName = "My Company"
+	bh.CompanyIdentification = fh.ImmediateOrigin
+	bh.StandardEntryClassCode = ach.PPD
+	bh.CompanyEntryDescription = "Cash Back"
+	// fix EffectiveEntryDate for consistent output
+	bh.EffectiveEntryDate = "190816"
+	bh.ODFIIdentification = "987654320"
+
+	entry := ach.NewEntryDetail()
+	entry.TransactionCode = ach.CheckingCredit
+	entry.SetRDFI("123456780")
+	entry.DFIAccountNumber = "12567"
+	entry.Amount = 100000000
+	entry.TraceNumber = "321888"
+	entry.IndividualName = "Jane Smith"
+
+	// build the batch
+	batch := ach.NewBatchPPD(bh)
+	batch.SetValidation(&ach.ValidateOpts{
+		CustomTraceNumbers: true,
+	})
+	batch.AddEntry(entry)
+
+	if err := batch.Create(); err != nil {
+		log.Fatalf("Unexpected error building batch: %s\n", err)
+	}
+
+	// build the file
+	file := ach.NewFile()
+	file.SetHeader(fh)
+	file.AddBatch(batch)
+	if err := file.Create(); err != nil {
+		log.Fatalf("Unexpected error building file: %s\n", err)
+	}
+	if err := file.Validate(); err != nil {
+		log.Fatalf("Unexpected validation error: %v", err)
+	}
+
+	if trace := file.Batches[0].GetEntries()[0].TraceNumber; trace != "321888" {
+		log.Fatalf("Unexpected trace number: %s", trace)
+	}
+
+	fmt.Printf("TransactionCode=%d\n", file.Batches[0].GetEntries()[0].TransactionCode)
+	fmt.Printf("%s\n", file.Batches[0].GetEntries()[0].String())
+
+	// Output:
+	// TransactionCode=22
+	// 62212345678012567            0100000000               Jane Smith              0000000000321888
+}