Repository: moov-io/ach
Pull request #141 (2017-10-17 23:01:59)

Implement COR SEC   Return File Processing
Thanks @killianbrackey and @Sezzle for all the work in adding support.

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

Coverage decreased (-4.7%) to 91.24% when pulling **477e545ce8baeac39b07bff171bf4b0825a3329f on wadearnold:Implement-COR-SEC---Return-File-Processing** into **81e7c97d7e5accfe1b5bbcea3e143a4124f6d9db on moov-io:master**.

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

Coverage decreased (-4.9%) to 91.024% when pulling **0f42d771e1f38649a977e42bf49ee8d3ff033b1b on wadearnold:Implement-COR-SEC---Return-File-Processing** into **81e7c97d7e5accfe1b5bbcea3e143a4124f6d9db on moov-io:master**.

wadearnold: The deeper I jump into this rabbit hole the more I think we need a Addenda, AddendaReturn, AddendaCOR, AddendaIAT, etc like we have done with Batch. Just talking to myself publicly. 
coveralls: 
[![Coverage Status](https://coveralls.io/builds/13785689/badge)](https://coveralls.io/builds/13785689)

Coverage decreased (-4.4%) to 91.578% when pulling **9e1072f996188ac51d05f5c05baef71ae5f52cb7 on wadearnold:Implement-COR-SEC---Return-File-Processing** into **81e7c97d7e5accfe1b5bbcea3e143a4124f6d9db on moov-io:master**.

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

Coverage decreased (-3.3%) to 92.614% when pulling **6cb4ddec6c60e80bb33bc7bd65fe278119d8092d on wadearnold:Implement-COR-SEC---Return-File-Processing** into **81e7c97d7e5accfe1b5bbcea3e143a4124f6d9db on moov-io:master**.

wadearnold: Adding changes to master that support @zelle changes. I am working on a new branch that address the Addenda types such as NOC, IAT, etc that need to eventually be supported. @killianbrackey you may need to comment out the ReturnAddenda validator on the ReturnCodeDict so that this works for BatchCOR/NOC until I get the second phase of this refactor committed. 

Patch:
diff --git a/addenda.go b/addenda.go
index a818e6a5..9a8b0157 100644
--- a/addenda.go
+++ b/addenda.go
@@ -63,7 +63,6 @@ func (addenda *Addenda) Parse(record string) {
 	addenda.SequenceNumber = addenda.parseNumField(record[83:87])
 	// 88-94 Contains the last seven digits of the number entered in the Trace Number field in the corresponding Entry Detail Record
 	addenda.EntryDetailSequenceNumber = addenda.parseNumField(record[87:94])
-
 }
 
 // String writes the Addenda struct to a 94 character string.
diff --git a/batch.go b/batch.go
index 2365585d..089e80cd 100644
--- a/batch.go
+++ b/batch.go
@@ -20,6 +20,8 @@ func NewBatch(bp BatchParam) (Batcher, error) {
 		return NewBatchWEB(bp), nil
 	case "CCD":
 		return NewBatchCCD(bp), nil
+	case "COR":
+		return NewBatchCOR(bp), nil
 	default:
 		msg := fmt.Sprintf(msgFileNoneSEC, sec)
 		return nil, &FileError{FieldName: "StandardEntryClassCode", Msg: msg}
@@ -185,7 +187,7 @@ func (batch *batch) isFieldInclusion() error {
 func (batch *batch) isBatchEntryCount() error {
 	entryCount := 0
 	for _, entry := range batch.entries {
-		entryCount = entryCount + 1 + len(entry.Addendum)
+		entryCount = entryCount + 1 + len(entry.Addendum) + len(entry.ReturnAddendum)
 	}
 	if entryCount != batch.control.EntryAddendaCount {
 		msg := fmt.Sprintf(msgBatchCalculatedControlEquality, entryCount, batch.control.EntryAddendaCount)
@@ -213,18 +215,10 @@ func (batch *batch) isBatchAmount() error {
 
 func (batch *batch) calculateBatchAmounts() (credit int, debit int) {
 	for _, entry := range batch.entries {
-		if entry.TransactionCode == 22 || entry.TransactionCode == 23 {
+		if entry.TransactionCode == 21 || entry.TransactionCode == 22 || entry.TransactionCode == 23 || entry.TransactionCode == 32 || entry.TransactionCode == 33 {
 			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 {
+		if entry.TransactionCode == 26 || entry.TransactionCode == 27 || entry.TransactionCode == 28 || entry.TransactionCode == 36 || entry.TransactionCode == 37 || entry.TransactionCode == 38 {
 			debit = debit + entry.Amount
 		}
 	}
@@ -323,9 +317,16 @@ func (batch *batch) isAddendaSequence() error {
 // "PPD", "WEB", "CCD", "CIE", "DNE", "MTE", "POS", "SHR"
 func (batch *batch) isAddendaCount(count int) error {
 	for _, entry := range batch.entries {
-		if len(entry.Addendum) > count {
-			msg := fmt.Sprintf(msgBatchAddendaCount, len(entry.Addendum), count, batch.header.StandardEntryClassCode)
-			return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "AddendaCount", Msg: msg}
+		if !entry.HasReturnAddenda() {
+			if len(entry.Addendum) > count {
+				msg := fmt.Sprintf(msgBatchAddendaCount, len(entry.Addendum), count, batch.header.StandardEntryClassCode)
+				return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "AddendaCount", Msg: msg}
+			}
+		} else {
+			if len(entry.ReturnAddendum) > count {
+				msg := fmt.Sprintf(msgBatchAddendaCount, len(entry.ReturnAddendum), count, batch.header.StandardEntryClassCode)
+				return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "ReturnAddendaCount", Msg: msg}
+			}
 		}
 	}
 	return nil
diff --git a/batchCOR.go b/batchCOR.go
new file mode 100644
index 00000000..6663b408
--- /dev/null
+++ b/batchCOR.go
@@ -0,0 +1,66 @@
+package ach
+
+import (
+	"fmt"
+)
+
+// BatchCOR COR - Automated Notification of Change or Refused Notification of Change
+// This Standard Entry Class Code is used by an RDFI or ODFI when originating a Notification of Change or Refused Notification of Change in automated format.
+// It is also used by the ACH operator that converts paper Notifications of Change to automated format.
+type BatchCOR struct {
+	batch
+}
+
+// NewBatchCOR returns a *BatchCOR
+func NewBatchCOR(params ...BatchParam) *BatchCOR {
+	batch := new(BatchCOR)
+	batch.SetControl(NewBatchControl())
+
+	if len(params) > 0 {
+		bh := NewBatchHeader(params[0])
+		bh.StandardEntryClassCode = "COR"
+		batch.SetHeader(bh)
+		return batch
+	}
+	bh := NewBatchHeader()
+	bh.StandardEntryClassCode = "COR"
+	batch.SetHeader(bh)
+	return batch
+}
+
+// Validate ensures the batch meets NACHA rules specific to this batch type.
+func (batch *BatchCOR) Validate() error {
+	// basic verification of the batch before we validate specific rules.
+	if err := batch.verify(); err != nil {
+		return err
+	}
+	// Add configuration based validation for this type.
+	// Web can have up to one addenda per entry record
+	if err := batch.isAddendaCount(1); err != nil {
+		return err
+	}
+	if err := batch.isTypeCode("05"); err != nil {
+		return err
+	}
+
+	// Add type specific validation.
+	if batch.header.StandardEntryClassCode != "COR" {
+		msg := fmt.Sprintf(msgBatchSECType, batch.header.StandardEntryClassCode, "COR")
+		return &BatchError{BatchNumber: batch.header.BatchNumber, FieldName: "StandardEntryClassCode", Msg: msg}
+	}
+
+	return nil
+}
+
+// Create builds the batch sequence numbers and batch control. Additional creation
+func (batch *BatchCOR) Create() error {
+	// generates sequence numbers and batch control
+	if err := batch.build(); err != nil {
+		return err
+	}
+
+	if err := batch.Validate(); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/entryDetail.go b/entryDetail.go
index 6fc6b26e..d4b7915b 100644
--- a/entryDetail.go
+++ b/entryDetail.go
@@ -76,6 +76,8 @@ type EntryDetail struct {
 
 	// Addendum a list of Addenda for the Entry Detail
 	Addendum []Addenda
+	// ReturnAddendum stores COR sec return types. These are processed separately
+	ReturnAddendum []ReturnAddenda
 	// validator is composed for data validation
 	validator
 	// converters is composed for ACH to golang Converters
@@ -217,9 +219,10 @@ func (ed *EntryDetail) fieldInclusion() error {
 	if ed.DFIAccountNumber == "" {
 		return &FieldError{FieldName: "DFIAccountNumber", Value: ed.DFIAccountNumber, Msg: msgFieldInclusion}
 	}
-	if ed.Amount == 0 {
+	// TODO: amount can be 0 if it's COR, should probably be more specific...
+	/*if ed.Amount == 0 {
 		return &FieldError{FieldName: "Amount", Value: ed.AmountField(), Msg: msgFieldInclusion}
-	}
+	}*/
 	if ed.IndividualName == "" {
 		return &FieldError{FieldName: "IndividualName", Value: ed.IndividualName, Msg: msgFieldInclusion}
 	}
@@ -232,10 +235,25 @@ func (ed *EntryDetail) fieldInclusion() error {
 // AddAddenda appends an EntryDetail to the Addendum
 func (ed *EntryDetail) AddAddenda(addenda Addenda) []Addenda {
 	ed.AddendaRecordIndicator = 1
+	// checks to make sure that we only have either or, not both
+	if ed.ReturnAddendum != nil {
+		return nil
+	}
 	ed.Addendum = append(ed.Addendum, addenda)
 	return ed.Addendum
 }
 
+// AddReturnAddenda appends an ReturnAddendum to the entry
+func (ed *EntryDetail) AddReturnAddenda(returnAddendum ReturnAddenda) []ReturnAddenda {
+	ed.AddendaRecordIndicator = 1
+	// checks to make sure that we only have either or, not both
+	if ed.Addendum != nil {
+		return nil
+	}
+	ed.ReturnAddendum = append(ed.ReturnAddendum, returnAddendum)
+	return ed.ReturnAddendum
+}
+
 // 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)
@@ -280,6 +298,7 @@ func (ed *EntryDetail) ReceivingCompanyField() string {
 	return ed.IndividualNameField()
 }
 
+// SetReceivingCompany setter for CCD receiving company individual name
 func (ed *EntryDetail) SetReceivingCompany(s string) {
 	ed.IndividualName = s
 }
@@ -289,7 +308,7 @@ func (ed *EntryDetail) DiscretionaryDataField() string {
 	return ed.alphaField(ed.DiscretionaryData, 2)
 }
 
-// PaymentType returns the discretionary data field used in WEB batch files
+// PaymentTypeField returns the discretionary data field used in WEB batch files
 func (ed *EntryDetail) PaymentTypeField() string {
 	// because DiscretionaryData can be changed outside of PaymentType we reset the value for safety
 	ed.SetPaymentType(ed.DiscretionaryData)
@@ -310,3 +329,8 @@ func (ed *EntryDetail) SetPaymentType(t string) {
 func (ed *EntryDetail) TraceNumberField() string {
 	return ed.numericField(ed.TraceNumber, 15)
 }
+
+// HasReturnAddenda returns true if entry has return addenda
+func (ed *EntryDetail) HasReturnAddenda() bool {
+	return ed.ReturnAddendum != nil
+}
diff --git a/file.go b/file.go
index 0da9082c..b4c1a6eb 100644
--- a/file.go
+++ b/file.go
@@ -30,6 +30,9 @@ const (
 // currently supported SEC codes
 const (
 	ppd = "PPD"
+	web = "WEB"
+	ccd = "CCD"
+	cor = "COR"
 )
 
 // Errors strings specific to parsing a Batch container
diff --git a/reader.go b/reader.go
index a99b9ead..fb986145 100644
--- a/reader.go
+++ b/reader.go
@@ -248,6 +248,18 @@ func (r *Reader) parseAddenda() error {
 			msg := fmt.Sprintf(msgBatchAddendaIndicator)
 			return r.error(&FileError{FieldName: "AddendaRecordIndicator", Msg: msg})
 		}
+	case web, ccd, cor: // only care for returns
+		if entry.AddendaRecordIndicator == 1 {
+			returnAddenda := ReturnAddenda{}
+			returnAddenda.Parse(r.line)
+			if err := returnAddenda.Validate(); err != nil {
+				return r.error(err)
+			}
+			r.currentBatch.GetEntries()[entryIndex].AddReturnAddenda(returnAddenda)
+		} else {
+			msg := fmt.Sprintf(msgBatchAddendaIndicator)
+			return r.error(&FileError{FieldName: "AddendaRecordIndicator", Msg: msg})
+		}
 	}
 
 	return nil
diff --git a/returnAddenda.go b/returnAddenda.go
new file mode 100644
index 00000000..16b301d9
--- /dev/null
+++ b/returnAddenda.go
@@ -0,0 +1,201 @@
+package ach
+
+import (
+	"fmt"
+	"strings"
+	"time"
+)
+
+// When a Return Entry is prepared, the original Company/Batch Header Record, the original Entry Detail Record,
+// and the Company/Batch Control Record are copied for return to the Originator.
+//
+// The Return Entry is a new Entry. These Entries must be assigned new batch and trace numbers, new identification numbers for the returning institution,
+// appropriate transaction codes, etc., as required per format specifications.
+//
+// See Appendix Four: Return Entries in the NACHA Corporate
+
+var (
+	returnCodeDict = map[string]*returnCode{}
+
+	// Error messages specific to Return Addenda
+	msgReturnAddendaReturnCode = "found is not a valid return code"
+)
+
+func init() {
+	// populate the returnCode map with lookup values
+	returnCodeDict = makeReturnCodeDict()
+}
+
+// ReturnAddenda utilized for Notification of Change Entry (COR) and Return types.
+type ReturnAddenda struct {
+	// RecordType defines the type of record in the block. entryAddendaPos 7
+	recordType string
+	// TypeCode Addenda types code '99'
+	TypeCode string
+	// ReturnCode field contains a standard code used by an ACH Operator or RDFI to describe the reason for returning an Entry.
+	// Must exist in returnCodeDict
+	ReturnCode string
+	// OriginalTrace This field contains the Trace Number as originally included on the forward Entry or Prenotification.
+	// The RDFI must include the Original Entry Trace Number in the Addenda Record of an Entry being returned to an ODFI,
+	// in the Addenda Record of an NOC, within an Acknowledgment Entry, or with an RDFI request for a copy of an authorization.
+	OriginalTrace int
+	// DateOfDeath The field date of death is to be supplied on Entries being returned for reason of death (return reason codes R14 and R15).
+	DateOfDeath time.Time
+	// OriginalDFI field contains the Receiving DFI Identification (addenda.RDFIIdentification) as originally included on the forward Entry or Prenotification that the RDFI is returning or correcting.
+	OriginalDFI int
+	// AddendaInformation
+	AddendaInformation string
+	// TraceNumber matches the Entry Detail Trace Number of the entry being returned.
+	TraceNumber int
+
+	// validator is composed for data validation
+	validator
+	// converters is composed for ACH to GoLang Converters
+	converters
+}
+
+// returnCode holds a return Code, Reason/Title, and Description
+//
+// Table of return codes exists in Part 4.2 of the NACHA corporate rules and guidelines
+type returnCode struct {
+	Code, Reason, Description string
+}
+
+// NewReturnAddenda returns a new ReturnAddenda with default values for none exported fields
+func NewReturnAddenda(params ...AddendaParam) ReturnAddenda {
+	rAddenda := ReturnAddenda{
+		recordType: "7",
+		TypeCode:   "99",
+	}
+	return rAddenda
+}
+
+// Parse takes the input record string and parses the ReturnAddenda values
+func (returnAddenda *ReturnAddenda) Parse(record string) {
+	// 1-1 Always "7"
+	returnAddenda.recordType = "7"
+	// 2-3 Defines the specific explanation and format for the addenda information contained in the same record
+	returnAddenda.TypeCode = record[1:3]
+	// 4-6
+	returnAddenda.ReturnCode = record[3:6]
+	// 7-21
+	returnAddenda.OriginalTrace = returnAddenda.parseNumField(record[6:21])
+	// 22-27, might be a date or blank
+	returnAddenda.DateOfDeath = returnAddenda.parseSimpleDate(record[21:27])
+	// 28-35
+	returnAddenda.OriginalDFI = returnAddenda.parseNumField(record[27:35])
+	// 36-79
+	returnAddenda.AddendaInformation = strings.TrimSpace(record[35:79])
+	// 80-94
+	returnAddenda.TraceNumber = returnAddenda.parseNumField(record[79:94])
+}
+
+// String writes the ReturnAddenda struct to a 94 character string
+func (returnAddenda *ReturnAddenda) String() string {
+	return fmt.Sprintf("%v%v%v%v%v%v%v%v",
+		returnAddenda.recordType,
+		returnAddenda.TypeCode,
+		returnAddenda.ReturnCode,
+		returnAddenda.OriginalTraceField(),
+		returnAddenda.DateOfDeathField(),
+		returnAddenda.OriginalDFIField(),
+		returnAddenda.AddendaInformationField(),
+		returnAddenda.TraceNumberField(),
+	)
+}
+
+// Validate verifies NACHA rules for ReturnAddenda
+func (returnAddenda *ReturnAddenda) Validate() error {
+
+	if returnAddenda.recordType != "7" {
+		msg := fmt.Sprintf(msgRecordType, 7)
+		return &FieldError{FieldName: "recordType", Value: returnAddenda.recordType, Msg: msg}
+	}
+	// @TODO Type Code should be 99.
+
+	_, ok := returnCodeDict[returnAddenda.ReturnCode]
+	if !ok {
+		// Return Addenda requires a valid ReturnCode
+		return &FieldError{FieldName: "ReturnCode", Value: returnAddenda.ReturnCode, Msg: msgReturnAddendaReturnCode}
+	}
+	return nil
+}
+
+// OriginalTraceField returns a zero padded OriginalTrace string
+func (returnAddenda *ReturnAddenda) OriginalTraceField() string {
+	return returnAddenda.numericField(returnAddenda.OriginalTrace, 15)
+}
+
+// DateOfDeathField returns a space padded DateOfDeath string
+func (returnAddenda *ReturnAddenda) DateOfDeathField() string {
+	// Return space padded 6 characters if it is a zero value of DateOfDeath
+	if returnAddenda.DateOfDeath.IsZero() {
+		return returnAddenda.alphaField("", 6)
+	}
+	// YYMMDD
+	return returnAddenda.formatSimpleDate(returnAddenda.DateOfDeath)
+}
+
+// OriginalDFIField returns a zero padded OriginalDFI string
+func (returnAddenda *ReturnAddenda) OriginalDFIField() string {
+	return returnAddenda.numericField(returnAddenda.OriginalDFI, 8)
+}
+
+//AddendaInformationField returns a space padded AddendaInformation string
+func (returnAddenda *ReturnAddenda) AddendaInformationField() string {
+	return returnAddenda.alphaField(returnAddenda.AddendaInformation, 44)
+}
+
+// TraceNumberField returns a zero padded traceNumber string
+func (returnAddenda *ReturnAddenda) TraceNumberField() string {
+	return returnAddenda.numericField(returnAddenda.TraceNumber, 15)
+}
+
+func makeReturnCodeDict() map[string]*returnCode {
+	dict := make(map[string]*returnCode)
+
+	codes := []returnCode{
+		// Return Reason Codes for RDFIs
+		{"R01", "Insufficient Funds", "Available balance is not sufficient to cover the dollar value of the debit entry"},
+		{"R02", "Account Closed", "Previously active account has been closed by customer or RDFI"},
+		{"R03", "No Account/Unable to Locate Account", "Account number structure is valid and passes editing process, but does not correspond to individual or is not an open account"},
+		{"R04", "Invalid Account Number", "Account number structure not valid; entry may fail check digit validation or may contain an incorrect number of digits."},
+		{"R05", "Improper Debit to Consumer Account", "A CCD, CTX, or CBR debit entry was transmitted to a Consumer Account of the Receiver and was not authorized by the Receiver"},
+		{"R06", "Returned per ODFI's Request", "ODFI has requested RDFI to return the ACH entry (optional to RDFI - ODFI indemnifies RDFI)}"},
+		{"R07", "Authorization Revoked by Customer", "Consumer, who previously authorized ACH payment, has revoked authorization from Originator (must be returned no later than 60 days from settlement date and customer must sign affidavit)"},
+		{"R08", "Payment Stopped", "Receiver of a recurring debit transaction has stopped payment to a specific ACH debit. RDFI should verify the Receiver's intent when a request for stop payment is made to insure this is not intended to be a revocation of authorization"},
+		{"R09", "Uncollected Funds", "Sufficient book or ledger balance exists to satisfy dollar value of the transaction, but the dollar value of transaction is in process of collection (i.e., uncollected checks) or cash reserve balance below dollar value of the debit entry."},
+		{"R10", "Customer Advises Not Authorized", "Consumer has advised RDFI that Originator of transaction is not authorized to debit account (must be returned no later than 60 days from settlement date of original entry and customer must sign affidavit)."},
+		{"R11", "Check Truncation Entry Returned", "Used when returning a check safekeeping entry; RDFI should use appropriate field in addenda record to specify reason for return (i.e., 'exceeds dollar limit,' 'stale date,' etc.)."},
+		{"R12", "Branch Sold to Another DFI", "Financial institution receives entry destined for an account at a branch that has been sold to another financial institution."},
+		{"R13", "RDFI not qualified to participate", "Financial institution does not receive commercial ACH entries"},
+		{"R14", "Representative payee deceased or unable to continue in that capacity", "The representative payee authorized to accept entries on behalf of a beneficiary is either deceased or unable to continue in that capacity"},
+		{"R15", "Beneficiary or bank account holder", "(Other than representative payee) deceased* - (1) the beneficiary entitled to payments is deceased or (2) the bank account holder other than a representative payee is deceased"},
+		{"R16", "Bank account frozen", "Funds in bank account are unavailable due to action by RDFI or legal order"},
+		{"R17", "File record edit criteria", "Fields rejected by RDFI processing (identified in return addenda)"},
+		{"R18", "Improper effective entry date", "Entries have been presented prior to the first available processing window for the effective date."},
+		{"R19", "Amount field error", "Improper formatting of the amount field"},
+		{"R20", "Non-payment bank account", "Entry destined for non-payment bank account defined by reg."},
+		{"R21", "Invalid company ID number", "The company ID information not valid (normally CIE entries)"},
+		{"R22", "Invalid individual ID number", "Individual id used by receiver is incorrect (CIE entries)"},
+		{"R23", "Credit entry refused by receiver", "Receiver returned entry because minimum or exact amount not remitted, bank account is subject to litigation, or payment represents an overpayment, originator is not known to receiver or receiver has not authorized this credit entry to this bank account"},
+		{"R24", "Duplicate entry", "RDFI has received a duplicate entry"},
+		{"R25", "Addenda error", "Improper formatting of the addenda record information"},
+		{"R26", "Mandatory field error", "Improper information in one of the mandatory fields"},
+		{"R27", "Trace number error", "Original entry trace number is not valid for return entry; or addenda trace numbers do not correspond with entry detail record"},
+		{"R28", "Transit routing number check digit error", "Check digit for the transit routing number is incorrect"},
+		{"R29", "Corporate customer advises not authorized", "RDFI has bee notified by corporate receiver that debit entry of originator is not authorized"},
+		{"R30", "RDFI not participant in check truncation program", "Financial institution not participating in automated check safekeeping application"},
+		{"R31", "Permissible return entry (CCD and CTX only)", "RDFI has been notified by the ODFI that it agrees to accept a CCD or CTX return entry"},
+		{"R32", "RDFI non-settlement", "RDFI is not able to settle the entry"},
+		{"R33", "Return of XCK entry", "RDFI determines at its sole discretion to return an XCK entry; an XCK return entry may be initiated by midnight of the sixtieth day following the settlement date if the XCK entry"},
+		{"R34", "Limited participation RDFI", "RDFI participation has been limited by a federal or state supervisor"},
+		{"R35", "Return of improper debit entry", "ACH debit not permitted for use with the CIE standard entry class code (except for reversals)"},
+		// More return codes will be added when more SEC types are added to the library.
+	}
+	// populate the map
+	for _, code := range codes {
+		dict[code.Code] = &code
+	}
+	return dict
+}
diff --git a/validators.go b/validators.go
index 597d940e..a5a167c6 100644
--- a/validators.go
+++ b/validators.go
@@ -107,6 +107,8 @@ func (v *validator) isTransactionCode(code int) error {
 		23,
 		// Zero dollar with remittance data (CCD/CTX only)
 		24,
+		// Automated Return or Notification of Change for original transaction code 27, 28, or 29
+		26,
 		// Debit (withdrawal) to checking account ‘27’
 		27,
 		// Prenote for debit to checking account ‘28’


Test patch:
diff --git a/returnAddenda_test.go b/returnAddenda_test.go
new file mode 100644
index 00000000..7f5b66bd
--- /dev/null
+++ b/returnAddenda_test.go
@@ -0,0 +1,152 @@
+// Copyright 2017 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"
+	"time"
+)
+
+func mockReturnAddenda() ReturnAddenda {
+	rAddenda := NewReturnAddenda()
+	rAddenda.TypeCode = "R07"
+	rAddenda.AddendaInformation = "Authorization Revoked"
+
+	return rAddenda
+}
+
+func TestMockReturnAddenda(t *testing.T) {
+	// TODO: build a mock addenda
+}
+
+func TestReturnAddendaParse(t *testing.T) {
+	rAddenda := NewReturnAddenda()
+	line := "799R07099912340000015      09101298Authorization revoked                       091012980000066"
+	rAddenda.Parse(line)
+	// walk the returnAddenda struct
+	if rAddenda.recordType != "7" {
+		t.Errorf("expected %v got %v", "7", rAddenda.recordType)
+	}
+	if rAddenda.TypeCode != "99" {
+		t.Errorf("expected %v got %v", "99", rAddenda.TypeCode)
+	}
+	if rAddenda.ReturnCode != "R07" {
+		t.Errorf("expected %v got %v", "R07", rAddenda.ReturnCode)
+	}
+	if rAddenda.OriginalTrace != 99912340000015 {
+		t.Errorf("expected: %v got: %v", 99912340000015, rAddenda.OriginalTrace)
+	}
+	if rAddenda.DateOfDeath.IsZero() != true {
+		t.Errorf("expected: %v got: %v", time.Time{}, rAddenda.DateOfDeath)
+	}
+	if rAddenda.OriginalDFI != 9101298 {
+		t.Errorf("expected: %v got: %v", 9101298, rAddenda.OriginalDFI)
+	}
+	if rAddenda.AddendaInformation != "Authorization revoked" {
+		t.Errorf("expected: %v got: %v", "Authorization revoked", rAddenda.AddendaInformation)
+	}
+	if rAddenda.TraceNumber != 91012980000066 {
+		t.Errorf("expected: %v got: %v", 91012980000066, rAddenda.TraceNumber)
+	}
+}
+
+func TestReturnAddendaString(t *testing.T) {
+	rAddenda := NewReturnAddenda()
+	line := "799R07099912340000015      09101298Authorization revoked                       091012980000066"
+	rAddenda.Parse(line)
+
+	if rAddenda.String() != line {
+		t.Errorf("\n expected: %v\n got     : %v", line, rAddenda.String())
+	}
+}
+
+// This is not an exported function but utilized for validation
+func TestReturnAddendaMakeReturnCodeDict(t *testing.T) {
+	codes := makeReturnCodeDict()
+	// check if known code is present
+	_, prs := codes["R01"]
+	if !prs {
+		t.Error("Return Code R01 was not found in the ReturnCodeDict")
+	}
+	// check if invalid code is present
+	_, prs = codes["ABC"]
+	if prs {
+		t.Error("Valid return for an invalid return code key")
+	}
+}
+
+func TestReturnAddendaValidateTrue(t *testing.T) {
+	rAddenda := mockReturnAddenda()
+	rAddenda.ReturnCode = "R13"
+	if err := rAddenda.Validate(); err != nil {
+		if e, ok := err.(*FieldError); ok {
+			if e.FieldName != "ReturnCode" {
+				t.Errorf("%T: %s", err, err)
+			}
+		} else {
+			t.Errorf("%T: %s", err, err)
+		}
+	}
+}
+
+func TestReturnAddendaValidateReturnCodeFalse(t *testing.T) {
+	rAddenda := mockReturnAddenda()
+	rAddenda.ReturnCode = ""
+	if err := rAddenda.Validate(); err != nil {
+		if e, ok := err.(*FieldError); ok {
+			if e.FieldName != "ReturnCode" {
+				t.Errorf("%T: %s", err, err)
+			}
+		} else {
+			t.Errorf("%T: %s", err, err)
+		}
+	}
+}
+
+func TestReturnAddendaOriginalTraceField(t *testing.T) {
+	rAddenda := mockReturnAddenda()
+	rAddenda.OriginalTrace = 12345
+	if rAddenda.OriginalTraceField() != "000000000012345" {
+		t.Errorf("expected %v received %v", "000000000012345", rAddenda.OriginalTraceField())
+	}
+}
+
+func TestReturnAddendaDateOfDeathField(t *testing.T) {
+	rAddenda := mockReturnAddenda()
+	// Check for all zeros
+	if rAddenda.DateOfDeathField() != "      " {
+		t.Errorf("expected %v received %v", "      ", rAddenda.DateOfDeathField())
+	}
+	// Year: 1978 Month: October Day: 23
+	rAddenda.DateOfDeath = time.Date(1978, time.October, 23, 0, 0, 0, 0, time.UTC)
+	if rAddenda.DateOfDeathField() != "781023" {
+		t.Errorf("expected %v received %v", "781023", rAddenda.DateOfDeathField())
+	}
+}
+
+func TestReturnAddendaOriginalDFIField(t *testing.T) {
+	rAddenda := mockReturnAddenda()
+	exp := "00000000"
+	if rAddenda.OriginalDFIField() != exp {
+		t.Errorf("expected %v received %v", exp, rAddenda.OriginalDFIField())
+	}
+}
+
+func TestReturnAddendaAddendaInformationField(t *testing.T) {
+	rAddenda := mockReturnAddenda()
+	exp := "Authorization Revoked                       "
+	if rAddenda.AddendaInformationField() != exp {
+		t.Errorf("expected %v received %v", exp, rAddenda.AddendaInformationField())
+	}
+}
+
+func TestReturnAddendaTraceNumberField(t *testing.T) {
+	rAddenda := mockReturnAddenda()
+	rAddenda.TraceNumber = 91012980000066
+	exp := "091012980000066"
+	if rAddenda.TraceNumberField() != exp {
+		t.Errorf("expected %v received %v", exp, rAddenda.TraceNumberField())
+	}
+}