diff --git a/README.md b/README.md index 21203e8fd..d1071d26e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ Docs: [docs.moov.io](https://docs.moov.io/en/latest/) | [api docs](https://api.m ACH is under active development but already in production for multiple companies. Please star the project if you are interested in its progress. * Library currently supports the reading and writing + * ACK (Acknowledgment Entry for CCD) * ARC (Accounts Receivable Entry) + * ATX (Acknowledgment Entry for CTX) * BOC (Back Office Conversion) * CCD (Corporate credit or debit) * CIE (Customer-Initiated Entry) diff --git a/batch.go b/batch.go index 205689b55..717f8acfd 100644 --- a/batch.go +++ b/batch.go @@ -27,8 +27,12 @@ type batch struct { // NewBatch takes a BatchHeader and returns a matching SEC code batch type that is a batcher. Returns an error if the SEC code is not supported. func NewBatch(bh *BatchHeader) (Batcher, error) { switch bh.StandardEntryClassCode { + case "ACK": + return NewBatchACK(bh), nil case "ARC": return NewBatchARC(bh), nil + case "ATX": + return NewBatchATX(bh), nil case "BOC": return NewBatchBOC(bh), nil case "CCD": diff --git a/batchACK.go b/batchACK.go new file mode 100644 index 000000000..7e92f698f --- /dev/null +++ b/batchACK.go @@ -0,0 +1,84 @@ +// 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" +) + +// BatchACK is a batch file that handles SEC payment type ACK and ACK+. +// Acknowledgement of a Corporate credit by the Receiving Depository Financial Institution (RDFI). +// For commercial accounts only. +type BatchACK struct { + batch +} + +// NewBatchACK returns a *BatchACK +func NewBatchACK(bh *BatchHeader) *BatchACK { + batch := new(BatchACK) + batch.SetControl(NewBatchControl()) + batch.SetHeader(bh) + return batch +} + +// Validate ensures the batch meets NACHA rules specific to this batch type. +func (batch *BatchACK) Validate() error { + // basic verification of the batch before we validate specific rules. + if err := batch.verify(); err != nil { + return err + } + // Add configuration and type specific validation. + if batch.Header.StandardEntryClassCode != "ACK" { + msg := fmt.Sprintf(msgBatchSECType, batch.Header.StandardEntryClassCode, "ACK") + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "StandardEntryClassCode", Msg: msg} + } + // Range through Entries + for _, entry := range batch.Entries { + // Amount must be zero for Acknowledgement Entries + if entry.Amount > 0 { + msg := fmt.Sprintf(msgBatchAmountZero, entry.Amount, "ACK") + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Amount", Msg: msg} + } + // TransactionCode must be either 24 or 34 for Acknowledgement Entries + switch entry.TransactionCode { + case 24, 34: + default: + msg := fmt.Sprintf(msgBatchTransactionCode, entry.TransactionCode, "ACK") + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TransactionCode", Msg: msg} + } + + // ACK can have up to one Record TypeCode = 05, or there can be a NOC (98) or Return (99) + for _, addenda := range entry.Addendum { + switch entry.Category { + case CategoryForward: + if err := batch.categoryForwardAddenda05(entry, addenda); err != nil { + return err + } + if len(entry.Addendum) > 1 { + msg := fmt.Sprintf(msgBatchAddendaCount, len(entry.Addendum), 1, batch.Header.StandardEntryClassCode) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "AddendaCount", Msg: msg} + } + case CategoryNOC: + if err := batch.categoryNOCAddenda98(entry, addenda); err != nil { + return err + } + case CategoryReturn: + if err := batch.categoryReturnAddenda99(entry, addenda); err != nil { + return err + } + } + } + } + return nil +} + +// Create builds the batch sequence numbers and batch control. Additional creation +func (batch *BatchACK) Create() error { + // generates sequence numbers and batch control + if err := batch.build(); err != nil { + return err + } + return batch.Validate() +} diff --git a/batchACK_test.go b/batchACK_test.go new file mode 100644 index 000000000..14ac34d48 --- /dev/null +++ b/batchACK_test.go @@ -0,0 +1,337 @@ +// 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 ( + "log" + "testing" +) + +// mockBatchACKHeader creates a ACK batch header +func mockBatchACKHeader() *BatchHeader { + bh := NewBatchHeader() + bh.ServiceClassCode = 220 + bh.StandardEntryClassCode = "ACK" + bh.CompanyName = "Your Company, inc" + bh.CompanyIdentification = "231380104" + bh.CompanyEntryDescription = "Vndr Pay" + bh.ODFIIdentification = "23138010" + return bh +} + +// mockACKEntryDetail creates a ACK entry detail +func mockACKEntryDetail() *EntryDetail { + entry := NewEntryDetail() + entry.TransactionCode = 24 + entry.SetRDFI("121042882") + entry.DFIAccountNumber = "744-5678-99" + entry.Amount = 0 + entry.SetOriginalTraceNumber("121042880000001") + entry.SetReceivingCompany("Best Co. #23") + entry.SetTraceNumber(mockBatchACKHeader().ODFIIdentification, 1) + entry.DiscretionaryData = "S" + return entry +} + +// mockBatchACK creates a ACK batch +func mockBatchACK() *BatchACK { + mockBatch := NewBatchACK(mockBatchACKHeader()) + mockBatch.AddEntry(mockACKEntryDetail()) + mockBatch.GetEntries()[0].AddAddenda(mockAddenda05()) + if err := mockBatch.Create(); err != nil { + log.Fatal(err) + } + return mockBatch +} + +// testBatchACKHeader creates a ACK batch header +func testBatchACKHeader(t testing.TB) { + batch, _ := NewBatch(mockBatchACKHeader()) + _, ok := batch.(*BatchACK) + if !ok { + t.Error("Expecting BatchACK") + } +} + +// TestBatchACKHeader tests creating a ACK batch header +func TestBatchACKHeader(t *testing.T) { + testBatchACKHeader(t) +} + +// BenchmarkBatchACKHeader benchmark creating a ACK batch header +func BenchmarkBatchACKHeader(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchACKHeader(b) + } +} + +// testBatchACKAddendumCount batch control ACK can only have one addendum per entry detail +func testBatchACKAddendumCount(t testing.TB) { + mockBatch := mockBatchACK() + // Adding a second addenda to the mock entry + mockBatch.GetEntries()[0].AddAddenda(mockAddenda05()) + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "EntryAddendaCount" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchACKAddendumCount tests batch control ACK can only have one addendum per entry detail +func TestBatchACKAddendumCount(t *testing.T) { + testBatchACKAddendumCount(t) +} + +// BenchmarkBatchACKAddendumCount benchmarks batch control ACK can only have one addendum per entry detail +func BenchmarkBatchACKAddendumCount(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchACKAddendumCount(b) + } +} + +// TestBatchACKAddendum98 validates Addenda98 returns an error +func TestBatchACKAddendum98(t *testing.T) { + mockBatch := NewBatchACK(mockBatchACKHeader()) + mockBatch.AddEntry(mockACKEntryDetail()) + mockAddenda98 := mockAddenda98() + mockAddenda98.TypeCode = "05" + mockBatch.GetEntries()[0].AddAddenda(mockAddenda98) + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchACKAddendum99 validates Addenda99 returns an error +func TestBatchACKAddendum99(t *testing.T) { + mockBatch := NewBatchACK(mockBatchACKHeader()) + mockBatch.AddEntry(mockACKEntryDetail()) + mockAddenda99 := mockAddenda99() + mockAddenda99.TypeCode = "05" + mockBatch.GetEntries()[0].AddAddenda(mockAddenda99) + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// testBatchACKReceivingCompanyName validates Receiving company / Individual name is a mandatory field +func testBatchACKReceivingCompanyName(t testing.TB) { + mockBatch := mockBatchACK() + // modify the Individual name / receiving company to nothing + mockBatch.GetEntries()[0].SetReceivingCompany("") + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "IndividualName" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchACKReceivingCompanyName tests validating receiving company / Individual name is a mandatory field +func TestBatchACKReceivingCompanyName(t *testing.T) { + testBatchACKReceivingCompanyName(t) +} + +// BenchmarkBatchACKReceivingCompanyName benchmarks validating receiving company / Individual name is a mandatory field +func BenchmarkBatchACKReceivingCompanyName(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchACKReceivingCompanyName(b) + } +} + +// testBatchACKAddendaTypeCode validates addenda type code is 05 +func testBatchACKAddendaTypeCode(t testing.TB) { + mockBatch := mockBatchACK() + mockBatch.GetEntries()[0].Addendum[0].(*Addenda05).TypeCode = "07" + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchACKAddendaTypeCode tests validating addenda type code is 05 +func TestBatchACKAddendaTypeCode(t *testing.T) { + testBatchACKAddendaTypeCode(t) +} + +// BenchmarkBatchACKAddendaTypeCod benchmarks validating addenda type code is 05 +func BenchmarkBatchACKAddendaTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchACKAddendaTypeCode(b) + } +} + +// testBatchACKSEC validates that the standard entry class code is ACK for batchACK +func testBatchACKSEC(t testing.TB) { + mockBatch := mockBatchACK() + mockBatch.Header.StandardEntryClassCode = "RCK" + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "StandardEntryClassCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchACKSEC tests validating that the standard entry class code is ACK for batchACK +func TestBatchACKSEC(t *testing.T) { + testBatchACKSEC(t) +} + +// BenchmarkBatchACKSEC benchmarks validating that the standard entry class code is ACK for batch ACK +func BenchmarkBatchACKSEC(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchACKSEC(b) + } +} + +// testBatchACKAddendaCount validates batch ACK addenda count +func testBatchACKAddendaCount(t testing.TB) { + mockBatch := mockBatchACK() + mockBatch.GetEntries()[0].AddAddenda(mockAddenda05()) + mockBatch.Create() + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "AddendaCount" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchACKAddendaCount tests validating batch ACK addenda count +func TestBatchACKAddendaCount(t *testing.T) { + testBatchACKAddendaCount(t) +} + +// BenchmarkBatchACKAddendaCount benchmarks validating batch ACK addenda count +func BenchmarkBatchACKAddendaCount(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchACKAddendaCount(b) + } +} + +// testBatchACKServiceClassCode validates ServiceClassCode +func testBatchACKServiceClassCode(t testing.TB) { + mockBatch := mockBatchACK() + // Batch Header information is required to Create a batch. + mockBatch.GetHeader().ServiceClassCode = 0 + mockBatch.Create() + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "ServiceClassCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchACKServiceClassCode tests validating ServiceClassCode +func TestBatchACKServiceClassCode(t *testing.T) { + testBatchACKServiceClassCode(t) +} + +// BenchmarkBatchACKServiceClassCode benchmarks validating ServiceClassCode +func BenchmarkBatchACKServiceClassCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchACKServiceClassCode(b) + } +} + +// testBatchACKReceivingCompanyField validates ACKReceivingCompanyField +// underlying IndividualName +func testBatchACKReceivingCompanyField(t testing.TB) { + mockBatch := mockBatchACK() + ts := mockBatch.Entries[0].ReceivingCompanyField() + if ts != "Best Co. #23 " { + t.Error("Receiving Company Field is invalid") + } +} + +// TestBatchACKReceivingCompanyField tests validating ACKReceivingCompanyField +// underlying IndividualName +func TestBatchACKReceivingCompanyFieldField(t *testing.T) { + testBatchACKReceivingCompanyField(t) +} + +// BenchmarkBatchACKReceivingCompanyField benchmarks validating ACKReceivingCompanyField +// underlying IndividualName +func BenchmarkBatchACKReceivingCompanyField(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchACKReceivingCompanyField(b) + } +} + +// TestBatchACKAmount validates Amount +func TestBatchACKAmount(t *testing.T) { + mockBatch := mockBatchACK() + // Batch Header information is required to Create a batch. + mockBatch.GetEntries()[0].Amount = 25000 + mockBatch.Create() + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "Amount" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchACKTransactionCode validates Amount +func TestBatchACKTransactionCode(t *testing.T) { + mockBatch := mockBatchACK() + // Batch Header information is required to Create a batch. + mockBatch.GetEntries()[0].TransactionCode = 22 + mockBatch.Create() + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TransactionCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} diff --git a/batchATX.go b/batchATX.go new file mode 100644 index 000000000..2f768b15f --- /dev/null +++ b/batchATX.go @@ -0,0 +1,110 @@ +// 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" +) + +// BatchATX holds the BatchHeader and BatchControl and all EntryDetail for ATX (Acknowledgment) +// Entries. +// +// The ATX entry is an acknowledgement by the Receiving Depository Financial Institution (RDFI) that a +// Corporate Credit (CTX) has been received. +type BatchATX struct { + batch +} + +var ( + msgBatchATXAddendaCount = "%v entry detail addenda records not equal to addendum %v" +) + +// NewBatchATX returns a *BatchATX +func NewBatchATX(bh *BatchHeader) *BatchATX { + batch := new(BatchATX) + batch.SetControl(NewBatchControl()) + batch.SetHeader(bh) + return batch +} + +// Validate checks valid NACHA batch rules. Assumes properly parsed records. +func (batch *BatchATX) Validate() error { + // basic verification of the batch before we validate specific rules. + if err := batch.verify(); err != nil { + return err + } + + // Add configuration and type specific validation for this type. + if batch.Header.StandardEntryClassCode != "ATX" { + msg := fmt.Sprintf(msgBatchSECType, batch.Header.StandardEntryClassCode, "ATX") + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "StandardEntryClassCode", Msg: msg} + } + + for _, entry := range batch.Entries { + // Amount must be zero for Acknowledgement Entries + if entry.Amount > 0 { + msg := fmt.Sprintf(msgBatchAmountZero, entry.Amount, "ATX") + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Amount", Msg: msg} + } + + // TransactionCode must be either 24 or 34 for Acknowledgement Entries + switch entry.TransactionCode { + // Prenote credit 23, 33, 43, 53 + // Prenote debit 28, 38, 48 + case 24, 34: + default: + msg := fmt.Sprintf(msgBatchTransactionCode, entry.TransactionCode, "ATX") + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TransactionCode", Msg: msg} + } + + // Trapping this error, as entry.ATXAddendaRecordsField() can not be greater than 9999 + if len(entry.Addendum) > 9999 { + msg := fmt.Sprintf(msgBatchAddendaCount, len(entry.Addendum), 9999, batch.Header.StandardEntryClassCode) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "AddendaCount", Msg: msg} + } + + // validate ATXAddendaRecord Field is equal to the actual number of Addenda records + // use 0 value if there is no Addenda records + addendaRecords, _ := strconv.Atoi(entry.CATXAddendaRecordsField()) + if len(entry.Addendum) != addendaRecords { + msg := fmt.Sprintf(msgBatchATXAddendaCount, addendaRecords, len(entry.Addendum)) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addendum", Msg: msg} + } + + if len(entry.Addendum) > 0 { + + // ATX can have up to 9999 Addenda Record TypeCode = 05, or there can be a NOC (98) or Return (99) + for _, addenda := range entry.Addendum { + switch entry.Category { + case CategoryForward: + if err := batch.categoryForwardAddenda05(entry, addenda); err != nil { + return err + } + case CategoryNOC: + if err := batch.categoryNOCAddenda98(entry, addenda); err != nil { + return err + } + case CategoryReturn: + if err := batch.categoryReturnAddenda99(entry, addenda); err != nil { + return err + } + } + } + } + } + return nil +} + +// Create takes Batch Header and Entries and builds a valid batch +func (batch *BatchATX) Create() error { + // generates sequence numbers and batch control + if err := batch.build(); err != nil { + return err + } + // Additional steps specific to batch type + // ... + return batch.Validate() +} diff --git a/batchATX_test.go b/batchATX_test.go new file mode 100644 index 000000000..c5abba6b7 --- /dev/null +++ b/batchATX_test.go @@ -0,0 +1,620 @@ +// 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 ( + "log" + "testing" +) + +// mockBatchATXHeader creates a BatchATX BatchHeader +func mockBatchATXHeader() *BatchHeader { + bh := NewBatchHeader() + bh.ServiceClassCode = 220 + bh.StandardEntryClassCode = "ATX" + bh.CompanyName = "Payee Name" + bh.CompanyIdentification = "231380104" + bh.CompanyEntryDescription = "ACH ATX" + bh.ODFIIdentification = "23138010" + return bh +} + +// mockATXEntryDetail creates a BatchATX EntryDetail +func mockATXEntryDetail() *EntryDetail { + entry := NewEntryDetail() + entry.TransactionCode = 24 + entry.SetRDFI("121042882") + entry.DFIAccountNumber = "744-5678-99" + entry.Amount = 0 + entry.SetOriginalTraceNumber("121042880000001") + entry.SetCATXAddendaRecords(1) + entry.SetCATXReceivingCompany("Receiver Company") + entry.SetTraceNumber(mockBatchATXHeader().ODFIIdentification, 1) + entry.DiscretionaryData = "01" + entry.Category = CategoryForward + return entry +} + +// mockBatchATX creates a BatchATX +func mockBatchATX() *BatchATX { + mockBatch := NewBatchATX(mockBatchATXHeader()) + mockBatch.AddEntry(mockATXEntryDetail()) + mockBatch.GetEntries()[0].AddAddenda(mockAddenda05()) + if err := mockBatch.Create(); err != nil { + log.Fatal(err) + } + return mockBatch +} + +// testBatchATXHeader creates a BatchATX BatchHeader +func testBatchATXHeader(t testing.TB) { + batch, _ := NewBatch(mockBatchATXHeader()) + err, ok := batch.(*BatchATX) + if !ok { + t.Errorf("Expecting BatchATX got %T", err) + } +} + +// TestBatchATXHeader tests validating BatchATX BatchHeader +func TestBatchATXHeader(t *testing.T) { + testBatchATXHeader(t) +} + +// BenchmarkBatchATXHeader benchmarks validating BatchATX BatchHeader +func BenchmarkBatchATXHeader(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXHeader(b) + } +} + +// testBatchATXCreate validates BatchATX create +func testBatchATXCreate(t testing.TB) { + mockBatch := mockBatchATX() + if err := mockBatch.Validate(); err != nil { + t.Errorf("%T: %s", err, err) + } +} + +// TestBatchATXCreate tests validating BatchATX create +func TestBatchATXCreate(t *testing.T) { + testBatchATXCreate(t) +} + +// BenchmarkBatchATXCreate benchmarks validating BatchATX create +func BenchmarkBatchATXCreate(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXCreate(b) + } +} + +// testBatchATXStandardEntryClassCode validates BatchATX create for an invalid StandardEntryClassCode +func testBatchATXStandardEntryClassCode(t testing.TB) { + mockBatch := mockBatchATX() + mockBatch.Header.StandardEntryClassCode = "WEB" + mockBatch.Create() + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "StandardEntryClassCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchATXStandardEntryClassCode tests validating BatchATX create for an invalid StandardEntryClassCode +func TestBatchATXStandardEntryClassCode(t *testing.T) { + testBatchATXStandardEntryClassCode(t) +} + +// BenchmarkBatchATXStandardEntryClassCode benchmarks validating BatchATX create for an invalid StandardEntryClassCode +func BenchmarkBatchATXStandardEntryClassCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXStandardEntryClassCode(b) + } +} + +// testBatchATXServiceClassCodeEquality validates service class code equality +func testBatchATXServiceClassCodeEquality(t testing.TB) { + mockBatch := mockBatchATX() + mockBatch.GetControl().ServiceClassCode = 200 + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "ServiceClassCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchATXServiceClassCodeEquality tests validating service class code equality +func TestBatchATXServiceClassCodeEquality(t *testing.T) { + testBatchATXServiceClassCodeEquality(t) +} + +// BenchmarkBatchATXServiceClassCodeEquality benchmarks validating service class code equality +func BenchmarkBatchATXServiceClassCodeEquality(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXServiceClassCodeEquality(b) + } +} + +// testBatchATXAddendaCount validates BatchATX Addendum count of 2 +func testBatchATXAddendaCount(t testing.TB) { + mockBatch := mockBatchATX() + mockBatch.GetEntries()[0].AddAddenda(mockAddenda05()) + mockBatch.Create() + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "Addendum" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchATXAddendaCount tests validating BatchATX Addendum count of 2 +func TestBatchATXAddendaCount(t *testing.T) { + testBatchATXAddendaCount(t) +} + +// BenchmarkBatchATXAddendaCount benchmarks validating BatchATX Addendum count of 2 +func BenchmarkBatchATXAddendaCount(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXAddendaCount(b) + } +} + +// testBatchATXAddendaCountZero validates Addendum count of 0 +func testBatchATXAddendaCountZero(t testing.TB) { + mockBatch := NewBatchATX(mockBatchATXHeader()) + mockBatch.AddEntry(mockATXEntryDetail()) + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "Addendum" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchATXAddendaCountZero tests validating Addendum count of 0 +func TestBatchATXAddendaCountZero(t *testing.T) { + testBatchATXAddendaCountZero(t) +} + +// BenchmarkBatchATXAddendaCountZero benchmarks validating Addendum count of 0 +func BenchmarkBatchATXAddendaCountZero(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXAddendaCountZero(b) + } +} + +// testBatchATXInvalidAddendum validates Addendum must be Addenda05 +func testBatchATXInvalidAddendum(t testing.TB) { + mockBatch := NewBatchATX(mockBatchATXHeader()) + mockBatch.AddEntry(mockATXEntryDetail()) + mockBatch.GetEntries()[0].AddAddenda(mockAddenda02()) + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchATXInvalidAddendum tests validating Addendum must be Addenda05 +func TestBatchATXInvalidAddendum(t *testing.T) { + testBatchATXInvalidAddendum(t) +} + +// BenchmarkBatchATXInvalidAddendum benchmarks validating Addendum must be Addenda05 +func BenchmarkBatchATXInvalidAddendum(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXInvalidAddendum(b) + } +} + +// testBatchATXInvalidAddenda validates Addendum must be Addenda05 with record type 7 +func testBatchATXInvalidAddenda(t testing.TB) { + mockBatch := NewBatchATX(mockBatchATXHeader()) + mockBatch.AddEntry(mockATXEntryDetail()) + addenda05 := mockAddenda05() + addenda05.recordType = "63" + mockBatch.GetEntries()[0].AddAddenda(addenda05) + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchATXInvalidAddenda tests validating Addendum must be Addenda05 with record type 7 +func TestBatchATXInvalidAddenda(t *testing.T) { + testBatchATXInvalidAddenda(t) +} + +// BenchmarkBatchATXInvalidAddenda benchmarks validating Addendum must be Addenda05 with record type 7 +func BenchmarkBatchATXInvalidAddenda(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXInvalidAddenda(b) + } +} + +// testBatchATXInvalidBuild validates an invalid batch build +func testBatchATXInvalidBuild(t testing.TB) { + mockBatch := mockBatchATX() + mockBatch.GetHeader().recordType = "3" + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchATXInvalidBuild tests validating an invalid batch build +func TestBatchATXInvalidBuild(t *testing.T) { + testBatchATXInvalidBuild(t) +} + +// BenchmarkBatchATXInvalidBuild benchmarks validating an invalid batch build +func BenchmarkBatchATXInvalidBuild(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXInvalidBuild(b) + } +} + +// testBatchATXAddenda10000 validates error for 10000 Addenda +func testBatchATXAddenda10000(t testing.TB) { + + bh := NewBatchHeader() + bh.ServiceClassCode = 220 + bh.StandardEntryClassCode = "ATX" + bh.CompanyName = "Payee Name" + bh.CompanyIdentification = "231380104" + bh.CompanyEntryDescription = "ACH ATX" + bh.ODFIIdentification = "23138010" + + entry := NewEntryDetail() + entry.TransactionCode = 24 + entry.SetRDFI("121042882") + entry.DFIAccountNumber = "744-5678-99" + entry.Amount = 0 + entry.SetOriginalTraceNumber("121042880000001") + entry.SetCATXAddendaRecords(9999) + entry.SetCATXReceivingCompany("Receiver Company") + entry.SetTraceNumber(mockBatchATXHeader().ODFIIdentification, 1) + entry.DiscretionaryData = "01" + entry.Category = CategoryForward + + mockBatch := NewBatchATX(bh) + mockBatch.AddEntry(entry) + + for i := 0; i < 10000; i++ { + mockBatch.GetEntries()[0].AddAddenda(mockAddenda05()) + } + + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "AddendaCount" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchATXAddenda10000 tests validating error for 10000 Addenda +func TestBatchATXAddenda10000(t *testing.T) { + testBatchATXAddenda10000(t) +} + +// BenchmarkBatchATXAddenda10000 benchmarks validating error for 10000 Addenda +func BenchmarkBatchATXAddenda10000(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXAddenda10000(b) + } +} + +// testBatchATXAddendaRecords validates error for AddendaRecords not equal to addendum +func testBatchATXAddendaRecords(t testing.TB) { + bh := NewBatchHeader() + bh.ServiceClassCode = 220 + bh.StandardEntryClassCode = "ATX" + bh.CompanyName = "Payee Name" + bh.CompanyIdentification = "231380104" + bh.CompanyEntryDescription = "ACH ATX" + bh.ODFIIdentification = "23138010" + + entry := NewEntryDetail() + entry.TransactionCode = 24 + entry.SetRDFI("121042882") + entry.DFIAccountNumber = "744-5678-99" + entry.Amount = 0 + entry.SetOriginalTraceNumber("121042880000001") + entry.SetCATXAddendaRecords(500) + entry.SetCATXReceivingCompany("Receiver Company") + entry.SetTraceNumber(mockBatchATXHeader().ODFIIdentification, 1) + entry.DiscretionaryData = "01" + entry.Category = CategoryForward + + mockBatch := NewBatchATX(bh) + mockBatch.AddEntry(entry) + + for i := 0; i < 565; i++ { + mockBatch.GetEntries()[0].AddAddenda(mockAddenda05()) + } + + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "Addendum" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchATXAddendaRecords tests validating error for AddendaRecords not equal to addendum +func TestBatchATXAddendaRecords(t *testing.T) { + testBatchATXAddendaRecords(t) +} + +// BenchmarkBatchAddendaRecords benchmarks validating error for AddendaRecords not equal to addendum +func BenchmarkBatchATXAddendaRecords(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXAddendaRecords(b) + } +} + +// testBatchATXReceivingCompany validates ATXReceivingCompany +func testBatchATXReceivingCompany(t testing.TB) { + mockBatch := mockBatchATX() + //mockBatch.GetEntries()[0].SetCATXReceivingCompany("Receiver") + + if mockBatch.GetEntries()[0].CATXReceivingCompanyField() != "Receiver Company" { + t.Errorf("expected %v got %v", "Receiver Company", mockBatch.GetEntries()[0].CATXReceivingCompanyField()) + } +} + +// TestBatchATXReceivingCompany tests validating ATXReceivingCompany +func TestBatchATXReceivingCompany(t *testing.T) { + testBatchATXReceivingCompany(t) +} + +// BenchmarkBatchATXReceivingCompany benchmarks validating ATXReceivingCompany +func BenchmarkBatchATXReceivingCompany(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXReceivingCompany(b) + } +} + +// testBatchATXReserved validates ATXReservedField +func testBatchATXReserved(t testing.TB) { + mockBatch := mockBatchATX() + + if mockBatch.GetEntries()[0].CATXReservedField() != " " { + t.Errorf("expected %v got %v", " ", mockBatch.GetEntries()[0].CATXReservedField()) + } +} + +// TestBatchATXReserved tests validating ATXReservedField +func TestBatchATXReserved(t *testing.T) { + testBatchATXReserved(t) +} + +// BenchmarkBatchATXReserved benchmarks validating ATXReservedField +func BenchmarkBatchATXReserved(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXReserved(b) + } +} + +// testBatchATXZeroAddendaRecords validates zero addenda records +func testBatchATXZeroAddendaRecords(t testing.TB) { + bh := NewBatchHeader() + bh.ServiceClassCode = 220 + bh.StandardEntryClassCode = "ATX" + bh.CompanyName = "Payee Name" + bh.CompanyIdentification = "231380104" + bh.CompanyEntryDescription = "ACH ATX" + bh.ODFIIdentification = "23138010" + + entry := NewEntryDetail() + entry.TransactionCode = 24 + entry.SetRDFI("121042882") + entry.DFIAccountNumber = "744-5678-99" + entry.Amount = 0 + entry.SetOriginalTraceNumber("121042880000001") + entry.SetCATXAddendaRecords(1) + entry.SetCATXReceivingCompany("Receiver Company") + entry.SetTraceNumber(mockBatchATXHeader().ODFIIdentification, 1) + entry.DiscretionaryData = "01" + entry.Category = CategoryForward + + mockBatch := NewBatchATX(bh) + mockBatch.AddEntry(entry) + + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "Addendum" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchATXZeroAddendaRecords tests validating zero addenda records +func TestBatchATXZeroAddendaRecords(t *testing.T) { + testBatchATXZeroAddendaRecords(t) +} + +// BenchmarkBatchZeroAddendaRecords benchmarks validating zero addenda records +func BenchmarkBatchATXZeroAddendaRecords(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXZeroAddendaRecords(b) + } +} + +// testBatchATXTransactionCode validates TransactionCode +func testBatchATXTransactionCode(t testing.TB) { + bh := NewBatchHeader() + bh.ServiceClassCode = 220 + bh.StandardEntryClassCode = "ATX" + bh.CompanyName = "Payee Name" + bh.CompanyIdentification = "231380104" + bh.CompanyEntryDescription = "ACH ATX" + bh.ODFIIdentification = "23138010" + bh.OriginatorStatusCode = 2 + + entry := NewEntryDetail() + entry.TransactionCode = 23 + entry.SetRDFI("121042882") + entry.DFIAccountNumber = "744-5678-99" + entry.Amount = 0 + entry.SetOriginalTraceNumber("121042880000001") + entry.SetCATXAddendaRecords(1) + entry.SetCATXReceivingCompany("Receiver Company") + entry.SetTraceNumber(mockBatchATXHeader().ODFIIdentification, 1) + entry.DiscretionaryData = "01" + entry.Category = CategoryForward + + mockBatch := NewBatchATX(bh) + mockBatch.AddEntry(entry) + mockBatch.GetEntries()[0].AddAddenda(mockAddenda05()) + + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TransactionCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchATXTransactionCode tests validating prenote addenda records +func TestBatchATXTransactionCode(t *testing.T) { + testBatchATXTransactionCode(t) +} + +// BenchmarkBatchATXTransactionCode benchmarks validating prenote addenda records +func BenchmarkBatchATXTransactionCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchATXTransactionCode(b) + } +} + +// TestBatchATXAmount validates Amount +func TestBatchATXAmount(t *testing.T) { + bh := NewBatchHeader() + bh.ServiceClassCode = 220 + bh.StandardEntryClassCode = "ATX" + bh.CompanyName = "Payee Name" + bh.CompanyIdentification = "231380104" + bh.CompanyEntryDescription = "ACH ATX" + bh.ODFIIdentification = "23138010" + bh.OriginatorStatusCode = 2 + + entry := NewEntryDetail() + entry.TransactionCode = 23 + entry.SetRDFI("121042882") + entry.DFIAccountNumber = "744-5678-99" + entry.Amount = 25000 + entry.SetOriginalTraceNumber("121042880000001") + entry.SetCATXAddendaRecords(1) + entry.SetCATXReceivingCompany("Receiver Company") + entry.SetTraceNumber(mockBatchATXHeader().ODFIIdentification, 1) + entry.DiscretionaryData = "01" + entry.Category = CategoryForward + + mockBatch := NewBatchATX(bh) + mockBatch.AddEntry(entry) + mockBatch.GetEntries()[0].AddAddenda(mockAddenda05()) + + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "Amount" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchATXAddendum98 validates Addenda98 returns an error +func TestBatchATXAddendum98(t *testing.T) { + mockBatch := NewBatchATX(mockBatchATXHeader()) + mockBatch.AddEntry(mockATXEntryDetail()) + mockAddenda98 := mockAddenda98() + mockAddenda98.TypeCode = "05" + mockBatch.GetEntries()[0].AddAddenda(mockAddenda98) + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchATXAddendum99 validates Addenda99 returns an error +func TestBatchATXAddendum99(t *testing.T) { + mockBatch := NewBatchATX(mockBatchATXHeader()) + mockBatch.AddEntry(mockATXEntryDetail()) + mockAddenda99 := mockAddenda99() + mockAddenda99.TypeCode = "05" + mockBatch.GetEntries()[0].AddAddenda(mockAddenda99) + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} diff --git a/batchCCD.go b/batchCCD.go index d0533ac7c..25b88b095 100644 --- a/batchCCD.go +++ b/batchCCD.go @@ -8,7 +8,7 @@ import ( "fmt" ) -// BatchCCD is a batch file that handles SEC payment type CCD amd CCD+. +// BatchCCD is a batch file that handles SEC payment type CCD and CCD+. // Corporate credit or debit. Identifies an Entry initiated by an Organization to transfer funds to or from an account of that Organization or another Organization. // For commercial accounts only. type BatchCCD struct { diff --git a/batchCTX.go b/batchCTX.go index 2c1c1b478..bd02eab84 100644 --- a/batchCTX.go +++ b/batchCTX.go @@ -54,7 +54,7 @@ func (batch *BatchCTX) Validate() error { // validate CTXAddendaRecord Field is equal to the actual number of Addenda records // use 0 value if there is no Addenda records - addendaRecords, _ := strconv.Atoi(entry.CTXAddendaRecordsField()) + addendaRecords, _ := strconv.Atoi(entry.CATXAddendaRecordsField()) if len(entry.Addendum) != addendaRecords { msg := fmt.Sprintf(msgBatchCTXAddendaCount, addendaRecords, len(entry.Addendum)) return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addendum", Msg: msg} @@ -71,7 +71,7 @@ func (batch *BatchCTX) Validate() error { default: } - // CTX can have up to one Addenda Record TypeCode = 05, or there can be a NOC (98) or Return (99) + // CTX can have up to 9999 Addenda Record TypeCode = 05, or there can be a NOC (98) or Return (99) for _, addenda := range entry.Addendum { switch entry.Category { case CategoryForward: diff --git a/batchCTX_test.go b/batchCTX_test.go index a3fc287dd..1ed5d27aa 100644 --- a/batchCTX_test.go +++ b/batchCTX_test.go @@ -29,8 +29,8 @@ func mockCTXEntryDetail() *EntryDetail { entry.DFIAccountNumber = "744-5678-99" entry.Amount = 25000 entry.IdentificationNumber = "45689033" - entry.SetCTXAddendaRecords(1) - entry.SetCTXReceivingCompany("Receiver Company") + entry.SetCATXAddendaRecords(1) + entry.SetCATXReceivingCompany("Receiver Company") entry.SetTraceNumber(mockBatchCTXHeader().ODFIIdentification, 1) entry.DiscretionaryData = "01" entry.Category = CategoryForward @@ -310,8 +310,8 @@ func testBatchCTXAddenda10000(t testing.TB) { entry.DFIAccountNumber = "744-5678-99" entry.Amount = 25000 entry.IdentificationNumber = "45689033" - entry.SetCTXAddendaRecords(9999) - entry.SetCTXReceivingCompany("Receiver Company") + entry.SetCATXAddendaRecords(9999) + entry.SetCATXReceivingCompany("Receiver Company") entry.SetTraceNumber(mockBatchCTXHeader().ODFIIdentification, 1) entry.DiscretionaryData = "01" entry.Category = CategoryForward @@ -363,8 +363,8 @@ func testBatchCTXAddendaRecords(t testing.TB) { entry.DFIAccountNumber = "744-5678-99" entry.Amount = 25000 entry.IdentificationNumber = "45689033" - entry.SetCTXAddendaRecords(500) - entry.SetCTXReceivingCompany("Receiver Company") + entry.SetCATXAddendaRecords(500) + entry.SetCATXReceivingCompany("Receiver Company") entry.SetTraceNumber(mockBatchCTXHeader().ODFIIdentification, 1) entry.DiscretionaryData = "01" entry.Category = CategoryForward @@ -403,10 +403,10 @@ func BenchmarkBatchCTXAddendaRecords(b *testing.B) { // testBatchCTXReceivingCompany validates CTXReceivingCompany func testBatchCTXReceivingCompany(t testing.TB) { mockBatch := mockBatchCTX() - //mockBatch.GetEntries()[0].SetCTXReceivingCompany("Receiver") + //mockBatch.GetEntries()[0].SetCATXReceivingCompany("Receiver") - if mockBatch.GetEntries()[0].CTXReceivingCompanyField() != "Receiver Company" { - t.Errorf("expected %v got %v", "Receiver Company", mockBatch.GetEntries()[0].CTXReceivingCompanyField()) + if mockBatch.GetEntries()[0].CATXReceivingCompanyField() != "Receiver Company" { + t.Errorf("expected %v got %v", "Receiver Company", mockBatch.GetEntries()[0].CATXReceivingCompanyField()) } } @@ -427,8 +427,8 @@ func BenchmarkBatchCTXReceivingCompany(b *testing.B) { func testBatchCTXReserved(t testing.TB) { mockBatch := mockBatchCTX() - if mockBatch.GetEntries()[0].CTXReservedField() != " " { - t.Errorf("expected %v got %v", " ", mockBatch.GetEntries()[0].CTXReservedField()) + if mockBatch.GetEntries()[0].CATXReservedField() != " " { + t.Errorf("expected %v got %v", " ", mockBatch.GetEntries()[0].CATXReservedField()) } } @@ -461,8 +461,8 @@ func testBatchCTXZeroAddendaRecords(t testing.TB) { entry.DFIAccountNumber = "744-5678-99" entry.Amount = 25000 entry.IdentificationNumber = "45689033" - entry.SetCTXAddendaRecords(1) - entry.SetCTXReceivingCompany("Receiver Company") + entry.SetCATXAddendaRecords(1) + entry.SetCATXReceivingCompany("Receiver Company") entry.SetTraceNumber(mockBatchCTXHeader().ODFIIdentification, 1) entry.DiscretionaryData = "01" entry.Category = CategoryForward @@ -511,8 +511,8 @@ func testBatchCTXPrenoteAddendaRecords(t testing.TB) { entry.DFIAccountNumber = "744-5678-99" entry.Amount = 25000 entry.IdentificationNumber = "45689033" - entry.SetCTXAddendaRecords(1) - entry.SetCTXReceivingCompany("Receiver Company") + entry.SetCATXAddendaRecords(1) + entry.SetCATXReceivingCompany("Receiver Company") entry.SetTraceNumber(mockBatchCTXHeader().ODFIIdentification, 1) entry.DiscretionaryData = "01" entry.Category = CategoryForward diff --git a/batcher.go b/batcher.go index a5a9499e1..61db54209 100644 --- a/batcher.go +++ b/batcher.go @@ -64,7 +64,8 @@ var ( msgBatchForwardReturn = "Forward and Return entries found in the same batch" msgBatchAmount = "Amount must be less than %v for SEC code %v" msgBatchCheckSerialNumber = "Check Serial Number is required for SEC code %v" - msgBatchTransactionCode = "Transaction code %v is not allowed for batch type %v" + msgBatchTransactionCode = "%v is not allowed for batch type %v" msgBatchCardTransactionType = "Card Transaction Type %v is invalid" msgBatchTransactionCodeAddenda = "Addenda not allowed for transaction code %v for batch type %v" + msgBatchAmountZero = "%v must be zero for SEC code %v" ) diff --git a/entryDetail.go b/entryDetail.go index 6abe83bb1..15607f687 100644 --- a/entryDetail.go +++ b/entryDetail.go @@ -400,32 +400,78 @@ func (ed *EntryDetail) SetReceivingCompany(s string) { ed.IndividualName = s } +// OriginalTraceNumberField is used in ACK and ATX files but returns the underlying IdentificationNumber field +func (ed *EntryDetail) OriginalTraceNumberField() string { + return ed.IdentificationNumberField() +} + +// SetOriginalTraceNumber setter for ACK and ATX OriginalTraceNumber which is underlying IdentificationNumber +func (ed *EntryDetail) SetOriginalTraceNumber(s string) { + ed.IdentificationNumber = s +} + +// ToDo: Deprecate and use SetCATXAddendaRecords + // SetCTXAddendaRecords setter for CTX AddendaRecords characters 1-4 of underlying IndividualName func (ed *EntryDetail) SetCTXAddendaRecords(i int) { ed.IndividualName = ed.numericField(i, 4) } +// ToDo: Deprecate and use SetCATXReceivingCompany + // SetCTXReceivingCompany setter for CTX ReceivingCompany characters 5-20 underlying IndividualName // Position 21-22 of underlying Individual Name are reserved blank space for CTX " " func (ed *EntryDetail) SetCTXReceivingCompany(s string) { ed.IndividualName = ed.IndividualName + ed.alphaField(s, 16) + " " } +// ToDo: Deprecate and use CATXAddendaRecordsField + // CTXAddendaRecordsField is used in CTX files, characters 1-4 of underlying IndividualName field func (ed *EntryDetail) CTXAddendaRecordsField() string { return ed.parseStringField(ed.IndividualName[0:4]) } +// ToDo: Deprecate and use CATXReceivingCompanyField + // CTXReceivingCompanyField is used in CTX files, characters 5-20 of underlying IndividualName field func (ed *EntryDetail) CTXReceivingCompanyField() string { return ed.parseStringField(ed.IndividualName[4:20]) } +// ToDo: Deprecate and use CATXReservedField + // CTXReservedField is used in CTX files, characters 21-22 of underlying IndividualName field func (ed *EntryDetail) CTXReservedField() string { return ed.IndividualName[20:22] } +// SetCATXAddendaRecords setter for CTX and ATX AddendaRecords characters 1-4 of underlying IndividualName +func (ed *EntryDetail) SetCATXAddendaRecords(i int) { + ed.IndividualName = ed.numericField(i, 4) +} + +// SetCATXReceivingCompany setter for CTX and ATX ReceivingCompany characters 5-20 underlying IndividualName +// Position 21-22 of underlying Individual Name are reserved blank space for CTX " " +func (ed *EntryDetail) SetCATXReceivingCompany(s string) { + ed.IndividualName = ed.IndividualName + ed.alphaField(s, 16) + " " +} + +// CATXAddendaRecordsField is used in CTX and ATX files, characters 1-4 of underlying IndividualName field +func (ed *EntryDetail) CATXAddendaRecordsField() string { + return ed.parseStringField(ed.IndividualName[0:4]) +} + +// CATXReceivingCompanyField is used in CTX and ATX files, characters 5-20 of underlying IndividualName field +func (ed *EntryDetail) CATXReceivingCompanyField() string { + return ed.parseStringField(ed.IndividualName[4:20]) +} + +// CATXReservedField is used in CTX and ATX files, characters 21-22 of underlying IndividualName field +func (ed *EntryDetail) CATXReservedField() string { + return ed.IndividualName[20:22] +} + // DiscretionaryDataField returns a space padded string of DiscretionaryData func (ed *EntryDetail) DiscretionaryDataField() string { return ed.alphaField(ed.DiscretionaryData, 2) diff --git a/test/ach-ack-read/ack-read.ach b/test/ach-ack-read/ack-read.ach new file mode 100644 index 000000000..73f1a08f9 --- /dev/null +++ b/test/ach-ack-read/ack-read.ach @@ -0,0 +1,10 @@ +101 031300012 2313801041810290000A094101Federal Reserve Bank My Bank Name +5220Name on Account 231380104 ACKVndr Pay 181030 0231380100000001 +624031300012744-5678-99 0000000000031300010000001Best. #1 0231380100000001 +624031300012744-5678-99 0000000000031300010000002Best. #1 0231380100000002 +82200000020006260002000000000000000000000000231380104 231380100000001 +9000001000001000000020006260002000000000000000000000000 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 \ No newline at end of file diff --git a/test/ach-ack-read/main.go b/test/ach-ack-read/main.go new file mode 100644 index 000000000..a8e1bd0bb --- /dev/null +++ b/test/ach-ack-read/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/moov-io/ach" +) + +func main() { + // open a file for reading. Any io.Reader Can be used + f, err := os.Open("ack-read.ach") + if err != nil { + log.Fatal(err) + } + r := ach.NewReader(f) + achFile, err := r.Read() + if err != nil { + fmt.Printf("Issue reading file: %+v \n", err) + } + // ensure we have a validated file structure + if achFile.Validate(); err != nil { + fmt.Printf("Could not validate entire read file: %v", err) + } + // If you trust the file but it's formatting is off building will probably resolve the malformed file. + if achFile.Create(); err != nil { + fmt.Printf("Could not build file with read properties: %v", err) + } + + fmt.Printf("Credit Total Amount: %v \n", achFile.Control.TotalCreditEntryDollarAmountInFile) + fmt.Printf("Total Amount: %v \n", achFile.Batches[0].GetEntries()[0].Amount) + fmt.Printf("SEC Code: %v \n", achFile.Batches[0].GetHeader().StandardEntryClassCode) + fmt.Printf("Original Trace Number: %v \n", achFile.Batches[0].GetEntries()[0].OriginalTraceNumberField()) +} diff --git a/test/ach-ack-read/main_test.go b/test/ach-ack-read/main_test.go new file mode 100644 index 000000000..45fcc779c --- /dev/null +++ b/test/ach-ack-read/main_test.go @@ -0,0 +1,7 @@ +package main + +import "testing" + +func Test(t *testing.T) { + main() +} diff --git a/test/ach-ack-write/main.go b/test/ach-ack-write/main.go new file mode 100644 index 000000000..854e004a8 --- /dev/null +++ b/test/ach-ack-write/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "log" + "os" + "time" + + "github.com/moov-io/ach" +) + +func main() { + // Example transfer to write an ACH ACK file acknowledging a credit + // Important: All financial institutions are different and will require registration and exact field values. + + // Set originator bank ODFI and destination Operator for the financial institution + // this is the funding/receiving source of the transfer + fh := ach.NewFileHeader() + fh.ImmediateDestination = "031300012" // Routing Number of the ACH Operator or receiving point to which the file is being sent + fh.ImmediateOrigin = "231380104" // Routing Number of the ACH Operator or sending point that is sending the file + fh.FileCreationDate = time.Now() // Today's Date + fh.ImmediateDestinationName = "Federal Reserve Bank" + fh.ImmediateOriginName = "My Bank Name" + + // BatchHeader identifies the originating entity and the type of transactions contained in the batch + bh := ach.NewBatchHeader() + bh.ServiceClassCode = 220 // ACH credit pushes money out, 225 debits/pulls money in. + bh.CompanyName = "Name on Account" // The name of the company/person that has relationship with receiver + bh.CompanyIdentification = fh.ImmediateOrigin + bh.StandardEntryClassCode = "ACK" // Consumer destination vs Company CCD + bh.CompanyEntryDescription = "Vndr Pay" // will be on receiving accounts statement + bh.EffectiveEntryDate = time.Now().AddDate(0, 0, 1) + bh.ODFIIdentification = "23138010" // Originating Routing Number + + // Identifies the receivers account information + // can be multiple entry's per batch + entry := ach.NewEntryDetail() + // Identifies the entry as a debit and credit entry AND to what type of account (Savings, DDA, Loan, GL) + entry.TransactionCode = 24 // Code 22: Demand Debit(deposit) to checking account + entry.SetRDFI("031300012") // Receivers bank transit routing number + entry.DFIAccountNumber = "744-5678-99" // Receivers bank account number + entry.Amount = 0 // Amount of transaction with no decimal. One dollar and eleven cents = 111 + entry.SetOriginalTraceNumber("031300010000001") + entry.SetReceivingCompany("Best. #1") + entry.SetTraceNumber(bh.ODFIIdentification, 1) + + entryOne := ach.NewEntryDetail() // Fee Entry + entryOne.TransactionCode = 24 // Demand Credit + entryOne.SetRDFI("031300012") // Receivers bank transit routing number + entryOne.DFIAccountNumber = "744-5678-99" // Receivers bank account number + entryOne.Amount = 0 // Amount of transaction with no decimal. One dollar and eleven cents = 111 + entryOne.SetOriginalTraceNumber("031300010000002") + entryOne.SetReceivingCompany("Best. #1") + entryOne.SetTraceNumber(bh.ODFIIdentification, 2) + + // build the batch + batch := ach.NewBatchACK(bh) + batch.AddEntry(entry) + batch.AddEntry(entryOne) + 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) + } + + // write the file to std out. Anything io.Writer + w := ach.NewWriter(os.Stdout) + if err := w.Write(file); err != nil { + log.Fatalf("Unexpected error: %s\n", err) + } + w.Flush() +} diff --git a/test/ach-ack-write/main_test.go b/test/ach-ack-write/main_test.go new file mode 100644 index 000000000..45fcc779c --- /dev/null +++ b/test/ach-ack-write/main_test.go @@ -0,0 +1,7 @@ +package main + +import "testing" + +func Test(t *testing.T) { + main() +} diff --git a/test/ach-axt-read/atx-read.ach b/test/ach-axt-read/atx-read.ach new file mode 100644 index 000000000..6573c5015 --- /dev/null +++ b/test/ach-axt-read/atx-read.ach @@ -0,0 +1,10 @@ +101 031300012 2313801041810290000A094101Federal Reserve Bank My Bank Name +5220Name on Account 231380104 ATXVndr Pay 181030 0231380100000001 +624031300012744-5678-99 00000000000313000100000010002Receiver Company 011231380100000001 +705Credit account 1 for service 00010000001 +705Credit account 2 for service 00020000001 +624031300012744-5678-99 00000000000313000100000020002Receiver Company 011231380100000002 +705Credit account 1 for leadership 00010000002 +705Credit account 2 for leadership 00020000002 +82200000060006260002000000000000000000000000231380104 231380100000001 +9000001000001000000060006260002000000000000000000000000 \ No newline at end of file diff --git a/test/ach-axt-read/main.go b/test/ach-axt-read/main.go new file mode 100644 index 000000000..2f97ac9ef --- /dev/null +++ b/test/ach-axt-read/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/moov-io/ach" +) + +func main() { + // open a file for reading. Any io.Reader Can be used + f, err := os.Open("atx-read.ach") + if err != nil { + log.Fatal(err) + } + r := ach.NewReader(f) + achFile, err := r.Read() + if err != nil { + fmt.Printf("Issue reading file: %+v \n", err) + } + // ensure we have a validated file structure + if achFile.Validate(); err != nil { + fmt.Printf("Could not validate entire read file: %v", err) + } + // If you trust the file but it's formatting is off building will probably resolve the malformed file. + if achFile.Create(); err != nil { + fmt.Printf("Could not build file with read properties: %v", err) + } + + fmt.Printf("Total Amount Debit: %v \n", achFile.Control.TotalDebitEntryDollarAmountInFile) + fmt.Printf("Total Amount Credit: %v \n", achFile.Control.TotalCreditEntryDollarAmountInFile) + fmt.Printf("SEC Code: %v \n", achFile.Batches[0].GetHeader().StandardEntryClassCode) + fmt.Printf("Total Amount: %v \n", achFile.Batches[0].GetEntries()[0].Amount) + fmt.Printf("Original Trace Number: %v \n", achFile.Batches[0].GetEntries()[0].OriginalTraceNumberField()) + fmt.Printf("Addenda1: %v \n", achFile.Batches[0].GetEntries()[0].Addendum[0].String()) + fmt.Printf("Addenda2: %v \n", achFile.Batches[0].GetEntries()[0].Addendum[1].String()) + fmt.Printf("Total Amount: %v \n", achFile.Batches[0].GetEntries()[1].Amount) + fmt.Printf("Original Trace Number: %v \n", achFile.Batches[0].GetEntries()[1].OriginalTraceNumberField()) + fmt.Printf("Addenda1: %v \n", achFile.Batches[0].GetEntries()[1].Addendum[0].String()) + fmt.Printf("Addenda2: %v \n", achFile.Batches[0].GetEntries()[1].Addendum[1].String()) +} diff --git a/test/ach-axt-read/main_test.go b/test/ach-axt-read/main_test.go new file mode 100644 index 000000000..45fcc779c --- /dev/null +++ b/test/ach-axt-read/main_test.go @@ -0,0 +1,7 @@ +package main + +import "testing" + +func Test(t *testing.T) { + main() +} diff --git a/test/ach-axt-write/main.go b/test/ach-axt-write/main.go new file mode 100644 index 000000000..1d1500312 --- /dev/null +++ b/test/ach-axt-write/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "log" + "os" + "time" + + "github.com/moov-io/ach" +) + +func main() { + // Example transfer to write an ACH AXT file acknowledging a CTX credit + // Important: All financial institutions are different and will require registration and exact field values. + + // Set originator bank ODFI and destination Operator for the financial institution + // this is the funding/receiving source of the transfer + fh := ach.NewFileHeader() + fh.ImmediateDestination = "031300012" // Routing Number of the ACH Operator or receiving point to which the file is being sent + fh.ImmediateOrigin = "231380104" // Routing Number of the ACH Operator or sending point that is sending the file + fh.FileCreationDate = time.Now() // Today's Date + fh.ImmediateDestinationName = "Federal Reserve Bank" + fh.ImmediateOriginName = "My Bank Name" + + // BatchHeader identifies the originating entity and the type of transactions contained in the batch + bh := ach.NewBatchHeader() + bh.ServiceClassCode = 220 // ACH credit pushes money out, 225 debits/pulls money in. + bh.CompanyName = "Name on Account" // The name of the company/person that has relationship with receiver + bh.CompanyIdentification = fh.ImmediateOrigin + bh.StandardEntryClassCode = "ATX" // Consumer destination vs Company CCD + bh.CompanyEntryDescription = "Vndr Pay" // will be on receiving accounts statement + bh.EffectiveEntryDate = time.Now().AddDate(0, 0, 1) + bh.ODFIIdentification = "23138010" // Originating Routing Number + + // Identifies the receivers account information + // can be multiple entry's per batch + entry := ach.NewEntryDetail() + // Identifies the entry as a debit and credit entry AND to what type of account (Savings, DDA, Loan, GL) + entry.TransactionCode = 24 // Code 22: Demand Debit(deposit) to checking account + entry.SetRDFI("031300012") // Receivers bank transit routing number + entry.DFIAccountNumber = "744-5678-99" // Receivers bank account number + entry.Amount = 0 // Amount of transaction with no decimal. One dollar and eleven cents = 111 + entry.SetOriginalTraceNumber("031300010000001") + entry.SetCATXAddendaRecords(2) + entry.SetCATXReceivingCompany("Receiver Company") + entry.SetTraceNumber(bh.ODFIIdentification, 1) + entry.DiscretionaryData = "01" + + entryOne := ach.NewEntryDetail() // Fee Entry + entryOne.TransactionCode = 24 // Demand Credit + entryOne.SetRDFI("031300012") // Receivers bank transit routing number + entryOne.DFIAccountNumber = "744-5678-99" // Receivers bank account number + entryOne.Amount = 0 // Amount of transaction with no decimal. One dollar and eleven cents = 111 + entryOne.SetOriginalTraceNumber("031300010000002") + entryOne.SetCATXAddendaRecords(2) + entryOne.SetCATXReceivingCompany("Receiver Company") + entryOne.SetTraceNumber(bh.ODFIIdentification, 2) + entryOne.DiscretionaryData = "01" + + entryAd1 := ach.NewAddenda05() + entryAd1.PaymentRelatedInformation = "Credit account 1 for service" + entryAd1.SequenceNumber = 1 + entryAd1.EntryDetailSequenceNumber = 0000001 + + entryAd2 := ach.NewAddenda05() + entryAd2.PaymentRelatedInformation = "Credit account 2 for service" + entryAd2.SequenceNumber = 2 + entryAd2.EntryDetailSequenceNumber = 0000001 + + entryOneAd1 := ach.NewAddenda05() + entryOneAd1.PaymentRelatedInformation = "Credit account 1 for leadership" + entryOneAd1.SequenceNumber = 1 + entryOneAd1.EntryDetailSequenceNumber = 0000002 + + entryOneAd2 := ach.NewAddenda05() + entryOneAd2.PaymentRelatedInformation = "Credit account 2 for leadership" + entryOneAd2.SequenceNumber = 2 + entryOneAd2.EntryDetailSequenceNumber = 0000002 + + // build the batch + batch := ach.NewBatchATX(bh) + batch.AddEntry(entry) + batch.GetEntries()[0].AddAddenda(entryAd1) + batch.GetEntries()[0].AddAddenda(entryAd2) + batch.AddEntry(entryOne) + batch.GetEntries()[1].AddAddenda(entryOneAd1) + batch.GetEntries()[1].AddAddenda(entryOneAd2) + 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) + } + + // write the file to std out. Anything io.Writer + w := ach.NewWriter(os.Stdout) + if err := w.Write(file); err != nil { + log.Fatalf("Unexpected error: %s\n", err) + } + w.Flush() +} diff --git a/test/ach-axt-write/main_test.go b/test/ach-axt-write/main_test.go new file mode 100644 index 000000000..45fcc779c --- /dev/null +++ b/test/ach-axt-write/main_test.go @@ -0,0 +1,7 @@ +package main + +import "testing" + +func Test(t *testing.T) { + main() +} diff --git a/test/ach-ctx-write/main.go b/test/ach-ctx-write/main.go index a27f71aa9..d7f1714fb 100644 --- a/test/ach-ctx-write/main.go +++ b/test/ach-ctx-write/main.go @@ -39,8 +39,8 @@ func main() { entry.DFIAccountNumber = "12345678" // Receivers bank account number entry.Amount = 100000000 // Amount of transaction with no decimal. One dollar and eleven cents = 111 entry.IdentificationNumber = "45689033" - entry.SetCTXAddendaRecords(2) - entry.SetCTXReceivingCompany("Receiver Company") + entry.SetCATXAddendaRecords(2) + entry.SetCATXReceivingCompany("Receiver Company") entry.SetTraceNumber(bh.ODFIIdentification, 1) entry.DiscretionaryData = "01"