Repository: moov-io/ach
Pull request #133 (2017-08-30 04:31:00)

Support for Batch SEC type CCD

coveralls: 
[![Coverage Status](https://coveralls.io/builds/13046089/badge)](https://coveralls.io/builds/13046089)

Coverage decreased (-0.9%) to 94.513% when pulling **e28abd9a16c16cf3bacb5666e77b7b647c9c94d2 on Support-for-Batch-SEC-type-CCD** into **04d8e77f617959f44de2bd6c0e4e2005dbfe81cf on master**.


Patch:
diff --git a/README.md b/README.md
index be79be32..8342aef0 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ ACH is at an early stage and under active development. Please star the project i
 * Library currently supports the reading and writing 
 	* PPD (Prearranged payment and deposits)
 	* WEB (Internet-initiated Entries )
+	* CCD (Corporate credit or debit)
 
 
 ## Project Roadmap
diff --git a/batch.go b/batch.go
index deb3cbd2..2365585d 100644
--- a/batch.go
+++ b/batch.go
@@ -18,6 +18,8 @@ func NewBatch(bp BatchParam) (Batcher, error) {
 		return NewBatchPPD(bp), nil
 	case "WEB":
 		return NewBatchWEB(bp), nil
+	case "CCD":
+		return NewBatchCCD(bp), nil
 	default:
 		msg := fmt.Sprintf(msgFileNoneSEC, sec)
 		return nil, &FileError{FieldName: "StandardEntryClassCode", Msg: msg}
diff --git a/batchCCD.go b/batchCCD.go
new file mode 100644
index 00000000..26a4f2e9
--- /dev/null
+++ b/batchCCD.go
@@ -0,0 +1,66 @@
+package ach
+
+import (
+	"fmt"
+)
+
+// BatchCCD creates a batch file that handles SEC payment type CCD amd 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 {
+	batch
+}
+
+// NewBatchCCD returns a *BatchCCD
+func NewBatchCCD(params ...BatchParam) *BatchCCD {
+	batch := new(BatchCCD)
+	batch.SetControl(NewBatchControl())
+
+	if len(params) > 0 {
+		bh := NewBatchHeader(params[0])
+		bh.StandardEntryClassCode = "CCD"
+		batch.SetHeader(bh)
+		return batch
+	}
+	bh := NewBatchHeader()
+	bh.StandardEntryClassCode = "CCD"
+	batch.SetHeader(bh)
+	return batch
+}
+
+// Validate ensures the batch meets NACHA rules specific to this batch type.
+func (batch *BatchCCD) Validate() error {
+	// basic verification of the batch before we validate specific rules.
+	if err := batch.verify(); err != nil {
+		return err
+	}
+	// Add configuration based validation for this type.
+	// Web can have up to one addenda per entry record
+	if err := batch.isAddendaCount(1); err != nil {
+		return err
+	}
+	if err := batch.isTypeCode("05"); err != nil {
+		return err
+	}
+
+	// Add type specific validation.
+	if batch.header.StandardEntryClassCode != "CCD" {
+		msg := fmt.Sprintf(msgBatchSECType, batch.header.StandardEntryClassCode, "CCD")
+		return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "StandardEntryClassCode", Msg: msg}
+	}
+
+	return nil
+}
+
+// Create builds the batch sequence numbers and batch control. Additional creation
+func (batch *BatchCCD) Create() error {
+	// generates sequence numbers and batch control
+	if err := batch.build(); err != nil {
+		return err
+	}
+
+	if err := batch.Validate(); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/batcher.go b/batcher.go
index e77e5a71..da004263 100644
--- a/batcher.go
+++ b/batcher.go
@@ -64,6 +64,7 @@ var (
 	msgBatchHeaderControlEquality     = "header %v is not equal to control %v"
 	msgBatchCalculatedControlEquality = "calculated %v is out-of-balance with control %v"
 	msgBatchAscending                 = "%v is less than last %v. Must be in ascending order"
+	msgBatchFieldInclusion            = "%v is a required field "
 	// specific messages for error
 	msgBatchOriginatorDNE         = "%v is not “2” for DNE with entry transaction code of 23 or 33"
 	msgBatchTraceNumberNotODFI    = "%v in header does not match entry trace number %v"
diff --git a/entryDetail.go b/entryDetail.go
index 3fe9891c..d62ff10f 100644
--- a/entryDetail.go
+++ b/entryDetail.go
@@ -41,9 +41,9 @@ type EntryDetail struct {
 	// Amount Number of cents you are debiting/crediting this account
 	Amount int
 
-	// IndividualIdentificationNumber n internal identification (alphanumeric) that
+	// IdentificationNumber n internal identification (alphanumeric) that
 	// you use to uniquely identify this Entry Detail Record
-	IndividualIdentificationNumber string
+	IdentificationNumber string
 
 	// IndividualName The name of the receiver, usually the name on the bank account
 	IndividualName string
@@ -87,7 +87,8 @@ type EntryParam struct {
 	RDFIAccount       string `json:"rdfi_account"`
 	Amount            string `json:"amount"`
 	IDNumber          string `json:"id_number,omitempty"`
-	IndividualName    string `json:"individual_name"`
+	IndividualName    string `json:"individual_name,omitempty"`
+	ReceivingCompany  string `json:"receiving_company,omitempty"`
 	DiscretionaryData string `json:"discretionary_data,omitempty"`
 	TransactionCode   string `json:"transaction_code"`
 }
@@ -101,8 +102,12 @@ func NewEntryDetail(params ...EntryParam) *EntryDetail {
 		entry.SetRDFI(entry.parseNumField(params[0].ReceivingDFI))
 		entry.DFIAccountNumber = params[0].RDFIAccount
 		entry.Amount = entry.parseNumField(params[0].Amount)
-		entry.IndividualIdentificationNumber = params[0].IDNumber
-		entry.IndividualName = params[0].IndividualName
+		entry.IdentificationNumber = params[0].IDNumber
+		if params[0].IndividualName != "" {
+			entry.IndividualName = params[0].IndividualName
+		} else {
+			entry.IndividualName = params[0].ReceivingCompany
+		}
 		entry.DiscretionaryData = params[0].DiscretionaryData
 		entry.TransactionCode = entry.parseNumField(params[0].TransactionCode)
 
@@ -127,7 +132,7 @@ func (ed *EntryDetail) Parse(record string) {
 	// 30-39 Number of cents you are debiting/crediting this account
 	ed.Amount = ed.parseNumField(record[29:39])
 	// 40-54 An internal identification (alphanumeric) that you use to uniquely identify this Entry Detail Record
-	ed.IndividualIdentificationNumber = record[39:54]
+	ed.IdentificationNumber = record[39:54]
 	// 55-76 The name of the receiver, usually the name on the bank account
 	ed.IndividualName = record[54:76]
 	// 77-78 allows ODFIs to include codes of significance only to them
@@ -149,7 +154,7 @@ func (ed *EntryDetail) String() string {
 		ed.CheckDigit,
 		ed.DFIAccountNumberField(),
 		ed.AmountField(),
-		ed.IndividualIdentificationNumberField(),
+		ed.IdentificationNumberField(),
 		ed.IndividualNameField(),
 		ed.DiscretionaryDataField(),
 		ed.AddendaRecordIndicator,
@@ -172,8 +177,8 @@ func (ed *EntryDetail) Validate() error {
 	if err := ed.isAlphanumeric(ed.DFIAccountNumber); err != nil {
 		return &FieldError{FieldName: "DFIAccountNumber", Value: ed.DFIAccountNumber, Msg: err.Error()}
 	}
-	if err := ed.isAlphanumeric(ed.IndividualIdentificationNumber); err != nil {
-		return &FieldError{FieldName: "IndividualIdentificationNumber", Value: ed.IndividualIdentificationNumber, Msg: err.Error()}
+	if err := ed.isAlphanumeric(ed.IdentificationNumber); err != nil {
+		return &FieldError{FieldName: "IdentificationNumber", Value: ed.IdentificationNumber, Msg: err.Error()}
 	}
 	if err := ed.isAlphanumeric(ed.IndividualName); err != nil {
 		return &FieldError{FieldName: "IndividualName", Value: ed.IndividualName, Msg: err.Error()}
@@ -253,9 +258,9 @@ func (ed *EntryDetail) AmountField() string {
 	return ed.numericField(ed.Amount, 10)
 }
 
-// IndividualIdentificationNumberField returns a space padded string of IndividualIdentificationNumber
-func (ed *EntryDetail) IndividualIdentificationNumberField() string {
-	return ed.alphaField(ed.IndividualIdentificationNumber, 15)
+// IdentificationNumberField returns a space padded string of IdentificationNumber
+func (ed *EntryDetail) IdentificationNumberField() string {
+	return ed.alphaField(ed.IdentificationNumber, 15)
 }
 
 // IndividualNameField returns a space padded string of IndividualName
@@ -263,6 +268,11 @@ func (ed *EntryDetail) IndividualNameField() string {
 	return ed.alphaField(ed.IndividualName, 22)
 }
 
+// ReceivingCompanyField is used in CCD files but returns the underlying IndividualName field
+func (ed *EntryDetail) ReceivingCompanyField() string {
+	return ed.IndividualNameField()
+}
+
 // DiscretionaryDataField returns a space padded string of DiscretionaryData
 func (ed *EntryDetail) DiscretionaryDataField() string {
 	return ed.alphaField(ed.DiscretionaryData, 2)


Test patch:
diff --git a/batchCCD_test.go b/batchCCD_test.go
new file mode 100644
index 00000000..f1608b87
--- /dev/null
+++ b/batchCCD_test.go
@@ -0,0 +1,100 @@
+package ach
+
+import "testing"
+
+func mockBatchCCDHeader() *BatchHeader {
+	bh := NewBatchHeader()
+	bh.ServiceClassCode = 220
+	bh.StandardEntryClassCode = "CCD"
+	bh.CompanyName = "Your Company, inc"
+	bh.CompanyIdentification = "123456789"
+	bh.CompanyEntryDescription = "Vndr Pay"
+	bh.ODFIIdentification = 6200001
+	return bh
+}
+
+func mockCCDEntryDetail() *EntryDetail {
+	entry := NewEntryDetail()
+	entry.TransactionCode = 27
+	entry.SetRDFI(9101298)
+	entry.DFIAccountNumber = "744-5678-99"
+	entry.Amount = 5000000
+	entry.IdentificationNumber = "location #23"
+	entry.IndividualName = "Best Co. #23"
+	entry.TraceNumber = 123456789
+	entry.DiscretionaryData = "S"
+	return entry
+}
+
+func mockBatchCCD() *BatchCCD {
+	mockBatch := NewBatchCCD()
+	mockBatch.SetHeader(mockBatchCCDHeader())
+	mockBatch.AddEntry(mockCCDEntryDetail())
+	mockBatch.GetEntries()[0].AddAddenda(mockAddenda())
+	if err := mockBatch.Create(); err != nil {
+		panic(err)
+	}
+	return mockBatch
+}
+
+// A Batch CCD can only have one addendum per entry detail
+func TestBatchCCDAddendumCount(t *testing.T) {
+	mockBatch := mockBatchCCD()
+	// Adding a second addenda to the mock entry
+	mockBatch.GetEntries()[0].AddAddenda(mockAddenda())
+	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)
+		}
+	}
+}
+
+// receiving company / Individual name is a mandatory field
+func TestBatchCCDReceivingCompanyName(t *testing.T) {
+	mockBatch := mockBatchCCD()
+	// modify the Individual name / receiving company to nothing
+	mockBatch.GetEntries()[0].IndividualName = ""
+	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)
+		}
+	}
+}
+
+// verify addenda type code is 05
+func TestBatchCCDAddendaTypeCode(t *testing.T) {
+	mockBatch := mockBatchCCD()
+	mockBatch.GetEntries()[0].Addendum[0].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)
+		}
+	}
+}
+
+// verify that the standard entry class code is CCD for batchCCD
+func TestBatchCCDSEC(t *testing.T) {
+	mockBatch := mockBatchCCD()
+	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)
+		}
+	}
+}
diff --git a/batchPPD_test.go b/batchPPD_test.go
index a6bc9004..f456be26 100644
--- a/batchPPD_test.go
+++ b/batchPPD_test.go
@@ -297,11 +297,11 @@ func TestBatchBuild(t *testing.T) {
 	mockBatch.SetHeader(header)
 
 	entry := NewEntryDetail()
-	entry.TransactionCode = 22                            // ACH Credit
-	entry.SetRDFI(81086674)                               // scottrade bank routing number
-	entry.DFIAccountNumber = "62292250"                   // scottrade account number
-	entry.Amount = 1000000                                // 1k dollars
-	entry.IndividualIdentificationNumber = "658-888-2468" // Unique ID for payment
+	entry.TransactionCode = 22                  // ACH Credit
+	entry.SetRDFI(81086674)                     // scottrade bank routing number
+	entry.DFIAccountNumber = "62292250"         // scottrade account number
+	entry.Amount = 1000000                      // 1k dollars
+	entry.IdentificationNumber = "658-888-2468" // Unique ID for payment
 	entry.IndividualName = "Wade Arnold"
 	entry.setTraceNumber(header.ODFIIdentification, 1)
 	a1 := NewAddenda()
diff --git a/batchWEB_test.go b/batchWEB_test.go
index 4f9cebaf..074ddb17 100644
--- a/batchWEB_test.go
+++ b/batchWEB_test.go
@@ -86,7 +86,7 @@ func TestBatchWebIndividualNameRequired(t *testing.T) {
 }
 
 // verify addenda type code is 05
-func TestBatchAddendaTypeCode(t *testing.T) {
+func TestBatchWEBAddendaTypeCode(t *testing.T) {
 	mockBatch := mockBatchWEB()
 	mockBatch.GetEntries()[0].Addendum[0].TypeCode = "07"
 	if err := mockBatch.Validate(); err != nil {
diff --git a/entryDetail_internal_test.go b/entryDetail_internal_test.go
index 48ff82b2..8789efaf 100644
--- a/entryDetail_internal_test.go
+++ b/entryDetail_internal_test.go
@@ -73,8 +73,8 @@ func TestParseEntryDetail(t *testing.T) {
 		t.Errorf("Amount Expected '0000010500' got: %v", record.AmountField())
 	}
 
-	if record.IndividualIdentificationNumber != "c-1            " {
-		t.Errorf("IndividualIdentificationNumber Expected 'c-1            ' got: %v", record.IndividualIdentificationNumber)
+	if record.IdentificationNumber != "c-1            " {
+		t.Errorf("IdentificationNumber Expected 'c-1            ' got: %v", record.IdentificationNumber)
 	}
 	if record.IndividualName != "Arnold Wade           " {
 		t.Errorf("IndividualName Expected 'Arnold Wade           ' got: %v", record.IndividualName)
@@ -157,12 +157,12 @@ func TestEDdfiAccountNumberAlphaNumeric(t *testing.T) {
 	}
 }
 
-func TestEDIndividualIdentificationNumberAlphaNumeric(t *testing.T) {
+func TestEDIdentificationNumberAlphaNumeric(t *testing.T) {
 	ed := mockEntryDetail()
-	ed.IndividualIdentificationNumber = "®"
+	ed.IdentificationNumber = "®"
 	if err := ed.Validate(); err != nil {
 		if e, ok := err.(*FieldError); ok {
-			if e.FieldName != "IndividualIdentificationNumber" {
+			if e.FieldName != "IdentificationNumber" {
 				t.Errorf("%T: %s", err, err)
 			}
 		}