diff --git a/.travis.yml b/.travis.yml index 9675d5e26..163aba0b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ env: secure: QPkcX77j8QEqTwOYyLGItqvxYwE6Na5WaSZWjmhp48OlxYatWRHxJBwcFYSn1OWD5FMn+3oW39fHknReIxtrnhXMaNvI7x3/0gy4zujD/xZ2xAg7NsQ+l5buvEFO8/LEwwo0fp4knItFcBv8xH/ziJBJyXvgfMtj7Is4Q/pB1p6pWDdVy1vtAj3zH02bcqh1yXXS3HvcD8UhTszfU017gVNXDN1ow0rp1L3ainr3btrVK9izUxZfKvb7PlWJO1ogah7xNr/dIOJLsx2SfKgzKp+3H28L2WegtbzON74Op4jXvRywCwqjmUt/nwJ/Y9anunMNHT136h+ye4ziG1i/VdbWq0Q4PopQ8yYqinujG7SjfQio+wNCV2cwc2r/WjNBjbH0N9/Pflogq3RHvgy/9VtPif1tY+RrZCSntohoEZbYpVcFQFE1xDyf6xq/uLxVeEcCU33gqq7cKEfpcUgyCITa+yCPfBdtgkLBJ8h7Sew1j08D1kTKUW6g3D1epmwlCh/Z16oHG5VwSnCLGDjJy8wm/hQk1i/g7qeP7g24CfNzffzlFBCy88HhjzmrhUpcaTyfVVDf4h8wK6Zu/J3dHjHXQYwfiQRqpMa+2DYyjGgZhniccuh4GWolGZauDQdmO9SD4Ugyt9PEMk02i32ax3A4XE/Q6VNOam+qszviX3Q= before_install: - go get github.com/mattn/goveralls -- go get -u honnef.co/go/tools/cmd/megacheck - go get -u github.com/client9/misspell/cmd/misspell - go get -u golang.org/x/lint/golint - go get github.com/fzipp/gocyclo @@ -17,7 +16,6 @@ before_script: script: - test -z $(gofmt -s -l $GO_FILES) - go vet $GO_FILES -- megacheck $GO_FILES - misspell -error -locale US . - gocyclo -over 19 $GO_FILES - golint -set_exit_status $GO_FILES diff --git a/README.md b/README.md index 4c14fcdc6..ec6e0a10f 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,13 @@ ACH is under active development but already in production for multiple companies * Return Entries * Addenda Type Code 02 * Addenda Type Code 05 - * Addenda Type Code 98 - * Addenda Type Code 99 - - + * Addenda Type Code 10 (IAT) + * Addenda Type Code 11 (IAT) + * Addenda Type Code 12 (IAT) + * Addenda Type Code 98 (NOC) + * Addenda Type Code 99 (Return) + * IAT + ## Project Roadmap * Additional SEC codes will be added based on library users needs. Please open an issue with a valid test file. * Review the project issues for more detailed information @@ -259,6 +262,129 @@ Which will generate a well formed ACH flat file. 82200000020010200101000000000000000000000799123456789 234567890000002 9000002000001000000040020400202000000017500000000000799 ``` + +### Create an IAT file + +```go + 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(mockAddenda17()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda17B()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18B()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18C()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18D()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18E()) + iatBatch.Create() + file.AddIATBatch(iatBatch) + + iatBatch2 := IATBatch{} + iatBatch2.SetHeader(mockIATBatchHeaderFF()) + iatBatch2.AddEntry(mockIATEntryDetail()) + iatBatch2.GetEntries()[0].TransactionCode = 27 + iatBatch2.GetEntries()[0].Amount = 2000 + iatBatch2.Entries[0].Addenda10 = mockAddenda10() + iatBatch2.Entries[0].Addenda11 = mockAddenda11() + iatBatch2.Entries[0].Addenda12 = mockAddenda12() + iatBatch2.Entries[0].Addenda13 = mockAddenda13() + iatBatch2.Entries[0].Addenda14 = mockAddenda14() + iatBatch2.Entries[0].Addenda15 = mockAddenda15() + iatBatch2.Entries[0].Addenda16 = mockAddenda16() + iatBatch2.Entries[0].AddIATAddenda(mockAddenda17()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda17B()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18B()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18C()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18D()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18E()) + iatBatch2.Create() + file.AddIATBatch(iatBatch2) + + 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() +``` + +This will generate a well formed flat IAT ACH file + +```text +101 987654321 1234567891807200000A094101Federal Reserve Bank My Bank Name +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000001 +6221210428820007 0000100000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US*19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +717This is an international payment 00010000001 +717Transfer of money from one country to another 00020000001 +718Bank of Germany 01987987987654654 DE 00010000001 +718Bank of Spain 01987987987123123 ES 00020000001 +718Bank of France 01456456456987987 FR 00030000001 +718Bank of Turkey 0112312345678910 TR 00040000001 +718Bank of United Kingdom 011234567890123456789012345678901234GB 00050000001 +82200000150012104288000000000000000000100000 231380100000001 +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000002 +6271210428820007 0000002000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US*19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +717This is an international payment 00010000001 +717Transfer of money from one country to another 00020000001 +718Bank of Germany 01987987987654654 DE 00010000001 +718Bank of Spain 01987987987123123 ES 00020000001 +718Bank of France 01456456456987987 FR 00030000001 +718Bank of Turkey 0112312345678910 TR 00040000001 +718Bank of United Kingdom 011234567890123456789012345678901234GB 00050000001 +82200000150012104288000000002000000000000000 231380100000002 +9000002000004000000300024208576000000002000000000100000 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +``` + # Getting Help channel | info diff --git a/addenda02_test.go b/addenda02_test.go index dcc0b26b5..adb20c3ca 100644 --- a/addenda02_test.go +++ b/addenda02_test.go @@ -8,6 +8,7 @@ import ( "testing" ) +// mockAddenda02 creates a mock Addenda02 record func mockAddenda02() *Addenda02 { addenda02 := NewAddenda02() addenda02.ReferenceInformationOne = "REFONEA" @@ -23,6 +24,7 @@ func mockAddenda02() *Addenda02 { return addenda02 } +// TestMockAddenda02 validates mockAddenda02 func TestMockAddenda02(t *testing.T) { addenda02 := mockAddenda02() if err := addenda02.Validate(); err != nil { @@ -30,6 +32,7 @@ func TestMockAddenda02(t *testing.T) { } } +// testAddenda02ValidRecordType validates Addenda02 recordType func testAddenda02ValidRecordType(t testing.TB) { addenda02 := mockAddenda02() addenda02.recordType = "63" @@ -43,10 +46,13 @@ func testAddenda02ValidRecordType(t testing.TB) { } } } + +// TestAddenda02ValidRecordType tests validating Addenda02 recordType func TestAddenda02ValidRecordType(t *testing.T) { testAddenda02ValidRecordType(t) } +// BenchmarkAddenda02ValidRecordType benchmarks validating Addenda02 recordType func BenchmarkAddenda02ValidRecordType(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -54,6 +60,7 @@ func BenchmarkAddenda02ValidRecordType(b *testing.B) { } } +// testAddenda02ValidTypeCode validates Addenda02 TypeCode func testAddenda02ValidTypeCode(t testing.TB) { addenda02 := mockAddenda02() addenda02.typeCode = "65" @@ -67,10 +74,13 @@ func testAddenda02ValidTypeCode(t testing.TB) { } } } + +// TestAddenda02ValidTypeCode tests validating Addenda02 TypeCode func TestAddenda02ValidTypeCode(t *testing.T) { testAddenda02ValidTypeCode(t) } +// BenchmarkAddenda02ValidTypeCode benchmarks validating Addenda02 TypeCode func BenchmarkAddenda02ValidTypeCode(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -78,6 +88,7 @@ func BenchmarkAddenda02ValidTypeCode(b *testing.B) { } } +// testAddenda02TypeCode02 TypeCode is 02 if typeCode is a valid TypeCode func testAddenda02TypeCode02(t testing.TB) { addenda02 := mockAddenda02() addenda02.typeCode = "05" @@ -91,10 +102,13 @@ func testAddenda02TypeCode02(t testing.TB) { } } } + +// TestAddenda02TypeCode02 tests TypeCode is 02 if typeCode is a valid TypeCode func TestAddenda02TypeCode02(t *testing.T) { testAddenda02TypeCode02(t) } +// BenchmarkAddenda02TypeCode02 benchmarks TypeCode is 02 if typeCode is a valid TypeCode func BenchmarkAddenda02TypeCode02(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -102,7 +116,8 @@ func BenchmarkAddenda02TypeCode02(b *testing.B) { } } -func testAddenda02RecordType(t testing.TB) { +// testAddenda02FieldInclusionRecordType validates recordType fieldInclusion +func testAddenda02FieldInclusionRecordType(t testing.TB) { addenda02 := mockAddenda02() addenda02.recordType = "" if err := addenda02.Validate(); err != nil { @@ -114,18 +129,21 @@ func testAddenda02RecordType(t testing.TB) { } } -func TestAddenda02RecordType(t *testing.T) { - testAddenda02RecordType(t) +// TestAddenda02FieldInclusionRecordType tests validating recordType fieldInclusion +func TestAddenda02FieldInclusionRecordType(t *testing.T) { + testAddenda02FieldInclusionRecordType(t) } +// BenchmarkAddenda02FieldInclusionRecordType benchmarks validating recordType fieldInclusion func BenchmarkAddenda02FieldInclusionRecordType(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - testAddenda02RecordType(b) + testAddenda02FieldInclusionRecordType(b) } } -func testAddenda02TypeCode(t testing.TB) { +// testAddenda02FieldInclusionTypeCode validates TypeCode fieldInclusion +func testAddenda02FieldInclusionTypeCode(t testing.TB) { addenda02 := mockAddenda02() addenda02.typeCode = "" if err := addenda02.Validate(); err != nil { @@ -137,17 +155,20 @@ func testAddenda02TypeCode(t testing.TB) { } } -func TestAddenda02TypeCode(t *testing.T) { - testAddenda02TypeCode(t) +// TestAddenda02FieldInclusionTypeCode tests validating TypeCode fieldInclusion +func TestAddenda02FieldInclusionTypeCode(t *testing.T) { + testAddenda02FieldInclusionTypeCode(t) } -func BenchmarkAddenda02TypeCode(b *testing.B) { +// BenchmarkAddenda02FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion +func BenchmarkAddenda02FieldInclusionTypeCode(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - testAddenda02TypeCode(b) + testAddenda02FieldInclusionTypeCode(b) } } +// testAddenda02TerminalIdentificationCode validates TerminalIdentificationCode is required func testAddenda02TerminalIdentificationCode(t testing.TB) { addenda02 := mockAddenda02() addenda02.TerminalIdentificationCode = "" @@ -160,10 +181,12 @@ func testAddenda02TerminalIdentificationCode(t testing.TB) { } } +// TestAddenda02TerminalIdentificationCode tests validating TerminalIdentificationCode is required func TestAddenda02TerminalIdentificationCode(t *testing.T) { testAddenda02TerminalIdentificationCode(t) } +// BenchmarkAddenda02TerminalIdentificationCode benchmarks validating TerminalIdentificationCode is required func BenchmarkAddenda02TerminalIdentificationCode(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -171,6 +194,7 @@ func BenchmarkAddenda02TerminalIdentificationCode(b *testing.B) { } } +// testAddenda02TransactionSerialNumber validates TransactionSerialNumber is required func testAddenda02TransactionSerialNumber(t testing.TB) { addenda02 := mockAddenda02() addenda02.TransactionSerialNumber = "" @@ -183,10 +207,12 @@ func testAddenda02TransactionSerialNumber(t testing.TB) { } } +// TestAddenda02TransactionSerialNumber tests validating TransactionSerialNumber is required func TestAddenda02TransactionSerialNumber(t *testing.T) { testAddenda02TransactionSerialNumber(t) } +// BenchmarkAddenda02TransactionSerialNumber benchmarks validating TransactionSerialNumber is required func BenchmarkAddenda02TransactionSerialNumber(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -194,6 +220,7 @@ func BenchmarkAddenda02TransactionSerialNumber(b *testing.B) { } } +// testAddenda02TransactionDate validates TransactionDate is required func testAddenda02TransactionDate(t testing.TB) { addenda02 := mockAddenda02() addenda02.TransactionDate = "" @@ -206,10 +233,12 @@ func testAddenda02TransactionDate(t testing.TB) { } } +// TestAddenda02TransactionDate tests validating TransactionDate is required func TestAddenda02TransactionDate(t *testing.T) { testAddenda02TransactionDate(t) } +// BenchmarkAddenda02TransactionDate benchmarks validating TransactionDate is required func BenchmarkAddenda02TransactionDate(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -217,6 +246,7 @@ func BenchmarkAddenda02TransactionDate(b *testing.B) { } } +// testAddenda02TerminalLocation validates TerminalLocation is required func testAddenda02TerminalLocation(t testing.TB) { addenda02 := mockAddenda02() addenda02.TerminalLocation = "" @@ -229,10 +259,12 @@ func testAddenda02TerminalLocation(t testing.TB) { } } +// TestAddenda02TerminalLocation tests validating TerminalLocation is required func TestAddenda02TerminalLocation(t *testing.T) { testAddenda02TerminalLocation(t) } +// BenchmarkAddenda02TerminalLocation benchmarks validating TerminalLocation is required func BenchmarkAddenda02TerminalLocation(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -240,6 +272,7 @@ func BenchmarkAddenda02TerminalLocation(b *testing.B) { } } +// testAddenda02TerminalCity validates TerminalCity is required func testAddenda02TerminalCity(t testing.TB) { addenda02 := mockAddenda02() addenda02.TerminalCity = "" @@ -252,10 +285,12 @@ func testAddenda02TerminalCity(t testing.TB) { } } +// TestAddenda02TerminalCity tests validating TerminalCity is required func TestAddenda02TerminalCity(t *testing.T) { testAddenda02TerminalCity(t) } +// BenchmarkAddenda02TerminalCity benchmarks validating TerminalCity is required func BenchmarkAddenda02TerminalCity(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -263,6 +298,7 @@ func BenchmarkAddenda02TerminalCity(b *testing.B) { } } +// testAddenda02TerminalState validates TerminalState is required func testAddenda02TerminalState(t testing.TB) { addenda02 := mockAddenda02() addenda02.TerminalState = "" @@ -275,10 +311,12 @@ func testAddenda02TerminalState(t testing.TB) { } } +// TestAddenda02TerminalState tests validating TerminalState is required func TestAddenda02TerminalState(t *testing.T) { testAddenda02TerminalState(t) } +// BenchmarkAddenda02TerminalState benchmarks validating TerminalState is required func BenchmarkAddenda02TerminalState(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -286,7 +324,7 @@ func BenchmarkAddenda02TerminalState(b *testing.B) { } } -// TestAddenda02 String validates that a known parsed file can be return to a string of the same value +// TestAddenda02String validates that a known parsed Addenda02 record can be return to a string of the same value func testAddenda02String(t testing.TB) { addenda02 := NewAddenda02() var line = "702REFONEAREFTERM021000490612123456Target Store 0049 PHILADELPHIA PA121042880000123" @@ -299,12 +337,12 @@ func testAddenda02String(t testing.TB) { } } -// TestAddenda02String tests validating that a known parsed file can be return to a string of the same value +// TestAddenda02String tests validating that a known parsed Addenda02 record can be return to a string of the same value func TestAddenda02String(t *testing.T) { testAddenda02String(t) } -// BenchmarkAddenda02String benchmarks validating that a known parsed file can be return to a string of the same value +// BenchmarkAddenda02String benchmarks validating that a known parsed Addenda02 record can be return to a string of the same value func BenchmarkAddenda02String(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -332,7 +370,7 @@ func TestAddenda02TransactionDateMonth(t *testing.T) { testAddenda02TransactionDateMonth(t) } -// BenchmarkAddenda02TransactionDateMonth test validating the month is valid for transactionDate +// BenchmarkAddenda02TransactionDateMonth benchmarks validating the month is valid for transactionDate func BenchmarkAddenda02TransactionDateMonth(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -360,7 +398,7 @@ func TestAddenda02TransactionDateDay(t *testing.T) { testAddenda02TransactionDateDay(t) } -// BenchmarkAddenda02TransactionDateDay test validating the day is valid for transactionDate +// BenchmarkAddenda02TransactionDateDay benchmarks validating the day is valid for transactionDate func BenchmarkAddenda02TransactionDateDay(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -388,7 +426,7 @@ func TestAddenda02TransactionDateFeb(t *testing.T) { testAddenda02TransactionDateFeb(t) } -// BenchmarkAddenda02TransactionDateFeb test validating the day is valid for transactionDate +// BenchmarkAddenda02TransactionDateFeb benchmarks validating the day is valid for transactionDate func BenchmarkAddenda02TransactionDateFeb(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -416,7 +454,7 @@ func TestAddenda02TransactionDate30Day(t *testing.T) { testAddenda02TransactionDate30Day(t) } -// BenchmarkAddenda02TransactionDate30Day test validating the day is valid for transactionDate +// BenchmarkAddenda02TransactionDate30Day benchmarks validating the day is valid for transactionDate func BenchmarkAddenda02TransactionDate30Day(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -444,7 +482,7 @@ func TestAddenda02TransactionDate31Day(t *testing.T) { testAddenda02TransactionDate31Day(t) } -// BenchmarkAddenda02TransactionDate31Day test validating the day is valid for transactionDate +// BenchmarkAddenda02TransactionDate31Day benchmarks validating the day is valid for transactionDate func BenchmarkAddenda02TransactionDate31Day(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -472,7 +510,7 @@ func TestAddenda02TransactionDateInvalidDay(t *testing.T) { testAddenda02TransactionDateInvalidDay(t) } -// BenchmarkAddenda02TransactionDateInvalidDay test validating the day is invalid for transactionDate +// BenchmarkAddenda02TransactionDateInvalidDay benchmarks validating the day is invalid for transactionDate func BenchmarkAddenda02TransactionDateInvalidDay(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { diff --git a/addenda10.go b/addenda10.go new file mode 100644 index 000000000..1d6cf7cec --- /dev/null +++ b/addenda10.go @@ -0,0 +1,181 @@ +// 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" + "strings" +) + +// Addenda10 is an addenda which provides business transaction information for Addenda Type +// Code 10 in a machine readable format. It is usually formatted according to ANSI, ASC, X12 Standard. +// +// Addenda10 is mandatory for IAT entries +// +// The Addenda10 Record identifies the Receiver of the transaction and the dollar amount of +// the payment. +type Addenda10 struct { + // ID is a client defined string used as a reference to this record. + ID string `json:"id"` + // RecordType defines the type of record in the block. + recordType string + // TypeCode Addenda10 types code '10' + typeCode string + // Transaction Type Code Describes the type of payment: + // ANN = Annuity, BUS = Business/Commercial, DEP = Deposit, LOA = Loan, MIS = Miscellaneous, MOR = Mortgage + // PEN = Pension, RLS = Rent/Lease, REM = Remittance2, SAL = Salary/Payroll, TAX = Tax, TEL = Telephone-Initiated Transaction + // WEB = Internet-Initiated Transaction, ARC = Accounts Receivable Entry, BOC = Back Office Conversion Entry, + // POP = Point of Purchase Entry, RCK = Re-presented Check Entry + TransactionTypeCode string `json:"transactionTypeCode"` + // Foreign Payment Amount $$$$$$$$$$$$$$$$¢¢ + // For inbound IAT payments this field should contain the USD amount or may be blank. + ForeignPaymentAmount int `json:"foreignPaymentAmount"` + // Foreign Trace Number + ForeignTraceNumber string `json:"foreignTraceNumber,omitempty"` + // Receiving Company Name/Individual Name + Name string `json:"name"` + // reserved - Leave blank + reserved string + // EntryDetailSequenceNumber contains the ascending sequence number section of the Entry + // 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 `json:"entryDetailSequenceNumber,omitempty"` + // validator is composed for data validation + validator + // converters is composed for ACH to GoLang Converters + converters +} + +// NewAddenda10 returns a new Addenda10 with default values for none exported fields +func NewAddenda10() *Addenda10 { + addenda10 := new(Addenda10) + addenda10.recordType = "7" + addenda10.typeCode = "10" + return addenda10 +} + +// Parse takes the input record string and parses the Addenda10 values +func (addenda10 *Addenda10) Parse(record string) { + // 1-1 Always "7" + addenda10.recordType = "7" + // 2-3 Always 10 + addenda10.typeCode = record[1:3] + // 04-06 Describes the type of payment + addenda10.TransactionTypeCode = record[3:6] + // 07-24 Payment Amount For inbound IAT payments this field should contain the USD amount or may be blank. + addenda10.ForeignPaymentAmount = addenda10.parseNumField(record[06:24]) + // 25-46 Insert blanks or zeros + addenda10.ForeignTraceNumber = strings.TrimSpace(record[24:46]) + // 47-81 Receiving Company Name/Individual Name + addenda10.Name = strings.TrimSpace(record[46:81]) + // 82-87 reserved - Leave blank + addenda10.reserved = " " + // 88-94 Contains the last seven digits of the number entered in the Trace Number field in the corresponding Entry Detail Record + addenda10.EntryDetailSequenceNumber = addenda10.parseNumField(record[87:94]) +} + +// String writes the Addenda10 struct to a 94 character string. +func (addenda10 *Addenda10) String() string { + return fmt.Sprintf("%v%v%v%v%v%v%v%v", + addenda10.recordType, + addenda10.typeCode, + // TransactionTypeCode Validator + addenda10.TransactionTypeCode, + addenda10.ForeignPaymentAmountField(), + addenda10.ForeignTraceNumberField(), + addenda10.NameField(), + addenda10.reservedField(), + addenda10.EntryDetailSequenceNumberField()) +} + +// Validate performs NACHA format rule checks on the record and returns an error if not Validated +// The first error encountered is returned and stops that parsing. +func (addenda10 *Addenda10) Validate() error { + if err := addenda10.fieldInclusion(); err != nil { + return err + } + if addenda10.recordType != "7" { + msg := fmt.Sprintf(msgRecordType, 7) + return &FieldError{FieldName: "recordType", Value: addenda10.recordType, Msg: msg} + } + if err := addenda10.isTypeCode(addenda10.typeCode); err != nil { + return &FieldError{FieldName: "TypeCode", Value: addenda10.typeCode, Msg: err.Error()} + } + // Type Code must be 10 + if addenda10.typeCode != "10" { + return &FieldError{FieldName: "TypeCode", Value: addenda10.typeCode, Msg: msgAddendaTypeCode} + } + if err := addenda10.isTransactionTypeCode(addenda10.TransactionTypeCode); err != nil { + return &FieldError{FieldName: "TransactionTypeCode", Value: addenda10.TransactionTypeCode, Msg: err.Error()} + } + // ToDo: Foreign Payment Amount blank ? + if err := addenda10.isAlphanumeric(addenda10.ForeignTraceNumber); err != nil { + return &FieldError{FieldName: "ForeignTraceNumber", Value: addenda10.ForeignTraceNumber, Msg: err.Error()} + } + if err := addenda10.isAlphanumeric(addenda10.Name); err != nil { + return &FieldError{FieldName: "Name", Value: addenda10.Name, Msg: err.Error()} + } + return nil +} + +// fieldInclusion validate mandatory fields are not default values. If fields are +// invalid the ACH transfer will be returned. +func (addenda10 *Addenda10) fieldInclusion() error { + if addenda10.recordType == "" { + return &FieldError{FieldName: "recordType", Value: addenda10.recordType, Msg: msgFieldInclusion} + } + if addenda10.typeCode == "" { + return &FieldError{FieldName: "TypeCode", Value: addenda10.typeCode, Msg: msgFieldInclusion} + } + if addenda10.TransactionTypeCode == "" { + return &FieldError{FieldName: "TransactionTypeCode", + Value: addenda10.TransactionTypeCode, Msg: msgFieldRequired} + } + // ToDo: Commented because it appears this value can be all 000 (maybe blank?) + /* if addenda10.ForeignPaymentAmount == 0 { + return &FieldError{FieldName: "ForeignPaymentAmount", + Value: strconv.Itoa(addenda10.ForeignPaymentAmount), Msg: msgFieldRequired} + }*/ + if addenda10.Name == "" { + return &FieldError{FieldName: "Name", Value: addenda10.Name, Msg: msgFieldInclusion} + } + if addenda10.EntryDetailSequenceNumber == 0 { + return &FieldError{FieldName: "EntryDetailSequenceNumber", + Value: addenda10.EntryDetailSequenceNumberField(), Msg: msgFieldInclusion} + } + return nil +} + +// ForeignPaymentAmountField returns ForeignPaymentAmount zero padded +// ToDo: Review/Add logic for blank ? +func (addenda10 *Addenda10) ForeignPaymentAmountField() string { + return addenda10.numericField(addenda10.ForeignPaymentAmount, 18) +} + +// ForeignTraceNumberField gets the Foreign TraceNumber left padded +func (addenda10 *Addenda10) ForeignTraceNumberField() string { + return addenda10.alphaField(addenda10.ForeignTraceNumber, 22) +} + +// NameField gets the name field - Receiving Company Name/Individual Name left padded +func (addenda10 *Addenda10) NameField() string { + return addenda10.alphaField(addenda10.Name, 35) +} + +// reservedField gets reserved - blank space +func (addenda10 *Addenda10) reservedField() string { + return addenda10.alphaField(addenda10.reserved, 6) +} + +// TypeCode Defines the specific explanation and format for the addenda10 information left padded +func (addenda10 *Addenda10) TypeCode() string { + return addenda10.typeCode +} + +// EntryDetailSequenceNumberField returns a zero padded EntryDetailSequenceNumber string +func (addenda10 *Addenda10) EntryDetailSequenceNumberField() string { + return addenda10.numericField(addenda10.EntryDetailSequenceNumber, 7) +} diff --git a/addenda10_test.go b/addenda10_test.go new file mode 100644 index 000000000..e5bfe9df6 --- /dev/null +++ b/addenda10_test.go @@ -0,0 +1,424 @@ +// 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" +) + +// mockAddenda10 creates a mock Addenda10 record +func mockAddenda10() *Addenda10 { + addenda10 := NewAddenda10() + addenda10.TransactionTypeCode = "ANN" + addenda10.ForeignPaymentAmount = 100000 + addenda10.ForeignTraceNumber = "928383-23938" + addenda10.Name = "BEK Enterprises" + addenda10.EntryDetailSequenceNumber = 00000001 + return addenda10 +} + +// TestMockAddenda10 validates mockAddenda10 +func TestMockAddenda10(t *testing.T) { + addenda10 := mockAddenda10() + if err := addenda10.Validate(); err != nil { + t.Error("mockAddenda10 does not validate and will break other tests") + } +} + +// testAddenda10Parse parses Addenda10 record +func testAddenda10Parse(t testing.TB) { + addenda10 := NewAddenda10() + line := "710ANN000000000000100000928383-23938 BEK Enterprises 0000001" + addenda10.Parse(line) + // walk the Addenda10 struct + if addenda10.recordType != "7" { + t.Errorf("expected %v got %v", "7", addenda10.recordType) + } + if addenda10.typeCode != "10" { + t.Errorf("expected %v got %v", "10", addenda10.typeCode) + } + if addenda10.TransactionTypeCode != "ANN" { + t.Errorf("expected %v got %v", "ANN", addenda10.TransactionTypeCode) + } + if addenda10.ForeignPaymentAmount != 100000 { + t.Errorf("expected: %v got: %v", 100000, addenda10.ForeignPaymentAmount) + } + if addenda10.ForeignTraceNumber != "928383-23938" { + t.Errorf("expected: %v got: %v", "928383-23938", addenda10.ForeignTraceNumber) + } + if addenda10.Name != "BEK Enterprises" { + t.Errorf("expected: %s got: %s", "BEK Enterprises", addenda10.Name) + } + if addenda10.reserved != " " { + t.Errorf("expected: %v got: %v", " ", addenda10.reserved) + } + if addenda10.EntryDetailSequenceNumber != 0000001 { + t.Errorf("expected: %v got: %v", 0000001, addenda10.EntryDetailSequenceNumber) + } +} + +// TestAddenda10Parse tests parsing Addenda10 record +func TestAddenda10Parse(t *testing.T) { + testAddenda10Parse(t) +} + +// BenchmarkAddenda10Parse benchmarks parsing Addenda10 record +func BenchmarkAddenda10Parse(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10Parse(b) + } +} + +// testAddenda10ValidRecordType validates Addenda10 recordType +func testAddenda10ValidRecordType(t testing.TB) { + addenda10 := mockAddenda10() + addenda10.recordType = "63" + if err := addenda10.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda10ValidRecordType tests validating Addenda10 recordType +func TestAddenda10ValidRecordType(t *testing.T) { + testAddenda10ValidRecordType(t) +} + +// BenchmarkAddenda10ValidRecordType benchmarks validating Addenda10 recordType +func BenchmarkAddenda10ValidRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10ValidRecordType(b) + } +} + +// testAddenda10ValidTypeCode validates Addenda10 TypeCode +func testAddenda10ValidTypeCode(t testing.TB) { + addenda10 := mockAddenda10() + addenda10.typeCode = "65" + if err := addenda10.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda10ValidTypeCode tests validating Addenda10 TypeCode +func TestAddenda10ValidTypeCode(t *testing.T) { + testAddenda10ValidTypeCode(t) +} + +// BenchmarkAddenda10ValidTypeCode benchmarks validating Addenda10 TypeCode +func BenchmarkAddenda10ValidTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10ValidTypeCode(b) + } +} + +// testAddenda10TypeCode10 TypeCode is 10 if typeCode is a valid TypeCode +func testAddenda10TypeCode10(t testing.TB) { + addenda10 := mockAddenda10() + addenda10.typeCode = "05" + if err := addenda10.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda10TypeCode10 tests TypeCode is 10 if typeCode is a valid TypeCode +func TestAddenda10TypeCode10(t *testing.T) { + testAddenda10TypeCode10(t) +} + +// BenchmarkAddenda10TypeCode10 benchmarks TypeCode is 10 if typeCode is a valid TypeCode +func BenchmarkAddenda10TypeCode10(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10TypeCode10(b) + } +} + +// testAddenda10TransactionTypeCode validates TransactionTypeCode +func testAddenda10TransactionTypeCode(t testing.TB) { + addenda10 := mockAddenda10() + addenda10.TransactionTypeCode = "ABC" + if err := addenda10.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TransactionTypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda10TransactionTypeCode tests validating TransactionTypeCode +func TestAddenda10TransactionTypeCode(t *testing.T) { + testAddenda10TransactionTypeCode(t) +} + +// BenchmarkAddenda10TransactionTypeCode benchmarks validating TransactionTypeCode +func BenchmarkAddenda10TransactionTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10TransactionTypeCode(b) + } +} + +// testForeignTraceNumberAlphaNumeric validates ForeignTraceNumber is alphanumeric +func testForeignTraceNumberAlphaNumeric(t testing.TB) { + addenda10 := mockAddenda10() + addenda10.ForeignTraceNumber = "®" + if err := addenda10.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ForeignTraceNumber" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestForeignTraceNumberAlphaNumeric tests validating ForeignTraceNumber is alphanumeric +func TestForeignTraceNumberAlphaNumeric(t *testing.T) { + testForeignTraceNumberAlphaNumeric(t) +} + +// BenchmarkForeignTraceNumberAlphaNumeric benchmarks validating ForeignTraceNumber is alphanumeric +func BenchmarkForeignTraceNumberAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testForeignTraceNumberAlphaNumeric(b) + } +} + +// testNameAlphaNumeric validates Name is alphanumeric +func testNameAlphaNumeric(t testing.TB) { + addenda10 := mockAddenda10() + addenda10.Name = "Jas®n" + if err := addenda10.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "Name" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestNameAlphaNumeric tests validating Name is alphanumeric +func TestNameAlphaNumeric(t *testing.T) { + testNameAlphaNumeric(t) +} + +// BenchmarkNameAlphaNumeric benchmarks validating Name is alphanumeric +func BenchmarkNameAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testNameAlphaNumeric(b) + } +} + +// testAddenda10FieldInclusionRecordType validates recordType fieldInclusion +func testAddenda10FieldInclusionRecordType(t testing.TB) { + addenda10 := mockAddenda10() + addenda10.recordType = "" + if err := addenda10.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda10FieldInclusionRecordType tests validating recordType fieldInclusion +func TestAddenda10FieldInclusionRecordType(t *testing.T) { + testAddenda10FieldInclusionRecordType(t) +} + +// BenchmarkAddenda10FieldInclusionRecordType benchmarks validating recordType fieldInclusion +func BenchmarkAddenda10FieldInclusionRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10FieldInclusionRecordType(b) + } +} + +// testAddenda10FieldInclusionTypeCode validates TypeCode fieldInclusion +func testAddenda10FieldInclusionTypeCode(t testing.TB) { + addenda10 := mockAddenda10() + addenda10.typeCode = "" + if err := addenda10.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda10FieldInclusionTypeCode tests validating TypeCode fieldInclusion +func TestAddenda10FieldInclusionTypeCode(t *testing.T) { + testAddenda10FieldInclusionTypeCode(t) +} + +// BenchmarkAddenda10FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion +func BenchmarkAddenda10FieldInclusionTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10FieldInclusionTypeCode(b) + } +} + +// testAddenda10FieldInclusionTransactionTypeCode validates TransactionTypeCode fieldInclusion +func testAddenda10FieldInclusionTransactionTypeCode(t testing.TB) { + addenda10 := mockAddenda10() + addenda10.TransactionTypeCode = "" + if err := addenda10.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldRequired { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda10FieldInclusionTransactionTypeCode tests validating +// TransactionTypeCode fieldInclusion +func TestAddenda10FieldInclusionTransactionTypeCode(t *testing.T) { + testAddenda10FieldInclusionTransactionTypeCode(t) +} + +// BenchmarkAddenda10FieldInclusionTransactionTypeCode benchmarks validating +// TransactionTypeCode fieldInclusion +func BenchmarkAddenda10FieldInclusionTransactionTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10FieldInclusionTransactionTypeCode(b) + } +} + +// testAddenda10FieldInclusionForeignPaymentAmount validates ForeignPaymentAmount fieldInclusion +func testAddenda10FieldInclusionForeignPaymentAmount(t testing.TB) { + addenda10 := mockAddenda10() + addenda10.ForeignPaymentAmount = 0 + if err := addenda10.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldRequired { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda10FieldInclusionForeignPaymentAmount tests validating ForeignPaymentAmount fieldInclusion +func TestAddenda10FieldInclusionForeignPaymentAmount(t *testing.T) { + testAddenda10FieldInclusionForeignPaymentAmount(t) +} + +// BenchmarkAddenda10FieldInclusionForeignPaymentAmount benchmarks validating ForeignPaymentAmount fieldInclusion +func BenchmarkAddenda10FieldInclusionForeignPaymentAmount(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10FieldInclusionForeignPaymentAmount(b) + } +} + +// testAddenda10FieldInclusionName validates Name fieldInclusion +func testAddenda10FieldInclusionName(t testing.TB) { + addenda10 := mockAddenda10() + addenda10.Name = "" + if err := addenda10.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda10FieldInclusionName tests validating Name fieldInclusion +func TestAddenda10FieldInclusionName(t *testing.T) { + testAddenda10FieldInclusionName(t) +} + +// BenchmarkAddenda10FieldInclusionName benchmarks validating Name fieldInclusion +func BenchmarkAddenda10FieldInclusionName(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10FieldInclusionName(b) + } +} + +// testAddenda10FieldInclusionEntryDetailSequenceNumber validates EntryDetailSequenceNumber fieldInclusion +func testAddenda10FieldInclusionEntryDetailSequenceNumber(t testing.TB) { + addenda10 := mockAddenda10() + addenda10.EntryDetailSequenceNumber = 0 + if err := addenda10.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda10FieldInclusionEntryDetailSequenceNumber tests validating +// EntryDetailSequenceNumber fieldInclusion +func TestAddenda10FieldInclusionEntryDetailSequenceNumber(t *testing.T) { + testAddenda10FieldInclusionEntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda10FieldInclusionEntryDetailSequenceNumber benchmarks validating +// EntryDetailSequenceNumber fieldInclusion +func BenchmarkAddenda10FieldInclusionEntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10FieldInclusionEntryDetailSequenceNumber(b) + } +} + +// TestAddenda10String validates that a known parsed Addenda10 record can be return to a string of the same value +func testAddenda10String(t testing.TB) { + addenda10 := NewAddenda10() + var line = "710ANN000000000000100000928383-23938 BEK Enterprises 0000001" + addenda10.Parse(line) + + if addenda10.String() != line { + t.Errorf("Strings do not match") + } + if addenda10.TypeCode() != "10" { + t.Errorf("TypeCode Expected 10 got: %v", addenda10.TypeCode()) + } +} + +// TestAddenda10String tests validating that a known parsed Addenda10 record can be return to a string of the same value +func TestAddenda10String(t *testing.T) { + testAddenda10String(t) +} + +// BenchmarkAddenda10String benchmarks validating that a known parsed Addenda10 record can be return to a string of the same value +func BenchmarkAddenda10String(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10String(b) + } +} diff --git a/addenda11.go b/addenda11.go new file mode 100644 index 000000000..831ba1775 --- /dev/null +++ b/addenda11.go @@ -0,0 +1,151 @@ +// 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" + "strings" +) + +// Addenda11 is an addenda which provides business transaction information for Addenda Type +// Code 11 in a machine readable format. It is usually formatted according to ANSI, ASC, X12 Standard. +// +// Addenda11 is mandatory for IAT entries +// +// The Addenda11 record identifies key information related to the Originator of +// the entry. +type Addenda11 struct { + // ID is a client defined string used as a reference to this record. + ID string `json:"id"` + // RecordType defines the type of record in the block. + recordType string + // TypeCode Addenda11 types code '11' + typeCode string + // Originator Name contains the originators name (your company name / name) + OriginatorName string `json:"originatorName"` + // Originator Street Address Contains the originators street address (your company's address / your address) + OriginatorStreetAddress string `json:"originatorStreetAddress"` + // reserved - Leave blank + reserved string + // EntryDetailSequenceNumber contains the ascending sequence number section of the Entry + // 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 `json:"entryDetailSequenceNumber,omitempty"` + // validator is composed for data validation + validator + // converters is composed for ACH to GoLang Converters + converters +} + +// NewAddenda11 returns a new Addenda11 with default values for none exported fields +func NewAddenda11() *Addenda11 { + addenda11 := new(Addenda11) + addenda11.recordType = "7" + addenda11.typeCode = "11" + return addenda11 +} + +// Parse takes the input record string and parses the Addenda11 values +func (addenda11 *Addenda11) Parse(record string) { + // 1-1 Always "7" + addenda11.recordType = "7" + // 2-3 Always 11 + addenda11.typeCode = record[1:3] + // 4-38 + addenda11.OriginatorName = strings.TrimSpace(record[3:38]) + // 39-73 + addenda11.OriginatorStreetAddress = strings.TrimSpace(record[38:73]) + // 74-87 reserved - Leave blank + addenda11.reserved = " " + // 88-94 Contains the last seven digits of the number entered in the Trace Number field in the corresponding Entry Detail Record + addenda11.EntryDetailSequenceNumber = addenda11.parseNumField(record[87:94]) +} + +// String writes the Addenda11 struct to a 94 character string. +func (addenda11 *Addenda11) String() string { + return fmt.Sprintf("%v%v%v%v%v%v", + addenda11.recordType, + addenda11.typeCode, + addenda11.OriginatorNameField(), + addenda11.OriginatorStreetAddressField(), + addenda11.reservedField(), + addenda11.EntryDetailSequenceNumberField()) +} + +// Validate performs NACHA format rule checks on the record and returns an error if not Validated +// The first error encountered is returned and stops that parsing. +func (addenda11 *Addenda11) Validate() error { + if err := addenda11.fieldInclusion(); err != nil { + return err + } + if addenda11.recordType != "7" { + msg := fmt.Sprintf(msgRecordType, 7) + return &FieldError{FieldName: "recordType", Value: addenda11.recordType, Msg: msg} + } + if err := addenda11.isTypeCode(addenda11.typeCode); err != nil { + return &FieldError{FieldName: "TypeCode", Value: addenda11.typeCode, Msg: err.Error()} + } + // Type Code must be 11 + if addenda11.typeCode != "11" { + return &FieldError{FieldName: "TypeCode", Value: addenda11.typeCode, Msg: msgAddendaTypeCode} + } + if err := addenda11.isAlphanumeric(addenda11.OriginatorName); err != nil { + return &FieldError{FieldName: "OriginatorName", Value: addenda11.OriginatorName, Msg: err.Error()} + } + if err := addenda11.isAlphanumeric(addenda11.OriginatorStreetAddress); err != nil { + return &FieldError{FieldName: "OriginatorStreetAddress", Value: addenda11.OriginatorStreetAddress, Msg: err.Error()} + } + return nil +} + +// fieldInclusion validate mandatory fields are not default values. If fields are +// invalid the ACH transfer will be returned. +func (addenda11 *Addenda11) fieldInclusion() error { + if addenda11.recordType == "" { + return &FieldError{FieldName: "recordType", Value: addenda11.recordType, Msg: msgFieldInclusion} + } + if addenda11.typeCode == "" { + return &FieldError{FieldName: "TypeCode", Value: addenda11.typeCode, Msg: msgFieldInclusion} + } + if addenda11.OriginatorName == "" { + return &FieldError{FieldName: "OriginatorName", + Value: addenda11.OriginatorName, Msg: msgFieldInclusion} + } + if addenda11.OriginatorStreetAddress == "" { + return &FieldError{FieldName: "OriginatorStreetAddress", + Value: addenda11.OriginatorStreetAddress, Msg: msgFieldInclusion} + } + if addenda11.EntryDetailSequenceNumber == 0 { + return &FieldError{FieldName: "EntryDetailSequenceNumber", + Value: addenda11.EntryDetailSequenceNumberField(), Msg: msgFieldInclusion} + } + return nil +} + +// OriginatorNameField gets the OriginatorName field - Originator Company Name/Individual Name left padded +func (addenda11 *Addenda11) OriginatorNameField() string { + return addenda11.alphaField(addenda11.OriginatorName, 35) +} + +// OriginatorStreetAddressField gets the OriginatorStreetAddress field - Originator Street Address left padded +func (addenda11 *Addenda11) OriginatorStreetAddressField() string { + return addenda11.alphaField(addenda11.OriginatorStreetAddress, 35) +} + +// reservedField gets reserved - blank space +func (addenda11 *Addenda11) reservedField() string { + return addenda11.alphaField(addenda11.reserved, 14) +} + +// TypeCode Defines the specific explanation and format for the addenda11 information left padded +func (addenda11 *Addenda11) TypeCode() string { + return addenda11.typeCode +} + +// EntryDetailSequenceNumberField returns a zero padded EntryDetailSequenceNumber string +func (addenda11 *Addenda11) EntryDetailSequenceNumberField() string { + return addenda11.numericField(addenda11.EntryDetailSequenceNumber, 7) +} diff --git a/addenda11_test.go b/addenda11_test.go new file mode 100644 index 000000000..283563545 --- /dev/null +++ b/addenda11_test.go @@ -0,0 +1,360 @@ +// 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" +) + +// mockAddenda11 creates a mock Addenda11 record +func mockAddenda11() *Addenda11 { + addenda11 := NewAddenda11() + addenda11.OriginatorName = "BEK Solutions" + addenda11.OriginatorStreetAddress = "15 West Place Street" + addenda11.EntryDetailSequenceNumber = 00000001 + return addenda11 +} + +// TestMockAddenda11 validates mockAddenda11 +func TestMockAddenda11(t *testing.T) { + addenda11 := mockAddenda11() + if err := addenda11.Validate(); err != nil { + t.Error("mockAddenda11 does not validate and will break other tests") + } +} + +// testAddenda11Parse parses Addenda11 record +func testAddenda11Parse(t testing.TB) { + Addenda11 := NewAddenda11() + line := "711BEK Solutions 15 West Place Street 0000001" + Addenda11.Parse(line) + // walk the Addenda11 struct + if Addenda11.recordType != "7" { + t.Errorf("expected %v got %v", "7", Addenda11.recordType) + } + if Addenda11.typeCode != "11" { + t.Errorf("expected %v got %v", "11", Addenda11.typeCode) + } + if Addenda11.OriginatorName != "BEK Solutions" { + t.Errorf("expected %v got %v", "BEK Solutions", Addenda11.OriginatorName) + } + if Addenda11.OriginatorStreetAddress != "15 West Place Street" { + t.Errorf("expected: %v got: %v", "15 West Place Street", Addenda11.OriginatorStreetAddress) + } + if Addenda11.reserved != " " { + t.Errorf("expected: %v got: %v", " ", Addenda11.reserved) + } + if Addenda11.EntryDetailSequenceNumber != 0000001 { + t.Errorf("expected: %v got: %v", 0000001, Addenda11.EntryDetailSequenceNumber) + } +} + +// TestAddenda11Parse tests parsing Addenda11 record +func TestAddenda11Parse(t *testing.T) { + testAddenda11Parse(t) +} + +// BenchmarkAddenda11Parse benchmarks parsing Addenda11 record +func BenchmarkAddenda11Parse(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda11Parse(b) + } +} + +// testAddenda11ValidRecordType validates Addenda11 recordType +func testAddenda11ValidRecordType(t testing.TB) { + addenda11 := mockAddenda11() + addenda11.recordType = "63" + if err := addenda11.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda11ValidRecordType tests validating Addenda11 recordType +func TestAddenda11ValidRecordType(t *testing.T) { + testAddenda11ValidRecordType(t) +} + +// BenchmarkAddenda11ValidRecordType benchmarks validating Addenda11 recordType +func BenchmarkAddenda11ValidRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda11ValidRecordType(b) + } +} + +// testAddenda11ValidTypeCode validates Addenda11 TypeCode +func testAddenda11ValidTypeCode(t testing.TB) { + addenda11 := mockAddenda11() + addenda11.typeCode = "65" + if err := addenda11.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda11ValidTypeCode tests validating Addenda11 TypeCode +func TestAddenda11ValidTypeCode(t *testing.T) { + testAddenda11ValidTypeCode(t) +} + +// BenchmarkAddenda11ValidTypeCode benchmarks validating Addenda11 TypeCode +func BenchmarkAddenda11ValidTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda11ValidTypeCode(b) + } +} + +// testAddenda11TypeCode11 TypeCode is 11 if typeCode is a valid TypeCode +func testAddenda11TypeCode11(t testing.TB) { + addenda11 := mockAddenda11() + addenda11.typeCode = "05" + if err := addenda11.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda11TypeCode11 tests TypeCode is 11 if typeCode is a valid TypeCode +func TestAddenda11TypeCode11(t *testing.T) { + testAddenda11TypeCode11(t) +} + +// BenchmarkAddenda11TypeCode11 benchmarks TypeCode is 11 if typeCode is a valid TypeCode +func BenchmarkAddenda11TypeCode11(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda11TypeCode11(b) + } +} + +// testOriginatorNameAlphaNumeric validates OriginatorName is alphanumeric +func testOriginatorNameAlphaNumeric(t testing.TB) { + addenda11 := mockAddenda11() + addenda11.OriginatorName = "BEK S®lutions" + if err := addenda11.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "OriginatorName" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestOriginatorNameAlphaNumeric tests validating OriginatorName is alphanumeric +func TestOriginatorNameAlphaNumeric(t *testing.T) { + testOriginatorNameAlphaNumeric(t) +} + +// BenchmarkOriginatorNameAlphaNumeric benchmarks validating OriginatorName is alphanumeric +func BenchmarkOriginatorNameAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testOriginatorNameAlphaNumeric(b) + } +} + +// testOriginatorStreetAddressAlphaNumeric validates OriginatorStreetAddress is alphanumeric +func testOriginatorStreetAddressAlphaNumeric(t testing.TB) { + addenda11 := mockAddenda11() + addenda11.OriginatorStreetAddress = "15 W®st" + if err := addenda11.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "OriginatorStreetAddress" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestOriginatorStreetAddressAlphaNumeric tests validating OriginatorStreetAddress is alphanumeric +func TestOriginatorStreetAddressAlphaNumeric(t *testing.T) { + testOriginatorStreetAddressAlphaNumeric(t) +} + +// BenchmarkOriginatorStreetAddressAlphaNumeric benchmarks validating OriginatorStreetAddress is alphanumeric +func BenchmarkOriginatorStreetAddressAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testOriginatorStreetAddressAlphaNumeric(b) + } +} + +// testAddenda11FieldInclusionRecordType validates recordType fieldInclusion +func testAddenda11FieldInclusionRecordType(t testing.TB) { + addenda11 := mockAddenda11() + addenda11.recordType = "" + if err := addenda11.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda11FieldInclusionRecordType tests validating recordType fieldInclusion +func TestAddenda11FieldInclusionRecordType(t *testing.T) { + testAddenda11FieldInclusionRecordType(t) +} + +// BenchmarkAddenda11FieldInclusionRecordType benchmarks validating recordType fieldInclusion +func BenchmarkAddenda11FieldInclusionRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda11FieldInclusionRecordType(b) + } +} + +// testAddenda11FieldInclusionTypeCode validates TypeCode fieldInclusion +func testAddenda11FieldInclusionTypeCode(t testing.TB) { + addenda11 := mockAddenda11() + addenda11.typeCode = "" + if err := addenda11.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda11FieldInclusionTypeCode tests validating TypeCode fieldInclusion +func TestAddenda11FieldInclusionTypeCode(t *testing.T) { + testAddenda11FieldInclusionTypeCode(t) +} + +// BenchmarkAddenda11FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion +func BenchmarkAddenda11FieldInclusionTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda11FieldInclusionTypeCode(b) + } +} + +// testAddenda11FieldInclusionOriginatorName validates OriginatorName fieldInclusion +func testAddenda11FieldInclusionOriginatorName(t testing.TB) { + addenda11 := mockAddenda11() + addenda11.OriginatorName = "" + if err := addenda11.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda11FieldInclusionOriginatorName tests validating OriginatorName fieldInclusion +func TestAddenda11FieldInclusionOriginatorName(t *testing.T) { + testAddenda11FieldInclusionOriginatorName(t) +} + +// BenchmarkAddenda11FieldInclusionOriginatorName benchmarks validating OriginatorName fieldInclusion +func BenchmarkAddenda11FieldInclusionOriginatorName(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda11FieldInclusionOriginatorName(b) + } +} + +// testAddenda11FieldInclusionOriginatorStreetAddress validates OriginatorStreetAddress fieldInclusion +func testAddenda11FieldInclusionOriginatorStreetAddress(t testing.TB) { + addenda11 := mockAddenda11() + addenda11.OriginatorStreetAddress = "" + if err := addenda11.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda11FieldInclusionOriginatorStreetAddress tests validating OriginatorStreetAddress fieldInclusion +func TestAddenda11FieldInclusionOriginatorStreetAddress(t *testing.T) { + testAddenda11FieldInclusionOriginatorStreetAddress(t) +} + +// BenchmarkAddenda11FieldInclusionOriginatorStreetAddress benchmarks validating OriginatorStreetAddress fieldInclusion +func BenchmarkAddenda11FieldInclusionOriginatorStreetAddress(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda11FieldInclusionOriginatorStreetAddress(b) + } +} + +// testAddenda11FieldInclusionEntryDetailSequenceNumber validates EntryDetailSequenceNumber fieldInclusion +func testAddenda11FieldInclusionEntryDetailSequenceNumber(t testing.TB) { + addenda11 := mockAddenda11() + addenda11.EntryDetailSequenceNumber = 0 + if err := addenda11.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda11FieldInclusionEntryDetailSequenceNumber tests validating +// EntryDetailSequenceNumber fieldInclusion +func TestAddenda11FieldInclusionEntryDetailSequenceNumber(t *testing.T) { + testAddenda11FieldInclusionEntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda11FieldInclusionEntryDetailSequenceNumber benchmarks validating +// EntryDetailSequenceNumber fieldInclusion +func BenchmarkAddenda11FieldInclusionEntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda11FieldInclusionEntryDetailSequenceNumber(b) + } +} + +// TestAddenda11String validates that a known parsed Addenda11 record can be return to a string of the same value +func testAddenda11String(t testing.TB) { + addenda11 := NewAddenda11() + var line = "711BEK Solutions 15 West Place Street 0000001" + addenda11.Parse(line) + + if addenda11.String() != line { + t.Errorf("Strings do not match") + } + if addenda11.TypeCode() != "11" { + t.Errorf("TypeCode Expected 11 got: %v", addenda11.TypeCode()) + } +} + +// TestAddenda11String tests validating that a known parsed Addenda11 record can be return to a string of the same value +func TestAddenda11String(t *testing.T) { + testAddenda11String(t) +} + +// BenchmarkAddenda11String benchmarks validating that a known parsed Addenda11 record can be return to a string of the same value +func BenchmarkAddenda11String(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda11String(b) + } +} diff --git a/addenda12.go b/addenda12.go new file mode 100644 index 000000000..054cda81a --- /dev/null +++ b/addenda12.go @@ -0,0 +1,159 @@ +// 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" + "strings" +) + +// Addenda12 is an addenda which provides business transaction information for Addenda Type +// Code 12 in a machine readable format. It is usually formatted according to ANSI, ASC, X12 Standard. +// +// Addenda12 is mandatory for IAT entries +// +// The Addenda12 record identifies key information related to the Originator of +// the entry. +type Addenda12 struct { + // ID is a client defined string used as a reference to this record. + ID string `json:"id"` + // RecordType defines the type of record in the block. + recordType string + // TypeCode Addenda12 types code '12' + typeCode string + // Originator City & State / Province + // Data elements City and State / Province should be separated with an asterisk (*) as a delimiter + // and the field should end with a backslash (\). + // For example: San FranciscoCA. + OriginatorCityStateProvince string `json:"originatorCityStateProvince"` + // Originator Country & Postal Code + // Data elements must be separated by an asterisk (*) and must end with a backslash (\) + // For example: US10036\ + OriginatorCountryPostalCode string `json:"originatorCountryPostalCode"` + // reserved - Leave blank + reserved string + // EntryDetailSequenceNumber contains the ascending sequence number section of the Entry + // 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 `json:"entryDetailSequenceNumber,omitempty"` + // validator is composed for data validation + validator + // converters is composed for ACH to GoLang Converters + converters +} + +// NewAddenda12 returns a new Addenda12 with default values for none exported fields +func NewAddenda12() *Addenda12 { + addenda12 := new(Addenda12) + addenda12.recordType = "7" + addenda12.typeCode = "12" + return addenda12 +} + +// Parse takes the input record string and parses the Addenda12 values +func (addenda12 *Addenda12) Parse(record string) { + // 1-1 Always "7" + addenda12.recordType = "7" + // 2-3 Always 12 + addenda12.typeCode = record[1:3] + // 4-38 + addenda12.OriginatorCityStateProvince = strings.TrimSpace(record[3:38]) + // 39-73 + addenda12.OriginatorCountryPostalCode = strings.TrimSpace(record[38:73]) + // 74-87 reserved - Leave blank + addenda12.reserved = " " + // 88-94 Contains the last seven digits of the number entered in the Trace Number field in the corresponding Entry Detail Record + addenda12.EntryDetailSequenceNumber = addenda12.parseNumField(record[87:94]) +} + +// String writes the Addenda12 struct to a 94 character string. +func (addenda12 *Addenda12) String() string { + return fmt.Sprintf("%v%v%v%v%v%v", + addenda12.recordType, + addenda12.typeCode, + addenda12.OriginatorCityStateProvinceField(), + // ToDo Validator for backslash + addenda12.OriginatorCountryPostalCodeField(), + addenda12.reservedField(), + addenda12.EntryDetailSequenceNumberField()) +} + +// Validate performs NACHA format rule checks on the record and returns an error if not Validated +// The first error encountered is returned and stops that parsing. +func (addenda12 *Addenda12) Validate() error { + if err := addenda12.fieldInclusion(); err != nil { + return err + } + if addenda12.recordType != "7" { + msg := fmt.Sprintf(msgRecordType, 7) + return &FieldError{FieldName: "recordType", Value: addenda12.recordType, Msg: msg} + } + if err := addenda12.isTypeCode(addenda12.typeCode); err != nil { + return &FieldError{FieldName: "TypeCode", Value: addenda12.typeCode, Msg: err.Error()} + } + // Type Code must be 12 + if addenda12.typeCode != "12" { + return &FieldError{FieldName: "TypeCode", Value: addenda12.typeCode, Msg: msgAddendaTypeCode} + } + if err := addenda12.isAlphanumeric(addenda12.OriginatorCityStateProvince); err != nil { + return &FieldError{FieldName: "OriginatorCityStateProvince", + Value: addenda12.OriginatorCityStateProvince, Msg: err.Error()} + } + if err := addenda12.isAlphanumeric(addenda12.OriginatorCountryPostalCode); err != nil { + return &FieldError{FieldName: "OriginatorCountryPostalCode", + Value: addenda12.OriginatorCountryPostalCode, Msg: err.Error()} + } + return nil +} + +// fieldInclusion validate mandatory fields are not default values. If fields are +// invalid the ACH transfer will be returned. +func (addenda12 *Addenda12) fieldInclusion() error { + if addenda12.recordType == "" { + return &FieldError{FieldName: "recordType", Value: addenda12.recordType, Msg: msgFieldInclusion} + } + if addenda12.typeCode == "" { + return &FieldError{FieldName: "TypeCode", Value: addenda12.typeCode, Msg: msgFieldInclusion} + } + if addenda12.OriginatorCityStateProvince == "" { + return &FieldError{FieldName: "OriginatorCityStateProvince", + Value: addenda12.OriginatorCityStateProvince, Msg: msgFieldInclusion} + } + if addenda12.OriginatorCountryPostalCode == "" { + return &FieldError{FieldName: "OriginatorCountryPostalCode", + Value: addenda12.OriginatorCountryPostalCode, Msg: msgFieldInclusion} + } + if addenda12.EntryDetailSequenceNumber == 0 { + return &FieldError{FieldName: "EntryDetailSequenceNumber", + Value: addenda12.EntryDetailSequenceNumberField(), Msg: msgFieldInclusion} + } + return nil +} + +// OriginatorCityStateProvinceField gets the OriginatorCityStateProvinceField left padded +func (addenda12 *Addenda12) OriginatorCityStateProvinceField() string { + return addenda12.alphaField(addenda12.OriginatorCityStateProvince, 35) +} + +// OriginatorCountryPostalCodeField gets the OriginatorCountryPostalCode field left padded +func (addenda12 *Addenda12) OriginatorCountryPostalCodeField() string { + return addenda12.alphaField(addenda12.OriginatorCountryPostalCode, 35) +} + +// reservedField gets reserved - blank space +func (addenda12 *Addenda12) reservedField() string { + return addenda12.alphaField(addenda12.reserved, 14) +} + +// TypeCode Defines the specific explanation and format for the addenda12 information left padded +func (addenda12 *Addenda12) TypeCode() string { + return addenda12.typeCode +} + +// EntryDetailSequenceNumberField returns a zero padded EntryDetailSequenceNumber string +func (addenda12 *Addenda12) EntryDetailSequenceNumberField() string { + return addenda12.numericField(addenda12.EntryDetailSequenceNumber, 7) +} diff --git a/addenda12_test.go b/addenda12_test.go new file mode 100644 index 000000000..4f8556e78 --- /dev/null +++ b/addenda12_test.go @@ -0,0 +1,366 @@ +// 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" +) + +// mockAddenda12 creates a mock Addenda12 record +func mockAddenda12() *Addenda12 { + addenda12 := NewAddenda12() + addenda12.OriginatorCityStateProvince = "JacobsTown*PA\\" + addenda12.OriginatorCountryPostalCode = "US*19305\\" + addenda12.EntryDetailSequenceNumber = 00000001 + return addenda12 +} + +// TestMockAddenda12 validates mockAddenda12 +func TestMockAddenda12(t *testing.T) { + addenda12 := mockAddenda12() + if err := addenda12.Validate(); err != nil { + t.Error("mockAddenda12 does not validate and will break other tests") + } +} + +// testAddenda12Parse parses Addenda12 record +func testAddenda12Parse(t testing.TB) { + Addenda12 := NewAddenda12() + line := "712" + "JacobsTown*PA\\ " + "US*19305\\ " + "0000001" + Addenda12.Parse(line) + // walk the Addenda12 struct + if Addenda12.recordType != "7" { + t.Errorf("expected %v got %v", "7", Addenda12.recordType) + } + if Addenda12.typeCode != "12" { + t.Errorf("expected %v got %v", "12", Addenda12.typeCode) + } + if Addenda12.OriginatorCityStateProvince != "JacobsTown*PA\\" { + t.Errorf("expected %v got %v", "JacobsTown*PA\\", Addenda12.OriginatorCityStateProvince) + } + if Addenda12.OriginatorCountryPostalCode != "US*19305\\" { + t.Errorf("expected: %v got: %v", "US*19305\\", Addenda12.OriginatorCountryPostalCode) + } + if Addenda12.reserved != " " { + t.Errorf("expected: %v got: %v", " ", Addenda12.reserved) + } + if Addenda12.EntryDetailSequenceNumber != 0000001 { + t.Errorf("expected: %v got: %v", 0000001, Addenda12.EntryDetailSequenceNumber) + } +} + +// TestAddenda12Parse tests parsing Addenda12 record +func TestAddenda12Parse(t *testing.T) { + testAddenda12Parse(t) +} + +// BenchmarkAddenda12Parse benchmarks parsing Addenda12 record +func BenchmarkAddenda12Parse(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda12Parse(b) + } +} + +// testAddenda12ValidRecordType validates Addenda12 recordType +func testAddenda12ValidRecordType(t testing.TB) { + addenda12 := mockAddenda12() + addenda12.recordType = "63" + if err := addenda12.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda12ValidRecordType tests validating Addenda12 recordType +func TestAddenda12ValidRecordType(t *testing.T) { + testAddenda12ValidRecordType(t) +} + +// BenchmarkAddenda12ValidRecordType benchmarks validating Addenda12 recordType +func BenchmarkAddenda12ValidRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda12ValidRecordType(b) + } +} + +// testAddenda12ValidTypeCode validates Addenda12 TypeCode +func testAddenda12ValidTypeCode(t testing.TB) { + addenda12 := mockAddenda12() + addenda12.typeCode = "65" + if err := addenda12.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda12ValidTypeCode tests validating Addenda12 TypeCode +func TestAddenda12ValidTypeCode(t *testing.T) { + testAddenda12ValidTypeCode(t) +} + +// BenchmarkAddenda12ValidTypeCode benchmarks validating Addenda12 TypeCode +func BenchmarkAddenda12ValidTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda12ValidTypeCode(b) + } +} + +// testAddenda12TypeCode12 TypeCode is 12 if typeCode is a valid TypeCode +func testAddenda12TypeCode12(t testing.TB) { + addenda12 := mockAddenda12() + addenda12.typeCode = "05" + if err := addenda12.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda12TypeCode12 tests TypeCode is 12 if typeCode is a valid TypeCode +func TestAddenda12TypeCode12(t *testing.T) { + testAddenda12TypeCode12(t) +} + +// BenchmarkAddenda12TypeCode12 benchmarks TypeCode is 12 if typeCode is a valid TypeCode +func BenchmarkAddenda12TypeCode12(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda12TypeCode12(b) + } +} + +// testOriginatorCityStateProvinceAlphaNumeric validates OriginatorCityStateProvince is alphanumeric +func testOriginatorCityStateProvinceAlphaNumeric(t testing.TB) { + addenda12 := mockAddenda12() + addenda12.OriginatorCityStateProvince = "Jacobs®Town*PA" + if err := addenda12.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "OriginatorCityStateProvince" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestOriginatorCityStateProvinceAlphaNumeric tests validating OriginatorCityStateProvince is alphanumeric +func TestOriginatorCityStateProvinceAlphaNumeric(t *testing.T) { + testOriginatorCityStateProvinceAlphaNumeric(t) +} + +// BenchmarkOriginatorCityStateProvinceAlphaNumeric benchmarks validating OriginatorCityStateProvince is alphanumeric +func BenchmarkOriginatorCityStateProvinceAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testOriginatorCityStateProvinceAlphaNumeric(b) + } +} + +// testOriginatorCountryPostalCodeAlphaNumeric validates OriginatorCountryPostalCode is alphanumeric +func testOriginatorCountryPostalCodeAlphaNumeric(t testing.TB) { + addenda12 := mockAddenda12() + addenda12.OriginatorCountryPostalCode = "US19®305" + if err := addenda12.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "OriginatorCountryPostalCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestOriginatorCountryPostalCodeAlphaNumeric tests validating OriginatorCountryPostalCode is alphanumeric +func TestOriginatorCountryPostalCodeAlphaNumeric(t *testing.T) { + testOriginatorCountryPostalCodeAlphaNumeric(t) +} + +// BenchmarkOriginatorCountryPostalCodeAlphaNumeric benchmarks validating OriginatorCountryPostalCode is alphanumeric +func BenchmarkOriginatorCountryPostalCodeAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testOriginatorCountryPostalCodeAlphaNumeric(b) + } +} + +// testAddenda12FieldInclusionRecordType validates recordType fieldInclusion +func testAddenda12FieldInclusionRecordType(t testing.TB) { + addenda12 := mockAddenda12() + addenda12.recordType = "" + if err := addenda12.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda12FieldInclusionRecordType tests validating recordType fieldInclusion +func TestAddenda12FieldInclusionRecordType(t *testing.T) { + testAddenda12FieldInclusionRecordType(t) +} + +// BenchmarkAddenda12FieldInclusionRecordType benchmarks validating recordType fieldInclusion +func BenchmarkAddenda12FieldInclusionRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda12FieldInclusionRecordType(b) + } +} + +// testAddenda12FieldInclusionTypeCode validates TypeCode fieldInclusion +func testAddenda12FieldInclusionTypeCode(t testing.TB) { + addenda12 := mockAddenda12() + addenda12.typeCode = "" + if err := addenda12.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda12FieldInclusionTypeCode tests validating TypeCode fieldInclusion +func TestAddenda12FieldInclusionTypeCode(t *testing.T) { + testAddenda12FieldInclusionTypeCode(t) +} + +// BenchmarkAddenda12FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion +func BenchmarkAddenda12FieldInclusionTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda12FieldInclusionTypeCode(b) + } +} + +// testAddenda12FieldInclusionOriginatorCityStateProvince validates OriginatorCityStateProvince fieldInclusion +func testAddenda12FieldInclusionOriginatorCityStateProvince(t testing.TB) { + addenda12 := mockAddenda12() + addenda12.OriginatorCityStateProvince = "" + if err := addenda12.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda12FieldInclusionOriginatorCityStateProvince tests validating OriginatorCityStateProvince fieldInclusion +func TestAddenda12FieldInclusionOriginatorCityStateProvince(t *testing.T) { + testAddenda12FieldInclusionOriginatorCityStateProvince(t) +} + +// BenchmarkAddenda12FieldInclusionOriginatorCityStateProvince benchmarks validating OriginatorCityStateProvince fieldInclusion +func BenchmarkAddenda12FieldInclusionOriginatorCityStateProvince(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda12FieldInclusionOriginatorCityStateProvince(b) + } +} + +// testAddenda12FieldInclusionOriginatorCountryPostalCode validates OriginatorCountryPostalCode fieldInclusion +func testAddenda12FieldInclusionOriginatorCountryPostalCode(t testing.TB) { + addenda12 := mockAddenda12() + addenda12.OriginatorCountryPostalCode = "" + if err := addenda12.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda12FieldInclusionOriginatorCountryPostalCode tests validating OriginatorCountryPostalCode fieldInclusion +func TestAddenda12FieldInclusionOriginatorCountryPostalCode(t *testing.T) { + testAddenda12FieldInclusionOriginatorCountryPostalCode(t) +} + +// BenchmarkAddenda12FieldInclusionOriginatorCountryPostalCode benchmarks validating OriginatorCountryPostalCode fieldInclusion +func BenchmarkAddenda12FieldInclusionOriginatorCountryPostalCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda12FieldInclusionOriginatorCountryPostalCode(b) + } +} + +// testAddenda12FieldInclusionEntryDetailSequenceNumber validates EntryDetailSequenceNumber fieldInclusion +func testAddenda12FieldInclusionEntryDetailSequenceNumber(t testing.TB) { + addenda12 := mockAddenda12() + addenda12.EntryDetailSequenceNumber = 0 + if err := addenda12.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda12FieldInclusionEntryDetailSequenceNumber tests validating +// EntryDetailSequenceNumber fieldInclusion +func TestAddenda12FieldInclusionEntryDetailSequenceNumber(t *testing.T) { + testAddenda12FieldInclusionEntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda12FieldInclusionEntryDetailSequenceNumber benchmarks validating +// EntryDetailSequenceNumber fieldInclusion +func BenchmarkAddenda12FieldInclusionEntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda12FieldInclusionEntryDetailSequenceNumber(b) + } +} + +// TestAddenda12String validates that a known parsed Addenda12 record can be return to a string of the same value +func testAddenda12String(t testing.TB) { + addenda12 := NewAddenda12() + // Backslash logic + var line = "712" + + "JacobsTown*PA\\ " + + "US*19305\\ " + + " " + + "0000001" + + addenda12.Parse(line) + + if addenda12.String() != line { + t.Errorf("Strings do not match") + } + if addenda12.TypeCode() != "12" { + t.Errorf("TypeCode Expected 12 got: %v", addenda12.TypeCode()) + } +} + +// TestAddenda12String tests validating that a known parsed Addenda12 record can be return to a string of the same value +func TestAddenda12String(t *testing.T) { + testAddenda12String(t) +} + +// BenchmarkAddenda12String benchmarks validating that a known parsed Addenda12 record can be return to a string of the same value +func BenchmarkAddenda12String(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda12String(b) + } +} diff --git a/addenda13.go b/addenda13.go new file mode 100644 index 000000000..3a154f1a6 --- /dev/null +++ b/addenda13.go @@ -0,0 +1,207 @@ +// 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" + "strings" +) + +// Addenda13 is an addenda which provides business transaction information for Addenda Type +// Code 13 in a machine readable format. It is usually formatted according to ANSI, ASC, X13 Standard. +// +// Addenda13 is mandatory for IAT entries +// +// The Addenda13 contains information related to the financial institution originating the entry. +// For inbound IAT entries, the Fourth Addenda Record must contain information to identify the +// foreign financial institution that is providing the funding and payment instruction for the IAT entry. +type Addenda13 struct { + // ID is a client defined string used as a reference to this record. + ID string `json:"id"` + // RecordType defines the type of record in the block. + recordType string + // TypeCode Addenda13 types code '13' + typeCode string + // Originating DFI Name + // For Outbound IAT Entries, this field must contain the name of the U.S. ODFI. + // For Inbound IATs: Name of the foreign bank providing funding for the payment transaction + ODFIName string `json:"ODFIName"` + // Originating DFI Identification Number Qualifier + // For Inbound IATs: The 2-digit code that identifies the numbering scheme used in the + // Foreign DFI Identification Number field: + // 01 = National Clearing System + // 02 = BIC Code + // 03 = IBAN Code + ODFIIDNumberQualifier string `json:"ODFIIDNumberQualifier"` + // Originating DFI Identification + // This field contains the routing number that identifies the U.S. ODFI initiating the entry. + // For Inbound IATs: This field contains the bank ID number of the Foreign Bank providing funding + // for the payment transaction. + ODFIIdentification string `json:"ODFIIdentification"` + // Originating DFI Branch Country Code + // USb” = United States + //(“b” indicates a blank space) + // For Inbound IATs: This 3 position field contains a 2-character code as approved by the + // International Organization for Standardization (ISO) used to identify the country in which + // the branch of the bank that originated the entry is located. Values for other countries can + // be found on the International Organization for Standardization website: www.iso.org. + ODFIBranchCountryCode string `json:"ODFIBranchCountryCode"` + // reserved - Leave blank + reserved string + // EntryDetailSequenceNumber contains the ascending sequence number section of the Entry + // 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 `json:"entryDetailSequenceNumber,omitempty"` + // validator is composed for data validation + validator + // converters is composed for ACH to GoLang Converters + converters +} + +// NewAddenda13 returns a new Addenda13 with default values for none exported fields +func NewAddenda13() *Addenda13 { + addenda13 := new(Addenda13) + addenda13.recordType = "7" + addenda13.typeCode = "13" + return addenda13 +} + +// Parse takes the input record string and parses the Addenda13 values +func (addenda13 *Addenda13) Parse(record string) { + // 1-1 Always "7" + addenda13.recordType = "7" + // 2-3 Always 13 + addenda13.typeCode = record[1:3] + // 4-38 ODFIName + addenda13.ODFIName = strings.TrimSpace(record[3:38]) + // 39-40 ODFIIDNumberQualifier + addenda13.ODFIIDNumberQualifier = record[38:40] + // 41-74 ODFIIdentification + addenda13.ODFIIdentification = addenda13.parseStringField(record[40:74]) + // 75-77 + addenda13.ODFIBranchCountryCode = strings.TrimSpace(record[74:77]) + // 78-87 reserved - Leave blank + addenda13.reserved = " " + // 88-94 Contains the last seven digits of the number entered in the Trace Number field in the corresponding Entry Detail Record + addenda13.EntryDetailSequenceNumber = addenda13.parseNumField(record[87:94]) +} + +// String writes the Addenda13 struct to a 94 character string. +func (addenda13 *Addenda13) String() string { + return fmt.Sprintf("%v%v%v%v%v%v%v%v", + addenda13.recordType, + addenda13.typeCode, + addenda13.ODFINameField(), + addenda13.ODFIIDNumberQualifierField(), + addenda13.ODFIIdentificationField(), + addenda13.ODFIBranchCountryCodeField(), + addenda13.reservedField(), + addenda13.EntryDetailSequenceNumberField()) +} + +// Validate performs NACHA format rule checks on the record and returns an error if not Validated +// The first error encountered is returned and stops that parsing. +func (addenda13 *Addenda13) Validate() error { + if err := addenda13.fieldInclusion(); err != nil { + return err + } + if addenda13.recordType != "7" { + msg := fmt.Sprintf(msgRecordType, 7) + return &FieldError{FieldName: "recordType", Value: addenda13.recordType, Msg: msg} + } + if err := addenda13.isTypeCode(addenda13.typeCode); err != nil { + return &FieldError{FieldName: "TypeCode", Value: addenda13.typeCode, Msg: err.Error()} + } + // Type Code must be 13 + if addenda13.typeCode != "13" { + return &FieldError{FieldName: "TypeCode", Value: addenda13.typeCode, Msg: msgAddendaTypeCode} + } + if err := addenda13.isAlphanumeric(addenda13.ODFIName); err != nil { + return &FieldError{FieldName: "ODFIName", + Value: addenda13.ODFIName, Msg: err.Error()} + } + // Valid ODFI Identification Number Qualifier + if err := addenda13.isIDNumberQualifier(addenda13.ODFIIDNumberQualifier); err != nil { + return &FieldError{FieldName: "ODFIIDNumberQualifier", + Value: addenda13.ODFIIDNumberQualifier, Msg: err.Error()} + } + if err := addenda13.isAlphanumeric(addenda13.ODFIIdentification); err != nil { + return &FieldError{FieldName: "ODFIIdentification", + Value: addenda13.ODFIIdentification, Msg: err.Error()} + } + if err := addenda13.isAlphanumeric(addenda13.ODFIBranchCountryCode); err != nil { + return &FieldError{FieldName: "ODFIBranchCountryCode", + Value: addenda13.ODFIBranchCountryCode, Msg: err.Error()} + } + return nil +} + +// fieldInclusion validate mandatory fields are not default values. If fields are +// invalid the ACH transfer will be returned. +func (addenda13 *Addenda13) fieldInclusion() error { + if addenda13.recordType == "" { + return &FieldError{FieldName: "recordType", Value: addenda13.recordType, Msg: msgFieldInclusion} + } + if addenda13.typeCode == "" { + return &FieldError{FieldName: "TypeCode", Value: addenda13.typeCode, Msg: msgFieldInclusion} + } + if addenda13.ODFIName == "" { + return &FieldError{FieldName: "ODFIName", + Value: addenda13.ODFIName, Msg: msgFieldInclusion} + } + if addenda13.ODFIIDNumberQualifier == "" { + return &FieldError{FieldName: "ODFIIDNumberQualifier", + Value: addenda13.ODFIIDNumberQualifier, Msg: msgFieldInclusion} + } + if addenda13.ODFIIdentification == "" { + return &FieldError{FieldName: "ODFIIdentification", + Value: addenda13.ODFIIdentification, Msg: msgFieldInclusion} + } + if addenda13.ODFIBranchCountryCode == "" { + return &FieldError{FieldName: "ODFIBranchCountryCode", + Value: addenda13.ODFIBranchCountryCode, Msg: msgFieldInclusion} + } + if addenda13.EntryDetailSequenceNumber == 0 { + return &FieldError{FieldName: "EntryDetailSequenceNumber", + Value: addenda13.EntryDetailSequenceNumberField(), Msg: msgFieldInclusion} + } + return nil +} + +// ODFINameField gets the ODFIName field left padded +func (addenda13 *Addenda13) ODFINameField() string { + return addenda13.alphaField(addenda13.ODFIName, 35) +} + +// ODFIIDNumberQualifierField gets the ODFIIDNumberQualifier field left padded +func (addenda13 *Addenda13) ODFIIDNumberQualifierField() string { + return addenda13.alphaField(addenda13.ODFIIDNumberQualifier, 2) +} + +// ODFIIdentificationField gets the ODFIIdentificationCode field left padded +func (addenda13 *Addenda13) ODFIIdentificationField() string { + return addenda13.alphaField(addenda13.ODFIIdentification, 34) +} + +// ODFIBranchCountryCodeField gets the ODFIBranchCountryCode field left padded +func (addenda13 *Addenda13) ODFIBranchCountryCodeField() string { + return addenda13.alphaField(addenda13.ODFIBranchCountryCode, 3) +} + +// reservedField gets reserved - blank space +func (addenda13 *Addenda13) reservedField() string { + return addenda13.alphaField(addenda13.reserved, 10) +} + +// TypeCode Defines the specific explanation and format for the addenda13 information left padded +func (addenda13 *Addenda13) TypeCode() string { + return addenda13.typeCode +} + +// EntryDetailSequenceNumberField returns a zero padded EntryDetailSequenceNumber string +func (addenda13 *Addenda13) EntryDetailSequenceNumberField() string { + return addenda13.numericField(addenda13.EntryDetailSequenceNumber, 7) +} diff --git a/addenda13_test.go b/addenda13_test.go new file mode 100644 index 000000000..f454d391d --- /dev/null +++ b/addenda13_test.go @@ -0,0 +1,472 @@ +// 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" +) + +// mockAddenda13 creates a mock Addenda13 record +func mockAddenda13() *Addenda13 { + addenda13 := NewAddenda13() + addenda13.ODFIName = "Wells Fargo" + addenda13.ODFIIDNumberQualifier = "01" + addenda13.ODFIIdentification = "121042882" + addenda13.ODFIBranchCountryCode = "US" + addenda13.EntryDetailSequenceNumber = 00000001 + return addenda13 +} + +// testAddenda13Parse parses Addenda13 record +func testAddenda13Parse(t testing.TB) { + Addenda13 := NewAddenda13() + line := "713Wells Fargo 01121042882 US 0000001" + Addenda13.Parse(line) + // walk the Addenda13 struct + if Addenda13.recordType != "7" { + t.Errorf("expected %v got %v", "7", Addenda13.recordType) + } + if Addenda13.typeCode != "13" { + t.Errorf("expected %v got %v", "13", Addenda13.typeCode) + } + if Addenda13.ODFIName != "Wells Fargo" { + t.Errorf("expected %v got %v", "Wells Fargo", Addenda13.ODFIName) + } + if Addenda13.ODFIIDNumberQualifier != "01" { + t.Errorf("expected: %v got: %v", "01", Addenda13.ODFIIDNumberQualifier) + } + if Addenda13.ODFIIdentification != "121042882" { + t.Errorf("expected: %v got: %v", "121042882", Addenda13.ODFIIdentification) + } + if Addenda13.ODFIBranchCountryCode != "US" { + t.Errorf("expected: %s got: %s", "US", Addenda13.ODFIBranchCountryCode) + } + if Addenda13.reserved != " " { + t.Errorf("expected: %v got: %v", " ", Addenda13.reserved) + } + if Addenda13.EntryDetailSequenceNumber != 0000001 { + t.Errorf("expected: %v got: %v", 0000001, Addenda13.EntryDetailSequenceNumber) + } +} + +// TestAddenda13Parse tests parsing Addenda13 record +func TestAddenda13Parse(t *testing.T) { + testAddenda13Parse(t) +} + +// BenchmarkAddenda13Parse benchmarks parsing Addenda13 record +func BenchmarkAddenda13Parse(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda13Parse(b) + } +} + +// TestMockAddenda13 validates mockAddenda13 +func TestMockAddenda13(t *testing.T) { + addenda13 := mockAddenda13() + if err := addenda13.Validate(); err != nil { + t.Error("mockAddenda13 does not validate and will break other tests") + } +} + +// testAddenda13ValidRecordType validates Addenda13 recordType +func testAddenda13ValidRecordType(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.recordType = "63" + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda13ValidRecordType tests validating Addenda13 recordType +func TestAddenda13ValidRecordType(t *testing.T) { + testAddenda13ValidRecordType(t) +} + +// BenchmarkAddenda13ValidRecordType benchmarks validating Addenda13 recordType +func BenchmarkAddenda13ValidRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda13ValidRecordType(b) + } +} + +// testAddenda13ValidTypeCode validates Addenda13 TypeCode +func testAddenda13ValidTypeCode(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.typeCode = "65" + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda13ValidTypeCode tests validating Addenda13 TypeCode +func TestAddenda13ValidTypeCode(t *testing.T) { + testAddenda13ValidTypeCode(t) +} + +// BenchmarkAddenda13ValidTypeCode benchmarks validating Addenda13 TypeCode +func BenchmarkAddenda13ValidTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda13ValidTypeCode(b) + } +} + +// testAddenda13TypeCode13 TypeCode is 13 if typeCode is a valid TypeCode +func testAddenda13TypeCode13(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.typeCode = "05" + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda13TypeCode13 tests TypeCode is 13 if typeCode is a valid TypeCode +func TestAddenda13TypeCode13(t *testing.T) { + testAddenda13TypeCode13(t) +} + +// BenchmarkAddenda13TypeCode13 benchmarks TypeCode is 13 if typeCode is a valid TypeCode +func BenchmarkAddenda13TypeCode13(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda13TypeCode13(b) + } +} + +// testODFINameAlphaNumeric validates ODFIName is alphanumeric +func testODFINameAlphaNumeric(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.ODFIName = "Wells®Fargo" + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ODFIName" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestODFINameAlphaNumeric tests validating ODFIName is alphanumeric +func TestODFINameAlphaNumeric(t *testing.T) { + testODFINameAlphaNumeric(t) +} + +// BenchmarkODFINameAlphaNumeric benchmarks validating ODFIName is alphanumeric +func BenchmarkODFINameAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testODFINameAlphaNumeric(b) + } +} + +// testODFIIDNumberQualifierValid validates ODFIIDNumberQualifier is valid +func testODFIIDNumberQualifierValid(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.ODFIIDNumberQualifier = "®1" + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ODFIIDNumberQualifier" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestODFIIDNumberQualifierValid tests validating ODFIIDNumberQualifier is valid +func TestODFIIDNumberQualifierValid(t *testing.T) { + testODFIIDNumberQualifierValid(t) +} + +// BenchmarkODFIIDNumberQualifierValid benchmarks validating ODFIIDNumberQualifier is valid +func BenchmarkODFIIDNumberQualifierValid(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testODFIIDNumberQualifierValid(b) + } +} + +// testODFIIdentificationAlphaNumeric validates ODFIIdentification is alphanumeric +func testODFIIdentificationAlphaNumeric(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.ODFIIdentification = "®121042882" + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ODFIIdentification" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestODFIIdentificationAlphaNumeric tests validating ODFIIdentification is alphanumeric +func TestODFIIdentificationAlphaNumeric(t *testing.T) { + testODFIIdentificationAlphaNumeric(t) +} + +// BenchmarkODFIIdentificationAlphaNumeric benchmarks validating ODFIIdentification is alphanumeric +func BenchmarkODFIIdentificationAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testODFIIdentificationAlphaNumeric(b) + } +} + +// testODFIBranchCountryCodeAlphaNumeric validates ODFIBranchCountryCode is alphanumeric +func testODFIBranchCountryCodeAlphaNumeric(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.ODFIBranchCountryCode = "U®" + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ODFIBranchCountryCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestODFIBranchCountryCodeAlphaNumeric tests validating ODFIBranchCountryCode is alphanumeric +func TestODFIBranchCountryCodeAlphaNumeric(t *testing.T) { + testODFIBranchCountryCodeAlphaNumeric(t) +} + +// BenchmarkODFIBranchCountryCodeAlphaNumeric benchmarks validating ODFIBranchCountryCode is alphanumeric +func BenchmarkODFIBranchCountryCodeAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testODFIBranchCountryCodeAlphaNumeric(b) + } +} + +// testAddenda13FieldInclusionRecordType validates recordType fieldInclusion +func testAddenda13FieldInclusionRecordType(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.recordType = "" + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda13FieldInclusionRecordType tests validating recordType fieldInclusion +func TestAddenda13FieldInclusionRecordType(t *testing.T) { + testAddenda13FieldInclusionRecordType(t) +} + +// BenchmarkAddenda13FieldInclusionRecordType benchmarks validating recordType fieldInclusion +func BenchmarkAddenda13FieldInclusionRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda13FieldInclusionRecordType(b) + } +} + +// testAddenda13FieldInclusionTypeCode validates TypeCode fieldInclusion +func testAddenda13FieldInclusionTypeCode(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.typeCode = "" + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda13FieldInclusionTypeCode tests validating TypeCode fieldInclusion +func TestAddenda13FieldInclusionTypeCode(t *testing.T) { + testAddenda13FieldInclusionTypeCode(t) +} + +// BenchmarkAddenda13FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion +func BenchmarkAddenda13FieldInclusionTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda13FieldInclusionTypeCode(b) + } +} + +// testAddenda13FieldInclusionODFIName validates ODFIName fieldInclusion +func testAddenda13FieldInclusionODFIName(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.ODFIName = "" + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda13FieldInclusionODFIName tests validating ODFIName fieldInclusion +func TestAddenda13FieldInclusionODFIName(t *testing.T) { + testAddenda13FieldInclusionODFIName(t) +} + +// BenchmarkAddenda13FieldInclusionODFIName benchmarks validating ODFIName fieldInclusion +func BenchmarkAddenda13FieldInclusionODFIName(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda13FieldInclusionODFIName(b) + } +} + +// testAddenda13FieldInclusionODFIIDNumberQualifier validates ODFIIDNumberQualifier fieldInclusion +func testAddenda13FieldInclusionODFIIDNumberQualifier(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.ODFIIDNumberQualifier = "" + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda13FieldInclusionODFIIDNumberQualifier tests validating ODFIIDNumberQualifier fieldInclusion +func TestAddenda13FieldInclusionODFIIDNumberQualifier(t *testing.T) { + testAddenda13FieldInclusionODFIIDNumberQualifier(t) +} + +// BenchmarkAddenda13FieldInclusionODFIIDNumberQualifier benchmarks validating ODFIIDNumberQualifier fieldInclusion +func BenchmarkAddenda13FieldInclusionODFIIDNumberQualifier(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda13FieldInclusionODFIIDNumberQualifier(b) + } +} + +// testAddenda13FieldInclusionODFIIdentification validates ODFIIdentification fieldInclusion +func testAddenda13FieldInclusionODFIIdentification(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.ODFIIdentification = "" + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda13FieldInclusionODFIIdentification tests validating ODFIIdentification fieldInclusion +func TestAddenda13FieldInclusionODFIIdentification(t *testing.T) { + testAddenda13FieldInclusionODFIIdentification(t) +} + +// BenchmarkAddenda13FieldInclusionODFIIdentification benchmarks validating ODFIIdentification fieldInclusion +func BenchmarkAddenda13FieldInclusionODFIIdentification(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda13FieldInclusionODFIIdentification(b) + } +} + +// testAddenda13FieldInclusionODFIBranchCountryCode validates ODFIBranchCountryCode fieldInclusion +func testAddenda13FieldInclusionODFIBranchCountryCode(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.ODFIBranchCountryCode = "" + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda13FieldInclusionODFIBranchCountryCode tests validating ODFIBranchCountryCode fieldInclusion +func TestAddenda13FieldInclusionODFIBranchCountryCode(t *testing.T) { + testAddenda13FieldInclusionODFIBranchCountryCode(t) +} + +// BenchmarkAddenda13FieldInclusionODFIBranchCountryCode benchmarks validating ODFIBranchCountryCode fieldInclusion +func BenchmarkAddenda13FieldInclusionODFIBranchCountryCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda13FieldInclusionODFIBranchCountryCode(b) + } +} + +// testAddenda13FieldInclusionEntryDetailSequenceNumber validates EntryDetailSequenceNumber fieldInclusion +func testAddenda13FieldInclusionEntryDetailSequenceNumber(t testing.TB) { + addenda13 := mockAddenda13() + addenda13.EntryDetailSequenceNumber = 0 + if err := addenda13.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda13FieldInclusionEntryDetailSequenceNumber tests validating +// EntryDetailSequenceNumber fieldInclusion +func TestAddenda13FieldInclusionEntryDetailSequenceNumber(t *testing.T) { + testAddenda13FieldInclusionEntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda13FieldInclusionEntryDetailSequenceNumber benchmarks validating +// EntryDetailSequenceNumber fieldInclusion +func BenchmarkAddenda13FieldInclusionEntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda13FieldInclusionEntryDetailSequenceNumber(b) + } +} + +// TestAddenda13String validates that a known parsed Addenda13 record can be return to a string of the same value +func testAddenda13String(t testing.TB) { + addenda13 := NewAddenda13() + var line = "713Wells Fargo 121042882 US 0000001" + addenda13.Parse(line) + + if addenda13.String() != line { + t.Errorf("Strings do not match") + } + if addenda13.TypeCode() != "13" { + t.Errorf("TypeCode Expected 13 got: %v", addenda13.TypeCode()) + } +} + +// TestAddenda13String tests validating that a known parsed Addenda13 record can be return to a string of the same value +func TestAddenda13String(t *testing.T) { + testAddenda13String(t) +} + +// BenchmarkAddenda13String benchmarks validating that a known parsed Addenda13 record can be return to a string of the same value +func BenchmarkAddenda13String(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda13String(b) + } +} diff --git a/addenda14.go b/addenda14.go new file mode 100644 index 000000000..1c9ff163f --- /dev/null +++ b/addenda14.go @@ -0,0 +1,203 @@ +// 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" + "strings" +) + +// Addenda14 is an addenda which provides business transaction information for Addenda Type +// Code 14 in a machine readable format. It is usually formatted according to ANSI, ASC, X14 Standard. +// +// Addenda14 is mandatory for IAT entries +// +// The Addenda14 identifies the Receiving financial institution holding the Receiver's account. +type Addenda14 struct { + // ID is a client defined string used as a reference to this record. + ID string `json:"id"` + // RecordType defines the type of record in the block. + recordType string + // TypeCode Addenda14 types code '14' + typeCode string + // Receiving DFI Name + // Name of the Receiver's bank + RDFIName string `json:"RDFIName"` + // Receiving DFI Identification Number Qualifier + // The 2-digit code that identifies the numbering scheme used in the + // Receiving DFI Identification Number field:: + // 01 = National Clearing System + // 02 = BIC Code + // 03 = IBAN Code + RDFIIDNumberQualifier string `json:"RDFIIDNumberQualifier"` + // Receiving DFI Identification + // This field contains the bank identification number of the DFI at which the + // Receiver maintains his account. + RDFIIdentification string `json:"RDFIIdentification"` + // Receiving DFI Branch Country Code + // USb” = United States + //(“b” indicates a blank space) + // This 3 position field contains a 2-character code as approved by the International + // Organization for Standardization (ISO) used to identify the country in which the + // branch of the bank that receives the entry is located. Values for other countries can + // be found on the International Organization for Standardization website: www.iso.org + RDFIBranchCountryCode string `json:"RDFIBranchCountryCode"` + // reserved - Leave blank + reserved string + // EntryDetailSequenceNumber contains the ascending sequence number section of the Entry + // 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 `json:"entryDetailSequenceNumber,omitempty"` + // validator is composed for data validation + validator + // converters is composed for ACH to GoLang Converters + converters +} + +// NewAddenda14 returns a new Addenda14 with default values for none exported fields +func NewAddenda14() *Addenda14 { + addenda14 := new(Addenda14) + addenda14.recordType = "7" + addenda14.typeCode = "14" + return addenda14 +} + +// Parse takes the input record string and parses the Addenda14 values +func (addenda14 *Addenda14) Parse(record string) { + // 1-1 Always "7" + addenda14.recordType = "7" + // 2-3 Always 14 + addenda14.typeCode = record[1:3] + // 4-38 RDFIName + addenda14.RDFIName = strings.TrimSpace(record[3:38]) + // 39-40 RDFIIDNumberQualifier + addenda14.RDFIIDNumberQualifier = record[38:40] + // 41-74 RDFIIdentification + addenda14.RDFIIdentification = addenda14.parseStringField(record[40:74]) + // 75-77 + addenda14.RDFIBranchCountryCode = strings.TrimSpace(record[74:77]) + // 78-87 reserved - Leave blank + addenda14.reserved = " " + // 88-94 Contains the last seven digits of the number entered in the Trace Number field in the corresponding Entry Detail Record + addenda14.EntryDetailSequenceNumber = addenda14.parseNumField(record[87:94]) +} + +// String writes the Addenda14 struct to a 94 character string. +func (addenda14 *Addenda14) String() string { + return fmt.Sprintf("%v%v%v%v%v%v%v%v", + addenda14.recordType, + addenda14.typeCode, + addenda14.RDFINameField(), + addenda14.RDFIIDNumberQualifierField(), + addenda14.RDFIIdentificationField(), + addenda14.RDFIBranchCountryCodeField(), + addenda14.reservedField(), + addenda14.EntryDetailSequenceNumberField()) +} + +// Validate performs NACHA format rule checks on the record and returns an error if not Validated +// The first error encountered is returned and stops that parsing. +func (addenda14 *Addenda14) Validate() error { + if err := addenda14.fieldInclusion(); err != nil { + return err + } + if addenda14.recordType != "7" { + msg := fmt.Sprintf(msgRecordType, 7) + return &FieldError{FieldName: "recordType", Value: addenda14.recordType, Msg: msg} + } + if err := addenda14.isTypeCode(addenda14.typeCode); err != nil { + return &FieldError{FieldName: "TypeCode", Value: addenda14.typeCode, Msg: err.Error()} + } + // Type Code must be 14 + if addenda14.typeCode != "14" { + return &FieldError{FieldName: "TypeCode", Value: addenda14.typeCode, Msg: msgAddendaTypeCode} + } + if err := addenda14.isAlphanumeric(addenda14.RDFIName); err != nil { + return &FieldError{FieldName: "RDFIName", + Value: addenda14.RDFIName, Msg: err.Error()} + } + // Valid RDFI Identification Number Qualifier + if err := addenda14.isIDNumberQualifier(addenda14.RDFIIDNumberQualifier); err != nil { + return &FieldError{FieldName: "RDFIIDNumberQualifier", + Value: addenda14.RDFIIDNumberQualifier, Msg: msgIDNumberQualifier} + } + if err := addenda14.isAlphanumeric(addenda14.RDFIIdentification); err != nil { + return &FieldError{FieldName: "RDFIIdentification", + Value: addenda14.RDFIIdentification, Msg: err.Error()} + } + if err := addenda14.isAlphanumeric(addenda14.RDFIBranchCountryCode); err != nil { + return &FieldError{FieldName: "RDFIBranchCountryCode", + Value: addenda14.RDFIBranchCountryCode, Msg: err.Error()} + } + return nil +} + +// fieldInclusion validate mandatory fields are not default values. If fields are +// invalid the ACH transfer will be returned. +func (addenda14 *Addenda14) fieldInclusion() error { + if addenda14.recordType == "" { + return &FieldError{FieldName: "recordType", Value: addenda14.recordType, Msg: msgFieldInclusion} + } + if addenda14.typeCode == "" { + return &FieldError{FieldName: "TypeCode", Value: addenda14.typeCode, Msg: msgFieldInclusion} + } + if addenda14.RDFIName == "" { + return &FieldError{FieldName: "RDFIName", + Value: addenda14.RDFIName, Msg: msgFieldInclusion} + } + if addenda14.RDFIIDNumberQualifier == "" { + return &FieldError{FieldName: "RDFIIDNumberQualifier", + Value: addenda14.RDFIIDNumberQualifier, Msg: msgFieldInclusion} + } + if addenda14.RDFIIdentification == "" { + return &FieldError{FieldName: "RDFIIdentification", + Value: addenda14.RDFIIdentification, Msg: msgFieldInclusion} + } + if addenda14.RDFIBranchCountryCode == "" { + return &FieldError{FieldName: "RDFIBranchCountryCode", + Value: addenda14.RDFIBranchCountryCode, Msg: msgFieldInclusion} + } + if addenda14.EntryDetailSequenceNumber == 0 { + return &FieldError{FieldName: "EntryDetailSequenceNumber", + Value: addenda14.EntryDetailSequenceNumberField(), Msg: msgFieldInclusion} + } + return nil +} + +// RDFINameField gets the RDFIName field left padded +func (addenda14 *Addenda14) RDFINameField() string { + return addenda14.alphaField(addenda14.RDFIName, 35) +} + +// RDFIIDNumberQualifierField gets the RDFIIDNumberQualifier field left padded +func (addenda14 *Addenda14) RDFIIDNumberQualifierField() string { + return addenda14.alphaField(addenda14.RDFIIDNumberQualifier, 2) +} + +// RDFIIdentificationField gets the RDFIIdentificationCode field left padded +func (addenda14 *Addenda14) RDFIIdentificationField() string { + return addenda14.alphaField(addenda14.RDFIIdentification, 34) +} + +// RDFIBranchCountryCodeField gets the RDFIBranchCountryCode field left padded +func (addenda14 *Addenda14) RDFIBranchCountryCodeField() string { + return addenda14.alphaField(addenda14.RDFIBranchCountryCode, 3) +} + +// reservedField gets reserved - blank space +func (addenda14 *Addenda14) reservedField() string { + return addenda14.alphaField(addenda14.reserved, 10) +} + +// TypeCode Defines the specific explanation and format for the addenda14 information left padded +func (addenda14 *Addenda14) TypeCode() string { + return addenda14.typeCode +} + +// EntryDetailSequenceNumberField returns a zero padded EntryDetailSequenceNumber string +func (addenda14 *Addenda14) EntryDetailSequenceNumberField() string { + return addenda14.numericField(addenda14.EntryDetailSequenceNumber, 7) +} diff --git a/addenda14_test.go b/addenda14_test.go new file mode 100644 index 000000000..9954290ba --- /dev/null +++ b/addenda14_test.go @@ -0,0 +1,473 @@ +// 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" +) + +// mockAddenda14 creates a mock Addenda14 record +func mockAddenda14() *Addenda14 { + addenda14 := NewAddenda14() + addenda14.RDFIName = "Citadel Bank" + addenda14.RDFIIDNumberQualifier = "01" + addenda14.RDFIIdentification = "231380104" + addenda14.RDFIBranchCountryCode = "US" + addenda14.EntryDetailSequenceNumber = 00000001 + return addenda14 +} + +// TestMockAddenda14 validates mockAddenda14 +func TestMockAddenda14(t *testing.T) { + addenda14 := mockAddenda14() + if err := addenda14.Validate(); err != nil { + t.Error("mockAddenda14 does not validate and will break other tests") + } +} + +// testAddenda14Parse parses Addenda14 record +func testAddenda14Parse(t testing.TB) { + Addenda14 := NewAddenda14() + line := "714Citadel Bank 01231380104 US 0000001" + Addenda14.Parse(line) + // walk the Addenda14 struct + if Addenda14.recordType != "7" { + t.Errorf("expected %v got %v", "7", Addenda14.recordType) + } + if Addenda14.typeCode != "14" { + t.Errorf("expected %v got %v", "14", Addenda14.typeCode) + } + if Addenda14.RDFIName != "Citadel Bank" { + t.Errorf("expected %v got %v", "Citadel Bank", Addenda14.RDFIName) + } + if Addenda14.RDFIIDNumberQualifier != "01" { + t.Errorf("expected: %v got: %v", "01", Addenda14.RDFIIDNumberQualifier) + } + if Addenda14.RDFIIdentification != "231380104" { + t.Errorf("expected: %v got: %v", "928383-23938", Addenda14.RDFIIdentification) + } + if Addenda14.RDFIBranchCountryCode != "US" { + t.Errorf("expected: %s got: %s", "US", Addenda14.RDFIBranchCountryCode) + } + if Addenda14.reserved != " " { + t.Errorf("expected: %v got: %v", " ", Addenda14.reserved) + } + if Addenda14.EntryDetailSequenceNumber != 0000001 { + t.Errorf("expected: %v got: %v", 0000001, Addenda14.EntryDetailSequenceNumber) + } +} + +// TestAddenda14Parse tests parsing Addenda14 record +func TestAddenda14Parse(t *testing.T) { + testAddenda14Parse(t) +} + +// BenchmarkAddenda14Parse benchmarks parsing Addenda14 record +func BenchmarkAddenda14Parse(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda14Parse(b) + } +} + +// testAddenda14ValidRecordType validates Addenda14 recordType +func testAddenda14ValidRecordType(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.recordType = "63" + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda14ValidRecordType tests validating Addenda14 recordType +func TestAddenda14ValidRecordType(t *testing.T) { + testAddenda14ValidRecordType(t) +} + +// BenchmarkAddenda14ValidRecordType benchmarks validating Addenda14 recordType +func BenchmarkAddenda14ValidRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda14ValidRecordType(b) + } +} + +// testAddenda14ValidTypeCode validates Addenda14 TypeCode +func testAddenda14ValidTypeCode(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.typeCode = "65" + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda14ValidTypeCode tests validating Addenda14 TypeCode +func TestAddenda14ValidTypeCode(t *testing.T) { + testAddenda14ValidTypeCode(t) +} + +// BenchmarkAddenda14ValidTypeCode benchmarks validating Addenda14 TypeCode +func BenchmarkAddenda14ValidTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda14ValidTypeCode(b) + } +} + +// testAddenda14TypeCode14 TypeCode is 14 if typeCode is a valid TypeCode +func testAddenda14TypeCode14(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.typeCode = "05" + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda14TypeCode14 tests TypeCode is 14 if typeCode is a valid TypeCode +func TestAddenda14TypeCode14(t *testing.T) { + testAddenda14TypeCode14(t) +} + +// BenchmarkAddenda14TypeCode14 benchmarks TypeCode is 14 if typeCode is a valid TypeCode +func BenchmarkAddenda14TypeCode14(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda14TypeCode14(b) + } +} + +// testRDFINameAlphaNumeric validates RDFIName is alphanumeric +func testRDFINameAlphaNumeric(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.RDFIName = "Wells®Fargo" + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "RDFIName" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestRDFINameAlphaNumeric tests validating RDFIName is alphanumeric +func TestRDFINameAlphaNumeric(t *testing.T) { + testRDFINameAlphaNumeric(t) +} + +// BenchmarkRDFINameAlphaNumeric benchmarks validating RDFIName is alphanumeric +func BenchmarkRDFINameAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testRDFINameAlphaNumeric(b) + } +} + +// testRDFIIDNumberQualifierValid validates RDFIIDNumberQualifier is valid +func testRDFIIDNumberQualifierValid(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.RDFIIDNumberQualifier = "®1" + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "RDFIIDNumberQualifier" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestRDFIIDNumberQualifierValid tests validating RDFIIDNumberQualifier is valid +func TestRDFIIDNumberQualifierValid(t *testing.T) { + testRDFIIDNumberQualifierValid(t) +} + +// BenchmarkRDFIIDNumberQualifierValid benchmarks validating RDFIIDNumberQualifier is valid +func BenchmarkRDFIIDNumberQualifierValid(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testRDFIIDNumberQualifierValid(b) + } +} + +// testRDFIIdentificationAlphaNumeric validates RDFIIdentification is alphanumeric +func testRDFIIdentificationAlphaNumeric(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.RDFIIdentification = "®121042882" + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "RDFIIdentification" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestRDFIIdentificationAlphaNumeric tests validating RDFIIdentification is alphanumeric +func TestRDFIIdentificationAlphaNumeric(t *testing.T) { + testRDFIIdentificationAlphaNumeric(t) +} + +// BenchmarkRDFIIdentificationAlphaNumeric benchmarks validating RDFIIdentification is alphanumeric +func BenchmarkRDFIIdentificationAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testRDFIIdentificationAlphaNumeric(b) + } +} + +// testRDFIBranchCountryCodeAlphaNumeric validates RDFIBranchCountryCode is alphanumeric +func testRDFIBranchCountryCodeAlphaNumeric(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.RDFIBranchCountryCode = "U®" + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "RDFIBranchCountryCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestRDFIBranchCountryCodeAlphaNumeric tests validating RDFIBranchCountryCode is alphanumeric +func TestRDFIBranchCountryCodeAlphaNumeric(t *testing.T) { + testRDFIBranchCountryCodeAlphaNumeric(t) +} + +// BenchmarkRDFIBranchCountryCodeAlphaNumeric benchmarks validating RDFIBranchCountryCode is alphanumeric +func BenchmarkRDFIBranchCountryCodeAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testRDFIBranchCountryCodeAlphaNumeric(b) + } +} + +// testAddenda14FieldInclusionRecordType validates recordType fieldInclusion +func testAddenda14FieldInclusionRecordType(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.recordType = "" + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda14FieldInclusionRecordType tests validating recordType fieldInclusion +func TestAddenda14FieldInclusionRecordType(t *testing.T) { + testAddenda14FieldInclusionRecordType(t) +} + +// BenchmarkAddenda14FieldInclusionRecordType benchmarks validating recordType fieldInclusion +func BenchmarkAddenda14FieldInclusionRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda14FieldInclusionRecordType(b) + } +} + +// testAddenda14FieldInclusionTypeCode validates TypeCode fieldInclusion +func testAddenda14FieldInclusionTypeCode(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.typeCode = "" + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda14FieldInclusionTypeCode tests validating TypeCode fieldInclusion +func TestAddenda14FieldInclusionTypeCode(t *testing.T) { + testAddenda14FieldInclusionTypeCode(t) +} + +// BenchmarkAddenda14FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion +func BenchmarkAddenda14FieldInclusionTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda14FieldInclusionTypeCode(b) + } +} + +// testAddenda14FieldInclusionRDFIName validates RDFIName fieldInclusion +func testAddenda14FieldInclusionRDFIName(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.RDFIName = "" + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda14FieldInclusionRDFIName tests validating RDFIName fieldInclusion +func TestAddenda14FieldInclusionRDFIName(t *testing.T) { + testAddenda14FieldInclusionRDFIName(t) +} + +// BenchmarkAddenda14FieldInclusionRDFIName benchmarks validating RDFIName fieldInclusion +func BenchmarkAddenda14FieldInclusionRDFIName(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda14FieldInclusionRDFIName(b) + } +} + +// testAddenda14FieldInclusionRDFIIDNumberQualifier validates RDFIIDNumberQualifier fieldInclusion +func testAddenda14FieldInclusionRDFIIDNumberQualifier(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.RDFIIDNumberQualifier = "" + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda14FieldInclusionRDFIIdNumberQualifier tests validating RDFIIdNumberQualifier fieldInclusion +func TestAddenda14FieldInclusionRDFIIdNumberQualifier(t *testing.T) { + testAddenda14FieldInclusionRDFIIDNumberQualifier(t) +} + +// BenchmarkAddenda14FieldInclusionRDFIIdNumberQualifier benchmarks validating RDFIIdNumberQualifier fieldInclusion +func BenchmarkAddenda14FieldInclusionRDFIIdNumberQualifier(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda14FieldInclusionRDFIIDNumberQualifier(b) + } +} + +// testAddenda14FieldInclusionRDFIIdentification validates RDFIIdentification fieldInclusion +func testAddenda14FieldInclusionRDFIIdentification(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.RDFIIdentification = "" + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda14FieldInclusionRDFIIdentification tests validating RDFIIdentification fieldInclusion +func TestAddenda14FieldInclusionRDFIIdentification(t *testing.T) { + testAddenda14FieldInclusionRDFIIdentification(t) +} + +// BenchmarkAddenda14FieldInclusionRDFIIdentification benchmarks validating RDFIIdentification fieldInclusion +func BenchmarkAddenda14FieldInclusionRDFIIdentification(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda14FieldInclusionRDFIIdentification(b) + } +} + +// testAddenda14FieldInclusionRDFIBranchCountryCode validates RDFIBranchCountryCode fieldInclusion +func testAddenda14FieldInclusionRDFIBranchCountryCode(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.RDFIBranchCountryCode = "" + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda14FieldInclusionRDFIBranchCountryCode tests validating RDFIBranchCountryCode fieldInclusion +func TestAddenda14FieldInclusionRDFIBranchCountryCode(t *testing.T) { + testAddenda14FieldInclusionRDFIBranchCountryCode(t) +} + +// BenchmarkAddenda14FieldInclusionRDFIBranchCountryCode benchmarks validating RDFIBranchCountryCode fieldInclusion +func BenchmarkAddenda14FieldInclusionRDFIBranchCountryCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda14FieldInclusionRDFIBranchCountryCode(b) + } +} + +// testAddenda14FieldInclusionEntryDetailSequenceNumber validates EntryDetailSequenceNumber fieldInclusion +func testAddenda14FieldInclusionEntryDetailSequenceNumber(t testing.TB) { + addenda14 := mockAddenda14() + addenda14.EntryDetailSequenceNumber = 0 + if err := addenda14.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda14FieldInclusionEntryDetailSequenceNumber tests validating +// EntryDetailSequenceNumber fieldInclusion +func TestAddenda14FieldInclusionEntryDetailSequenceNumber(t *testing.T) { + testAddenda14FieldInclusionEntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda14FieldInclusionEntryDetailSequenceNumber benchmarks validating +// EntryDetailSequenceNumber fieldInclusion +func BenchmarkAddenda14FieldInclusionEntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda14FieldInclusionEntryDetailSequenceNumber(b) + } +} + +// TestAddenda14String validates that a known parsed Addenda14 record can be return to a string of the same value +func testAddenda14String(t testing.TB) { + addenda14 := NewAddenda14() + var line = "714Citadel Bank 231380104 US 0000001" + + addenda14.Parse(line) + + if addenda14.String() != line { + t.Errorf("Strings do not match") + } + if addenda14.TypeCode() != "14" { + t.Errorf("TypeCode Expected 14 got: %v", addenda14.TypeCode()) + } +} + +// TestAddenda14String tests validating that a known parsed Addenda14 record can be return to a string of the same value +func TestAddenda14String(t *testing.T) { + testAddenda14String(t) +} + +// BenchmarkAddenda14String benchmarks validating that a known parsed Addenda14 record can be return to a string of the same value +func BenchmarkAddenda14String(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda14String(b) + } +} diff --git a/addenda15.go b/addenda15.go new file mode 100644 index 000000000..0cdd8665f --- /dev/null +++ b/addenda15.go @@ -0,0 +1,148 @@ +// 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" + "strings" +) + +// Addenda15 is an addenda which provides business transaction information for Addenda Type +// Code 15 in a machine readable format. It is usually formatted according to ANSI, ASC, X12 Standard. +// +// Addenda15 is mandatory for IAT entries +// +// The Addenda15 record identifies key information related to the Receiver. +type Addenda15 struct { + // ID is a client defined string used as a reference to this record. + ID string `json:"id"` + // RecordType defines the type of record in the block. + recordType string + // TypeCode Addenda15 types code '15' + typeCode string + // Receiver Identification Number contains the accounting number by which the Originator is known to + // the Receiver for descriptive purposes. NACHA Rules recommend but do not require the RDFI to print + // the contents of this field on the receiver's statement. + ReceiverIDNumber string `json:"receiverIDNumber,omitempty"` + // Receiver Street Address contains the Receiver‟s physical address + ReceiverStreetAddress string `json:"receiverStreetAddress"` + // reserved - Leave blank + reserved string + // EntryDetailSequenceNumber contains the ascending sequence number section of the Entry + // 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 `json:"entryDetailSequenceNumber,omitempty"` + // validator is composed for data validation + validator + // converters is composed for ACH to GoLang Converters + converters +} + +// NewAddenda15 returns a new Addenda15 with default values for none exported fields +func NewAddenda15() *Addenda15 { + addenda15 := new(Addenda15) + addenda15.recordType = "7" + addenda15.typeCode = "15" + return addenda15 +} + +// Parse takes the input record string and parses the Addenda15 values +func (addenda15 *Addenda15) Parse(record string) { + // 1-1 Always "7" + addenda15.recordType = "7" + // 2-3 Always 15 + addenda15.typeCode = record[1:3] + // 4-18 + addenda15.ReceiverIDNumber = addenda15.parseStringField(record[3:18]) + // 19-53 + addenda15.ReceiverStreetAddress = strings.TrimSpace(record[18:53]) + // 54-87 reserved - Leave blank + addenda15.reserved = " " + // 88-94 Contains the last seven digits of the number entered in the Trace Number field in the corresponding Entry Detail Record + addenda15.EntryDetailSequenceNumber = addenda15.parseNumField(record[87:94]) +} + +// String writes the Addenda15 struct to a 94 character string. +func (addenda15 *Addenda15) String() string { + return fmt.Sprintf("%v%v%v%v%v%v", + addenda15.recordType, + addenda15.typeCode, + addenda15.ReceiverIDNumberField(), + addenda15.ReceiverStreetAddressField(), + addenda15.reservedField(), + addenda15.EntryDetailSequenceNumberField()) +} + +// Validate performs NACHA format rule checks on the record and returns an error if not Validated +// The first error encountered is returned and stops that parsing. +func (addenda15 *Addenda15) Validate() error { + if err := addenda15.fieldInclusion(); err != nil { + return err + } + if addenda15.recordType != "7" { + msg := fmt.Sprintf(msgRecordType, 7) + return &FieldError{FieldName: "recordType", Value: addenda15.recordType, Msg: msg} + } + if err := addenda15.isTypeCode(addenda15.typeCode); err != nil { + return &FieldError{FieldName: "TypeCode", Value: addenda15.typeCode, Msg: err.Error()} + } + // Type Code must be 15 + if addenda15.typeCode != "15" { + return &FieldError{FieldName: "TypeCode", Value: addenda15.typeCode, Msg: msgAddendaTypeCode} + } + if err := addenda15.isAlphanumeric(addenda15.ReceiverIDNumber); err != nil { + return &FieldError{FieldName: "ReceiverIDNumber", Value: addenda15.ReceiverIDNumber, Msg: err.Error()} + } + if err := addenda15.isAlphanumeric(addenda15.ReceiverStreetAddress); err != nil { + return &FieldError{FieldName: "ReceiverStreetAddress", Value: addenda15.ReceiverStreetAddress, Msg: err.Error()} + } + return nil +} + +// fieldInclusion validate mandatory fields are not default values. If fields are +// invalid the ACH transfer will be returned. +func (addenda15 *Addenda15) fieldInclusion() error { + if addenda15.recordType == "" { + return &FieldError{FieldName: "recordType", Value: addenda15.recordType, Msg: msgFieldInclusion} + } + if addenda15.typeCode == "" { + return &FieldError{FieldName: "TypeCode", Value: addenda15.typeCode, Msg: msgFieldInclusion} + } + if addenda15.ReceiverStreetAddress == "" { + return &FieldError{FieldName: "ReceiverStreetAddress", + Value: addenda15.ReceiverStreetAddress, Msg: msgFieldInclusion} + } + if addenda15.EntryDetailSequenceNumber == 0 { + return &FieldError{FieldName: "EntryDetailSequenceNumber", + Value: addenda15.EntryDetailSequenceNumberField(), Msg: msgFieldInclusion} + } + return nil +} + +// ReceiverIDNumberField gets the ReceiverIDNumber field left padded +func (addenda15 *Addenda15) ReceiverIDNumberField() string { + return addenda15.alphaField(addenda15.ReceiverIDNumber, 15) +} + +// ReceiverStreetAddressField gets the ReceiverStreetAddressField field left padded +func (addenda15 *Addenda15) ReceiverStreetAddressField() string { + return addenda15.alphaField(addenda15.ReceiverStreetAddress, 35) +} + +// reservedField gets reserved - blank space +func (addenda15 *Addenda15) reservedField() string { + return addenda15.alphaField(addenda15.reserved, 34) +} + +// TypeCode Defines the specific explanation and format for the addenda15 information left padded +func (addenda15 *Addenda15) TypeCode() string { + return addenda15.typeCode +} + +// EntryDetailSequenceNumberField returns a zero padded EntryDetailSequenceNumber string +func (addenda15 *Addenda15) EntryDetailSequenceNumberField() string { + return addenda15.numericField(addenda15.EntryDetailSequenceNumber, 7) +} diff --git a/addenda15_test.go b/addenda15_test.go new file mode 100644 index 000000000..e384885df --- /dev/null +++ b/addenda15_test.go @@ -0,0 +1,334 @@ +// 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" +) + +// mockAddenda15 creates a mock Addenda15 record +func mockAddenda15() *Addenda15 { + addenda15 := NewAddenda15() + addenda15.ReceiverIDNumber = "987465493213987" + addenda15.ReceiverStreetAddress = "2121 Front Street" + addenda15.EntryDetailSequenceNumber = 00000001 + return addenda15 +} + +// TestMockAddenda15 validates mockAddenda15 +func TestMockAddenda15(t *testing.T) { + addenda15 := mockAddenda15() + if err := addenda15.Validate(); err != nil { + t.Error("mockAddenda15 does not validate and will break other tests") + } +} + +// testAddenda15Parse parses Addenda15 record +func testAddenda15Parse(t testing.TB) { + Addenda15 := NewAddenda15() + line := "7159874654932139872121 Front Street 0000001" + Addenda15.Parse(line) + // walk the Addenda15 struct + if Addenda15.recordType != "7" { + t.Errorf("expected %v got %v", "7", Addenda15.recordType) + } + if Addenda15.typeCode != "15" { + t.Errorf("expected %v got %v", "15", Addenda15.typeCode) + } + if Addenda15.ReceiverIDNumber != "987465493213987" { + t.Errorf("expected %v got %v", "987465493213987", Addenda15.ReceiverIDNumber) + } + if Addenda15.ReceiverStreetAddress != "2121 Front Street" { + t.Errorf("expected: %v got: %v", "2121 Front Street", Addenda15.ReceiverStreetAddress) + } + if Addenda15.reserved != " " { + t.Errorf("expected: %v got: %v", " ", Addenda15.reserved) + } + if Addenda15.EntryDetailSequenceNumber != 0000001 { + t.Errorf("expected: %v got: %v", 0000001, Addenda15.EntryDetailSequenceNumber) + } +} + +// TestAddenda15Parse tests parsing Addenda15 record +func TestAddenda15Parse(t *testing.T) { + testAddenda15Parse(t) +} + +// BenchmarkAddenda15Parse benchmarks parsing Addenda15 record +func BenchmarkAddenda15Parse(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda15Parse(b) + } +} + +// testAddenda15ValidRecordType validates Addenda15 recordType +func testAddenda15ValidRecordType(t testing.TB) { + addenda15 := mockAddenda15() + addenda15.recordType = "63" + if err := addenda15.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda15ValidRecordType tests validating Addenda15 recordType +func TestAddenda15ValidRecordType(t *testing.T) { + testAddenda15ValidRecordType(t) +} + +// BenchmarkAddenda15ValidRecordType benchmarks validating Addenda15 recordType +func BenchmarkAddenda15ValidRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda15ValidRecordType(b) + } +} + +// testAddenda15ValidTypeCode validates Addenda15 TypeCode +func testAddenda15ValidTypeCode(t testing.TB) { + addenda15 := mockAddenda15() + addenda15.typeCode = "65" + if err := addenda15.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda15ValidTypeCode tests validating Addenda15 TypeCode +func TestAddenda15ValidTypeCode(t *testing.T) { + testAddenda15ValidTypeCode(t) +} + +// BenchmarkAddenda15ValidTypeCode benchmarks validating Addenda15 TypeCode +func BenchmarkAddenda15ValidTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda15ValidTypeCode(b) + } +} + +// testAddenda15TypeCode15 TypeCode is 15 if typeCode is a valid TypeCode +func testAddenda15TypeCode15(t testing.TB) { + addenda15 := mockAddenda15() + addenda15.typeCode = "05" + if err := addenda15.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda15TypeCode15 tests TypeCode is 15 if typeCode is a valid TypeCode +func TestAddenda15TypeCode15(t *testing.T) { + testAddenda15TypeCode15(t) +} + +// BenchmarkAddenda15TypeCode15 benchmarks TypeCode is 15 if typeCode is a valid TypeCode +func BenchmarkAddenda15TypeCode15(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda15TypeCode15(b) + } +} + +// testReceiverIDNumberAlphaNumeric validates ReceiverIDNumber is alphanumeric +func testReceiverIDNumberAlphaNumeric(t testing.TB) { + addenda15 := mockAddenda15() + addenda15.ReceiverIDNumber = "9874654932®1398" + if err := addenda15.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ReceiverIDNumber" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestReceiverIDNumberAlphaNumeric tests validating ReceiverIDNumber is alphanumeric +func TestReceiverIDNumberAlphaNumeric(t *testing.T) { + testReceiverIDNumberAlphaNumeric(t) +} + +// BenchmarkReceiverIDNumberAlphaNumeric benchmarks validating ReceiverIDNumber is alphanumeric +func BenchmarkReceiverIDNumberAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testReceiverIDNumberAlphaNumeric(b) + } +} + +// testReceiverStreetAddressAlphaNumeric validates ReceiverStreetAddress is alphanumeric +func testReceiverStreetAddressAlphaNumeric(t testing.TB) { + addenda15 := mockAddenda15() + addenda15.ReceiverStreetAddress = "2121 Fr®nt Street" + if err := addenda15.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ReceiverStreetAddress" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestReceiverStreetAddressAlphaNumeric tests validating ReceiverStreetAddress is alphanumeric +func TestReceiverStreetAddressAlphaNumeric(t *testing.T) { + testReceiverStreetAddressAlphaNumeric(t) +} + +// BenchmarkReceiverStreetAddressAlphaNumeric benchmarks validating ReceiverStreetAddress is alphanumeric +func BenchmarkReceiverStreetAddressAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testReceiverStreetAddressAlphaNumeric(b) + } +} + +// testAddenda15FieldInclusionRecordType validates recordType fieldInclusion +func testAddenda15FieldInclusionRecordType(t testing.TB) { + addenda15 := mockAddenda15() + addenda15.recordType = "" + if err := addenda15.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda15FieldInclusionRecordType tests validating recordType fieldInclusion +func TestAddenda15FieldInclusionRecordType(t *testing.T) { + testAddenda15FieldInclusionRecordType(t) +} + +// BenchmarkAddenda15FieldInclusionRecordType benchmarks validating recordType fieldInclusion +func BenchmarkAddenda15FieldInclusionRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda15FieldInclusionRecordType(b) + } +} + +// testAddenda15FieldInclusionTypeCode validates TypeCode fieldInclusion +func testAddenda15FieldInclusionTypeCode(t testing.TB) { + addenda15 := mockAddenda15() + addenda15.typeCode = "" + if err := addenda15.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda15FieldInclusionTypeCode tests validating TypeCode fieldInclusion +func TestAddenda15FieldInclusionTypeCode(t *testing.T) { + testAddenda15FieldInclusionTypeCode(t) +} + +// BenchmarkAddenda15FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion +func BenchmarkAddenda15FieldInclusionTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda15FieldInclusionTypeCode(b) + } +} + +// testAddenda15FieldInclusionReceiverStreetAddress validates ReceiverStreetAddress fieldInclusion +func testAddenda15FieldInclusionReceiverStreetAddress(t testing.TB) { + addenda15 := mockAddenda15() + addenda15.ReceiverStreetAddress = "" + if err := addenda15.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda15FieldInclusionReceiverStreetAddress tests validating ReceiverStreetAddress fieldInclusion +func TestAddenda15FieldInclusionReceiverStreetAddress(t *testing.T) { + testAddenda15FieldInclusionReceiverStreetAddress(t) +} + +// BenchmarkAddenda15FieldInclusionReceiverStreetAddress benchmarks validating ReceiverStreetAddress fieldInclusion +func BenchmarkAddenda15FieldInclusionReceiverStreetAddress(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda15FieldInclusionReceiverStreetAddress(b) + } +} + +// testAddenda15FieldInclusionEntryDetailSequenceNumber validates EntryDetailSequenceNumber fieldInclusion +func testAddenda15FieldInclusionEntryDetailSequenceNumber(t testing.TB) { + addenda15 := mockAddenda15() + addenda15.EntryDetailSequenceNumber = 0 + if err := addenda15.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda15FieldInclusionEntryDetailSequenceNumber tests validating +// EntryDetailSequenceNumber fieldInclusion +func TestAddenda15FieldInclusionEntryDetailSequenceNumber(t *testing.T) { + testAddenda15FieldInclusionEntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda15FieldInclusionEntryDetailSequenceNumber benchmarks validating +// EntryDetailSequenceNumber fieldInclusion +func BenchmarkAddenda15FieldInclusionEntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda15FieldInclusionEntryDetailSequenceNumber(b) + } +} + +// TestAddenda15String validates that a known parsed Addenda15 record can be return to a string of the same value +func testAddenda15String(t testing.TB) { + addenda15 := NewAddenda15() + var line = "7159874654932139872121 Front Street 0000001" + addenda15.Parse(line) + + if addenda15.String() != line { + t.Errorf("Strings do not match") + } + if addenda15.TypeCode() != "15" { + t.Errorf("TypeCode Expected 15 got: %v", addenda15.TypeCode()) + } +} + +// TestAddenda15String tests validating that a known parsed Addenda15 record can be return to a string of the same value +func TestAddenda15String(t *testing.T) { + testAddenda15String(t) +} + +// BenchmarkAddenda15String benchmarks validating that a known parsed Addenda15 record can be return to a string of the same value +func BenchmarkAddenda15String(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda15String(b) + } +} diff --git a/addenda16.go b/addenda16.go new file mode 100644 index 000000000..a53728213 --- /dev/null +++ b/addenda16.go @@ -0,0 +1,157 @@ +// 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" + "strings" +) + +// Addenda16 is an addenda which provides business transaction information for Addenda Type +// Code 16 in a machine readable format. It is usually formatted according to ANSI, ASC, X16 Standard. +// +// Addenda16 is mandatory for IAT entries +// +// The Addenda16 record identifies key information related to the Receiver. +type Addenda16 struct { + // ID is a client defined string used as a reference to this record. + ID string `json:"id"` + // RecordType defines the type of record in the block. + recordType string + // TypeCode Addenda16 types code '16' + typeCode string + // Receiver City & State / Province + // Data elements City and State / Province should be separated with an asterisk (*) as a delimiter + // and the field should end with a backslash (\). + // For example: San FranciscoCA. + ReceiverCityStateProvince string `json:"receiverCityStateProvince"` + // Receiver Country & Postal Code + // Data elements must be separated by an asterisk (*) and must end with a backslash (\) + // For example: US10036\ + ReceiverCountryPostalCode string `json:"receiverCountryPostalCode"` + // reserved - Leave blank + reserved string + // EntryDetailSequenceNumber contains the ascending sequence number section of the Entry + // 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 `json:"entryDetailSequenceNumber,omitempty"` + // validator is composed for data validation + validator + // converters is composed for ACH to GoLang Converters + converters +} + +// NewAddenda16 returns a new Addenda16 with default values for none exported fields +func NewAddenda16() *Addenda16 { + addenda16 := new(Addenda16) + addenda16.recordType = "7" + addenda16.typeCode = "16" + return addenda16 +} + +// Parse takes the input record string and parses the Addenda16 values +func (addenda16 *Addenda16) Parse(record string) { + // 1-1 Always "7" + addenda16.recordType = "7" + // 2-3 Always 16 + addenda16.typeCode = record[1:3] + // 4-38 ReceiverCityStateProvince + addenda16.ReceiverCityStateProvince = strings.TrimSpace(record[3:38]) + // 39-73 ReceiverCountryPostalCode + addenda16.ReceiverCountryPostalCode = strings.TrimSpace(record[38:73]) + // 74-87 reserved - Leave blank + addenda16.reserved = " " + // 88-94 Contains the last seven digits of the number entered in the Trace Number field in the corresponding Entry Detail Record + addenda16.EntryDetailSequenceNumber = addenda16.parseNumField(record[87:94]) +} + +// String writes the Addenda16 struct to a 94 character string. +func (addenda16 *Addenda16) String() string { + return fmt.Sprintf("%v%v%v%v%v%v", + addenda16.recordType, + addenda16.typeCode, + addenda16.ReceiverCityStateProvinceField(), + addenda16.ReceiverCountryPostalCodeField(), + addenda16.reservedField(), + addenda16.EntryDetailSequenceNumberField()) +} + +// Validate performs NACHA format rule checks on the record and returns an error if not Validated +// The first error encountered is returned and stops that parsing. +func (addenda16 *Addenda16) Validate() error { + if err := addenda16.fieldInclusion(); err != nil { + return err + } + if addenda16.recordType != "7" { + msg := fmt.Sprintf(msgRecordType, 7) + return &FieldError{FieldName: "recordType", Value: addenda16.recordType, Msg: msg} + } + if err := addenda16.isTypeCode(addenda16.typeCode); err != nil { + return &FieldError{FieldName: "TypeCode", Value: addenda16.typeCode, Msg: err.Error()} + } + // Type Code must be 16 + if addenda16.typeCode != "16" { + return &FieldError{FieldName: "TypeCode", Value: addenda16.typeCode, Msg: msgAddendaTypeCode} + } + if err := addenda16.isAlphanumeric(addenda16.ReceiverCityStateProvince); err != nil { + return &FieldError{FieldName: "ReceiverCityStateProvince", + Value: addenda16.ReceiverCityStateProvince, Msg: err.Error()} + } + if err := addenda16.isAlphanumeric(addenda16.ReceiverCountryPostalCode); err != nil { + return &FieldError{FieldName: "ReceiverCountryPostalCode", + Value: addenda16.ReceiverCountryPostalCode, Msg: err.Error()} + } + return nil +} + +// fieldInclusion validate mandatory fields are not default values. If fields are +// invalid the ACH transfer will be returned. +func (addenda16 *Addenda16) fieldInclusion() error { + if addenda16.recordType == "" { + return &FieldError{FieldName: "recordType", Value: addenda16.recordType, Msg: msgFieldInclusion} + } + if addenda16.typeCode == "" { + return &FieldError{FieldName: "TypeCode", Value: addenda16.typeCode, Msg: msgFieldInclusion} + } + if addenda16.ReceiverCityStateProvince == "" { + return &FieldError{FieldName: "ReceiverCityStateProvince", + Value: addenda16.ReceiverCityStateProvince, Msg: msgFieldInclusion} + } + if addenda16.ReceiverCountryPostalCode == "" { + return &FieldError{FieldName: "ReceiverCountryPostalCode", + Value: addenda16.ReceiverCountryPostalCode, Msg: msgFieldInclusion} + } + if addenda16.EntryDetailSequenceNumber == 0 { + return &FieldError{FieldName: "EntryDetailSequenceNumber", + Value: addenda16.EntryDetailSequenceNumberField(), Msg: msgFieldInclusion} + } + return nil +} + +// ReceiverCityStateProvinceField gets the ReceiverCityStateProvinceField left padded +func (addenda16 *Addenda16) ReceiverCityStateProvinceField() string { + return addenda16.alphaField(addenda16.ReceiverCityStateProvince, 35) +} + +// ReceiverCountryPostalCodeField gets the ReceiverCountryPostalCode field left padded +func (addenda16 *Addenda16) ReceiverCountryPostalCodeField() string { + return addenda16.alphaField(addenda16.ReceiverCountryPostalCode, 35) +} + +// reservedField gets reserved - blank space +func (addenda16 *Addenda16) reservedField() string { + return addenda16.alphaField(addenda16.reserved, 14) +} + +// TypeCode Defines the specific explanation and format for the addenda16 information left padded +func (addenda16 *Addenda16) TypeCode() string { + return addenda16.typeCode +} + +// EntryDetailSequenceNumberField returns a zero padded EntryDetailSequenceNumber string +func (addenda16 *Addenda16) EntryDetailSequenceNumberField() string { + return addenda16.numericField(addenda16.EntryDetailSequenceNumber, 7) +} diff --git a/addenda16_test.go b/addenda16_test.go new file mode 100644 index 000000000..41c5abce1 --- /dev/null +++ b/addenda16_test.go @@ -0,0 +1,366 @@ +// 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" +) + +// mockAddenda16 creates a mock Addenda16 record +func mockAddenda16() *Addenda16 { + addenda16 := NewAddenda16() + addenda16.ReceiverCityStateProvince = "LetterTown*AB\\" + addenda16.ReceiverCountryPostalCode = "CA*80014\\" + addenda16.EntryDetailSequenceNumber = 00000001 + return addenda16 +} + +// TestMockAddenda16 validates mockAddenda16 +func TestMockAddenda16(t *testing.T) { + addenda16 := mockAddenda16() + if err := addenda16.Validate(); err != nil { + t.Error("mockAddenda16 does not validate and will break other tests") + } +} + +// testAddenda16Parse parses Addenda16 record +func testAddenda16Parse(t testing.TB) { + Addenda16 := NewAddenda16() + line := "716LetterTown*AB\\ CA*80014\\ 0000001" + Addenda16.Parse(line) + // walk the Addenda16 struct + if Addenda16.recordType != "7" { + t.Errorf("expected %v got %v", "7", Addenda16.recordType) + } + if Addenda16.typeCode != "16" { + t.Errorf("expected %v got %v", "16", Addenda16.typeCode) + } + if Addenda16.ReceiverCityStateProvince != "LetterTown*AB\\" { + t.Errorf("expected %v got %v", "LetterTown*AB\\", Addenda16.ReceiverCityStateProvince) + } + if Addenda16.ReceiverCountryPostalCode != "CA*80014\\" { + t.Errorf("expected: %v got: %v", "CA*80014\\", Addenda16.ReceiverCountryPostalCode) + } + if Addenda16.reserved != " " { + t.Errorf("expected: %v got: %v", " ", Addenda16.reserved) + } + if Addenda16.EntryDetailSequenceNumber != 0000001 { + t.Errorf("expected: %v got: %v", 0000001, Addenda16.EntryDetailSequenceNumber) + } +} + +// TestAddenda16Parse tests parsing Addenda16 record +func TestAddenda16Parse(t *testing.T) { + testAddenda16Parse(t) +} + +// BenchmarkAddenda16Parse benchmarks parsing Addenda16 record +func BenchmarkAddenda16Parse(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda16Parse(b) + } +} + +// testAddenda16ValidRecordType validates Addenda16 recordType +func testAddenda16ValidRecordType(t testing.TB) { + addenda16 := mockAddenda16() + addenda16.recordType = "63" + if err := addenda16.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda16ValidRecordType tests validating Addenda16 recordType +func TestAddenda16ValidRecordType(t *testing.T) { + testAddenda16ValidRecordType(t) +} + +// BenchmarkAddenda16ValidRecordType benchmarks validating Addenda16 recordType +func BenchmarkAddenda16ValidRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda16ValidRecordType(b) + } +} + +// testAddenda16ValidTypeCode validates Addenda16 TypeCode +func testAddenda16ValidTypeCode(t testing.TB) { + addenda16 := mockAddenda16() + addenda16.typeCode = "65" + if err := addenda16.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda16ValidTypeCode tests validating Addenda16 TypeCode +func TestAddenda16ValidTypeCode(t *testing.T) { + testAddenda16ValidTypeCode(t) +} + +// BenchmarkAddenda16ValidTypeCode benchmarks validating Addenda16 TypeCode +func BenchmarkAddenda16ValidTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda16ValidTypeCode(b) + } +} + +// testAddenda16TypeCode16 TypeCode is 16 if typeCode is a valid TypeCode +func testAddenda16TypeCode16(t testing.TB) { + addenda16 := mockAddenda16() + addenda16.typeCode = "05" + if err := addenda16.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda16TypeCode16 tests TypeCode is 16 if typeCode is a valid TypeCode +func TestAddenda16TypeCode16(t *testing.T) { + testAddenda16TypeCode16(t) +} + +// BenchmarkAddenda16TypeCode16 benchmarks TypeCode is 16 if typeCode is a valid TypeCode +func BenchmarkAddenda16TypeCode16(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda16TypeCode16(b) + } +} + +// testReceiverCityStateProvinceAlphaNumeric validates ReceiverCityStateProvince is alphanumeric +func testReceiverCityStateProvinceAlphaNumeric(t testing.TB) { + addenda16 := mockAddenda16() + addenda16.ReceiverCityStateProvince = "Jacobs®Town*PA" + if err := addenda16.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ReceiverCityStateProvince" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestReceiverCityStateProvinceAlphaNumeric tests validating ReceiverCityStateProvince is alphanumeric +func TestReceiverCityStateProvinceAlphaNumeric(t *testing.T) { + testReceiverCityStateProvinceAlphaNumeric(t) +} + +// BenchmarkReceiverCityStateProvinceAlphaNumeric benchmarks validating ReceiverCityStateProvince is alphanumeric +func BenchmarkReceiverCityStateProvinceAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testReceiverCityStateProvinceAlphaNumeric(b) + } +} + +// testReceiverCountryPostalCodeAlphaNumeric validates ReceiverCountryPostalCode is alphanumeric +func testReceiverCountryPostalCodeAlphaNumeric(t testing.TB) { + addenda16 := mockAddenda16() + addenda16.ReceiverCountryPostalCode = "US19®305" + if err := addenda16.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ReceiverCountryPostalCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestReceiverCountryPostalCodeAlphaNumeric tests validating ReceiverCountryPostalCode is alphanumeric +func TestReceiverCountryPostalCodeAlphaNumeric(t *testing.T) { + testReceiverCountryPostalCodeAlphaNumeric(t) +} + +// BenchmarkReceiverCountryPostalCodeAlphaNumeric benchmarks validating ReceiverCountryPostalCode is alphanumeric +func BenchmarkReceiverCountryPostalCodeAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testReceiverCountryPostalCodeAlphaNumeric(b) + } +} + +// testAddenda16FieldInclusionRecordType validates recordType fieldInclusion +func testAddenda16FieldInclusionRecordType(t testing.TB) { + addenda16 := mockAddenda16() + addenda16.recordType = "" + if err := addenda16.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda16FieldInclusionRecordType tests validating recordType fieldInclusion +func TestAddenda16FieldInclusionRecordType(t *testing.T) { + testAddenda16FieldInclusionRecordType(t) +} + +// BenchmarkAddenda16FieldInclusionRecordType benchmarks validating recordType fieldInclusion +func BenchmarkAddenda16FieldInclusionRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda16FieldInclusionRecordType(b) + } +} + +// testAddenda16FieldInclusionTypeCode validates TypeCode fieldInclusion +func testAddenda16FieldInclusionTypeCode(t testing.TB) { + addenda16 := mockAddenda16() + addenda16.typeCode = "" + if err := addenda16.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda16FieldInclusionTypeCode tests validating TypeCode fieldInclusion +func TestAddenda16FieldInclusionTypeCode(t *testing.T) { + testAddenda16FieldInclusionTypeCode(t) +} + +// BenchmarkAddenda16FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion +func BenchmarkAddenda16FieldInclusionTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda16FieldInclusionTypeCode(b) + } +} + +// testAddenda16FieldInclusionReceiverCityStateProvince validates ReceiverCityStateProvince fieldInclusion +func testAddenda16FieldInclusionReceiverCityStateProvince(t testing.TB) { + addenda16 := mockAddenda16() + addenda16.ReceiverCityStateProvince = "" + if err := addenda16.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda16FieldInclusionReceiverCityStateProvince tests validating ReceiverCityStateProvince fieldInclusion +func TestAddenda16FieldInclusionReceiverCityStateProvince(t *testing.T) { + testAddenda16FieldInclusionReceiverCityStateProvince(t) +} + +// BenchmarkAddenda16FieldInclusionReceiverCityStateProvince benchmarks validating ReceiverCityStateProvince fieldInclusion +func BenchmarkAddenda16FieldInclusionReceiverCityStateProvince(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda16FieldInclusionReceiverCityStateProvince(b) + } +} + +// testAddenda16FieldInclusionReceiverCountryPostalCode validates ReceiverCountryPostalCode fieldInclusion +func testAddenda16FieldInclusionReceiverCountryPostalCode(t testing.TB) { + addenda16 := mockAddenda16() + addenda16.ReceiverCountryPostalCode = "" + if err := addenda16.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda16FieldInclusionReceiverCountryPostalCode tests validating ReceiverCountryPostalCode fieldInclusion +func TestAddenda16FieldInclusionReceiverCountryPostalCode(t *testing.T) { + testAddenda16FieldInclusionReceiverCountryPostalCode(t) +} + +// BenchmarkAddenda16FieldInclusionReceiverCountryPostalCode benchmarks validating ReceiverCountryPostalCode fieldInclusion +func BenchmarkAddenda16FieldInclusionReceiverCountryPostalCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda16FieldInclusionReceiverCountryPostalCode(b) + } +} + +// testAddenda16FieldInclusionEntryDetailSequenceNumber validates EntryDetailSequenceNumber fieldInclusion +func testAddenda16FieldInclusionEntryDetailSequenceNumber(t testing.TB) { + addenda16 := mockAddenda16() + addenda16.EntryDetailSequenceNumber = 0 + if err := addenda16.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda16FieldInclusionEntryDetailSequenceNumber tests validating +// EntryDetailSequenceNumber fieldInclusion +func TestAddenda16FieldInclusionEntryDetailSequenceNumber(t *testing.T) { + testAddenda16FieldInclusionEntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda16FieldInclusionEntryDetailSequenceNumber benchmarks validating +// EntryDetailSequenceNumber fieldInclusion +func BenchmarkAddenda16FieldInclusionEntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda16FieldInclusionEntryDetailSequenceNumber(b) + } +} + +// TestAddenda16String validates that a known parsed Addenda16 record can be return to a string of the same value +func testAddenda16String(t testing.TB) { + addenda16 := NewAddenda16() + // Backslash logic + var line = "716" + + "LetterTown*AB\\ " + + "CA*80014\\ " + + " " + + "0000001" + + addenda16.Parse(line) + + if addenda16.String() != line { + t.Errorf("Strings do not match") + } + if addenda16.TypeCode() != "16" { + t.Errorf("TypeCode Expected 16 got: %v", addenda16.TypeCode()) + } +} + +// TestAddenda16String tests validating that a known parsed Addenda16 record can be return to a string of the same value +func TestAddenda16String(t *testing.T) { + testAddenda16String(t) +} + +// BenchmarkAddenda16String benchmarks validating that a known parsed Addenda16 record can be return to a string of the same value +func BenchmarkAddenda16String(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda16String(b) + } +} diff --git a/addenda17.go b/addenda17.go new file mode 100644 index 000000000..fe897f088 --- /dev/null +++ b/addenda17.go @@ -0,0 +1,136 @@ +// 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" + "strings" +) + +// Addenda17 is an addenda which provides business transaction information for Addenda Type +// Code 17 in a machine readable format. It is usually formatted according to ANSI, ASC, X12 Standard. +// +// Addenda17 is optional for IAT entries +// +// The Addenda17 record identifies payment-related data. A maximum of two of these Addenda Records +// may be included with each IAT entry. +type Addenda17 struct { + // ID is a client defined string used as a reference to this record. + ID string `json:"id"` + // RecordType defines the type of record in the block. entryAddenda17 Pos 7 + recordType string + // TypeCode Addenda17 types code '17' + typeCode string + // PaymentRelatedInformation + PaymentRelatedInformation string `json:"paymentRelatedInformation"` + // SequenceNumber is consecutively assigned to each Addenda17 Record following + // an Entry Detail Record. The first addenda17 sequence number must always + // be a "1". + SequenceNumber int `json:"sequenceNumber,omitempty"` + // EntryDetailSequenceNumber contains the ascending sequence number section of the Entry + // 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 `json:"entryDetailSequenceNumber,omitempty"` + // validator is composed for data validation + validator + // converters is composed for ACH to GoLang Converters + converters +} + +// NewAddenda17 returns a new Addenda17 with default values for none exported fields +func NewAddenda17() *Addenda17 { + addenda17 := new(Addenda17) + addenda17.recordType = "7" + addenda17.typeCode = "17" + return addenda17 +} + +// Parse takes the input record string and parses the Addenda17 values +func (addenda17 *Addenda17) Parse(record string) { + // 1-1 Always "7" + addenda17.recordType = "7" + // 2-3 Always 17 + addenda17.typeCode = record[1:3] + // 4-83 Based on the information entered (04-83) 80 alphanumeric + addenda17.PaymentRelatedInformation = strings.TrimSpace(record[3:83]) + // 84-87 SequenceNumber is consecutively assigned to each Addenda17 Record following + // an Entry Detail Record + addenda17.SequenceNumber = addenda17.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 + addenda17.EntryDetailSequenceNumber = addenda17.parseNumField(record[87:94]) +} + +// String writes the Addenda17 struct to a 94 character string. +func (addenda17 *Addenda17) String() string { + return fmt.Sprintf("%v%v%v%v%v", + addenda17.recordType, + addenda17.typeCode, + addenda17.PaymentRelatedInformationField(), + addenda17.SequenceNumberField(), + addenda17.EntryDetailSequenceNumberField()) +} + +// Validate performs NACHA format rule checks on the record and returns an error if not Validated +// The first error encountered is returned and stops that parsing. +func (addenda17 *Addenda17) Validate() error { + if err := addenda17.fieldInclusion(); err != nil { + return err + } + if addenda17.recordType != "7" { + msg := fmt.Sprintf(msgRecordType, 7) + return &FieldError{FieldName: "recordType", Value: addenda17.recordType, Msg: msg} + } + if err := addenda17.isTypeCode(addenda17.typeCode); err != nil { + return &FieldError{FieldName: "TypeCode", Value: addenda17.typeCode, Msg: err.Error()} + } + // Type Code must be 17 + if addenda17.typeCode != "17" { + return &FieldError{FieldName: "TypeCode", Value: addenda17.typeCode, Msg: msgAddendaTypeCode} + } + if err := addenda17.isAlphanumeric(addenda17.PaymentRelatedInformation); err != nil { + return &FieldError{FieldName: "PaymentRelatedInformation", Value: addenda17.PaymentRelatedInformation, Msg: err.Error()} + } + + return nil +} + +// fieldInclusion validate mandatory fields are not default values. If fields are +// invalid the ACH transfer will be returned. +func (addenda17 *Addenda17) fieldInclusion() error { + if addenda17.recordType == "" { + return &FieldError{FieldName: "recordType", Value: addenda17.recordType, Msg: msgFieldInclusion} + } + if addenda17.typeCode == "" { + return &FieldError{FieldName: "TypeCode", Value: addenda17.typeCode, Msg: msgFieldInclusion} + } + if addenda17.SequenceNumber == 0 { + return &FieldError{FieldName: "SequenceNumber", Value: addenda17.SequenceNumberField(), Msg: msgFieldInclusion} + } + if addenda17.EntryDetailSequenceNumber == 0 { + return &FieldError{FieldName: "EntryDetailSequenceNumber", Value: addenda17.EntryDetailSequenceNumberField(), Msg: msgFieldInclusion} + } + return nil +} + +// PaymentRelatedInformationField returns a zero padded PaymentRelatedInformation string +func (addenda17 *Addenda17) PaymentRelatedInformationField() string { + return addenda17.alphaField(addenda17.PaymentRelatedInformation, 80) +} + +// SequenceNumberField returns a zero padded SequenceNumber string +func (addenda17 *Addenda17) SequenceNumberField() string { + return addenda17.numericField(addenda17.SequenceNumber, 4) +} + +// EntryDetailSequenceNumberField returns a zero padded EntryDetailSequenceNumber string +func (addenda17 *Addenda17) EntryDetailSequenceNumberField() string { + return addenda17.numericField(addenda17.EntryDetailSequenceNumber, 7) +} + +// TypeCode Defines the specific explanation and format for the addenda17 information +func (addenda17 *Addenda17) TypeCode() string { + return addenda17.typeCode +} diff --git a/addenda17_test.go b/addenda17_test.go new file mode 100644 index 000000000..08de4c377 --- /dev/null +++ b/addenda17_test.go @@ -0,0 +1,229 @@ +// 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" +) + +// mockAddenda17 creates a mock Addenda17 record +func mockAddenda17() *Addenda17 { + addenda17 := NewAddenda17() + addenda17.PaymentRelatedInformation = "This is an international payment" + addenda17.SequenceNumber = 1 + addenda17.EntryDetailSequenceNumber = 0000001 + return addenda17 +} + +func mockAddenda17B() *Addenda17 { + addenda17 := NewAddenda17() + addenda17.PaymentRelatedInformation = "Transfer of money from one country to another" + addenda17.SequenceNumber = 2 + addenda17.EntryDetailSequenceNumber = 0000001 + + return addenda17 +} + +// testAddenda17Parse parses Addenda17 record +func testAddenda17Parse(t testing.TB) { + Addenda17 := NewAddenda17() + line := "717This is an international payment 00010000001" + Addenda17.Parse(line) + // walk the Addenda17 struct + if Addenda17.recordType != "7" { + t.Errorf("expected %v got %v", "7", Addenda17.recordType) + } + if Addenda17.typeCode != "17" { + t.Errorf("expected %v got %v", "17", Addenda17.typeCode) + } + if Addenda17.PaymentRelatedInformation != "This is an international payment" { + t.Errorf("expected %v got %v", "This is an international payment", Addenda17.PaymentRelatedInformation) + } + if Addenda17.SequenceNumber != 1 { + t.Errorf("expected: %v got: %v", 1, Addenda17.SequenceNumber) + } + if Addenda17.EntryDetailSequenceNumber != 0000001 { + t.Errorf("expected: %v got: %v", 0000001, Addenda17.EntryDetailSequenceNumber) + } +} + +// TestAddenda17Parse tests parsing Addenda17 record +func TestAddenda17Parse(t *testing.T) { + testAddenda17Parse(t) +} + +// BenchmarkAddenda17Parse benchmarks parsing Addenda17 record +func BenchmarkAddenda17Parse(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda17Parse(b) + } +} + +// TestMockAddenda17 validates mockAddenda17 +func TestMockAddenda17(t *testing.T) { + addenda17 := mockAddenda17() + if err := addenda17.Validate(); err != nil { + t.Error("mockAddenda17 does not validate and will break other tests") + } + if addenda17.EntryDetailSequenceNumber != 0000001 { + t.Error("EntryDetailSequenceNumber dependent default value has changed") + } +} + +// testAddenda17String validates that a known parsed file can be return to a string of the same value +func testAddenda17String(t testing.TB) { + addenda17 := NewAddenda17() + var line = "717IAT DIEGO MAY 00010000001" + addenda17.Parse(line) + + if addenda17.String() != line { + t.Errorf("Strings do not match") + } +} + +// TestAddenda17 String tests validating that a known parsed file can be return to a string of the same value +func TestAddenda17String(t *testing.T) { + testAddenda17String(t) +} + +// BenchmarkAddenda17 String benchmarks validating that a known parsed file can be return to a string of the same value +func BenchmarkAddenda17String(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda17String(b) + } +} + +func TestValidateAddenda17RecordType(t *testing.T) { + addenda17 := mockAddenda17() + addenda17.recordType = "63" + if err := addenda17.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +func TestAddenda17TypeCodeFieldInclusion(t *testing.T) { + addenda17 := mockAddenda17() + addenda17.typeCode = "" + if err := addenda17.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +func TestAddenda17FieldInclusion(t *testing.T) { + addenda17 := mockAddenda17() + addenda17.EntryDetailSequenceNumber = 0 + if err := addenda17.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "EntryDetailSequenceNumber" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +func TestAddenda17FieldInclusionRecordType(t *testing.T) { + addenda17 := mockAddenda17() + addenda17.recordType = "" + if err := addenda17.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +//testAddenda17PaymentRelatedInformationAlphaNumeric validates PaymentRelatedInformation is alphanumeric +func testAddenda17PaymentRelatedInformationAlphaNumeric(t testing.TB) { + addenda17 := mockAddenda17() + addenda17.PaymentRelatedInformation = "®©" + if err := addenda17.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "PaymentRelatedInformation" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda17PaymentRelatedInformationAlphaNumeric tests validating PaymentRelatedInformation is alphanumeric +func TestAddenda17PaymentRelatedInformationAlphaNumeric(t *testing.T) { + testAddenda17PaymentRelatedInformationAlphaNumeric(t) + +} + +// BenchmarkAddenda17PaymentRelatedInformationAlphaNumeric benchmarks PaymentRelatedInformation is alphanumeric +func BenchmarkAddenda17PaymentRelatedInformationAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda17PaymentRelatedInformationAlphaNumeric(b) + } +} + +// testAddenda17ValidTypeCode validates Addenda17 TypeCode +func testAddenda17ValidTypeCode(t testing.TB) { + addenda17 := mockAddenda17() + addenda17.typeCode = "65" + if err := addenda17.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda17ValidTypeCode tests validating Addenda17 TypeCode +func TestAddenda17ValidTypeCode(t *testing.T) { + testAddenda17ValidTypeCode(t) +} + +// BenchmarkAddenda17ValidTypeCode benchmarks validating Addenda17 TypeCode +func BenchmarkAddenda17ValidTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda17ValidTypeCode(b) + } +} + +// testAddenda17TypeCode17 TypeCode is 17 if typeCode is a valid TypeCode +func testAddenda17TypeCode17(t testing.TB) { + addenda17 := mockAddenda17() + addenda17.typeCode = "05" + if err := addenda17.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda17TypeCode17 tests TypeCode is 17 if typeCode is a valid TypeCode +func TestAddenda17TypeCode17(t *testing.T) { + testAddenda17TypeCode17(t) +} + +// BenchmarkAddenda17TypeCode17 benchmarks TypeCode is 17 if typeCode is a valid TypeCode +func BenchmarkAddenda17TypeCode17(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda17TypeCode17(b) + } +} diff --git a/addenda18.go b/addenda18.go new file mode 100644 index 000000000..bfa597dac --- /dev/null +++ b/addenda18.go @@ -0,0 +1,209 @@ +// 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" + "strings" +) + +// Addenda18 is an addenda which provides business transaction information for Addenda Type +// Code 18 in a machine readable format. It is usually formatted according to ANSI, ASC, X12 Standard. +// +// Addenda18 is optional for IAT entries +// +// The Addenda18 record identifies information on each Foreign Correspondent Bank involved in the +// processing of the IAT entry. If no Foreign Correspondent Bank is involved,t he record should not be +// included. A maximum of five of these Addenda Records may be included with each IAT entry. +type Addenda18 struct { + // ID is a client defined string used as a reference to this record. + ID string `json:"id"` + // RecordType defines the type of record in the block. entryAddenda18 Pos 7 + recordType string + // TypeCode Addenda18 types code '18' + typeCode string + // ForeignCorrespondentBankName contains the name of the Foreign Correspondent Bank + ForeignCorrespondentBankName string `json:"foreignCorrespondentBankName"` + // Foreign Correspondent Bank Identification Number Qualifier contains a 2-digit code that + // identifies the numbering scheme used in the Foreign Correspondent Bank Identification Number + // field. Code values for this field are: + // “01” = National Clearing System + // “02” = BIC Code + // “03” = IBAN Code + ForeignCorrespondentBankIDNumberQualifier string `json:"foreignCorrespondentBankIDNumberQualifier"` + // Foreign Correspondent Bank Identification Number contains the bank ID number of the Foreign + // Correspondent Bank + ForeignCorrespondentBankIDNumber string `json:"foreignCorrespondentBankIDNumber"` + // Foreign Correspondent Bank Branch Country Code contains the two-character code, as approved by + // the International Organization for Standardization (ISO), to identify the country in which the + // branch of the Foreign Correspondent Bank is located. Values can be found on the International + // Organization for Standardization website: www.iso.org + ForeignCorrespondentBankBranchCountryCode string `json:"foreignCorrespondentBankBranchCountryCode"` + // reserved - Leave blank + reserved string + // SequenceNumber is consecutively assigned to each Addenda18 Record following + // an Entry Detail Record. The first addenda18 sequence number must always + // be a "1". + SequenceNumber int `json:"sequenceNumber,omitempty"` + // EntryDetailSequenceNumber contains the ascending sequence number section of the Entry + // 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 `json:"entryDetailSequenceNumber,omitempty"` + // validator is composed for data validation + validator + // converters is composed for ACH to GoLang Converters + converters +} + +// NewAddenda18 returns a new Addenda18 with default values for none exported fields +func NewAddenda18() *Addenda18 { + addenda18 := new(Addenda18) + addenda18.recordType = "7" + addenda18.typeCode = "18" + return addenda18 +} + +// Parse takes the input record string and parses the Addenda18 values +func (addenda18 *Addenda18) Parse(record string) { + // 1-1 Always "7" + addenda18.recordType = "7" + // 2-3 Always 18 + addenda18.typeCode = record[1:3] + // 4-83 Based on the information entered (04-38) 35 alphanumeric + addenda18.ForeignCorrespondentBankName = strings.TrimSpace(record[3:38]) + // 39-40 Based on the information entered (39-40) 2 alphanumeric + // “01” = National Clearing System + // “02” = BIC Code + // “03” = IBAN Code + addenda18.ForeignCorrespondentBankIDNumberQualifier = record[38:40] + // 41-74 Based on the information entered (41-74) 34 alphanumeric + addenda18.ForeignCorrespondentBankIDNumber = strings.TrimSpace(record[40:74]) + // 75-77 Based on the information entered (75-77) 3 alphanumeric + addenda18.ForeignCorrespondentBankBranchCountryCode = strings.TrimSpace(record[74:77]) + // 78-83 - Blank space + addenda18.reserved = " " + // 84-87 SequenceNumber is consecutively assigned to each Addenda18 Record following + // an Entry Detail Record + addenda18.SequenceNumber = addenda18.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 + addenda18.EntryDetailSequenceNumber = addenda18.parseNumField(record[87:94]) +} + +// String writes the Addenda18 struct to a 94 character string. +func (addenda18 *Addenda18) String() string { + return fmt.Sprintf("%v%v%v%v%v%v%v%v%v", + addenda18.recordType, + addenda18.typeCode, + addenda18.ForeignCorrespondentBankNameField(), + addenda18.ForeignCorrespondentBankIDNumberQualifierField(), + addenda18.ForeignCorrespondentBankIDNumberField(), + addenda18.ForeignCorrespondentBankBranchCountryCodeField(), + addenda18.reservedField(), + addenda18.SequenceNumberField(), + addenda18.EntryDetailSequenceNumberField()) +} + +// Validate performs NACHA format rule checks on the record and returns an error if not Validated +// The first error encountered is returned and stops that parsing. +func (addenda18 *Addenda18) Validate() error { + if err := addenda18.fieldInclusion(); err != nil { + return err + } + if addenda18.recordType != "7" { + msg := fmt.Sprintf(msgRecordType, 7) + return &FieldError{FieldName: "recordType", Value: addenda18.recordType, Msg: msg} + } + if err := addenda18.isTypeCode(addenda18.typeCode); err != nil { + return &FieldError{FieldName: "TypeCode", Value: addenda18.typeCode, Msg: err.Error()} + } + // Type Code must be 18 + if addenda18.typeCode != "18" { + return &FieldError{FieldName: "TypeCode", Value: addenda18.typeCode, Msg: msgAddendaTypeCode} + } + if err := addenda18.isAlphanumeric(addenda18.ForeignCorrespondentBankName); err != nil { + return &FieldError{FieldName: "ForeignCorrespondentBankName", Value: addenda18.ForeignCorrespondentBankName, Msg: err.Error()} + } + if err := addenda18.isAlphanumeric(addenda18.ForeignCorrespondentBankIDNumberQualifier); err != nil { + return &FieldError{FieldName: "ForeignCorrespondentBankIDNumberQualifier", Value: addenda18.ForeignCorrespondentBankIDNumberQualifier, Msg: err.Error()} + } + if err := addenda18.isAlphanumeric(addenda18.ForeignCorrespondentBankIDNumber); err != nil { + return &FieldError{FieldName: "ForeignCorrespondentBankIDNumber", Value: addenda18.ForeignCorrespondentBankIDNumber, Msg: err.Error()} + } + if err := addenda18.isAlphanumeric(addenda18.ForeignCorrespondentBankBranchCountryCode); err != nil { + return &FieldError{FieldName: "ForeignCorrespondentBankBranchCountryCode", Value: addenda18.ForeignCorrespondentBankBranchCountryCode, Msg: err.Error()} + } + return nil +} + +// fieldInclusion validate mandatory fields are not default values. If fields are +// invalid the ACH transfer will be returned. +func (addenda18 *Addenda18) fieldInclusion() error { + if addenda18.recordType == "" { + return &FieldError{FieldName: "recordType", Value: addenda18.recordType, Msg: msgFieldInclusion} + } + if addenda18.typeCode == "" { + return &FieldError{FieldName: "TypeCode", Value: addenda18.typeCode, Msg: msgFieldInclusion} + } + if addenda18.ForeignCorrespondentBankName == "" { + return &FieldError{FieldName: "ForeignCorrespondentBankName", Value: addenda18.ForeignCorrespondentBankName, Msg: msgFieldInclusion} + } + if addenda18.ForeignCorrespondentBankIDNumberQualifier == "" { + return &FieldError{FieldName: "ForeignCorrespondentBankIDNumberQualifier", Value: addenda18.ForeignCorrespondentBankIDNumberQualifier, Msg: msgFieldInclusion} + } + if addenda18.ForeignCorrespondentBankIDNumber == "" { + return &FieldError{FieldName: "ForeignCorrespondentBankIDNumber", Value: addenda18.ForeignCorrespondentBankIDNumber, Msg: msgFieldInclusion} + } + if addenda18.ForeignCorrespondentBankBranchCountryCode == "" { + return &FieldError{FieldName: "ForeignCorrespondentBankBranchCountryCode", Value: addenda18.ForeignCorrespondentBankBranchCountryCode, Msg: msgFieldInclusion} + } + if addenda18.SequenceNumber == 0 { + return &FieldError{FieldName: "SequenceNumber", Value: addenda18.SequenceNumberField(), Msg: msgFieldInclusion} + } + if addenda18.EntryDetailSequenceNumber == 0 { + return &FieldError{FieldName: "EntryDetailSequenceNumber", Value: addenda18.EntryDetailSequenceNumberField(), Msg: msgFieldInclusion} + } + return nil +} + +// ForeignCorrespondentBankNameField returns a zero padded ForeignCorrespondentBankName string +func (addenda18 *Addenda18) ForeignCorrespondentBankNameField() string { + return addenda18.alphaField(addenda18.ForeignCorrespondentBankName, 35) +} + +// ForeignCorrespondentBankIDNumberQualifierField returns a zero padded ForeignCorrespondentBankIDNumberQualifier string +func (addenda18 *Addenda18) ForeignCorrespondentBankIDNumberQualifierField() string { + return addenda18.alphaField(addenda18.ForeignCorrespondentBankIDNumberQualifier, 2) +} + +// ForeignCorrespondentBankIDNumberField returns a zero padded ForeignCorrespondentBankIDNumber string +func (addenda18 *Addenda18) ForeignCorrespondentBankIDNumberField() string { + return addenda18.alphaField(addenda18.ForeignCorrespondentBankIDNumber, 34) +} + +// ForeignCorrespondentBankBranchCountryCodeField returns a zero padded ForeignCorrespondentBankBranchCountryCode string +func (addenda18 *Addenda18) ForeignCorrespondentBankBranchCountryCodeField() string { + return addenda18.alphaField(addenda18.ForeignCorrespondentBankBranchCountryCode, 3) +} + +// SequenceNumberField returns a zero padded SequenceNumber string +func (addenda18 *Addenda18) SequenceNumberField() string { + return addenda18.numericField(addenda18.SequenceNumber, 4) +} + +// reservedField gets reserved - blank space +func (addenda18 *Addenda18) reservedField() string { + return addenda18.alphaField(addenda18.reserved, 6) +} + +// EntryDetailSequenceNumberField returns a zero padded EntryDetailSequenceNumber string +func (addenda18 *Addenda18) EntryDetailSequenceNumberField() string { + return addenda18.numericField(addenda18.EntryDetailSequenceNumber, 7) +} + +// TypeCode Defines the specific explanation and format for the addenda18 information +func (addenda18 *Addenda18) TypeCode() string { + return addenda18.typeCode +} diff --git a/addenda18_test.go b/addenda18_test.go new file mode 100644 index 000000000..e1e59f3b9 --- /dev/null +++ b/addenda18_test.go @@ -0,0 +1,377 @@ +// 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" +) + +// mockAddenda18 creates a mock Addenda18 record +func mockAddenda18() *Addenda18 { + addenda18 := NewAddenda18() + addenda18.ForeignCorrespondentBankName = "Bank of Germany" + addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18.ForeignCorrespondentBankIDNumber = "987987987654654" + addenda18.ForeignCorrespondentBankBranchCountryCode = "DE" + addenda18.SequenceNumber = 1 + addenda18.EntryDetailSequenceNumber = 0000001 + return addenda18 +} + +func mockAddenda18B() *Addenda18 { + addenda18 := NewAddenda18() + addenda18.ForeignCorrespondentBankName = "Bank of Spain" + addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18.ForeignCorrespondentBankIDNumber = "987987987123123" + addenda18.ForeignCorrespondentBankBranchCountryCode = "ES" + addenda18.SequenceNumber = 2 + addenda18.EntryDetailSequenceNumber = 0000001 + return addenda18 +} + +func mockAddenda18C() *Addenda18 { + addenda18 := NewAddenda18() + addenda18.ForeignCorrespondentBankName = "Bank of France" + addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18.ForeignCorrespondentBankIDNumber = "456456456987987" + addenda18.ForeignCorrespondentBankBranchCountryCode = "FR" + addenda18.SequenceNumber = 3 + addenda18.EntryDetailSequenceNumber = 0000001 + return addenda18 +} + +func mockAddenda18D() *Addenda18 { + addenda18 := NewAddenda18() + addenda18.ForeignCorrespondentBankName = "Bank of Turkey" + addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18.ForeignCorrespondentBankIDNumber = "12312345678910" + addenda18.ForeignCorrespondentBankBranchCountryCode = "TR" + addenda18.SequenceNumber = 4 + addenda18.EntryDetailSequenceNumber = 0000001 + return addenda18 +} + +func mockAddenda18E() *Addenda18 { + addenda18 := NewAddenda18() + addenda18.ForeignCorrespondentBankName = "Bank of United Kingdom" + addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18.ForeignCorrespondentBankIDNumber = "1234567890123456789012345678901234" + addenda18.ForeignCorrespondentBankBranchCountryCode = "GB" + addenda18.SequenceNumber = 5 + addenda18.EntryDetailSequenceNumber = 0000001 + return addenda18 +} + +func mockAddenda18F() *Addenda18 { + addenda18 := NewAddenda18() + addenda18.ForeignCorrespondentBankName = "Bank of Antarctica" + addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18.ForeignCorrespondentBankIDNumber = "123456789012345678901" + addenda18.ForeignCorrespondentBankBranchCountryCode = "AQ" + addenda18.SequenceNumber = 6 + addenda18.EntryDetailSequenceNumber = 0000001 + return addenda18 +} + +// TestMockAddenda18 validates mockAddenda18 +func TestMockAddenda18(t *testing.T) { + addenda18 := mockAddenda18() + if err := addenda18.Validate(); err != nil { + t.Error("mockAddenda18 does not validate and will break other tests") + } + if addenda18.ForeignCorrespondentBankName != "Bank of Germany" { + t.Error("ForeignCorrespondentBankName dependent default value has changed") + } + if addenda18.ForeignCorrespondentBankIDNumberQualifier != "01" { + t.Error("ForeignCorrespondentBankIDNumberQualifier dependent default value has changed") + } + if addenda18.ForeignCorrespondentBankIDNumber != "987987987654654" { + t.Error("ForeignCorrespondentBankIDNumber dependent default value has changed") + } + if addenda18.ForeignCorrespondentBankBranchCountryCode != "DE" { + t.Error("ForeignCorrespondentBankBranchCountryCode dependent default value has changed") + } + if addenda18.EntryDetailSequenceNumber != 0000001 { + t.Error("EntryDetailSequenceNumber dependent default value has changed") + } +} + +// testAddenda18Parse parses Addenda18 record +func testAddenda18Parse(t testing.TB) { + Addenda18 := NewAddenda18() + line := "718Bank of Germany 01987987987654654 DE 00010000001" + Addenda18.Parse(line) + // walk the Addenda18 struct + if Addenda18.recordType != "7" { + t.Errorf("expected %v got %v", "7", Addenda18.recordType) + } + if Addenda18.typeCode != "18" { + t.Errorf("expected %v got %v", "18", Addenda18.typeCode) + } + if Addenda18.ForeignCorrespondentBankName != "Bank of Germany" { + t.Errorf("expected %v got %v", "Bank of Germany", Addenda18.ForeignCorrespondentBankName) + } + if Addenda18.ForeignCorrespondentBankIDNumberQualifier != "01" { + t.Errorf("expected: %v got: %v", "01", Addenda18.ForeignCorrespondentBankIDNumberQualifier) + } + if Addenda18.ForeignCorrespondentBankIDNumber != "987987987654654" { + t.Errorf("expected: %v got: %v", "987987987654654", Addenda18.ForeignCorrespondentBankIDNumber) + } + if Addenda18.ForeignCorrespondentBankBranchCountryCode != "DE" { + t.Errorf("expected: %s got: %s", "DE", Addenda18.ForeignCorrespondentBankBranchCountryCode) + } + if Addenda18.reserved != " " { + t.Errorf("expected: %v got: %v", " ", Addenda18.reserved) + } + if Addenda18.EntryDetailSequenceNumber != 0000001 { + t.Errorf("expected: %v got: %v", 0000001, Addenda18.EntryDetailSequenceNumber) + } +} + +// TestAddenda18Parse tests parsing Addenda18 record +func TestAddenda18Parse(t *testing.T) { + testAddenda18Parse(t) +} + +// BenchmarkAddenda18Parse benchmarks parsing Addenda18 record +func BenchmarkAddenda18Parse(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda18Parse(b) + } +} + +// testAddenda18String validates that a known parsed file can be return to a string of the same value +func testAddenda18String(t testing.TB) { + addenda18 := NewAddenda18() + var line = "718Bank of United Kingdom 011234567890123456789012345678901234GB 00010000001" + addenda18.Parse(line) + + if addenda18.String() != line { + t.Errorf("Strings do not match") + } +} + +// TestAddenda18 String tests validating that a known parsed file can be return to a string of the same value +func TestAddenda18String(t *testing.T) { + testAddenda18String(t) +} + +// BenchmarkAddenda18 String benchmarks validating that a known parsed file can be return to a string of the same value +func BenchmarkAddenda18String(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda18String(b) + } +} + +func TestValidateAddenda18RecordType(t *testing.T) { + addenda18 := mockAddenda18() + addenda18.recordType = "63" + if err := addenda18.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +func TestAddenda18TypeCodeFieldInclusion(t *testing.T) { + addenda18 := mockAddenda18() + addenda18.typeCode = "" + if err := addenda18.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +func TestAddenda18FieldInclusion(t *testing.T) { + addenda18 := mockAddenda18() + addenda18.EntryDetailSequenceNumber = 0 + if err := addenda18.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "EntryDetailSequenceNumber" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +func TestAddenda18FieldInclusionRecordType(t *testing.T) { + addenda18 := mockAddenda18() + addenda18.recordType = "" + if err := addenda18.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.Msg != msgFieldInclusion { + t.Errorf("%T: %s", err, err) + } + } + } +} + +//testAddenda18ForeignCorrespondentBankNameAlphaNumeric validates ForeignCorrespondentBankName is alphanumeric +func testAddenda18ForeignCorrespondentBankNameAlphaNumeric(t testing.TB) { + addenda18 := mockAddenda18() + addenda18.ForeignCorrespondentBankName = "®©" + if err := addenda18.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ForeignCorrespondentBankName" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda18ForeignCorrespondentBankNameAlphaNumeric tests validating ForeignCorrespondentBankName is alphanumeric +func TestAddenda18ForeignCorrespondentBankNameAlphaNumeric(t *testing.T) { + testAddenda18ForeignCorrespondentBankNameAlphaNumeric(t) + +} + +// BenchmarkAddenda18ForeignCorrespondentBankNameAlphaNumeric benchmarks ForeignCorrespondentBankName is alphanumeric +func BenchmarkAddenda18ForeignCorrespondentBankNameAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda18ForeignCorrespondentBankNameAlphaNumeric(b) + } +} + +//testAddenda18ForeignCorrespondentBankIDQualifierAlphaNumeric validates ForeignCorrespondentBankIDNumberQualifier is alphanumeric +func testAddenda18ForeignCorrespondentBankIDQualifierAlphaNumeric(t testing.TB) { + addenda18 := mockAddenda18() + addenda18.ForeignCorrespondentBankIDNumberQualifier = "®©" + if err := addenda18.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ForeignCorrespondentBankIDNumberQualifier" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda18ForeignCorrespondentBankIDQualifierAlphaNumeric tests validating ForeignCorrespondentBankIDNumberQualifier is alphanumeric +func TestAddenda18ForeignCorrespondentBankIDQualifierAlphaNumeric(t *testing.T) { + testAddenda18ForeignCorrespondentBankIDQualifierAlphaNumeric(t) +} + +// BenchmarkAddenda18ForeignCorrespondentBankIDQualifierAlphaNumeric benchmarks ForeignCorrespondentBankIDNumberQualifier is alphanumeric +func BenchmarkAddenda18ForeignCorrespondentBankIDQualifierAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda18ForeignCorrespondentBankIDQualifierAlphaNumeric(b) + } +} + +//testAddenda18ForeignCorrespondentBankBranchCountryCodeAlphaNumeric validates ForeignCorrespondentBankBranchCountryCode is alphanumeric +func testAddenda18ForeignCorrespondentBankBranchCountryCodeAlphaNumeric(t testing.TB) { + addenda18 := mockAddenda18() + addenda18.ForeignCorrespondentBankBranchCountryCode = "®©" + if err := addenda18.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ForeignCorrespondentBankBranchCountryCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda18ForeignCorrespondentBankBranchCountryCodeNumeric tests validating ForeignCorrespondentBankBranchCountryCode is alphanumeric +func TestAddenda18ForeignCorrespondentBankBranchCountryCodeAlphaNumeric(t *testing.T) { + testAddenda18ForeignCorrespondentBankBranchCountryCodeAlphaNumeric(t) +} + +// BenchmarkAddenda18ForeignCorrespondentBankBranchCountryCodeAlphaNumeric benchmarks ForeignCorrespondentBankBranchCountryCode is alphanumeric +func BenchmarkAddenda18ForeignCorrespondentBankBranchCountryCodeAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda18ForeignCorrespondentBankBranchCountryCodeAlphaNumeric(b) + } +} + +//testAddenda18ForeignCorrespondentBankIDNumberAlphaNumeric validates ForeignCorrespondentBankIDNumber is alphanumeric +func testAddenda18ForeignCorrespondentBankIDNumberAlphaNumeric(t testing.TB) { + addenda18 := mockAddenda18() + addenda18.ForeignCorrespondentBankIDNumber = "®©" + if err := addenda18.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ForeignCorrespondentBankIDNumber" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestAddenda18ForeignCorrespondentBankIDNumberAlphaNumeric tests validating ForeignCorrespondentBankIDNumber is alphanumeric +func TestAddenda18ForeignCorrespondentBankIDNumberAlphaNumeric(t *testing.T) { + testAddenda18ForeignCorrespondentBankIDNumberAlphaNumeric(t) +} + +// BenchmarkAddenda18ForeignCorrespondentBankIDNumberAlphaNumeric benchmarks ForeignCorrespondentBankIDNumber is alphanumeric +func BenchmarkAddendaForeignCorrespondentBankIDNumberAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda18ForeignCorrespondentBankIDNumberAlphaNumeric(b) + } +} + +// testAddenda18ValidTypeCode validates Addenda18 TypeCode +func testAddenda18ValidTypeCode(t testing.TB) { + addenda18 := mockAddenda18() + addenda18.typeCode = "65" + if err := addenda18.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda18ValidTypeCode tests validating Addenda18 TypeCode +func TestAddenda18ValidTypeCode(t *testing.T) { + testAddenda18ValidTypeCode(t) +} + +// BenchmarkAddenda18ValidTypeCode benchmarks validating Addenda18 TypeCode +func BenchmarkAddenda18ValidTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda18ValidTypeCode(b) + } +} + +// testAddenda18TypeCode18 TypeCode is 18 if typeCode is a valid TypeCode +func testAddenda18TypeCode18(t testing.TB) { + addenda18 := mockAddenda18() + addenda18.typeCode = "05" + if err := addenda18.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda18TypeCode18 tests TypeCode is 18 if typeCode is a valid TypeCode +func TestAddenda18TypeCode18(t *testing.T) { + testAddenda18TypeCode18(t) +} + +// BenchmarkAddenda18TypeCode18 benchmarks TypeCode is 18 if typeCode is a valid TypeCode +func BenchmarkAddenda18TypeCode18(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda18TypeCode18(b) + } +} diff --git a/batch.go b/batch.go index 6eae8d548..274b2255a 100644 --- a/batch.go +++ b/batch.go @@ -37,6 +37,10 @@ func NewBatch(bh *BatchHeader) (Batcher, error) { return NewBatchCIE(bh), nil case "COR": return NewBatchCOR(bh), nil + case "IAT": + //ToDo: Update message to tell user to use iatBatch.go + msg := fmt.Sprintf(msgFileNoneSEC, bh.StandardEntryClassCode) + return nil, &FileError{FieldName: "StandardEntryClassCode", Msg: msg} case "POP": return NewBatchPOP(bh), nil case "POS": @@ -61,6 +65,10 @@ func NewBatch(bh *BatchHeader) (Batcher, error) { func (batch *batch) verify() error { batchNumber := batch.Header.BatchNumber + // No entries in batch + if len(batch.Entries) <= 0 { + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "entries", Msg: msgBatchEntries} + } // verify field inclusion in all the records of the batch. if err := batch.isFieldInclusion(); err != nil { // convert the field error in to a batch error for a consistent api @@ -86,30 +94,24 @@ func (batch *batch) verify() error { } // batch number header and control must match if batch.Header.BatchNumber != batch.Control.BatchNumber { - msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.Header.ODFIIdentification, batch.Control.ODFIIdentification) + msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.Header.BatchNumber, batch.Control.BatchNumber) return &BatchError{BatchNumber: batchNumber, FieldName: "BatchNumber", Msg: msg} } - if err := batch.isBatchEntryCount(); err != nil { return err } - if err := batch.isSequenceAscending(); err != nil { return err } - if err := batch.isBatchAmount(); err != nil { return err } - if err := batch.isEntryHash(); err != nil { return err } - if err := batch.isOriginatorDNE(); err != nil { return err } - if err := batch.isTraceNumberODFI(); err != nil { return err } @@ -117,7 +119,10 @@ func (batch *batch) verify() error { if err := batch.isAddendaSequence(); err != nil { return err } - return batch.isCategory() + if err := batch.isCategory(); err != nil { + return err + } + return nil } // Build creates valid batch by building sequence numbers and batch batch control. An error is returned if @@ -342,7 +347,6 @@ func (batch *batch) isTraceNumberODFI() error { return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "ODFIIdentificationField", Msg: msg} } } - return nil } @@ -408,7 +412,7 @@ func (batch *batch) isTypeCode(typeCode string) error { func (batch *batch) isCategory() error { category := batch.GetEntries()[0].Category if len(batch.Entries) > 1 { - for i := 1; i < len(batch.Entries); i++ { + for i := 0; i < len(batch.Entries); i++ { if batch.Entries[i].Category == CategoryNOC { continue } diff --git a/batchCIE_test.go b/batchCIE_test.go index e4c8f2c3f..60f6bfd90 100644 --- a/batchCIE_test.go +++ b/batchCIE_test.go @@ -377,8 +377,6 @@ func BenchmarkBatchCIEInvalidAddenda(b *testing.B) { } } -// ToDo: Using a FieldError may need to add a BatchError and use *BatchError - // testBatchCIEInvalidBuild validates an invalid batch build func testBatchCIEInvalidBuild(t testing.TB) { mockBatch := mockBatchCIE() diff --git a/batchPOS_test.go b/batchPOS_test.go index 06428f26c..5ce7bd8bc 100644 --- a/batchPOS_test.go +++ b/batchPOS_test.go @@ -378,8 +378,6 @@ func BenchmarkBatchPOSInvalidAddenda(b *testing.B) { } } -// ToDo: Using a FieldError may need to add a BatchError and use *BatchError - // testBatchPOSInvalidBuild validates an invalid batch build func testBatchPOSInvalidBuild(t testing.TB) { mockBatch := mockBatchPOS() diff --git a/batchSHR_test.go b/batchSHR_test.go index 52649899e..5eb13e1d4 100644 --- a/batchSHR_test.go +++ b/batchSHR_test.go @@ -380,8 +380,6 @@ func BenchmarkBatchSHRInvalidAddenda(b *testing.B) { } } -// ToDo: Using a FieldError may need to add a BatchError and use *BatchError - // testBatchSHRInvalidBuild validates an invalid batch build func testBatchSHRInvalidBuild(t testing.TB) { mockBatch := mockBatchSHR() diff --git a/batch_test.go b/batch_test.go index 47e9b14ce..24ddeafd6 100644 --- a/batch_test.go +++ b/batch_test.go @@ -125,6 +125,7 @@ func BenchmarkCreditBatchisBatchAmount(b *testing.B) { } } + func testSavingsBatchisBatchAmount(t testing.TB) { mockBatch := mockBatch() mockBatch.SetHeader(mockBatchHeader()) @@ -524,6 +525,7 @@ func BenchmarkBatchFieldInclusion(b *testing.B) { } } +// testBatchInvalidTraceNumberODFI validates TraceNumberODFI func testBatchInvalidTraceNumberODFI(t testing.TB) { mockBatch := mockBatchInvalidTraceNumberODFI() if err := mockBatch.build(); err != nil { @@ -531,10 +533,12 @@ func testBatchInvalidTraceNumberODFI(t testing.TB) { } } +// TestBatchInvalidTraceNumberODFI tests validating TraceNumberODFI func TestBatchInvalidTraceNumberODFI(t *testing.T) { testBatchInvalidTraceNumberODFI(t) } +// BenchmarkBatchInvalidTraceNumberODFI benchmarks validating TraceNumberODFI func BenchmarkBatchInvalidTraceNumberODFI(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -566,6 +570,7 @@ func BenchmarkBatchNoEntry(b *testing.B) { } } +// testBatchControl validates BatchControl ODFIIdentification func testBatchControl(t testing.TB) { mockBatch := mockBatch() mockBatch.Control.ODFIIdentification = "" @@ -580,10 +585,12 @@ func testBatchControl(t testing.TB) { } } +// TestBatchControl tests validating BatchControl ODFIIdentification func TestBatchControl(t *testing.T) { testBatchControl(t) } +// BenchmarkBatchControl benchmarks validating BatchControl ODFIIdentification func BenchmarkBatchControl(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { diff --git a/entryDetail.go b/entryDetail.go index 3a3c3bca5..0594650b2 100644 --- a/entryDetail.go +++ b/entryDetail.go @@ -29,28 +29,21 @@ type EntryDetail struct { // Debit to savings account ‘37’ // Prenote for debit to savings account ‘38’ TransactionCode int `json:"transactionCode"` - // RDFIIdentification is the RDFI's routing number without the last digit. // Receiving Depository Financial Institution RDFIIdentification string `json:"RDFIIdentification"` - // CheckDigit the last digit of the RDFI's routing number CheckDigit string `json:"checkDigit"` - // DFIAccountNumber is the receiver's bank account number you are crediting/debiting. // It important to note that this is an alphanumeric field, so its space padded, no zero padded DFIAccountNumber string `json:"DFIAccountNumber"` - // Amount Number of cents you are debiting/crediting this account Amount int `json:"amount"` - // IdentificationNumber an internal identification (alphanumeric) that // you use to uniquely identify this Entry Detail Record IdentificationNumber string `json:"identificationNumber,omitempty"` - // IndividualName The name of the receiver, usually the name on the bank account IndividualName string `json:"individualName"` - // 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 @@ -60,12 +53,10 @@ type EntryDetail struct { // // WEB uses the Discretionary Data Field as the Payment Type Code DiscretionaryData string `json:"discretionaryData,omitempty"` - // AddendaRecordIndicator indicates the existence of an Addenda Record. // A value of "1" indicates that one ore more addenda records follow, // and "0" means no such record is present. AddendaRecordIndicator int `json:"addendaRecordIndicator,omitempty"` - // TraceNumber assigned by the ODFI in ascending sequence, is included in each // Entry Detail Record, Corporate Entry Detail Record, and addenda Record. // Trace Numbers uniquely identify each entry within a batch in an ACH input file. @@ -75,7 +66,6 @@ type EntryDetail struct { // in the associated Entry Detail Record, since the Trace Number is associated // with an entry or item rather than a physical record. TraceNumber int `json:"traceNumber,omitempty"` - // Addendum a list of Addenda for the Entry Detail Addendum []Addendumer `json:"addendum,omitempty"` // Category defines if the entry is a Forward, Return, or NOC diff --git a/entryDetail_test.go b/entryDetail_test.go index 40228666b..74b6632d6 100644 --- a/entryDetail_test.go +++ b/entryDetail_test.go @@ -378,7 +378,7 @@ func TestEDSetRDFI(t *testing.T) { testEDSetRDFI(t) } -// Benchmark benchmarks validating setting RDFI +// BenchmarkEDSetRDFI benchmarks validating setting RDFI func BenchmarkEDSetRDFI(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { diff --git a/file.go b/file.go index 74123019d..4ddb0786c 100644 --- a/file.go +++ b/file.go @@ -53,10 +53,11 @@ func (e *FileError) Error() string { // File contains the structures of a parsed ACH File. type File struct { - ID string `json:"id"` - Header FileHeader `json:"fileHeader"` - Batches []Batcher `json:"batches"` - Control FileControl `json:"fileControl"` + ID string `json:"id"` + Header FileHeader `json:"fileHeader"` + Batches []Batcher `json:"batches"` + IATBatches []IATBatch `json:"IATBatches"` + Control FileControl `json:"fileControl"` // NotificationOfChange (Notification of change) is a slice of references to BatchCOR in file.Batches NotificationOfChange []*BatchCOR @@ -81,7 +82,7 @@ func (f *File) Create() error { return err } // Requires at least one Batch in the new file. - if len(f.Batches) <= 0 { + if len(f.Batches) <= 0 && len(f.IATBatches) <= 0 { return &FileError{FieldName: "Batches", Value: strconv.Itoa(len(f.Batches)), Msg: "must have []*Batches to be built"} } // add 2 for FileHeader/control and reset if build was called twice do to error @@ -105,6 +106,21 @@ func (f *File) Create() error { totalDebitAmount = totalDebitAmount + batch.GetControl().TotalDebitEntryDollarAmount totalCreditAmount = totalCreditAmount + batch.GetControl().TotalCreditEntryDollarAmount + } + for i, iatBatch := range f.IATBatches { + // create ascending batch numbers + f.IATBatches[i].GetHeader().BatchNumber = batchSeq + f.IATBatches[i].GetControl().BatchNumber = batchSeq + batchSeq++ + // sum file entry and addenda records. Assume batch.Create() batch properly calculated control + fileEntryAddendaCount = fileEntryAddendaCount + iatBatch.GetControl().EntryAddendaCount + // add 2 for Batch header/control + entry added count + totalRecordsInFile = totalRecordsInFile + 2 + iatBatch.GetControl().EntryAddendaCount + // sum hash from batch control. Assume Batch.Build properly calculated field. + fileEntryHashSum = fileEntryHashSum + iatBatch.GetControl().EntryHash + totalDebitAmount = totalDebitAmount + iatBatch.GetControl().TotalDebitEntryDollarAmount + totalCreditAmount = totalCreditAmount + iatBatch.GetControl().TotalCreditEntryDollarAmount + } // create FileControl from calculated values fc := NewFileControl() @@ -137,6 +153,12 @@ func (f *File) AddBatch(batch Batcher) []Batcher { return f.Batches } +// AddIATBatch appends a IATBatch to the ach.File +func (f *File) AddIATBatch(iatBatch IATBatch) []IATBatch { + f.IATBatches = append(f.IATBatches, iatBatch) + return f.IATBatches +} + // SetHeader allows for header to be built. func (f *File) SetHeader(h FileHeader) *File { f.Header = h @@ -146,7 +168,7 @@ func (f *File) SetHeader(h FileHeader) *File { // Validate NACHA rules on the entire batch before being added to a File func (f *File) Validate() error { // The value of the Batch Count Field is equal to the number of Company/Batch/Header Records in the file. - if f.Control.BatchCount != len(f.Batches) { + if f.Control.BatchCount != (len(f.Batches) + len(f.IATBatches)) { msg := fmt.Sprintf(msgFileCalculatedControlEquality, len(f.Batches), f.Control.BatchCount) return &FileError{FieldName: "BatchCount", Value: strconv.Itoa(len(f.Batches)), Msg: msg} } @@ -162,7 +184,7 @@ func (f *File) Validate() error { return f.isEntryHash() } -// isEntryAddenda is prepared by hashing the RDFI’s 8-digit Routing Number in each entry. +// isEntryAddendaCount is prepared by hashing the RDFI’s 8-digit Routing Number in each entry. //The Entry Hash provides a check against inadvertent alteration of data func (f *File) isEntryAddendaCount() error { count := 0 @@ -170,6 +192,10 @@ func (f *File) isEntryAddendaCount() error { for _, batch := range f.Batches { count += batch.GetControl().EntryAddendaCount } + // IAT + for _, iatBatch := range f.IATBatches { + count += iatBatch.GetControl().EntryAddendaCount + } if f.Control.EntryAddendaCount != count { msg := fmt.Sprintf(msgFileCalculatedControlEquality, count, f.Control.EntryAddendaCount) return &FileError{FieldName: "EntryAddendaCount", Value: f.Control.EntryAddendaCountField(), Msg: msg} @@ -186,6 +212,11 @@ func (f *File) isFileAmount() error { debit += batch.GetControl().TotalDebitEntryDollarAmount credit += batch.GetControl().TotalCreditEntryDollarAmount } + // IAT + for _, iatBatch := range f.IATBatches { + debit += iatBatch.GetControl().TotalDebitEntryDollarAmount + credit += iatBatch.GetControl().TotalCreditEntryDollarAmount + } if f.Control.TotalDebitEntryDollarAmountInFile != debit { msg := fmt.Sprintf(msgFileCalculatedControlEquality, debit, f.Control.TotalDebitEntryDollarAmountInFile) return &FileError{FieldName: "TotalDebitEntryDollarAmountInFile", Value: f.Control.TotalDebitEntryDollarAmountInFileField(), Msg: msg} @@ -214,5 +245,9 @@ func (f *File) calculateEntryHash() string { for _, batch := range f.Batches { hash = hash + batch.GetControl().EntryHash } + // IAT + for _, iatBatch := range f.IATBatches { + hash = hash + iatBatch.GetControl().EntryHash + } return f.numericField(hash, 10) } diff --git a/iatBatch.go b/iatBatch.go new file mode 100644 index 000000000..22eccb13d --- /dev/null +++ b/iatBatch.go @@ -0,0 +1,533 @@ +// 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" + "strconv" +) + +var ( + msgIATBatchAddendaRequired = "is required for an IAT detail entry" + 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" + msgBatchIATInvalidAddendumer = "invalid Addendumer for SEC Code IAT" +) + +// IATBatch holds the Batch Header and Batch Control and all Entry Records for an IAT batch +// +// An IAT entry is a credit or debit ACH entry that is part of a payment transaction involving +// a financial agency’s office (i.e., depository financial institution or business issuing money +// orders) that is not located in the territorial jurisdiction of the United States. IAT entries +// can be made to or from a corporate or consumer account and must be accompanied by seven (7) +// mandatory addenda records identifying the name and physical address of the Originator, name +// and physical address of the Receiver, Receiver’s account number, Receiver’s bank identity and +// reason for the payment. +type IATBatch struct { + // ID is a client defined string used as a reference to this record. + ID string `json:"id"` + Header *IATBatchHeader `json:"IATBatchHeader,omitempty"` + Entries []*IATEntryDetail `json:"IATEntryDetails,omitempty"` + Control *BatchControl `json:"batchControl,omitempty"` + + // category defines if the entry is a Forward, Return, or NOC + category string + // Converters is composed for ACH to GoLang Converters + converters +} + +// IATNewBatch takes a BatchHeader and returns a matching SEC code batch type that is a batcher. Returns an error if the SEC code is not supported. +func IATNewBatch(bh *IATBatchHeader) IATBatch { + iatBatch := IATBatch{} + iatBatch.SetControl(NewBatchControl()) + iatBatch.SetHeader(bh) + return iatBatch +} + +// verify checks basic valid NACHA batch rules. Assumes properly parsed records. This does not mean it is a valid batch as validity is tied to each batch type +func (batch *IATBatch) verify() error { + batchNumber := batch.Header.BatchNumber + + // No entries in batch + if len(batch.Entries) <= 0 { + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "entries", Msg: msgBatchEntries} + } + // verify field inclusion in all the records of the batch. + if err := batch.isFieldInclusion(); err != nil { + // convert the field error in to a batch error for a consistent api + if e, ok := err.(*FieldError); ok { + return &BatchError{BatchNumber: batchNumber, FieldName: e.FieldName, Msg: e.Msg} + } + return &BatchError{BatchNumber: batchNumber, FieldName: "FieldError", Msg: err.Error()} + } + // validate batch header and control codes are the same + if batch.Header.ServiceClassCode != batch.Control.ServiceClassCode { + msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.Header.ServiceClassCode, batch.Control.ServiceClassCode) + return &BatchError{BatchNumber: batchNumber, FieldName: "ServiceClassCode", Msg: msg} + } + // Control ODFIIdentification must be the same as batch header + if batch.Header.ODFIIdentification != batch.Control.ODFIIdentification { + msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.Header.ODFIIdentification, batch.Control.ODFIIdentification) + return &BatchError{BatchNumber: batchNumber, FieldName: "ODFIIdentification", Msg: msg} + } + // batch number header and control must match + if batch.Header.BatchNumber != batch.Control.BatchNumber { + msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.Header.BatchNumber, batch.Control.BatchNumber) + return &BatchError{BatchNumber: batchNumber, FieldName: "BatchNumber", Msg: msg} + } + if err := batch.isBatchEntryCount(); err != nil { + return err + } + if err := batch.isSequenceAscending(); err != nil { + return err + } + if err := batch.isBatchAmount(); err != nil { + return err + } + if err := batch.isEntryHash(); err != nil { + return err + } + if err := batch.isTraceNumberODFI(); err != nil { + return err + } + if err := batch.isAddendaSequence(); err != nil { + return err + } + if err := batch.isCategory(); err != nil { + return err + } + return nil +} + +// Build creates valid batch by building sequence numbers and batch batch control. An error is returned if +// the batch being built has invalid records. +func (batch *IATBatch) build() error { + // Requires a valid BatchHeader + if err := batch.Header.Validate(); err != nil { + return err + } + if len(batch.Entries) <= 0 { + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "entries", Msg: msgBatchEntries} + } + // Create record sequence numbers + entryCount := 0 + seq := 1 + for i, entry := range batch.Entries { + entryCount = entryCount + 1 + 7 + len(entry.Addendum) + + // Verifies the required addenda* properties for an IAT entry detail are defined + if err := batch.addendaFieldInclusion(entry); err != nil { + return err + } + + currentTraceNumberODFI, err := strconv.Atoi(entry.TraceNumberField()[:8]) + if err != nil { + return err + } + + batchHeaderODFI, err := strconv.Atoi(batch.Header.ODFIIdentificationField()[:8]) + if err != nil { + return err + } + + // Add a sequenced TraceNumber if one is not already set. + if currentTraceNumberODFI != batchHeaderODFI { + batch.Entries[i].SetTraceNumber(batch.Header.ODFIIdentification, seq) + } + + // Set TraceNumber for IATEntryDetail Addenda10-16 Record Properties + entry.Addenda10.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + entry.Addenda11.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + entry.Addenda12.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + entry.Addenda13.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + entry.Addenda14.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + entry.Addenda15.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + entry.Addenda16.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + + // Set TraceNumber for Addendumer Addenda17 and Addenda18 SequenceNumber and EntryDetailSequenceNumber + seq++ + addenda17Seq := 1 + addenda18Seq := 1 + for x := range entry.Addendum { + if a, ok := batch.Entries[i].Addendum[x].(*Addenda17); ok { + a.SequenceNumber = addenda17Seq + a.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + addenda17Seq++ + } + + if a, ok := batch.Entries[i].Addendum[x].(*Addenda18); ok { + a.SequenceNumber = addenda18Seq + a.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + addenda18Seq++ + } + } + } + + // build a BatchControl record + bc := NewBatchControl() + bc.ServiceClassCode = batch.Header.ServiceClassCode + bc.ODFIIdentification = batch.Header.ODFIIdentification + bc.BatchNumber = batch.Header.BatchNumber + bc.EntryAddendaCount = entryCount + bc.EntryHash = batch.parseNumField(batch.calculateEntryHash()) + bc.TotalCreditEntryDollarAmount, bc.TotalDebitEntryDollarAmount = batch.calculateBatchAmounts() + batch.Control = bc + + return nil +} + +// SetHeader appends an BatchHeader to the Batch +func (batch *IATBatch) SetHeader(batchHeader *IATBatchHeader) { + batch.Header = batchHeader +} + +// GetHeader returns the current Batch header +func (batch *IATBatch) GetHeader() *IATBatchHeader { + return batch.Header +} + +// SetControl appends an BatchControl to the Batch +func (batch *IATBatch) SetControl(batchControl *BatchControl) { + batch.Control = batchControl +} + +// GetControl returns the current Batch Control +func (batch *IATBatch) GetControl() *BatchControl { + return batch.Control +} + +// GetEntries returns a slice of entry details for the batch +func (batch *IATBatch) GetEntries() []*IATEntryDetail { + return batch.Entries +} + +// AddEntry appends an EntryDetail to the Batch +func (batch *IATBatch) AddEntry(entry *IATEntryDetail) { + batch.category = entry.Category + batch.Entries = append(batch.Entries, entry) +} + +// Category returns IATBatch Category +func (batch *IATBatch) Category() string { + return batch.category +} + +// isFieldInclusion iterates through all the records in the batch and verifies against default fields +func (batch *IATBatch) isFieldInclusion() error { + if err := batch.Header.Validate(); err != nil { + return err + } + for _, entry := range batch.Entries { + if err := entry.Validate(); err != nil { + return err + } + // Verifies the required Addenda* properties for an IAT entry detail are included + if err := batch.addendaFieldInclusion(entry); err != nil { + return err + } + // Verifies each Addenda* record is valid + if err := entry.Addenda10.Validate(); err != nil { + return err + } + if err := entry.Addenda11.Validate(); err != nil { + return err + } + if err := entry.Addenda12.Validate(); err != nil { + return err + } + if err := entry.Addenda13.Validate(); err != nil { + return err + } + if err := entry.Addenda14.Validate(); err != nil { + return err + } + if err := entry.Addenda15.Validate(); err != nil { + return err + } + if err := entry.Addenda16.Validate(); err != nil { + return err + } + // Verifies addendumer Addenda17 and Addenda18 records are valid + for _, IATAddenda := range entry.Addendum { + if err := IATAddenda.Validate(); err != nil { + return err + } + } + } + return batch.Control.Validate() +} + +// isBatchEntryCount validate Entry count is accurate +// The Entry/Addenda Count Field is a tally of each Entry Detail and Addenda +// Record processed within the batch +func (batch *IATBatch) isBatchEntryCount() error { + entryCount := 0 + for _, entry := range batch.Entries { + entryCount = entryCount + 1 + 7 + len(entry.Addendum) + } + if entryCount != batch.Control.EntryAddendaCount { + msg := fmt.Sprintf(msgBatchCalculatedControlEquality, entryCount, batch.Control.EntryAddendaCount) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "EntryAddendaCount", Msg: msg} + } + return nil +} + +// isBatchAmount validate Amount is the same as what is in the Entries +// The Total Debit and Credit Entry Dollar Amount fields contain accumulated +// Entry Detail debit and credit totals within a given batch +func (batch *IATBatch) isBatchAmount() error { + credit, debit := batch.calculateBatchAmounts() + if debit != batch.Control.TotalDebitEntryDollarAmount { + msg := fmt.Sprintf(msgBatchCalculatedControlEquality, debit, batch.Control.TotalDebitEntryDollarAmount) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TotalDebitEntryDollarAmount", Msg: msg} + } + + if credit != batch.Control.TotalCreditEntryDollarAmount { + msg := fmt.Sprintf(msgBatchCalculatedControlEquality, credit, batch.Control.TotalCreditEntryDollarAmount) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TotalCreditEntryDollarAmount", Msg: msg} + } + return nil +} + +func (batch *IATBatch) calculateBatchAmounts() (credit int, debit int) { + for _, entry := range batch.Entries { + if entry.TransactionCode == 21 || entry.TransactionCode == 22 || entry.TransactionCode == 23 || entry.TransactionCode == 32 || entry.TransactionCode == 33 { + credit = credit + entry.Amount + } + if entry.TransactionCode == 26 || entry.TransactionCode == 27 || entry.TransactionCode == 28 || entry.TransactionCode == 36 || entry.TransactionCode == 37 || entry.TransactionCode == 38 { + debit = debit + entry.Amount + } + } + return credit, debit +} + +// isSequenceAscending Individual Entry Detail Records within individual batches must +// be in ascending Trace Number order (although Trace Numbers need not necessarily be consecutive). +func (batch *IATBatch) isSequenceAscending() error { + lastSeq := -1 + for _, entry := range batch.Entries { + if entry.TraceNumber <= lastSeq { + msg := fmt.Sprintf(msgBatchAscending, entry.TraceNumber, lastSeq) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TraceNumber", Msg: msg} + } + lastSeq = entry.TraceNumber + } + return nil +} + +// isEntryHash validates the hash by recalculating the result +func (batch *IATBatch) isEntryHash() error { + hashField := batch.calculateEntryHash() + if hashField != batch.Control.EntryHashField() { + msg := fmt.Sprintf(msgBatchCalculatedControlEquality, hashField, batch.Control.EntryHashField()) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "EntryHash", Msg: msg} + } + return nil +} + +// calculateEntryHash This field is prepared by hashing the 8-digit Routing Number in each entry. +// The Entry Hash provides a check against inadvertent alteration of data +func (batch *IATBatch) calculateEntryHash() string { + hash := 0 + for _, entry := range batch.Entries { + + entryRDFI, _ := strconv.Atoi(entry.RDFIIdentification) + + hash = hash + entryRDFI + } + return batch.numericField(hash, 10) +} + +// isTraceNumberODFI checks if the first 8 positions of the entry detail trace number +// match the batch header ODFI +func (batch *IATBatch) isTraceNumberODFI() error { + for _, entry := range batch.Entries { + if batch.Header.ODFIIdentificationField() != entry.TraceNumberField()[:8] { + msg := fmt.Sprintf(msgBatchTraceNumberNotODFI, batch.Header.ODFIIdentificationField(), entry.TraceNumberField()[:8]) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "ODFIIdentificationField", Msg: msg} + } + } + + return nil +} + +// isAddendaSequence check multiple errors on addenda records in the batch entries +func (batch *IATBatch) isAddendaSequence() error { + for _, entry := range batch.Entries { + // addenda without indicator flag of 1 + if entry.AddendaRecordIndicator != 1 { + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "AddendaRecordIndicator", Msg: msgIATBatchAddendaIndicator} + } + // Verify Addenda* entry detail sequence numbers are valid + entryTN := entry.TraceNumberField()[8:] + + if entry.Addenda10.EntryDetailSequenceNumberField() != entryTN { + msg := fmt.Sprintf(msgBatchAddendaTraceNumber, entry.Addenda10.EntryDetailSequenceNumberField(), entry.TraceNumberField()[8:]) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TraceNumber", Msg: msg} + } + if entry.Addenda11.EntryDetailSequenceNumberField() != entryTN { + msg := fmt.Sprintf(msgBatchAddendaTraceNumber, entry.Addenda11.EntryDetailSequenceNumberField(), entry.TraceNumberField()[8:]) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TraceNumber", Msg: msg} + } + if entry.Addenda12.EntryDetailSequenceNumberField() != entryTN { + msg := fmt.Sprintf(msgBatchAddendaTraceNumber, entry.Addenda12.EntryDetailSequenceNumberField(), entry.TraceNumberField()[8:]) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TraceNumber", Msg: msg} + } + if entry.Addenda13.EntryDetailSequenceNumberField() != entryTN { + msg := fmt.Sprintf(msgBatchAddendaTraceNumber, entry.Addenda13.EntryDetailSequenceNumberField(), entry.TraceNumberField()[8:]) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TraceNumber", Msg: msg} + } + if entry.Addenda14.EntryDetailSequenceNumberField() != entryTN { + msg := fmt.Sprintf(msgBatchAddendaTraceNumber, entry.Addenda14.EntryDetailSequenceNumberField(), entry.TraceNumberField()[8:]) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TraceNumber", Msg: msg} + } + if entry.Addenda15.EntryDetailSequenceNumberField() != entryTN { + msg := fmt.Sprintf(msgBatchAddendaTraceNumber, entry.Addenda15.EntryDetailSequenceNumberField(), entry.TraceNumberField()[8:]) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TraceNumber", Msg: msg} + } + if entry.Addenda16.EntryDetailSequenceNumberField() != entryTN { + msg := fmt.Sprintf(msgBatchAddendaTraceNumber, entry.Addenda16.EntryDetailSequenceNumberField(), entry.TraceNumberField()[8:]) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TraceNumber", Msg: msg} + } + + // check if sequence is ascending for addendumer - Addenda17 and Addenda18 + lastAddenda17Seq := -1 + lastAddenda18Seq := -1 + + for _, IATAddenda := range entry.Addendum { + if a, ok := IATAddenda.(*Addenda17); ok { + + if a.SequenceNumber < lastAddenda17Seq { + msg := fmt.Sprintf(msgBatchAscending, a.SequenceNumber, lastAddenda17Seq) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "SequenceNumber", Msg: msg} + } + lastAddenda17Seq = a.SequenceNumber + // check that we are in the correct Entry Detail + if !(a.EntryDetailSequenceNumberField() == entry.TraceNumberField()[8:]) { + msg := fmt.Sprintf(msgBatchAddendaTraceNumber, a.EntryDetailSequenceNumberField(), entry.TraceNumberField()[8:]) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TraceNumber", Msg: msg} + } + } + if a, ok := IATAddenda.(*Addenda18); ok { + + if a.SequenceNumber < lastAddenda18Seq { + msg := fmt.Sprintf(msgBatchAscending, a.SequenceNumber, lastAddenda18Seq) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "SequenceNumber", Msg: msg} + } + lastAddenda18Seq = a.SequenceNumber + // check that we are in the correct Entry Detail + if !(a.EntryDetailSequenceNumberField() == entry.TraceNumberField()[8:]) { + msg := fmt.Sprintf(msgBatchAddendaTraceNumber, a.EntryDetailSequenceNumberField(), entry.TraceNumberField()[8:]) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TraceNumber", Msg: msg} + } + } + } + } + return nil +} + +// isCategory verifies that a Forward and Return Category are not in the same batch +func (batch *IATBatch) isCategory() error { + category := batch.GetEntries()[0].Category + if len(batch.Entries) > 1 { + for i := 1; i < len(batch.Entries); i++ { + // ToDo: Need to research requirements for Notice of change fo IAT + /*if batch.Entries[i].Category == CategoryNOC { + continue + }*/ + if batch.Entries[i].Category != category { + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Category", Msg: msgBatchForwardReturn} + } + } + } + return nil +} + +func (batch *IATBatch) addendaFieldInclusion(entry *IATEntryDetail) error { + if entry.Addenda10 == nil { + msg := fmt.Sprint(msgIATBatchAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda10", Msg: msg} + } + if entry.Addenda11 == nil { + msg := fmt.Sprint(msgIATBatchAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda11", Msg: msg} + } + if entry.Addenda12 == nil { + msg := fmt.Sprint(msgIATBatchAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda12", Msg: msg} + } + if entry.Addenda13 == nil { + msg := fmt.Sprint(msgIATBatchAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda13", Msg: msg} + } + if entry.Addenda14 == nil { + msg := fmt.Sprint(msgIATBatchAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda14", Msg: msg} + } + if entry.Addenda15 == nil { + msg := fmt.Sprint(msgIATBatchAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda15", Msg: msg} + } + if entry.Addenda16 == nil { + msg := fmt.Sprint(msgIATBatchAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda16", Msg: msg} + } + return nil +} + +// Create takes Batch Header and Entries and builds a valid batch +func (batch *IATBatch) 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() +} + +// Validate checks valid NACHA batch rules. Assumes properly parsed records. +func (batch *IATBatch) 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. + + for _, entry := range batch.Entries { + + // Addendum cannot be greater than 7, There can be a maximum of 2 Addenda17 and a maximum of 5 Addenda18 + if len(entry.Addendum) > 7 { + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addendum", Msg: msgBatchIATAddendum} + } + addenda17Count := 0 + addenda18Count := 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" { + addenda17Count = addenda17Count + 1 + } + if IATAddenda.TypeCode() == "18" { + addenda18Count = addenda18Count + 1 + } + } + 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. + // ... + return nil +} diff --git a/iatBatchHeader.go b/iatBatchHeader.go new file mode 100644 index 000000000..22d86253f --- /dev/null +++ b/iatBatchHeader.go @@ -0,0 +1,423 @@ +// 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" + "strconv" + "strings" + "time" +) + +// msgServiceClass + +// IATBatchHeader identifies the originating entity and the type of transactions +// contained in the batch for SEC Code IAT. 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 operator. +// +// An IAT entry is a credit or debit ACH entry that is part of a payment transaction +// involving a financial agency’s office (i.e., depository financial institution or +// business issuing money orders) that is not located in the territorial jurisdiction +// of the United States. IAT entries can be made to or from a corporate or consumer +// account and must be accompanied by seven (7) mandatory addenda records identifying +// the name and physical address of the Originator, name and physical address of the +// Receiver, Receiver’s account number, Receiver’s bank identity and reason for the payment. +type IATBatchHeader struct { + // ID is a client defined string used as a reference to this record. + ID string `json:"id"` + + // RecordType defines the type of record in the block. 5 + recordType string + + // ServiceClassCode ACH Mixed Debits and Credits ‘200’ + // ACH Credits Only ‘220’ + // ACH Debits Only ‘225' + ServiceClassCode int `json:"serviceClassCode"` + + // IATIndicator - Leave Blank - It is only used for corrected IAT entries + IATIndicator string `json:"IATIndicator,omitempty"` + + // ForeignExchangeIndicator is a code indicating currency conversion + // + // FV Fixed-to-Variable – Entry is originated in a fixed-value amount + // and is to be received in a variable amount resulting from the + // execution of the foreign exchange conversion. + // + // VF Variable-to-Fixed – Entry is originated in a variable-value + // amount based on a specific foreign exchange rate for conversion to a + // fixed-value amount in which the entry is to be received. + // + // FF Fixed-to-Fixed – Entry is originated in a fixed-value amount and + // is to be received in the same fixed-value amount in the same + // currency denomination. There is no foreign exchange conversion for + // entries transmitted using this code. For entries originated in a fixed value + // amount, the foreign Exchange Reference Field will be space + // filled. + ForeignExchangeIndicator string `json:"foreignExchangeIndicator"` + + // ForeignExchangeReferenceIndicator is a code used to indicate the content of the + // Foreign Exchange Reference Field and is filled by the gateway operator. + // Valid entries are: + // 1 - Foreign Exchange Rate; + // 2 - Foreign Exchange Reference Number; or + // 3 - Space Filled + ForeignExchangeReferenceIndicator int `json:"foreignExchangeReferenceIndicator"` + + // ForeignExchangeReference Contains either the foreign exchange rate used to execute + // the foreign exchange conversion of a cross-border entry or another reference to the foreign + // exchange transaction. + // ToDo: potentially write a validator + ForeignExchangeReference string `json:"foreignExchangeReference"` + + // ISODestinationCountryCode is the two-character code, as approved by the International + // Organization for Standardization (ISO), to identify the country in which the entry is + // to be received. Values can be found on the International Organization for Standardization + // website: www.iso.org. For entries destined to account holder in the U.S., this would be US. + ISODestinationCountryCode string `json:"ISODestinationCountryCode"` + + // OriginatorIdentification identifies the following: + // For U.S. entities: the number assigned will be your tax ID + // For non-U.S. entities: the number assigned will be your DDA number, + // or the last 9 characters of your account number if it exceeds 9 characters + OriginatorIdentification string `json:"originatorIdentification"` + + // StandardEntryClassCode for consumer and non consumer international payments is IAT + // Identifies the payment type (product) found within an ACH batch-using a 3-character code. + // The SEC Code pertains to all items within batch. + // Determines format of the detail records. + // Determines addenda records (required or optional PLUS one or up to 9,999 records). + // Determines rules to follow (return time frames). + // Some SEC codes require specific data in predetermined fields within the ACH record + StandardEntryClassCode string `json:"standardEntryClassCode,omitempty"` + + // CompanyEntryDescription A description of the entries contained in the batch + // + //The Originator establishes the value of this field to provide a + // description of the purpose of the entry to be displayed back to + // the receive For example, "GAS BILL," "REG. SALARY," "INS. PREM," + // "SOC. SEC.," "DTC," "TRADE PAY," "PURCHASE," etc. + // + // This field must contain the word "REVERSAL" (left justified) when the + // batch contains reversing entries. + // + // This field must contain the word "RECLAIM" (left justified) when the + // batch contains reclamation entries. + // + // This field must contain the word "NONSETTLED" (left justified) when the + // batch contains entries which could not settle. + CompanyEntryDescription string `json:"companyEntryDescription,omitempty"` + + // ISOOriginatingCurrencyCode is the three-character code, as approved by the International + // Organization for Standardization (ISO), to identify the currency denomination in which the + // entry was first originated. If the source of funds is within the territorial jurisdiction + // of the U.S., enter 'USD', otherwise refer to International Organization for Standardization + // website for value: www.iso.org -- (Account Currency) + ISOOriginatingCurrencyCode string `json:"ISOOriginatingCurrencyCode"` + + // ISODestinationCurrencyCode is the three-character code, as approved by the International + // Organization for Standardization (ISO), to identify the currency denomination in which the + // entry will ultimately be settled. If the final destination of funds is within the territorial + // jurisdiction of the U.S., enter “USD”, otherwise refer to International Organization for + // Standardization website for value: www.iso.org -- (Payment Currency) + ISODestinationCurrencyCode string `json:"ISODestinationCurrencyCode"` + + // EffectiveEntryDate the date on which the entries are to settle format YYMMDD + EffectiveEntryDate time.Time `json:"effectiveEntryDate,omitempty"` + + // SettlementDate Leave blank, this field is inserted by the ACH operator + settlementDate string + // OriginatorStatusCode refers to the ODFI initiating the Entry. + // 0 ADV File prepared by an ACH Operator. + // 1 This code identifies the Originator as a depository financial institution. + // 2 This code identifies the Originator as a Federal Government entity or agency. + OriginatorStatusCode int `json:"originatorStatusCode,omitempty"` + + // ODFIIdentification First 8 digits of the originating DFI transit routing number + // For Inbound IAT Entries, this field contains the routing number of the U.S. Gateway + // Operator. For Outbound IAT Entries, this field contains the standard routing number, + // as assigned by Accuity, that identifies the U.S. ODFI initiating the Entry. + // Format - TTTTAAAA + ODFIIdentification string `json:"ODFIIdentification"` + + // BatchNumber is assigned in ascending sequence to each batch by the ODFI + // or its Sending Point in a given file of entries. Since the batch number + // in the Batch Header Record and the Batch Control Record is the same, + // the ascending sequence number should be assigned by batch and not by + // record. + BatchNumber int `json:"batchNumber,omitempty"` + + // validator is composed for data validation + validator + + // converters is composed for ACH to golang Converters + converters +} + +// NewIATBatchHeader returns a new BatchHeader with default values for non exported fields +func NewIATBatchHeader() *IATBatchHeader { + iatBh := &IATBatchHeader{ + recordType: "5", + OriginatorStatusCode: 0, //Prepared by an Originator + BatchNumber: 1, + } + return iatBh +} + +// Parse takes the input record string and parses the BatchHeader values +func (iatBh *IATBatchHeader) Parse(record string) { + // 1-1 Always "5" + iatBh.recordType = "5" + // 2-4 If the entries are credits, always "220". If the entries are debits, always "225" + iatBh.ServiceClassCode = iatBh.parseNumField(record[1:4]) + // 05-20 Leave Blank - It is only used for corrected IAT entries + iatBh.IATIndicator = " " + // 21-22 A code indicating currency conversion + // “FV” Fixed-to-Variable + // “VF” Variable-to-Fixed + // “FF” Fixed-to-Fixed + iatBh.ForeignExchangeIndicator = iatBh.parseStringField(record[20:22]) + // 23-23 Foreign Exchange Reference Indicator – Refers to “Foreign Exchange Reference” + // field and is filled by the gateway operator. Valid entries are: + // 1 - Foreign Exchange Rate; + // 2 - Foreign Exchange Reference Number; or + // 3 - Space Filled + iatBh.ForeignExchangeReferenceIndicator = iatBh.parseNumField(record[22:23]) + // 24-38 Contains either the foreign exchange rate used to execute the + // foreign exchange conversion of a cross-border entry or another + // reference to the foreign exchange transaction. + iatBh.ForeignExchangeReference = iatBh.parseStringField(record[23:38]) + // 39-40 Receiver ISO Country Code - For entries + // destined to account holder in the U.S., this would be ‘US’. + iatBh.ISODestinationCountryCode = iatBh.parseStringField(record[38:40]) + // 41-50 For U.S. entities: the number assigned will be your tax ID + // For non-U.S. entities: the number assigned will be your DDA number, + // or the last 9 characters of your account number if it exceeds 9 characters + iatBh.OriginatorIdentification = iatBh.parseStringField(record[40:50]) + // 51-53 IAT for both consumer and non consumer international payments + iatBh.StandardEntryClassCode = record[50:53] + // 54-63 Your description of the transaction. This text will appear on the receivers’ bank statement. + // For example: "Payroll " + iatBh.CompanyEntryDescription = strings.TrimSpace(record[53:63]) + // 64-66 Originator ISO Currency Code + iatBh.ISOOriginatingCurrencyCode = iatBh.parseStringField(record[63:66]) + // 67-69 Receiver ISO Currency Code + iatBh.ISODestinationCurrencyCode = iatBh.parseStringField(record[66:69]) + // 70-75 Date transactions are to be posted to the receivers’ account. + // You almost always want the transaction to post as soon as possible, so put tomorrow's date in YYMMDD format + iatBh.EffectiveEntryDate = iatBh.parseSimpleDate(record[69:75]) + // 76-79 Always blank (just fill with spaces) + iatBh.settlementDate = " " + // 79-79 Always 1 + iatBh.OriginatorStatusCode = iatBh.parseNumField(record[78:79]) + // 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 + iatBh.ODFIIdentification = iatBh.parseStringField(record[79:87]) + // 88-94 Sequential number of this Batch Header Record + // For example, put "1" if this is the first Batch Header Record in the file + iatBh.BatchNumber = iatBh.parseNumField(record[87:94]) +} + +// String writes the BatchHeader struct to a 94 character string. +func (iatBh *IATBatchHeader) String() string { + return fmt.Sprintf("%v%v%v%v%v%v%v%v%v%v%v%v%v%v%v%v%v", + iatBh.recordType, + iatBh.ServiceClassCode, + iatBh.IATIndicatorField(), + iatBh.ForeignExchangeIndicatorField(), + iatBh.ForeignExchangeReferenceIndicatorField(), + iatBh.ForeignExchangeReferenceField(), + iatBh.ISODestinationCountryCodeField(), + iatBh.OriginatorIdentificationField(), + iatBh.StandardEntryClassCode, + iatBh.CompanyEntryDescriptionField(), + iatBh.ISOOriginatingCurrencyCodeField(), + iatBh.ISODestinationCurrencyCodeField(), + iatBh.EffectiveEntryDateField(), + iatBh.settlementDateField(), + iatBh.OriginatorStatusCode, + iatBh.ODFIIdentificationField(), + iatBh.BatchNumberField(), + ) +} + +// Validate performs NACHA format rule checks on the record and returns an error if not Validated +// The first error encountered is returned and stops that parsing. +func (iatBh *IATBatchHeader) Validate() error { + if err := iatBh.fieldInclusion(); err != nil { + return err + } + if iatBh.recordType != "5" { + msg := fmt.Sprintf(msgRecordType, 5) + return &FieldError{FieldName: "recordType", Value: iatBh.recordType, Msg: msg} + } + if err := iatBh.isServiceClass(iatBh.ServiceClassCode); err != nil { + return &FieldError{FieldName: "ServiceClassCode", + Value: strconv.Itoa(iatBh.ServiceClassCode), Msg: err.Error()} + } + if err := iatBh.isForeignExchangeIndicator(iatBh.ForeignExchangeIndicator); err != nil { + return &FieldError{FieldName: "ForeignExchangeIndicator", + Value: iatBh.ForeignExchangeIndicator, Msg: err.Error()} + } + if err := iatBh.isForeignExchangeReferenceIndicator(iatBh.ForeignExchangeReferenceIndicator); err != nil { + return &FieldError{FieldName: "ForeignExchangeReferenceIndicator", + Value: strconv.Itoa(iatBh.ForeignExchangeReferenceIndicator), Msg: err.Error()} + } + if err := iatBh.isAlphanumeric(iatBh.ISODestinationCountryCode); err != nil { + return &FieldError{FieldName: "ISODestinationCountryCode", + Value: iatBh.ISODestinationCountryCode, Msg: err.Error()} + } + if err := iatBh.isAlphanumeric(iatBh.OriginatorIdentification); err != nil { + return &FieldError{FieldName: "OriginatorIdentification", + Value: iatBh.OriginatorIdentification, Msg: err.Error()} + } + if err := iatBh.isSECCode(iatBh.StandardEntryClassCode); err != nil { + return &FieldError{FieldName: "StandardEntryClassCode", + Value: iatBh.StandardEntryClassCode, Msg: err.Error()} + } + if err := iatBh.isAlphanumeric(iatBh.CompanyEntryDescription); err != nil { + return &FieldError{FieldName: "CompanyEntryDescription", + Value: iatBh.CompanyEntryDescription, Msg: err.Error()} + } + if err := iatBh.isAlphanumeric(iatBh.ISOOriginatingCurrencyCode); err != nil { + return &FieldError{FieldName: "ISOOriginatingCurrencyCode", + Value: iatBh.ISOOriginatingCurrencyCode, Msg: err.Error()} + } + + if err := iatBh.isAlphanumeric(iatBh.ISODestinationCurrencyCode); err != nil { + return &FieldError{FieldName: "ISODestinationCurrencyCode", + Value: iatBh.ISODestinationCurrencyCode, Msg: err.Error()} + } + if err := iatBh.isOriginatorStatusCode(iatBh.OriginatorStatusCode); err != nil { + return &FieldError{FieldName: "OriginatorStatusCode", + Value: strconv.Itoa(iatBh.OriginatorStatusCode), Msg: err.Error()} + } + return nil +} + +// fieldInclusion validate mandatory fields are not default values. If fields are +// invalid the ACH transfer will be returned. +func (iatBh *IATBatchHeader) fieldInclusion() error { + if iatBh.recordType == "" { + return &FieldError{FieldName: "recordType", Value: iatBh.recordType, Msg: msgFieldInclusion} + } + if iatBh.ServiceClassCode == 0 { + return &FieldError{FieldName: "ServiceClassCode", + Value: strconv.Itoa(iatBh.ServiceClassCode), Msg: msgFieldInclusion} + } + if iatBh.ForeignExchangeIndicator == "" { + return &FieldError{FieldName: "ForeignExchangeIndicator", + Value: iatBh.ForeignExchangeIndicator, Msg: msgFieldInclusion} + } + if iatBh.ForeignExchangeReferenceIndicator == 0 { + return &FieldError{FieldName: "ForeignExchangeReferenceIndicator", + Value: strconv.Itoa(iatBh.ForeignExchangeReferenceIndicator), Msg: msgFieldRequired} + } + // ToDo: It can be space filled based on ForeignExchangeReferenceIndicator just use a validator to handle - + // ToDo: Calling Field ok for validation? + /* if iatBh.ForeignExchangeReference == "" { + return &FieldError{FieldName: "ForeignExchangeReference", + Value: iatBh.ForeignExchangeReference, Msg: msgFieldRequired} + }*/ + if iatBh.ISODestinationCountryCode == "" { + return &FieldError{FieldName: "ISODestinationCountryCode", + Value: iatBh.ISODestinationCountryCode, Msg: msgFieldInclusion} + } + if iatBh.OriginatorIdentification == "" { + return &FieldError{FieldName: "OriginatorIdentification", + Value: iatBh.OriginatorIdentification, Msg: msgFieldInclusion} + } + if iatBh.StandardEntryClassCode == "" { + return &FieldError{FieldName: "StandardEntryClassCode", + Value: iatBh.StandardEntryClassCode, Msg: msgFieldInclusion} + } + if iatBh.CompanyEntryDescription == "" { + return &FieldError{FieldName: "CompanyEntryDescription", + Value: iatBh.CompanyEntryDescription, Msg: msgFieldInclusion} + } + if iatBh.ISOOriginatingCurrencyCode == "" { + return &FieldError{FieldName: "ISOOriginatingCurrencyCode", + Value: iatBh.ISOOriginatingCurrencyCode, Msg: msgFieldInclusion} + } + if iatBh.ISODestinationCurrencyCode == "" { + return &FieldError{FieldName: "ISODestinationCurrencyCode", + Value: iatBh.ISODestinationCurrencyCode, Msg: msgFieldInclusion} + } + if iatBh.ODFIIdentification == "" { + return &FieldError{FieldName: "ODFIIdentification", + Value: iatBh.ODFIIdentificationField(), Msg: msgFieldInclusion} + } + return nil +} + +// IATIndicatorField gets the IATIndicator left padded +func (iatBh *IATBatchHeader) IATIndicatorField() string { + // should this be left padded + return iatBh.alphaField(iatBh.IATIndicator, 16) +} + +// ForeignExchangeIndicatorField gets the ForeignExchangeIndicator +func (iatBh *IATBatchHeader) ForeignExchangeIndicatorField() string { + return iatBh.alphaField(iatBh.ForeignExchangeIndicator, 2) +} + +// ForeignExchangeReferenceIndicatorField gets the ForeignExchangeReferenceIndicator +func (iatBh *IATBatchHeader) ForeignExchangeReferenceIndicatorField() string { + return iatBh.numericField(iatBh.ForeignExchangeReferenceIndicator, 1) +} + +// ForeignExchangeReferenceField gets the ForeignExchangeReference left padded +func (iatBh *IATBatchHeader) ForeignExchangeReferenceField() string { + if iatBh.ForeignExchangeReferenceIndicator == 3 { + //blank space + return " " + } + return iatBh.alphaField(iatBh.ForeignExchangeReference, 15) +} + +// ISODestinationCountryCodeField gets the ISODestinationCountryCode +func (iatBh *IATBatchHeader) ISODestinationCountryCodeField() string { + return iatBh.alphaField(iatBh.ISODestinationCountryCode, 2) +} + +// OriginatorIdentificationField gets the OriginatorIdentification left padded +func (iatBh *IATBatchHeader) OriginatorIdentificationField() string { + return iatBh.alphaField(iatBh.OriginatorIdentification, 10) +} + +// CompanyEntryDescriptionField gets the CompanyEntryDescription left padded +func (iatBh *IATBatchHeader) CompanyEntryDescriptionField() string { + return iatBh.alphaField(iatBh.CompanyEntryDescription, 10) +} + +// ISOOriginatingCurrencyCodeField gets the ISOOriginatingCurrencyCode +func (iatBh *IATBatchHeader) ISOOriginatingCurrencyCodeField() string { + return iatBh.alphaField(iatBh.ISOOriginatingCurrencyCode, 3) +} + +// ISODestinationCurrencyCodeField gets the ISODestinationCurrencyCode +func (iatBh *IATBatchHeader) ISODestinationCurrencyCodeField() string { + return iatBh.alphaField(iatBh.ISODestinationCurrencyCode, 3) +} + +// EffectiveEntryDateField get the EffectiveEntryDate in YYMMDD format +func (iatBh *IATBatchHeader) EffectiveEntryDateField() string { + return iatBh.formatSimpleDate(iatBh.EffectiveEntryDate) +} + +// ODFIIdentificationField get the odfi number zero padded +func (iatBh *IATBatchHeader) ODFIIdentificationField() string { + return iatBh.stringField(iatBh.ODFIIdentification, 8) +} + +// BatchNumberField get the batch number zero padded +func (iatBh *IATBatchHeader) BatchNumberField() string { + return iatBh.numericField(iatBh.BatchNumber, 7) +} + +// settlementDateField gets the settlementDate +func (iatBh *IATBatchHeader) settlementDateField() string { + return iatBh.alphaField(iatBh.settlementDate, 3) +} diff --git a/iatBatchHeader_test.go b/iatBatchHeader_test.go new file mode 100644 index 000000000..db223ec95 --- /dev/null +++ b/iatBatchHeader_test.go @@ -0,0 +1,811 @@ +// 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 ( + "strings" + "testing" +) + +// mockIATBatchHeaderFF creates a IAT BatchHeader that is Fixed-Fixed +func mockIATBatchHeaderFF() *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 = "23138010" + return bh +} + +// testMockIATBatchHeaderFF creates a IAT BatchHeader Fixed-Fixed +func testMockIATBatchHeaderFF(t testing.TB) { + bh := mockIATBatchHeaderFF() + if err := bh.Validate(); err != nil { + t.Error("mockIATBatchHeaderFF does not validate and will break other tests: ", err) + } + if bh.ServiceClassCode != 220 { + t.Error("ServiceClassCode dependent default value has changed") + } + if bh.ForeignExchangeIndicator != "FF" { + t.Error("ForeignExchangeIndicator does not validate and will break other tests") + } + if bh.ForeignExchangeReferenceIndicator != 3 { + t.Error("ForeignExchangeReferenceIndicator does not validate and will break other tests") + } + if bh.ISODestinationCountryCode != "US" { + t.Error("DestinationCountryCode dependent default value has changed") + } + if bh.OriginatorIdentification != "123456789" { + t.Error("OriginatorIdentification dependent default value has changed") + } + if bh.StandardEntryClassCode != "IAT" { + t.Error("StandardEntryClassCode dependent default value has changed") + } + if bh.CompanyEntryDescription != "TRADEPAYMT" { + t.Error("CompanyEntryDescription dependent default value has changed") + } + if bh.ISOOriginatingCurrencyCode != "CAD" { + t.Error("ISOOriginatingCurrencyCode dependent default value has changed") + } + if bh.ISODestinationCurrencyCode != "USD" { + t.Error("ISODestinationCurrencyCode dependent default value has changed") + } + if bh.ODFIIdentification != "23138010" { + t.Error("ODFIIdentification dependent default value has changed") + } +} + +// TestMockIATBatchHeaderFF tests creating a IAT BatchHeader Fixed-Fixed +func TestMockIATBatchHeaderFF(t *testing.T) { + testMockIATBatchHeaderFF(t) +} + +// BenchmarkMockIATBatchHeaderFF benchmarks creating a IAT BatchHeader Fixed-Fixed +func BenchmarkMockIATBatchHeaderFF(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testMockIATBatchHeaderFF(b) + } +} + +// testParseIATBatchHeader parses a known IAT BatchHeader record string +func testParseIATBatchHeader(t testing.TB) { + var line = "5220 FF3 US123456789 IATTRADEPAYMTCADUSD180621 1231380100000001" + r := NewReader(strings.NewReader(line)) + r.line = line + if err := r.parseIATBatchHeader(); err != nil { + t.Errorf("%T: %s", err, err) + } + record := r.IATCurrentBatch.GetHeader() + + if record.recordType != "5" { + t.Errorf("RecordType Expected '5' got: %v", record.recordType) + } + if record.ServiceClassCode != 220 { + t.Errorf("ServiceClassCode Expected '225' got: %v", record.ServiceClassCode) + } + if record.IATIndicator != " " { + t.Errorf("IATIndicator Expected ' ' got: %v", record.IATIndicator) + } + if record.ForeignExchangeIndicator != "FF" { + t.Errorf("ForeignExchangeIndicator Expected ' ' got: %v", + record.ForeignExchangeIndicator) + } + if record.ForeignExchangeReferenceIndicator != 3 { + t.Errorf("ForeignExchangeReferenceIndicator Expected ' ' got: %v", + record.ForeignExchangeReferenceIndicator) + } + if record.ForeignExchangeReferenceField() != " " { + t.Errorf("ForeignExchangeReference Expected ' ' got: %v", + record.ForeignExchangeReference) + } + if record.StandardEntryClassCode != "IAT" { + t.Errorf("StandardEntryClassCode Expected 'PPD' got: %v", record.StandardEntryClassCode) + } + if record.CompanyEntryDescription != "TRADEPAYMT" { + t.Errorf("CompanyEntryDescription Expected 'TRADEPAYMT' got: %v", record.CompanyEntryDescriptionField()) + } + + if record.EffectiveEntryDateField() != "180621" { + t.Errorf("EffectiveEntryDate Expected '080730' got: %v", record.EffectiveEntryDateField()) + } + if record.settlementDate != " " { + t.Errorf("SettlementDate Expected ' ' got: %v", record.settlementDate) + } + if record.OriginatorStatusCode != 1 { + t.Errorf("OriginatorStatusCode Expected 1 got: %v", record.OriginatorStatusCode) + } + if record.ODFIIdentification != "23138010" { + t.Errorf("OdfiIdentification Expected '07640125' got: %v", record.ODFIIdentificationField()) + } + if record.BatchNumberField() != "0000001" { + t.Errorf("BatchNumber Expected '0000001' got: %v", record.BatchNumberField()) + } +} + +// TestParseIATBatchHeader tests parsing a known IAT BatchHeader record string +func TestParseIATBatchHeader(t *testing.T) { + testParseIATBatchHeader(t) +} + +// BenchmarkParseBatchHeader benchmarks parsing a known IAT BatchHeader record string +func BenchmarkParseIATBatchHeader(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testParseIATBatchHeader(b) + } +} + +// testIATBHString validates that a known parsed IAT Batch Header +// can be return to a string of the same value +func testIATBHString(t testing.TB) { + var line = "5220 FF3 US123456789 IATTRADEPAYMTCADUSD180621 1231380100000001" + r := NewReader(strings.NewReader(line)) + r.line = line + if err := r.parseIATBatchHeader(); err != nil { + t.Errorf("%T: %s", err, err) + } + record := r.IATCurrentBatch.GetHeader() + + if record.String() != line { + t.Errorf("Strings do not match") + } +} + +// TestIATBHString tests validating that a known parsed IAT BatchHeader +// can be return to a string of the same value +func TestIATBHString(t *testing.T) { + testIATBHString(t) +} + +// BenchmarkIATBHString benchmarks validating that a known parsed IAT BatchHeader +// can be return to a string of the same value +func BenchmarkIATBHString(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBHString(b) + } +} + +// testIATBHFVString validates that a known parsed IAT Batch Header +// can be return to a string of the same value +func testIATBHFVString(t testing.TB) { + var line = "5220 FV2123456789012345US123456789 IATTRADEPAYMTCADUSD180621 1231380100000001" + r := NewReader(strings.NewReader(line)) + r.line = line + if err := r.parseIATBatchHeader(); err != nil { + t.Errorf("%T: %s", err, err) + } + record := r.IATCurrentBatch.GetHeader() + + if record.String() != line { + t.Errorf("Strings do not match") + } +} + +// TestIATBHFVString tests validating that a known parsed IAT BatchHeader +// can be return to a string of the same value +func TestIATBHFVString(t *testing.T) { + testIATBHFVString(t) +} + +// BenchmarkIATBHFVString benchmarks validating that a known parsed IAT BatchHeader +// can be return to a string of the same value +func BenchmarkIATBHFVString(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBHFVString(b) + } +} + +// testValidateIATBHRecordType validates error if IATBatchHeader recordType is invalid +func testValidateIATBHRecordType(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.recordType = "2" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestValidateIATBHRecordType tests validating error if IATBatchHeader recordType is invalid +func TestValidateIATBHRecordType(t *testing.T) { + testValidateIATBHRecordType(t) +} + +// BenchmarkValidateIATBHRecordType benchmarks validating error if IATBatchHeader recordType is invalid +func BenchmarkValidateIATBHRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testValidateIATBHRecordType(b) + } +} + +// testValidateIATBHServiceClassCode validates error if IATBatchHeader +// ServiceClassCode is invalid +func testValidateIATBHServiceClassCode(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.ServiceClassCode = 999 + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ServiceClassCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestValidateIATBHServiceClassCode tests validating error if IATBatchHeader +// ServiceClassCode is invalid +func TestValidateIATBHServiceClassCode(t *testing.T) { + testValidateIATBHServiceClassCode(t) +} + +// BenchmarkValidateIATBHServiceClassCode benchmarks validating error if IATBatchHeader +// ServiceClassCode is invalid +func BenchmarkValidateIATBHServiceClassCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testValidateIATBHServiceClassCode(b) + } +} + +// testValidateIATBHForeignExchangeIndicator validates error if IATBatchHeader +// ForeignExchangeIndicator is invalid +func testValidateIATBHForeignExchangeIndicator(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.ForeignExchangeIndicator = "XY" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ForeignExchangeIndicator" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestValidateIATBHForeignExchangeIndicator tests validating error if IATBatchHeader +// ForeignExchangeIndicator is invalid +func TestValidateIATBHForeignExchangeIndicator(t *testing.T) { + testValidateIATBHForeignExchangeIndicator(t) +} + +// BenchmarkValidateIATBHForeignExchangeIndicator benchmarks validating error if IATBatchHeader +// ForeignExchangeIndicator is invalid +func BenchmarkValidateIATBHForeignExchangeIndicator(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testValidateIATBHForeignExchangeIndicator(b) + } +} + +// testValidateIATBHForeignExchangeReferenceIndicator validates error if IATBatchHeader +// ForeignExchangeReferenceIndicator is invalid +func testValidateIATBHForeignExchangeReferenceIndicator(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.ForeignExchangeReferenceIndicator = 5 + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ForeignExchangeReferenceIndicator" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestValidateIATBHForeignExchangeReferenceIndicator tests validating error if IATBatchHeader +// ForeignExchangeReferenceIndicator is invalid +func TestValidateIATBHForeignExchangeReferenceIndicator(t *testing.T) { + testValidateIATBHForeignExchangeReferenceIndicator(t) +} + +// BenchmarkValidateIATBHForeignExchangeReferenceIndicator benchmarks validating error if IATBatchHeader +// ForeignExchangeReferenceIndicator is invalid +func BenchmarkValidateIATBHForeignExchangeReferenceIndicator(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testValidateIATBHForeignExchangeReferenceIndicator(b) + } +} + +// testValidateIATBHISODestinationCountryCode validates error if IATBatchHeader +// ISODestinationCountryCode is invalid +func testValidateIATBHISODestinationCountryCode(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.ISODestinationCountryCode = "®" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ISODestinationCountryCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestValidateIATBHISODestinationCountryCode tests validating error if IATBatchHeader +// ISODestinationCountryCode is invalid +func TestValidateIATBHISODestinationCountryCode(t *testing.T) { + testValidateIATBHISODestinationCountryCode(t) +} + +// BenchmarkValidateIATBHISODestinationCountryCode benchmarks validating error if IATBatchHeader +// ISODestinationCountryCode is invalid +func BenchmarkValidateIATBHISODestinationCountryCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testValidateIATBHISODestinationCountryCode(b) + } +} + +// testValidateIATBHOriginatorIdentification validates error if IATBatchHeader +// OriginatorIdentification is invalid +func testValidateIATBHOriginatorIdentification(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.OriginatorIdentification = "®" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "OriginatorIdentification" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestValidateIATBHOriginatorIdentification tests validating error if IATBatchHeader +// OriginatorIdentification is invalid +func TestValidateIATBHOriginatorIdentification(t *testing.T) { + testValidateIATBHOriginatorIdentification(t) +} + +// BenchmarkValidateIATBHOriginatorIdentification benchmarks validating error if IATBatchHeader +// OriginatorIdentification is invalid +func BenchmarkValidateIATBHOriginatorIdentification(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testValidateIATBHOriginatorIdentification(b) + } +} + +// testValidateIATBHStandardEntryClassCode validates error if IATBatchHeader +// StandardEntryClassCode is invalid +func testValidateIATBHStandardEntryClassCode(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.StandardEntryClassCode = "ABC" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "StandardEntryClassCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestValidateIATBHStandardEntryClassCode tests validating error if IATBatchHeader +// StandardEntryClassCode is invalid +func TestValidateIATBHStandardEntryClassCode(t *testing.T) { + testValidateIATBHStandardEntryClassCode(t) +} + +// BenchmarkValidateIATBHStandardEntryClassCode benchmarks validating error if IATBatchHeader +// StandardEntryClassCode is invalid +func BenchmarkValidateIATBHStandardEntryClassCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testValidateIATBHStandardEntryClassCode(b) + } +} + +// testValidateIATBHCompanyEntryDescription validates error if IATBatchHeader +// CompanyEntryDescription is invalid +func testValidateIATBHCompanyEntryDescription(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.CompanyEntryDescription = "®" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "CompanyEntryDescription" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestValidateIATBHCompanyEntryDescription tests validating error if IATBatchHeader +// CompanyEntryDescription is invalid +func TestValidateIATBHCompanyEntryDescription(t *testing.T) { + testValidateIATBHCompanyEntryDescription(t) +} + +// BenchmarkValidateIATBHCompanyEntryDescription benchmarks validating error if IATBatchHeader +// CompanyEntryDescription is invalid +func BenchmarkValidateIATBHCompanyEntryDescription(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testValidateIATBHCompanyEntryDescription(b) + } +} + +// testValidateIATBHISOOriginatingCurrencyCode validates error if IATBatchHeader +// ISOOriginatingCurrencyCode is invalid +func testValidateIATBHISOOriginatingCurrencyCode(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.ISOOriginatingCurrencyCode = "®" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ISOOriginatingCurrencyCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestValidateIATBHISOOriginatingCurrencyCode tests validating error if IATBatchHeader +// ISOOriginatingCurrencyCode is invalid +func TestValidateIATBHISOOriginatingCurrencyCode(t *testing.T) { + testValidateIATBHISOOriginatingCurrencyCode(t) +} + +// BenchmarkValidateIATBHISOOriginatingCurrencyCode benchmarks validating error if IATBatchHeader +// ISOOriginatingCurrencyCode is invalid +func BenchmarkValidateIATBHISOOriginatingCurrencyCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testValidateIATBHISOOriginatingCurrencyCode(b) + } +} + +// testValidateIATBHISODestinationCurrencyCode validates error if IATBatchHeader +// ISODestinationCurrencyCode is invalid +func testValidateIATBHISODestinationCurrencyCode(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.ISODestinationCurrencyCode = "®" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ISODestinationCurrencyCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestValidateIATBHISODestinationCurrencyCode tests validating error if IATBatchHeader +// ISODestinationCurrencyCode is invalid +func TestValidateIATBHISODestinationCurrencyCode(t *testing.T) { + testValidateIATBHISODestinationCurrencyCode(t) +} + +// BenchmarkValidateIATBHISODestinationCurrencyCode benchmarks validating error if IATBatchHeader +// ISODestinationCurrencyCode is invalid +func BenchmarkValidateIATBHISODestinationCurrencyCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testValidateIATBHISODestinationCurrencyCode(b) + } +} + +// testValidateIATBHOriginatorStatusCode validates error if IATBatchHeader +// OriginatorStatusCode is invalid +func testValidateIATBHOriginatorStatusCode(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.OriginatorStatusCode = 7 + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "OriginatorStatusCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestValidateIATBHOriginatorStatusCode tests validating error if IATBatchHeader +// OriginatorStatusCode is invalid +func TestValidateIATBHOriginatorStatusCode(t *testing.T) { + testValidateIATBHOriginatorStatusCode(t) +} + +// BenchmarkValidateIATBHOriginatorStatusCode benchmarks validating error if IATBatchHeader +// OriginatorStatusCode is invalid +func BenchmarkValidateIATBHOriginatorStatusCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testValidateIATBHOriginatorStatusCode(b) + } +} + +//FieldInclusion + +// testIATBHRecordType validates IATBatchHeader recordType fieldInclusion +func testIATBHRecordType(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.recordType = "" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATBHRecordType tests validating IATBatchHeader recordType fieldInclusion +func TestIATBHRecordType(t *testing.T) { + testIATBHRecordType(t) +} + +// BenchmarkIATBHRecordType benchmarks validating IATBatchHeader recordType fieldInclusion +func BenchmarkIATBHRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBHRecordType(b) + } +} + +// testIATBHServiceClassCode validates IATBatchHeader ServiceClassCode fieldInclusion +func testIATBHServiceClassCode(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.ServiceClassCode = 0 + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ServiceClassCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATBHServiceClassCode tests validating IATBatchHeader ServiceClassCode fieldInclusion +func TestIATBHServiceClassCode(t *testing.T) { + testIATBHServiceClassCode(t) +} + +// BenchmarkIATBHServiceClassCode benchmarks validating IATBatchHeader ServiceClassCode fieldInclusion +func BenchmarkIATBHServiceClassCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBHServiceClassCode(b) + } +} + +// testIATBHForeignExchangeIndicator validates IATBatchHeader ForeignExchangeIndicator fieldInclusion +func testIATBHForeignExchangeIndicator(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.ForeignExchangeIndicator = "" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ForeignExchangeIndicator" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATBHForeignExchangeIndicator tests validating IATBatchHeader ForeignExchangeIndicator fieldInclusion +func TestIATBHForeignExchangeIndicator(t *testing.T) { + testIATBHForeignExchangeIndicator(t) +} + +// BenchmarkIATBHForeignExchangeIndicator benchmarks validating IATBatchHeader ForeignExchangeIndicator fieldInclusion +func BenchmarkIATBHForeignExchangeIndicator(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBHForeignExchangeIndicator(b) + } +} + +// testIATBHForeignExchangeReferenceIndicator validates IATBatchHeader ForeignExchangeReferenceIndicator fieldInclusion +func testIATBHForeignExchangeReferenceIndicator(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.ForeignExchangeReferenceIndicator = 0 + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ForeignExchangeReferenceIndicator" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATBHForeignExchangeReferenceIndicator tests validating IATBatchHeader ForeignExchangeReferenceIndicator fieldInclusion +func TestIATBHForeignExchangeReferenceIndicator(t *testing.T) { + testIATBHForeignExchangeReferenceIndicator(t) +} + +// BenchmarkIATBHForeignExchangeReferenceIndicator benchmarks validating IATBatchHeader ForeignExchangeReferenceIndicator fieldInclusion +func BenchmarkIATBHForeignExchangeReferenceIndicator(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBHForeignExchangeReferenceIndicator(b) + } +} + +// testIATBHISODestinationCountryCode validates IATBatchHeader ISODestinationCountryCode fieldInclusion +func testIATBHISODestinationCountryCode(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.ISODestinationCountryCode = "" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ISODestinationCountryCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATBHISODestinationCountryCode tests validating IATBatchHeader ISODestinationCountryCode fieldInclusion +func TestIATBHISODestinationCountryCode(t *testing.T) { + testIATBHISODestinationCountryCode(t) +} + +// BenchmarkIATBHISODestinationCountryCode benchmarks validating IATBatchHeader ISODestinationCountryCode fieldInclusion +func BenchmarkIATBHISODestinationCountryCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBHISODestinationCountryCode(b) + } +} + +// testIATBHOriginatorIdentification validates IATBatchHeader OriginatorIdentification fieldInclusion +func testIATBHOriginatorIdentification(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.OriginatorIdentification = "" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "OriginatorIdentification" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATBHOriginatorIdentification tests validating IATBatchHeader OriginatorIdentification fieldInclusion +func TestIATBHOriginatorIdentification(t *testing.T) { + testIATBHOriginatorIdentification(t) +} + +// BenchmarkIATBHOriginatorIdentification benchmarks validating IATBatchHeader OriginatorIdentification fieldInclusion +func BenchmarkIATBHOriginatorIdentification(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBHOriginatorIdentification(b) + } +} + +// testIATBHStandardEntryClassCode validates IATBatchHeader StandardEntryClassCode fieldInclusion +func testIATBHStandardEntryClassCode(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.StandardEntryClassCode = "" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "StandardEntryClassCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATBHStandardEntryClassCode tests validating IATBatchHeader StandardEntryClassCode fieldInclusion +func TestIATBHStandardEntryClassCode(t *testing.T) { + testIATBHStandardEntryClassCode(t) +} + +// BenchmarkIATBHStandardEntryClassCode benchmarks validating IATBatchHeader StandardEntryClassCode fieldInclusion +func BenchmarkIATBHStandardEntryClassCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBHStandardEntryClassCode(b) + } +} + +// testIATBHCompanyEntryDescription validates IATBatchHeader CompanyEntryDescription fieldInclusion +func testIATBHCompanyEntryDescription(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.CompanyEntryDescription = "" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "CompanyEntryDescription" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATBHCompanyEntryDescription tests validating IATBatchHeader CompanyEntryDescription fieldInclusion +func TestIATBHCompanyEntryDescription(t *testing.T) { + testIATBHCompanyEntryDescription(t) +} + +// BenchmarkIATBHCompanyEntryDescription benchmarks validating IATBatchHeader CompanyEntryDescription fieldInclusion +func BenchmarkIATBHCompanyEntryDescription(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBHCompanyEntryDescription(b) + } +} + +// testIATBHISOOriginatingCurrencyCode validates IATBatchHeader ISOOriginatingCurrencyCode fieldInclusion +func testIATBHISOOriginatingCurrencyCode(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.ISOOriginatingCurrencyCode = "" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ISOOriginatingCurrencyCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATBHISOOriginatingCurrencyCode tests validating IATBatchHeader ISOOriginatingCurrencyCode fieldInclusion +func TestIATBHISOOriginatingCurrencyCode(t *testing.T) { + testIATBHISOOriginatingCurrencyCode(t) +} + +// BenchmarkIATBHISOOriginatingCurrencyCode benchmarks validating IATBatchHeader ISOOriginatingCurrencyCode fieldInclusion +func BenchmarkIATBHISOOriginatingCurrencyCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBHISOOriginatingCurrencyCode(b) + } +} + +// testIATBHISODestinationCurrencyCode validates IATBatchHeader ISODestinationCurrencyCode fieldInclusion +func testIATBHISODestinationCurrencyCode(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.ISODestinationCurrencyCode = "" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ISODestinationCurrencyCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATBHISODestinationCurrencyCode tests validating IATBatchHeader ISODestinationCurrencyCode fieldInclusion +func TestIATBHISODestinationCurrencyCode(t *testing.T) { + testIATBHISODestinationCurrencyCode(t) +} + +// BenchmarkIATBHISODestinationCurrencyCode benchmarks validating IATBatchHeader ISODestinationCurrencyCode fieldInclusion +func BenchmarkIATBHISODestinationCurrencyCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBHISODestinationCurrencyCode(b) + } +} + +// testIATBHODFIIdentification validates IATBatchHeader ODFIIdentification fieldInclusion +func testIATBHODFIIdentification(t testing.TB) { + bh := mockIATBatchHeaderFF() + bh.ODFIIdentification = "" + if err := bh.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "ODFIIdentification" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATBHODFIIdentification tests validating IATBatchHeader ODFIIdentification fieldInclusion +func TestIATBHODFIIdentification(t *testing.T) { + testIATBHODFIIdentification(t) +} + +// BenchmarkIATBHODFIIdentification benchmarks validating IATBatchHeader ODFIIdentification fieldInclusion +func BenchmarkIATBHODFIIdentification(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBHODFIIdentification(b) + } +} diff --git a/iatBatch_test.go b/iatBatch_test.go new file mode 100644 index 000000000..3aaa03c69 --- /dev/null +++ b/iatBatch_test.go @@ -0,0 +1,1884 @@ +// 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 ( + "log" + "testing" +) + +// mockIATBatch +func mockIATBatch() IATBatch { + mockBatch := IATBatch{} + mockBatch.SetHeader(mockIATBatchHeaderFF()) + 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() + if err := mockBatch.build(); err != nil { + log.Fatal(err) + } + return mockBatch +} + +// mockIATBatchManyEntries +func mockIATBatchManyEntries() IATBatch { + mockBatch := IATBatch{} + mockBatch.SetHeader(mockIATBatchHeaderFF()) + + 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(mockAddenda17()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda17B()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda18()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda18B()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda18C()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda18D()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda18E()) + + mockBatch.AddEntry(mockIATEntryDetail2()) + + mockBatch.Entries[1].Addenda10 = mockAddenda10() + mockBatch.Entries[1].Addenda11 = mockAddenda11() + mockBatch.Entries[1].Addenda12 = mockAddenda12() + mockBatch.Entries[1].Addenda13 = mockAddenda13() + mockBatch.Entries[1].Addenda14 = mockAddenda14() + mockBatch.Entries[1].Addenda15 = mockAddenda15() + mockBatch.Entries[1].Addenda16 = mockAddenda16() + mockBatch.Entries[1].AddIATAddenda(mockAddenda17()) + mockBatch.Entries[1].AddIATAddenda(mockAddenda17B()) + mockBatch.Entries[1].AddIATAddenda(mockAddenda18()) + mockBatch.Entries[1].AddIATAddenda(mockAddenda18B()) + mockBatch.Entries[1].AddIATAddenda(mockAddenda18C()) + mockBatch.Entries[1].AddIATAddenda(mockAddenda18D()) + mockBatch.Entries[1].AddIATAddenda(mockAddenda18E()) + + if err := mockBatch.build(); err != nil { + log.Fatal(err) + } + return mockBatch +} + +// mockIATBatch +func mockInvalidIATBatch() IATBatch { + mockBatch := IATBatch{} + mockBatch.SetHeader(mockIATBatchHeaderFF()) + 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(mockInvalidAddenda17()) + if err := mockBatch.build(); err != nil { + log.Fatal(err) + } + return mockBatch +} + +func mockInvalidAddenda17() *Addenda17 { + addenda17 := NewAddenda17() + addenda17.PaymentRelatedInformation = "Transfer of money from one country to another" + addenda17.typeCode = "02" + addenda17.SequenceNumber = 2 + addenda17.EntryDetailSequenceNumber = 0000002 + + return addenda17 +} + +// TestMockIATBatch validates mockIATBatch +func TestMockIATBatch(t *testing.T) { + iatBatch := mockIATBatch() + if err := iatBatch.verify(); err != nil { + t.Error("mockIATBatch does not validate and will break other tests") + } +} + +// testIATBatchAddenda10Error validates IATBatch returns an error if Addenda10 is not included +func testIATBatchAddenda10Error(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda10 = nil + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "FieldError" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchAddenda10Error tests validating IATBatch returns an error +// if Addenda10 is not included +func TestIATBatchAddenda10Error(t *testing.T) { + testIATBatchAddenda10Error(t) +} + +// BenchmarkIATBatchAddenda10Error benchmarks validating IATBatch returns an error +// if Addenda10 is not included +func BenchmarkIATBatchAddenda10Error(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddenda10Error(b) + } +} + +// testIATBatchAddenda11Error validates IATBatch returns an error if Addenda11 is not included +func testIATBatchAddenda11Error(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda11 = nil + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "FieldError" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchAddenda11Error tests validating IATBatch returns an error +// if Addenda11 is not included +func TestIATBatchAddenda11Error(t *testing.T) { + testIATBatchAddenda11Error(t) +} + +// BenchmarkIATBatchAddenda11Error benchmarks validating IATBatch returns an error +// if Addenda11 is not included +func BenchmarkIATBatchAddenda11Error(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddenda11Error(b) + } +} + +// testIATBatchAddenda12Error validates IATBatch returns an error if Addenda12 is not included +func testIATBatchAddenda12Error(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda12 = nil + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "FieldError" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchAddenda12Error tests validating IATBatch returns an error +// if Addenda12 is not included +func TestIATBatchAddenda12Error(t *testing.T) { + testIATBatchAddenda12Error(t) +} + +// BenchmarkIATBatchAddenda12Error benchmarks validating IATBatch returns an error +// if Addenda12 is not included +func BenchmarkIATBatchAddenda12Error(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddenda12Error(b) + } +} + +// testIATBatchAddenda13Error validates IATBatch returns an error if Addenda13 is not included +func testIATBatchAddenda13Error(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda13 = nil + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "FieldError" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchAddenda13Error tests validating IATBatch returns an error +// if Addenda13 is not included +func TestIATBatchAddenda13Error(t *testing.T) { + testIATBatchAddenda13Error(t) +} + +// BenchmarkIATBatchAddenda13Error benchmarks validating IATBatch returns an error +// if Addenda13 is not included +func BenchmarkIATBatchAddenda13Error(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddenda13Error(b) + } +} + +// testIATBatchAddenda14Error validates IATBatch returns an error if Addenda14 is not included +func testIATBatchAddenda14Error(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda14 = nil + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "FieldError" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchAddenda14Error tests validating IATBatch returns an error +// if Addenda14 is not included +func TestIATBatchAddenda14Error(t *testing.T) { + testIATBatchAddenda14Error(t) +} + +// BenchmarkIATBatchAddenda14Error benchmarks validating IATBatch returns an error +// if Addenda14 is not included +func BenchmarkIATBatchAddenda14Error(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddenda14Error(b) + } +} + +// testIATBatchAddenda15Error validates IATBatch returns an error if Addenda15 is not included +func testIATBatchAddenda15Error(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda15 = nil + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "FieldError" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchAddenda15Error tests validating IATBatch returns an error +// if Addenda15 is not included +func TestIATBatchAddenda15Error(t *testing.T) { + testIATBatchAddenda15Error(t) +} + +// BenchmarkIATBatchAddenda15Error benchmarks validating IATBatch returns an error +// if Addenda15 is not included +func BenchmarkIATBatchAddenda15Error(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddenda15Error(b) + } +} + +// testIATBatchAddenda16Error validates IATBatch returns an error if Addenda16 is not included +func testIATBatchAddenda16Error(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda16 = nil + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "FieldError" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchAddenda16Error tests validating IATBatch returns an error +// if Addenda16 is not included +func TestIATBatchAddenda16Error(t *testing.T) { + testIATBatchAddenda16Error(t) +} + +// BenchmarkIATBatchAddenda16Error benchmarks validating IATBatch returns an error +// if Addenda16 is not included +func BenchmarkIATBatchAddenda16Error(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddenda16Error(b) + } +} + +// testAddenda10EntryDetailSequenceNumber validates IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func testAddenda10EntryDetailSequenceNumber(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda10.EntryDetailSequenceNumber = 00000005 + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TraceNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda10EntryDetailSequenceNumber tests validating IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func TestAddenda10EntryDetailSequenceNumber(t *testing.T) { + testAddenda10EntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda10EntryDetailSequenceNumber benchmarks validating IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func BenchmarkAddenda10EntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10EntryDetailSequenceNumber(b) + } +} + +// testAddenda11EntryDetailSequenceNumber validates IATBatch returns an error if EntryDetailSequenceNumber +// is not valid +func testAddenda11EntryDetailSequenceNumber(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda11.EntryDetailSequenceNumber = 00000005 + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TraceNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda11EntryDetailSequenceNumber tests validating IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func TestAddenda11EntryDetailSequenceNumber(t *testing.T) { + testAddenda11EntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda11EntryDetailSequenceNumber benchmarks validating IATBatch returns an error +// if EntryDetailSequenceNumber is not valid +func BenchmarkAddenda11EntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda11EntryDetailSequenceNumber(b) + } +} + +// testAddenda12EntryDetailSequenceNumber validates IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func testAddenda12EntryDetailSequenceNumber(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda12.EntryDetailSequenceNumber = 00000005 + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TraceNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda12EntryDetailSequenceNumber tests validating IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func TestAddenda12EntryDetailSequenceNumber(t *testing.T) { + testAddenda12EntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda12EntryDetailSequenceNumber benchmarks validating IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func BenchmarkAddenda12EntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda12EntryDetailSequenceNumber(b) + } +} + +// testAddenda13EntryDetailSequenceNumber validates IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func testAddenda13EntryDetailSequenceNumber(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda13.EntryDetailSequenceNumber = 00000005 + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TraceNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda13EntryDetailSequenceNumber tests validating IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func TestAddenda13EntryDetailSequenceNumber(t *testing.T) { + testAddenda13EntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda13EntryDetailSequenceNumber benchmarks validating IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func BenchmarkAddenda13EntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda13EntryDetailSequenceNumber(b) + } +} + +// testAddenda14EntryDetailSequenceNumber validates IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func testAddenda14EntryDetailSequenceNumber(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda14.EntryDetailSequenceNumber = 00000005 + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TraceNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda14EntryDetailSequenceNumber tests validating IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func TestAddenda14EntryDetailSequenceNumber(t *testing.T) { + testAddenda14EntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda14EntryDetailSequenceNumber benchmarks validating IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func BenchmarkAddenda14EntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda14EntryDetailSequenceNumber(b) + } +} + +// testAddenda15EntryDetailSequenceNumber validates IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func testAddenda15EntryDetailSequenceNumber(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda15.EntryDetailSequenceNumber = 00000005 + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TraceNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda15EntryDetailSequenceNumber tests validating IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func TestAddenda15EntryDetailSequenceNumber(t *testing.T) { + testAddenda15EntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda15EntryDetailSequenceNumber benchmarks validating IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func BenchmarkAddenda15EntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda15EntryDetailSequenceNumber(b) + } +} + +// testAddenda16EntryDetailSequenceNumber validates IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func testAddenda16EntryDetailSequenceNumber(t testing.TB) { + iatBatch := mockIATBatch() + iatBatch.GetEntries()[0].Addenda16.EntryDetailSequenceNumber = 00000005 + if err := iatBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TraceNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestAddenda16EntryDetailSequenceNumber tests validating IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func TestAddenda16EntryDetailSequenceNumber(t *testing.T) { + testAddenda16EntryDetailSequenceNumber(t) +} + +// BenchmarkAddenda16EntryDetailSequenceNumber benchmarks validating IATBatch returns an error if +// EntryDetailSequenceNumber is not valid +func BenchmarkAddenda16EntryDetailSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda16EntryDetailSequenceNumber(b) + } +} + +// testIATBatchNumberMismatch validates BatchNumber mismatch +func testIATBatchNumberMismatch(t testing.TB) { + mockBatch := mockIATBatch() + mockBatch.GetControl().BatchNumber = 2 + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "BatchNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchNumberMismatch tests validating BatchNumber mismatch +func TestIATBatchNumberMismatch(t *testing.T) { + testIATBatchNumberMismatch(t) +} + +// BenchmarkIATBatchNumberMismatch benchmarks validating BatchNumber mismatch +func BenchmarkIATBatchNumberMismatch(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchNumberMismatch(b) + } +} + +// testIATServiceClassCodeMismatch validates ServiceClassCode mismatch +func testIATServiceClassCodeMismatch(t testing.TB) { + mockBatch := mockIATBatch() + mockBatch.GetControl().ServiceClassCode = 225 + if err := mockBatch.verify(); 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) + } + } +} + +// TestIATServiceClassCodeMismatch tests validating ServiceClassCode mismatch +func TestServiceClassCodeMismatch(t *testing.T) { + testIATServiceClassCodeMismatch(t) +} + +// BenchmarkIATServiceClassCoderMismatch benchmarks validating ServiceClassCode mismatch +func BenchmarkIATServiceClassCodeMismatch(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATServiceClassCodeMismatch(b) + } +} + +// testIATBatchCreditIsBatchAmount validates credit isBatchAmount +func testIATBatchCreditIsBatchAmount(t testing.TB) { + mockBatch := mockIATBatch() + e1 := mockBatch.GetEntries()[0] + e2 := mockIATEntryDetail() + e2.TransactionCode = 22 + e2.Amount = 5000 + e2.TraceNumber = e1.TraceNumber + 10 + mockBatch.AddEntry(e2) + mockBatch.Entries[1].Addenda10 = mockAddenda10() + mockBatch.Entries[1].Addenda11 = mockAddenda11() + mockBatch.Entries[1].Addenda12 = mockAddenda12() + mockBatch.Entries[1].Addenda13 = mockAddenda13() + mockBatch.Entries[1].Addenda14 = mockAddenda14() + mockBatch.Entries[1].Addenda15 = mockAddenda15() + mockBatch.Entries[1].Addenda16 = mockAddenda16() + if err := mockBatch.build(); err != nil { + t.Errorf("%T: %s", err, err) + } + + mockBatch.GetControl().TotalCreditEntryDollarAmount = 1000 + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TotalCreditEntryDollarAmount" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchCreditIsBatchAmount tests validating credit isBatchAmount +func TestIATBatchCreditIsBatchAmount(t *testing.T) { + testIATBatchCreditIsBatchAmount(t) +} + +// BenchmarkIATBatchCreditIsBatchAmount benchmarks validating credit isBatchAmount +func BenchmarkIATBatchCreditIsBatchAmount(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchCreditIsBatchAmount(b) + } + +} + +// testIATBatchDebitIsBatchAmount validates debit isBatchAmount +func testIATBatchDebitIsBatchAmount(t testing.TB) { + mockBatch := mockIATBatch() + e1 := mockBatch.GetEntries()[0] + e1.TransactionCode = 27 + e2 := mockIATEntryDetail() + e2.TransactionCode = 27 + e2.Amount = 5000 + e2.TraceNumber = e1.TraceNumber + 10 + mockBatch.AddEntry(e2) + mockBatch.Entries[1].Addenda10 = mockAddenda10() + mockBatch.Entries[1].Addenda11 = mockAddenda11() + mockBatch.Entries[1].Addenda12 = mockAddenda12() + mockBatch.Entries[1].Addenda13 = mockAddenda13() + mockBatch.Entries[1].Addenda14 = mockAddenda14() + mockBatch.Entries[1].Addenda15 = mockAddenda15() + mockBatch.Entries[1].Addenda16 = mockAddenda16() + if err := mockBatch.build(); err != nil { + t.Errorf("%T: %s", err, err) + } + + mockBatch.GetControl().TotalDebitEntryDollarAmount = 1000 + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TotalDebitEntryDollarAmount" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchDebitIsBatchAmount tests validating debit isBatchAmount +func TestIATBatchDebitIsBatchAmount(t *testing.T) { + testIATBatchDebitIsBatchAmount(t) +} + +// BenchmarkIATBatchDebitIsBatchAmount benchmarks validating debit isBatchAmount +func BenchmarkIATBatchDebitIsBatchAmount(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchDebitIsBatchAmount(b) + } + +} + +// testIATBatchFieldInclusion validates IATBatch FieldInclusion +func testIATBatchFieldInclusion(t testing.TB) { + mockBatch := mockIATBatch() + mockBatch2 := mockIATBatch() + mockBatch2.Header.recordType = "4" + + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } + if err := mockBatch2.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } + if err := mockBatch2.build(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchFieldInclusion tests validating IATBatch FieldInclusion +func TestIATBatchFieldInclusion(t *testing.T) { + testIATBatchFieldInclusion(t) +} + +// BenchmarkIATBatchFieldInclusion benchmarks validating IATBatch FieldInclusion +func BenchmarkIATBatchFieldInclusion(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchFieldInclusion(b) + } + +} + +// testIATBatchBuild validates IATBatch build error +func testIATBatchBuild(t testing.TB) { + mockBatch := IATBatch{} + mockBatch.SetHeader(mockIATBatchHeaderFF()) + + if err := mockBatch.build(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "entries" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchBuild tests validating IATBatch build error +func TestIATBatchBuild(t *testing.T) { + testIATBatchBuild(t) +} + +// BenchmarkIATBatchBuild benchmarks validating IATBatch build error +func BenchmarkIATBatchBuild(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchBuild(b) + } + +} + +// testIATODFIIdentificationMismatch validates ODFIIdentification mismatch +func testIATODFIIdentificationMismatch(t testing.TB) { + mockBatch := mockIATBatch() + mockBatch.GetControl().ODFIIdentification = "53158020" + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "ODFIIdentification" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATODFIIdentificationMismatch tests validating ODFIIdentification mismatch +func TestODFIIdentificationMismatch(t *testing.T) { + testIATODFIIdentificationMismatch(t) +} + +// BenchmarkIATODFIIdentificationMismatch benchmarks validating ODFIIdentification mismatch +func BenchmarkIATODFIIdentificationMismatch(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATODFIIdentificationMismatch(b) + } +} + +// testIATBatchAddendaRecordIndicator validates IATEntryDetail AddendaRecordIndicator +func testIATBatchAddendaRecordIndicator(t testing.TB) { + mockBatch := mockIATBatch() + mockBatch.GetEntries()[0].AddendaRecordIndicator = 2 + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "AddendaRecordIndicator" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchAddendaRecordIndicator tests validating IATEntryDetail AddendaRecordIndicator +func TestIATBatchAddendaRecordIndicator(t *testing.T) { + testIATBatchAddendaRecordIndicator(t) +} + +// BenchmarkIATBatchAddendaRecordIndicator benchmarks IATEntryDetail AddendaRecordIndicator +func BenchmarkIATBatchAddendaRecordIndicator(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddendaRecordIndicator(b) + } +} + +// testIATBatchInvalidTraceNumberODFI validates TraceNumberODFI +func testIATBatchInvalidTraceNumberODFI(t testing.TB) { + mockBatch := mockIATBatch() + mockBatch.GetEntries()[0].SetTraceNumber("9928272", 1) + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "ODFIIdentificationField" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchInvalidTraceNumberODFI tests validating TraceNumberODFI +func TestIATBatchInvalidTraceNumberODFI(t *testing.T) { + testIATBatchInvalidTraceNumberODFI(t) +} + +// BenchmarkIATBatchInvalidTraceNumberODFI benchmarks validating TraceNumberODFI +func BenchmarkIATBatchInvalidTraceNumberODFI(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchInvalidTraceNumberODFI(b) + } +} + +// testIATBatchControl validates BatchControl ODFIIdentification +func testIATBatchControl(t testing.TB) { + mockBatch := mockIATBatch() + mockBatch.Control.ODFIIdentification = "" + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "ODFIIdentification" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchControl tests validating BatchControl ODFIIdentification +func TestIATBatchControl(t *testing.T) { + testIATBatchControl(t) +} + +// BenchmarkIATBatchControl benchmarks validating BatchControl ODFIIdentification +func BenchmarkIATBatchControl(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchControl(b) + } +} + +// testIATBatchEntryCountEquality validates IATBatch EntryAddendaCount +func testIATBatchEntryCountEquality(t testing.TB) { + mockBatch := mockIATBatch() + if err := mockBatch.build(); err != nil { + t.Errorf("%T: %s", err, err) + } + + mockBatch.GetControl().EntryAddendaCount = 1 + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "EntryAddendaCount" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchEntryCountEquality tests validating IATBatch EntryAddendaCount +func TestIATBatchEntryCountEquality(t *testing.T) { + testIATBatchEntryCountEquality(t) +} + +// BenchmarkIATBatchEntryCountEquality benchmarks validating IATBatch EntryAddendaCount +func BenchmarkIATBatchEntryCountEquality(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchEntryCountEquality(b) + } +} + +// testIATBatchisEntryHash validates IATBatch EntryHash +func testIATBatchisEntryHash(t testing.TB) { + mockBatch := mockIATBatch() + mockBatch.GetControl().EntryHash = 1 + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "EntryHash" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +//TestIATBatchisEntryHash tests validating IATBatch EntryHash +func TestIATBatchisEntryHash(t *testing.T) { + testIATBatchisEntryHash(t) +} + +//BenchmarkIATBatchisEntryHash benchmarks validating IATBatch EntryHash +func BenchmarkIATBatchisEntryHash(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchisEntryHash(b) + } +} + +// testIATBatchIsSequenceAscending validates sequence ascending +func testIATBatchIsSequenceAscending(t testing.TB) { + mockBatch := mockIATBatch() + e2 := mockIATEntryDetail() + e2.TraceNumber = 1 + mockBatch.AddEntry(e2) + mockBatch.Entries[1].Addenda10 = mockAddenda10() + mockBatch.Entries[1].Addenda11 = mockAddenda11() + mockBatch.Entries[1].Addenda12 = mockAddenda12() + mockBatch.Entries[1].Addenda13 = mockAddenda13() + mockBatch.Entries[1].Addenda14 = mockAddenda14() + mockBatch.Entries[1].Addenda15 = mockAddenda15() + mockBatch.Entries[1].Addenda16 = mockAddenda16() + mockBatch.GetControl().EntryAddendaCount = 16 + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TraceNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchIsSequenceAscending tests validating sequence ascending +func TestIATBatchIsSequenceAscending(t *testing.T) { + testIATBatchIsSequenceAscending(t) +} + +// BenchmarkIATBatchIsSequenceAscending tests validating sequence ascending +func BenchmarkIATBatchIsSequenceAscending(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchIsSequenceAscending(b) + } +} + +// testIATBatchIsCategory validates category +func testIATBatchIsCategory(t testing.TB) { + mockBatch := mockIATBatchManyEntries() + mockBatch.GetEntries()[1].Category = CategoryReturn + + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "Category" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchIsCategory tests validating category +func TestIATBatchIsCategory(t *testing.T) { + testIATBatchIsCategory(t) +} + +// BenchmarkIATBatchIsCategory tests validating category +func BenchmarkIATBatchIsCategory(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchIsCategory(b) + } +} + +//testIATBatchCategory tests IATBatch Category +func testIATBatchCategory(t testing.TB) { + mockBatch := mockIATBatch() + + if err := mockBatch.build(); err != nil { + t.Errorf("%T: %s", err, err) + } + + if mockBatch.Category() != CategoryForward { + t.Errorf("No returns and Category is %s", mockBatch.Category()) + } +} + +// TestIATBatchCategory tests IATBatch Category +func TestIATBatchCategory(t *testing.T) { + testIATBatchCategory(t) +} + +// BenchmarkIATBatchCategory benchmarks IATBatch Category +func BenchmarkIATBatchCategory(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchCategory(b) + } +} + +// testIATBatchValidateEntry validates EntryDetail +func testIATBatchValidateEntry(t testing.TB) { + mockBatch := mockIATBatch() + mockBatch.GetEntries()[0].recordType = "5" + + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchValidateEntry tests validating Entry +func TestIATBatchValidateEntry(t *testing.T) { + testIATBatchValidateEntry(t) +} + +// BenchmarkIATBatchValidateEntry tests validating Entry +func BenchmarkIATBatchValidateEntry(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchValidateEntry(b) + } +} + +// testIATBatchValidateAddenda10 validates Addenda10 +func testIATBatchValidateAddenda10(t testing.TB) { + mockBatch := mockIATBatchManyEntries() + mockBatch.GetEntries()[1].Addenda10.typeCode = "02" + + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchValidateAddenda10 tests validating Addenda10 +func TestIATBatchValidateAddenda10(t *testing.T) { + testIATBatchValidateAddenda10(t) +} + +// BenchmarkIATBatchValidateAddenda10 tests validating Addenda10 +func BenchmarkIATBatchValidateAddenda10(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchValidateAddenda10(b) + } +} + +// testIATBatchValidateAddenda11 validates Addenda11 +func testIATBatchValidateAddenda11(t testing.TB) { + mockBatch := mockIATBatchManyEntries() + mockBatch.GetEntries()[1].Addenda11.typeCode = "02" + + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchValidateAddenda11 tests validating Addenda11 +func TestIATBatchValidateAddenda11(t *testing.T) { + testIATBatchValidateAddenda11(t) +} + +// BenchmarkIATBatchValidateAddenda11 tests validating Addenda11 +func BenchmarkIATBatchValidateAddenda11(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchValidateAddenda11(b) + } +} + +// testIATBatchValidateAddenda12 validates Addenda12 +func testIATBatchValidateAddenda12(t testing.TB) { + mockBatch := mockIATBatchManyEntries() + mockBatch.GetEntries()[1].Addenda12.typeCode = "02" + + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchValidateAddenda12 tests validating Addenda12 +func TestIATBatchValidateAddenda12(t *testing.T) { + testIATBatchValidateAddenda12(t) +} + +// BenchmarkIATBatchValidateAddenda12 tests validating Addenda12 +func BenchmarkIATBatchValidateAddenda12(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchValidateAddenda12(b) + } +} + +// testIATBatchValidateAddenda13 validates Addenda13 +func testIATBatchValidateAddenda13(t testing.TB) { + mockBatch := mockIATBatchManyEntries() + mockBatch.GetEntries()[1].Addenda13.typeCode = "02" + + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchValidateAddenda13 tests validating Addenda13 +func TestIATBatchValidateAddenda13(t *testing.T) { + testIATBatchValidateAddenda13(t) +} + +// BenchmarkIATBatchValidateAddenda13 tests validating Addenda13 +func BenchmarkIATBatchValidateAddenda13(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchValidateAddenda13(b) + } +} + +// testIATBatchValidateAddenda14 validates Addenda14 +func testIATBatchValidateAddenda14(t testing.TB) { + mockBatch := mockIATBatchManyEntries() + mockBatch.GetEntries()[1].Addenda14.typeCode = "02" + + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchValidateAddenda14 tests validating Addenda14 +func TestIATBatchValidateAddenda14(t *testing.T) { + testIATBatchValidateAddenda14(t) +} + +// BenchmarkIATBatchValidateAddenda14 tests validating Addenda14 +func BenchmarkIATBatchValidateAddenda14(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchValidateAddenda14(b) + } +} + +// testIATBatchValidateAddenda15 validates Addenda15 +func testIATBatchValidateAddenda15(t testing.TB) { + mockBatch := mockIATBatchManyEntries() + mockBatch.GetEntries()[1].Addenda15.typeCode = "02" + + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchValidateAddenda15 tests validating Addenda15 +func TestIATBatchValidateAddenda15(t *testing.T) { + testIATBatchValidateAddenda15(t) +} + +// BenchmarkIATBatchValidateAddenda15 tests validating Addenda15 +func BenchmarkIATBatchValidateAddenda15(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchValidateAddenda15(b) + } +} + +// testIATBatchValidateAddenda16 validates Addenda16 +func testIATBatchValidateAddenda16(t testing.TB) { + mockBatch := mockIATBatchManyEntries() + mockBatch.GetEntries()[1].Addenda16.typeCode = "02" + + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchValidateAddenda16 tests validating Addenda16 +func TestIATBatchValidateAddenda16(t *testing.T) { + testIATBatchValidateAddenda16(t) +} + +// BenchmarkIATBatchValidateAddenda16 tests validating Addenda16 +func BenchmarkIATBatchValidateAddenda16(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchValidateAddenda16(b) + } +} + +// testIATBatchValidateAddenda17 validates Addenda17 +func testIATBatchValidateAddenda17(t testing.TB) { + mockBatch := mockInvalidIATBatch() + + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TypeCode" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchValidateAddenda17 tests validating Addenda17 +func TestIATBatchValidateAddenda17(t *testing.T) { + testIATBatchValidateAddenda17(t) +} + +// BenchmarkIATBatchValidateAddenda17 tests validating Addenda17 +func BenchmarkIATBatchValidateAddenda17(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchValidateAddenda17(b) + } +} + +// testIATBatchCreateError validates IATBatch create error +func testIATBatchCreate(t testing.TB) { + file := NewFile().SetHeader(mockFileHeader()) + mockBatch := mockIATBatch() + mockBatch.GetHeader().recordType = "7" + + if err := mockBatch.Create(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } + + file.AddIATBatch(mockBatch) +} + +// TestIATBatchCreate tests validating IATBatch create error +func TestIATBatchCreate(t *testing.T) { + testIATBatchCreate(t) +} + +// BenchmarkIATBatchCreate benchmarks validating IATBatch create error +func BenchmarkIATBatchCreate(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchCreate(b) + } + +} + +// testIATBatchValidate validates IATBatch validate error +func testIATBatchValidate(t testing.TB) { + file := NewFile().SetHeader(mockFileHeader()) + mockBatch := mockIATBatch() + mockBatch.GetHeader().ServiceClassCode = 225 + + 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) + } + } + + file.AddIATBatch(mockBatch) +} + +// TestIATBatchValidate tests validating IATBatch validate error +func TestIATBatchValidate(t *testing.T) { + testIATBatchValidate(t) +} + +// BenchmarkIATBatchValidate benchmarks validating IATBatch validate error +func BenchmarkIATBatchValidate(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchValidate(b) + } + +} + +// testIATBatchEntryAddendum validates IATBatch EntryAddendum error +func testIATBatchEntryAddendum(t testing.TB) { + file := NewFile().SetHeader(mockFileHeader()) + mockBatch := mockIATBatch() + 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()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda18D()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda18E()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda18F()) + + 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) + } + } + + file.AddIATBatch(mockBatch) +} + +// TestIATBatchEntryAddendum tests validating IATBatch EntryAddendum error +func TestIATBatchEntryAddendum(t *testing.T) { + testIATBatchEntryAddendum(t) +} + +// BenchmarkIATBatchEntryAddendum benchmarks validating IATBatch EntryAddendum error +func BenchmarkIATBatchEntryAddendum(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchEntryAddendum(b) + } +} + +// testIATBatchAddenda17EDSequenceNumber validates IATBatch Addenda17 Entry Detail Sequence Number error +func testIATBatchAddenda17EDSequenceNumber(t testing.TB) { + mockBatch := IATBatch{} + mockBatch.SetHeader(mockIATBatchHeaderFF()) + mockBatch.AddEntry(mockIATEntryDetail()) + addenda17 := NewAddenda17() + addenda17.PaymentRelatedInformation = "This is an international payment" + addenda17.SequenceNumber = 1 + addenda17.EntryDetailSequenceNumber = 0000001 + addenda17B := NewAddenda17() + addenda17B.PaymentRelatedInformation = "Transfer of money from one country to another" + addenda17B.SequenceNumber = 2 + addenda17B.EntryDetailSequenceNumber = 0000001 + 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(addenda17) + mockBatch.Entries[0].AddIATAddenda(addenda17B) + + if err := mockBatch.build(); err != nil { + t.Errorf("%T: %s", err, err) + } + + addenda17B.SequenceNumber = 1 + addenda17B.EntryDetailSequenceNumber = 0000002 + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TraceNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } + +} + +// TestIATBatchAddenda17EDSequenceNumber tests validating IATBatch Addenda17 Entry Detail Sequence Number error +func TestIATBatchAddenda17EDSequenceNumber(t *testing.T) { + testIATBatchAddenda17EDSequenceNumber(t) +} + +// BenchmarkIATBatchAddenda17EDSequenceNumber benchmarks validating IATBatch Addenda17 Entry Detail Sequence Number error +func BenchmarkIATBatchAddenda17EDSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddenda17EDSequenceNumber(b) + } +} + +// testIATBatchAddenda17Sequence validates IATBatch Addenda17 Sequence Number error +func testIATBatchAddenda17Sequence(t testing.TB) { + mockBatch := IATBatch{} + mockBatch.SetHeader(mockIATBatchHeaderFF()) + mockBatch.AddEntry(mockIATEntryDetail()) + addenda17 := NewAddenda17() + addenda17.PaymentRelatedInformation = "This is an international payment" + addenda17.SequenceNumber = 2 + addenda17.EntryDetailSequenceNumber = 0000001 + addenda17B := NewAddenda17() + addenda17B.PaymentRelatedInformation = "Transfer of money from one country to another" + addenda17B.SequenceNumber = 1 + addenda17B.EntryDetailSequenceNumber = 0000001 + 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(addenda17) + mockBatch.Entries[0].AddIATAddenda(addenda17B) + + if err := mockBatch.build(); err != nil { + t.Errorf("%T: %s", err, err) + } + + addenda17B.SequenceNumber = -1 + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "SequenceNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } + +} + +// TestIATBatchAddenda17Sequence tests validating IATBatch Addenda17 Sequence Number error +func TestIATBatchAddenda17Sequence(t *testing.T) { + testIATBatchAddenda17Sequence(t) +} + +// BenchmarkIATBatchAddenda17Sequence benchmarks validating IATBatch Addenda17 Sequence Number error +func BenchmarkIATBatchAddenda17Sequence(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddenda17Sequence(b) + } +} + +// testIATBatchAddenda18EDSequenceNumber validates IATBatch Addenda18 Entry Detail Sequence Number error +func testIATBatchAddenda18EDSequenceNumber(t testing.TB) { + mockBatch := IATBatch{} + mockBatch.SetHeader(mockIATBatchHeaderFF()) + mockBatch.AddEntry(mockIATEntryDetail()) + addenda17 := NewAddenda17() + addenda17.PaymentRelatedInformation = "This is an international payment" + addenda17.SequenceNumber = 1 + addenda17.EntryDetailSequenceNumber = 0000001 + addenda17B := NewAddenda17() + addenda17B.PaymentRelatedInformation = "Transfer of money from one country to another" + addenda17B.SequenceNumber = 2 + addenda17B.EntryDetailSequenceNumber = 0000001 + addenda18 := NewAddenda18() + addenda18.ForeignCorrespondentBankName = "Bank of Turkey" + addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18.ForeignCorrespondentBankIDNumber = "12312345678910" + addenda18.ForeignCorrespondentBankBranchCountryCode = "TR" + addenda18.SequenceNumber = 1 + addenda18.EntryDetailSequenceNumber = 0000001 + addenda18B := NewAddenda18() + addenda18B.ForeignCorrespondentBankName = "Bank of United Kingdom" + addenda18B.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18B.ForeignCorrespondentBankIDNumber = "1234567890123456789012345678901234" + addenda18B.ForeignCorrespondentBankBranchCountryCode = "GB" + addenda18B.SequenceNumber = 2 + addenda18B.EntryDetailSequenceNumber = 0000001 + 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(addenda17) + mockBatch.Entries[0].AddIATAddenda(addenda17B) + mockBatch.Entries[0].AddIATAddenda(addenda18) + mockBatch.Entries[0].AddIATAddenda(addenda18B) + + if err := mockBatch.build(); err != nil { + t.Errorf("%T: %s", err, err) + } + + addenda18B.SequenceNumber = 1 + addenda18B.EntryDetailSequenceNumber = 0000002 + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "TraceNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } + +} + +// TestIATBatchAddenda18EDSequenceNumber tests validating IATBatch Addenda18 Entry Detail Sequence Number error +func TestIATBatchAddenda18EDSequenceNumber(t *testing.T) { + testIATBatchAddenda18EDSequenceNumber(t) +} + +// BenchmarkIATBatchAddenda18EDSequenceNumber benchmarks validating IATBatch Addenda18 Entry Detail Sequence Number error +func BenchmarkIATBatchAddenda18EDSequenceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddenda18EDSequenceNumber(b) + } +} + +// testIATBatchAddenda18Sequence validates IATBatch Addenda18 Sequence Number error +func testIATBatchAddenda18Sequence(t testing.TB) { + mockBatch := IATBatch{} + mockBatch.SetHeader(mockIATBatchHeaderFF()) + mockBatch.AddEntry(mockIATEntryDetail()) + addenda17 := NewAddenda17() + addenda17.PaymentRelatedInformation = "This is an international payment" + addenda17.SequenceNumber = 1 + addenda17.EntryDetailSequenceNumber = 0000001 + addenda17B := NewAddenda17() + addenda17B.PaymentRelatedInformation = "Transfer of money from one country to another" + addenda17B.SequenceNumber = 2 + addenda17B.EntryDetailSequenceNumber = 0000001 + addenda18 := NewAddenda18() + addenda18.ForeignCorrespondentBankName = "Bank of Turkey" + addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18.ForeignCorrespondentBankIDNumber = "12312345678910" + addenda18.ForeignCorrespondentBankBranchCountryCode = "TR" + addenda18.SequenceNumber = 1 + addenda18.EntryDetailSequenceNumber = 0000001 + addenda18B := NewAddenda18() + addenda18B.ForeignCorrespondentBankName = "Bank of United Kingdom" + addenda18B.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18B.ForeignCorrespondentBankIDNumber = "1234567890123456789012345678901234" + addenda18B.ForeignCorrespondentBankBranchCountryCode = "GB" + addenda18B.SequenceNumber = 2 + addenda18B.EntryDetailSequenceNumber = 0000001 + 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(addenda17) + mockBatch.Entries[0].AddIATAddenda(addenda17B) + mockBatch.Entries[0].AddIATAddenda(addenda18) + mockBatch.Entries[0].AddIATAddenda(addenda18B) + + if err := mockBatch.build(); err != nil { + t.Errorf("%T: %s", err, err) + } + + addenda18B.SequenceNumber = -1 + if err := mockBatch.Validate(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "SequenceNumber" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } + +} + +// TestIATBatchAddenda18Sequence tests validating IATBatch Addenda18 Sequence Number error +func TestIATBatchAddenda18Sequence(t *testing.T) { + testIATBatchAddenda18Sequence(t) +} + +// BenchmarkIATBatchAddenda18Sequence benchmarks validating IATBatch Addenda18 Sequence Number error +func BenchmarkIATBatchAddenda18Sequence(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddenda18Sequence(b) + } +} + +// testIATNoEntry validates error for no entries +func testIATNoEntry(t testing.TB) { + mockBatch := IATBatch{} + mockBatch.SetHeader(mockIATBatchHeaderFF()) + if err := mockBatch.verify(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "entries" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATNoEntry tests validating error for no entries +func TestIATNoEntry(t *testing.T) { + testIATNoEntry(t) +} + +// BenchmarkIATNoEntry benchmarks validating error for no entries +func BenchmarkIATNoEntry(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATNoEntry(b) + } +} + +// testIATBatchAddendumTypeCode validates IATBatch Addendum TypeCode +func testIATBatchAddendumTypeCode(t testing.TB) { + mockBatch := mockIATBatch() + mockBatch.GetEntries()[0].AddIATAddenda(mockAddenda12()) + + 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) + } + } +} + +// TestIATBatchAddendumTypeCode tests validating IATBatch Addendum TypeCode +func TestIATBatchAddendumTypeCode(t *testing.T) { + testIATBatchAddendumTypeCode(t) +} + +// BenchmarkIATBatchAddendumTypeCode benchmarks validating IATBatch Addendum TypeCode +func BenchmarkIATBatchAddendumTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddendumTypeCode(b) + } + +} + +// testIATBatchAddenda17Count validates IATBatch Addenda17 Count +func testIATBatchAddenda17Count(t testing.TB) { + mockBatch := IATBatch{} + mockBatch.SetHeader(mockIATBatchHeaderFF()) + mockBatch.AddEntry(mockIATEntryDetail()) + addenda17 := NewAddenda17() + addenda17.PaymentRelatedInformation = "This is an international payment" + addenda17.SequenceNumber = 1 + addenda17.EntryDetailSequenceNumber = 0000001 + addenda17B := NewAddenda17() + addenda17B.PaymentRelatedInformation = "Transfer of money from one country to another" + addenda17B.SequenceNumber = 2 + addenda17B.EntryDetailSequenceNumber = 0000001 + addenda17C := NewAddenda17() + addenda17C.PaymentRelatedInformation = "Send money Internationally" + addenda17C.SequenceNumber = 3 + addenda17C.EntryDetailSequenceNumber = 0000001 + 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(addenda17) + mockBatch.Entries[0].AddIATAddenda(addenda17B) + mockBatch.Entries[0].AddIATAddenda(addenda17C) + + 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) + } + } + +} + +// TestIATBatchAddenda17Count tests validating IATBatch Addenda17 Count +func TestIATBatchAddenda17Count(t *testing.T) { + testIATBatchAddenda17Count(t) +} + +// BenchmarkIATBatchAddenda17Count benchmarks validating IATBatch Addenda17 Count +func BenchmarkIATBatchAddenda17Count(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddenda17Count(b) + } +} + +// testIATBatchAddenda18Count validates IATBatch Addenda18 Count +func testIATBatchAddenda18Count(t testing.TB) { + mockBatch := IATBatch{} + mockBatch.SetHeader(mockIATBatchHeaderFF()) + 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(mockAddenda17()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda17B()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda18()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda18B()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda18C()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda18D()) + mockBatch.Entries[0].AddIATAddenda(mockAddenda18E()) + + addenda18F := NewAddenda18() + addenda18F.ForeignCorrespondentBankName = "Russian Federation Bank" + addenda18F.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18F.ForeignCorrespondentBankIDNumber = "123123456789943874" + addenda18F.ForeignCorrespondentBankBranchCountryCode = "RU" + addenda18F.SequenceNumber = 6 + addenda18F.EntryDetailSequenceNumber = 0000001 + + mockBatch.Entries[0].AddIATAddenda(mockAddenda18F()) + + 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) + } + } + +} + +// TestIATBatchAddenda18Count tests validating IATBatch Addenda18 Count +func TestIATBatchAddenda18Count(t *testing.T) { + testIATBatchAddenda18Count(t) +} + +// BenchmarkIATBatchAddenda18Count benchmarks validating IATBatch Addenda18 Count +func BenchmarkIATBatchAddenda18Count(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchAddenda18Count(b) + } +} + +// testIATBatchBuildAddendaError validates IATBatch build Addenda error +func testIATBatchBuildAddendaError(t testing.TB) { + mockBatch := IATBatch{} + mockBatch.SetHeader(mockIATBatchHeaderFF()) + mockBatch.AddEntry(mockIATEntryDetail()) + + if err := mockBatch.build(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "Addenda10" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchBuildAddendaError tests validating IATBatch build Addenda error +func TestIATBatchBuildAddendaError(t *testing.T) { + testIATBatchBuildAddendaError(t) +} + +// BenchmarkIATBatchBuildAddendaError benchmarks validating IATBatch build Addenda error +func BenchmarkIATBatchBuildAddendaError(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchBuildAddendaError(b) + } + +} + +// testIATBatchBHODFI validates IATBatchHeader ODFI error +func testIATBatchBHODFI(t testing.TB) { + mockBatch := mockIATBatch() + mockBatch.GetEntries()[0].SetTraceNumber("39387337", 1) + + if err := mockBatch.build(); err != nil { + if e, ok := err.(*BatchError); ok { + if e.FieldName != "entries" { + t.Errorf("%T: %s", err, err) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestIATBatchBHODFI tests validating IATBatchHeader ODFI error +func TestIATBatchBHODFI(t *testing.T) { + testIATBatchBHODFI(t) +} + +// BenchmarkIATBatchBHODFI benchmarks validating IATBatchHeader ODFI error +func BenchmarkIATBatchBHODFI(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATBatchBHODFI(b) + } + +} diff --git a/iatEntryDetail.go b/iatEntryDetail.go new file mode 100644 index 000000000..66ee7cdd5 --- /dev/null +++ b/iatEntryDetail.go @@ -0,0 +1,304 @@ +// 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" + "strconv" +) + +// IATEntryDetail contains the actual transaction data for an individual entry. +// Fields include those designating the entry as a deposit (credit) or +// withdrawal (debit), the transit routing number for the entry recipient’s financial +// institution, the account number (left justify,no zero fill), name, and dollar amount. +type IATEntryDetail struct { + // ID is a client defined string used as a reference to this record. + ID string `json:"id"` + // RecordType defines the type of record in the block. 6 + recordType string + // TransactionCode if the receivers account is: + // Credit (deposit) to checking account ‘22’ + // Prenote for credit to checking account ‘23’ + // Debit (withdrawal) to checking account ‘27’ + // Prenote for debit to checking account ‘28’ + // Credit to savings account ‘32’ + // Prenote for credit to savings account ‘33’ + // Debit to savings account ‘37’ + // Prenote for debit to savings account ‘38’ + TransactionCode int `json:"transactionCode"` + // RDFIIdentification is the RDFI's routing number without the last digit. + // Receiving Depository Financial Institution + RDFIIdentification string `json:"RDFIIdentification"` + // CheckDigit the last digit of the RDFI's routing number + CheckDigit string `json:"checkDigit"` + // AddendaRecords is the number of Addenda Records + AddendaRecords int `json:"AddendaRecords"` + // reserved - Leave blank + reserved string + // Amount Number of cents you are debiting/crediting this account + Amount int `json:"amount"` + // DFIAccountNumber is the receiver's bank account number you are crediting/debiting. + // It important to note that this is an alphanumeric field, so its space padded, no zero padded + DFIAccountNumber string `json:"DFIAccountNumber"` + // reservedTwo - Leave blank + reservedTwo string + // OFACSreeningIndicator - Leave blank + OFACSreeningIndicator string `json:"OFACSreeningIndicator"` + // SecondaryOFACSreeningIndicator - Leave blank + SecondaryOFACSreeningIndicator string `json:"SecondaryOFACSreeningIndicator"` + // AddendaRecordIndicator indicates the existence of an Addenda Record. + // A value of "1" indicates that one ore more addenda records follow, + // and "0" means no such record is present. + AddendaRecordIndicator int `json:"addendaRecordIndicator,omitempty"` + // TraceNumber assigned by the ODFI in ascending sequence, is included in each + // Entry Detail Record, Corporate Entry Detail Record, and addenda Record. + // Trace Numbers uniquely identify each entry within a batch in an ACH input file. + // In association with the Batch Number, transmission (File Creation) Date, + // and File ID Modifier, the Trace Number uniquely identifies an entry within a given file. + // For addenda Records, the Trace Number will be identical to the Trace Number + // in the associated Entry Detail Record, since the Trace Number is associated + // with an entry or item rather than a physical record. + TraceNumber int `json:"traceNumber,omitempty"` + // Addenda10 is mandatory for IAT entries + // + // The Addenda10 Record identifies the Receiver of the transaction and the dollar amount of + // the payment. + Addenda10 *Addenda10 `json:"addenda10,omitempty"` + // Addenda11 is mandatory for IAT entries + // + // The Addenda11 record identifies key information related to the Originator of + // the entry. + Addenda11 *Addenda11 `json:"addenda11,omitempty"` + // Addenda12 is mandatory for IAT entries + // + // The Addenda12 record identifies key information related to the Originator of + // the entry. + Addenda12 *Addenda12 `json:"addenda12,omitempty"` + // Addenda13 is mandatory for IAT entries + // + // The Addenda13 contains information related to the financial institution originating the entry. + // For inbound IAT entries, the Fourth Addenda Record must contain information to identify the + // foreign financial institution that is providing the funding and payment instruction for + // the IAT entry. + Addenda13 *Addenda13 `json:"addenda13,omitempty"` + // Addenda14 is mandatory for IAT entries + // + // The Addenda14 identifies the Receiving financial institution holding the Receiver's account. + Addenda14 *Addenda14 `json:"addenda14,omitempty"` + // Addenda15 is mandatory for IAT entries + // + // The Addenda15 record identifies key information related to the Receiver. + Addenda15 *Addenda15 `json:"addenda15,omitempty"` + // Addenda16 is mandatory for IAt entries + // + // Addenda16 record identifies additional key information related to the Receiver. + Addenda16 *Addenda16 `json:"addenda16,omitempty"` + // Addendum a list of Addenda for the Entry Detail. For IAT the addendumer is currently being used + // for the optional Addenda17 and Addenda18 records. + // ToDo: Consider reverting Addenda* explicit properties back to being addendumer + Addendum []Addendumer `json:"addendum,omitempty"` + // Category defines if the entry is a Forward, Return, or NOC + Category string `json:"category,omitempty"` + // validator is composed for data validation + validator + // converters is composed for ACH to golang Converters + converters +} + +// NewIATEntryDetail returns a new IATEntryDetail with default values for non exported fields +func NewIATEntryDetail() *IATEntryDetail { + iatEd := &IATEntryDetail{ + recordType: "6", + Category: CategoryForward, + AddendaRecordIndicator: 1, + } + return iatEd +} + +// Parse takes the input record string and parses the EntryDetail values +func (ed *IATEntryDetail) Parse(record string) { + // 1-1 Always "6" + ed.recordType = "6" + // 2-3 is checking credit 22 debit 27 savings credit 32 debit 37 + ed.TransactionCode = ed.parseNumField(record[1:3]) + // 4-11 the RDFI's routing number without the last digit. + ed.RDFIIdentification = ed.parseStringField(record[3:11]) + // 12-12 The last digit of the RDFI's routing number + ed.CheckDigit = ed.parseStringField(record[11:12]) + // 13-16 Number of addenda records + ed.AddendaRecords = ed.parseNumField(record[12:16]) + // 17-29 reserved - Leave blank + ed.reserved = " " + // 30-39 Number of cents you are debiting/crediting this account + ed.Amount = ed.parseNumField(record[29:39]) + // 40-74 The foreign receiver's account number you are crediting/debiting + ed.DFIAccountNumber = record[39:74] + // 75-76 reserved2 Leave blank + ed.reservedTwo = " " + // 77 OFACScreeningIndicator + ed.OFACSreeningIndicator = " " + // 78-78 Secondary SecondaryOFACSreeningIndicator + ed.SecondaryOFACSreeningIndicator = " " + // 79-79 1 if addenda exists 0 if it does not + //ed.AddendaRecordIndicator = 1 + ed.AddendaRecordIndicator = ed.parseNumField(record[78:79]) + // 80-94 An internal identification (alphanumeric) that you use to uniquely identify + // this Entry Detail Record This number should be unique to the transaction and will help identify the transaction in case of an inquiry + ed.TraceNumber = ed.parseNumField(record[79:94]) +} + +// String writes the EntryDetail struct to a 94 character string. +func (ed *IATEntryDetail) String() string { + return fmt.Sprintf("%v%v%v%v%v%v%v%v%v%v%v%v%v", + ed.recordType, + ed.TransactionCode, + ed.RDFIIdentificationField(), + ed.CheckDigit, + ed.AddendaRecordsField(), + ed.reservedField(), + ed.AmountField(), + ed.DFIAccountNumberField(), + ed.reservedTwoField(), + ed.OFACSreeningIndicatorField(), + ed.SecondaryOFACSreeningIndicatorField(), + ed.AddendaRecordIndicator, + ed.TraceNumberField()) +} + +// Validate performs NACHA format rule checks on the record and returns an error if not Validated +// The first error encountered is returned and stops that parsing. +func (ed *IATEntryDetail) Validate() error { + if err := ed.fieldInclusion(); err != nil { + return err + } + if ed.recordType != "6" { + msg := fmt.Sprintf(msgRecordType, 6) + return &FieldError{FieldName: "recordType", Value: ed.recordType, Msg: msg} + } + if err := ed.isTransactionCode(ed.TransactionCode); err != nil { + return &FieldError{FieldName: "TransactionCode", Value: strconv.Itoa(ed.TransactionCode), Msg: err.Error()} + } + if err := ed.isAlphanumeric(ed.DFIAccountNumber); err != nil { + return &FieldError{FieldName: "DFIAccountNumber", Value: ed.DFIAccountNumber, Msg: err.Error()} + } + // CheckDigit calculations + calculated := ed.CalculateCheckDigit(ed.RDFIIdentificationField()) + + edCheckDigit, err := strconv.Atoi(ed.CheckDigit) + if err != nil { + return &FieldError{FieldName: "CheckDigit", Value: ed.CheckDigit, Msg: err.Error()} + } + if calculated != edCheckDigit { + msg := fmt.Sprintf(msgValidCheckDigit, calculated) + return &FieldError{FieldName: "RDFIIdentification", Value: ed.CheckDigit, Msg: msg} + } + return nil +} + +// fieldInclusion validate mandatory fields are not default values. If fields are +// invalid the ACH transfer will be returned. +func (ed *IATEntryDetail) fieldInclusion() error { + if ed.recordType == "" { + return &FieldError{FieldName: "recordType", + Value: ed.recordType, Msg: msgFieldInclusion} + } + if ed.TransactionCode == 0 { + return &FieldError{FieldName: "TransactionCode", + Value: strconv.Itoa(ed.TransactionCode), Msg: msgFieldInclusion} + } + if ed.RDFIIdentification == "" { + return &FieldError{FieldName: "RDFIIdentification", + Value: ed.RDFIIdentificationField(), Msg: msgFieldInclusion} + } + if ed.AddendaRecords == 0 { + return &FieldError{FieldName: "AddendaRecords", + Value: strconv.Itoa(ed.AddendaRecords), Msg: msgFieldInclusion} + } + if ed.DFIAccountNumber == "" { + return &FieldError{FieldName: "DFIAccountNumber", + Value: ed.DFIAccountNumber, Msg: msgFieldInclusion} + } + if ed.AddendaRecordIndicator == 0 { + return &FieldError{FieldName: "AddendaRecordIndicator", + Value: strconv.Itoa(ed.AddendaRecordIndicator), Msg: msgFieldInclusion} + } + if ed.TraceNumber == 0 { + return &FieldError{FieldName: "TraceNumber", + Value: ed.TraceNumberField(), Msg: msgFieldInclusion} + } + return nil +} + +// SetRDFI takes the 9 digit RDFI account number and separates it for RDFIIdentification and CheckDigit +func (ed *IATEntryDetail) SetRDFI(rdfi string) *IATEntryDetail { + s := ed.stringField(rdfi, 9) + ed.RDFIIdentification = ed.parseStringField(s[:8]) + ed.CheckDigit = ed.parseStringField(s[8:9]) + return ed +} + +// SetTraceNumber takes first 8 digits of ODFI and concatenates a sequence number onto the TraceNumber +func (ed *IATEntryDetail) SetTraceNumber(ODFIIdentification string, seq int) { + trace := ed.stringField(ODFIIdentification, 8) + ed.numericField(seq, 7) + ed.TraceNumber = ed.parseNumField(trace) +} + +// RDFIIdentificationField get the rdfiIdentification with zero padding +func (ed *IATEntryDetail) RDFIIdentificationField() string { + return ed.stringField(ed.RDFIIdentification, 8) +} + +// AddendaRecordsField returns a zero padded TraceNumber string +func (ed *IATEntryDetail) AddendaRecordsField() string { + return ed.numericField(ed.AddendaRecords, 4) +} + +func (ed *IATEntryDetail) reservedField() string { + return ed.alphaField(ed.reserved, 13) +} + +// AmountField returns a zero padded string of amount +func (ed *IATEntryDetail) AmountField() string { + return ed.numericField(ed.Amount, 10) +} + +// DFIAccountNumberField gets the DFIAccountNumber with space padding +func (ed *IATEntryDetail) DFIAccountNumberField() string { + return ed.alphaField(ed.DFIAccountNumber, 35) +} + +// reservedTwoField gets the reservedTwo +func (ed *IATEntryDetail) reservedTwoField() string { + return ed.alphaField(ed.reservedTwo, 2) +} + +// OFACSreeningIndicatorField gets the OFACSreeningIndicator +func (ed *IATEntryDetail) OFACSreeningIndicatorField() string { + return ed.alphaField(ed.OFACSreeningIndicator, 1) +} + +// SecondaryOFACSreeningIndicatorField gets the SecondaryOFACSreeningIndicator +func (ed *IATEntryDetail) SecondaryOFACSreeningIndicatorField() string { + return ed.alphaField(ed.SecondaryOFACSreeningIndicator, 1) +} + +// TraceNumberField returns a zero padded TraceNumber string +func (ed *IATEntryDetail) TraceNumberField() string { + return ed.numericField(ed.TraceNumber, 15) +} + +// AddIATAddenda appends an Addendumer to the IATEntryDetail +// Currently this is used to add Addenda17 and Addenda18 IAT Addenda records +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 + default: + ed.Category = CategoryForward + ed.Addendum = append(ed.Addendum, addenda) + return ed.Addendum + } +} diff --git a/iatEntryDetail_test.go b/iatEntryDetail_test.go new file mode 100644 index 000000000..b6ceed1b8 --- /dev/null +++ b/iatEntryDetail_test.go @@ -0,0 +1,502 @@ +// 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 ( + "strings" + "testing" +) + +// mockIATEntryDetail creates an IAT EntryDetail +func mockIATEntryDetail() *IATEntryDetail { + entry := NewIATEntryDetail() + entry.TransactionCode = 22 + entry.SetRDFI("121042882") + entry.AddendaRecords = 007 + entry.DFIAccountNumber = "123456789" + entry.Amount = 100000 // 1000.00 + entry.SetTraceNumber(mockIATBatchHeaderFF().ODFIIdentification, 1) + entry.Category = CategoryForward + return entry +} + +// mockIATEntryDetail2 creates an EntryDetail +func mockIATEntryDetail2() *IATEntryDetail { + entry := NewIATEntryDetail() + entry.TransactionCode = 22 + entry.SetRDFI("121042882") + entry.AddendaRecords = 007 + entry.DFIAccountNumber = "123456789" + entry.Amount = 200000 // 2000.00 + entry.SetTraceNumber(mockIATBatchHeaderFF().ODFIIdentification, 2) + entry.Category = CategoryForward + return entry +} + +// testMockIATEntryDetail validates an IATEntryDetail record +func testMockIATEntryDetail(t testing.TB) { + entry := mockIATEntryDetail() + if err := entry.Validate(); err != nil { + t.Error("mockEntryDetail does not validate and will break other tests") + } + if entry.TransactionCode != 22 { + t.Error("TransactionCode dependent default value has changed") + } + if entry.RDFIIdentification != "12104288" { + t.Error("RDFIIdentification dependent default value has changed") + } + if entry.AddendaRecords != 7 { + t.Error("AddendaRecords default dependent value has changed") + } + if entry.DFIAccountNumber != "123456789" { + t.Error("DFIAccountNumber dependent default value has changed") + } + if entry.Amount != 100000 { + t.Error("Amount dependent default value has changed") + } + if entry.TraceNumber != 231380100000001 { + t.Errorf("TraceNumber dependent default value has changed %v", entry.TraceNumber) + } +} + +// TestMockIATEntryDetail tests validating an IATEntryDetail record +func TestMockIATEntryDetail(t *testing.T) { + testMockIATEntryDetail(t) +} + +// BenchmarkMockIATEntryDetail benchmarks validating an IATEntryDetail record +func BenchmarkIATMockEntryDetail(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testMockIATEntryDetail(b) + } +} + +// testParseIATEntryDetail parses a known IATEntryDetail record string. +func testParseIATEntryDetail(t testing.TB) { + var line = "6221210428820007 000010000012345678901234567890123456789012345 1231380100000001" + r := NewReader(strings.NewReader(line)) + r.addIATCurrentBatch(IATNewBatch(mockIATBatchHeaderFF())) + r.IATCurrentBatch.SetHeader(mockIATBatchHeaderFF()) + r.line = line + if err := r.parseIATEntryDetail(); err != nil { + t.Errorf("%T: %s", err, err) + } + record := r.IATCurrentBatch.GetEntries()[0] + + if record.recordType != "6" { + t.Errorf("RecordType Expected '6' got: %v", record.recordType) + } + if record.TransactionCode != 22 { + t.Errorf("TransactionCode Expected '22' got: %v", record.TransactionCode) + } + if record.RDFIIdentificationField() != "12104288" { + t.Errorf("RDFIIdentification Expected '12104288' got: '%v'", record.RDFIIdentificationField()) + } + + if record.AddendaRecordsField() != "0007" { + t.Errorf("addendaRecords Expected '0007' got: %v", record.AddendaRecords) + } + if record.CheckDigit != "2" { + t.Errorf("CheckDigit Expected '2' got: %v", record.CheckDigit) + } + if record.AmountField() != "0000100000" { + t.Errorf("Amount Expected '0000100000' got: %v", record.AmountField()) + } + if record.DFIAccountNumberField() != "12345678901234567890123456789012345" { + t.Errorf("DfiAccountNumber Expected '12345678901234567890123456789012345' got: %v", record.DFIAccountNumberField()) + } + if record.AddendaRecordIndicator != 1 { + t.Errorf("AddendaRecordIndicator Expected '0' got: %v", record.AddendaRecordIndicator) + } + if record.TraceNumberField() != "231380100000001" { + t.Errorf("TraceNumber Expected '231380100000001' got: %v", record.TraceNumberField()) + } +} + +// TestParseIATEntryDetail tests parsing a known IATEntryDetail record string. +func TestParseIATEntryDetail(t *testing.T) { + testParseIATEntryDetail(t) +} + +// BenchmarkParseIATEntryDetail benchmarks parsing a known IATEntryDetail record string. +func BenchmarkParseIATEntryDetail(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testParseIATEntryDetail(b) + } +} + +// testIATEDString validates that a known parsed entry +// detail can be returned to a string of the same value +func testIATEDString(t testing.TB) { + var line = "6221210428820007 000010000012345678901234567890123456789012345 1231380100000001" + r := NewReader(strings.NewReader(line)) + r.addIATCurrentBatch(IATNewBatch(mockIATBatchHeaderFF())) + r.IATCurrentBatch.SetHeader(mockIATBatchHeaderFF()) + r.line = line + if err := r.parseIATEntryDetail(); err != nil { + t.Errorf("%T: %s", err, err) + } + record := r.IATCurrentBatch.GetEntries()[0] + + if record.String() != line { + t.Errorf("Strings do not match") + } +} + +// TestIATEDString tests validating that a known parsed entry +// detail can be returned to a string of the same value +func TestIATEDString(t *testing.T) { + testIATEDString(t) +} + +// BenchmarkIATEDString benchmarks validating that a known parsed entry +// detail can be returned to a string of the same value +func BenchmarkIATEDString(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATEDString(b) + } +} + +// testIATEDInvalidRecordType validates error for IATEntryDetail invalid recordType +func testIATEDInvalidRecordType(t testing.TB) { + iatEd := mockIATEntryDetail() + iatEd.recordType = "2" + if err := iatEd.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATEDInvalidRecordType tests validating error for IATEntryDetail invalid recordType +func TestIATEDInvalidRecordType(t *testing.T) { + testIATEDInvalidRecordType(t) +} + +// BenchmarkIATEDRecordType benchmarks validating error for IATEntryDetail invalid recordType +func BenchmarkIATEDInvalidRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATEDInvalidRecordType(b) + } +} + +// testIATEDInvalidTransactionCode validates error for IATEntryDetail invalid TransactionCode +func testIATEDInvalidTransactionCode(t testing.TB) { + iatEd := mockIATEntryDetail() + iatEd.TransactionCode = 77 + if err := iatEd.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TransactionCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATEDInvalidTransactionCode tests validating error for IATEntryDetail invalid TransactionCode +func TestIATEDInvalidTransactionCode(t *testing.T) { + testIATEDInvalidTransactionCode(t) +} + +// BenchmarkIATEDTransactionCode benchmarks validating error for IATEntryDetail invalid TransactionCode +func BenchmarkIATEDInvalidTransactionCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATEDInvalidTransactionCode(b) + } +} + +// testEDIATDFIAccountNumberAlphaNumeric validates company identification is alphanumeric +func testEDIATDFIAccountNumberAlphaNumeric(t testing.TB) { + ed := mockIATEntryDetail() + ed.DFIAccountNumber = "®" + if err := ed.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "DFIAccountNumber" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestEDIATDFIAccountNumberAlphaNumeric tests validating company identification is alphanumeric +func TestEDIATDFIAccountNumberAlphaNumeric(t *testing.T) { + testEDIATDFIAccountNumberAlphaNumeric(t) +} + +// BenchmarkEDIATDFIAccountNumberAlphaNumeric benchmarks validating company identification is alphanumeric +func BenchmarkEDIATDFIAccountNumberAlphaNumeric(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testEDIATDFIAccountNumberAlphaNumeric(b) + } +} + +// testEDIATisCheckDigit validates check digit +func testEDIATisCheckDigit(t testing.TB) { + ed := mockIATEntryDetail() + ed.CheckDigit = "1" + if err := ed.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "RDFIIdentification" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestEDIATisCheckDigit tests validating check digit +func TestEDIATisCheckDigit(t *testing.T) { + testEDIATisCheckDigit(t) +} + +// BenchmarkEDIATisCheckDigit benchmarks validating check digit +func BenchmarkEDIATisCheckDigit(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testEDIATisCheckDigit(b) + } +} + +// testEDIATSetRDFII validates setting RDFI +func testEDIATSetRDFI(t testing.TB) { + ed := NewIATEntryDetail() + ed.SetRDFI("810866774") + if ed.RDFIIdentification != "81086677" { + t.Error("RDFI identification") + } + if ed.CheckDigit != "4" { + t.Error("Unexpected check digit") + } +} + +// TestEDIATSetRDFI tests validating setting RDFI +func TestEDIATSetRDFI(t *testing.T) { + testEDIATSetRDFI(t) +} + +// BenchmarkEDIATSetRDFI benchmarks validating setting RDFI +func BenchmarkEDIATSetRDFI(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testEDIATSetRDFI(b) + } +} + +// testValidateEDIATCheckDigit validates CheckDigit error +func testValidateEDIATCheckDigit(t testing.TB) { + ed := mockIATEntryDetail() + ed.CheckDigit = "XYZ" + if err := ed.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "CheckDigit" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestValidateEDIATCheckDigit tests validating validates CheckDigit error +func TestValidateEDIATCheckDigit(t *testing.T) { + testValidateEDIATCheckDigit(t) +} + +// BenchmarkValidateEDIATCheckDigit benchmarks validating CheckDigit error +func BenchmarkValidateEDIATCheckDigit(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testValidateEDIATCheckDigit(b) + } +} + +//FieldInclusion + +// testIATEDRecordType validates IATEntryDetail recordType fieldInclusion +func testIATEDRecordType(t testing.TB) { + iatEd := mockIATEntryDetail() + iatEd.recordType = "" + if err := iatEd.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "recordType" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATEDRecordType tests validating IATEntryDetail recordType fieldInclusion +func TestIATEDRecordType(t *testing.T) { + testIATEDRecordType(t) +} + +// BenchmarkIATEDRecordType benchmarks validating IATEntryDetail recordType fieldInclusion +func BenchmarkIATEDRecordType(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATEDRecordType(b) + } +} + +// testIATEDTransactionCode validates IATEntryDetail TransactionCode fieldInclusion +func testIATEDTransactionCode(t testing.TB) { + iatEd := mockIATEntryDetail() + iatEd.TransactionCode = 0 + if err := iatEd.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TransactionCode" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATEDTransactionCode tests validating IATEntryDetail TransactionCode fieldInclusion +func TestIATEDTransactionCode(t *testing.T) { + testIATEDTransactionCode(t) +} + +// BenchmarkIATEDTransactionCode benchmarks validating IATEntryDetail TransactionCode fieldInclusion +func BenchmarkIATEDTransactionCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATEDTransactionCode(b) + } +} + +// testIATEDRDFIIdentification validates IATEntryDetail RDFIIdentification fieldInclusion +func testIATEDRDFIIdentification(t testing.TB) { + iatEd := mockIATEntryDetail() + iatEd.RDFIIdentification = "" + if err := iatEd.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "RDFIIdentification" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATEDRDFIIdentification tests validating IATEntryDetail RDFIIdentification fieldInclusion +func TestIATEDRDFIIdentification(t *testing.T) { + testIATEDRDFIIdentification(t) +} + +// BenchmarkIATEDRDFIIdentification benchmarks validating IATEntryDetail RDFIIdentification fieldInclusion +func BenchmarkIATEDRDFIIdentification(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATEDRDFIIdentification(b) + } +} + +// testIATEDAddendaRecords validates IATEntryDetail AddendaRecords fieldInclusion +func testIATEDAddendaRecords(t testing.TB) { + iatEd := mockIATEntryDetail() + iatEd.AddendaRecords = 0 + if err := iatEd.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "AddendaRecords" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATEDAddendaRecords tests validating IATEntryDetail AddendaRecords fieldInclusion +func TestIATEDAddendaRecords(t *testing.T) { + testIATEDAddendaRecords(t) +} + +// BenchmarkIATEDAddendaRecords benchmarks validating IATEntryDetail AddendaRecords fieldInclusion +func BenchmarkIATEDAddendaRecords(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATEDAddendaRecords(b) + } +} + +// testIATEDDFIAccountNumber validates IATEntryDetail DFIAccountNumber fieldInclusion +func testIATEDDFIAccountNumber(t testing.TB) { + iatEd := mockIATEntryDetail() + iatEd.DFIAccountNumber = "" + if err := iatEd.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "DFIAccountNumber" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATEDDFIAccountNumber tests validating IATEntryDetail DFIAccountNumber fieldInclusion +func TestIATEDDFIAccountNumber(t *testing.T) { + testIATEDDFIAccountNumber(t) +} + +// BenchmarkIATEDDFIAccountNumber benchmarks validating IATEntryDetail DFIAccountNumber fieldInclusion +func BenchmarkIATEDDFIAccountNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATEDDFIAccountNumber(b) + } +} + +// testIATEDTraceNumber validates IATEntryDetail TraceNumber fieldInclusion +func testIATEDTraceNumber(t testing.TB) { + iatEd := mockIATEntryDetail() + iatEd.TraceNumber = 0 + if err := iatEd.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "TraceNumber" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATEDTraceNumber tests validating IATEntryDetail TraceNumber fieldInclusion +func TestIATEDTraceNumber(t *testing.T) { + testIATEDTraceNumber(t) +} + +// BenchmarkIATEDTraceNumber benchmarks validating IATEntryDetail TraceNumber fieldInclusion +func BenchmarkIATEDTraceNumber(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATEDTraceNumber(b) + } +} + +// testIATEDAddendaRecordIndicator validates IATEntryDetail AddendaIndicator fieldInclusion +func testIATEDAddendaRecordIndicator(t testing.TB) { + iatEd := mockIATEntryDetail() + iatEd.AddendaRecordIndicator = 0 + if err := iatEd.Validate(); err != nil { + if e, ok := err.(*FieldError); ok { + if e.FieldName != "AddendaRecordIndicator" { + t.Errorf("%T: %s", err, err) + } + } + } +} + +// TestIATEDAddendaRecordIndicator tests validating IATEntryDetail AddendaRecordIndicator fieldInclusion +func TestIATEDAddendaRecordIndicator(t *testing.T) { + testIATEDAddendaRecordIndicator(t) +} + +// BenchmarkIATEDAddendaRecordIndicator benchmarks validating IATEntryDetail AddendaRecordIndicator fieldInclusion +func BenchmarkIATEDAddendaRecordIndicator(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATEDAddendaRecordIndicator(b) + } +} diff --git a/reader.go b/reader.go index e3f7c2202..3af43a330 100644 --- a/reader.go +++ b/reader.go @@ -36,6 +36,8 @@ type Reader struct { line string // currentBatch is the current Batch entries being parsed currentBatch Batcher + // IATCurrentBatch is the current IATBatch entries being parsed + IATCurrentBatch IATBatch // line number of the file being parsed lineNum int // recordName holds the current record name being parsed. @@ -57,6 +59,12 @@ func (r *Reader) addCurrentBatch(batch Batcher) { r.currentBatch = batch } +// addCurrentBatch creates the current batch type for the file being read. A successful +// current batch will be added to r.File once parsed. +func (r *Reader) addIATCurrentBatch(iatBatch IATBatch) { + r.IATCurrentBatch = iatBatch +} + // NewReader returns a new ACH Reader that reads from r. func NewReader(r io.Reader) *Reader { return &Reader{ @@ -128,27 +136,36 @@ func (r *Reader) parseLine() error { return err } case batchHeaderPos: - if err := r.parseBatchHeader(); err != nil { + if err := r.parseBH(); err != nil { return err } case entryDetailPos: - if err := r.parseEntryDetail(); err != nil { + if err := r.parseED(); err != nil { return err } case entryAddendaPos: - if err := r.parseAddenda(); err != nil { + if err := r.parseEDAddenda(); err != nil { return err } case batchControlPos: if err := r.parseBatchControl(); err != nil { return err } - if err := r.currentBatch.Validate(); err != nil { - r.recordName = "Batches" - return r.error(err) + if r.currentBatch != nil { + if err := r.currentBatch.Validate(); err != nil { + r.recordName = "Batches" + return r.error(err) + } + r.File.AddBatch(r.currentBatch) + r.currentBatch = nil + } else { + if err := r.IATCurrentBatch.Validate(); err != nil { + r.recordName = "Batches" + return r.error(err) + } + r.File.AddIATBatch(r.IATCurrentBatch) + r.IATCurrentBatch = IATBatch{} } - r.File.AddBatch(r.currentBatch) - r.currentBatch = nil case fileControlPos: if r.line[:2] == "99" { // final blocking padding @@ -164,6 +181,53 @@ func (r *Reader) parseLine() error { return nil } +// parseBH parses determines whether to parse an IATBatchHeader or BatchHeader +func (r *Reader) parseBH() error { + if r.line[50:53] == "IAT" { + if err := r.parseIATBatchHeader(); err != nil { + return err + } + } else { + if err := r.parseBatchHeader(); err != nil { + return err + } + } + return nil +} + +// parseEd parses determines whether to parse an IATEntryDetail or EntryDetail +func (r *Reader) parseED() error { + // ToDo: Review if this can be true for domestic files. Also this field may be + // ToDo: used for IAT Corrections so consider using another field + // IATIndicator field + if r.line[16:29] == " " { + if err := r.parseIATEntryDetail(); err != nil { + return err + } + } else { + if err := r.parseEntryDetail(); err != nil { + return err + } + } + return nil +} + +// 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 { + return err + } + default: + if err := r.parseAddenda(); err != nil { + return err + } + } + return nil +} + // parseFileHeader takes the input record string and parses the FileHeaderRecord values func (r *Reader) parseFileHeader() error { r.recordName = "FileHeader" @@ -207,6 +271,7 @@ func (r *Reader) parseBatchHeader() error { // parseEntryDetail takes the input record string and parses the EntryDetailRecord values func (r *Reader) parseEntryDetail() error { r.recordName = "EntryDetail" + if r.currentBatch == nil { return r.error(&FileError{Msg: msgFileBatchOutside}) } @@ -234,6 +299,7 @@ func (r *Reader) parseAddenda() error { entry := r.currentBatch.GetEntries()[entryIndex] if entry.AddendaRecordIndicator == 1 { + switch r.line[1:3] { case "02": addenda02 := NewAddenda02() @@ -275,13 +341,22 @@ func (r *Reader) parseAddenda() error { // parseBatchControl takes the input record string and parses the BatchControlRecord values func (r *Reader) parseBatchControl() error { r.recordName = "BatchControl" - if r.currentBatch == nil { + if r.currentBatch == nil && r.IATCurrentBatch.GetEntries() == nil { // batch Control without a current batch return r.error(&FileError{Msg: msgFileBatchOutside}) } - r.currentBatch.GetControl().Parse(r.line) - if err := r.currentBatch.GetControl().Validate(); err != nil { - return r.error(err) + + if r.currentBatch != nil { + r.currentBatch.GetControl().Parse(r.line) + if err := r.currentBatch.GetControl().Validate(); err != nil { + return r.error(err) + } + } else { + r.IATCurrentBatch.GetControl().Parse(r.line) + if err := r.IATCurrentBatch.GetControl().Validate(); err != nil { + return r.error(err) + } + } return nil } @@ -299,3 +374,142 @@ func (r *Reader) parseFileControl() error { } return nil } + +// IAT specific reader functions + +// parseIATBatchHeader takes the input record string and parses the FileHeaderRecord values +func (r *Reader) parseIATBatchHeader() error { + r.recordName = "BatchHeader" + if r.IATCurrentBatch.Header != nil { + // batch header inside of current batch + return r.error(&FileError{Msg: msgFileBatchInside}) + } + + // Ensure we have a valid IAT BatchHeader before building a batch. + bh := NewIATBatchHeader() + bh.Parse(r.line) + if err := bh.Validate(); err != nil { + return r.error(err) + } + + // Passing BatchHeader into NewBatchIAT creates a Batcher of IAT SEC code type. + iatBatch := IATNewBatch(bh) + + r.addIATCurrentBatch(iatBatch) + + return nil +} + +// parseIATEntryDetail takes the input record string and parses the EntryDetailRecord values +func (r *Reader) parseIATEntryDetail() error { + r.recordName = "EntryDetail" + + if r.IATCurrentBatch.Header == nil { + return r.error(&FileError{Msg: msgFileBatchOutside}) + } + + ed := new(IATEntryDetail) + ed.Parse(r.line) + if err := ed.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.AddEntry(ed) + return nil +} + +// parseIATAddenda takes the input record string and create an Addenda Type appended to the last EntryDetail +func (r *Reader) parseIATAddenda() error { + r.recordName = "Addenda" + + if r.IATCurrentBatch.GetEntries() == nil { + msg := fmt.Sprint(msgFileBatchOutside) + return r.error(&FileError{FieldName: "Addenda", Msg: msg}) + } + if len(r.IATCurrentBatch.GetEntries()) == 0 { + return r.error(&FileError{FieldName: "Addenda", Msg: msgFileBatchOutside}) + } + entryIndex := len(r.IATCurrentBatch.GetEntries()) - 1 + entry := r.IATCurrentBatch.GetEntries()[entryIndex] + + if entry.AddendaRecordIndicator == 1 { + err := r.switchIATAddenda(entryIndex) + if err != nil { + return r.error(err) + } + } else { + msg := fmt.Sprint(msgIATBatchAddendaIndicator) + return r.error(&FileError{FieldName: "AddendaRecordIndicator", Msg: msg}) + } + + return nil +} + +func (r *Reader) switchIATAddenda(entryIndex int) error { + switch r.line[1:3] { + case "10": + addenda10 := NewAddenda10() + addenda10.Parse(r.line) + if err := addenda10.Validate(); err != nil { + return err + } + r.IATCurrentBatch.GetEntries()[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 + case "12": + addenda12 := NewAddenda12() + addenda12.Parse(r.line) + if err := addenda12.Validate(); err != nil { + return err + } + r.IATCurrentBatch.GetEntries()[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 + case "14": + addenda14 := NewAddenda14() + addenda14.Parse(r.line) + if err := addenda14.Validate(); err != nil { + return err + } + r.IATCurrentBatch.GetEntries()[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 + case "16": + addenda16 := NewAddenda16() + addenda16.Parse(r.line) + if err := addenda16.Validate(); err != nil { + return err + } + r.IATCurrentBatch.GetEntries()[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) + case "18": + addenda18 := NewAddenda18() + addenda18.Parse(r.line) + if err := addenda18.Validate(); err != nil { + return err + } + r.IATCurrentBatch.GetEntries()[entryIndex].AddIATAddenda(addenda18) + } + return nil +} diff --git a/reader_test.go b/reader_test.go index d0e57a14c..4e51a9807 100644 --- a/reader_test.go +++ b/reader_test.go @@ -998,3 +998,423 @@ func BenchmarkFileFHImmediateOrigin(b *testing.B) { testFileFHImmediateOrigin(b) } } + +// testACHFileRead validates reading a file with PPD and IAT entries +func testACHFileRead(t testing.TB) { + f, err := os.Open("./test/data/20110805A.ach") + if err != nil { + t.Errorf("%T: %s", err, err) + } + defer f.Close() + r := NewReader(f) + _, err = r.Read() + + if err != nil { + if p, ok := err.(*ParseError); ok { + if e, ok := p.Err.(*BatchError); ok { + if e.FieldName != "entries" { + t.Errorf("%T: %s", e, e) + } + } + } else { + t.Errorf("%T: %s", err, err) + } + } + + err2 := r.File.Validate() + + if err2 != nil { + if e, ok := err2.(*FileError); ok { + if e.FieldName != "BatchCount" { + t.Errorf("%T: %s", e, e) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestACHFileRead tests validating reading a file with PPD and IAT entries +func TestACHFileRead(t *testing.T) { + testACHFileRead(t) +} + +// BenchmarkACHFileRead benchmarks validating reading a file with PPD and IAT entries +func BenchmarkACHFileRead(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testACHFileRead(b) + } +} + +// testACHFileRead2 validates reading a file with PPD and IAT entries +func testACHFileRead2(t testing.TB) { + f, err := os.Open("./test/data/20110729A.ach") + if err != nil { + t.Errorf("%T: %s", err, err) + } + defer f.Close() + r := NewReader(f) + _, err = r.Read() + + if err != nil { + if p, ok := err.(*ParseError); ok { + if e, ok := p.Err.(*BatchError); ok { + if e.FieldName != "entries" { + t.Errorf("%T: %s", e, e) + } + } + } else { + t.Errorf("%T: %s", err, err) + } + } + + err2 := r.File.Validate() + + if err2 != nil { + if e, ok := err2.(*FileError); ok { + if e.FieldName != "BatchCount" { + t.Errorf("%T: %s", e, e) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestACHFileRead2 tests validating reading a file with PPD and IAT entries +func TestACHFileRead2(t *testing.T) { + testACHFileRead2(t) +} + +// BenchmarkACHFileRead2 benchmarks validating reading a file with PPD and IAT entries +func BenchmarkACHFileRead2(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testACHFileRead2(b) + } +} + +// testACHFileRead3 validates reading a file with IAT entries only +func testACHFileRead3(t testing.TB) { + f, err := os.Open("./test/data/20180713-IAT.ach") + if err != nil { + t.Errorf("%T: %s", err, err) + } + defer f.Close() + r := NewReader(f) + _, err = r.Read() + + if err != nil { + if p, ok := err.(*ParseError); ok { + if e, ok := p.Err.(*BatchError); ok { + if e.FieldName != "entries" { + t.Errorf("%T: %s", e, e) + } + } + } else { + t.Errorf("%T: %s", err, err) + } + } + + err2 := r.File.Validate() + + if err2 != nil { + if e, ok := err2.(*FileError); ok { + if e.FieldName != "BatchCount" { + t.Errorf("%T: %s", e, e) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestACHFileRead3 tests validating reading a file with IAT entries that only +func TestACHFileRead3(t *testing.T) { + testACHFileRead3(t) +} + +// BenchmarkACHFileRead3 benchmarks validating reading a file with IAT entries only +func BenchmarkACHFileRead3(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testACHFileRead3(b) + } +} + +// testACHIATAddenda17 validates reading a file with IAT and Addenda17 entries +func testACHIATAddenda17(t testing.TB) { + f, err := os.Open("./test/data/20180716-IAT-A17.ach") + if err != nil { + t.Errorf("%T: %s", err, err) + } + defer f.Close() + r := NewReader(f) + _, err = r.Read() + + if err != nil { + if p, ok := err.(*ParseError); ok { + if e, ok := p.Err.(*BatchError); ok { + if e.FieldName != "entries" { + t.Errorf("%T: %s", e, e) + } + } + } else { + t.Errorf("%T: %s", err, err) + } + } + + err2 := r.File.Validate() + + if err2 != nil { + if e, ok := err2.(*FileError); ok { + if e.FieldName != "BatchCount" { + t.Errorf("%T: %s", e, e) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestACHIATAddenda17 tests validating reading a file with IAT and Addenda17 entries that +func TestACHIATAddenda17(t *testing.T) { + testACHIATAddenda17(t) +} + +// BenchmarkACHIATAddenda17 benchmarks validating reading a file with IAT and Addenda17 entries +func BenchmarkACHIATAddenda17(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testACHIATAddenda17(b) + } +} + +// testACHIATAddenda1718 validates reading a file with IAT and Addenda17 and Addenda18 entries +func testACHIATAddenda1718(t testing.TB) { + f, err := os.Open("./test/data/20180716-IAT-A17-A18.ach") + if err != nil { + t.Errorf("%T: %s", err, err) + } + defer f.Close() + r := NewReader(f) + _, err = r.Read() + + if err != nil { + if p, ok := err.(*ParseError); ok { + if e, ok := p.Err.(*BatchError); ok { + if e.FieldName != "entries" { + t.Errorf("%T: %s", e, e) + } + } + } else { + t.Errorf("%T: %s", err, err) + } + } + + err2 := r.File.Validate() + + if err2 != nil { + if e, ok := err2.(*FileError); ok { + if e.FieldName != "BatchCount" { + t.Errorf("%T: %s", e, e) + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestACHIATAddenda1718 tests validating reading a file with IAT and Addenda17 and Addenda18 entries +func TestACHIATAddenda1718(t *testing.T) { + testACHIATAddenda1718(t) +} + +// BenchmarkACHIATAddenda17 benchmarks validating reading a file with IAT Addenda17 and Addenda18 entries +func BenchmarkACHIATAddenda1718(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testACHIATAddenda1718(b) + } +} + +// testACHFileIATBatchHeader validates error when reading an invalid IATBatchHeader +func testACHFileIATBatchHeader(t testing.TB) { + f, err := os.Open("./test/data/IAT-InvalidBatchHeader.ach") + if err != nil { + t.Errorf("%T: %s", err, err) + } + defer f.Close() + r := NewReader(f) + _, err = r.Read() + + if err != nil { + if p, ok := err.(*ParseError); ok { + if e, ok := p.Err.(*FieldError); ok { + if e.FieldName != "ServiceClassCode" { + t.Errorf("%T: %s", e, e) + } + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestACHFileIATBatchHeader tests validating error when reading an invalid IATBatchHeader +func TestACHFileIATBatchHeader(t *testing.T) { + testACHFileIATBatchHeader(t) +} + +// BenchmarkACHFileIATBatchHeader benchmarks validating error when reading an invalid IATBatchHeader +func BenchmarkACHFileIATBatchHeader(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testACHFileIATBatchHeader(b) + } +} + +// testACHFileIATEntryDetail validates error when reading an invalid IATEntryDetail +func testACHFileIATEntryDetail(t testing.TB) { + f, err := os.Open("./test/data/IAT-InvalidEntryDetail.ach") + if err != nil { + t.Errorf("%T: %s", err, err) + } + defer f.Close() + r := NewReader(f) + _, err = r.Read() + + if err != nil { + if p, ok := err.(*ParseError); ok { + if e, ok := p.Err.(*FieldError); ok { + if e.FieldName != "TransactionCode" { + t.Errorf("%T: %s", e, e) + } + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestACHFileIATEntryDetail tests validating error when reading an invalid IATEntryDetail +func TestACHFileIATEntryDetail(t *testing.T) { + testACHFileIATEntryDetail(t) +} + +// BenchmarkACHFileIATEntryDetail benchmarks validating error when reading an invalid IATEntryDetail +func BenchmarkACHFileIATEntryDetail(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testACHFileIATEntryDetail(b) + } +} + +// testACHFileIATAddenda10 validates error when reading an invalid IATAddenda10 +func testACHFileIATAddenda10(t testing.TB) { + f, err := os.Open("./test/data/IAT-InvalidAddenda10.ach") + if err != nil { + t.Errorf("%T: %s", err, err) + } + defer f.Close() + r := NewReader(f) + _, err = r.Read() + + if err != nil { + if p, ok := err.(*ParseError); ok { + if e, ok := p.Err.(*FieldError); ok { + if e.FieldName != "TransactionTypeCode" { + t.Errorf("%T: %s", e, e) + } + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestACHFileIATAddenda10 tests validating error when reading an invalid IATAddenda10 +func TestACHFileIATAddenda10(t *testing.T) { + testACHFileIATAddenda10(t) +} + +// BenchmarkACHFileIATAddenda10 benchmarks validating error when reading an invalid IATAddenda10 +func BenchmarkACHFileIATAddenda10(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testACHFileIATAddenda10(b) + } +} + +// testACHFileIATBC validates error when reading an invalid IAT Batch Control +func testACHFileIATBC(t testing.TB) { + f, err := os.Open("./test/data/IAT-InvalidBatchControl.ach") + if err != nil { + t.Errorf("%T: %s", err, err) + } + defer f.Close() + r := NewReader(f) + _, err = r.Read() + + if err != nil { + if p, ok := err.(*ParseError); ok { + if e, ok := p.Err.(*BatchError); ok { + if e.FieldName != "ODFIIdentification" { + t.Errorf("%T: %s", e, e) + } + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestACHFileIATBC tests validating error when reading an invalid IAT Batch Control +func TestACHFileIATBC(t *testing.T) { + testACHFileIATBC(t) +} + +// BenchmarkACHFileIATBC benchmarks validating error when reading an invalid IAT Batch Control +func BenchmarkACHFileIATBC(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testACHFileIATBC(b) + } +} + +// testACHFileIATBH validates error when reading an invalid IAT Batch Header +func testACHFileIATBH(t testing.TB) { + f, err := os.Open("./test/data/IAT-BatchHeaderErr.ach") + if err != nil { + t.Errorf("%T: %s", err, err) + } + defer f.Close() + r := NewReader(f) + _, err = r.Read() + + if err != nil { + if p, ok := err.(*ParseError); ok { + if e, ok := p.Err.(*FileError); ok { + if e.Msg != msgFileBatchInside { + t.Errorf("%T: %s", e, e) + } + } + } else { + t.Errorf("%T: %s", err, err) + } + } +} + +// TestACHFileIATBH tests validating error when reading an invalid IAT Batch Header +func TestACHFileIATBH(t *testing.T) { + testACHFileIATBH(t) +} + +// BenchmarkACHFileIATBH benchmarks validating error when reading an invalid IAT Batch Header +func BenchmarkACHFileIATBH(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testACHFileIATBH(b) + } +} diff --git a/test/data/20110729A.ach b/test/data/20110729A.ach index e2153b518..8ed5b6a9d 100644 --- a/test/data/20110729A.ach +++ b/test/data/20110729A.ach @@ -1,175 +1,173 @@ 101 04200001312345678981107291600A094101US BANK NA TEST COMPANY -5225TEST COMPANY 11234567898PPDTEST BUYS 110801110801 1042000010000001 +5225TEST COMPANY 11234567898PPDTEST BUYS 110801110801 1098765430000001 627021200025998412345 0011900000OA313 AYDEN DERS 0098765434200001 -627021200025998412345 0009900000OA235 DYLAN HUNTER 0098765434200001 -627021200025998412345 0015100000OA357 COLTON LOWE 0098765434200001 -627021200025998412345 0026600000OA264 ISAIAH CARPENTER 0098765434200001 -627021200025998412345 0025500000OA363 COOPER BARNETT 0098765434200001 -627021200025998412345 0022800000OA298 KATHERINE GRAVES 0098765434200001 -627021200025998412345 0016100000OA332 JORDAN FRANKLIN 0098765434200001 -627021200025998412345 0022400000OA270 JACOB SIMPSON 0098765434200001 -627021200025998412345 0008900000OA268 ADAM WILLIS 0098765434200001 -627021200025998412345 0026600000OA354 ISAIAH CARPENTER 0098765434200001 -627021200025998412345 0026600000OA333 ISAIAH CARPENTER 0098765434200001 -627021200025998412345 0029700000OA250 JOSE JORDAN 0098765434200001 -627021200025998412345 0026900000OA300 JULIAN PERKINS 0098765434200001 -627021200025998412345 0029000000OA359 JACK GAS 0098765434200001 -627021200025998412345 0029900000OA281 SOFIA WATSON 0098765434200001 -627021200025998412345 0023800000OA316 ARIANNA KING 0098765434200001 -627021200025998412345 0029400000OA292 AYDEN SIMPSON 0098765434200001 -627021200025998412345 0024700000OA319 SOPHIE HENRY 0098765434200001 -627021200025998412345 0005900000OA267 MAKAYLA TERRY 0098765434200001 -627021200025998412345 0016100000OA257 JORDAN FRANKLIN 0098765434200001 -627021200025998412345 0016600000OA349 JASMINE CARTER 0098765434200001 -627021200025998412345 0015800000OA352 JACOB LEE 0098765434200001 -627021200025998412345 0022900000OA233 LIAM RTINEZ 0098765434200001 -627021200025998412345 0018200000OA283 LILY ROBERTSON 0098765434200001 -627021200025998412345 0028200000OA360 MORGAN NELSON 0098765434200001 -627021200025998412345 0018700000OA345 LUCAS THOMPSON 0098765434200001 -627021200025998412345 0022800000OA368 NOAH 0098765434200001 -627021200025998412345 0007600000OA340 ARIANNA LONG 0098765434200001 -627021200025998412345 0029000000OA338 JACK GAS 0098765434200001 -627021200025998412345 0020700000OA272 MORGAN FOX 0098765434200001 -627021200025998412345 0025500000OA342 COOPER BARNETT 0098765434200001 -627021200025998412345 0018500000OA224 JESSICA SILVA 0098765434200001 -627021200025998412345 0007800000OA273 JASMINE CARTER 0098765434200001 -627021200025998412345 0011700000OA299 ISABELLE ERRY 0098765434200001 -627021200025998412345 0006300000OA263 ZACHARY MEDINA 0098765434200001 -627021200025998412345 0001800000OA266 MATTHEW ALLEN 0098765434200001 -627021200025998412345 0015000000OA312 BROOKE SUTTON 0098765434200001 -627021200025998412345 0006600000OA242 ABIGAIL EVANS 0098765434200001 -627021200025998412345 0028000000OA304 ISAAC JORDAN 0098765434200001 -627021200025998412345 0007800000OA269 JUAN GILBERT 0098765434200001 -627021200025998412345 0020800000OA249 KEVIN CASTILLO 0098765434200001 -627021200025998412345 0010600000OA311 EVAN HARVEY 0098765434200001 -627021200025998412345 0019500000OA318 JESUS BRADLEY 0098765434200001 -627021200025998412345 0008100000OA260 MARIAH HERNANDEZ 0098765434200001 -627021200025998412345 0007600000OA361 ARIANNA LONG 0098765434200001 -627021200025998412345 0003700000OA265 ARIANNA BISHOP 0098765434200001 -627021200025998412345 0005900000OA254 CARLOS WILLIS 0098765434200001 -627021200025998412345 0025800000OA337 ISABELLA GREGORY 0098765434200001 -627021200025998412345 0011600000OA307 ALLISON SUTTON 0098765434200001 -627021200025998412345 0016200000OA290 COOPER LEZ 0098765434200001 -627021200025998412345 0010000000OA335 SOPHIA BAILEY 0098765434200001 -627021200025998412345 0017700000OA288 CHRISTIAN PEARSON 0098765434200001 -627021200025998412345 0028000000OA367 ISAAC JORDAN 0098765434200001 -627021200025998412345 0026100000OA244 LUKE CRAIG 0098765434200001 -627021200025998412345 0008300000OA278 GRACE HOWARD 0098765434200001 -627021200025998412345 0020300000OA228 SOPHIE NICHOLS 0098765434200001 -627021200025998412345 0027400000OA275 ALEXA WALKER 0098765434200001 -627021200025998412345 0006700000OA310 ADRIAN HOLLAND 0098765434200001 -627021200025998412345 0015400000OA314 GABRIEL MEDINA 0098765434200001 -627021200025998412345 0021600000OA344 KEVIN WOOD 0098765434200001 -627021200025998412345 0004700000OA262 KADEN POWELL 0098765434200001 -627021200025998412345 0009300000OA232 AVA GIBSON 0098765434200001 -627021200025998412345 0018700000OA303 LUCAS THOMPSON 0098765434200001 -627021200025998412345 0008500000OA308 ANGEL MCKINNEY 0098765434200001 -627021200025998412345 0002100000OA274 SEAN BRADLEY 0098765434200001 -627021200025998412345 0019100000OA245 IAN HOLLAND 0098765434200001 -627021200025998412345 0017500000OA323 ISABELLE FOX 0098765434200001 -627021200025998412345 0027400000OA355 ALEXA WALKER 0098765434200001 -627021200025998412345 0026100000OA226 JASON ANDERSON 0098765434200001 -627021200025998412345 0008600000OA243 GIANNA RUIZ 0098765434200001 -627021200025998412345 0011900000OA348 AYDEN DERS 0098765434200001 -627021200025998412345 0015100000OA336 COLTON LOWE 0098765434200001 -627021200025998412345 0002200000OA239 ARIANA THOMPSON 0098765434200001 -627021200025998412345 0025500000OA294 COOPER BARNETT 0098765434200001 -627021200025998412345 0028400000OA325 CAMILA HOLMES 0098765434200001 -627021200025998412345 0025800000OA358 ISABELLA GREGORY 0098765434200001 -627021200025998412345 0025800000OA259 HUNTER MILLS 0098765434200001 -627021200025998412345 0010000000OA279 SOPHIA BAILEY 0098765434200001 -627021200025998412345 0025600000OA293 ANGEL ORTIZ 0098765434200001 -627021200025998412345 0004600000OA252 JOSIAH GRIFFIN 0098765434200001 -627021200025998412345 0011200000OA364 SEBASTIAN MCDONALD 0098765434200001 -627021200025998412345 0022800000OA309 NOAH 0098765434200001 -627021200025998412345 0020500000OA324 AUBREY GRANT 0098765434200001 -627021200025998412345 0022800000OA256 KEVIN PERKINS 0098765434200001 -627021200025998412345 0014600000OA238 HENRY DAY 0098765434200001 -627021200025998412345 0011200000OA297 SEBASTIAN MCDONALD 0098765434200001 -627021200025998412345 0021100000OA282 CARLOS GREGORY 0098765434200001 -627021200025998412345 0009600000OA350 JACOB MORRISON 0098765434200001 -627021200025998412345 0009000000OA291 NATALIE JACOBS 0098765434200001 -627021200025998412345 0025800000OA285 ISABELLA GREGORY 0098765434200001 -627021200025998412345 0028200000OA339 MORGAN NELSON 0098765434200001 -627021200025998412345 0016100000OA353 JORDAN FRANKLIN 0098765434200001 -627021200025998412345 0016400000OA322 ELI BOWMAN 0098765434200001 -627021200025998412345 0003700000OA253 ANGEL SMITH 0098765434200001 -627021200025998412345 0026300000OA247 DIEGO WASHINGTON 0098765434200001 -627021200025998412345 0001900000OA328 ANGEL BROOKS 0098765434200001 -627021200025998412345 0015100000OA236 BAILEY PEREZ 0098765434200001 -627021200025998412345 0021600000OA301 KEVIN WOOD 0098765434200001 -627021200025998412345 0015800000OA331 JACOB LEE 0098765434200001 -627021200025998412345 0011200000OA343 SEBASTIAN MCDONALD 0098765434200001 -627021200025998412345 0026100000OA330 LUKE CRAIG 0098765434200001 -627021200025998412345 0016000000OA280 ABIGAIL ELTON 0098765434200001 -627021200025998412345 0010000000OA356 SOPHIA BAILEY 0098765434200001 -627021200025998412345 0029400000OA251 JAYDEN LYNCH 0098765434200001 -627021200025998412345 0011600000OA321 DESTINY MCDANIEL 0098765434200001 -627021200025998412345 0016600000OA315 JASMINE CARTER 0098765434200001 -627021200025998412345 0004000000OA306 MARY MARSHALL 0098765434200001 -627021200025998412345 0025600000OA362 ANGEL ORTIZ 0098765434200001 -627021200025998412345 0020800000OA258 SYDNEY REYES 0098765434200001 -627021200025998412345 0022800000OA347 NOAH 0098765434200001 -627021200025998412345 0006100000OA248 SYDNEY BUTLER 0098765434200001 -627021200025998412345 0015500000OA327 SOFIA GOMEZ 0098765434200001 -627021200025998412345 0017100000OA240 MARIA MITCHELL 0098765434200001 -627021200025998412345 0029500000OA241 MICHAEL WALLACE 0098765434200001 -627021200025998412345 0015100000OA305 HAYDEN DES 0098765434200001 -627021200025998412345 0006100000OA317 CAMILA T 0098765434200001 -627021200025998412345 0025600000OA341 ANGEL ORTIZ 0098765434200001 -627021200025998412345 0003600000OA329 LILY COLE 0098765434200001 -627021200025998412345 0012700000OA229 BRANDON WILSON 0098765434200001 -627021200025998412345 0018300000OA230 CONNOR GARDNER 0098765434200001 -627021200025998412345 0028200000OA287 MORGAN NELSON 0098765434200001 -627021200025998412345 0021600000OA365 KEVIN WOOD 0098765434200001 -627021200025998412345 0030000000OA326 ISABEL WEBB 0098765434200001 -627021200025998412345 0025600000OA261 MATTHEW ROSE 0098765434200001 -627021200025998412345 0029000000OA286 JACK GAS 0098765434200001 -627021200025998412345 0027400000OA302 JOSEPH DES 0098765434200001 -627021200025998412345 0013500000OA227 SARA NICHOLS 0098765434200001 -627021200025998412345 0027400000OA334 ALEXA WALKER 0098765434200001 -627021200025998412345 0007900000OA225 AIDEN MILES 0098765434200001 -627021200025998412345 0018700000OA366 LUCAS THOMPSON 0098765434200001 -627021200025998412345 0018000000OA234 CONNOR WILLIAMS 0098765434200001 -627021200025998412345 0009500000OA276 GABRIEL JENSEN 0098765434200001 -627021200025998412345 0015000000OA295 JAMES NEWMAN 0098765434200001 -627021200025998412345 0009600000OA320 JACOB MORRISON 0098765434200001 -627021200025998412345 0015800000OA255 JACOB LEE 0098765434200001 -627021200025998412345 0015100000OA284 COLTON LOWE 0098765434200001 -627021200025998412345 0013000000OA271 JUAN NICHOLS 0098765434200001 -627021200025998412345 0008200000OA237 ZOEY DAVIDSON 0098765434200001 -627021200025998412345 0013100000OA296 MARIA FLORES 0098765434200001 -627021200025998412345 0014000000OA277 SYDNEY TUCKER 0098765434200001 -627021200025998412345 0007600000OA289 ARIANNA LONG 0098765434200001 -627021200025998412345 0028000000OA346 ISAAC JORDAN 0098765434200001 -627021200025998412345 0005900000OA246 JESSICA WALKER 0098765434200001 -627021200025998412345 0008700000OA231 ELIZABETH COLE 0098765434200001 -627021200025998412345 0026100000OA351 LUKE CRAIG 0098765434200001 -822500014503074002900000247980000000000000001234567898 042000010000001 -5220TEST COMPANY 11234567898PPDVERIFY 110801110801 1042000010000003 +627021200025998412345 0009900000OA235 DYLAN HUNTER 0098765434200002 +627021200025998412345 0015100000OA357 COLTON LOWE 0098765434200003 +627021200025998412345 0026600000OA264 ISAIAH CARPENTER 0098765434200004 +627021200025998412345 0025500000OA363 COOPER BARNETT 0098765434200005 +627021200025998412345 0022800000OA298 KATHERINE GRAVES 0098765434200006 +627021200025998412345 0016100000OA332 JORDAN FRANKLIN 0098765434200007 +627021200025998412345 0022400000OA270 JACOB SIMPSON 0098765434200008 +627021200025998412345 0008900000OA268 ADAM WILLIS 0098765434200009 +627021200025998412345 0026600000OA354 ISAIAH CARPENTER 0098765434200010 +627021200025998412345 0026600000OA333 ISAIAH CARPENTER 0098765434200011 +627021200025998412345 0029700000OA250 JOSE JORDAN 0098765434200012 +627021200025998412345 0026900000OA300 JULIAN PERKINS 0098765434200013 +627021200025998412345 0029000000OA359 JACK GAS 0098765434200014 +627021200025998412345 0029900000OA281 SOFIA WATSON 0098765434200015 +627021200025998412345 0023800000OA316 ARIANNA KING 0098765434200016 +627021200025998412345 0029400000OA292 AYDEN SIMPSON 0098765434200017 +627021200025998412345 0024700000OA319 SOPHIE HENRY 0098765434200018 +627021200025998412345 0005900000OA267 MAKAYLA TERRY 0098765434200019 +627021200025998412345 0016100000OA257 JORDAN FRANKLIN 0098765434200020 +627021200025998412345 0016600000OA349 JASMINE CARTER 0098765434200021 +627021200025998412345 0015800000OA352 JACOB LEE 0098765434200022 +627021200025998412345 0022900000OA233 LIAM RTINEZ 0098765434200023 +627021200025998412345 0018200000OA283 LILY ROBERTSON 0098765434200024 +627021200025998412345 0028200000OA360 MORGAN NELSON 0098765434200025 +627021200025998412345 0018700000OA345 LUCAS THOMPSON 0098765434200026 +627021200025998412345 0022800000OA368 NOAH 0098765434200027 +627021200025998412345 0007600000OA340 ARIANNA LONG 0098765434200028 +627021200025998412345 0029000000OA338 JACK GAS 0098765434200029 +627021200025998412345 0020700000OA272 MORGAN FOX 0098765434200030 +627021200025998412345 0025500000OA342 COOPER BARNETT 0098765434200031 +627021200025998412345 0018500000OA224 JESSICA SILVA 0098765434200032 +627021200025998412345 0007800000OA273 JASMINE CARTER 0098765434200033 +627021200025998412345 0011700000OA299 ISABELLE ERRY 0098765434200034 +627021200025998412345 0006300000OA263 ZACHARY MEDINA 0098765434200035 +627021200025998412345 0001800000OA266 MATTHEW ALLEN 0098765434200036 +627021200025998412345 0015000000OA312 BROOKE SUTTON 0098765434200037 +627021200025998412345 0006600000OA242 ABIGAIL EVANS 0098765434200038 +627021200025998412345 0028000000OA304 ISAAC JORDAN 0098765434200039 +627021200025998412345 0007800000OA269 JUAN GILBERT 0098765434200040 +627021200025998412345 0020800000OA249 KEVIN CASTILLO 0098765434200041 +627021200025998412345 0010600000OA311 EVAN HARVEY 0098765434200042 +627021200025998412345 0019500000OA318 JESUS BRADLEY 0098765434200043 +627021200025998412345 0008100000OA260 MARIAH HERNANDEZ 0098765434200044 +627021200025998412345 0007600000OA361 ARIANNA LONG 0098765434200045 +627021200025998412345 0003700000OA265 ARIANNA BISHOP 0098765434200046 +627021200025998412345 0005900000OA254 CARLOS WILLIS 0098765434200047 +627021200025998412345 0025800000OA337 ISABELLA GREGORY 0098765434200048 +627021200025998412345 0011600000OA307 ALLISON SUTTON 0098765434200049 +627021200025998412345 0016200000OA290 COOPER LEZ 0098765434200050 +627021200025998412345 0010000000OA335 SOPHIA BAILEY 0098765434200051 +627021200025998412345 0017700000OA288 CHRISTIAN PEARSON 0098765434200052 +627021200025998412345 0028000000OA367 ISAAC JORDAN 0098765434200053 +627021200025998412345 0026100000OA244 LUKE CRAIG 0098765434200054 +627021200025998412345 0008300000OA278 GRACE HOWARD 0098765434200055 +627021200025998412345 0020300000OA228 SOPHIE NICHOLS 0098765434200056 +627021200025998412345 0027400000OA275 ALEXA WALKER 0098765434200057 +627021200025998412345 0006700000OA310 ADRIAN HOLLAND 0098765434200058 +627021200025998412345 0015400000OA314 GABRIEL MEDINA 0098765434200059 +627021200025998412345 0021600000OA344 KEVIN WOOD 0098765434200060 +627021200025998412345 0004700000OA262 KADEN POWELL 0098765434200061 +627021200025998412345 0009300000OA232 AVA GIBSON 0098765434200062 +627021200025998412345 0018700000OA303 LUCAS THOMPSON 0098765434200063 +627021200025998412345 0008500000OA308 ANGEL MCKINNEY 0098765434200064 +627021200025998412345 0002100000OA274 SEAN BRADLEY 0098765434200065 +627021200025998412345 0019100000OA245 IAN HOLLAND 0098765434200066 +627021200025998412345 0017500000OA323 ISABELLE FOX 0098765434200067 +627021200025998412345 0027400000OA355 ALEXA WALKER 0098765434200068 +627021200025998412345 0026100000OA226 JASON ANDERSON 0098765434200069 +627021200025998412345 0008600000OA243 GIANNA RUIZ 0098765434200070 +627021200025998412345 0011900000OA348 AYDEN DERS 0098765434200071 +627021200025998412345 0015100000OA336 COLTON LOWE 0098765434200072 +627021200025998412345 0002200000OA239 ARIANA THOMPSON 0098765434200073 +627021200025998412345 0025500000OA294 COOPER BARNETT 0098765434200074 +627021200025998412345 0028400000OA325 CAMILA HOLMES 0098765434200075 +627021200025998412345 0025800000OA358 ISABELLA GREGORY 0098765434200076 +627021200025998412345 0025800000OA259 HUNTER MILLS 0098765434200077 +627021200025998412345 0010000000OA279 SOPHIA BAILEY 0098765434200078 +627021200025998412345 0025600000OA293 ANGEL ORTIZ 0098765434200079 +627021200025998412345 0004600000OA252 JOSIAH GRIFFIN 0098765434200080 +627021200025998412345 0011200000OA364 SEBASTIAN MCDONALD 0098765434200081 +627021200025998412345 0022800000OA309 NOAH 0098765434200082 +627021200025998412345 0020500000OA324 AUBREY GRANT 0098765434200083 +627021200025998412345 0022800000OA256 KEVIN PERKINS 0098765434200084 +627021200025998412345 0014600000OA238 HENRY DAY 0098765434200085 +627021200025998412345 0011200000OA297 SEBASTIAN MCDONALD 0098765434200086 +627021200025998412345 0021100000OA282 CARLOS GREGORY 0098765434200087 +627021200025998412345 0009600000OA350 JACOB MORRISON 0098765434200088 +627021200025998412345 0009000000OA291 NATALIE JACOBS 0098765434200089 +627021200025998412345 0025800000OA285 ISABELLA GREGORY 0098765434200090 +627021200025998412345 0028200000OA339 MORGAN NELSON 0098765434200091 +627021200025998412345 0016100000OA353 JORDAN FRANKLIN 0098765434200092 +627021200025998412345 0016400000OA322 ELI BOWMAN 0098765434200093 +627021200025998412345 0003700000OA253 ANGEL SMITH 0098765434200094 +627021200025998412345 0026300000OA247 DIEGO WASHINGTON 0098765434200095 +627021200025998412345 0001900000OA328 ANGEL BROOKS 0098765434200096 +627021200025998412345 0015100000OA236 BAILEY PEREZ 0098765434200097 +627021200025998412345 0021600000OA301 KEVIN WOOD 0098765434200098 +627021200025998412345 0015800000OA331 JACOB LEE 0098765434200099 +627021200025998412345 0011200000OA343 SEBASTIAN MCDONALD 0098765434200100 +627021200025998412345 0026100000OA330 LUKE CRAIG 0098765434200101 +627021200025998412345 0016000000OA280 ABIGAIL ELTON 0098765434200102 +627021200025998412345 0010000000OA356 SOPHIA BAILEY 0098765434200103 +627021200025998412345 0029400000OA251 JAYDEN LYNCH 0098765434200104 +627021200025998412345 0011600000OA321 DESTINY MCDANIEL 0098765434200105 +627021200025998412345 0016600000OA315 JASMINE CARTER 0098765434200106 +627021200025998412345 0004000000OA306 MARY MARSHALL 0098765434200107 +627021200025998412345 0025600000OA362 ANGEL ORTIZ 0098765434200108 +627021200025998412345 0020800000OA258 SYDNEY REYES 0098765434200109 +627021200025998412345 0022800000OA347 NOAH 0098765434200110 +627021200025998412345 0006100000OA248 SYDNEY BUTLER 0098765434200111 +627021200025998412345 0015500000OA327 SOFIA GOMEZ 0098765434200112 +627021200025998412345 0017100000OA240 MARIA MITCHELL 0098765434200113 +627021200025998412345 0029500000OA241 MICHAEL WALLACE 0098765434200114 +627021200025998412345 0015100000OA305 HAYDEN DES 0098765434200115 +627021200025998412345 0006100000OA317 CAMILA T 0098765434200116 +627021200025998412345 0025600000OA341 ANGEL ORTIZ 0098765434200117 +627021200025998412345 0003600000OA329 LILY COLE 0098765434200118 +627021200025998412345 0012700000OA229 BRANDON WILSON 0098765434200119 +627021200025998412345 0018300000OA230 CONNOR GARDNER 0098765434200120 +627021200025998412345 0028200000OA287 MORGAN NELSON 0098765434200121 +627021200025998412345 0021600000OA365 KEVIN WOOD 0098765434200122 +627021200025998412345 0030000000OA326 ISABEL WEBB 0098765434200123 +627021200025998412345 0025600000OA261 MATTHEW ROSE 0098765434200124 +627021200025998412345 0029000000OA286 JACK GAS 0098765434200125 +627021200025998412345 0027400000OA302 JOSEPH DES 0098765434200126 +627021200025998412345 0013500000OA227 SARA NICHOLS 0098765434200127 +627021200025998412345 0027400000OA334 ALEXA WALKER 0098765434200128 +627021200025998412345 0007900000OA225 AIDEN MILES 0098765434200129 +627021200025998412345 0018700000OA366 LUCAS THOMPSON 0098765434200130 +627021200025998412345 0018000000OA234 CONNOR WILLIAMS 0098765434200131 +627021200025998412345 0009500000OA276 GABRIEL JENSEN 0098765434200132 +627021200025998412345 0015000000OA295 JAMES NEWMAN 0098765434200133 +627021200025998412345 0009600000OA320 JACOB MORRISON 0098765434200134 +627021200025998412345 0015800000OA255 JACOB LEE 0098765434200135 +627021200025998412345 0015100000OA284 COLTON LOWE 0098765434200136 +627021200025998412345 0013000000OA271 JUAN NICHOLS 0098765434200137 +627021200025998412345 0008200000OA237 ZOEY DAVIDSON 0098765434200138 +627021200025998412345 0013100000OA296 MARIA FLORES 0098765434200139 +627021200025998412345 0014000000OA277 SYDNEY TUCKER 0098765434200140 +627021200025998412345 0007600000OA289 ARIANNA LONG 0098765434200141 +627021200025998412345 0028000000OA346 ISAAC JORDAN 0098765434200142 +627021200025998412345 0005900000OA246 JESSICA WALKER 0098765434200143 +627021200025998412345 0008700000OA231 ELIZABETH COLE 0098765434200144 +627021200025998412345 0026100000OA351 LUKE CRAIG 0098765434200145 +822500014503074002900024798000000000000000001234567898 098765430000001 +5220TEST COMPANY 11234567898PPDVERIFY 110801110801 1098765430000003 622021200025998412345 0000001800OA370 CHASE BLACK 0098765434200001 -622021200025998412345 0000001300OA379 ANDREA BUTLER 0098765434200001 -622021200025998412345 0000000700OA371 BLAKE REYES 0098765434200001 -622021200025998412345 0000000300OA385 CONNOR MEDINA 0098765434200001 -622021200025998412345 0000001100OA369 CHASE BLACK 0098765434200001 -622021200025998412345 0000001300OA384 BRANDON ARMSTRONG 0098765434200001 -622021200025998412345 0000000300OA375 LUIS MEYER 0098765434200001 -622021200025998412345 0000001300OA374 BLAKE HICKS 0098765434200001 -622021200025998412345 0000000400OA372 BLAKE REYES 0098765434200001 -622021200025998412345 0000000100OA377 DOMINIC RAY 0098765434200001 -622021200025998412345 0000001900OA382 JONATHAN MORALES 0098765434200001 -622021200025998412345 0000000400OA381 JONATHAN MORALES 0098765434200001 -622021200025998412345 0000000200OA378 DOMINIC RAY 0098765434200001 -622021200025998412345 0000001100OA376 LUIS MEYER 0098765434200001 -622021200025998412345 0000002000OA373 BLAKE HICKS 0098765434200001 -622021200025998412345 0000000200OA386 CONNOR MEDINA 0098765434200001 -622021200025998412345 0000001600OA380 ANDREA BUTLER 0098765434200001 -622021200025998412345 0000001200OA383 BRANDON ARMSTRONG 0098765434200001 -822000001800381600360000000000000000000001721234567898 042000010000003 -5220TEST COMPANY 11234567898PPDTEST SALES110801110801 1042000010000002 -822000000000000000000000000000000000000000001234567898 042000010000002 -5225 FV3 CA1234567898IATTEST BUYS USDCAD110802 1042000010000004 -6270910502340 0012200000998412345 1098765430420000 +622021200025998412345 0000001300OA379 ANDREA BUTLER 0098765434200002 +622021200025998412345 0000000700OA371 BLAKE REYES 0098765434200003 +622021200025998412345 0000000300OA385 CONNOR MEDINA 0098765434200004 +622021200025998412345 0000001100OA369 CHASE BLACK 0098765434200005 +622021200025998412345 0000001300OA384 BRANDON ARMSTRONG 0098765434200006 +622021200025998412345 0000000300OA375 LUIS MEYER 0098765434200007 +622021200025998412345 0000001300OA374 BLAKE HICKS 0098765434200008 +622021200025998412345 0000000400OA372 BLAKE REYES 0098765434200009 +622021200025998412345 0000000100OA377 DOMINIC RAY 0098765434200010 +622021200025998412345 0000001900OA382 JONATHAN MORALES 0098765434200011 +622021200025998412345 0000000400OA381 JONATHAN MORALES 0098765434200012 +622021200025998412345 0000000200OA378 DOMINIC RAY 0098765434200013 +622021200025998412345 0000001100OA376 LUIS MEYER 0098765434200014 +622021200025998412345 0000002000OA373 BLAKE HICKS 0098765434200015 +622021200025998412345 0000000200OA386 CONNOR MEDINA 0098765434200016 +622021200025998412345 0000001600OA380 ANDREA BUTLER 0098765434200017 +622021200025998412345 0000001200OA383 BRANDON ARMSTRONG 0098765434200018 +822000001800381600360000000000000000000172001234567898 098765430000003 +5225 FV3 CA1234567898IATTEST BUYS USDCAD110802 1098765430000004 +627091050234007 0012200000998412345 1098765430000001 710WEB DIEGO MAY 0000001 711TEST COMPANY 123 EASY STREET 0000001 712ANYTOWN*KS\ US*12345\ 0000001 @@ -177,7 +175,7 @@ 714CENTRAL 01021200025 CA 0000001 715OA391 PO Box 450 0000001 716Metropolis*ON\ CA*01234\ 0000001 -6270910502340 0004000000998412345 1098765430420000 +627091050234007 0004000000998412345 1098765430000002 710WEB JOSHUA SILVA 0000002 711TEST COMPANY 123 EASY STREET 0000002 712ANYTOWN*KS\ US*12345\ 0000002 @@ -185,7 +183,7 @@ 714CENTRAL 01021200025 CA 0000002 715OA389 PO Box 230 0000002 716Metropolis*ON\ CA*01234\ 0000002 -6270910502340 0012200000998412345 1098765430420000 +627091050234007 0012200000998412345 1098765430000003 710WEB DIEGO MAY 0000003 711TEST COMPANY 123 EASY STREET 0000003 712ANYTOWN*KS\ US*12345\ 0000003 @@ -193,7 +191,7 @@ 714CENTRAL 01021200025 CA 0000003 715OA398 PO Box 450 0000003 716Metropolis*ON\ CA*01234\ 0000003 -6270910502340 0026800000998412345 1098765430420000 +6270910502340007 0026800000998412345 1098765430000004 710WEB ZOEY PAYNE 0000004 711TEST COMPANY 123 EASY STREET 0000004 712ANYTOWN*KS\ US*12345\ 0000004 @@ -201,7 +199,7 @@ 714CENTRAL 01021200025 CA 0000004 715OA392 PO Box 150 0000004 716Metropolis*ON\ CA*01234\ 0000004 -6270910502340 0012200000998412345 1098765430420000 +627091050234007 0012200000998412345 1098765430000005 710WEB DIEGO MAY 0000005 711TEST COMPANY 123 EASY STREET 0000005 712ANYTOWN*KS\ US*12345\ 0000005 @@ -209,7 +207,7 @@ 714CENTRAL 01021200025 CA 0000005 715OA395 PO Box 450 0000005 716Metropolis*ON\ CA*01234\ 0000005 -6270910502340 0014800000998412345 1098765430420000 +627091050234007 0014800000998412345 1098765430000006 710WEB DYLAN RAMOS 0000006 711TEST COMPANY 123 EASY STREET 0000006 712ANYTOWN*KS\ US*12345\ 0000006 @@ -217,7 +215,7 @@ 714CENTRAL 01021200025 CA 0000006 715OA390 PO Box 800 0000006 716Metropolis*ON\ CA*01234\ 0000006 -6270910502340 0026800000998412345 1098765430420000 +627091050234007 0026800000998412345 1098765430000007 710WEB ZOEY PAYNE 0000007 711TEST COMPANY 123 EASY STREET 0000007 712ANYTOWN*KS\ US*12345\ 0000007 @@ -225,7 +223,7 @@ 714CENTRAL 01021200025 CA 0000007 715OA396 PO Box 150 0000007 716Metropolis*ON\ CA*01234\ 0000007 -6270910502340 0020300000998412345 1098765430420000 +627091050234007 0020300000998412345 1098765430000008 710WEB JAMES ROMERO 0000008 711TEST COMPANY 123 EASY STREET 0000008 712ANYTOWN*KS\ US*12345\ 0000008 @@ -233,7 +231,7 @@ 714CENTRAL 01021200025 CA 0000008 715OA393 PO Box 810 0000008 716Metropolis*ON\ CA*01234\ 0000008 -6270910502340 0008700000998412345 1098765430420000 +627091050234007 0008700000998412345 1098765430000009 710WEB KEVIN LARSON 0000009 711TEST COMPANY 123 EASY STREET 0000009 712ANYTOWN*KS\ US*12345\ 0000009 @@ -241,7 +239,7 @@ 714CENTRAL 01021200025 CA 0000009 715OA387 PO Box 340 0000009 716Metropolis*ON\ CA*01234\ 0000009 -6270910502340 0008700000998412345 1098765430420000 +627091050234007 0008700000998412345 1098765430000010 710WEB KEVIN LARSON 0000010 711TEST COMPANY 123 EASY STREET 0000010 712ANYTOWN*KS\ US*12345\ 0000010 @@ -249,7 +247,7 @@ 714CENTRAL 01021200025 CA 0000010 715OA394 PO Box 340 0000010 716Metropolis*ON\ CA*01234\ 0000010 -6270910502340 0025100000998412345 1098765430420000 +627091050234007 0025100000998412345 1098765430000011 710WEB SEBASTIAN NEWMAN 0000011 711TEST COMPANY 123 EASY STREET 0000011 712ANYTOWN*KS\ US*12345\ 0000011 @@ -257,7 +255,7 @@ 714CENTRAL 01021200025 CA 0000011 715OA388 PO Box 600 0000011 716Metropolis*ON\ CA*01234\ 0000011 -6270910502340 0026800000998412345 1098765430420000 +627091050234007 0026800000998412345 1098765430000012 710WEB ZOEY PAYNE 0000012 711TEST COMPANY 123 EASY STREET 0000012 712ANYTOWN*KS\ US*12345\ 0000012 @@ -265,7 +263,7 @@ 714CENTRAL 01021200025 CA 0000012 715OA399 PO Box 150 0000012 716Metropolis*ON\ CA*01234\ 0000012 -6270910502340 0008700000998412345 1098765430420000 +627091050234007 0008700000998412345 1098765430000013 710WEB KEVIN LARSON 0000013 711TEST COMPANY 123 EASY STREET 0000013 712ANYTOWN*KS\ US*12345\ 0000013 @@ -273,23 +271,23 @@ 714CENTRAL 01021200025 CA 0000013 715OA397 PO Box 340 0000013 716Metropolis*ON\ CA*01234\ 0000013 -822500010401183652990000020730000000000000001234567898 042000010000004 -5220 FV3 CA1234567898IATVERIFY USDCAD110802 1042000010000005 -6220910502340 0000001200998412345 1098765430420000 -710WEB LEAH BRYANT 0000014 -711TEST COMPANY 123 EASY STREET 0000014 -712ANYTOWN*KS\ US*12345\ 0000014 -713U.S. BANK 0104200001 US 0000014 -714CENTRAL 01021200025 CA 0000014 -715OA400 PO Box 410 0000014 -716Metropolis*ON\ CA*01234\ 0000014 -6220910502340 0000000100998412345 1098765430420000 -710WEB LEAH BRYANT 0000015 -711TEST COMPANY 123 EASY STREET 0000015 -712ANYTOWN*KS\ US*12345\ 0000015 -713U.S. BANK 0104200001 US 0000015 -714CENTRAL 01021200025 CA 0000015 -715OA401 PO Box 410 0000015 -716Metropolis*ON\ CA*01234\ 0000015 -822000001600182100460000000000000000000000131234567898 042000010000005 -9000005000030000002830482135671000026871000000000000185 \ No newline at end of file +822500010401183652990002073000000000000000001234567898 098765430000004 +5220 FV3 CA1234567898IATVERIFY USDCAD110802 1098765430000005 +622091050234007 0000001200998412345 1098765430000001 +710WEB LEAH BRYANT 0000001 +711TEST COMPANY 123 EASY STREET 0000001 +712ANYTOWN*KS\ US*12345\ 0000001 +713U.S. BANK 0104200001 US 0000001 +714CENTRAL 01021200025 CA 0000001 +715OA400 PO Box 410 0000001 +716Metropolis*ON\ CA*01234\ 0000001 +622091050234007 0000000100998412345 1098765430000002 +710WEB LEAH BRYANT 0000002 +711TEST COMPANY 123 EASY STREET 0000002 +712ANYTOWN*KS\ US*12345\ 0000002 +713U.S. BANK 0104200001 US 0000002 +714CENTRAL 01021200025 CA 0000002 +715OA401 PO Box 410 0000002 +716Metropolis*ON\ CA*01234\ 0000002 +822000001600182100460000000000000000000013001234567898 098765430000005 +9000005000030000002830482135671002687100000000000018500 \ No newline at end of file diff --git a/test/data/20110805A.ach b/test/data/20110805A.ach index 799cd5a26..c95ccb39d 100644 --- a/test/data/20110805A.ach +++ b/test/data/20110805A.ach @@ -26,8 +26,6 @@ 627021200025998412345 0000149000A297 PAYTON MCCOY 0042000010000024 627021200025998412345 0000217000A298 DESTINY COLEMAN 0042000010000025 822500002500530000500000046100000000000000000123456789 042000010000001 -5220EXAMPLE COMPANY 0123456789PPDREFUND 110808110808 1042000010000002 -822000000000000000000000000000000000000000000123456789 042000010000002 5220EXAMPLE COMPANY 0123456789PPDVERIFY 110808110808 1042000010000003 622021200025998412345 0000000008A251 NATHAN NELSON 0042000010000001 622021200025998412345 0000000010A252 NATHAN NELSON 0042000010000002 @@ -92,4 +90,4 @@ 715A258 PO Box 160 0000002 716Metropolis*ON\ CA*01234\ 0000002 822000001600182100460000000000000000000000240123456789 042000010000005 -9000005000010000000830136685201000005101000000000000200 +9000005000010000000830136685201000005101000000000000200000000000000000000000000000000000000000 diff --git a/test/data/20180713-IAT.ach b/test/data/20180713-IAT.ach new file mode 100644 index 000000000..4c6c10ba0 --- /dev/null +++ b/test/data/20180713-IAT.ach @@ -0,0 +1,30 @@ +101 987654321 1234567891807130000A094101Federal Reserve Bank My Bank Name +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000001 +6221210428820007 0000100000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +82200000080012104288000000000000000000100000 231380100000001 +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000002 +6271210428820007 0000002000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +82200000080012104288000000002000000000000000 231380100000002 +9000002000003000000160024208576000000002000000000100000 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 \ No newline at end of file diff --git a/test/data/20180716-IAT-A17-A18.ach b/test/data/20180716-IAT-A17-A18.ach new file mode 100644 index 000000000..99059a3be --- /dev/null +++ b/test/data/20180716-IAT-A17-A18.ach @@ -0,0 +1,40 @@ +101 987654321 1234567891807160000A094101Federal Reserve Bank My Bank Name +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000001 +6221210428820007 0000100000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US*19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +717This is an international payment 00010000001 +717Transfer of money from one country to another 00020000001 +718Bank of Germany 01987987987654654 DE 00010000001 +718Bank of Spain 01987987987123123 ES 00020000001 +718Bank of France 01456456456987987 FR 00030000001 +718Bank of Turkey 0112312345678910 TR 00040000001 +718Bank of United Kingdom 011234567890123456789012345678901234GB 00050000001 +82200000150012104288000000000000000000100000 231380100000001 +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000002 +6271210428820007 0000002000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US*19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +717This is an international payment 00010000001 +717Transfer of money from one country to another 00020000001 +718Bank of Germany 01987987987654654 DE 00010000001 +718Bank of Spain 01987987987123123 ES 00020000001 +718Bank of France 01456456456987987 FR 00030000001 +718Bank of Turkey 0112312345678910 TR 00040000001 +718Bank of United Kingdom 011234567890123456789012345678901234GB 00050000001 +82200000150012104288000000002000000000000000 231380100000002 +9000002000004000000300024208576000000002000000000100000 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 \ No newline at end of file diff --git a/test/data/20180716-IAT-A17.ach b/test/data/20180716-IAT-A17.ach new file mode 100644 index 000000000..1f978f69d --- /dev/null +++ b/test/data/20180716-IAT-A17.ach @@ -0,0 +1,30 @@ +101 987654321 1234567891807160000A094101Federal Reserve Bank My Bank Name +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000001 +6221210428820007 0000100000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US*19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +717This is an international payment 00010000001 +717Transfer of money from one country to another 00020000001 +82200000100012104288000000000000000000100000 231380100000001 +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000002 +6271210428820007 0000002000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US*19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +717This is an international payment 00010000001 +82200000090012104288000000002000000000000000 231380100000002 +9000002000003000000190024208576000000002000000000100000 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 \ No newline at end of file diff --git a/test/data/IAT-BatchHeaderErr.ach b/test/data/IAT-BatchHeaderErr.ach new file mode 100644 index 000000000..873bcedeb --- /dev/null +++ b/test/data/IAT-BatchHeaderErr.ach @@ -0,0 +1,31 @@ +101 987654321 1234567891807130000A094101Federal Reserve Bank My Bank Name +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000001 +6221210428820007 0000100000123456789 1231380100000001 +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +82200000080012104288000000000000000000100000 231380100000001 +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000002 +6271210428820007 0000002000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +82200000080012104288000000002000000000000000 231380100000002 +9000002000003000000160024208576000000002000000000100000 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 \ No newline at end of file diff --git a/test/data/IAT-InvalidAddenda10.ach b/test/data/IAT-InvalidAddenda10.ach new file mode 100644 index 000000000..914f2e9dc --- /dev/null +++ b/test/data/IAT-InvalidAddenda10.ach @@ -0,0 +1,30 @@ +101 987654321 1234567891807130000A094101Federal Reserve Bank My Bank Name +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000001 +6221210428820007 0000100000123456789 1231380100000001 +710BIL000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +82200000080012104288000000000000000000100000 231380100000001 +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000002 +6271210428820007 0000002000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +82200000080012104288000000002000000000000000 231380100000002 +9000002000003000000160024208576000000002000000000100000 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 \ No newline at end of file diff --git a/test/data/IAT-InvalidBatchControl.ach b/test/data/IAT-InvalidBatchControl.ach new file mode 100644 index 000000000..88616111c --- /dev/null +++ b/test/data/IAT-InvalidBatchControl.ach @@ -0,0 +1,30 @@ +101 987654321 1234567891807130000A094101Federal Reserve Bank My Bank Name +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000001 +6221210428820007 0000100000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +82200000080012104288000000000000000000100000 231000000000000 +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000002 +6271210428820007 0000002000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +82200000080012104288000000002000000000000000 231380100000002 +9000002000003000000160024208576000000002000000000100000 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 diff --git a/test/data/IAT-InvalidBatchHeader.ach b/test/data/IAT-InvalidBatchHeader.ach new file mode 100644 index 000000000..7b6707817 --- /dev/null +++ b/test/data/IAT-InvalidBatchHeader.ach @@ -0,0 +1,30 @@ +101 987654321 1234567891807130000A094101Federal Reserve Bank My Bank Name +5100 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000001 +6221210428820007 0000100000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +82200000080012104288000000000000000000100000 231380100000001 +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000002 +6271210428820007 0000002000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +82200000080012104288000000002000000000000000 231380100000002 +9000002000003000000160024208576000000002000000000100000 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 \ No newline at end of file diff --git a/test/data/IAT-InvalidEntryDetail.ach b/test/data/IAT-InvalidEntryDetail.ach new file mode 100644 index 000000000..0584a0b40 --- /dev/null +++ b/test/data/IAT-InvalidEntryDetail.ach @@ -0,0 +1,30 @@ +101 987654321 1234567891807160000A094101Federal Reserve Bank My Bank Name +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000001 +6901210428820007 0000100000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US*19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +717This is an international payment 00010000001 +717Transfer of money from one country to another 00020000001 +82200000100012104288000000000000000000100000 231380100000001 +5220 FF3 US123456789 IATTRADEPAYMTCADUSD010101 0231380100000002 +6271210428820007 0000002000123456789 1231380100000001 +710ANN000000000000100000928383-23938 BEK Enterprises 0000001 +711BEK Solutions 15 West Place Street 0000001 +712JacobsTown*PA\ US*19305\ 0000001 +713Wells Fargo 01121042882 US 0000001 +714Citadel Bank 01231380104 US 0000001 +7159874654932139872121 Front Street 0000001 +716LetterTown*AB\ CA*80014\ 0000001 +717This is an international payment 00010000001 +82200000090012104288000000002000000000000000 231380100000002 +9000002000003000000190024208576000000002000000000100000 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 \ No newline at end of file diff --git a/validators.go b/validators.go index a88a9f7ea..5ff0e8505 100644 --- a/validators.go +++ b/validators.go @@ -31,6 +31,11 @@ var ( msgValidMonth = "is an invalid month" msgValidDay = "is an invalid day" msgValidYear = "is an invalid year" + // IAT + msgForeignExchangeIndicator = "is an invalid Foreign Exchange Indicator" + msgForeignExchangeReferenceIndicator = "is an invalid Foreign Exchange Reference Indicator" + msgTransactionTypeCode = "is an invalid Addenda10 Transaction Type Code" + msgIDNumberQualifier = "is an invalid Identification Number Qualifier" ) // validator is common validation and formatting of golang types to ach type strings @@ -43,6 +48,8 @@ type FieldError struct { Msg string // context of the error. } +// ToDo: Add state and country look-up or use a 3rd party look up -verify with Wade + // Error message is constructed // FieldName Msg Value // Example1: BatchCount $% has none alphanumeric characters @@ -144,6 +151,44 @@ func (v *validator) isDay(m string, d string) error { return errors.New(msgValidDay) } +// isForeignExchangeIndicator ensures foreign exchange indicators of an +// IATBatchHeader is valid +func (v *validator) isForeignExchangeIndicator(code string) error { + switch code { + case + "FV", "VF", "FF": + return nil + } + return errors.New(msgForeignExchangeIndicator) +} + +// isForeignExchangeReferenceIndicator ensures foreign exchange reference +// indicator of am IATBatchHeader is valid +func (v *validator) isForeignExchangeReferenceIndicator(code int) error { + switch code { + case + 1, 2, 3: + return nil + } + return errors.New(msgForeignExchangeReferenceIndicator) +} + +// isIDNumberQualifier ensures ODFI Identification Number Qualifier is valid +// For Inbound IATs: The 2-digit code that identifies the numbering scheme used in the +// Foreign DFI Identification Number field: +// 01 = National Clearing System +// 02 = BIC Code +// 03 = IBAN Code +// used for both ODFIIDNumberQualifier and RDFIIDNumberQualifier +func (v *validator) isIDNumberQualifier(s string) error { + switch s { + case + "01", "02", "03": + return nil + } + return errors.New(msgIDNumberQualifier) +} + // isOriginatorStatusCode ensures status code of a batch is valid func (v *validator) isOriginatorStatusCode(code int) error { switch code { @@ -202,7 +247,7 @@ func (v *validator) isTypeCode(code string) error { // Return, Dishonored Return and Contested Dishonored Return Entries "99", // IAT forward Entries and IAT Returns - "10", "11", "12", "13", "14", "15", "16", "17", + "10", "11", "12", "13", "14", "15", "16", "17", "18", // ACK, ATX, CCD, CIE, CTX, DNE, ENR, PPD, TRX and WEB Entries "05": return nil @@ -214,20 +259,20 @@ func (v *validator) isTypeCode(code string) error { // // The Tran Code is a two-digit code in positions 2 - 3 of the Entry Detail Record (6 Record) within an ACH File. // The first digit of the Tran Code indicates the account type to which the entry will post, where the number: -// "2"designates a Checking Account. -// "3"designates a Savings Account. -// "4"designates a General Ledger Account. -// "5"designates Loan Account. +// "2" designates a Checking Account. +// "3" designates a Savings Account. +// "4" designates a General Ledger Account. +// "5" designates Loan Account. //The second digit of the Tran Code identifies the entry as: // an original forward entry, where the number: -// "2"designates a credit. or -// "7"designates a debit. +// "2" designates a credit. or +// "7" designates a debit. // a return or NOC, where the number: -// "1"designates the return/NOC of a credit, or -// "6"designates a return/NOC of a debit. +// "1" designates the return/NOC of a credit, or +// "6" designates a return/NOC of a debit. // a pre-note or non-monetary informational transaction, where the number: -// "3"designates a credit, or -// "8"designates a debit. +// "3" designates a credit, or +// "8" designates a debit. func (v *validator) isTransactionCode(code int) error { switch code { // TransactionCode if the receivers account is: @@ -339,6 +384,21 @@ func (v *validator) isTransactionCode(code int) error { return errors.New(msgTransactionCode) } +// isTransactionTypeCode verifies Addenda10 TransactionTypeCode is a valid value +// ANN = Annuity, BUS = Business/Commercial, DEP = Deposit, LOA = Loan, MIS = Miscellaneous, MOR = Mortgage +// PEN = Pension, RLS = Rent/Lease, REM = Remittance2, SAL = Salary/Payroll, TAX = Tax, TEL = Telephone-Initiated Transaction +// WEB = Internet-Initiated Transaction, ARC = Accounts Receivable Entry, BOC = Back Office Conversion Entry, +// POP = Point of Purchase Entry, RCK = Re-presented Check Entry +func (v *validator) isTransactionTypeCode(s string) error { + switch s { + case "ANN", "BUS", "DEP", "LOA", "MIS", "MOR", + "PEN", "RLS", "REM", "SAL", "TAX", "TEL", "WEB", + "ARC", "BOC", "POP", "RCK": + return nil + } + return errors.New(msgTransactionTypeCode) +} + // isUpperAlphanumeric checks if string only contains ASCII alphanumeric upper case characters func (v *validator) isUpperAlphanumeric(s string) error { if upperAlphanumericRegex.MatchString(s) { diff --git a/writer.go b/writer.go index 688966608..fb24e1859 100644 --- a/writer.go +++ b/writer.go @@ -40,6 +40,36 @@ func (w *Writer) Write(file *File) error { } w.lineNum++ + if err := w.writeBatch(file); err != nil { + return err + } + + if err := w.writeIATBatch(file); err != nil { + return err + } + + if _, err := w.w.WriteString(file.Control.String() + "\n"); err != nil { + return err + } + w.lineNum++ + + // pad the final block + for i := 0; i < (10-(w.lineNum%10)) && w.lineNum%10 != 0; i++ { + if _, err := w.w.WriteString(strings.Repeat("9", 94) + "\n"); err != nil { + return err + } + } + + return w.w.Flush() +} + +// Flush writes any buffered data to the underlying io.Writer. +// To check if an error occurred during the Flush, call Error. +func (w *Writer) Flush() { + w.w.Flush() +} + +func (w *Writer) writeBatch(file *File) error { for _, batch := range file.Batches { if _, err := w.w.WriteString(batch.GetHeader().String() + "\n"); err != nil { return err @@ -62,23 +92,60 @@ func (w *Writer) Write(file *File) error { } w.lineNum++ } - if _, err := w.w.WriteString(file.Control.String() + "\n"); err != nil { - return err - } - w.lineNum++ + return nil +} - // pad the final block - for i := 0; i < (10-(w.lineNum%10)) && w.lineNum%10 != 0; i++ { - if _, err := w.w.WriteString(strings.Repeat("9", 94) + "\n"); err != nil { +func (w *Writer) writeIATBatch(file *File) error { + for _, iatBatch := range file.IATBatches { + if _, err := w.w.WriteString(iatBatch.GetHeader().String() + "\n"); err != nil { return err } + w.lineNum++ + for _, entry := range iatBatch.GetEntries() { + if _, err := w.w.WriteString(entry.String() + "\n"); err != nil { + return err + } + w.lineNum++ + if _, err := w.w.WriteString(entry.Addenda10.String() + "\n"); err != nil { + return err + } + w.lineNum++ + if _, err := w.w.WriteString(entry.Addenda11.String() + "\n"); err != nil { + return err + } + w.lineNum++ + if _, err := w.w.WriteString(entry.Addenda12.String() + "\n"); err != nil { + return err + } + w.lineNum++ + if _, err := w.w.WriteString(entry.Addenda13.String() + "\n"); err != nil { + return err + } + w.lineNum++ + if _, err := w.w.WriteString(entry.Addenda14.String() + "\n"); err != nil { + return err + } + w.lineNum++ + if _, err := w.w.WriteString(entry.Addenda15.String() + "\n"); err != nil { + return err + } + w.lineNum++ + if _, err := w.w.WriteString(entry.Addenda16.String() + "\n"); err != nil { + return err + } + w.lineNum++ + // IAT Addenda17 and IAT Addenda18 records + for _, IATaddenda := range entry.Addendum { + if _, err := w.w.WriteString(IATaddenda.String() + "\n"); err != nil { + return err + } + w.lineNum++ + } + } + if _, err := w.w.WriteString(iatBatch.GetControl().String() + "\n"); err != nil { + return err + } + w.lineNum++ } - - return w.w.Flush() -} - -// Flush writes any buffered data to the underlying io.Writer. -// To check if an error occurred during the Flush, call Error. -func (w *Writer) Flush() { - w.w.Flush() + return nil } diff --git a/writer_test.go b/writer_test.go index 2de2e9455..389e067f8 100644 --- a/writer_test.go +++ b/writer_test.go @@ -104,3 +104,190 @@ func BenchmarkFileWriteErr(b *testing.B) { testFileWriteErr(b) } } + +// testIATWrite writes a IAT ACH file +func testIATWrite(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(mockAddenda17()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda17B()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18B()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18C()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18D()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18E()) + iatBatch.Create() + file.AddIATBatch(iatBatch) + + iatBatch2 := IATBatch{} + iatBatch2.SetHeader(mockIATBatchHeaderFF()) + iatBatch2.AddEntry(mockIATEntryDetail()) + iatBatch2.GetEntries()[0].TransactionCode = 27 + iatBatch2.GetEntries()[0].Amount = 2000 + iatBatch2.Entries[0].Addenda10 = mockAddenda10() + iatBatch2.Entries[0].Addenda11 = mockAddenda11() + iatBatch2.Entries[0].Addenda12 = mockAddenda12() + iatBatch2.Entries[0].Addenda13 = mockAddenda13() + iatBatch2.Entries[0].Addenda14 = mockAddenda14() + iatBatch2.Entries[0].Addenda15 = mockAddenda15() + iatBatch2.Entries[0].Addenda16 = mockAddenda16() + iatBatch2.Entries[0].AddIATAddenda(mockAddenda17()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda17B()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18B()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18C()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18D()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18E()) + iatBatch2.Create() + file.AddIATBatch(iatBatch2) + + 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()*/ +} + +// TestIATWrite tests writing a IAT ACH file +func TestIATWrite(t *testing.T) { + testIATWrite(t) +} + +// BenchmarkIATWrite benchmarks validating writing a IAT ACH file +func BenchmarkIATWrite(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATWrite(b) + } +} + +// testPPDIATWrite writes an ACH file which writing an ACH file which contains PPD and IAT entries +func testPPDIATWrite(t testing.TB) { + file := NewFile().SetHeader(mockFileHeader()) + + entry := mockEntryDetail() + entry.AddAddenda(mockAddenda05()) + batch := NewBatchPPD(mockBatchPPDHeader()) + batch.SetHeader(mockBatchHeader()) + batch.AddEntry(entry) + batch.Create() + file.AddBatch(batch) + + 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(mockAddenda17()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda17B()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18B()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18C()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18D()) + iatBatch.Entries[0].AddIATAddenda(mockAddenda18E()) + iatBatch.Create() + file.AddIATBatch(iatBatch) + + iatBatch2 := IATBatch{} + iatBatch2.SetHeader(mockIATBatchHeaderFF()) + iatBatch2.AddEntry(mockIATEntryDetail()) + iatBatch2.GetEntries()[0].TransactionCode = 27 + iatBatch2.GetEntries()[0].Amount = 2000 + iatBatch2.Entries[0].Addenda10 = mockAddenda10() + iatBatch2.Entries[0].Addenda11 = mockAddenda11() + iatBatch2.Entries[0].Addenda12 = mockAddenda12() + iatBatch2.Entries[0].Addenda13 = mockAddenda13() + iatBatch2.Entries[0].Addenda14 = mockAddenda14() + iatBatch2.Entries[0].Addenda15 = mockAddenda15() + iatBatch2.Entries[0].Addenda16 = mockAddenda16() + iatBatch2.Entries[0].AddIATAddenda(mockAddenda17()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda17B()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18B()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18C()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18D()) + iatBatch2.Entries[0].AddIATAddenda(mockAddenda18E()) + iatBatch2.Create() + file.AddIATBatch(iatBatch2) + + 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 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()*/ +} + +// TestPPDIATWrite tests writing a IAT ACH file +func TestPPDIATWrite(t *testing.T) { + testPPDIATWrite(t) +} + +// BenchmarkPPDIATWrite benchmarks validating writing an ACH file which contain PPD and IAT entries +func BenchmarkPPDIATWrite(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testPPDIATWrite(b) + } +}