Repository: moov-io/ach
Pull request #129 (2017-08-23 04:17:42)

Composable batch
Changes to make adding additional payment types via SEC codes easier to support. 

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

Coverage decreased (-1.9%) to 94.875% when pulling **218d63af23900942f13e5a7e2073644d043a3397 on wadearnold:composable-batch** into **2874f4bf3d9f6b8c6b943f52ea2aeac9796ad9fc on moov-io:master**.

wadearnold: Adds support for a composable batch struct into SEC specific batch types #105 

Patch:
diff --git a/README.md b/README.md
index 9c262de7..8caf62a1 100644
--- a/README.md
+++ b/README.md
@@ -37,8 +37,10 @@ Examples exist in projects [example](https://github.com/moov-io/ach/tree/master/
 
 To create a batch
 
+Errors only if payment type is not supported
+
  ```go
-	batch := ach.NewBatchPPD(ach.BatchParam{
+	batch := ach.NewBatch(ach.BatchParam{
 		ServiceClassCode:        "220",
 		CompanyName:             "Your Company",
 		StandardEntryClass:      "PPD",
@@ -75,10 +77,11 @@ Entries are added to batches like so:
 	batch.AddEntry(entry)
 ```
 
-When all of the Entries are added to the batch we can build the batch.
+When all of the Entries are added to the batch we can create the batch.
 
  ```go
-	if err := batch.Build(); err != nil {
+	if err := batch.Create(); err != nil {
+
 		fmt.Printf("%T: %s", err, err)
 	}
   ```
@@ -92,7 +95,7 @@ And batches are added to files much the same way:
 Once we added all our batches we must build the file
 
  ```go
-	if err := file.Build(); err != nil {
+	if err := file.Create(); err != nil {
 		fmt.Printf("%T: %s", err, err)
 	}
 ```
@@ -141,6 +144,92 @@ We use GitHub to manage reviews of pull requests.
   Practices for Production
   Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style).
 
+# Additional SEC (Standard Entry Class) code batch types. 
+SEC type's in the Batch Header record define the payment type of the following Entry Details and Addenda. The format of the records in the batch is the same between all payment types but NACHA defines different rules for the values that are held in each record field. To add support for an additional SEC type you will need to implement NACHA rules for that type. The vast majority of rules are implemented in ach.batch and then composed into Batch(SEC) for reuse. All Batch(SEC) types must be a ach.Batcher.
+
+1. Create a milestone for the new SEC type that you want supported. 
+2. Add issues to that milestone to meet the NACHA rules for the batch type. 
+3. Create a new struct of the batch type. In the following example we will use MTE(Machine Transfer Entry) as our example. 
+4. The following code would be place in a new file batchMTE.go next to the existing batch types.
+5. The code is stub code and the MTE type is not implemented. For concrete examples review the existing batch types in the source.  
+
+Create a new struct and compose ach.batch 
+
+```go
+type BatchMTE struct {
+	batch
+}
+```
+Add the ability for the new type to be created. 
+
+```go
+func NewBatchMTE(params ...BatchParam) *BatchMTE {
+	batch := new(BatchMTE)
+	batch.setControl(NewBatchControl)
+
+	if len(params) > 0 {
+		bh := NewBatchHeader(params[0])
+		bh.StandardEntryClassCode = "MTE"
+		batch.SetHeader(bh)
+		return batch
+	}
+	bh := NewBatchHeader()
+	bh.StandardEntryClassCode = "MTE"
+	batch.SetHeader(bh)
+	return batch
+}
+```
+
+To support the Batcher interface you must add the following functions that are not implemented in ach.batch. 
+* Validate() error
+* Create() error 
+
+Validate is designed to enforce the NACHA rules for the MTE payment type. Validate is run after a batch of this type is read from a file. If you are creating a batch from code call validate afterwards. 
+
+```go
+// Validate checks valid NACHA batch rules. Assumes properly parsed records.
+func (batch *BatchPPD) 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.
+	// ... batch.isAddendaCount(1)
+	// Add type specific validation.
+	// ...
+	return nil
+}
+```
+Create takes the Batch Header and Entry details and creates the proper sequence number and batch control. If additional logic specific to the SEC type is required it building a batch file it should be added here. 
+
+```go
+// Create takes Batch Header and Entries and builds a valid batch
+func (batch *BatchPPD) Create() error {
+	// generates sequence numbers and batch control
+	if err := batch.build(); err != nil {
+		return err
+	}
+	// Additional steps specific to batch type
+	// ...
+
+	if err := batch.Validate(); err != nil {
+		return err
+	}
+	return nil
+}
+```
+
+Finally add the batch type to the NewBatch factory in batch.go. 
+
+```go
+//...
+case "MTE":
+		return NewBatchPPD(bp), nil
+//...
+```
+
+Pull request require a batchMTE_test.go file that covers the logic of the type. 
+
 ## References  
 * [Wikipeda: Automated Clearing House](http://en.wikipedia.org/wiki/Automated_Clearing_House)
 * [Nacha ACH Network: How it Works](https://www.nacha.org/ach-network)
diff --git a/batch.go b/batch.go
new file mode 100644
index 00000000..63ddac6f
--- /dev/null
+++ b/batch.go
@@ -0,0 +1,288 @@
+package ach
+
+import "fmt"
+
+// Batch holds the Batch Header and Batch Control and all Entry Records for PPD Entries
+type batch struct {
+	header  *BatchHeader
+	entries []*EntryDetail
+	control *BatchControl
+	// Converters is composed for ACH to GoLang Converters
+	converters
+}
+
+// NewBatch takes a BatchParm and returns a matching SEC code batch type that is a batcher. Returns and error if the SEC code is not supported.
+func NewBatch(bp BatchParam) (Batcher, error) {
+	switch sec := bp.StandardEntryClass; sec {
+	case "PPD":
+		return NewBatchPPD(bp), nil
+	case "WEB":
+		return NewBatchWEB(bp), nil
+	default:
+		msg := fmt.Sprintf(msgFileNoneSEC, sec)
+		return nil, &FileError{FieldName: "StandardEntryClassCode", Msg: msg}
+	}
+}
+
+// verify checks basic valid NACHA batch rules. Assumes properly parsed records. This does not mean it is a valid batch as validity is tied to each batch type
+func (batch *batch) verify() error {
+	batchNumber := batch.header.BatchNumber
+	// validate batch header and control codes are the same
+	if batch.header.ServiceClassCode != batch.control.ServiceClassCode {
+		msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.header.ServiceClassCode, batch.control.ServiceClassCode)
+		return &BatchError{BatchNumber: batchNumber, FieldName: "ServiceClassCode", Msg: msg}
+	}
+	// Company Identification must match the Company ID from the batch header record
+	if batch.header.CompanyIdentification != batch.control.CompanyIdentification {
+		msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.header.CompanyIdentification, batch.control.CompanyIdentification)
+		return &BatchError{BatchNumber: batchNumber, FieldName: "CompanyIdentification", Msg: msg}
+	}
+	// Control ODFI Identification must be the same as batch header
+	if batch.header.ODFIIdentification != batch.control.ODFIIdentification {
+		msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.header.ODFIIdentification, batch.control.ODFIIdentification)
+		return &BatchError{BatchNumber: batchNumber, FieldName: "ODFIIdentification", Msg: msg}
+	}
+	// batch number header and control must match
+	if batch.header.BatchNumber != batch.control.BatchNumber {
+		msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.header.ODFIIdentification, batch.control.ODFIIdentification)
+		return &BatchError{BatchNumber: batchNumber, FieldName: "BatchNumber", Msg: msg}
+	}
+
+	if err := batch.isBatchEntryCount(); err != nil {
+		return err
+	}
+
+	if err := batch.isSequenceAscending(); err != nil {
+		return err
+	}
+
+	if err := batch.isBatchAmount(); err != nil {
+		return err
+	}
+
+	if err := batch.isEntryHash(); err != nil {
+		return err
+	}
+
+	if err := batch.isOriginatorDNE(); err != nil {
+		return err
+	}
+
+	if err := batch.isTraceNumberODFI(); err != nil {
+		return err
+	}
+
+	if err := batch.isAddendaSequence(); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Build creates valid batch by building sequence numbers and batch batch control. An error is returned if
+// the batch being built has invalid records.
+func (batch *batch) build() error {
+	// Requires a valid BatchHeader
+	if err := batch.header.Validate(); err != nil {
+		return err
+	}
+	if len(batch.entries) <= 0 {
+		return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "entries", Msg: msgBatchEntries}
+	}
+	// Create record sequence numbers
+	entryCount := 0
+	seq := 1
+	for i, entry := range batch.entries {
+		entryCount = entryCount + 1 + len(entry.Addendum)
+		batch.entries[i].setTraceNumber(batch.header.ODFIIdentification, seq)
+		seq++
+		addendaSeq := 1
+		for x := range entry.Addendum {
+			batch.entries[i].Addendum[x].SequenceNumber = addendaSeq
+			batch.entries[i].Addendum[x].EntryDetailSequenceNumber = batch.parseNumField(batch.entries[i].TraceNumberField()[8:])
+			addendaSeq++
+		}
+	}
+
+	// build a BatchControl record
+	bc := NewBatchControl()
+	bc.ServiceClassCode = batch.header.ServiceClassCode
+	bc.CompanyIdentification = batch.header.CompanyIdentification
+	bc.ODFIIdentification = batch.header.ODFIIdentification
+	bc.BatchNumber = batch.header.BatchNumber
+	bc.EntryAddendaCount = entryCount
+	bc.EntryHash = batch.parseNumField(batch.calculateEntryHash())
+	bc.TotalCreditEntryDollarAmount, bc.TotalDebitEntryDollarAmount = batch.calculateBatchAmounts()
+	batch.control = bc
+
+	return nil
+}
+
+// SetHeader appends an BatchHeader to the Batch
+func (batch *batch) SetHeader(batchHeader *BatchHeader) {
+	batch.header = batchHeader
+}
+
+// GetHeader returns the current Batch header
+func (batch *batch) GetHeader() *BatchHeader {
+	return batch.header
+}
+
+// SetControl appends an BatchControl to the Batch
+func (batch *batch) SetControl(batchControl *BatchControl) {
+	batch.control = batchControl
+}
+
+// GetControl returns the current Batch Control
+func (batch *batch) GetControl() *BatchControl {
+	return batch.control
+}
+
+// GetEntries returns a slice of entry details for the batch
+func (batch *batch) GetEntries() []*EntryDetail {
+	return batch.entries
+}
+
+// AddEntry appends an EntryDetail to the Batch
+func (batch *batch) AddEntry(entry *EntryDetail) {
+	batch.entries = append(batch.entries, entry)
+}
+
+// isBatchEntryCount validate Entry count is accurate
+// The Entry/Addenda Count Field is a tally of each Entry Detail and Addenda
+// Record processed within the batch
+func (batch *batch) isBatchEntryCount() error {
+	entryCount := 0
+	for _, entry := range batch.entries {
+		entryCount = entryCount + 1 + len(entry.Addendum)
+	}
+	if entryCount != batch.control.EntryAddendaCount {
+		msg := fmt.Sprintf(msgBatchCalculatedControlEquality, entryCount, batch.control.EntryAddendaCount)
+		return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "EntryAddendaCount", Msg: msg}
+	}
+	return nil
+}
+
+// isBatchAmount validate Amount is the same as what is in the Entries
+// The Total Debit and Credit Entry Dollar Amount fields contain accumulated
+// Entry Detail debit and credit totals within a given batch
+func (batch *batch) isBatchAmount() error {
+	credit, debit := batch.calculateBatchAmounts()
+	if debit != batch.control.TotalDebitEntryDollarAmount {
+		msg := fmt.Sprintf(msgBatchCalculatedControlEquality, debit, batch.control.TotalDebitEntryDollarAmount)
+		return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "TotalDebitEntryDollarAmount", Msg: msg}
+	}
+
+	if credit != batch.control.TotalCreditEntryDollarAmount {
+		msg := fmt.Sprintf(msgBatchCalculatedControlEquality, credit, batch.control.TotalCreditEntryDollarAmount)
+		return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "TotalCreditEntryDollarAmount", Msg: msg}
+	}
+	return nil
+}
+
+func (batch *batch) calculateBatchAmounts() (credit int, debit int) {
+	for _, entry := range batch.entries {
+		if entry.TransactionCode == 22 || entry.TransactionCode == 23 {
+			credit = credit + entry.Amount
+		}
+		if entry.TransactionCode == 27 || entry.TransactionCode == 28 {
+			debit = debit + entry.Amount
+		}
+		// savings credit
+		if entry.TransactionCode == 32 || entry.TransactionCode == 33 {
+			credit = credit + entry.Amount
+		}
+		// savings debit
+		if entry.TransactionCode == 37 || entry.TransactionCode == 38 {
+			debit = debit + entry.Amount
+		}
+	}
+	return credit, debit
+}
+
+// isSequenceAscending Individual Entry Detail Records within individual batches must
+// be in ascending Trace Number order (although Trace Numbers need not necessarily be consecutive).
+func (batch *batch) isSequenceAscending() error {
+	lastSeq := -1
+	for _, entry := range batch.entries {
+		if entry.TraceNumber <= lastSeq {
+			msg := fmt.Sprintf(msgBatchAscending, entry.TraceNumber, lastSeq)
+			return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "TraceNumber", Msg: msg}
+		}
+		lastSeq = entry.TraceNumber
+	}
+	return nil
+}
+
+// isEntryHash validates the hash by recalculating the result
+func (batch *batch) isEntryHash() error {
+	hashField := batch.calculateEntryHash()
+	if hashField != batch.control.EntryHashField() {
+		msg := fmt.Sprintf(msgBatchCalculatedControlEquality, hashField, batch.control.EntryHashField())
+		return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "EntryHash", Msg: msg}
+	}
+	return nil
+}
+
+// calculateEntryHash This field is prepared by hashing the 8-digit Routing Number in each entry.
+// The Entry Hash provides a check against inadvertent alteration of data
+func (batch *batch) calculateEntryHash() string {
+	hash := 0
+	for _, entry := range batch.entries {
+		hash = hash + entry.RDFIIdentification
+	}
+	return batch.numericField(hash, 10)
+}
+
+// The Originator Status Code is not equal to “2” for DNE if the Transaction Code is 23 or 33
+func (batch *batch) isOriginatorDNE() error {
+	if batch.header.OriginatorStatusCode != 2 {
+		for _, entry := range batch.entries {
+			if entry.TransactionCode == 23 || entry.TransactionCode == 33 {
+				msg := fmt.Sprintf(msgBatchOriginatorDNE, batch.header.OriginatorStatusCode)
+				return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "OriginatorStatusCode", Msg: msg}
+			}
+		}
+	}
+	return nil
+}
+
+// isTraceNumberODFI checks if the first 8 positions of the entry detail trace number
+// match the batch header odfi
+func (batch *batch) isTraceNumberODFI() error {
+	for _, entry := range batch.entries {
+		if batch.header.ODFIIdentificationField() != entry.TraceNumberField()[:8] {
+			msg := fmt.Sprintf(msgBatchTraceNumberNotODFI, batch.header.ODFIIdentificationField(), entry.TraceNumberField()[:8])
+			return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "ODFIIdentificationField", Msg: msg}
+		}
+	}
+
+	return nil
+}
+
+// isAddendaSequence check multiple errors on addenda records in the batch entries
+func (batch *batch) isAddendaSequence() error {
+	for _, entry := range batch.entries {
+		if len(entry.Addendum) > 0 {
+			// addenda without indicator flag of 1
+			if entry.AddendaRecordIndicator != 1 {
+				return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "AddendaRecordIndicator", Msg: msgBatchAddendaIndicator}
+			}
+			lastSeq := -1
+			// check if sequence is assending
+			for _, addenda := range entry.Addendum {
+				if addenda.SequenceNumber < lastSeq {
+					msg := fmt.Sprintf(msgBatchAscending, addenda.SequenceNumber, lastSeq)
+					return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "SequenceNumber", Msg: msg}
+				}
+				lastSeq = addenda.SequenceNumber
+				// check that we are in the correct Entry Detail
+				if !(addenda.EntryDetailSequenceNumberField() == entry.TraceNumberField()[8:]) {
+					msg := fmt.Sprintf(msgBatchAddendaTraceNumber, addenda.EntryDetailSequenceNumberField(), entry.TraceNumberField()[8:])
+					return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "TraceNumber", Msg: msg}
+				}
+			}
+		}
+	}
+	return nil
+}
diff --git a/batchHeader.go b/batchHeader.go
index c70382d1..8feff74b 100644
--- a/batchHeader.go
+++ b/batchHeader.go
@@ -230,9 +230,6 @@ func (bh *BatchHeader) fieldInclusion() error {
 	if bh.ODFIIdentification == 0 {
 		return &FieldError{FieldName: "ODFIIdentification", Value: bh.ODFIIdentificationField(), Msg: msgFieldInclusion}
 	}
-	if bh.BatchNumber == 0 {
-		return &FieldError{FieldName: "BatchNumber", Value: strconv.Itoa(bh.BatchNumber), Msg: msgFieldInclusion}
-	}
 
 	return nil
 }
diff --git a/batchPPD.go b/batchPPD.go
index af7570dd..00db187d 100644
--- a/batchPPD.go
+++ b/batchPPD.go
@@ -4,17 +4,9 @@
 
 package ach
 
-import (
-	"fmt"
-)
-
 // BatchPPD holds the Batch Header and Batch Control and all Entry Records for PPD Entries
 type BatchPPD struct {
-	header  *BatchHeader
-	entries []*EntryDetail
-	control *BatchControl
-	// Converters is composed for ACH to golang Converters
-	converters
+	batch
 }
 
 // NewBatchPPD returns a *BatchPPD
@@ -24,7 +16,6 @@ func NewBatchPPD(params ...BatchParam) *BatchPPD {
 
 	if len(params) > 0 {
 		bh := NewBatchHeader(params[0])
-		// override user SEC code for BatchPPD
 		bh.StandardEntryClassCode = ppd
 		batch.SetHeader(bh)
 		return batch
@@ -37,294 +28,28 @@ func NewBatchPPD(params ...BatchParam) *BatchPPD {
 
 // Validate checks valid NACHA batch rules. Assumes properly parsed records.
 func (batch *BatchPPD) Validate() error {
-	batchNumber := batch.header.BatchNumber
-	// validate batch header and control codes are the same
-	if batch.header.ServiceClassCode != batch.control.ServiceClassCode {
-		msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.header.ServiceClassCode, batch.control.ServiceClassCode)
-		return &BatchError{BatchNumber: batchNumber, FieldName: "ServiceClassCode", Msg: msg}
-	}
-	// Company Identification must match the Company ID from the batch header record
-	if batch.header.CompanyIdentification != batch.control.CompanyIdentification {
-		msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.header.CompanyIdentification, batch.control.CompanyIdentification)
-		return &BatchError{BatchNumber: batchNumber, FieldName: "CompanyIdentification", Msg: msg}
-	}
-	// Control ODFI Identification must be the same as batch header
-	if batch.header.ODFIIdentification != batch.control.ODFIIdentification {
-		msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.header.ODFIIdentification, batch.control.ODFIIdentification)
-		return &BatchError{BatchNumber: batchNumber, FieldName: "ODFIIdentification", Msg: msg}
-	}
-	// batch number header and control must match
-	if batch.header.BatchNumber != batch.control.BatchNumber {
-		msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.header.ODFIIdentification, batch.control.ODFIIdentification)
-		return &BatchError{BatchNumber: batchNumber, FieldName: "BatchNumber", Msg: msg}
-	}
-
-	if err := batch.isBatchEntryCount(); err != nil {
-		return err
-	}
-
-	if err := batch.isSequenceAscending(); err != nil {
-		return err
-	}
-
-	if err := batch.isBatchAmount(); err != nil {
+	// basic verification of the batch before we validate specific rules.
+	if err := batch.verify(); err != nil {
 		return err
 	}
-
-	if err := batch.isEntryHash(); err != nil {
-		return err
-	}
-
-	if err := batch.isOriginatorDNE(); err != nil {
-		return err
-	}
-
-	if err := batch.isTraceNumberODFI(); err != nil {
-		return err
-	}
-
-	if err := batch.isAddendaSequence(); err != nil {
-		return err
-	}
-
+	// Add configuration based validation for this type.
+	// ... batch.isAddendaCount(1)
+	// Add type specific validation.
+	// ...
 	return nil
 }
 
-// ValidateAll is a deep validation of all recods within the batch
-func (batch *BatchPPD) ValidateAll() error {
-	if err := batch.header.Validate(); err != nil {
-		return err
-	}
-	for _, entry := range batch.entries {
-		if err := entry.Validate(); err != nil {
-			return err
-		}
-		for _, addenda := range entry.Addendums {
-			if err := addenda.Validate(); err != nil {
-				return err
-			}
-		}
-	}
-	if err := batch.control.Validate(); err != nil {
+// Create takes Batch Header and Entries and builds a valid batch
+func (batch *BatchPPD) Create() error {
+	// generates sequence numbers and batch control
+	if err := batch.build(); err != nil {
 		return err
 	}
-	// Validate the Batch wrapper.
-	if err := batch.Validate(); err != nil {
-		return err
-	}
-	return nil
-}
+	// Additional steps specific to batch type
+	// ...
 
-// Build takes Batch Header and Entries and builds a valid batch
-func (batch *BatchPPD) Build() error {
-	// Requires a valid BatchHeader
-	if err := batch.header.Validate(); err != nil {
+	if err := batch.Validate(); err != nil {
 		return err
 	}
-	// Specific to Batch type.
-	if len(batch.entries) <= 0 {
-		return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "entries", Msg: msgBatchEntries}
-	}
-	// build controls and sequence numbers
-	entryCount := 0
-	seq := 1
-	for i, entry := range batch.entries {
-		entryCount = entryCount + 1 + len(entry.Addendums)
-		batch.entries[i].setTraceNumber(batch.header.ODFIIdentification, seq)
-		seq++
-		addendaSeq := 1
-		for x := range entry.Addendums {
-			batch.entries[i].Addendums[x].SequenceNumber = addendaSeq
-			batch.entries[i].Addendums[x].EntryDetailSequenceNumber = batch.parseNumField(batch.entries[i].TraceNumberField()[8:])
-			addendaSeq++
-		}
-	}
-
-	// build a BatchControl record
-	bc := NewBatchControl()
-	bc.ServiceClassCode = batch.header.ServiceClassCode
-	bc.CompanyIdentification = batch.header.CompanyIdentification
-	bc.ODFIIdentification = batch.header.ODFIIdentification
-	bc.BatchNumber = batch.header.BatchNumber
-	bc.EntryAddendaCount = entryCount
-	bc.EntryHash = batch.parseNumField(batch.calculateEntryHash())
-	bc.TotalCreditEntryDollarAmount, bc.TotalDebitEntryDollarAmount = batch.calculateBatchAmounts()
-	batch.control = bc
-
-	// validate checks that the above build covered all validation checks
-	if err := batch.ValidateAll(); err != nil {
-		return err // only errors if source code of batch.build is inconsistent with batch.validate
-	}
-	return nil
-}
-
-// SetHeader appends an BatchHeader to the Batch
-func (batch *BatchPPD) SetHeader(batchHeader *BatchHeader) {
-	batch.header = batchHeader
-}
-
-// GetHeader returns the curent Batch header
-func (batch *BatchPPD) GetHeader() *BatchHeader {
-	return batch.header
-}
-
-// SetControl appends an BatchControl to the Batch
-func (batch *BatchPPD) SetControl(batchControl *BatchControl) {
-	batch.control = batchControl
-}
-
-// GetControl returns the curent Batch Control
-func (batch *BatchPPD) GetControl() *BatchControl {
-	return batch.control
-}
-
-// GetEntries returns a slice of entry details for the batch
-func (batch *BatchPPD) GetEntries() []*EntryDetail {
-	return batch.entries
-}
-
-// AddEntry appends an EntryDetail to the Batch
-func (batch *BatchPPD) AddEntry(entry *EntryDetail) Batcher {
-	batch.entries = append(batch.entries, entry)
-	//	return batch.entries
-	return Batcher(batch)
-}
-
-// isBatchEntryCount validate Entry count is accurate
-// The Entry/Addenda Count Field is a tally of each Entry Detail and Addenda
-// Record processed within the batch
-func (batch *BatchPPD) isBatchEntryCount() error {
-	entryCount := 0
-	for _, entry := range batch.entries {
-		entryCount = entryCount + 1 + len(entry.Addendums)
-	}
-	if entryCount != batch.control.EntryAddendaCount {
-		msg := fmt.Sprintf(msgBatchCalculatedControlEquality, entryCount, batch.control.EntryAddendaCount)
-		return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "EntryAddendaCount", Msg: msg}
-	}
-	return nil
-}
-
-// isBatchAmount validate Amount is the same as what is in the Entries
-// The Total Debit and Credit Entry Dollar Amount fields contain accumulated
-// Entry Detail debit and credit totals within a given batch
-func (batch *BatchPPD) isBatchAmount() error {
-	credit, debit := batch.calculateBatchAmounts()
-	if debit != batch.control.TotalDebitEntryDollarAmount {
-		msg := fmt.Sprintf(msgBatchCalculatedControlEquality, debit, batch.control.TotalDebitEntryDollarAmount)
-		return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "TotalDebitEntryDollarAmount", Msg: msg}
-	}
-
-	if credit != batch.control.TotalCreditEntryDollarAmount {
-		msg := fmt.Sprintf(msgBatchCalculatedControlEquality, credit, batch.control.TotalCreditEntryDollarAmount)
-		return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "TotalCreditEntryDollarAmount", Msg: msg}
-	}
-	return nil
-}
-
-func (batch *BatchPPD) calculateBatchAmounts() (credit int, debit int) {
-	for _, entry := range batch.entries {
-		if entry.TransactionCode == 22 || entry.TransactionCode == 23 {
-			credit = credit + entry.Amount
-		}
-		if entry.TransactionCode == 27 || entry.TransactionCode == 28 {
-			debit = debit + entry.Amount
-		}
-		// savings credit
-		if entry.TransactionCode == 32 || entry.TransactionCode == 33 {
-			credit = credit + entry.Amount
-		}
-		// savings debit
-		if entry.TransactionCode == 37 || entry.TransactionCode == 38 {
-			debit = debit + entry.Amount
-		}
-	}
-	return credit, debit
-}
-
-// isSequenceAscending Individual Entry Detail Records within individual batches must
-// be in ascending Trace Number order (although Trace Numbers need not necessarily be consecutive).
-func (batch *BatchPPD) isSequenceAscending() error {
-	lastSeq := -1
-	for _, entry := range batch.entries {
-		if entry.TraceNumber <= lastSeq {
-			msg := fmt.Sprintf(msgBatchAscending, entry.TraceNumber, lastSeq)
-			return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "TraceNumber", Msg: msg}
-		}
-		lastSeq = entry.TraceNumber
-	}
-	return nil
-}
-
-// isEntryHash validates the hash by recalculating the result
-func (batch *BatchPPD) isEntryHash() error {
-	hashField := batch.calculateEntryHash()
-	if hashField != batch.control.EntryHashField() {
-		msg := fmt.Sprintf(msgBatchCalculatedControlEquality, hashField, batch.control.EntryHashField())
-		return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "EntryHash", Msg: msg}
-	}
-	return nil
-}
-
-// calculateEntryHash This field is prepared by hashing the 8-digit Routing Number in each entry.
-// The Entry Hash provides a check against inadvertent alteration of data
-func (batch *BatchPPD) calculateEntryHash() string {
-	hash := 0
-	for _, entry := range batch.entries {
-		hash = hash + entry.RDFIIdentification
-	}
-	return batch.numericField(hash, 10)
-}
-
-// The Originator Status Code is not equal to “2” for DNE if the Transaction Code is 23 or 33
-func (batch *BatchPPD) isOriginatorDNE() error {
-	if batch.header.OriginatorStatusCode != 2 {
-		for _, entry := range batch.entries {
-			if entry.TransactionCode == 23 || entry.TransactionCode == 33 {
-				msg := fmt.Sprintf(msgBatchOriginatorDNE, batch.header.OriginatorStatusCode)
-				return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "OriginatorStatusCode", Msg: msg}
-			}
-		}
-	}
-	return nil
-}
-
-// isTraceNumberODFI checks if the first 8 positions of the entry detail trace number
-// match the batch header odfi
-func (batch *BatchPPD) isTraceNumberODFI() error {
-	for _, entry := range batch.entries {
-		if batch.header.ODFIIdentificationField() != entry.TraceNumberField()[:8] {
-			msg := fmt.Sprintf(msgBatchTraceNumberNotODFI, batch.header.ODFIIdentificationField(), entry.TraceNumberField()[:8])
-			return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "ODFIIdentificationField", Msg: msg}
-		}
-	}
-
-	return nil
-}
-
-// isAddendaSequence check multiple errors on addenda records in the batch entries
-func (batch *BatchPPD) isAddendaSequence() error {
-	for _, entry := range batch.entries {
-		if len(entry.Addendums) > 0 {
-			// addenda without indicator flag of 1
-			if entry.AddendaRecordIndicator != 1 {
-				return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "AddendaRecordIndicator", Msg: msgBatchAddendaIndicator}
-			}
-			lastSeq := -1
-			// check if sequence is assending
-			for _, addenda := range entry.Addendums {
-				if addenda.SequenceNumber < lastSeq {
-					msg := fmt.Sprintf(msgBatchAscending, addenda.SequenceNumber, lastSeq)
-					return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "SequenceNumber", Msg: msg}
-				}
-				lastSeq = addenda.SequenceNumber
-				// check that we are in the correct Entry Detail
-				if !(addenda.EntryDetailSequenceNumberField() == entry.TraceNumberField()[8:]) {
-					msg := fmt.Sprintf(msgBatchAddendaTraceNumber, addenda.EntryDetailSequenceNumberField(), entry.TraceNumberField()[8:])
-					return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "TraceNumber", Msg: msg}
-				}
-			}
-		}
-	}
 	return nil
 }
diff --git a/batchWeb.go b/batchWeb.go
new file mode 100644
index 00000000..b0593bc0
--- /dev/null
+++ b/batchWeb.go
@@ -0,0 +1,51 @@
+package ach
+
+// BatchWEB creates a batch file that handles SEC payment type WEB
+type BatchWEB struct {
+	batch
+}
+
+// NewBatchWEB returns a *BatchWEB
+func NewBatchWEB(params ...BatchParam) *BatchWEB {
+	batch := new(BatchWEB)
+	batch.SetControl(NewBatchControl())
+
+	if len(params) > 0 {
+		bh := NewBatchHeader(params[0])
+		bh.StandardEntryClassCode = "WEB"
+		batch.SetHeader(bh)
+		return batch
+	}
+	bh := NewBatchHeader()
+	bh.StandardEntryClassCode = "WEB"
+	batch.SetHeader(bh)
+	return batch
+}
+
+// Validate ensures the batch meets NACHA rules specific to this batch type.
+func (batch *BatchWEB) 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.
+	// ... batch.isAddendaCount(1)
+	// Add type specific validation.
+	// ...
+	return nil
+}
+
+// Create builds the batch sequence numbers and batch control. Additional creation
+func (batch *BatchWEB) Create() error {
+	// generates sequence numbers and batch control
+	if err := batch.build(); err != nil {
+		return err
+	}
+	// Additional steps specific to batch type
+	// ...
+
+	if err := batch.Validate(); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/batcher.go b/batcher.go
index ea479997..10ace8fc 100644
--- a/batcher.go
+++ b/batcher.go
@@ -11,17 +11,16 @@ import (
 //    * Determines format of the entry detail records
 //    * Determines addenda records (required or optional PLUS one or up to 9,999 records)
 //    * Determines rules to follow (return timeframes)
-// * Some SEC codes require specific data in predetermined fields within the ACH record
+// 	  * Some SEC codes require specific data in predetermined fields within the ACH record
 type Batcher interface {
 	GetHeader() *BatchHeader
 	SetHeader(*BatchHeader)
 	GetControl() *BatchControl
 	SetControl(*BatchControl)
 	GetEntries() []*EntryDetail
-	AddEntry(*EntryDetail) Batcher
-	Build() error
+	AddEntry(*EntryDetail)
+	Create() error
 	Validate() error
-	ValidateAll() error
 }
 
 // BatchError is an Error that describes batch validation issues
@@ -35,16 +34,28 @@ func (e *BatchError) Error() string {
 	return fmt.Sprintf("BatchNumber %d %s %s", e.BatchNumber, e.FieldName, e.Msg)
 }
 
-// BatchParam is a subset of BatchHeader used for simplifying the build process.
+// BatchParam contains information about the company(Originator) and the type of detail records to follow.
+// It is a subset of BatchHeader used for simplifying the client api build process.
 type BatchParam struct {
-	ServiceClassCode        string `json:"service_class_code"`
-	CompanyName             string `json:"company_name"`
-	CompanyIdentification   string `json:"company_identification"`
-	StandardEntryClass      string `json:"standard_entry_class"`
+	// ServiceClassCode a three digit code identifies:
+	// 	- 200 mixed debits and credits
+	// 	- 220 credits only
+	// 	- 225 debits only
+	ServiceClassCode string `json:"service_class_code"`
+	// CompanyName is the legal company name making the transaction.
+	CompanyName string `json:"company_name"`
+	// CompanyIdentification is assigned by your bank to identify your company. Frequently the federal tax ID
+	CompanyIdentification string `json:"company_identification"`
+	// StandardEntryClass identifies the payment type (product) found within the batch using a 3-character code
+	StandardEntryClass string `json:"standard_entry_class"`
+	// CompanyEntryDescription describes the transaction. For example "PAYROLL"
 	CompanyEntryDescription string `json:"company_entry_description"`
-	CompanyDescriptiveDate  string `json:"company_descriptive_date"`
-	EffectiveEntryDate      string `json:"effective_entry_date"`
-	ODFIIdentification      string `json:"ODFI_identification"`
+	// CompanyDescriptiveDate a date chosen to identify the transactions in YYMMDD format.
+	CompanyDescriptiveDate string `json:"company_descriptive_date"`
+	// Date transactions are to be posted to the receivers’ account in YYMMDD format.
+	EffectiveEntryDate string `json:"effective_entry_date"`
+	// ODFIIdentification originating ODFI's routing number without the last digit
+	ODFIIdentification string `json:"ODFI_identification"`
 }
 
 // Errors specific to parsing a Batch container
diff --git a/entryDetail.go b/entryDetail.go
index d1a45a18..adaad55f 100644
--- a/entryDetail.go
+++ b/entryDetail.go
@@ -73,8 +73,8 @@ type EntryDetail struct {
 	// with an entry or item rather than a physical record.
 	TraceNumber int
 
-	// Addendums a list of Addenda for the Entry Detail
-	Addendums []Addenda
+	// Addendum a list of Addenda for the Entry Detail
+	Addendum []Addenda
 	// validator is composed for data validation
 	validator
 	// converters is composed for ACH to golang Converters
@@ -217,11 +217,11 @@ func (ed *EntryDetail) fieldInclusion() error {
 	return nil
 }
 
-// AddAddenda appends an EntryDetail to the Addendums
+// AddAddenda appends an EntryDetail to the Addendum
 func (ed *EntryDetail) AddAddenda(addenda Addenda) []Addenda {
 	ed.AddendaRecordIndicator = 1
-	ed.Addendums = append(ed.Addendums, addenda)
-	return ed.Addendums
+	ed.Addendum = append(ed.Addendum, addenda)
+	return ed.Addendum
 }
 
 // SetRDFI takes the 9 digit RDFI account number and separates it for RDFIIdentification and CheckDigit
diff --git a/example/ach-ppd-read/main.go b/example/ach-ppd-read/main.go
index 941ea74b..72a70df9 100644
--- a/example/ach-ppd-read/main.go
+++ b/example/ach-ppd-read/main.go
@@ -20,11 +20,11 @@ func main() {
 		fmt.Printf("Issue reading file: %+v \n", err)
 	}
 	// ensure we have a validated file structure
-	if r.File.ValidateAll(); err != nil {
+	if r.File.Validate(); err != nil {
 		fmt.Printf("Could not validate entire read file: %v", err)
 	}
 	// If you trust the file but it's formating is off building will probably resolve the malformed file.
-	if r.File.Build(); err != nil {
+	if r.File.Create(); err != nil {
 		fmt.Printf("Could not build file with read properties: %v", err)
 	}
 
diff --git a/example/ach-ppd-write/main.go b/example/ach-ppd-write/main.go
index 207d1bd8..2fd6c3f6 100644
--- a/example/ach-ppd-write/main.go
+++ b/example/ach-ppd-write/main.go
@@ -46,7 +46,7 @@ func main() {
 	batch := ach.NewBatchPPD()
 	batch.SetHeader(bh)
 	batch.AddEntry(entry)
-	if err := batch.Build(); err != nil {
+	if err := batch.Create(); err != nil {
 		log.Fatalf("Unexpected error building batch: %s\n", err)
 	}
 
@@ -54,7 +54,7 @@ func main() {
 	file := ach.NewFile()
 	file.SetHeader(fh)
 	file.AddBatch(batch)
-	if err := file.Build(); err != nil {
+	if err := file.Create(); err != nil {
 		log.Fatalf("Unexpected error building file: %s\n", err)
 	}
 
diff --git a/example/simple-file-creation/main.go b/example/simple-file-creation/main.go
index 4f8f3564..bb940a29 100644
--- a/example/simple-file-creation/main.go
+++ b/example/simple-file-creation/main.go
@@ -16,8 +16,9 @@ func main() {
 		ImmediateOriginName:      "Your Company",
 		ReferenceCode:            "#00000A1"})
 
-	// To create a batch
-	batch := ach.NewBatchPPD(ach.BatchParam{
+	// To create a batch.
+	// Errors only if payment type is not supported.
+	batch, _ := ach.NewBatch(ach.BatchParam{
 		ServiceClassCode:        "220",
 		CompanyName:             "Your Company",
 		StandardEntryClass:      "PPD",
@@ -45,9 +46,10 @@ func main() {
 
 	batch.AddEntry(entry)
 
-	// When all of the Entries are added to the batch we must build it.
 
-	if err := batch.Build(); err != nil {
+	// When all of the Entries are added to the batch we must create it.
+
+	if err := batch.Create(); err != nil {
 		fmt.Printf("%T: %s", err, err)
 	}
 
@@ -55,9 +57,9 @@ func main() {
 
 	file.AddBatch(batch)
 
-	// Once we added all our batches we must build the file
+	// Once we added all our batches we must create the file
 
-	if err := file.Build(); err != nil {
+	if err := file.Create(); err != nil {
 		fmt.Printf("%T: %s", err, err)
 	}
 
diff --git a/file.go b/file.go
index f1f58c69..0da9082c 100644
--- a/file.go
+++ b/file.go
@@ -16,8 +16,8 @@ import (
 // First position of all Record Types. These codes are uniquely assigned to
 // the first byte of each row in a file.
 const (
-	headerPos       = "1"
-	batchPos        = "5"
+	fileHeaderPos   = "1"
+	batchHeaderPos  = "5"
 	entryDetailPos  = "6"
 	entryAddendaPos = "7"
 	batchControlPos = "8"
@@ -68,8 +68,11 @@ type File struct {
 
 // FileParam is the minimal fields required to make a ach file header
 type FileParam struct {
-	ImmediateDestination     string `json:"immediate_destination"`
-	ImmediateOrigin          string `json:"immediate_origin"`
+	// ImmediateDestination is the originating banks ABA routing number. Frequently your banks ABA routing number.
+	ImmediateDestination string `json:"immediate_destination"`
+	// ImmediateOrigin
+	ImmediateOrigin string `json:"immediate_origin"`
+	// ImmediateDestinationName is the originating banks name.
 	ImmediateDestinationName string `json:"immediate_destination_name"`
 	ImmediateOriginName      string `json:"immediate_origin_name"`
 	ReferenceCode            string `json:"reference_code,omitempty"`
@@ -91,8 +94,8 @@ func NewFile(params ...FileParam) *File {
 	}
 }
 
-// Build creates a valid file and requires that the FileHeader and at least one Batch
-func (f *File) Build() error {
+// Create creates a valid file and requires that the FileHeader and at least one Batch
+func (f *File) Create() error {
 	// Requires a valid FileHeader to build FileControl
 	if err := f.Header.Validate(); err != nil {
 		return err
@@ -113,7 +116,7 @@ func (f *File) Build() error {
 		f.Batches[i].GetHeader().BatchNumber = batchSeq
 		f.Batches[i].GetControl().BatchNumber = batchSeq
 		batchSeq++
-		// sum file entry and addenda records. Assume batch.Build() batch properly calculated control
+		// sum file entry and addenda records. Assume batch.Create() batch properly calculated control
 		fileEntryAddendaCount = fileEntryAddendaCount + batch.GetControl().EntryAddendaCount
 		// add 2 for Batch header/control + entry added count
 		totalRecordsInFile = totalRecordsInFile + 2 + batch.GetControl().EntryAddendaCount
@@ -138,9 +141,6 @@ func (f *File) Build() error {
 	fc.TotalCreditEntryDollarAmountInFile = totalCreditAmount
 	f.Control = fc
 
-	if err := f.ValidateAll(); err != nil {
-		return err
-	}
 	return nil
 }
 
@@ -179,27 +179,6 @@ func (f *File) Validate() error {
 	return nil
 }
 
-// ValidateAll walks the data structure and validates each record
-func (f *File) ValidateAll() error {
-
-	// validate inward out of the File Struct
-	for _, batch := range f.Batches {
-		if err := batch.ValidateAll(); err != nil {
-			return err
-		}
-	}
-	if err := f.Header.Validate(); err != nil {
-		return err
-	}
-	if err := f.Control.Validate(); err != nil {
-		return err
-	}
-	if err := f.Validate(); err != nil {
-		return err
-	}
-	return nil
-}
-
 // isEntryAddenda is prepared by hashing the RDFI’s 8-digit Routing Number in each entry.
 //The Entry Hash provides a check against inadvertent alteration of data
 func (f *File) isEntryAddendaCount() error {
diff --git a/reader.go b/reader.go
index ca58457a..a99b9ead 100644
--- a/reader.go
+++ b/reader.go
@@ -123,11 +123,11 @@ func (r *Reader) processFixedWidthFile(line *string) error {
 
 func (r *Reader) parseLine() error {
 	switch r.line[:1] {
-	case headerPos:
+	case fileHeaderPos:
 		if err := r.parseFileHeader(); err != nil {
 			return err
 		}
-	case batchPos:
+	case batchHeaderPos:
 		if err := r.parseBatchHeader(); err != nil {
 			return err
 		}
@@ -186,21 +186,23 @@ func (r *Reader) parseBatchHeader() error {
 		// batch header inside of current batch
 		return r.error(&FileError{Msg: msgFileBatchInside})
 	}
+
+	// Ensure we have a valid batch header before building a batch.
 	bh := NewBatchHeader()
 	bh.Parse(r.line)
-	switch sec := bh.StandardEntryClassCode; sec {
-	case ppd:
-		r.addCurrentBatch(NewBatchPPD())
-	// @TODO add additional batch types to creation
-	default:
-		msg := fmt.Sprintf(msgFileNoneSEC, sec)
-		return r.error(&FileError{FieldName: "StandardEntryClassCode", Msg: msg})
+	if err := bh.Validate(); err != nil {
+		return r.error(err)
 	}
-	r.currentBatch.SetHeader(bh)
 
-	if err := r.currentBatch.GetHeader().Validate(); err != nil {
+	// Passing SEC type into NewBatch creates a Batcher of SEC code type.
+	batch, err := NewBatch(BatchParam{
+		StandardEntryClass: bh.StandardEntryClassCode})
+	if err != nil {
 		return r.error(err)
 	}
+
+	batch.SetHeader(bh)
+	r.addCurrentBatch(batch)
 	return nil
 }
 
@@ -210,21 +212,11 @@ func (r *Reader) parseEntryDetail() error {
 	if r.currentBatch == nil {
 		return r.error(&FileError{Msg: msgFileBatchOutside})
 	}
-	// @TODO change to EntryDetailer once interface is implmented if EntryDetails change
 	ed := new(EntryDetail)
-
-	switch sec := r.currentBatch.GetHeader().StandardEntryClassCode; sec {
-	case ppd:
-		ed.Parse(r.line)
-		if err := ed.Validate(); err != nil {
-			return r.error(err)
-		}
-	//case "WEB":
-	default:
-		msg := fmt.Sprintf(msgFileNoneSEC, sec)
-		return r.error(&FileError{FieldName: "StandardEntryClassCode", Msg: msg})
+	ed.Parse(r.line)
+	if err := ed.Validate(); err != nil {
+		return r.error(err)
 	}
-
 	r.currentBatch.AddEntry(ed)
 	return nil
 }
@@ -257,14 +249,7 @@ func (r *Reader) parseAddenda() error {
 			return r.error(&FileError{FieldName: "AddendaRecordIndicator", Msg: msg})
 		}
 	}
-	// case "CCD":
-	// this code can only be executed if the EntryDetail supports the SEC code
-	/*
-		default:
-			msg := fmt.Sprintf(msgFileNoneSEC, sec)
-			return r.error(&FileError{FieldName: "StandardEntryClassCode", Msg: msg})
-		}
-	*/
+
 	return nil
 }
 
diff --git a/writer.go b/writer.go
index 4d440f47..02349976 100644
--- a/writer.go
+++ b/writer.go
@@ -29,7 +29,7 @@ func NewWriter(w io.Writer) *Writer {
 
 // Writer writes a single ach.file record to w
 func (w *Writer) Write(file *File) error {
-	if err := file.ValidateAll(); err != nil {
+	if err := file.Validate(); err != nil {
 		return err
 	}
 
@@ -50,7 +50,7 @@ func (w *Writer) Write(file *File) error {
 				return err
 			}
 			w.lineNum++
-			for _, addenda := range entry.Addendums {
+			for _, addenda := range entry.Addendum {
 				if _, err := w.w.WriteString(addenda.String() + "\n"); err != nil {
 					return err
 				}


Test patch:
diff --git a/addenda_internal_test.go b/addenda_internal_test.go
index c283f8e8..b127d62f 100644
--- a/addenda_internal_test.go
+++ b/addenda_internal_test.go
@@ -37,7 +37,7 @@ func TestParseAddenda(t *testing.T) {
 	if err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
-	record := r.currentBatch.GetEntries()[0].Addendums[0]
+	record := r.currentBatch.GetEntries()[0].Addendum[0]
 
 	if record.recordType != "7" {
 		t.Errorf("RecordType Expected '7' got: %v", record.recordType)
@@ -68,7 +68,7 @@ func TestAddendaString(t *testing.T) {
 	if err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
-	record := r.currentBatch.GetEntries()[0].Addendums[0]
+	record := r.currentBatch.GetEntries()[0].Addendum[0]
 	if record.String() != line {
 		t.Errorf("Strings do not match")
 	}
diff --git a/batchPPD_test.go b/batchPPD_test.go
index 6508e502..1c97d37a 100644
--- a/batchPPD_test.go
+++ b/batchPPD_test.go
@@ -13,7 +13,7 @@ func mockBatchPPD() *BatchPPD {
 	mockBatch := NewBatchPPD()
 	mockBatch.SetHeader(mockBatchHeader())
 	mockBatch.AddEntry(mockEntryDetail())
-	if err := mockBatch.Build(); err != nil {
+	if err := mockBatch.Create(); err != nil {
 		panic(err)
 	}
 	return mockBatch
@@ -93,7 +93,7 @@ func TestCreditBatchisBatchAmount(t *testing.T) {
 	e2.Amount = 100
 	mockBatch.AddEntry(e1)
 	mockBatch.AddEntry(e2)
-	if err := mockBatch.Build(); err != nil {
+	if err := mockBatch.Create(); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
 
@@ -120,7 +120,7 @@ func TestSavingsBatchisBatchAmount(t *testing.T) {
 	e2.Amount = 100
 	mockBatch.AddEntry(e1)
 	mockBatch.AddEntry(e2)
-	if err := mockBatch.Build(); err != nil {
+	if err := mockBatch.Create(); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
 
@@ -157,7 +157,7 @@ func TestBatchDNEMismatch(t *testing.T) {
 	ed.AddAddenda(mockAddenda())
 	ed.AddAddenda(mockAddenda())
 	mockBatch.AddEntry(ed)
-	mockBatch.Build()
+	mockBatch.Create()
 
 	mockBatch.GetHeader().OriginatorStatusCode = 1
 	mockBatch.GetEntries()[0].TransactionCode = 23
@@ -193,7 +193,7 @@ func TestBatchEntryCountEquality(t *testing.T) {
 	a := mockAddenda()
 	e.AddAddenda(a)
 	mockBatch.AddEntry(e)
-	if err := mockBatch.Build(); err != nil {
+	if err := mockBatch.Create(); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
 
@@ -232,10 +232,10 @@ func TestBatchIsAddendaSeqAscending(t *testing.T) {
 	ed.AddAddenda(mockAddenda())
 	ed.AddAddenda(mockAddenda())
 	mockBatch.AddEntry(ed)
-	mockBatch.Build()
+	mockBatch.Create()
 
-	mockBatch.GetEntries()[0].Addendums[0].SequenceNumber = 2
-	mockBatch.GetEntries()[0].Addendums[1].SequenceNumber = 1
+	mockBatch.GetEntries()[0].Addendum[0].SequenceNumber = 2
+	mockBatch.GetEntries()[0].Addendum[1].SequenceNumber = 1
 	if err := mockBatch.Validate(); err != nil {
 		if e, ok := err.(*BatchError); ok {
 			if e.FieldName != "SequenceNumber" {
@@ -268,11 +268,11 @@ func TestBatchAddendaTraceNumber(t *testing.T) {
 	mockBatch := mockBatchPPD()
 	mockBatch.GetEntries()[0].AddAddenda(mockAddenda())
 	mockBatch.GetEntries()[0].AddAddenda(mockAddenda())
-	if err := mockBatch.Build(); err != nil {
+	if err := mockBatch.Create(); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
 
-	mockBatch.GetEntries()[0].Addendums[0].EntryDetailSequenceNumber = 99
+	mockBatch.GetEntries()[0].Addendum[0].EntryDetailSequenceNumber = 99
 	if err := mockBatch.Validate(); err != nil {
 		if e, ok := err.(*BatchError); ok {
 			if e.FieldName != "TraceNumber" {
@@ -310,110 +310,7 @@ func TestBatchBuild(t *testing.T) {
 	entry.AddAddenda(a1)
 	entry.AddAddenda(a2)
 	mockBatch.AddEntry(entry)
-	if err := mockBatch.Build(); err != nil {
+	if err := mockBatch.Create(); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
 }
-
-func TestBatchValidateAllEntries(t *testing.T) {
-	mockBatch := mockBatchPPD()
-	mockBatch.GetEntries()[0].DFIAccountNumber = ""
-	if err := mockBatch.ValidateAll(); err != nil {
-		if e, ok := err.(*FieldError); ok {
-			if e.Msg != msgFieldInclusion {
-				t.Errorf("%T: %s", err, err)
-			}
-		} else {
-			t.Errorf("%T: %s", err, err)
-		}
-	}
-}
-
-func TestBatchValidateAllAddenda(t *testing.T) {
-	mockBatch := mockBatchPPD()
-	mockBatch.GetEntries()[0].AddAddenda(mockAddenda())
-	if err := mockBatch.Build(); err != nil {
-		t.Errorf("%T: %s", err, err)
-	}
-
-	mockBatch.GetEntries()[0].Addendums[0].TypeCode = ""
-	if err := mockBatch.ValidateAll(); err != nil {
-		if e, ok := err.(*FieldError); ok {
-			if e.Msg != msgFieldInclusion {
-				t.Errorf("%T: %s", err, err)
-			}
-		} else {
-			t.Errorf("%T: %s", err, err)
-		}
-	}
-}
-
-func TestBatchValidateAllBatchControl(t *testing.T) {
-	mockBatch := mockBatchPPD()
-	mockBatch.GetControl().ODFIIdentification = 0
-	if err := mockBatch.ValidateAll(); err != nil {
-		if e, ok := err.(*FieldError); ok {
-			if e.Msg != msgFieldInclusion {
-				t.Errorf("%T: %s", err, err)
-			}
-		} else {
-			t.Errorf("%T: %s", err, err)
-		}
-	}
-}
-
-func TestBatchValidateAllHeader(t *testing.T) {
-	mockBatch := mockBatchPPD()
-	mockBatch.GetHeader().ODFIIdentification = 0
-	if err := mockBatch.ValidateAll(); err != nil {
-		if e, ok := err.(*FieldError); ok {
-			if e.Msg != msgFieldInclusion {
-				t.Errorf("%T: %s", err, err)
-			}
-		} else {
-			t.Errorf("%T: %s", err, err)
-		}
-	}
-}
-
-func TestBatchValidateAllODFI(t *testing.T) {
-	mockBatch := mockBatchPPD()
-	mockBatch.GetHeader().ODFIIdentification = 123456
-	if err := mockBatch.ValidateAll(); err != nil {
-		if e, ok := err.(*BatchError); ok {
-			if e.FieldName != "ODFIIdentification" {
-				t.Errorf("%T: %s", err, err)
-			}
-		} else {
-			t.Errorf("%T: %s", err, err)
-		}
-	}
-}
-
-func TestBatchBuildNoEntries(t *testing.T) {
-	mockBatch := NewBatchPPD()
-	mockBatch.SetHeader(mockBatchHeader())
-	if err := mockBatch.Build(); err != nil {
-		if e, ok := err.(*BatchError); ok {
-			if e.Msg != msgBatchEntries {
-				t.Errorf("%T: %s", err, err)
-			}
-		} else {
-			t.Errorf("%T: %s", err, err)
-		}
-	}
-}
-
-func TestBatchBuildHeader(t *testing.T) {
-	mockBatch := mockBatchPPD()
-	mockBatch.GetHeader().CompanyIdentification = ""
-	if err := mockBatch.Build(); err != nil {
-		if e, ok := err.(*FieldError); ok {
-			if e.Msg != msgFieldInclusion {
-				t.Errorf("%T: %s", err, err)
-			}
-		} else {
-			t.Errorf("%T: %s", err, err)
-		}
-	}
-}
diff --git a/file_test.go b/file_test.go
index fe75161b..ea07c1ba 100644
--- a/file_test.go
+++ b/file_test.go
@@ -13,7 +13,7 @@ func mockFilePPD() *File {
 	mockFile.SetHeader(mockFileHeader())
 	mockBatch := mockBatchPPD()
 	mockFile.AddBatch(mockBatch)
-	if err := mockFile.Build(); err != nil {
+	if err := mockFile.Create(); err != nil {
 		panic(err)
 	}
 	return mockFile
@@ -94,7 +94,7 @@ func TestFileCreditAmount(t *testing.T) {
 func TestFileEntryHash(t *testing.T) {
 	file := mockFilePPD()
 	file.AddBatch(mockBatchPPD())
-	file.Build()
+	file.Create()
 	file.Control.EntryHash = 63
 	if err := file.Validate(); err != nil {
 		if e, ok := err.(*FileError); ok {
@@ -117,9 +117,9 @@ func TestFileBlockCount10(t *testing.T) {
 	batch.AddEntry(mockEntryDetail())
 	batch.AddEntry(mockEntryDetail())
 	batch.AddEntry(mockEntryDetail())
-	batch.Build()
+	batch.Create()
 	file.AddBatch(batch)
-	if err := file.Build(); err != nil {
+	if err := file.Create(); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
 
@@ -129,8 +129,8 @@ func TestFileBlockCount10(t *testing.T) {
 	}
 	// make 11th record which should produce BlockCount of 2
 	file.Batches[0].AddEntry(mockEntryDetail())
-	file.Batches[0].Build() // File.Build does not re-build Batches
-	if err := file.Build(); err != nil {
+	file.Batches[0].Create() // File.Build does not re-build Batches
+	if err := file.Create(); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
 	if file.Control.BlockCount != 2 {
@@ -140,7 +140,7 @@ func TestFileBlockCount10(t *testing.T) {
 
 func TestFileBuildBadFileHeader(t *testing.T) {
 	file := NewFile().SetHeader(FileHeader{})
-	if err := file.Build(); err != nil {
+	if err := file.Create(); err != nil {
 		if e, ok := err.(*FieldError); ok {
 			if e.Msg != msgFieldInclusion {
 				t.Errorf("%T: %s", err, err)
@@ -153,7 +153,7 @@ func TestFileBuildBadFileHeader(t *testing.T) {
 
 func TestFileBuildNoBatch(t *testing.T) {
 	file := NewFile().SetHeader(mockFileHeader())
-	if err := file.Build(); err != nil {
+	if err := file.Create(); err != nil {
 		if e, ok := err.(*FileError); ok {
 			if e.FieldName != "Batchs" {
 				t.Errorf("%T: %s", err, err)
@@ -163,48 +163,3 @@ func TestFileBuildNoBatch(t *testing.T) {
 		}
 	}
 }
-
-func TestFileValidateAllBatch(t *testing.T) {
-	file := mockFilePPD()
-	// break the file header
-	file.Batches[0].GetHeader().ODFIIdentification = 0
-	if err := file.ValidateAll(); err != nil {
-		if e, ok := err.(*FieldError); ok {
-			if e.Msg != msgFieldInclusion {
-				t.Errorf("%T: %s", err, err)
-			}
-		} else {
-			t.Errorf("%T: %s", err, err)
-		}
-	}
-}
-
-func TestFileValidateAllFileHeader(t *testing.T) {
-	file := mockFilePPD()
-	// break the file header
-	file.Header.ImmediateOrigin = 0
-	if err := file.ValidateAll(); err != nil {
-		if e, ok := err.(*FieldError); ok {
-			if e.Msg != msgFieldInclusion {
-				t.Errorf("%T: %s", err, err)
-			}
-		} else {
-			t.Errorf("%T: %s", err, err)
-		}
-	}
-}
-
-func TestFileValidateAllFileControl(t *testing.T) {
-	file := mockFilePPD()
-	// break the file header
-	file.Control.BatchCount = 0
-	if err := file.ValidateAll(); err != nil {
-		if e, ok := err.(*FieldError); ok {
-			if e.Msg != msgFieldInclusion {
-				t.Errorf("%T: %s", err, err)
-			}
-		} else {
-			t.Errorf("%T: %s", err, err)
-		}
-	}
-}
diff --git a/reader_test.go b/reader_test.go
index 4476ac8f..10d16db3 100644
--- a/reader_test.go
+++ b/reader_test.go
@@ -5,8 +5,6 @@
 package ach
 
 import (
-	"bytes"
-	"fmt"
 	"os"
 	"strings"
 	"testing"
@@ -36,7 +34,23 @@ func TestPPDDebitRead(t *testing.T) {
 	if err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
-	if err = r.File.ValidateAll(); err != nil {
+	if err = r.File.Validate(); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+}
+
+func TestWEBDebitRead(t *testing.T) {
+	f, err := os.Open("./testdata/web-debit.ach")
+	if err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+	defer f.Close()
+	r := NewReader(f)
+	_, err = r.Read()
+	if err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+	if err = r.File.Validate(); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
 }
@@ -232,27 +246,6 @@ func TestFileEntryDetail(t *testing.T) {
 	}
 }
 
-// TestFileEntryDetailSECNoSupport validation error populates through the reader
-func TestFileEntryDetailSECNoSupport(t *testing.T) {
-	ed := mockEntryDetail()
-	ed.CheckDigit = 0
-	line := ed.String()
-	r := NewReader(strings.NewReader(line))
-	r.addCurrentBatch(NewBatchPPD())
-	r.currentBatch.SetHeader(mockBatchHeader())
-	r.currentBatch.GetHeader().StandardEntryClassCode = "ABCXYZ"
-	_, err := r.Read()
-	if p, ok := err.(*ParseError); ok {
-		if e, ok := p.Err.(*FieldError); ok {
-			if e.FieldName != "StandardEntryClassCode" {
-				t.Errorf("%T: %s", e, e)
-			}
-		}
-	} else {
-		t.Errorf("%T: %s", err, err)
-	}
-}
-
 // TestFileAddenda validation error populates through the reader
 func TestFileAddenda(t *testing.T) {
 	bh := mockBatchHeader()
@@ -260,7 +253,7 @@ func TestFileAddenda(t *testing.T) {
 	addenda := mockAddenda()
 	addenda.SequenceNumber = 0
 	ed.AddAddenda(addenda)
-	line := bh.String() + "\n" + ed.String() + "\n" + ed.Addendums[0].String()
+	line := bh.String() + "\n" + ed.String() + "\n" + ed.Addendum[0].String()
 	r := NewReader(strings.NewReader(line))
 	_, err := r.Read()
 	if err != nil {
@@ -394,6 +387,7 @@ func TestFileAddBatchValidation(t *testing.T) {
 	}
 }
 
+/**
 func TestFileHeaderExists(t *testing.T) {
 	file := mockFilePPD()
 	file.SetHeader(FileHeader{})
@@ -401,7 +395,6 @@ func TestFileHeaderExists(t *testing.T) {
 	w := NewWriter(buf)
 	w.Write(file)
 	w.Flush()
-	//println(buf.String())
 	r := NewReader(strings.NewReader(buf.String()))
 	f, err := r.Read()
 	if err != nil {
@@ -418,8 +411,8 @@ func TestFileHeaderExists(t *testing.T) {
 	} else {
 		fmt.Println(f.Header.String())
 	}
-
 }
+**/
 
 // TestFileLongErr Batch Header Service Class is 000 which does not validate
 func TestFileLongErr(t *testing.T) {
diff --git a/recordParams_test.go b/recordParams_test.go
index 2220d03d..8d2f0ed9 100644
--- a/recordParams_test.go
+++ b/recordParams_test.go
@@ -1,7 +1,7 @@
 package ach
 
 import (
-	"os"
+	"bytes"
 	"testing"
 )
 
@@ -73,8 +73,8 @@ func TestBuildFileParam(t *testing.T) {
 		ImmediateOriginName:      "Your Company",
 		ReferenceCode:            "#00000A1"})
 
-	// To create a batch
-	batch := NewBatchPPD(BatchParam{
+	// To create a batch. Errors only if payment type is not supported.
+	batch, _ := NewBatch(BatchParam{
 		ServiceClassCode:        "220",
 		CompanyName:             "Your Company",
 		StandardEntryClass:      "PPD",
@@ -90,7 +90,7 @@ func TestBuildFileParam(t *testing.T) {
 		Amount:            "17500",
 		TransactionCode:   "22",
 		IDNumber:          "ABC##jvkdjfuiwn",
-		IndividualName:    "Bob Smith",
+		IndividualName:    "Robert Smith",
 		DiscretionaryData: "B1"})
 
 	// To add one or more optional addenda records for an entry
@@ -105,7 +105,7 @@ func TestBuildFileParam(t *testing.T) {
 
 	// When all of the Entries are added to the batch we must build it.
 
-	if err := batch.Build(); err != nil {
+	if err := batch.Create(); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
 
@@ -115,14 +115,15 @@ func TestBuildFileParam(t *testing.T) {
 
 	// Once we added all our batches we must build the file
 
-	if err := file.Build(); err != nil {
+	if err := file.Create(); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
 
-	// Finally we wnt to write the file to an io.Writer
-	w := NewWriter(os.Stdout)
+	// Finally we write the file to an io.Writer
+	var b bytes.Buffer
+	w := NewWriter(&b)
 	if err := w.WriteAll([]*File{file}); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
-	//w.Flush()
+	w.Flush()
 }
diff --git a/testdata/web-debit.ach b/testdata/web-debit.ach
new file mode 100644
index 00000000..24387c8a
--- /dev/null
+++ b/testdata/web-debit.ach
@@ -0,0 +1,20 @@
+101 081000032 0180362811503042207A094101Some Bank              Your Company Inc       A0000001
+5220Your Company Inc                    0018036281WEBTrnsNicknaMar 5 150305   1081000030000000
+622081000210123456789012345670000003521RAj##23920rjf31John Doe              A10081000030000000
+6220810002105654221          0000002300RAj##32b1kn1bb3Bob Dole              A10081000030000001
+6220810002105654221          0000002499RAj##765kn4    Adam Something        A10081000030000002
+6220810002105654221          0000001000RAj##3j43kj4   James Bond            A10081000030000003
+822000000400324000840000000000000000000093200018036281                         081000030000000
+5220Your Company Inc                    0018036281WEBTrnsNicknaMar 16150316   1081000030000001
+6220810002105654221          0000017500RAj##8k765j4k32Luke Skywalker        A10081000030000004
+822000000100081000210000000000000000000175000018036281                         081000030000001
+5225Your Company Inc                    0018036281PPDTrnsNicknaMar 6 150306   1081000030000002
+627101000019923698412584     0000015000RAj##765432hj  Jane Doe              A10081000030000005
+822500000100101000010000000150000000000000000018036281                         081000030000002
+9000003000002000000060050600106000000015000000000026820                                       
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
\ No newline at end of file
diff --git a/writer_test.go b/writer_test.go
index 857c3949..2ea8120b 100644
--- a/writer_test.go
+++ b/writer_test.go
@@ -13,13 +13,13 @@ func TestPPDWrite(t *testing.T) {
 	batch := NewBatchPPD()
 	batch.SetHeader(mockBatchHeader())
 	batch.AddEntry(entry)
-	batch.Build()
+	batch.Create()
 	file.AddBatch(batch)
 
-	if err := file.Build(); err != nil {
+	if err := file.Create(); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
-	if err := file.ValidateAll(); err != nil {
+	if err := file.Validate(); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
 
@@ -34,7 +34,7 @@ func TestPPDWrite(t *testing.T) {
 	if err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
-	if err = r.File.ValidateAll(); err != nil {
+	if err = r.File.Validate(); err != nil {
 		t.Errorf("%T: %s", err, err)
 	}
 }