Repository: moov-io/ach
Pull request #204 (2018-06-06 14:30:45)

#189 Support for SEC Code POP
#189 Support for SEC Code POP


Patch:
diff --git a/README.md b/README.md
index fc629343..30bbaa65 100644
--- a/README.md
+++ b/README.md
@@ -16,14 +16,15 @@ Package 'moov-io/ach' implements a file reader and writer for parsing [ACH](http
 ACH is under active development but already in production for multiple companies. Please star the project if you are interested in its progress.
 
 * Library currently supports the reading and writing 
-	* PPD (Prearranged payment and deposits)
-	* WEB (Internet-initiated Entries )
+	* ARC (Accounts Receivable Entry
+	* BOC (Back Office Conversion)
 	* CCD (Corporate credit or debit)
-	* TEL (Telephone-Initiated Entry)
 	* COR (Automated Notification of Change(NOC))
+	* POP (Point of Purchase)
+	* PPD (Prearranged payment and deposits)
 	* RCK (Represented Check Entries)
-	* BOC (Back Office Conversion)
-	* ARC (Accounts Receivable Entry)
+	* TEL (Telephone-Initiated Entry)
+	* WEB (Internet-initiated Entries)
 	* Return Entries
 
 
diff --git a/batch.go b/batch.go
index c78d23dd..6d9c7461 100644
--- a/batch.go
+++ b/batch.go
@@ -35,6 +35,8 @@ func NewBatch(bh *BatchHeader) (Batcher, error) {
 		return NewBatchCCD(bh), nil
 	case "COR":
 		return NewBatchCOR(bh), nil
+	case "POP":
+		return NewBatchPOP(bh), nil
 	case "PPD":
 		return NewBatchPPD(bh), nil
 	case "RCK":
diff --git a/batchPOP.go b/batchPOP.go
new file mode 100644
index 00000000..e150e243
--- /dev/null
+++ b/batchPOP.go
@@ -0,0 +1,95 @@
+// Copyright 2018 The ACH Authors
+// Use of this source code is governed by an Apache License
+// license that can be found in the LICENSE file.
+
+package ach
+
+import "fmt"
+
+// BatchPOP holds the BatchHeader and BatchControl and all EntryDetail for POP Entries.
+//
+// Point of Purchase. A check presented in-person to a merchant for purchase is presented
+// as an ACH entry instead of a physical check.
+//
+// This ACH debit application is used by originators as a method of payment for the
+// in-person purchase of goods or services by consumers. These Single Entry debit
+// entries are initiated by the originator based on a written authorization and
+// account information drawn from the source document (a check) obtained from the
+// consumer at the point-of-purchase. The source document, which is voided by the
+// merchant and returned to the consumer at the point-of-purchase, is used to
+// collect the consumer’s routing number, account number and check serial number that
+// will be used to generate the debit entry to the consumer’s account.
+//
+// The difference between POP and ARC is that ARC can result from a check mailed in whereas POP is in-person.
+type BatchPOP struct {
+	batch
+}
+
+// NewBatchPOP returns a *BatchPOP
+func NewBatchPOP(bh *BatchHeader) *BatchPOP {
+	batch := new(BatchPOP)
+	batch.SetControl(NewBatchControl())
+	batch.SetHeader(bh)
+	return batch
+}
+
+// Validate checks valid NACHA batch rules. Assumes properly parsed records.
+func (batch *BatchPOP) 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 POP cannot have an addenda record
+	if err := batch.isAddendaCount(0); err != nil {
+		return err
+	}
+
+	// Add type specific validation.
+	if batch.Header.StandardEntryClassCode != "POP" {
+		msg := fmt.Sprintf(msgBatchSECType, batch.Header.StandardEntryClassCode, "POP")
+		return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "StandardEntryClassCode", Msg: msg}
+	}
+
+	// RCK detail entries can only be a debit, ServiceClassCode must allow debits
+	switch batch.Header.ServiceClassCode {
+	case 200, 220, 280:
+		msg := fmt.Sprintf(msgBatchServiceClassCode, batch.Header.ServiceClassCode, "POP")
+		return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "ServiceClassCode", Msg: msg}
+	}
+
+	for _, entry := range batch.Entries {
+		// POP detail entries must be a debit
+		if entry.CreditOrDebit() != "D" {
+			msg := fmt.Sprintf(msgBatchTransactionCodeCredit, entry.TransactionCode)
+			return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TransactionCode", Msg: msg}
+		}
+
+		// Amount must be 25,000 or less
+		if entry.Amount > 2500000 {
+			msg := fmt.Sprintf(msgBatchAmount, "25,000", "POP")
+			return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Amount", Msg: msg}
+		}
+
+		// CheckSerialNumber, Terminal City, Terminal State underlying IdentificationNumber, must be defined
+		if entry.IdentificationNumber == "" {
+			msg := fmt.Sprintf(msgBatchCheckSerialNumber, "POP")
+			return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "CheckSerialNumber", Msg: msg}
+		}
+
+	}
+	return nil
+}
+
+// Create takes Batch Header and Entries and builds a valid batch
+func (batch *BatchPOP) Create() error {
+	// generates sequence numbers and batch control
+	if err := batch.build(); err != nil {
+		return err
+	}
+	// Additional steps specific to batch type
+	// ...
+
+	return batch.Validate()
+}
diff --git a/entryDetail.go b/entryDetail.go
index abb3cffe..8507bc7d 100644
--- a/entryDetail.go
+++ b/entryDetail.go
@@ -282,6 +282,45 @@ func (ed *EntryDetail) SetCheckSerialNumber(s string) {
 	ed.IdentificationNumber = s
 }
 
+// SetPOPCheckSerialNumber setter for POP CheckSerialNumber
+// which is characters 1-9 of underlying CheckSerialNumber \ IdentificationNumber
+func (ed *EntryDetail) SetPOPCheckSerialNumber(s string) {
+	ed.IdentificationNumber = ed.alphaField(s, 9)
+}
+
+// SetPOPTerminalCity setter for POP Terminal City
+// which is characters 10-13 of underlying CheckSerialNumber \ IdentificationNumber
+func (ed *EntryDetail) SetPOPTerminalCity(s string) {
+	ed.IdentificationNumber = ed.IdentificationNumber + ed.alphaField(s, 4)
+}
+
+// SetPOPTerminalState setter for POP Terminal State
+// which is characters 14-15 of underlying CheckSerialNumber \ IdentificationNumber
+func (ed *EntryDetail) SetPOPTerminalState(s string) {
+	ed.IdentificationNumber = ed.IdentificationNumber + ed.alphaField(s, 2)
+}
+
+// POPCheckSerialNumberField is used in POP, characters 1-9 of underlying BatchPOP
+// CheckSerialNumber / IdentificationNumber
+func (ed *EntryDetail) POPCheckSerialNumberField() string {
+	//return ed.alphaField(ed.IdentificationNumber, 9)
+	return ed.parseStringField(ed.IdentificationNumber[0:9])
+}
+
+// POPTerminalCityField is used in POP, characters 10-13 of underlying BatchPOP
+// CheckSerialNumber / IdentificationNumber
+func (ed *EntryDetail) POPTerminalCityField() string {
+	//return ed.alphaField(ed.IdentificationNumber, 9)
+	return ed.parseStringField(ed.IdentificationNumber[9:13])
+}
+
+// POPTerminalStateField is used in POP, characters 14-15 of underlying BatchPOP
+// CheckSerialNumber / IdentificationNumber
+func (ed *EntryDetail) POPTerminalStateField() string {
+	//return ed.alphaField(ed.IdentificationNumber, 9)
+	return ed.parseStringField(ed.IdentificationNumber[13:15])
+}
+
 // IndividualNameField returns a space padded string of IndividualName
 func (ed *EntryDetail) IndividualNameField() string {
 	return ed.alphaField(ed.IndividualName, 22)


Test patch:
diff --git a/batchPOP_test.go b/batchPOP_test.go
new file mode 100644
index 00000000..2935c539
--- /dev/null
+++ b/batchPOP_test.go
@@ -0,0 +1,431 @@
+// Copyright 2018 The ACH Authors
+// Use of this source code is governed by an Apache License
+// license that can be found in the LICENSE file.
+
+package ach
+
+import "testing"
+
+// mockBatchPOPHeader creates a BatchPOP BatchHeader
+func mockBatchPOPHeader() *BatchHeader {
+	bh := NewBatchHeader()
+	bh.ServiceClassCode = 225
+	bh.StandardEntryClassCode = "POP"
+	bh.CompanyName = "Payee Name"
+	bh.CompanyIdentification = "121042882"
+	bh.CompanyEntryDescription = "Point of Purchase"
+	bh.ODFIIdentification = "12104288"
+	return bh
+}
+
+// mockPOPEntryDetail creates a BatchPOP EntryDetail
+func mockPOPEntryDetail() *EntryDetail {
+	entry := NewEntryDetail()
+	entry.TransactionCode = 27
+	entry.SetRDFI("231380104")
+	entry.DFIAccountNumber = "744-5678-99"
+	entry.Amount = 25000
+	entry.SetPOPCheckSerialNumber("123456789")
+	entry.SetPOPTerminalCity("PHIL")
+	entry.SetPOPTerminalState("PA")
+	entry.SetReceivingCompany("ABC Company")
+	entry.SetTraceNumber(mockBatchPOPHeader().ODFIIdentification, 123)
+	entry.Category = CategoryForward
+	return entry
+}
+
+// mockBatchPOP creates a BatchPOP
+func mockBatchPOP() *BatchPOP {
+	mockBatch := NewBatchPOP(mockBatchPOPHeader())
+	mockBatch.AddEntry(mockPOPEntryDetail())
+	if err := mockBatch.Create(); err != nil {
+		panic(err)
+	}
+	return mockBatch
+}
+
+// mockBatchPOPHeaderCredit creates a BatchPOP BatchHeader
+func mockBatchPOPHeaderCredit() *BatchHeader {
+	bh := NewBatchHeader()
+	bh.ServiceClassCode = 225
+	bh.StandardEntryClassCode = "POP"
+	bh.CompanyName = "Payee Name"
+	bh.CompanyIdentification = "121042882"
+	bh.CompanyEntryDescription = "POP"
+	bh.ODFIIdentification = "12104288"
+	return bh
+}
+
+// mockPOPEntryDetailCredit creates a POP EntryDetail with a credit entry
+func mockPOPEntryDetailCredit() *EntryDetail {
+	entry := NewEntryDetail()
+	entry.TransactionCode = 22
+	entry.SetRDFI("231380104")
+	entry.DFIAccountNumber = "744-5678-99"
+	entry.Amount = 25000
+	entry.SetPOPCheckSerialNumber("123456789")
+	entry.SetPOPTerminalCity("PHIL")
+	entry.SetPOPTerminalState("PA")
+	entry.SetReceivingCompany("ABC Company")
+	entry.SetTraceNumber(mockBatchPOPHeader().ODFIIdentification, 123)
+	entry.Category = CategoryForward
+	return entry
+}
+
+// mockBatchPOPCredit creates a BatchPOP with a Credit entry
+func mockBatchPOPCredit() *BatchPOP {
+	mockBatch := NewBatchPOP(mockBatchPOPHeaderCredit())
+	mockBatch.AddEntry(mockPOPEntryDetailCredit())
+	return mockBatch
+}
+
+// testBatchPOPHeader creates a BatchPOP BatchHeader
+func testBatchPOPHeader(t testing.TB) {
+	batch, _ := NewBatch(mockBatchPOPHeader())
+	err, ok := batch.(*BatchPOP)
+	if !ok {
+		t.Errorf("Expecting BatchPOP got %T", err)
+	}
+}
+
+// TestBatchPOPHeader tests validating BatchPOP BatchHeader
+func TestBatchPOPHeader(t *testing.T) {
+	testBatchPOPHeader(t)
+}
+
+// BenchmarkBatchPOPHeader benchmarks validating BatchPOP BatchHeader
+func BenchmarkBatchPOPHeader(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testBatchPOPHeader(b)
+	}
+}
+
+// testBatchPOPCreate validates BatchPOP create
+func testBatchPOPCreate(t testing.TB) {
+	mockBatch := mockBatchPOP()
+	mockBatch.Create()
+	if err := mockBatch.Validate(); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+}
+
+// TestBatchPOPCreate tests validating BatchPOP create
+func TestBatchPOPCreate(t *testing.T) {
+	testBatchPOPCreate(t)
+}
+
+// BenchmarkBatchPOPCreate benchmarks validating BatchPOP create
+func BenchmarkBatchPOPCreate(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testBatchPOPCreate(b)
+	}
+}
+
+// testBatchPOPStandardEntryClassCode validates BatchPOP create for an invalid StandardEntryClassCode
+func testBatchPOPStandardEntryClassCode(t testing.TB) {
+	mockBatch := mockBatchPOP()
+	mockBatch.Header.StandardEntryClassCode = "WEB"
+	mockBatch.Create()
+	if err := mockBatch.Validate(); err != nil {
+		if e, ok := err.(*BatchError); ok {
+			if e.FieldName != "StandardEntryClassCode" {
+				t.Errorf("%T: %s", err, err)
+			}
+		} else {
+			t.Errorf("%T: %s", err, err)
+		}
+	}
+}
+
+// TestBatchPOPStandardEntryClassCode tests validating BatchPOP create for an invalid StandardEntryClassCode
+func TestBatchPOPStandardEntryClassCode(t *testing.T) {
+	testBatchPOPStandardEntryClassCode(t)
+}
+
+// BenchmarkBatchPOPStandardEntryClassCode benchmarks validating BatchPOP create for an invalid StandardEntryClassCode
+func BenchmarkBatchPOPStandardEntryClassCode(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testBatchPOPStandardEntryClassCode(b)
+	}
+}
+
+// testBatchPOPServiceClass200 validates BatchPOP create for an invalid ServiceClassCode 200
+func testBatchPOPServiceClass200(t testing.TB) {
+	mockBatch := mockBatchPOP()
+	mockBatch.Header.ServiceClassCode = 200
+	mockBatch.Create()
+	if err := mockBatch.Validate(); err != nil {
+		if e, ok := err.(*BatchError); ok {
+			if e.FieldName != "ServiceClassCode" {
+				t.Errorf("%T: %s", err, err)
+			}
+		} else {
+			t.Errorf("%T: %s", err, err)
+		}
+	}
+}
+
+// TestBatchPOPServiceClass200 tests validating BatchPOP create for an invalid ServiceClassCode 200
+func TestBatchPOPServiceClass200(t *testing.T) {
+	testBatchPOPServiceClass200(t)
+}
+
+// BenchmarkBatchPOPServiceClass200 benchmarks validating BatchPOP create for an invalid ServiceClassCode 200
+func BenchmarkBatchPOPServiceClass200(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testBatchPOPServiceClass200(b)
+	}
+}
+
+// testBatchPOPServiceClass220 validates BatchPOP create for an invalid ServiceClassCode 220
+func testBatchPOPServiceClass220(t testing.TB) {
+	mockBatch := mockBatchPOP()
+	mockBatch.Header.ServiceClassCode = 220
+	mockBatch.Create()
+	if err := mockBatch.Validate(); err != nil {
+		if e, ok := err.(*BatchError); ok {
+			if e.FieldName != "ServiceClassCode" {
+				t.Errorf("%T: %s", err, err)
+			}
+		} else {
+			t.Errorf("%T: %s", err, err)
+		}
+	}
+}
+
+// TestBatchPOPServiceClass220 tests validating BatchPOP create for an invalid ServiceClassCode 220
+func TestBatchPOPServiceClass220(t *testing.T) {
+	testBatchPOPServiceClass220(t)
+}
+
+// BenchmarkBatchPOPServiceClass220 benchmarks validating BatchPOP create for an invalid ServiceClassCode 220
+func BenchmarkBatchPOPServiceClass220(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testBatchPOPServiceClass220(b)
+	}
+}
+
+// testBatchPOPServiceClass280 validates BatchPOP create for an invalid ServiceClassCode 280
+func testBatchPOPServiceClass280(t testing.TB) {
+	mockBatch := mockBatchPOP()
+	mockBatch.Header.ServiceClassCode = 280
+	mockBatch.Create()
+	if err := mockBatch.Validate(); err != nil {
+		if e, ok := err.(*BatchError); ok {
+			if e.FieldName != "ServiceClassCode" {
+				t.Errorf("%T: %s", err, err)
+			}
+		} else {
+			t.Errorf("%T: %s", err, err)
+		}
+	}
+}
+
+// TestBatchPOPServiceClass280 tests validating BatchPOP create for an invalid ServiceClassCode 280
+func TestBatchPOPServiceClass280(t *testing.T) {
+	testBatchPOPServiceClass280(t)
+}
+
+// BenchmarkBatchPOPServiceClass280 benchmarks validating BatchPOP create for an invalid ServiceClassCode 280
+func BenchmarkBatchPOPServiceClass280(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testBatchPOPServiceClass280(b)
+	}
+}
+
+// testBatchPOPAmount validates BatchPOP create for an invalid Amount
+func testBatchPOPAmount(t testing.TB) {
+	mockBatch := mockBatchPOP()
+	mockBatch.Entries[0].Amount = 2600000
+	mockBatch.Create()
+	if err := mockBatch.Validate(); err != nil {
+		if e, ok := err.(*BatchError); ok {
+			if e.FieldName != "Amount" {
+				t.Errorf("%T: %s", err, err)
+			}
+		} else {
+			t.Errorf("%T: %s", err, err)
+		}
+	}
+}
+
+// TestBatchPOPAmount validates BatchPOP create for an invalid Amount
+func TestBatchPOPAmount(t *testing.T) {
+	testBatchPOPAmount(t)
+}
+
+// BenchmarkBatchPOPAmount validates BatchPOP create for an invalid Amount
+func BenchmarkBatchPOPAmount(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testBatchPOPAmount(b)
+	}
+}
+
+// testBatchPOPCheckSerialNumber validates BatchPOP CheckSerialNumber / IdentificationNumber is a mandatory field
+func testBatchPOPCheckSerialNumber(t testing.TB) {
+	mockBatch := mockBatchPOP()
+	// modify CheckSerialNumber / IdentificationNumber to nothing
+	mockBatch.GetEntries()[0].SetCheckSerialNumber("")
+	if err := mockBatch.Validate(); err != nil {
+		if e, ok := err.(*BatchError); ok {
+			if e.FieldName != "CheckSerialNumber" {
+				t.Errorf("%T: %s", err, err)
+			}
+		} else {
+			t.Errorf("%T: %s", err, err)
+		}
+	}
+}
+
+// TestBatchPOPCheckSerialNumber tests validating BatchPOP
+// CheckSerialNumber / IdentificationNumber is a mandatory field
+func TestBatchPOPCheckSerialNumber(t *testing.T) {
+	testBatchPOPCheckSerialNumber(t)
+}
+
+// BenchmarkBatchPOPCheckSerialNumber benchmarks validating BatchPOP
+// CheckSerialNumber / IdentificationNumber is a mandatory field
+func BenchmarkBatchPOPCheckSerialNumber(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testBatchPOPCheckSerialNumber(b)
+	}
+}
+
+// testBatchPOPCheckSerialNumberField validates POPCheckSerialNumberField characters 1-9 of underlying BatchPOP
+// CheckSerialNumber / IdentificationNumber
+func testBatchPOPCheckSerialNumberField(t testing.TB) {
+	mockBatch := mockBatchPOP()
+	tc := mockBatch.Entries[0].POPCheckSerialNumberField()
+	if tc != "123456789" {
+		t.Error("CheckSerialNumber is invalid")
+	}
+}
+
+// TestBatchPPOPCheckSerialNumberField tests validating POPCheckSerialNumberField characters 1-9 of underlying BatchPOP
+// CheckSerialNumber / IdentificationNumber
+func TestBatchPOPCheckSerialNumberField(t *testing.T) {
+	testBatchPOPCheckSerialNumberField(t)
+}
+
+// BenchmarkBatchPOPCheckSerialNumberField benchmarks validating POPCheckSerialNumberField characters 1-9 of underlying
+// BatchPOP CheckSerialNumber / IdentificationNumber
+func BenchmarkBatchPOPCheckSerialNumberField(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testBatchPOPTerminalCityField(b)
+	}
+}
+
+// testBatchPOPTerminalCityField validates POPTerminalCity characters 10-13 of underlying BatchPOP
+// CheckSerialNumber / IdentificationNumber
+func testBatchPOPTerminalCityField(t testing.TB) {
+	mockBatch := mockBatchPOP()
+	tc := mockBatch.Entries[0].POPTerminalCityField()
+	if tc != "PHIL" {
+		t.Error("TerminalCity is invalid")
+	}
+}
+
+// TestBatchPOPTerminalCityField tests validating POPTerminalCity characters 10-13 of underlying BatchPOP
+// CheckSerialNumber / IdentificationNumber
+func TestBatchPOPTerminalCityField(t *testing.T) {
+	testBatchPOPTerminalCityField(t)
+}
+
+// BenchmarkBatchPOPTerminalCityField benchmarks validating POPTerminalCity characters 10-13 of underlying
+// BatchPOP CheckSerialNumber / IdentificationNumber
+func BenchmarkBatchPOPTerminalCityField(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testBatchPOPTerminalCityField(b)
+	}
+}
+
+// testBatchPOPTerminalStateField validates POPTerminalState characters 14-15 of underlying BatchPOP
+// CheckSerialNumber / IdentificationNumber
+func testBatchPOPTerminalStateField(t testing.TB) {
+	mockBatch := mockBatchPOP()
+	ts := mockBatch.Entries[0].POPTerminalStateField()
+	if ts != "PA" {
+		t.Error("TerminalState is invalid")
+	}
+}
+
+// TestBatchPOPTerminalStateField tests validating POPTerminalState characters 14-15 of underlying BatchPOP
+// CheckSerialNumber / IdentificationNumber
+func TestBatchPOPTerminalStateField(t *testing.T) {
+	testBatchPOPTerminalStateField(t)
+}
+
+// BenchmarkBatchPOPTerminalStateField benchmarks validating POPTerminalState characters 14-15 of underlying
+// BatchPOP CheckSerialNumber / IdentificationNumber
+func BenchmarkBatchPOPTerminalStateField(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testBatchPOPTerminalStateField(b)
+	}
+}
+
+// testBatchPOPTransactionCode validates BatchPOP TransactionCode is not a credit
+func testBatchPOPTransactionCode(t testing.TB) {
+	mockBatch := mockBatchPOPCredit()
+	if err := mockBatch.Create(); err != nil {
+		if e, ok := err.(*BatchError); ok {
+			if e.FieldName != "TransactionCode" {
+				t.Errorf("%T: %s", err, err)
+			}
+		} else {
+			t.Errorf("%T: %s", err, err)
+		}
+	}
+}
+
+// TestBatchPOPTransactionCode tests validating BatchPOP TransactionCode is not a credit
+func TestBatchPOPTransactionCode(t *testing.T) {
+	testBatchPOPTransactionCode(t)
+}
+
+// BenchmarkBatchPOPTransactionCode benchmarks validating BatchPOP TransactionCode is not a credit
+func BenchmarkBatchPOPTransactionCode(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testBatchPOPTransactionCode(b)
+	}
+}
+
+// testBatchPOPAddendaCount validates BatchPOP Addenda count
+func testBatchPOPAddendaCount(t testing.TB) {
+	mockBatch := mockBatchPOP()
+	mockBatch.GetEntries()[0].AddAddenda(mockAddenda05())
+	mockBatch.Create()
+	if err := mockBatch.Validate(); err != nil {
+		if e, ok := err.(*BatchError); ok {
+			if e.FieldName != "AddendaCount" {
+				t.Errorf("%T: %s", err, err)
+			}
+		} else {
+			t.Errorf("%T: %s", err, err)
+		}
+	}
+}
+
+// TestBatchPOPAddendaCount tests validating BatchPOP Addenda count
+func TestBatchPOPAddendaCount(t *testing.T) {
+	testBatchPOPAddendaCount(t)
+}
+
+// BenchmarkBatchPOPAddendaCount benchmarks validating BatchPOP Addenda count
+func BenchmarkBatchPOPAddendaCount(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testBatchARCAddendaCount(b)
+	}
+}
diff --git a/test/ach-pop-read/main.go b/test/ach-pop-read/main.go
new file mode 100644
index 00000000..e47c1fbb
--- /dev/null
+++ b/test/ach-pop-read/main.go
@@ -0,0 +1,36 @@
+package main
+
+import (
+	"github.com/moov-io/ach"
+	"fmt"
+	"log"
+	"os"
+
+
+)
+
+func main() {
+	// open a file for reading. Any io.Reader Can be used
+	f, err := os.Open("pop-debit.ach")
+	if err != nil {
+		log.Panicf("Can not open file: %s: \n", err)
+	}
+	r := ach.NewReader(f)
+	achFile, err := r.Read()
+	if err != nil {
+		fmt.Printf("Issue reading file: %+v \n", err)
+	}
+	// ensure we have a validated file structure
+	if achFile.Validate(); err != nil {
+		fmt.Printf("Could not validate entire read file: %v", err)
+	}
+	// If you trust the file but it's formatting is off building will probably resolve the malformed file.
+	if achFile.Create(); err != nil {
+		fmt.Printf("Could not build file with read properties: %v", err)
+	}
+
+	fmt.Printf("total amount debit: %v \n", achFile.Control.TotalDebitEntryDollarAmountInFile)
+	fmt.Printf("POP check serial number: %v \n", achFile.Batches[0].GetEntries()[0].POPCheckSerialNumberField())
+	fmt.Printf("POP terminal city: %v \n", achFile.Batches[0].GetEntries()[0].POPTerminalCityField())
+	fmt.Printf("POP terminal state: %v \n", achFile.Batches[0].GetEntries()[0].POPTerminalStateField())
+}
diff --git a/test/ach-pop-read/main_test.go b/test/ach-pop-read/main_test.go
new file mode 100644
index 00000000..45fcc779
--- /dev/null
+++ b/test/ach-pop-read/main_test.go
@@ -0,0 +1,7 @@
+package main
+
+import "testing"
+
+func Test(t *testing.T) {
+	main()
+}
diff --git a/test/ach-pop-read/pop-debit.ach b/test/ach-pop-read/pop-debit.ach
new file mode 100644
index 00000000..61c75692
--- /dev/null
+++ b/test/ach-pop-read/pop-debit.ach
@@ -0,0 +1,10 @@
+101 231380104 1210428821806060000A094101Federal Reserve Bank   My Bank Name                   
+5225Name on Account                     121042882 POPACH POP         180607   0121042880000001
+62723138010412345678         0000250500123456   PHILPAWade Arnold             0121042880000001
+82250000010023138010000000250500000000000000121042882                          121042880000001
+9000001000001000000010023138010000000250500000000000000                                       
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
\ No newline at end of file
diff --git a/test/ach-pop-write/main.go b/test/ach-pop-write/main.go
new file mode 100644
index 00000000..5cc82233
--- /dev/null
+++ b/test/ach-pop-write/main.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+	"github.com/moov-io/ach"
+	"log"
+	"os"
+	"time"
+)
+
+func main() {
+	// Example transfer to write an ACH RCK file to debit a external institutions account
+	// Important: All financial institutions are different and will require registration and exact field values.
+
+	fh := ach.NewFileHeader()
+	fh.ImmediateDestination = "231380104" // Routing Number of the ACH Operator or receiving point to which the file is being sent
+	fh.ImmediateOrigin = "121042882"      // Routing Number of the ACH Operator or sending point that is sending the file
+	fh.FileCreationDate = time.Now()      // Today's Date
+	fh.ImmediateDestinationName = "Federal Reserve Bank"
+	fh.ImmediateOriginName = "My Bank Name"
+
+	// BatchHeader identifies the originating entity and the type of transactions contained in the batch
+	bh := ach.NewBatchHeader()
+	bh.ServiceClassCode = 225          // ACH credit pushes money out, 225 debits/pulls money in.
+	bh.CompanyName = "Name on Account" // The name of the company/person that has relationship with receiver
+	bh.CompanyIdentification = fh.ImmediateOrigin
+	bh.StandardEntryClassCode = "POP"         // Consumer destination vs Company CCD
+	bh.CompanyEntryDescription = "ACH POP" // will be on receiving accounts statement
+	bh.EffectiveEntryDate = time.Now().AddDate(0, 0, 1)
+	bh.ODFIIdentification = "121042882" // Originating Routing Number
+
+	// Identifies the receivers account information
+	// can be multiple entry's per batch
+	entry := ach.NewEntryDetail()
+	// Identifies the entry as a debit and credit entry AND to what type of account (Savings, DDA, Loan, GL)
+	entry.TransactionCode = 27          // Code 27: Debit (withdrawal) from checking account
+	entry.SetRDFI("231380104")          // Receivers bank transit routing number
+	entry.DFIAccountNumber = "12345678" // Receivers bank account number
+	entry.Amount = 250500                 // Amount of transaction with no decimal. One dollar and eleven cents = 111
+	entry.SetTraceNumber(bh.ODFIIdentification, 1)
+	entry.IndividualName = "Wade Arnold" // Identifies the receiver of the transaction
+	entry.SetPOPCheckSerialNumber("123456")
+	entry.SetPOPTerminalCity("PHIL")
+	entry.SetPOPTerminalState("PA")
+
+	// build the batch
+	batch := ach.NewBatchPOP(bh)
+	batch.AddEntry(entry)
+	if err := batch.Create(); err != nil {
+		log.Fatalf("Unexpected error building batch: %s\n", err)
+	}
+
+	// build the file
+	file := ach.NewFile()
+	file.SetHeader(fh)
+	file.AddBatch(batch)
+	if err := file.Create(); err != nil {
+		log.Fatalf("Unexpected error building file: %s\n", err)
+	}
+
+	// write the file to std out. Anything io.Writer
+	w := ach.NewWriter(os.Stdout)
+	if err := w.WriteAll([]*ach.File{file}); err != nil {
+		log.Fatalf("Unexpected error: %s\n", err)
+	}
+	w.Flush()
+}
diff --git a/test/ach-pop-write/main_test.go b/test/ach-pop-write/main_test.go
new file mode 100644
index 00000000..45fcc779
--- /dev/null
+++ b/test/ach-pop-write/main_test.go
@@ -0,0 +1,7 @@
+package main
+
+import "testing"
+
+func Test(t *testing.T) {
+	main()
+}