From 3d6012eb75f07f9123062dcf16d5bcc61202e4ee Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Wed, 14 Nov 2018 13:38:21 -0500 Subject: [PATCH 1/5] Support XCK Support XCK --- batch.go | 2 + batchXCK.go | 81 ++++++ batchXCK_test.go | 489 ++++++++++++++++++++++++++++++++ test/ach-xck-read/main.go | 37 +++ test/ach-xck-read/main_test.go | 7 + test/ach-xck-read/xck-debit.ach | 10 + test/ach-xck-write/main.go | 65 +++++ test/ach-xck-write/main_test.go | 7 + 8 files changed, 698 insertions(+) create mode 100644 batchXCK.go create mode 100644 batchXCK_test.go create mode 100644 test/ach-xck-read/main.go create mode 100644 test/ach-xck-read/main_test.go create mode 100644 test/ach-xck-read/xck-debit.ach create mode 100644 test/ach-xck-write/main.go create mode 100644 test/ach-xck-write/main_test.go diff --git a/batch.go b/batch.go index a09b13b90..76ee5f69c 100644 --- a/batch.go +++ b/batch.go @@ -68,6 +68,8 @@ func NewBatch(bh *BatchHeader) (Batcher, error) { return NewBatchTRC(bh), nil case "WEB": return NewBatchWEB(bh), nil + case "XCK": + return NewBatchXCK(bh), nil default: } msg := fmt.Sprintf(msgFileNoneSEC, bh.StandardEntryClassCode) diff --git a/batchXCK.go b/batchXCK.go new file mode 100644 index 000000000..6ae44a00a --- /dev/null +++ b/batchXCK.go @@ -0,0 +1,81 @@ +// 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" + +// BatchXCK holds the BatchHeader and BatchControl and all EntryDetail for XCK Entries. +// +// Destroyed Check Entry identifies a debit entry initiated for a XCk eligible items. +type BatchXCK struct { + Batch +} + +// NewBatchXCK returns a *BatchXCK +func NewBatchXCK(bh *BatchHeader) *BatchXCK { + batch := new(BatchXCK) + batch.SetControl(NewBatchControl()) + batch.SetHeader(bh) + return batch +} + +// Validate checks valid NACHA batch rules. Assumes properly parsed records. +func (batch *BatchXCK) 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 != "XCK" { + msg := fmt.Sprintf(msgBatchSECType, batch.Header.StandardEntryClassCode, "XCK") + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "StandardEntryClassCode", Msg: msg} + } + + // XCK detail entries can only be a debit, ServiceClassCode must allow debits + switch batch.Header.ServiceClassCode { + case 200, 220, 280: + msg := fmt.Sprintf(msgBatchServiceClassCode, batch.Header.ServiceClassCode, "XCK") + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "ServiceClassCode", Msg: msg} + } + + for _, entry := range batch.Entries { + // XCK detail entries must be a debit + if entry.CreditOrDebit() != "D" { + msg := fmt.Sprintf(msgBatchTransactionCodeCredit, entry.TransactionCode) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TransactionCode", Msg: msg} + } + // Amount must be 2,500 or less + if entry.Amount > 250000 { + msg := fmt.Sprintf(msgBatchAmount, "2,500", "XCK") + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Amount", Msg: msg} + } + // ProcessControlField underlying IdentificationNumber, must be defined + if entry.ProcessControlField() == "" { + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "ProcessControlField", Msg: msgFieldRequired} + } + // ItemResearchNumber underlying IdentificationNumber, must be defined + if entry.ItemResearchNumber() == "" { + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "ItemResearchNumber", Msg: msgFieldRequired} + } + // Verify Addenda* FieldInclusion based on entry.Category and batchHeader.StandardEntryClassCode + if err := batch.addendaFieldInclusion(entry); err != nil { + return err + } + } + return nil +} + +// Create takes Batch Header and Entries and builds a valid batch +func (batch *BatchXCK) 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/batchXCK_test.go b/batchXCK_test.go new file mode 100644 index 000000000..6b1502891 --- /dev/null +++ b/batchXCK_test.go @@ -0,0 +1,489 @@ +// 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 "testing" + +// mockBatchXCKHeader creates a BatchXCK BatchHeader +func mockBatchXCKHeader() *BatchHeader { + bh := NewBatchHeader() + bh.ServiceClassCode = 225 + bh.StandardEntryClassCode = "XCK" + bh.CompanyName = "Payee Name" + bh.CompanyIdentification = "121042882" + bh.CompanyEntryDescription = "XCK" + bh.ODFIIdentification = "12104288" + return bh +} + +// mockXCKEntryDetail creates a BatchXCK EntryDetail +func mockXCKEntryDetail() *EntryDetail { + entry := NewEntryDetail() + entry.TransactionCode = 27 + entry.SetRDFI("231380104") + entry.DFIAccountNumber = "744-5678-99" + entry.Amount = 25000 + entry.SetCheckSerialNumber("123456789") + entry.SetProcessControlField("CHECK1") + entry.SetItemResearchNumber("182726") + entry.DiscretionaryData = "" + entry.SetTraceNumber(mockBatchXCKHeader().ODFIIdentification, 1) + entry.Category = CategoryForward + return entry +} + +// mockBatchXCK creates a BatchXCK +func mockBatchXCK() *BatchXCK { + mockBatch := NewBatchXCK(mockBatchXCKHeader()) + mockBatch.AddEntry(mockXCKEntryDetail()) + if err := mockBatch.Create(); err != nil { + panic(err) + } + return mockBatch +} + +// mockBatchXCKHeaderCredit creates a BatchXCK BatchHeader +func mockBatchXCKHeaderCredit() *BatchHeader { + bh := NewBatchHeader() + bh.ServiceClassCode = 225 + bh.StandardEntryClassCode = "XCK" + bh.CompanyName = "Payee Name" + bh.CompanyIdentification = "121042882" + bh.CompanyEntryDescription = "XCK" + bh.ODFIIdentification = "12104288" + return bh +} + +// mockXCKEntryDetailCredit creates a XCK EntryDetail with a credit entry +func mockXCKEntryDetailCredit() *EntryDetail { + entry := NewEntryDetail() + entry.TransactionCode = 22 + entry.SetRDFI("231380104") + entry.DFIAccountNumber = "744-5678-99" + entry.Amount = 25000 + entry.SetCheckSerialNumber("123456789") + entry.SetProcessControlField("CHECK1") + entry.SetItemResearchNumber("182726") + entry.SetTraceNumber(mockBatchXCKHeader().ODFIIdentification, 1) + entry.Category = CategoryForward + return entry +} + +// mockBatchXCKCredit creates a BatchXCK with a Credit entry +func mockBatchXCKCredit() *BatchXCK { + mockBatch := NewBatchXCK(mockBatchXCKHeaderCredit()) + mockBatch.AddEntry(mockXCKEntryDetailCredit()) + return mockBatch +} + +// testBatchXCKHeader creates a BatchXCK BatchHeader +func testBatchXCKHeader(t testing.TB) { + batch, _ := NewBatch(mockBatchXCKHeader()) + err, ok := batch.(*BatchXCK) + if !ok { + t.Errorf("Expecting BatchXCK got %T", err) + } +} + +// TestBatchXCKHeader tests validating BatchXCK BatchHeader +func TestBatchXCKHeader(t *testing.T) { + testBatchXCKHeader(t) +} + +// BenchmarkBatchXCKHeader benchmarks validating BatchXCK BatchHeader +func BenchmarkBatchXCKHeader(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchXCKHeader(b) + } +} + +// testBatchXCKCreate validates BatchXCK create +func testBatchXCKCreate(t testing.TB) { + mockBatch := mockBatchXCK() + mockBatch.Create() + if err := mockBatch.Validate(); err != nil { + t.Errorf("%T: %s", err, err) + } +} + +// TestBatchXCKCreate tests validating BatchXCK create +func TestBatchXCKCreate(t *testing.T) { + testBatchXCKCreate(t) +} + +// BenchmarkBatchXCKCreate benchmarks validating BatchXCK create +func BenchmarkBatchXCKCreate(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchXCKCreate(b) + } +} + +// testBatchXCKStandardEntryClassCode validates BatchXCK create for an invalid StandardEntryClassCode +func testBatchXCKStandardEntryClassCode(t testing.TB) { + mockBatch := mockBatchXCK() + 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) + } + } +} + +// TestBatchXCKStandardEntryClassCode tests validating BatchXCK create for an invalid StandardEntryClassCode +func TestBatchXCKStandardEntryClassCode(t *testing.T) { + testBatchXCKStandardEntryClassCode(t) +} + +// BenchmarkBatchXCKStandardEntryClassCode benchmarks validating BatchXCK create for an invalid StandardEntryClassCode +func BenchmarkBatchXCKStandardEntryClassCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchXCKStandardEntryClassCode(b) + } +} + +// testBatchXCKServiceClassCodeEquality validates service class code equality +func testBatchXCKServiceClassCodeEquality(t testing.TB) { + mockBatch := mockBatchXCK() + 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) + } + } +} + +// TestBatchXCKServiceClassCodeEquality tests validating service class code equality +func TestBatchXCKServiceClassCodeEquality(t *testing.T) { + testBatchXCKServiceClassCodeEquality(t) +} + +// BenchmarkBatchXCKServiceClassCodeEquality benchmarks validating service class code equality +func BenchmarkBatchXCKServiceClassCodeEquality(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchXCKServiceClassCodeEquality(b) + } +} + +// testBatchXCKServiceClass200 validates BatchXCK create for an invalid ServiceClassCode 200 +func testBatchXCKServiceClass200(t testing.TB) { + mockBatch := mockBatchXCK() + mockBatch.Header.ServiceClassCode = 200 + 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) + } + } +} + +// TestBatchXCKServiceClass200 tests validating BatchXCK create for an invalid ServiceClassCode 200 +func TestBatchXCKServiceClass200(t *testing.T) { + testBatchXCKServiceClass200(t) +} + +// BenchmarkBatchXCKServiceClass200 benchmarks validating BatchXCK create for an invalid ServiceClassCode 200 +func BenchmarkBatchXCKServiceClass200(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchXCKServiceClass200(b) + } +} + +// testBatchXCKServiceClass220 validates BatchXCK create for an invalid ServiceClassCode 220 +func testBatchXCKServiceClass220(t testing.TB) { + mockBatch := mockBatchXCK() + mockBatch.Header.ServiceClassCode = 220 + 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) + } + } +} + +// TestBatchXCKServiceClass220 tests validating BatchXCK create for an invalid ServiceClassCode 220 +func TestBatchXCKServiceClass220(t *testing.T) { + testBatchXCKServiceClass220(t) +} + +// BenchmarkBatchXCKServiceClass220 benchmarks validating BatchXCK create for an invalid ServiceClassCode 220 +func BenchmarkBatchXCKServiceClass220(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchXCKServiceClass220(b) + } +} + +// testBatchXCKServiceClass280 validates BatchXCK create for an invalid ServiceClassCode 280 +func testBatchXCKServiceClass280(t testing.TB) { + mockBatch := mockBatchXCK() + mockBatch.Header.ServiceClassCode = 280 + 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) + } + } +} + +// TestBatchXCKServiceClass280 tests validating BatchXCK create for an invalid ServiceClassCode 280 +func TestBatchXCKServiceClass280(t *testing.T) { + testBatchXCKServiceClass280(t) +} + +// BenchmarkBatchXCKServiceClass280 benchmarks validating BatchXCK create for an invalid ServiceClassCode 280 +func BenchmarkBatchXCKServiceClass280(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchXCKServiceClass280(b) + } +} + +// testBatchXCKCheckSerialNumber validates BatchXCK CheckSerialNumber is not mandatory +func testBatchXCKCheckSerialNumber(t testing.TB) { + mockBatch := mockBatchXCK() + // modify CheckSerialNumber / IdentificationNumber to nothing + mockBatch.GetEntries()[0].SetCheckSerialNumber("") + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "CheckSerialNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchXCKCheckSerialNumber tests validating BatchXCK +// CheckSerialNumber / IdentificationNumber is a mandatory field +func TestBatchXCKCheckSerialNumber(t *testing.T) { + testBatchXCKCheckSerialNumber(t) +} + +// BenchmarkBatchXCKCheckSerialNumber benchmarks validating BatchXCK +// CheckSerialNumber / IdentificationNumber is a mandatory field +func BenchmarkBatchXCKCheckSerialNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchXCKCheckSerialNumber(b) + } +} + +// testBatchXCKTransactionCode validates BatchXCK TransactionCode is not a credit +func testBatchXCKTransactionCode(t testing.TB) { + mockBatch := mockBatchXCKCredit() + 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) + } + } +} + +// TestBatchXCKTransactionCode tests validating BatchXCK TransactionCode is not a credit +func TestBatchXCKTransactionCode(t *testing.T) { + testBatchXCKTransactionCode(t) +} + +// BenchmarkBatchXCKTransactionCode benchmarks validating BatchXCK TransactionCode is not a credit +func BenchmarkBatchXCKTransactionCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchXCKTransactionCode(b) + } +} + +// testBatchXCKAddendaCount validates BatchXCK Addenda count +func testBatchXCKAddendaCount(t testing.TB) { + mockBatch := mockBatchXCK() + mockBatch.Entries[0].AddendaRecordIndicator = 1 + mockBatch.GetEntries()[0].AddAddenda05(mockAddenda05()) + mockBatch.Create() + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "Addenda05" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchXCKAddendaCount tests validating BatchXCK Addenda count +func TestBatchXCKAddendaCount(t *testing.T) { + testBatchXCKAddendaCount(t) +} + +// BenchmarkBatchXCKAddendaCount benchmarks validating BatchXCK Addenda count +func BenchmarkBatchXCKAddendaCount(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchXCKAddendaCount(b) + } +} + +// testBatchXCKInvalidBuild validates an invalid batch build +func testBatchXCKInvalidBuild(t testing.TB) { + mockBatch := mockBatchXCK() + 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) + } + } +} + +// TestBatchXCKInvalidBuild tests validating an invalid batch build +func TestBatchXCKInvalidBuild(t *testing.T) { + testBatchXCKInvalidBuild(t) +} + +// BenchmarkBatchXCKInvalidBuild benchmarks validating an invalid batch build +func BenchmarkBatchXCKInvalidBuild(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testBatchXCKInvalidBuild(b) + } +} + +// TestBatchXCKAddendum98 validates Addenda98 returns an error +func TestBatchXCKAddendum98(t *testing.T) { + mockBatch := NewBatchXCK(mockBatchXCKHeader()) + mockBatch.AddEntry(mockXCKEntryDetail()) + mockAddenda98 := mockAddenda98() + mockAddenda98.TypeCode = "05" + mockBatch.GetEntries()[0].Category = CategoryNOC + mockBatch.GetEntries()[0].Addenda98 = 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) + } + } +} + +// TestBatchXCKAddendum99 validates Addenda99 returns an error +func TestBatchXCKAddendum99(t *testing.T) { + mockBatch := NewBatchXCK(mockBatchXCKHeader()) + mockBatch.AddEntry(mockXCKEntryDetail()) + mockAddenda99 := mockAddenda99() + mockAddenda99.TypeCode = "05" + mockBatch.GetEntries()[0].Addenda99 = 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) + } + } +} + +// TestBatchXCKAddendum99Category validates Addenda99 returns an error +func TestBatchXCKAddendum99Category(t *testing.T) { + mockBatch := NewBatchXCK(mockBatchXCKHeader()) + mockBatch.AddEntry(mockXCKEntryDetail()) + mockAddenda99 := mockAddenda99() + mockBatch.Entries[0].AddendaRecordIndicator = 1 + mockBatch.GetEntries()[0].Category = CategoryForward + mockBatch.GetEntries()[0].Addenda99 = mockAddenda99 + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "Addenda99" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchXCKProcessControlField returns an error if ProcessControlField is not defined. +func TestBatchXCKProcessControlField(t *testing.T) { + mockBatch := NewBatchXCK(mockBatchXCKHeader()) + mockBatch.AddEntry(mockXCKEntryDetail()) + mockBatch.GetEntries()[0].SetProcessControlField("") + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "ProcessControlField" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchXCKItemResearchNumber returns an error if ItemResearchNumber is not defined. +func TestBatchXCKItemResearchNumber(t *testing.T) { + mockBatch := NewBatchXCK(mockBatchXCKHeader()) + mockBatch.AddEntry(mockXCKEntryDetail()) + mockBatch.GetEntries()[0].IndividualName = "" + mockBatch.GetEntries()[0].SetProcessControlField("CHECK1") + mockBatch.GetEntries()[0].SetItemResearchNumber("") + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "ItemResearchNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestBatchXCKAmount validates BatchXCK create for an invalid Amount +func TestBatchXCKAmount(t *testing.T) { + mockBatch := mockBatchXCK() + mockBatch.Entries[0].Amount = 260000 + 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) + } + } +} diff --git a/test/ach-xck-read/main.go b/test/ach-xck-read/main.go new file mode 100644 index 000000000..85df0f836 --- /dev/null +++ b/test/ach-xck-read/main.go @@ -0,0 +1,37 @@ +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("xck-debit.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("SEC Code: %v \n", achFile.Batches[0].GetHeader().StandardEntryClassCode) + fmt.Printf("Check Serial Number: %v \n", achFile.Batches[0].GetEntries()[0].CheckSerialNumberField()) + fmt.Printf("Process Control Field: %v \n", achFile.Batches[0].GetEntries()[0].ProcessControlField()) + fmt.Printf("Item Research Number: %v \n", achFile.Batches[0].GetEntries()[0].ItemResearchNumber()) + fmt.Printf("Discretionary Data: %v \n", achFile.Batches[0].GetEntries()[0].DiscretionaryData) +} diff --git a/test/ach-xck-read/main_test.go b/test/ach-xck-read/main_test.go new file mode 100644 index 000000000..45fcc779c --- /dev/null +++ b/test/ach-xck-read/main_test.go @@ -0,0 +1,7 @@ +package main + +import "testing" + +func Test(t *testing.T) { + main() +} diff --git a/test/ach-xck-read/xck-debit.ach b/test/ach-xck-read/xck-debit.ach new file mode 100644 index 000000000..c5c50fcfe --- /dev/null +++ b/test/ach-xck-read/xck-debit.ach @@ -0,0 +1,10 @@ +101 231380104 1210428821811140000A094101Federal Reserve Bank My Bank Name +5225Payee Name 121042882 XCKACH XCK 181115 0121042880000001 +62723138010412345678 0000250000123879654 CHECK1182726 0121042880000001 +82250000010023138010000000250000000000000000121042882 121042880000001 +9000001000001000000010023138010000000250000000000000000 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 \ No newline at end of file diff --git a/test/ach-xck-write/main.go b/test/ach-xck-write/main.go new file mode 100644 index 000000000..5b68a5868 --- /dev/null +++ b/test/ach-xck-write/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "github.com/moov-io/ach" + "log" + "os" + "time" +) + +func main() { + // Example transfer to write an ACH XCK file to debit an external institutions account + // Important: All financial institutions are different and will require registration and exact field values. + + fh := ach.NewFileHeader() + fh.ImmediateDestination = "231380104" // Routing Number of the ACH Operator or receiving point to which the file is being sent + fh.ImmediateOrigin = "121042882" // 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 = 225 // ACH credit pushes money out, 225 debits/pulls money in. + bh.CompanyName = "Payee Name" // The name of the company/person that has relationship with receiver + bh.CompanyIdentification = fh.ImmediateOrigin + bh.StandardEntryClassCode = "XCK" // Consumer destination vs Company CCD + bh.CompanyEntryDescription = "ACH XCK" // will be on receiving accounts statement + bh.EffectiveEntryDate = time.Now().AddDate(0, 0, 1) + bh.ODFIIdentification = "121042882" // 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 // Code 27: Debit (withdrawal) from checking account + entry.SetRDFI("231380104") // Receivers bank transit routing number + entry.DFIAccountNumber = "12345678" // Receivers bank account number + entry.Amount = 250000 // Amount of transaction with no decimal. One dollar and eleven cents = 111 + entry.SetCheckSerialNumber("123879654") + entry.SetProcessControlField("CHECK1") + entry.SetItemResearchNumber("182726") + entry.SetTraceNumber(bh.ODFIIdentification, 1) + + // build the batch + batch := ach.NewBatchXCK(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-xck-write/main_test.go b/test/ach-xck-write/main_test.go new file mode 100644 index 000000000..45fcc779c --- /dev/null +++ b/test/ach-xck-write/main_test.go @@ -0,0 +1,7 @@ +package main + +import "testing" + +func Test(t *testing.T) { + main() +} From 8c38478b20ccaf193f66d2b510e5fa90deffbf98 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Wed, 14 Nov 2018 21:22:23 -0500 Subject: [PATCH 2/5] Update README.md Add links for XCK --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2e374a5e5..4afbdf255 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ ACH is under active development but already in production for multiple companies | SHR | Shared Network Entry | [Link](test/ach-shr-read/main.go) | [Link](test/ach-shr-write/main.go) | | TEL | Telephone-Initiated Entry | [Link](test/ach-tel-read/main.go) | [Link](test/ach-tel-write/main.go) | | WEB | Internet-initiated Entries | [Link](test/ach-web-read/main.go) | [Link](test/ach-web-write/main.go) | +| XCK | Destroyed Check Entry | [Link](test/ach-xck-read/main.go) | [Link](test/ach-xck-write/main.go) | From f33d3d88094b7b2a489975662f6ec0aef5417716 Mon Sep 17 00:00:00 2001 From: Brooke Kline Date: Thu, 15 Nov 2018 09:36:22 -0500 Subject: [PATCH 3/5] Delete main_test.go --- test/ach-trc-write/main_test.go | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 test/ach-trc-write/main_test.go diff --git a/test/ach-trc-write/main_test.go b/test/ach-trc-write/main_test.go deleted file mode 100644 index 45fcc779c..000000000 --- a/test/ach-trc-write/main_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "testing" - -func Test(t *testing.T) { - main() -} From 8b036f10770d7cc049018ed4f6ccc0524de51e77 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 15 Nov 2018 09:52:30 -0500 Subject: [PATCH 4/5] Update CHANGELOG.md Update CHANGELOG for v0.4.1 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e8020b37..d4d1add06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## v0.4.1 (Unreleased) +ADDITIONS + +- Support ADV, TRC, TRX, XCK StandardEntryClassCode (SEC types) + ## v0.4.0 (Released 2018-11-06) BREAKING CHANGES From fee86f4dde3255f1bce4a7dac0486257764c56d1 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 15 Nov 2018 09:55:13 -0500 Subject: [PATCH 5/5] Re-Add main_test.go Re-Add main_test.go --- test/ach-trc-write/main_test.go | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 test/ach-trc-write/main_test.go diff --git a/test/ach-trc-write/main_test.go b/test/ach-trc-write/main_test.go new file mode 100644 index 000000000..45fcc779c --- /dev/null +++ b/test/ach-trc-write/main_test.go @@ -0,0 +1,7 @@ +package main + +import "testing" + +func Test(t *testing.T) { + main() +}