Repository: moov-io/ach
Pull request #234 (2018-08-03 16:02:30)

233 Add support for IAT Returns
233 Add support for IAT Returns


Patch:
diff --git a/addenda99.go b/addenda99.go
index 676cfbbf..81094720 100644
--- a/addenda99.go
+++ b/addenda99.go
@@ -157,6 +157,29 @@ func (Addenda99 *Addenda99) AddendaInformationField() string {
 	return Addenda99.alphaField(Addenda99.AddendaInformation, 44)
 }
 
+//IATPaymentAmount sets original forward entry payment amount characters 1-10 of underlying AddendaInformation
+func (Addenda99 *Addenda99) IATPaymentAmount(s string) {
+	Addenda99.AddendaInformation = Addenda99.stringField(s, 10)
+}
+
+//IATAddendaInformation sets Addenda Information for IAT return items, characters 10-44 of
+// underlying AddendaInformation
+func (Addenda99 *Addenda99) IATAddendaInformation(s string) {
+	Addenda99.AddendaInformation = Addenda99.AddendaInformation + Addenda99.alphaField(s, 34)
+}
+
+//IATPaymentAmountField returns original forward entry payment amount int, characters 1-10 of
+// underlying AddendaInformation
+func (Addenda99 *Addenda99) IATPaymentAmountField() int {
+	return Addenda99.parseNumField(Addenda99.AddendaInformation[0:10])
+}
+
+//IATAddendaInformationField returns a space padded AddendaInformation string, characters 10-44 of
+// underlying AddendaInformation
+func (Addenda99 *Addenda99) IATAddendaInformationField() string {
+	return Addenda99.alphaField(Addenda99.AddendaInformation[9:44], 34)
+}
+
 // TraceNumberField returns a zero padded traceNumber string
 func (Addenda99 *Addenda99) TraceNumberField() string {
 	return Addenda99.numericField(Addenda99.TraceNumber, 15)
diff --git a/iatBatch.go b/iatBatch.go
index 7b7723ed..8a1ffc92 100644
--- a/iatBatch.go
+++ b/iatBatch.go
@@ -14,8 +14,7 @@ var (
 	msgIATBatchAddendaIndicator = "is invalid for addenda record(s) found"
 	// There can be up to 2 optional Addenda17 records and up to 5 optional Addenda18 records
 	msgBatchIATAddendum          = "7 Addendum is the maximum for SEC code IAT"
-	msgBatchIATAddenda17         = "2 Addenda17 is the maximum for SEC code IAT"
-	msgBatchIATAddenda18         = "5 Addenda18 is the maximum for SEC code IAT"
+	msgBatchIATAddendumCount     = "%v Addenda %v for SEC Code IAT"
 	msgBatchIATInvalidAddendumer = "invalid Addendumer for SEC Code IAT"
 )
 
@@ -505,27 +504,39 @@ func (batch *IATBatch) Validate() error {
 		if len(entry.Addendum) > 7 {
 			return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addendum", Msg: msgBatchIATAddendum}
 		}
+
+		// Counter for addendumer for 17, 18,, and 99
+		// ToDo: Come up with a better way?
+
 		addenda17Count := 0
 		addenda18Count := 0
+		addenda99Count := 0
 
 		for _, IATAddenda := range entry.Addendum {
 
-			if (IATAddenda.TypeCode() != "17") && (IATAddenda.TypeCode() != "18") {
-				return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addendum", Msg: msgBatchIATInvalidAddendumer}
-			}
-			if IATAddenda.TypeCode() == "17" {
+			switch IATAddenda.TypeCode() {
+			case "17":
 				addenda17Count = addenda17Count + 1
-			}
-			if IATAddenda.TypeCode() == "18" {
+				if addenda17Count > 2 {
+					msg := fmt.Sprintf(msgBatchIATAddendumCount, addenda17Count, "17")
+					return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addendum", Msg: msg}
+				}
+			case "18":
 				addenda18Count = addenda18Count + 1
+				if addenda18Count > 5 {
+					msg := fmt.Sprintf(msgBatchIATAddendumCount, addenda18Count, "18")
+					return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addendum", Msg: msg}
+				}
+			case "99":
+				addenda99Count = addenda99Count + 1
+				if addenda99Count > 1 {
+					msg := fmt.Sprintf(msgBatchIATAddendumCount, addenda99Count, "99")
+					return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addendum", Msg: msg}
+				}
+			default:
+				return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addendum", Msg: msgBatchIATInvalidAddendumer}
 			}
 		}
-		if addenda17Count > 2 {
-			return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addendum", Msg: msgBatchIATAddenda17}
-		}
-		if addenda18Count > 5 {
-			return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addendum", Msg: msgBatchIATAddenda18}
-		}
 	}
 	// Add type specific validation.
 	// ...
diff --git a/iatEntryDetail.go b/iatEntryDetail.go
index 9255c2bb..7621dba5 100644
--- a/iatEntryDetail.go
+++ b/iatEntryDetail.go
@@ -295,7 +295,10 @@ func (ed *IATEntryDetail) AddIATAddenda(addenda Addendumer) []Addendumer {
 	ed.AddendaRecordIndicator = 1
 	// checks to make sure that we only have either or, not both
 	switch addenda.(type) {
-	// Addenda17
+	case *Addenda99:
+		ed.Category = CategoryReturn
+		ed.Addendum = append(ed.Addendum, addenda)
+		return ed.Addendum
 	default:
 		ed.Category = CategoryForward
 		ed.Addendum = append(ed.Addendum, addenda)
diff --git a/reader.go b/reader.go
index 6e12b280..8e0024a5 100644
--- a/reader.go
+++ b/reader.go
@@ -214,17 +214,28 @@ func (r *Reader) parseED() error {
 
 // parseEd parses determines whether to parse an IATEntryDetail Addenda or EntryDetail Addenda
 func (r *Reader) parseEDAddenda() error {
-	switch r.line[1:3] {
-	//ToDo;  What to do about 98 and 99 ?
-	case "10", "11", "12", "13", "14", "15", "16", "17", "18":
-		if err := r.parseIATAddenda(); err != nil {
+
+	if r.currentBatch != nil {
+		if err := r.parseAddenda(); err != nil {
 			return err
 		}
-	default:
-		if err := r.parseAddenda(); err != nil {
+	} else {
+		if err := r.parseIATAddenda(); err != nil {
 			return err
 		}
 	}
+
+	/*	switch r.line[1:3] {
+		//ToDo;  What to do about 98 and 99 ?
+		case "10", "11", "12", "13", "14", "15", "16", "17", "18":
+			if err := r.parseIATAddenda(); err != nil {
+				return err
+			}
+		default:
+			if err := r.parseAddenda(); err != nil {
+				return err
+			}
+		}*/
 	return nil
 }
 
@@ -445,6 +456,28 @@ func (r *Reader) parseIATAddenda() error {
 }
 
 func (r *Reader) switchIATAddenda(entryIndex int) error {
+
+	switch r.line[1:3] {
+	// IAT mandatory and optional Addenda
+	case "10", "11", "12", "13", "14", "15", "16", "17", "18":
+		err := r.mandatoryOptionalIATAddenda(entryIndex)
+		if err != nil {
+			return err
+		}
+	// IAT return Addenda
+	case "99":
+		err := r.returnIATAddenda(entryIndex)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// mandatoryOptionalIATAddenda parses and validates mandatory IAT addenda records: Addenda10,
+// Addenda11, Addenda12, Addenda13, Addenda14, Addenda15, Addenda16, Addenda17, Addenda18
+func (r *Reader) mandatoryOptionalIATAddenda(entryIndex int) error {
+
 	switch r.line[1:3] {
 	case "10":
 		addenda10 := NewAddenda10()
@@ -452,64 +485,77 @@ func (r *Reader) switchIATAddenda(entryIndex int) error {
 		if err := addenda10.Validate(); err != nil {
 			return err
 		}
-		r.IATCurrentBatch.GetEntries()[entryIndex].Addenda10 = addenda10
+		r.IATCurrentBatch.Entries[entryIndex].Addenda10 = addenda10
 	case "11":
 		addenda11 := NewAddenda11()
 		addenda11.Parse(r.line)
 		if err := addenda11.Validate(); err != nil {
 			return err
 		}
-		r.IATCurrentBatch.GetEntries()[entryIndex].Addenda11 = addenda11
+		r.IATCurrentBatch.Entries[entryIndex].Addenda11 = addenda11
 	case "12":
 		addenda12 := NewAddenda12()
 		addenda12.Parse(r.line)
 		if err := addenda12.Validate(); err != nil {
 			return err
 		}
-		r.IATCurrentBatch.GetEntries()[entryIndex].Addenda12 = addenda12
+		r.IATCurrentBatch.Entries[entryIndex].Addenda12 = addenda12
 	case "13":
-
 		addenda13 := NewAddenda13()
 		addenda13.Parse(r.line)
 		if err := addenda13.Validate(); err != nil {
 			return err
 		}
-		r.IATCurrentBatch.GetEntries()[entryIndex].Addenda13 = addenda13
+		r.IATCurrentBatch.Entries[entryIndex].Addenda13 = addenda13
 	case "14":
 		addenda14 := NewAddenda14()
 		addenda14.Parse(r.line)
 		if err := addenda14.Validate(); err != nil {
 			return err
 		}
-		r.IATCurrentBatch.GetEntries()[entryIndex].Addenda14 = addenda14
+		r.IATCurrentBatch.Entries[entryIndex].Addenda14 = addenda14
 	case "15":
 		addenda15 := NewAddenda15()
 		addenda15.Parse(r.line)
 		if err := addenda15.Validate(); err != nil {
 			return err
 		}
-		r.IATCurrentBatch.GetEntries()[entryIndex].Addenda15 = addenda15
+		r.IATCurrentBatch.Entries[entryIndex].Addenda15 = addenda15
 	case "16":
 		addenda16 := NewAddenda16()
 		addenda16.Parse(r.line)
 		if err := addenda16.Validate(); err != nil {
 			return err
 		}
-		r.IATCurrentBatch.GetEntries()[entryIndex].Addenda16 = addenda16
+		r.IATCurrentBatch.Entries[entryIndex].Addenda16 = addenda16
 	case "17":
 		addenda17 := NewAddenda17()
 		addenda17.Parse(r.line)
 		if err := addenda17.Validate(); err != nil {
 			return err
 		}
-		r.IATCurrentBatch.GetEntries()[entryIndex].AddIATAddenda(addenda17)
+		r.IATCurrentBatch.Entries[entryIndex].AddIATAddenda(addenda17)
 	case "18":
 		addenda18 := NewAddenda18()
 		addenda18.Parse(r.line)
 		if err := addenda18.Validate(); err != nil {
 			return err
 		}
-		r.IATCurrentBatch.GetEntries()[entryIndex].AddIATAddenda(addenda18)
+		r.IATCurrentBatch.Entries[entryIndex].AddIATAddenda(addenda18)
 	}
+
+	return nil
+}
+
+// returnIATAddenda parses and validates IAT return record Addenda99
+func (r *Reader) returnIATAddenda(entryIndex int) error {
+
+	addenda99 := NewAddenda99()
+	addenda99.Parse(r.line)
+	if err := addenda99.Validate(); err != nil {
+		return err
+	}
+	r.IATCurrentBatch.Entries[entryIndex].AddIATAddenda(addenda99)
+
 	return nil
 }


Test patch:
diff --git a/iatBatchHeader_test.go b/iatBatchHeader_test.go
index db223ec9..1a958b37 100644
--- a/iatBatchHeader_test.go
+++ b/iatBatchHeader_test.go
@@ -25,6 +25,22 @@ func mockIATBatchHeaderFF() *IATBatchHeader {
 	return bh
 }
 
+// mockIATBatchReturnHeaderFF creates a IAT Return BatchHeader that is Fixed-Fixed
+func mockIATReturnBatchHeaderFF() *IATBatchHeader {
+	bh := NewIATBatchHeader()
+	bh.ServiceClassCode = 220
+	bh.ForeignExchangeIndicator = "FF"
+	bh.ForeignExchangeReferenceIndicator = 3
+	bh.ISODestinationCountryCode = "US"
+	bh.OriginatorIdentification = "123456789"
+	bh.StandardEntryClassCode = "IAT"
+	bh.CompanyEntryDescription = "TRADEPAYMT"
+	bh.ISOOriginatingCurrencyCode = "CAD"
+	bh.ISODestinationCurrencyCode = "USD"
+	bh.ODFIIdentification = "12104288"
+	return bh
+}
+
 // testMockIATBatchHeaderFF creates a IAT BatchHeader Fixed-Fixed
 func testMockIATBatchHeaderFF(t testing.TB) {
 	bh := mockIATBatchHeaderFF()
diff --git a/iatBatch_test.go b/iatBatch_test.go
index 3aaa03c6..74cbb100 100644
--- a/iatBatch_test.go
+++ b/iatBatch_test.go
@@ -101,6 +101,16 @@ func mockInvalidAddenda17() *Addenda17 {
 	return addenda17
 }
 
+func mockIATAddenda99() *Addenda99 {
+	addenda99 := NewAddenda99()
+	addenda99.ReturnCode = "R07"
+	addenda99.OriginalTrace = 231380100000001
+	addenda99.OriginalDFI = "12104288"
+	addenda99.IATPaymentAmount("0000100000")
+	addenda99.IATAddendaInformation("Authorization Revoked")
+	return addenda99
+}
+
 // TestMockIATBatch validates mockIATBatch
 func TestMockIATBatch(t *testing.T) {
 	iatBatch := mockIATBatch()
@@ -1776,7 +1786,6 @@ func testIATBatchAddenda18Count(t testing.TB) {
 	mockBatch.Entries[0].Addenda15 = mockAddenda15()
 	mockBatch.Entries[0].Addenda16 = mockAddenda16()
 	mockBatch.Entries[0].AddIATAddenda(mockAddenda17())
-	mockBatch.Entries[0].AddIATAddenda(mockAddenda17B())
 	mockBatch.Entries[0].AddIATAddenda(mockAddenda18())
 	mockBatch.Entries[0].AddIATAddenda(mockAddenda18B())
 	mockBatch.Entries[0].AddIATAddenda(mockAddenda18C())
@@ -1882,3 +1891,47 @@ func BenchmarkIATBatchBHODFI(b *testing.B) {
 	}
 
 }
+
+// testIATBatchAddenda99Count validates IATBatch Addenda99 Count
+func testIATBatchAddenda99Count(t testing.TB) {
+	mockBatch := IATBatch{}
+	mockBatch.SetHeader(mockIATReturnBatchHeaderFF())
+	mockBatch.AddEntry(mockIATEntryDetail())
+	mockBatch.Entries[0].Addenda10 = mockAddenda10()
+	mockBatch.Entries[0].Addenda11 = mockAddenda11()
+	mockBatch.Entries[0].Addenda12 = mockAddenda12()
+	mockBatch.Entries[0].Addenda13 = mockAddenda13()
+	mockBatch.Entries[0].Addenda14 = mockAddenda14()
+	mockBatch.Entries[0].Addenda15 = mockAddenda15()
+	mockBatch.Entries[0].Addenda16 = mockAddenda16()
+	mockBatch.Entries[0].AddIATAddenda(mockIATAddenda99())
+	mockBatch.category = CategoryReturn
+
+	if err := mockBatch.build(); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+
+	if err := mockBatch.Validate(); err != nil {
+		if e, ok := err.(*BatchError); ok {
+			if e.FieldName != "Addendum" {
+				t.Errorf("%T: %s", err, err)
+			}
+		} else {
+			t.Errorf("%T: %s", err, err)
+		}
+	}
+
+}
+
+// TestIATBatchAddenda99Count tests validating IATBatch Addenda99 Count
+func TestIATBatchAddenda99Count(t *testing.T) {
+	testIATBatchAddenda99Count(t)
+}
+
+// BenchmarkIATBatchAddenda99Count benchmarks validating IATBatch Addenda99 Count
+func BenchmarkIATBatchAddenda99Count(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testIATBatchAddenda99Count(b)
+	}
+}
diff --git a/writer_test.go b/writer_test.go
index 389e067f..44c8f716 100644
--- a/writer_test.go
+++ b/writer_test.go
@@ -291,3 +291,64 @@ func BenchmarkPPDIATWrite(b *testing.B) {
 		testPPDIATWrite(b)
 	}
 }
+
+// testIATReturn writes a IAT ACH Return file
+func testIATReturn(t testing.TB) {
+	file := NewFile().SetHeader(mockFileHeader())
+	iatBatch := IATBatch{}
+	iatBatch.SetHeader(mockIATBatchHeaderFF())
+	iatBatch.AddEntry(mockIATEntryDetail())
+	iatBatch.Entries[0].Addenda10 = mockAddenda10()
+	iatBatch.Entries[0].Addenda11 = mockAddenda11()
+	iatBatch.Entries[0].Addenda12 = mockAddenda12()
+	iatBatch.Entries[0].Addenda13 = mockAddenda13()
+	iatBatch.Entries[0].Addenda14 = mockAddenda14()
+	iatBatch.Entries[0].Addenda15 = mockAddenda15()
+	iatBatch.Entries[0].Addenda16 = mockAddenda16()
+	iatBatch.Entries[0].AddIATAddenda(mockIATAddenda99())
+	iatBatch.Create()
+	file.AddIATBatch(iatBatch)
+
+	if err := file.Create(); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+	if err := file.Validate(); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+
+	b := &bytes.Buffer{}
+	f := NewWriter(b)
+
+	if err := f.Write(file); err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+
+	r := NewReader(strings.NewReader(b.String()))
+	_, 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)
+	}
+
+	/*		// Write IAT records to standard output. Anything io.Writer
+			w := NewWriter(os.Stdout)
+			if err := w.Write(file); err != nil {
+				log.Fatalf("Unexpected error: %s\n", err)
+			}
+			w.Flush()*/
+}
+
+// TestIATReturn tests writing a IAT ACH Return file
+func TestIATReturn(t *testing.T) {
+	testIATReturn(t)
+}
+
+// BenchmarkIATReturn benchmarks validating writing a IAT ACH Return file
+func BenchmarkIATReturn(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		testIATReturn(b)
+	}
+}