From 83902a2b56e05385853972b7640f4e71414f8d7c Mon Sep 17 00:00:00 2001 From: Adam Shannon Date: Fri, 2 Nov 2018 17:15:18 -0500 Subject: [PATCH] Add BatchENR Fixes: https://github.com/moov-io/ach/issues/343 --- README.md | 3 +- batch.go | 2 + batchENR.go | 143 ++++++++++++++ batchENR_test.go | 322 ++++++++++++++++++++++++++++++++ batchHeader.go | 7 +- batchHeader_test.go | 10 + test/ach-enr-read/enr-read.ach | 10 + test/ach-enr-read/main.go | 46 +++++ test/ach-enr-read/main_test.go | 7 + test/ach-enr-write/main.go | 71 +++++++ test/ach-enr-write/main_test.go | 7 + 11 files changed, 626 insertions(+), 2 deletions(-) create mode 100644 batchENR.go create mode 100644 batchENR_test.go create mode 100644 test/ach-enr-read/enr-read.ach create mode 100644 test/ach-enr-read/main.go create mode 100644 test/ach-enr-read/main_test.go create mode 100644 test/ach-enr-write/main.go create mode 100644 test/ach-enr-write/main_test.go diff --git a/README.md b/README.md index a742338c7..992032b5e 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ ACH is under active development but already in production for multiple companies * CIE (Customer-Initiated Entry) * COR (Automated Notification of Change(NOC)) * CTX (Corporate Trade Exchange) - * DNE (Death Notification Entry) + * DNE (Death Notification Entry) + * ENR (Automatic Enrollment Entry) * IAT (International ACH Transactions) * POP (Point of Purchase) * POS (Point of Sale) diff --git a/batch.go b/batch.go index e257cde5b..8e6a2639e 100644 --- a/batch.go +++ b/batch.go @@ -45,6 +45,8 @@ func NewBatch(bh *BatchHeader) (Batcher, error) { return NewBatchCTX(bh), nil case "DNE": return NewBatchDNE(bh), nil + case "ENR": + return NewBatchENR(bh), nil case "IAT": msg := fmt.Sprintf(msgFileIATSEC, bh.StandardEntryClassCode) return nil, &FileError{FieldName: "StandardEntryClassCode", Value: bh.StandardEntryClassCode, Msg: msg} diff --git a/batchENR.go b/batchENR.go new file mode 100644 index 000000000..cab09f950 --- /dev/null +++ b/batchENR.go @@ -0,0 +1,143 @@ +// 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" + "strings" +) + +// BatchENR is a non-monetary entry that enrolls a person with an agency of the US government +// for a depository financial institution. +// +// Allowed TransactionCode values: 22 Demand Credit, 27 Demand Debit, 32 Savings Credit, 37 Savings Debit +type BatchENR struct { + batch +} + +func NewBatchENR(bh *BatchHeader) *BatchENR { + batch := new(BatchENR) + batch.SetControl(NewBatchControl()) + batch.SetHeader(bh) + return batch +} + +// Validate ensures the batch meets NACHA rules specific to this batch type. +func (batch *BatchENR) Validate() error { + if err := batch.verify(); err != nil { + return err + } + + // Batch Header checks + if batch.Header.StandardEntryClassCode != "ENR" { + msg := fmt.Sprintf(msgBatchSECType, batch.Header.StandardEntryClassCode, "ENR") + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "StandardEntryClassCode", Msg: msg} + } + if batch.Header.CompanyEntryDescription != "AUTOENROLL" { + msg := fmt.Sprintf(msgBatchCompanyEntryDescription, batch.Header.CompanyEntryDescription, "ENR, must be AUTOENROLL") + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "CompanyEntryDescription", Msg: msg} + } + + // Range over Entries + for _, entry := range batch.Entries { + if err := entry.Validate(); err != nil { + return err + } + + if entry.Amount != 0 { + msg := fmt.Sprintf(msgBatchAmountZero, entry.Amount, "ENR") + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Amount", Msg: msg} + } + + switch entry.TransactionCode { + case 22, 27, 32, 37: + default: + msg := fmt.Sprintf(msgBatchTransactionCode, entry.TransactionCode, "ENR") + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TransactionCode", Msg: msg} + } + + // ENR must have one Addenda05 + // Verify Addenda* FieldInclusion based on entry.Category and batchHeader.StandardEntryClassCode + if err := batch.addendaFieldInclusion(entry); err != nil { + return err + } + } + return nil +} + +// Create builds the batch sequence numbers and batch control. +func (batch *BatchENR) Create() error { + // generates sequence numbers and batch control + if err := batch.build(); err != nil { + return err + } + return batch.Validate() +} + +type ENRPaymentInformation struct { + // TransactionCode is the Transaction Code of the holder's account + // Values: 22 (Demand Credit), 27 (Demand Debit), 32 (Savings Credit), 37 (Savings Debit) + TransactionCode int + + // RDFIIdentification is the Receiving Depository Identification Number. Typically the first 8 of their ABA routing number. + RDFIIdentification string + + // CheckDigit is the last digit from an ABA routing number. + CheckDigit string + + // DFIAccountNumber contains the holder's account number. + DFIAccountNumber string + + // IndividualIdentification contains the customer's Social Security Number (SSN) for automated enrollments and the + // taxpayer ID for companies. + IndividualIdentification string + + // IndividualName is the account holders full name. + IndividualName string + + // EnrolleeClassificationCode (also called Representative Payee Indicator) returns a code from a specific Addenda05 record. + // These codes represent: + // 0: (no) - Initiated by beneficiary + // 1: (yes) - Initiated by someone other than named beneficiary + // A: Enrollee is a consumer + // b: Enrollee is a company + EnrolleeClassificationCode int +} + +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) +} + +// ParsePaymentInformation returns an ENRPaymentInformation for a given Addenda05 record. The information is parsed from the addenda's +// PaymentRelatedInformation field. +// +// The returned information is not validated for correctness. +func (batch *BatchENR) ParsePaymentInformation(addenda05 *Addenda05) (*ENRPaymentInformation, error) { + parts := strings.Split(strings.TrimSuffix(addenda05.PaymentRelatedInformation, `\`), "*") // PaymentRelatedInformation is terminated by '\' + if len(parts) != 8 { + return nil, fmt.Errorf("ENR: unable to parse Addenda05 (%s) PaymentRelatedInformation", addenda05.ID) + } + + txCode, err := strconv.Atoi(parts[0]) + 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) + } + + return &ENRPaymentInformation{ + TransactionCode: txCode, + RDFIIdentification: parts[1], + CheckDigit: parts[2], + DFIAccountNumber: parts[3], + IndividualIdentification: parts[4], + IndividualName: fmt.Sprintf("%s %s", parts[6], parts[5]), + EnrolleeClassificationCode: enrolleeCode, + }, nil +} diff --git a/batchENR_test.go b/batchENR_test.go new file mode 100644 index 000000000..c356692c0 --- /dev/null +++ b/batchENR_test.go @@ -0,0 +1,322 @@ +// 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" +) + +// mockBatchENRHeader creates a ENR batch header +func mockBatchENRHeader() *BatchHeader { + bh := NewBatchHeader() + bh.ServiceClassCode = 220 + bh.CompanyName = "Name on Account" + bh.CompanyIdentification = "231380104" + bh.StandardEntryClassCode = "ENR" + bh.CompanyEntryDescription = "AUTOENROLL" + bh.ODFIIdentification = "23138010" + return bh +} + +// mockENREntryDetail creates a ENR entry detail +func mockENREntryDetail() *EntryDetail { + entry := NewEntryDetail() + entry.TransactionCode = 22 + entry.SetRDFI("031300012") + entry.DFIAccountNumber = "744-5678-99" + entry.Amount = 0 + entry.SetOriginalTraceNumber("031300010000001") + entry.SetReceivingCompany("Best. #1") + entry.SetTraceNumber("23138010", 1) + + addenda := NewAddenda05() + addenda.PaymentRelatedInformation = `21*12200004*3*123987654321*777777777*DOE*JOHN*1\` + entry.AddAddenda05(addenda) + entry.AddendaRecordIndicator = 1 + + return entry +} + +// mockBatchENR creates a ENR batch +func mockBatchENR() *BatchENR { + batch := NewBatchENR(mockBatchENRHeader()) + batch.AddEntry(mockENREntryDetail()) + if err := batch.Create(); err != nil { + log.Fatalf("Unexpected error building batch: %s\n", err) + } + return batch +} + +// testBatchENRHeader creates a ENR batch header +func testBatchENRHeader(t testing.TB) { + batch, _ := NewBatch(mockBatchENRHeader()) + _, ok := batch.(*BatchENR) + if !ok { + t.Error("Expecting BatchENR") + } +} + +// TestBatchENRHeader tests creating a ENR batch header +func TestBatchENRHeader(t *testing.T) { + testBatchENRHeader(t) +} + +// BenchmarkBatchENRHeader benchmark creating a ENR batch header +func BenchmarkBatchENRHeader(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchENRHeader(b) + } +} + +// testBatchENRAddendumCount batch control ENR must have 1-9999 Addenda05 records +func testBatchENRAddendumCount(t testing.TB) { + mockBatch := mockBatchENR() + // Adding a second addenda to the mock entry + mockBatch.GetEntries()[0].AddAddenda05(mockAddenda05()) + if err := mockBatch.Create(); err != nil { + t.Errorf("Adding addenda is allowed: %v", err) + } + if err := mockBatch.Validate(); err != nil { + t.Errorf("Adding addendas is allowed: %v", err) + } +} + +// TestBatchENRAddendumCount tests batch control ENR can only have one addendum per entry detail +func TestBatchENRAddendumCount(t *testing.T) { + testBatchENRAddendumCount(t) +} + +// BenchmarkBatchENRAddendumCount benchmarks batch control ENR can only have one addendum per entry detail +func BenchmarkBatchENRAddendumCount(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchENRAddendumCount(b) + } +} + +// TestBatchENRAddendum98 validates Addenda05 returns an error +func TestBatchENRAddendum98(t *testing.T) { + mockBatch := NewBatchENR(mockBatchENRHeader()) + mockBatch.AddEntry(mockENREntryDetail()) + 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) + } + } +} + +// testBatchENRCompanyEntryDescription validates CompanyEntryDescription +func testBatchENRCompanyEntryDescription(t testing.TB) { + mockBatch := mockBatchENR() + mockBatch.Header.CompanyEntryDescription = "bad" + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "CompanyEntryDescription" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchENRCompanyEntryDescription tests validating receiving company / Individual name is a mandatory field +func TestBatchENRCompanyEntryDescription(t *testing.T) { + testBatchENRCompanyEntryDescription(t) +} + +// BenchmarkBatchENRCompanyEntryDescription benchmarks validating receiving company / Individual name is a mandatory field +func BenchmarkBatchENRCompanyEntryDescription(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchENRCompanyEntryDescription(b) + } +} + +// testBatchENRAddendaTypeCode validates addenda type code is 05 +func testBatchENRAddendaTypeCode(t testing.TB) { + mockBatch := mockBatchENR() + mockBatch.GetEntries()[0].Addenda05[0].TypeCode = "98" + 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) + } + } +} + +// TestBatchENRAddendaTypeCode tests validating addenda type code is 05 +func TestBatchENRAddendaTypeCode(t *testing.T) { + testBatchENRAddendaTypeCode(t) +} + +// BenchmarkBatchENRAddendaTypeCod benchmarks validating addenda type code is 05 +func BenchmarkBatchENRAddendaTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchENRAddendaTypeCode(b) + } +} + +// testBatchENRSEC validates that the standard entry class code is ENR for batchENR +func testBatchENRSEC(t testing.TB) { + mockBatch := mockBatchENR() + mockBatch.Header.StandardEntryClassCode = "ACK" + 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) + } + } +} + +// TestBatchENRSEC tests validating that the standard entry class code is ENR for batchENR +func TestBatchENRSEC(t *testing.T) { + testBatchENRSEC(t) +} + +// BenchmarkBatchENRSEC benchmarks validating that the standard entry class code is ENR for batch ENR +func BenchmarkBatchENRSEC(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchENRSEC(b) + } +} + +// testBatchENRAddendaCount validates batch ENR addenda count +func testBatchENRAddendaCount(t testing.TB) { + mockBatch := mockBatchENR() + mockBatch.GetEntries()[0].AddAddenda05(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) + } + } +} + +// TestBatchENRAddendaCount tests validating batch ENR addenda count +func TestBatchENRAddendaCount(t *testing.T) { + testBatchENRAddendaCount(t) +} + +// BenchmarkBatchENRAddendaCount benchmarks validating batch ENR addenda count +func BenchmarkBatchENRAddendaCount(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchENRAddendaCount(b) + } +} + +// testBatchENRServiceClassCode validates ServiceClassCode +func testBatchENRServiceClassCode(t testing.TB) { + mockBatch := mockBatchENR() + 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) + } + } +} + +// TestBatchENRServiceClassCode tests validating ServiceClassCode +func TestBatchENRServiceClassCode(t *testing.T) { + testBatchENRServiceClassCode(t) +} + +// BenchmarkBatchENRServiceClassCode benchmarks validating ServiceClassCode +func BenchmarkBatchENRServiceClassCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchENRServiceClassCode(b) + } +} + +// TestBatchENRAmount validates Amount +func TestBatchENRAmount(t *testing.T) { + mockBatch := mockBatchENR() + 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) + } + } +} + +// TestBatchENRTransactionCode validates TransactionCode +func TestBatchENRTransactionCode(t *testing.T) { + mockBatch := mockBatchENR() + mockBatch.GetEntries()[0].TransactionCode = 21 + 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) + } + } +} + +func TestBatchENR__PaymentInformation(t *testing.T) { + batch := mockBatchENR() + if err := batch.Validate(); err != nil { + t.Fatal(err) + } + addenda05 := batch.GetEntries()[0].Addenda05[0] + info, err := batch.ParsePaymentInformation(addenda05) + if err != nil { + t.Fatal(err) + } + + if v := info.TransactionCode; v != 21 { + t.Errorf("TransactionCode: %d", v) + } + if v := info.RDFIIdentification; v != "12200004" { + t.Errorf("RDFIIdentification: %s", v) + } + if v := info.CheckDigit; v != "3" { + t.Errorf("CheckDigit: %s", v) + } + if v := info.DFIAccountNumber; v != "123987654321" { + t.Errorf("DFIAccountNumber: %s", v) + } + if v := info.IndividualIdentification; v != "777777777" { + t.Errorf("IndividualIdentification: %s", v) + } + if v := info.IndividualName; v != "JOHN DOE" { + t.Errorf("IndividualName: %s", v) + } + if v := info.EnrolleeClassificationCode; v != 1 { + t.Errorf("EnrolleeClassificationCode: %d", v) + } +} diff --git a/batchHeader.go b/batchHeader.go index 69af8d5d9..7abf20fe6 100644 --- a/batchHeader.go +++ b/batchHeader.go @@ -295,7 +295,12 @@ func (bh *BatchHeader) CompanyDescriptiveDateField() string { // EffectiveEntryDateField get the EffectiveEntryDate in YYMMDD format func (bh *BatchHeader) EffectiveEntryDateField() string { - return bh.formatSimpleDate(bh.EffectiveEntryDate) + // ENR records require EffectiveEntryDate to be space filled. NACHA Page OR108 + if bh.CompanyEntryDescription == "AUTOENROLL" { + return bh.alphaField("", 6) // YYMMDD + } else { + return bh.formatSimpleDate(bh.EffectiveEntryDate) + } } // ODFIIdentificationField get the odfi number zero padded diff --git a/batchHeader_test.go b/batchHeader_test.go index e0a85b008..2c1e7a604 100644 --- a/batchHeader_test.go +++ b/batchHeader_test.go @@ -567,3 +567,13 @@ func BenchmarkBHFieldInclusionODFIIdentification(b *testing.B) { testBHFieldInclusionODFIIdentification(b) } } + +func TestBatchHeaderENR__EffectiveEntryDateField(t *testing.T) { + bh := mockBatchHeader() + + // ENR batches require EffectiveEntryDate to be space filled + bh.CompanyEntryDescription = "AUTOENROLL" + if v, ans := bh.EffectiveEntryDateField(), " "; v != ans { + t.Errorf("got %q (len=%d), expected space filled (len=6)", v, len(ans)) + } +} diff --git a/test/ach-enr-read/enr-read.ach b/test/ach-enr-read/enr-read.ach new file mode 100644 index 000000000..455a323bd --- /dev/null +++ b/test/ach-enr-read/enr-read.ach @@ -0,0 +1,10 @@ +101 031300012 2313801041811020000A094101Federal Reserve Bank My Bank Name +5220Name on Account 231380104 ENRAUTOENROLL 0231380100000001 +627031300012744-5678-99 0000000000031300010000001Best. #1 1231380100000001 +70522*12200004*3*123987654321*777777777*DOE*JOHN*1\ 00010000001 +82200000020003130001000000000000000000000000231380104 231380100000001 +9000001000001000000020003130001000000000000000000000000 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 diff --git a/test/ach-enr-read/main.go b/test/ach-enr-read/main.go new file mode 100644 index 000000000..38730ab80 --- /dev/null +++ b/test/ach-enr-read/main.go @@ -0,0 +1,46 @@ +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("enr-read.ach") + if err != nil { + log.Fatal(err) + } + r := ach.NewReader(f) + achFile, err := r.Read() + if err != nil { + panic(fmt.Sprintf("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: %v \n", achFile.Batches[0].GetEntries()[0].Amount) + fmt.Printf("SEC Code: %v \n", achFile.Batches[0].GetHeader().StandardEntryClassCode) + + batch, ok := achFile.Batches[0].(*ach.BatchENR) + if !ok { + log.Fatalf("Batch not ENR, got %T %#v", achFile.Batches[0], achFile.Batches[0]) + } + add := batch.GetEntries()[0].Addenda05[0] + + fmt.Printf("Payment Related Information: %v \n", add.PaymentRelatedInformation) + info, err := batch.ParsePaymentInformation(add) + if err != nil { + log.Fatalf("Problem Parsing ENR Addenda05 PaymentRelatedInformation: %v", err) + } + fmt.Println(info.String()) +} diff --git a/test/ach-enr-read/main_test.go b/test/ach-enr-read/main_test.go new file mode 100644 index 000000000..45fcc779c --- /dev/null +++ b/test/ach-enr-read/main_test.go @@ -0,0 +1,7 @@ +package main + +import "testing" + +func Test(t *testing.T) { + main() +} diff --git a/test/ach-enr-write/main.go b/test/ach-enr-write/main.go new file mode 100644 index 000000000..ef0794312 --- /dev/null +++ b/test/ach-enr-write/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "log" + "os" + "time" + + "github.com/moov-io/ach" +) + +func main() { + // Example transfer to write an ACH ENR 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.ImmediateDestinationName = "Federal Reserve Bank" + fh.ImmediateOriginName = "My Bank Name" + fh.FileCreationDate = time.Now() + + // 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 = "ENR" + bh.CompanyEntryDescription = "AUTOENROLL" + 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 = 27 + 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) + + addenda05 := ach.NewAddenda05() + addenda05.PaymentRelatedInformation = `22*12200004*3*123987654321*777777777*DOE*JOHN*1\` // From NACHA 2013 Official Rules + entry.AddAddenda05(addenda05) + entry.AddendaRecordIndicator = 1 + + // build the batch + batch := ach.NewBatchENR(bh) + 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) + } + + // 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-enr-write/main_test.go b/test/ach-enr-write/main_test.go new file mode 100644 index 000000000..45fcc779c --- /dev/null +++ b/test/ach-enr-write/main_test.go @@ -0,0 +1,7 @@ +package main + +import "testing" + +func Test(t *testing.T) { + main() +}