Repository: moov-io/ach
Pull request #127 (2017-08-07 03:31:06)

Simplify building an ach file with record parameter's
Adding FileParam, BatchParam, EntryParam, and AddendaParam which can be used to create the corresponding record types without exposing the rest of the structures details. Parameter structs are annotated with JSON tags to be exposed to a future REST API. Review recordParams_test.go to see how the new parameter structs can be used to create a ACH file. 

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

Coverage increased (+0.1%) to 97.08% when pulling **a6637679b5b002261cef1ea1c8d5c6b43631f290 on wadearnold:Simplify-building-an-ACH-file** into **25b876d8c64923676851a290627b138023668d39 on moov-io:master**.

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

Coverage increased (+0.1%) to 97.08% when pulling **11e3578e2773faccb5873a1df950f1b9b83ed675 on wadearnold:Simplify-building-an-ACH-file** into **25b876d8c64923676851a290627b138023668d39 on moov-io:master**.


Patch:
diff --git a/.gitignore b/.gitignore
index 9f845902..44842dc2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,13 @@
 _obj
 _test
 
+# VSCode ignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
 # Architecture specific extensions/prefixes
 *.[568vq]
 [568vq].out
diff --git a/addenda.go b/addenda.go
index e0942061..a818e6a5 100644
--- a/addenda.go
+++ b/addenda.go
@@ -19,23 +19,35 @@ type Addenda struct {
 	// be a "1".
 	SequenceNumber int
 	// EntryDetailSequenceNumber contains the ascending sequence number section of the Entry
-	// Detail or Corporate Entry Detail Record's trace numbe This number is
+	// Detail or Corporate Entry Detail Record's trace number This number is
 	// the same as the last seven digits of the trace number of the related
 	// Entry Detail Record or Corporate Entry Detail Record.
 	EntryDetailSequenceNumber int
 	// validator is composed for data validation
 	validator
-	// converters is composed for ACH to golang Converters
+	// converters is composed for ACH to GoLang Converters
 	converters
 }
 
+// AddendaParam is the minimal fields required to make a ach addenda
+type AddendaParam struct {
+	PaymentRelatedInfo string `json:"payment_related_info"`
+}
+
 // NewAddenda returns a new Addenda with default values for none exported fields
-func NewAddenda() Addenda {
-	return Addenda{
-		recordType:     "7",
-		TypeCode:       "05",
-		SequenceNumber: 1,
+func NewAddenda(params ...AddendaParam) Addenda {
+	addenda := Addenda{
+		recordType:                "7",
+		TypeCode:                  "05",
+		SequenceNumber:            1,
+		EntryDetailSequenceNumber: 1,
+	}
+
+	if len(params) > 0 {
+		addenda.PaymentRelatedInformation = params[0].PaymentRelatedInfo
+		return addenda
 	}
+	return addenda
 }
 
 // Parse takes the input record string and parses the Addenda values
@@ -44,7 +56,7 @@ func (addenda *Addenda) Parse(record string) {
 	addenda.recordType = "7"
 	// 2-3 Defines the specific explanation and format for the addenda information contained in the same record
 	addenda.TypeCode = record[1:3]
-	// 4-83 Based on the information entere (04-83) 80 alphanumeric
+	// 4-83 Based on the information entered (04-83) 80 alphanumeric
 	addenda.PaymentRelatedInformation = strings.TrimSpace(record[3:83])
 	// 84-87 SequenceNumber is consecutively assigned to each Addenda Record following
 	// an Entry Detail Record
diff --git a/batchHeader.go b/batchHeader.go
index 5aa2c1c7..c70382d1 100644
--- a/batchHeader.go
+++ b/batchHeader.go
@@ -17,7 +17,7 @@ import (
 // contained in the batch (i.e., the standard entry class, PPD for consumer, CCD
 // or CTX for corporate). This record also contains the effective date, or desired
 // settlement date, for all entries contained in this batch. The settlement date
-// field is not entered as it is determined by the ACH operato
+// field is not entered as it is determined by the ACH operator
 type BatchHeader struct {
 	// RecordType defines the type of record in the block. 5
 	recordType string
@@ -90,13 +90,25 @@ type BatchHeader struct {
 	converters
 }
 
-// NewBatchHeader returns a new BatchHeader with default valus for none exported fields
-func NewBatchHeader() *BatchHeader {
-	return &BatchHeader{
+// NewBatchHeader returns a new BatchHeader with default values for none exported fields
+func NewBatchHeader(params ...BatchParam) *BatchHeader {
+	bh := &BatchHeader{
 		recordType:           "5",
 		OriginatorStatusCode: 1,
 		BatchNumber:          1,
 	}
+	if len(params) > 0 {
+		bh.ServiceClassCode = bh.parseNumField(params[0].ServiceClassCode)
+		bh.CompanyName = params[0].CompanyName
+		bh.CompanyIdentification = params[0].CompanyIdentification
+		bh.StandardEntryClassCode = params[0].StandardEntryClass
+		bh.CompanyEntryDescription = params[0].CompanyEntryDescription
+		bh.CompanyDescriptiveDate = params[0].CompanyDescriptiveDate
+		bh.EffectiveEntryDate = bh.parseSimpleDate(params[0].EffectiveEntryDate)
+		bh.ODFIIdentification = bh.parseNumField(params[0].ODFIIdentification)
+		return bh
+	}
+	return bh
 }
 
 // Parse takes the input record string and parses the BatchHeader values
@@ -132,7 +144,7 @@ func (bh *BatchHeader) Parse(record string) {
 	// 80-87 Your ODFI's routing number without the last digit. The last digit is simply a
 	// checksum digit, which is why it is not necessary
 	bh.ODFIIdentification = bh.parseNumField(record[79:87])
-	// 88-94 Sequential number of this Batch Header Recor
+	// 88-94 Sequential number of this Batch Header Record
 	// For example, put "1" if this is the first Batch Header Record in the file
 	bh.BatchNumber = bh.parseNumField(record[87:94])
 }
diff --git a/batchPPD.go b/batchPPD.go
index fcb30628..af7570dd 100644
--- a/batchPPD.go
+++ b/batchPPD.go
@@ -18,13 +18,20 @@ type BatchPPD struct {
 }
 
 // NewBatchPPD returns a *BatchPPD
-func NewBatchPPD() *BatchPPD {
+func NewBatchPPD(params ...BatchParam) *BatchPPD {
 	batch := new(BatchPPD)
+	batch.SetControl(NewBatchControl())
+
+	if len(params) > 0 {
+		bh := NewBatchHeader(params[0])
+		// override user SEC code for BatchPPD
+		bh.StandardEntryClassCode = ppd
+		batch.SetHeader(bh)
+		return batch
+	}
 	bh := NewBatchHeader()
 	bh.StandardEntryClassCode = ppd
 	batch.SetHeader(bh)
-	batch.SetControl(NewBatchControl())
-	batch.GetHeader().StandardEntryClassCode = ppd
 	return batch
 }
 
@@ -146,7 +153,7 @@ func (batch *BatchPPD) Build() error {
 
 	// validate checks that the above build covered all validation checks
 	if err := batch.ValidateAll(); err != nil {
-		return err // only errors if source code of build is inconcisstent with validate
+		return err // only errors if source code of batch.build is inconsistent with batch.validate
 	}
 	return nil
 }
@@ -249,7 +256,7 @@ func (batch *BatchPPD) isSequenceAscending() error {
 	return nil
 }
 
-// isEntryHash validates the hash by recalulating the result
+// isEntryHash validates the hash by recalculating the result
 func (batch *BatchPPD) isEntryHash() error {
 	hashField := batch.calculateEntryHash()
 	if hashField != batch.control.EntryHashField() {
diff --git a/batcher.go b/batcher.go
index 5b596d3f..ea479997 100644
--- a/batcher.go
+++ b/batcher.go
@@ -35,6 +35,18 @@ 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.
+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"`
+	CompanyEntryDescription string `json:"company_entry_description"`
+	CompanyDescriptiveDate  string `json:"company_descriptive_date"`
+	EffectiveEntryDate      string `json:"effective_entry_date"`
+	ODFIIdentification      string `json:"ODFI_identification"`
+}
+
 // Errors specific to parsing a Batch container
 var (
 	// generic messages
@@ -46,5 +58,5 @@ var (
 	msgBatchTraceNumberNotODFI = "%v in header does not match entry trace number %v"
 	msgBatchAddendaIndicator   = "is 0 but found addenda record(s)"
 	msgBatchAddendaTraceNumber = "%v does not match proceeding entry detail trace number %v"
-	msgBatchEntries            = "must have Entrie Record(s) to be built"
+	msgBatchEntries            = "must have Entry Record(s) to be built"
 )
diff --git a/entryDetail.go b/entryDetail.go
index 272c933f..d1a45a18 100644
--- a/entryDetail.go
+++ b/entryDetail.go
@@ -51,7 +51,7 @@ type EntryDetail struct {
 	// DiscretionaryData allows ODFIs to include codes, of significance only to them,
 	// to enable specialized handling of the entry. There will be no
 	// standardized interpretation for the value of this field. It can either
-	// be a single two-character code, or two distince one-character codes,
+	// be a single two-character code, or two distinct one-character codes,
 	// according to the needs of the ODFI and/or Originator involved. This
 	// field must be returned intact for any returned entry.
 	//
@@ -81,11 +81,35 @@ type EntryDetail struct {
 	converters
 }
 
+// EntryParam is the minimal fields required to make a ach entry
+type EntryParam struct {
+	ReceivingDFI      string `json:"receiving_dfi"`
+	RDFIAccount       string `json:"rdfi_account"`
+	Amount            string `json:"amount"`
+	IDNumber          string `json:"id_number,omitempty"`
+	IndividualName    string `json:"individual_name"`
+	DiscretionaryData string `json:"discretionary_data,omitempty"`
+	TransactionCode   string `json:"transaction_code"`
+}
+
 // NewEntryDetail returns a new EntryDetail with default values for none exported fields
-func NewEntryDetail() *EntryDetail {
-	return &EntryDetail{
+func NewEntryDetail(params ...EntryParam) *EntryDetail {
+	entry := &EntryDetail{
 		recordType: "6",
 	}
+	if len(params) > 0 {
+		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.DiscretionaryData = params[0].DiscretionaryData
+		entry.TransactionCode = entry.parseNumField(params[0].TransactionCode)
+
+		entry.setTraceNumber(entry.RDFIIdentification, 1)
+		return entry
+	}
+	return entry
 }
 
 // Parse takes the input record string and parses the EntryDetail values
@@ -200,7 +224,7 @@ func (ed *EntryDetail) AddAddenda(addenda Addenda) []Addenda {
 	return ed.Addendums
 }
 
-// SetRDFI takes the 9 digit RDFI account number and seperates it for RDFIIdentification and CheckDigit
+// SetRDFI takes the 9 digit RDFI account number and separates it for RDFIIdentification and CheckDigit
 func (ed *EntryDetail) SetRDFI(rdfi int) *EntryDetail {
 	s := ed.numericField(rdfi, 9)
 	ed.RDFIIdentification = ed.parseNumField(s[:8])
@@ -208,7 +232,7 @@ func (ed *EntryDetail) SetRDFI(rdfi int) *EntryDetail {
 	return ed
 }
 
-// setTraceNumber takes first 8 digits of RDFI and catinates a sequence number onto the TraceNumber
+// setTraceNumber takes first 8 digits of RDFI and concatenates a sequence number onto the TraceNumber
 func (ed *EntryDetail) setTraceNumber(RDFIIdentification int, seq int) {
 	trace := ed.numericField(RDFIIdentification, 8) + ed.numericField(seq, 7)
 	ed.TraceNumber = ed.parseNumField(trace)
diff --git a/file.go b/file.go
index 0291d6a1..f1f58c69 100644
--- a/file.go
+++ b/file.go
@@ -3,7 +3,7 @@
 // license that can be found in the LICENSE file.
 
 // Package ach reads and writes (ACH) Automated Clearing House files. ACH is the
-// primary method of electronic money movemenet through the United States.
+// primary method of electronic money movement through the United States.
 //
 // https://en.wikipedia.org/wiki/Automated_Clearing_House
 package ach
@@ -13,7 +13,7 @@ import (
 	"strconv"
 )
 
-// First position of all Record Types. These codes are uniquily assigned to
+// First position of all Record Types. These codes are uniquely assigned to
 // the first byte of each row in a file.
 const (
 	headerPos       = "1"
@@ -66,8 +66,25 @@ type File struct {
 	converters
 }
 
-// NewFile constucuts a file template.
-func NewFile() *File {
+// 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"`
+	ImmediateDestinationName string `json:"immediate_destination_name"`
+	ImmediateOriginName      string `json:"immediate_origin_name"`
+	ReferenceCode            string `json:"reference_code,omitempty"`
+}
+
+// NewFile constructs a file template.
+func NewFile(params ...FileParam) *File {
+	if len(params) > 0 {
+		fh := NewFileHeader(params[0])
+		return &File{
+			Header:  fh,
+			Control: NewFileControl(),
+		}
+
+	}
 	return &File{
 		Header:  NewFileHeader(),
 		Control: NewFileControl(),
@@ -162,7 +179,7 @@ func (f *File) Validate() error {
 	return nil
 }
 
-// ValidateAll walks the enture data structure and validates each record
+// ValidateAll walks the data structure and validates each record
 func (f *File) ValidateAll() error {
 
 	// validate inward out of the File Struct
@@ -218,7 +235,7 @@ func (f *File) isFileAmount() error {
 	return nil
 }
 
-// isEntryHash validates the hash by recalulating the result
+// isEntryHash validates the hash by recalculating the result
 func (f *File) isEntryHash() error {
 	hashField := f.calculateEntryHash()
 	if hashField != f.Control.EntryHashField() {
diff --git a/fileHeader.go b/fileHeader.go
index 8690c5b6..d5575f62 100644
--- a/fileHeader.go
+++ b/fileHeader.go
@@ -12,7 +12,7 @@ import (
 
 // Errors specific to a File Header Record
 var (
-	msgRecordType       = "recieved expecting %d"
+	msgRecordType       = "received expecting %d"
 	msgRecordSize       = "is not 094"
 	msgBlockingFactor   = "is not 10"
 	msgFormatCode       = "is not 1"
@@ -27,7 +27,7 @@ type FileHeader struct {
 	// RecordType defines the type of record in the block. headerPos
 	recordType string
 
-	// PriorityCode conists of the numerals 01
+	// PriorityCode consists of the numerals 01
 	priorityCode string
 
 	// ImmediateDestination contains the Routing Number of the ACH Operator or receiving
@@ -87,13 +87,13 @@ type FileHeader struct {
 	ReferenceCode string
 	// validator is composed for data validation
 	validator
-	// converters is composed for ACH to golang Converters
+	// converters is composed for ACH to GoLang Converters
 	converters
 }
 
 // NewFileHeader returns a new FileHeader with default values for none exported fields
-func NewFileHeader() FileHeader {
-	return FileHeader{
+func NewFileHeader(params ...FileParam) FileHeader {
+	fh := FileHeader{
 		recordType:     "1",
 		priorityCode:   "01",
 		FileIDModifier: "A",
@@ -101,6 +101,16 @@ func NewFileHeader() FileHeader {
 		blockingFactor: "10",
 		formatCode:     "1",
 	}
+	if len(params) > 0 {
+		fh.ImmediateDestination = fh.parseNumField(params[0].ImmediateDestination)
+		fh.ImmediateOrigin = fh.parseNumField(params[0].ImmediateOrigin)
+		fh.ImmediateDestinationName = params[0].ImmediateDestinationName
+		fh.ImmediateOriginName = params[0].ImmediateOriginName
+		fh.ReferenceCode = params[0].ReferenceCode
+		fh.FileCreationDate = time.Now()
+		return fh
+	}
+	return fh
 }
 
 // Parse takes the input record string and parses the FileHeader values
@@ -109,7 +119,7 @@ func (fh *FileHeader) Parse(record string) {
 	fh.recordType = "1"
 	// (2-3) Always "01"
 	fh.priorityCode = "01"
-	// (4-13) A blank space followed by your ODFI's routing numbefh. For example: " 121140399"
+	// (4-13) A blank space followed by your ODFI's routing number. For example: " 121140399"
 	fh.ImmediateDestination = fh.parseNumField(record[3:13])
 	// (14-23) A 10-digit number assigned to you by the ODFI once they approve you to originate ACH files through them
 	fh.ImmediateOrigin = fh.parseNumField(record[13:23])
@@ -235,12 +245,12 @@ func (fh *FileHeader) ImmediateDestinationField() string {
 	return " " + fh.numericField(fh.ImmediateDestination, 9)
 }
 
-// ImmediateOriginField gets the immediate origen number with 0 padding
+// ImmediateOriginField gets the immediate origin number with 0 padding
 func (fh *FileHeader) ImmediateOriginField() string {
 	return " " + fh.numericField(fh.ImmediateOrigin, 9)
 }
 
-// FileCreationDateField gets the file cereation date in YYMMDD format
+// FileCreationDateField gets the file creation date in YYMMDD format
 func (fh *FileHeader) FileCreationDateField() string {
 	return fh.formatSimpleDate(fh.FileCreationDate)
 }


Test patch:
diff --git a/addenda_internal_test.go b/addenda_internal_test.go
index 391f7bb6..c283f8e8 100644
--- a/addenda_internal_test.go
+++ b/addenda_internal_test.go
@@ -21,7 +21,7 @@ func TestMockAddenda(t *testing.T) {
 		t.Error("mockAddenda does not validate and will break other tests")
 	}
 	if addenda.EntryDetailSequenceNumber != 1234567 {
-		t.Error("EntryDetailSequenceNumber depedendent default value has changed")
+		t.Error("EntryDetailSequenceNumber dependent default value has changed")
 	}
 }
 
diff --git a/batchHeader_internal_test.go b/batchHeader_internal_test.go
index b4607755..cab77c8a 100644
--- a/batchHeader_internal_test.go
+++ b/batchHeader_internal_test.go
@@ -26,22 +26,22 @@ func TestMockBatchHeader(t *testing.T) {
 		t.Error("mockBatchHeader does not validate and will break other tests")
 	}
 	if bh.ServiceClassCode != 220 {
-		t.Error("ServiceClassCode depedendent default value has changed")
+		t.Error("ServiceClassCode dependent default value has changed")
 	}
 	if bh.StandardEntryClassCode != "PPD" {
-		t.Error("StandardEntryClassCode depedendent default value has changed")
+		t.Error("StandardEntryClassCode dependent default value has changed")
 	}
 	if bh.CompanyName != "ACME Corporation" {
-		t.Error("CompanyName depedendent default value has changed")
+		t.Error("CompanyName dependent default value has changed")
 	}
 	if bh.CompanyIdentification != "123456789" {
-		t.Error("CompanyIdentification depedendent default value has changed")
+		t.Error("CompanyIdentification dependent default value has changed")
 	}
 	if bh.CompanyEntryDescription != "PAYROLL" {
-		t.Error("CompanyEntryDescription depedendent default value has changed")
+		t.Error("CompanyEntryDescription dependent default value has changed")
 	}
 	if bh.ODFIIdentification != 6200001 {
-		t.Error("ODFIIdentification depedendent default value has changed")
+		t.Error("ODFIIdentification dependent default value has changed")
 	}
 }
 
diff --git a/entryDetail_internal_test.go b/entryDetail_internal_test.go
index 28e6ec07..48ff82b2 100644
--- a/entryDetail_internal_test.go
+++ b/entryDetail_internal_test.go
@@ -26,19 +26,19 @@ func TestMockEntryDetail(t *testing.T) {
 		t.Error("mockEntryDetail does not validate and will break other tests")
 	}
 	if entry.TransactionCode != 22 {
-		t.Error("TransactionCode depedendent default value has changed")
+		t.Error("TransactionCode dependent default value has changed")
 	}
 	if entry.DFIAccountNumber != "123456789" {
-		t.Error("DFIAccountNumber depedendent default value has changed")
+		t.Error("DFIAccountNumber dependent default value has changed")
 	}
 	if entry.Amount != 100000000 {
-		t.Error("Amount depedendent default value has changed")
+		t.Error("Amount dependent default value has changed")
 	}
 	if entry.IndividualName != "Wade Arnold" {
-		t.Error("IndividualName depedendent default value has changed")
+		t.Error("IndividualName dependent default value has changed")
 	}
 	if entry.TraceNumber != 123456789 {
-		t.Error("TraceNumber depedendent default value has changed")
+		t.Error("TraceNumber dependent default value has changed")
 	}
 }
 
diff --git a/reader_test.go b/reader_test.go
index 0724f1df..4476ac8f 100644
--- a/reader_test.go
+++ b/reader_test.go
@@ -42,7 +42,7 @@ func TestPPDDebitRead(t *testing.T) {
 }
 
 // TestDecode is a complete file decoding test. A canary test
-func TestPPDDebitFiexedLengthRead(t *testing.T) {
+func TestPPDDebitFixedLengthRead(t *testing.T) {
 	f, err := os.Open("./testdata/ppd-debit-fixedLength.ach")
 	if err != nil {
 		t.Errorf("%T: %s", err, err)
diff --git a/recordParams_test.go b/recordParams_test.go
new file mode 100644
index 00000000..2220d03d
--- /dev/null
+++ b/recordParams_test.go
@@ -0,0 +1,128 @@
+package ach
+
+import (
+	"os"
+	"testing"
+)
+
+func TestFileParam(t *testing.T) {
+	f := NewFile(
+		FileParam{
+			ImmediateDestination:     "0210000890",
+			ImmediateOrigin:          "123456789",
+			ImmediateDestinationName: "Your Bank",
+			ImmediateOriginName:      "Your Company",
+			ReferenceCode:            "#00000A1"})
+	if err := f.Header.Validate(); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+
+	if f.Header.ImmediateOriginName != "Your Company" {
+		t.Errorf("FileParam value was not copied to file.Header")
+	}
+}
+
+func TestBatchParam(t *testing.T) {
+	companyName := "Your Company"
+	batch := NewBatchPPD(BatchParam{
+		ServiceClassCode:        "220",
+		CompanyName:             companyName,
+		StandardEntryClass:      "PPD",
+		CompanyIdentification:   "123456789",
+		CompanyEntryDescription: "Trans. Description",
+		CompanyDescriptiveDate:  "Oct 23",
+		ODFIIdentification:      "123456789"})
+
+	if err := batch.header.Validate(); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+	if batch.header.CompanyName != companyName {
+		t.Errorf("BatchParam value was not copied to batch.header.CompanyName")
+	}
+}
+func TestEntryParam(t *testing.T) {
+	entry := NewEntryDetail(EntryParam{
+		ReceivingDFI:      "102001017",
+		RDFIAccount:       "5343121",
+		Amount:            "17500",
+		TransactionCode:   "22",
+		IDNumber:          "ABC##jvkdjfuiwn",
+		IndividualName:    "Bob Smith",
+		DiscretionaryData: "B1"})
+
+	if err := entry.Validate(); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+}
+
+func TestAddendaParam(t *testing.T) {
+	addenda := NewAddenda(AddendaParam{
+		PaymentRelatedInfo: "Currently string needs ASC X12 Interchange Control Structures",
+	})
+	if err := addenda.Validate(); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+}
+
+func TestBuildFileParam(t *testing.T) {
+	// To create a file
+	file := NewFile(FileParam{
+		ImmediateDestination:     "0210000890",
+		ImmediateOrigin:          "123456789",
+		ImmediateDestinationName: "Your Bank",
+		ImmediateOriginName:      "Your Company",
+		ReferenceCode:            "#00000A1"})
+
+	// To create a batch
+	batch := NewBatchPPD(BatchParam{
+		ServiceClassCode:        "220",
+		CompanyName:             "Your Company",
+		StandardEntryClass:      "PPD",
+		CompanyIdentification:   "123456789",
+		CompanyEntryDescription: "Trans. Description",
+		CompanyDescriptiveDate:  "Oct 23",
+		ODFIIdentification:      "123456789"})
+
+	// To create an entry
+	entry := NewEntryDetail(EntryParam{
+		ReceivingDFI:      "102001017",
+		RDFIAccount:       "5343121",
+		Amount:            "17500",
+		TransactionCode:   "22",
+		IDNumber:          "ABC##jvkdjfuiwn",
+		IndividualName:    "Bob Smith",
+		DiscretionaryData: "B1"})
+
+	// To add one or more optional addenda records for an entry
+
+	addenda := NewAddenda(AddendaParam{
+		PaymentRelatedInfo: "bonus pay for amazing work on #OSS"})
+	entry.AddAddenda(addenda)
+
+	// Entries are added to batches like so:
+
+	batch.AddEntry(entry)
+
+	// When all of the Entries are added to the batch we must build it.
+
+	if err := batch.Build(); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+
+	// And batches are added to files much the same way:
+
+	file.AddBatch(batch)
+
+	// Once we added all our batches we must build the file
+
+	if err := file.Build(); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+
+	// Finally we wnt to write the file to an io.Writer
+	w := NewWriter(os.Stdout)
+	if err := w.WriteAll([]*File{file}); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+	//w.Flush()
+}