From a57d6b3a0e8cc6657a43e9b692b5ee4ea15b1d84 Mon Sep 17 00:00:00 2001 From: Brooke Kline Date: Thu, 21 Jun 2018 22:23:46 -0400 Subject: [PATCH 01/64] Rename BatchARC_test.go to batchARC_test.go --- BatchARC_test.go => batchARC_test.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename BatchARC_test.go => batchARC_test.go (100%) diff --git a/BatchARC_test.go b/batchARC_test.go similarity index 100% rename from BatchARC_test.go rename to batchARC_test.go From 8a127066208dee9e499c55973b2cd2389dce157d Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 26 Jun 2018 13:09:52 -0400 Subject: [PATCH 02/64] #211 Support for IAT #211 Support for IAT --- batchIAT.go | 51 +++ iatBatch.go | 382 +++++++++++++++++++ iatBatchHeader.go | 423 +++++++++++++++++++++ iatBatchHeader_test.go | 811 +++++++++++++++++++++++++++++++++++++++++ iatBatcher.go | 55 +++ iatEntryDetail.go | 269 ++++++++++++++ iatEntryDetail_test.go | 490 +++++++++++++++++++++++++ reader.go | 53 +++ validators.go | 25 ++ 9 files changed, 2559 insertions(+) create mode 100644 batchIAT.go create mode 100644 iatBatch.go create mode 100644 iatBatchHeader.go create mode 100644 iatBatchHeader_test.go create mode 100644 iatBatcher.go create mode 100644 iatEntryDetail.go create mode 100644 iatEntryDetail_test.go diff --git a/batchIAT.go b/batchIAT.go new file mode 100644 index 000000000..e6a0fcbed --- /dev/null +++ b/batchIAT.go @@ -0,0 +1,51 @@ +// 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 + +// BatchIAT holds the Batch Header and Batch Control and all Entry Records for IAT Entries +type BatchIAT struct { + iatBatch +} + +// NewBatchIAT returns a *BatchIAT +func NewBatchIAT(bh *IATBatchHeader) *BatchIAT { + iatBatch := new(BatchIAT) + iatBatch.SetControl(NewBatchControl()) + iatBatch.SetHeader(bh) + return iatBatch +} + +// Validate checks valid NACHA batch rules. Assumes properly parsed records. +func (batch *BatchIAT) Validate() error { + // basic verification of the batch before we validate specific rules. + if err := batch.verify(); err != nil { + return err + } + // Add configuration based validation for this type. + + // Batch can have one addenda per entry record + /* if err := batch.isAddendaCount(1); err != nil { + return err + } + if err := batch.isTypeCode("05"); err != nil { + return err + }*/ + + // Add type specific validation. + // ... + return nil +} + +// Create takes Batch Header and Entries and builds a valid batch +func (batch *BatchIAT) Create() error { + // generates sequence numbers and batch control + if err := batch.build(); err != nil { + return err + } + // Additional steps specific to batch type + // ... + + return batch.Validate() +} diff --git a/iatBatch.go b/iatBatch.go new file mode 100644 index 000000000..9a3b7ae09 --- /dev/null +++ b/iatBatch.go @@ -0,0 +1,382 @@ +// 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" +) + +// Batch holds the Batch Header and Batch Control and all Entry Records +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) (IATBatcher, error) { + return NewBatchIAT(bh), nil +} + +// 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 + + // 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} + } + // Company Identification must match the Company ID from the batch header record + /* if batch.Header.CompanyIdentification != batch.Control.CompanyIdentification { + msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.Header.CompanyIdentification, batch.Control.CompanyIdentification) + return &BatchError{BatchNumber: batchNumber, FieldName: "CompanyIdentification", 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.ODFIIdentification, batch.Control.ODFIIdentification) + 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 + } + // TODO this is specific to batch SEC types and should be called by that validator + if err := batch.isAddendaSequence(); err != nil { + return err + } + return batch.isCategory() +} + +// 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 + len(entry.Addendum) + 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. Have to keep original trance number Return and NOC entries + if currentTraceNumberODFI != batchHeaderODFI { + batch.Entries[i].SetTraceNumber(batch.Header.ODFIIdentification, seq) + } + seq++ + addendaSeq := 1 + for x := range entry.Addendum { + // sequences don't exist in NOC or Return addenda + if a, ok := batch.Entries[i].Addendum[x].(*Addenda05); ok { + a.SequenceNumber = addendaSeq + a.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + } + addendaSeq++ + } + } + + // build a BatchControl record + bc := NewBatchControl() + bc.ServiceClassCode = batch.Header.ServiceClassCode + /*bc.CompanyIdentification = iatBatch.Header.CompanyIdentification*/ + 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) +} + +// IsReturn is true if the batch contains an Entry Return +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 + } + for _, addenda := range entry.Addendum { + if err := addenda.Validate(); err != nil { + return nil + } + } + } + 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 + 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) +} + +// The Originator Status Code is not equal to “2” for DNE if the Transaction Code is 23 or 33 +func (batch *iatBatch) isOriginatorDNE() error { + if batch.Header.OriginatorStatusCode != 2 { + for _, entry := range batch.Entries { + if entry.TransactionCode == 23 || entry.TransactionCode == 33 { + msg := fmt.Sprintf(msgBatchOriginatorDNE, batch.Header.OriginatorStatusCode) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "OriginatorStatusCode", Msg: msg} + } + } + } + return nil +} + +// 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 { + if len(entry.Addendum) > 0 { + // addenda without indicator flag of 1 + if entry.AddendaRecordIndicator != 1 { + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "AddendaRecordIndicator", Msg: msgBatchAddendaIndicator} + } + lastSeq := -1 + // check if sequence is ascending + for _, addenda := range entry.Addendum { + // sequences don't exist in NOC or Return addenda + if a, ok := addenda.(*Addenda05); ok { + + if a.SequenceNumber < lastSeq { + msg := fmt.Sprintf(msgBatchAscending, a.SequenceNumber, lastSeq) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "SequenceNumber", Msg: msg} + } + lastSeq = 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 +} + +// isAddendaCount iterates through each entry detail and checks the number of addendum is greater than the count parameter otherwise it returns an error. +// Following SEC codes allow for none or one Addendum +// "PPD", "WEB", "CCD", "CIE", "DNE", "MTE", "POS", "SHR" +func (batch *iatBatch) isAddendaCount(count int) error { + for _, entry := range batch.Entries { + if len(entry.Addendum) > count { + msg := fmt.Sprintf(msgBatchAddendaCount, len(entry.Addendum), count, batch.Header.StandardEntryClassCode) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "AddendaCount", Msg: msg} + } + + } + return nil +} + +// isTypeCode takes a TypeCode string and verifies Addenda records match +func (batch *iatBatch) isTypeCode(typeCode string) error { + for _, entry := range batch.Entries { + for _, addenda := range entry.Addendum { + if addenda.TypeCode() != typeCode { + msg := fmt.Sprintf(msgBatchTypeCode, addenda.TypeCode(), typeCode, batch.Header.StandardEntryClassCode) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TypeCode", 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++ { + 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 +} 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/iatBatcher.go b/iatBatcher.go new file mode 100644 index 000000000..9a60bc43e --- /dev/null +++ b/iatBatcher.go @@ -0,0 +1,55 @@ +// 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 + +// IATBatcher abstract an IAT ACH batch type +type IATBatcher interface { + GetHeader() *IATBatchHeader + SetHeader(*IATBatchHeader) + GetControl() *BatchControl + SetControl(*BatchControl) + GetEntries() []*IATEntryDetail + AddEntry(*IATEntryDetail) + Create() error + Validate() error + // Category defines if a Forward or Return + Category() string +} + +/*// BatchError is an Error that describes batch validation issues +type IATBatchError struct { + BatchNumber int + FieldName string + Msg string +} + +func (e *BatchError) IATError() string { + return fmt.Sprintf("BatchNumber %d %s %s", e.BatchNumber, e.FieldName, e.Msg) +}*/ + +// Errors specific to parsing a Batch container +var ( +// generic messages +/* msgBatchHeaderControlEquality = "header %v is not equal to control %v" + msgBatchCalculatedControlEquality = "calculated %v is out-of-balance with control %v" + msgBatchAscending = "%v is less than last %v. Must be in ascending order" + // specific messages for error + msgBatchCompanyEntryDescription = "Company entry description %v is not valid for batch type %v" + msgBatchOriginatorDNE = "%v is not “2” for DNE with entry transaction code of 23 or 33" + msgBatchTraceNumberNotODFI = "%v in header does not match entry trace number %v" + msgBatchAddendaIndicator = "is 0 but found addenda record(s)" + msgBatchAddendaTraceNumber = "%v does not match proceeding entry detail trace number %v" + msgBatchEntries = "must have Entry Record(s) to be built" + msgBatchAddendaCount = "%v addendum found where %v is allowed for batch type %v" + msgBatchTransactionCodeCredit = "%v a credit is not allowed" + msgBatchSECType = "header SEC type code %v for batch type %v" + msgBatchTypeCode = "%v found in addenda and expecting %v for batch type %v" + msgBatchServiceClassCode = "Service Class Code %v is not valid for batch type %v" + msgBatchForwardReturn = "Forward and Return entries found in the same batch" + msgBatchAmount = "Amount must be less than %v for SEC code %v" + msgBatchCheckSerialNumber = "Check Serial Number is required for SEC code %v" + msgBatchTransactionCode = "Transaction code %v is not allowed for batch type %v" + msgBatchCardTransactionType = "Card Transaction Type %v is invalid"*/ +) diff --git a/iatEntryDetail.go b/iatEntryDetail.go new file mode 100644 index 000000000..83eabe2d8 --- /dev/null +++ b/iatEntryDetail.go @@ -0,0 +1,269 @@ +// 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"` + + // reserved2 - 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"` + + // 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 + 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) +} diff --git a/iatEntryDetail_test.go b/iatEntryDetail_test.go new file mode 100644 index 000000000..ba1f293e0 --- /dev/null +++ b/iatEntryDetail_test.go @@ -0,0 +1,490 @@ +// 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 = 7 + entry.DFIAccountNumber = "123456789" + entry.Amount = 100000 + entry.SetTraceNumber(mockIATBatchHeaderFF().ODFIIdentification, 1) + 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") + } + // ToDo: Add checkDigit test + 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(NewBatchIAT(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(NewBatchIAT(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..c7a9dfe56 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 IATBatcher // 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 IATBatcher) { + r.IATCurrentBatch = iatBatch +} + // NewReader returns a new ACH Reader that reads from r. func NewReader(r io.Reader) *Reader { return &Reader{ @@ -204,9 +212,12 @@ func (r *Reader) parseBatchHeader() error { return nil } +// ToDo: come up with a switch - entryDetailer back to that? + // 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}) } @@ -299,3 +310,45 @@ func (r *Reader) parseFileControl() error { } return nil } + +// parseIATBatchHeader takes the input record string and parses the FileHeaderRecord values +func (r *Reader) parseIATBatchHeader() error { + r.recordName = "IATBatchHeader" + if r.IATCurrentBatch != 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, err := IATNewBatch(bh) + if err != nil { + return r.error(err) + } + + r.addIATCurrentBatch(iatBatch) + + return nil +} + +// parseIATEntryDetail takes the input record string and parses the EntryDetailRecord values +func (r *Reader) parseIATEntryDetail() error { + r.recordName = "IATEntryDetail" + + if r.IATCurrentBatch == 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 +} diff --git a/validators.go b/validators.go index a88a9f7ea..47f359897 100644 --- a/validators.go +++ b/validators.go @@ -31,6 +31,9 @@ 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" ) // validator is common validation and formatting of golang types to ach type strings @@ -144,6 +147,28 @@ 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) +} + // isOriginatorStatusCode ensures status code of a batch is valid func (v *validator) isOriginatorStatusCode(code int) error { switch code { From dcaef5005c5b6c3b4ae3acc19f290b8e03fb99f2 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 26 Jun 2018 13:16:50 -0400 Subject: [PATCH 03/64] #211 megacheck #211 megacheck --- iatBatch.go | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/iatBatch.go b/iatBatch.go index 9a3b7ae09..663044891 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -338,33 +338,6 @@ func (batch *iatBatch) isAddendaSequence() error { return nil } -// isAddendaCount iterates through each entry detail and checks the number of addendum is greater than the count parameter otherwise it returns an error. -// Following SEC codes allow for none or one Addendum -// "PPD", "WEB", "CCD", "CIE", "DNE", "MTE", "POS", "SHR" -func (batch *iatBatch) isAddendaCount(count int) error { - for _, entry := range batch.Entries { - if len(entry.Addendum) > count { - msg := fmt.Sprintf(msgBatchAddendaCount, len(entry.Addendum), count, batch.Header.StandardEntryClassCode) - return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "AddendaCount", Msg: msg} - } - - } - return nil -} - -// isTypeCode takes a TypeCode string and verifies Addenda records match -func (batch *iatBatch) isTypeCode(typeCode string) error { - for _, entry := range batch.Entries { - for _, addenda := range entry.Addendum { - if addenda.TypeCode() != typeCode { - msg := fmt.Sprintf(msgBatchTypeCode, addenda.TypeCode(), typeCode, batch.Header.StandardEntryClassCode) - return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TypeCode", 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 From 287e7a6c779beb7384325a9d7aaae8f7ecf610cd Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 26 Jun 2018 15:07:49 -0400 Subject: [PATCH 04/64] #211 Formatting #211 Formatting --- entryDetail.go | 10 ---------- entryDetail_test.go | 2 +- iatEntryDetail.go | 16 +--------------- 3 files changed, 2 insertions(+), 26 deletions(-) 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/iatEntryDetail.go b/iatEntryDetail.go index 83eabe2d8..886e28e0b 100644 --- a/iatEntryDetail.go +++ b/iatEntryDetail.go @@ -16,10 +16,8 @@ import ( 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’ @@ -30,41 +28,30 @@ type IATEntryDetail 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"` - // 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"` - - // reserved2 - Leave blank + // 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. @@ -74,7 +61,6 @@ type IATEntryDetail 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 From c676384b95b398f31d19b69ac93fdf63dfaaca46 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 26 Jun 2018 18:49:20 -0400 Subject: [PATCH 05/64] #211 SEC Code IAT #211 SEC Code IAT --- file.go | 32 +++++++++++++++++++++++++++----- reader.go | 25 +++++++++++++++++++------ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/file.go b/file.go index 74123019d..9fa9f6f7a 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 []IATBatcher `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.Batches) <= 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 IATBatcher) []IATBatcher { + 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 diff --git a/reader.go b/reader.go index c7a9dfe56..bb85def9d 100644 --- a/reader.go +++ b/reader.go @@ -136,12 +136,27 @@ func (r *Reader) parseLine() error { return err } case batchHeaderPos: - if err := r.parseBatchHeader(); err != nil { - return err + switch r.line[49:53] { + case "IAT": + if err := r.parseIATBatchHeader(); err != nil { + return err + } + default: + if err := r.parseBatchHeader(); err != nil { + return err + } } case entryDetailPos: - if err := r.parseEntryDetail(); err != nil { - return err + switch r.line[16:29] { + + case " ": + if err := r.parseIATEntryDetail(); err != nil { + return err + } + default: + if err := r.parseEntryDetail(); err != nil { + return err + } } case entryAddendaPos: if err := r.parseAddenda(); err != nil { @@ -212,8 +227,6 @@ func (r *Reader) parseBatchHeader() error { return nil } -// ToDo: come up with a switch - entryDetailer back to that? - // parseEntryDetail takes the input record string and parses the EntryDetailRecord values func (r *Reader) parseEntryDetail() error { r.recordName = "EntryDetail" From b11a0037c419ac2e4bbcca9b385a4410443a1590 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 26 Jun 2018 18:51:33 -0400 Subject: [PATCH 06/64] #211 IAT support #211 IAT Support --- file.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/file.go b/file.go index 9fa9f6f7a..fa983e0de 100644 --- a/file.go +++ b/file.go @@ -82,7 +82,7 @@ func (f *File) Create() error { return err } // Requires at least one Batch in the new file. - if len(f.Batches) <= 0 && 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 From d63c62a1d171729ce2b9ffaa9527c52861236ae2 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 26 Jun 2018 19:01:03 -0400 Subject: [PATCH 07/64] #211 Fix Reader.go megacheck issue #211 Fix Reader.go megacheck issue --- reader.go | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/reader.go b/reader.go index bb85def9d..bd7a026b4 100644 --- a/reader.go +++ b/reader.go @@ -136,27 +136,12 @@ func (r *Reader) parseLine() error { return err } case batchHeaderPos: - switch r.line[49:53] { - case "IAT": - if err := r.parseIATBatchHeader(); err != nil { - return err - } - default: - if err := r.parseBatchHeader(); err != nil { - return err - } + if err := r.parseBatchHeader(); err != nil { + return err } case entryDetailPos: - switch r.line[16:29] { - - case " ": - if err := r.parseIATEntryDetail(); err != nil { - return err - } - default: - if err := r.parseEntryDetail(); err != nil { - return err - } + if err := r.parseEntryDetail(); err != nil { + return err } case entryAddendaPos: if err := r.parseAddenda(); err != nil { @@ -227,6 +212,8 @@ func (r *Reader) parseBatchHeader() error { return nil } + + // parseEntryDetail takes the input record string and parses the EntryDetailRecord values func (r *Reader) parseEntryDetail() error { r.recordName = "EntryDetail" @@ -364,4 +351,4 @@ func (r *Reader) parseIATEntryDetail() error { } r.IATCurrentBatch.AddEntry(ed) return nil -} +} \ No newline at end of file From 3d98db2fb638fb3d99023702ecee74abce1ee10d Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 26 Jun 2018 19:01:42 -0400 Subject: [PATCH 08/64] #211 reader.go govet #211 reader.go govet --- reader.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/reader.go b/reader.go index bd7a026b4..06672f9f7 100644 --- a/reader.go +++ b/reader.go @@ -212,8 +212,6 @@ func (r *Reader) parseBatchHeader() error { return nil } - - // parseEntryDetail takes the input record string and parses the EntryDetailRecord values func (r *Reader) parseEntryDetail() error { r.recordName = "EntryDetail" @@ -351,4 +349,4 @@ func (r *Reader) parseIATEntryDetail() error { } r.IATCurrentBatch.AddEntry(ed) return nil -} \ No newline at end of file +} From 908b742fad4bf127c92f0a472dade0f83aa7d2f7 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 26 Jun 2018 19:16:44 -0400 Subject: [PATCH 09/64] #211 IAT reader modifications #211 IAT reader modifications --- reader.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/reader.go b/reader.go index 06672f9f7..5fb7f2c12 100644 --- a/reader.go +++ b/reader.go @@ -136,12 +136,26 @@ func (r *Reader) parseLine() error { return err } case batchHeaderPos: - if err := r.parseBatchHeader(); err != nil { - return err + switch r.line[50:53] { + case "IAT": + if err := r.parseIATBatchHeader(); err != nil { + return err + } + default: + if err := r.parseBatchHeader(); err != nil { + return err + } } case entryDetailPos: - if err := r.parseEntryDetail(); err != nil { - return err + switch r.line[16:29] { + case " ": + if err := r.parseIATEntryDetail(); err != nil { + return err + } + default: + if err := r.parseEntryDetail(); err != nil { + return err + } } case entryAddendaPos: if err := r.parseAddenda(); err != nil { From ecb470712a5afc0d44a617d15e56862655e61fec Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 26 Jun 2018 19:22:40 -0400 Subject: [PATCH 10/64] revert due to gocyclo error revert due to gocyclo error --- reader.go | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/reader.go b/reader.go index 5fb7f2c12..06672f9f7 100644 --- a/reader.go +++ b/reader.go @@ -136,26 +136,12 @@ func (r *Reader) parseLine() error { return err } case batchHeaderPos: - switch r.line[50:53] { - case "IAT": - if err := r.parseIATBatchHeader(); err != nil { - return err - } - default: - if err := r.parseBatchHeader(); err != nil { - return err - } + if err := r.parseBatchHeader(); err != nil { + return err } case entryDetailPos: - switch r.line[16:29] { - case " ": - if err := r.parseIATEntryDetail(); err != nil { - return err - } - default: - if err := r.parseEntryDetail(); err != nil { - return err - } + if err := r.parseEntryDetail(); err != nil { + return err } case entryAddendaPos: if err := r.parseAddenda(); err != nil { From 32640ba1c5de20f1633323e653660a7aeab847e6 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 26 Jun 2018 20:45:26 -0400 Subject: [PATCH 11/64] #211 reader.go Add parseBH and parseED --- reader.go | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/reader.go b/reader.go index 06672f9f7..ed5cf9c03 100644 --- a/reader.go +++ b/reader.go @@ -136,11 +136,11 @@ 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: @@ -350,3 +350,31 @@ func (r *Reader) parseIATEntryDetail() error { r.IATCurrentBatch.AddEntry(ed) 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 { + if r.line[16:29] == " " { + if err := r.parseIATEntryDetail(); err != nil { + return err + } + } else { + if err := r.parseEntryDetail(); err != nil { + return err + } + } + return nil +} From 8e5fd7e229b99ec67ba7ea1d76bed15913baeade Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 26 Jun 2018 22:26:52 -0400 Subject: [PATCH 12/64] #211 Writer #211 Writer --- writer.go | 68 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/writer.go b/writer.go index 688966608..1822d46ad 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,31 @@ 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++ + for _, addenda := range entry.Addendum { + if _, err := w.w.WriteString(addenda.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 } From 682bc006249a972e5742439b9cef25125cae9922 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 26 Jun 2018 22:31:38 -0400 Subject: [PATCH 13/64] #211 iatBatch #211 iatBatch --- iatBatch.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iatBatch.go b/iatBatch.go index 663044891..6a3357b6d 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -307,6 +307,8 @@ func (batch *iatBatch) isTraceNumberODFI() error { return nil } +// ToDo: Adjustments for IAT Addenda + // isAddendaSequence check multiple errors on addenda records in the batch entries func (batch *iatBatch) isAddendaSequence() error { for _, entry := range batch.Entries { From 9bfd27703611d98eea935c5657bcd50833b707c4 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Wed, 27 Jun 2018 11:03:47 -0400 Subject: [PATCH 14/64] #211 reader.go #211 reader.go --- reader.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reader.go b/reader.go index ed5cf9c03..c39791610 100644 --- a/reader.go +++ b/reader.go @@ -367,6 +367,8 @@ func (r *Reader) parseBH() error { // 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. + // IATIndicator field if r.line[16:29] == " " { if err := r.parseIATEntryDetail(); err != nil { return err From 5693d61c19161b8eea1d489a6a65885d8651a26f Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 28 Jun 2018 12:39:44 -0400 Subject: [PATCH 15/64] #211 Update unrelated to IAT Addenda02_test #211 Updates to Addenda02_test --- addenda02_test.go | 67 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/addenda02_test.go b/addenda02_test.go index dcc0b26b5..c174f126a 100644 --- a/addenda02_test.go +++ b/addenda02_test.go @@ -23,6 +23,7 @@ func mockAddenda02() *Addenda02 { return addenda02 } +// TestMockAddenda02 validates mockAddenda02 func TestMockAddenda02(t *testing.T) { addenda02 := mockAddenda02() if err := addenda02.Validate(); err != nil { @@ -30,6 +31,7 @@ func TestMockAddenda02(t *testing.T) { } } +// testAddenda02ValidRecordType validates Addenda02 recordType func testAddenda02ValidRecordType(t testing.TB) { addenda02 := mockAddenda02() addenda02.recordType = "63" @@ -43,10 +45,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 +59,7 @@ func BenchmarkAddenda02ValidRecordType(b *testing.B) { } } +// testAddenda02ValidTypeCode validates Addenda02 TypeCode func testAddenda02ValidTypeCode(t testing.TB) { addenda02 := mockAddenda02() addenda02.typeCode = "65" @@ -67,10 +73,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 +87,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 +101,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 +115,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 +128,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) { +// testAddenda02FieldInclusionRecordType validates TypeCode fieldInclusion +func testAddenda02FieldInclusionTypeCode(t testing.TB) { addenda02 := mockAddenda02() addenda02.typeCode = "" if err := addenda02.Validate(); err != nil { @@ -137,17 +154,20 @@ func testAddenda02TypeCode(t testing.TB) { } } -func TestAddenda02TypeCode(t *testing.T) { - testAddenda02TypeCode(t) +// TestAddenda02FieldInclusionRecordType tests validating TypeCode fieldInclusion +func TestAddenda02FieldInclusionTypeCode(t *testing.T) { + testAddenda02FieldInclusionTypeCode(t) } -func BenchmarkAddenda02TypeCode(b *testing.B) { +// BenchmarkAddenda02FieldInclusionRecordType 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 +180,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 +193,7 @@ func BenchmarkAddenda02TerminalIdentificationCode(b *testing.B) { } } +// testAddenda02TransactionSerialNumber validates TransactionSerialNumber is required func testAddenda02TransactionSerialNumber(t testing.TB) { addenda02 := mockAddenda02() addenda02.TransactionSerialNumber = "" @@ -183,10 +206,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 +219,7 @@ func BenchmarkAddenda02TransactionSerialNumber(b *testing.B) { } } +// testAddenda02TransactionDate validates TransactionDate is required func testAddenda02TransactionDate(t testing.TB) { addenda02 := mockAddenda02() addenda02.TransactionDate = "" @@ -206,10 +232,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 +245,7 @@ func BenchmarkAddenda02TransactionDate(b *testing.B) { } } +// testAddenda02TerminalLocation validates TerminalLocation is required func testAddenda02TerminalLocation(t testing.TB) { addenda02 := mockAddenda02() addenda02.TerminalLocation = "" @@ -229,10 +258,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 +271,7 @@ func BenchmarkAddenda02TerminalLocation(b *testing.B) { } } +// testAddenda02TerminalCity validates TerminalCity is required func testAddenda02TerminalCity(t testing.TB) { addenda02 := mockAddenda02() addenda02.TerminalCity = "" @@ -252,10 +284,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 +297,7 @@ func BenchmarkAddenda02TerminalCity(b *testing.B) { } } +// testAddenda02TerminalState validates TerminalState is required func testAddenda02TerminalState(t testing.TB) { addenda02 := mockAddenda02() addenda02.TerminalState = "" @@ -275,10 +310,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++ { @@ -332,7 +369,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 +397,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 +425,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 +453,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 +481,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 +509,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++ { From 04da5114b6a65adacb2a1b43c5ea2b06475bb633 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 28 Jun 2018 12:44:39 -0400 Subject: [PATCH 16/64] #211 Addenda10 TransactionTypeCode #211 Addenda10 TransactionTypeCode --- validators.go | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/validators.go b/validators.go index 47f359897..5efe892b2 100644 --- a/validators.go +++ b/validators.go @@ -34,6 +34,7 @@ var ( // IAT msgForeignExchangeIndicator = "is an invalid Foreign Exchange Indicator" msgForeignExchangeReferenceIndicator = "is an invalid Foreign Exchange Reference Indicator" + msgAddenda10TransactionTypeCode = "is an invalid Addenda10 Transaction Type Code" ) // validator is common validation and formatting of golang types to ach type strings @@ -239,20 +240,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: @@ -364,6 +365,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(msgAddenda10TransactionTypeCode) +} + // isUpperAlphanumeric checks if string only contains ASCII alphanumeric upper case characters func (v *validator) isUpperAlphanumeric(s string) error { if upperAlphanumericRegex.MatchString(s) { From ff4db1f4562c56ea4f45643f1132965205e9d03c Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 28 Jun 2018 12:52:27 -0400 Subject: [PATCH 17/64] # 211 Addenda10 Addenda10 code Addenda10_test --- addenda10.go | 173 ++++++++++++++++++++++++++++++++++++++++++++++ addenda10_test.go | 161 ++++++++++++++++++++++++++++++++++++++++++ validators.go | 4 +- 3 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 addenda10.go create mode 100644 addenda10_test.go diff --git a/addenda10.go b/addenda10.go new file mode 100644 index 000000000..e37697451 --- /dev/null +++ b/addenda10.go @@ -0,0 +1,173 @@ +// 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" +) + +// Addenda10 is a Addendumer 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. +// It is mandatory for IAT entries +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 = addenda10.parseStringField(record[24:46]) + // 47-81 Receiving Company Name/Individual Name + addenda10.Name = record[47: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()} + } + if err := addenda10.isTransactionTypeCode(addenda10.TransactionTypeCode); err != nil { + return &FieldError{FieldName: "TransactionTypeCode", Value: addenda10.TransactionTypeCode, Msg: err.Error()} + } + // ToDo: Foreign Exchange Amount + 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} + } + 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: msgFieldRequired} + } + if addenda10.EntryDetailSequenceNumber == 0 { + return &FieldError{FieldName: "EntryDetailSequenceNumber", + Value: addenda10.EntryDetailSequenceNumberField(), Msg: msgFieldInclusion} + } + return nil +} + +// ForeignPaymentAmountField returns ForeignPaymentAmount zero padded +// Payment Amount For inbound IAT payments this field should contain the USD amount or may be blank. +// 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, 35) +} + +// NameField gets th name field - Receiving Company Name/Individual Name left padded +func (addenda10 *Addenda10) NameField() string { + return addenda10.alphaField(addenda10.Name, 22) +} + +// 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..fa177aef3 --- /dev/null +++ b/addenda10_test.go @@ -0,0 +1,161 @@ +// 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" + +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") + } +} + +// 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) + } +} + +// 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) + } +} + +// testAddenda10FieldInclusionRecordType 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) + } + } + } +} + +// TestAddenda10FieldInclusionRecordType tests validating TypeCode fieldInclusion +func TestAddenda10FieldInclusionTypeCode(t *testing.T) { + testAddenda10FieldInclusionTypeCode(t) +} + +// BenchmarkAddenda10FieldInclusionRecordType benchmarks validating TypeCode fieldInclusion +func BenchmarkAddenda10FieldInclusionTypeCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testAddenda10FieldInclusionTypeCode(b) + } +} diff --git a/validators.go b/validators.go index 5efe892b2..382be9931 100644 --- a/validators.go +++ b/validators.go @@ -34,7 +34,7 @@ var ( // IAT msgForeignExchangeIndicator = "is an invalid Foreign Exchange Indicator" msgForeignExchangeReferenceIndicator = "is an invalid Foreign Exchange Reference Indicator" - msgAddenda10TransactionTypeCode = "is an invalid Addenda10 Transaction Type Code" + msgTransactionTypeCode = "is an invalid Addenda10 Transaction Type Code" ) // validator is common validation and formatting of golang types to ach type strings @@ -377,7 +377,7 @@ func (v *validator) isTransactionTypeCode(s string) error { "ARC", "BOC", "POP", "RCK": return nil } - return errors.New(msgAddenda10TransactionTypeCode) + return errors.New(msgTransactionTypeCode) } // isUpperAlphanumeric checks if string only contains ASCII alphanumeric upper case characters From d60b70eb1f0290a7282fc0d85f0c02c697119278 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 28 Jun 2018 13:16:50 -0400 Subject: [PATCH 18/64] #211 Addenda10 code coverage tests #211 Addenda10 code coverage tests --- addenda02_test.go | 6 +++--- addenda10.go | 6 +++--- addenda10_test.go | 31 ++++++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/addenda02_test.go b/addenda02_test.go index c174f126a..476418131 100644 --- a/addenda02_test.go +++ b/addenda02_test.go @@ -323,7 +323,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" @@ -336,12 +336,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++ { diff --git a/addenda10.go b/addenda10.go index e37697451..e34d1cef8 100644 --- a/addenda10.go +++ b/addenda10.go @@ -66,7 +66,7 @@ func (addenda10 *Addenda10) Parse(record string) { // 25-46 Insert blanks or zeros addenda10.ForeignTraceNumber = addenda10.parseStringField(record[24:46]) // 47-81 Receiving Company Name/Individual Name - addenda10.Name = record[47:81] + addenda10.Name = 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 @@ -149,12 +149,12 @@ func (addenda10 *Addenda10) ForeignPaymentAmountField() string { // ForeignTraceNumberField gets the Foreign TraceNumber left padded func (addenda10 *Addenda10) ForeignTraceNumberField() string { - return addenda10.alphaField(addenda10.ForeignTraceNumber, 35) + return addenda10.alphaField(addenda10.ForeignTraceNumber, 22) } // NameField gets th name field - Receiving Company Name/Individual Name left padded func (addenda10 *Addenda10) NameField() string { - return addenda10.alphaField(addenda10.Name, 22) + return addenda10.alphaField(addenda10.Name, 35) } // reservedField gets reserved - blank space diff --git a/addenda10_test.go b/addenda10_test.go index fa177aef3..7905be81f 100644 --- a/addenda10_test.go +++ b/addenda10_test.go @@ -4,7 +4,9 @@ package ach -import "testing" +import ( + "testing" +) func mockAddenda10() *Addenda10 { addenda10 := NewAddenda10() @@ -159,3 +161,30 @@ func BenchmarkAddenda10FieldInclusionTypeCode(b *testing.B) { testAddenda10FieldInclusionTypeCode(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) + } +} From 4a3affba6aab7b42acdc6e4548106dae4903d71a Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 28 Jun 2018 13:52:09 -0400 Subject: [PATCH 19/64] #211 Addenda10 code coverage #211 Addenda10 code coverage --- addenda02_test.go | 1 + addenda10.go | 4 + addenda10_test.go | 189 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+) diff --git a/addenda02_test.go b/addenda02_test.go index 476418131..26bcb2373 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" diff --git a/addenda10.go b/addenda10.go index e34d1cef8..9f5f57109 100644 --- a/addenda10.go +++ b/addenda10.go @@ -100,6 +100,10 @@ func (addenda10 *Addenda10) Validate() error { 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()} } diff --git a/addenda10_test.go b/addenda10_test.go index 7905be81f..1d16e179b 100644 --- a/addenda10_test.go +++ b/addenda10_test.go @@ -8,6 +8,7 @@ import ( "testing" ) +// mockAddenda10() creates a mock Addenda10 record func mockAddenda10() *Addenda10 { addenda10 := NewAddenda10() addenda10.TransactionTypeCode = "ANN" @@ -110,6 +111,86 @@ func BenchmarkAddenda10TypeCode10(b *testing.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() @@ -162,6 +243,114 @@ func BenchmarkAddenda10FieldInclusionTypeCode(b *testing.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 != msgFieldRequired { + 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() From 6c7b5683e133471779d77c4f827539d116238c0d Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 28 Jun 2018 14:07:09 -0400 Subject: [PATCH 20/64] #211 Test review modifications #211 Test review modifications --- addenda10.go | 9 ++++----- addenda10_test.go | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/addenda10.go b/addenda10.go index 9f5f57109..055f388ef 100644 --- a/addenda10.go +++ b/addenda10.go @@ -107,7 +107,7 @@ func (addenda10 *Addenda10) Validate() error { if err := addenda10.isTransactionTypeCode(addenda10.TransactionTypeCode); err != nil { return &FieldError{FieldName: "TransactionTypeCode", Value: addenda10.TransactionTypeCode, Msg: err.Error()} } - // ToDo: Foreign Exchange Amount + // ToDo: Foreign Exchange Amount blank ? if err := addenda10.isAlphanumeric(addenda10.ForeignTraceNumber); err != nil { return &FieldError{FieldName: "ForeignTraceNumber", Value: addenda10.ForeignTraceNumber, Msg: err.Error()} } @@ -135,7 +135,7 @@ func (addenda10 *Addenda10) fieldInclusion() error { Value: strconv.Itoa(addenda10.ForeignPaymentAmount), Msg: msgFieldRequired} } if addenda10.Name == "" { - return &FieldError{FieldName: "Name", Value: addenda10.Name, Msg: msgFieldRequired} + return &FieldError{FieldName: "Name", Value: addenda10.Name, Msg: msgFieldInclusion} } if addenda10.EntryDetailSequenceNumber == 0 { return &FieldError{FieldName: "EntryDetailSequenceNumber", @@ -145,8 +145,7 @@ func (addenda10 *Addenda10) fieldInclusion() error { } // ForeignPaymentAmountField returns ForeignPaymentAmount zero padded -// Payment Amount For inbound IAT payments this field should contain the USD amount or may be blank. -// ToDo: Review/Add logic for blank +// ToDo: Review/Add logic for blank ? func (addenda10 *Addenda10) ForeignPaymentAmountField() string { return addenda10.numericField(addenda10.ForeignPaymentAmount, 18) } @@ -156,7 +155,7 @@ func (addenda10 *Addenda10) ForeignTraceNumberField() string { return addenda10.alphaField(addenda10.ForeignTraceNumber, 22) } -// NameField gets th name field - Receiving Company Name/Individual Name left padded +// NameField gets the name field - Receiving Company Name/Individual Name left padded func (addenda10 *Addenda10) NameField() string { return addenda10.alphaField(addenda10.Name, 35) } diff --git a/addenda10_test.go b/addenda10_test.go index 1d16e179b..20e719e49 100644 --- a/addenda10_test.go +++ b/addenda10_test.go @@ -303,7 +303,7 @@ func testAddenda10FieldInclusionName(t testing.TB) { addenda10.Name = "" if err := addenda10.Validate(); err != nil { if e, ok := err.(*FieldError); ok { - if e.Msg != msgFieldRequired { + if e.Msg != msgFieldInclusion { t.Errorf("%T: %s", err, err) } } From ee974c3581a632f4b87773fc40cdeb031042fbe8 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 28 Jun 2018 16:59:23 -0400 Subject: [PATCH 21/64] #211 addenda11 and addenda12 #211 addenda11 and addenda12 --- README.md | 10 +- addenda10.go | 6 +- addenda11.go | 150 +++++++++++++++++++++ addenda11_test.go | 321 +++++++++++++++++++++++++++++++++++++++++++++ addenda12.go | 158 ++++++++++++++++++++++ addenda12_test.go | 327 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 967 insertions(+), 5 deletions(-) create mode 100644 addenda11.go create mode 100644 addenda11_test.go create mode 100644 addenda12.go create mode 100644 addenda12_test.go diff --git a/README.md b/README.md index 4c14fcdc6..32bbcbcaf 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,12 @@ 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) + ## 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 diff --git a/addenda10.go b/addenda10.go index 055f388ef..c220caa21 100644 --- a/addenda10.go +++ b/addenda10.go @@ -11,7 +11,11 @@ import ( // Addenda10 is a Addendumer 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. -// It is mandatory for IAT entries +// +// Addenda10 is mandatory for IAT entries +// +// The First Addenda 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"` diff --git a/addenda11.go b/addenda11.go new file mode 100644 index 000000000..81daa9277 --- /dev/null +++ b/addenda11.go @@ -0,0 +1,150 @@ +// 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" +) + +// Addenda11 is a Addendumer 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 = record[3:38] + // 38-73 + addenda11.OriginatorStreetAddress = 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..cb2b26cf5 --- /dev/null +++ b/addenda11_test.go @@ -0,0 +1,321 @@ +// 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") + } +} + +// 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) + } + } + } +} + +// TestNameOriginatorAlphaNumeric 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) + } + } + } +} + +// TestNameOriginatorAlphaNumeric 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) + } +} + +// testAddenda11FieldInclusionRecordType 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) + } + } + } +} + +// TestAddenda11FieldInclusionRecordType tests validating TypeCode fieldInclusion +func TestAddenda11FieldInclusionTypeCode(t *testing.T) { + testAddenda11FieldInclusionTypeCode(t) +} + +// BenchmarkAddenda11FieldInclusionRecordType 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..c591faf20 --- /dev/null +++ b/addenda12.go @@ -0,0 +1,158 @@ +// 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" +) + +// Addenda12 is a Addendumer 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 = record[3:38] + // 38-73 + addenda12.OriginatorCountryPostalCode = 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..207c60e20 --- /dev/null +++ b/addenda12_test.go @@ -0,0 +1,327 @@ +// 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 = "US19305\\" + 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") + } +} + +// 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) + } + } + } +} + +// TestNameOriginatorAlphaNumeric 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) + } + } + } +} + +// TestNameOriginatorAlphaNumeric 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) + } +} + +// testAddenda12FieldInclusionRecordType 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) + } + } + } +} + +// TestAddenda12FieldInclusionRecordType tests validating TypeCode fieldInclusion +func TestAddenda12FieldInclusionTypeCode(t *testing.T) { + testAddenda12FieldInclusionTypeCode(t) +} + +// BenchmarkAddenda12FieldInclusionRecordType 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\\ " + + "US19305\\ " + + " " + + "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) + } +} From 736d89a9d2f3e7ea55bf8c3472bafa98e0c6094a Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 29 Jun 2018 00:28:40 -0400 Subject: [PATCH 22/64] #211 Comment Changes #211 Comment Changes --- addenda11_test.go | 4 ++-- addenda12_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addenda11_test.go b/addenda11_test.go index cb2b26cf5..8251ad68e 100644 --- a/addenda11_test.go +++ b/addenda11_test.go @@ -122,7 +122,7 @@ func testOriginatorNameAlphaNumeric(t testing.TB) { } } -// TestNameOriginatorAlphaNumeric tests validating OriginatorName is alphanumeric +// TestOriginatorNameAlphaNumeric tests validating OriginatorName is alphanumeric func TestOriginatorNameAlphaNumeric(t *testing.T) { testOriginatorNameAlphaNumeric(t) } @@ -148,7 +148,7 @@ func testOriginatorStreetAddressAlphaNumeric(t testing.TB) { } } -// TestNameOriginatorAlphaNumeric tests validating OriginatorStreetAddress is alphanumeric +// TestOriginatorStreetAddressAlphaNumeric tests validating OriginatorStreetAddress is alphanumeric func TestOriginatorStreetAddressAlphaNumeric(t *testing.T) { testOriginatorStreetAddressAlphaNumeric(t) } diff --git a/addenda12_test.go b/addenda12_test.go index 207c60e20..10a67cec6 100644 --- a/addenda12_test.go +++ b/addenda12_test.go @@ -122,7 +122,7 @@ func testOriginatorCityStateProvinceAlphaNumeric(t testing.TB) { } } -// TestNameOriginatorAlphaNumeric tests validating OriginatorCityStateProvince is alphanumeric +// TestOriginatorCityStateProvinceAlphaNumeric tests validating OriginatorCityStateProvince is alphanumeric func TestOriginatorCityStateProvinceAlphaNumeric(t *testing.T) { testOriginatorCityStateProvinceAlphaNumeric(t) } @@ -148,7 +148,7 @@ func testOriginatorCountryPostalCodeAlphaNumeric(t testing.TB) { } } -// TestNameOriginatorAlphaNumeric tests validating OriginatorCountryPostalCode is alphanumeric +// TestOriginatorCountryPostalCodeAlphaNumeric tests validating OriginatorCountryPostalCode is alphanumeric func TestOriginatorCountryPostalCodeAlphaNumeric(t *testing.T) { testOriginatorCountryPostalCodeAlphaNumeric(t) } From 0fedc457299ad5b0f66c37c5c4e33fa264e2dfbf Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 29 Jun 2018 13:50:44 -0400 Subject: [PATCH 23/64] #211 Addenda 13 and Addenda14 Addenda 13 and Addenda14 --- addenda11.go | 2 +- addenda13.go | 206 ++++++++++++++++++++++ addenda13_test.go | 428 ++++++++++++++++++++++++++++++++++++++++++++++ addenda14.go | 202 ++++++++++++++++++++++ addenda14_test.go | 428 ++++++++++++++++++++++++++++++++++++++++++++++ validators.go | 17 ++ 6 files changed, 1282 insertions(+), 1 deletion(-) create mode 100644 addenda13.go create mode 100644 addenda13_test.go create mode 100644 addenda14.go create mode 100644 addenda14_test.go diff --git a/addenda11.go b/addenda11.go index 81daa9277..1b3335269 100644 --- a/addenda11.go +++ b/addenda11.go @@ -55,7 +55,7 @@ func (addenda11 *Addenda11) Parse(record string) { addenda11.typeCode = record[1:3] // 4-38 addenda11.OriginatorName = record[3:38] - // 38-73 + // 39-73 addenda11.OriginatorStreetAddress = record[38:73] // 74-87 reserved - Leave blank addenda11.reserved = " " diff --git a/addenda13.go b/addenda13.go new file mode 100644 index 000000000..874221db0 --- /dev/null +++ b/addenda13.go @@ -0,0 +1,206 @@ +// 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" +) + +// Addenda13 is a Addendumer 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 = record[3:38] + // 39-40 ODFIIDNumberQualifier + addenda13.ODFIIDNumberQualifier = record[38:40] + // 41-74 ODFIIdentification + addenda13.ODFIIdentification = record[40:74] + // 75-77 + addenda13.ODFIBranchCountryCode = 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, 2) + " " +} + +// 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..e6d930c9e --- /dev/null +++ b/addenda13_test.go @@ -0,0 +1,428 @@ +// 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 +} + +// 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) + } +} + +// testAddenda13FieldInclusionRecordType 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) + } + } + } +} + +// TestAddenda13FieldInclusionRecordType tests validating TypeCode fieldInclusion +func TestAddenda13FieldInclusionTypeCode(t *testing.T) { + testAddenda13FieldInclusionTypeCode(t) +} + +// BenchmarkAddenda13FieldInclusionRecordType 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..38cbc7299 --- /dev/null +++ b/addenda14.go @@ -0,0 +1,202 @@ +// 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" +) + +// Addenda14 is a Addendumer 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 = record[3:38] + // 39-40 RDFIIDNumberQualifier + addenda14.RDFIIDNumberQualifier = record[38:40] + // 41-74 RDFIIdentification + addenda14.RDFIIdentification = record[40:74] + // 75-77 + addenda14.RDFIBranchCountryCode = 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, 2) + " " +} + +// 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..3106abb48 --- /dev/null +++ b/addenda14_test.go @@ -0,0 +1,428 @@ +// 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") + } +} + +// 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) + } +} + +// testAddenda14FieldInclusionRecordType 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) + } + } + } +} + +// TestAddenda14FieldInclusionRecordType tests validating TypeCode fieldInclusion +func TestAddenda14FieldInclusionTypeCode(t *testing.T) { + testAddenda14FieldInclusionTypeCode(t) +} + +// BenchmarkAddenda14FieldInclusionRecordType 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/validators.go b/validators.go index 382be9931..e8263d902 100644 --- a/validators.go +++ b/validators.go @@ -35,6 +35,7 @@ var ( 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 @@ -170,6 +171,22 @@ func (v *validator) isForeignExchangeReferenceIndicator(code int) error { 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 { From 83986e300e862f7f6dc15bd654978d2de78840f1 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 29 Jun 2018 14:48:53 -0400 Subject: [PATCH 24/64] #211 Code Comment Modifications #211 Code Comment Modifications --- addenda02_test.go | 6 +++--- addenda10_test.go | 6 +++--- addenda11_test.go | 6 +++--- addenda12_test.go | 6 +++--- addenda13_test.go | 6 +++--- addenda14_test.go | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/addenda02_test.go b/addenda02_test.go index 26bcb2373..adb20c3ca 100644 --- a/addenda02_test.go +++ b/addenda02_test.go @@ -142,7 +142,7 @@ func BenchmarkAddenda02FieldInclusionRecordType(b *testing.B) { } } -// testAddenda02FieldInclusionRecordType validates TypeCode fieldInclusion +// testAddenda02FieldInclusionTypeCode validates TypeCode fieldInclusion func testAddenda02FieldInclusionTypeCode(t testing.TB) { addenda02 := mockAddenda02() addenda02.typeCode = "" @@ -155,12 +155,12 @@ func testAddenda02FieldInclusionTypeCode(t testing.TB) { } } -// TestAddenda02FieldInclusionRecordType tests validating TypeCode fieldInclusion +// TestAddenda02FieldInclusionTypeCode tests validating TypeCode fieldInclusion func TestAddenda02FieldInclusionTypeCode(t *testing.T) { testAddenda02FieldInclusionTypeCode(t) } -// BenchmarkAddenda02FieldInclusionRecordType benchmarks validating TypeCode fieldInclusion +// BenchmarkAddenda02FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion func BenchmarkAddenda02FieldInclusionTypeCode(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { diff --git a/addenda10_test.go b/addenda10_test.go index 20e719e49..a76d802e9 100644 --- a/addenda10_test.go +++ b/addenda10_test.go @@ -217,7 +217,7 @@ func BenchmarkAddenda10FieldInclusionRecordType(b *testing.B) { } } -// testAddenda10FieldInclusionRecordType validates TypeCode fieldInclusion +// testAddenda10FieldInclusionTypeCode validates TypeCode fieldInclusion func testAddenda10FieldInclusionTypeCode(t testing.TB) { addenda10 := mockAddenda10() addenda10.typeCode = "" @@ -230,12 +230,12 @@ func testAddenda10FieldInclusionTypeCode(t testing.TB) { } } -// TestAddenda10FieldInclusionRecordType tests validating TypeCode fieldInclusion +// TestAddenda10FieldInclusionTypeCode tests validating TypeCode fieldInclusion func TestAddenda10FieldInclusionTypeCode(t *testing.T) { testAddenda10FieldInclusionTypeCode(t) } -// BenchmarkAddenda10FieldInclusionRecordType benchmarks validating TypeCode fieldInclusion +// BenchmarkAddenda10FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion func BenchmarkAddenda10FieldInclusionTypeCode(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { diff --git a/addenda11_test.go b/addenda11_test.go index 8251ad68e..a36a835d3 100644 --- a/addenda11_test.go +++ b/addenda11_test.go @@ -187,7 +187,7 @@ func BenchmarkAddenda11FieldInclusionRecordType(b *testing.B) { } } -// testAddenda11FieldInclusionRecordType validates TypeCode fieldInclusion +// testAddenda11FieldInclusionTypeCode validates TypeCode fieldInclusion func testAddenda11FieldInclusionTypeCode(t testing.TB) { addenda11 := mockAddenda11() addenda11.typeCode = "" @@ -200,12 +200,12 @@ func testAddenda11FieldInclusionTypeCode(t testing.TB) { } } -// TestAddenda11FieldInclusionRecordType tests validating TypeCode fieldInclusion +// TestAddenda11FieldInclusionTypeCode tests validating TypeCode fieldInclusion func TestAddenda11FieldInclusionTypeCode(t *testing.T) { testAddenda11FieldInclusionTypeCode(t) } -// BenchmarkAddenda11FieldInclusionRecordType benchmarks validating TypeCode fieldInclusion +// BenchmarkAddenda11FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion func BenchmarkAddenda11FieldInclusionTypeCode(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { diff --git a/addenda12_test.go b/addenda12_test.go index 10a67cec6..6e099dd2e 100644 --- a/addenda12_test.go +++ b/addenda12_test.go @@ -187,7 +187,7 @@ func BenchmarkAddenda12FieldInclusionRecordType(b *testing.B) { } } -// testAddenda12FieldInclusionRecordType validates TypeCode fieldInclusion +// testAddenda12FieldInclusionTypeCode validates TypeCode fieldInclusion func testAddenda12FieldInclusionTypeCode(t testing.TB) { addenda12 := mockAddenda12() addenda12.typeCode = "" @@ -200,12 +200,12 @@ func testAddenda12FieldInclusionTypeCode(t testing.TB) { } } -// TestAddenda12FieldInclusionRecordType tests validating TypeCode fieldInclusion +// TestAddenda12FieldInclusionTypeCode tests validating TypeCode fieldInclusion func TestAddenda12FieldInclusionTypeCode(t *testing.T) { testAddenda12FieldInclusionTypeCode(t) } -// BenchmarkAddenda12FieldInclusionRecordType benchmarks validating TypeCode fieldInclusion +// BenchmarkAddenda12FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion func BenchmarkAddenda12FieldInclusionTypeCode(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { diff --git a/addenda13_test.go b/addenda13_test.go index e6d930c9e..29cf8b5ef 100644 --- a/addenda13_test.go +++ b/addenda13_test.go @@ -241,7 +241,7 @@ func BenchmarkAddenda13FieldInclusionRecordType(b *testing.B) { } } -// testAddenda13FieldInclusionRecordType validates TypeCode fieldInclusion +// testAddenda13FieldInclusionTypeCode validates TypeCode fieldInclusion func testAddenda13FieldInclusionTypeCode(t testing.TB) { addenda13 := mockAddenda13() addenda13.typeCode = "" @@ -254,12 +254,12 @@ func testAddenda13FieldInclusionTypeCode(t testing.TB) { } } -// TestAddenda13FieldInclusionRecordType tests validating TypeCode fieldInclusion +// TestAddenda13FieldInclusionTypeCode tests validating TypeCode fieldInclusion func TestAddenda13FieldInclusionTypeCode(t *testing.T) { testAddenda13FieldInclusionTypeCode(t) } -// BenchmarkAddenda13FieldInclusionRecordType benchmarks validating TypeCode fieldInclusion +// BenchmarkAddenda13FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion func BenchmarkAddenda13FieldInclusionTypeCode(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { diff --git a/addenda14_test.go b/addenda14_test.go index 3106abb48..a2a3c654f 100644 --- a/addenda14_test.go +++ b/addenda14_test.go @@ -241,7 +241,7 @@ func BenchmarkAddenda14FieldInclusionRecordType(b *testing.B) { } } -// testAddenda14FieldInclusionRecordType validates TypeCode fieldInclusion +// testAddenda14FieldInclusionTypeCode validates TypeCode fieldInclusion func testAddenda14FieldInclusionTypeCode(t testing.TB) { addenda14 := mockAddenda14() addenda14.typeCode = "" @@ -254,12 +254,12 @@ func testAddenda14FieldInclusionTypeCode(t testing.TB) { } } -// TestAddenda14FieldInclusionRecordType tests validating TypeCode fieldInclusion +// TestAddenda14FieldInclusionTypeCode tests validating TypeCode fieldInclusion func TestAddenda14FieldInclusionTypeCode(t *testing.T) { testAddenda14FieldInclusionTypeCode(t) } -// BenchmarkAddenda14FieldInclusionRecordType benchmarks validating TypeCode fieldInclusion +// BenchmarkAddenda14FieldInclusionTypeCode benchmarks validating TypeCode fieldInclusion func BenchmarkAddenda14FieldInclusionTypeCode(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { From a2fda039719ee0a19ecdab2ec579e903ba1532c9 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 29 Jun 2018 16:34:33 -0400 Subject: [PATCH 25/64] #211 addenda15 and addenda16 #211 addenda15 and addenda16 --- addenda12.go | 2 +- addenda15.go | 147 +++++++++++++++++++++ addenda15_test.go | 295 +++++++++++++++++++++++++++++++++++++++++ addenda16.go | 158 ++++++++++++++++++++++ addenda16_test.go | 327 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 928 insertions(+), 1 deletion(-) create mode 100644 addenda15.go create mode 100644 addenda15_test.go create mode 100644 addenda16.go create mode 100644 addenda16_test.go diff --git a/addenda12.go b/addenda12.go index c591faf20..6f79f0f7d 100644 --- a/addenda12.go +++ b/addenda12.go @@ -60,7 +60,7 @@ func (addenda12 *Addenda12) Parse(record string) { addenda12.typeCode = record[1:3] // 4-38 addenda12.OriginatorCityStateProvince = record[3:38] - // 38-73 + // 39-73 addenda12.OriginatorCountryPostalCode = record[38:73] // 74-87 reserved - Leave blank addenda12.reserved = " " diff --git a/addenda15.go b/addenda15.go new file mode 100644 index 000000000..dd5388e23 --- /dev/null +++ b/addenda15.go @@ -0,0 +1,147 @@ +// 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" +) + +// Addenda15 is a Addendumer 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 = record[3:18] + // 19-53 + addenda15.ReceiverStreetAddress = 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..631d611cf --- /dev/null +++ b/addenda15_test.go @@ -0,0 +1,295 @@ +// 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") + } +} + +// 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..f26f1c1c0 --- /dev/null +++ b/addenda16.go @@ -0,0 +1,158 @@ +// 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" +) + +// Addenda16 is a Addendumer 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 + addenda16.ReceiverCityStateProvince = record[3:38] + // 39-73 + addenda16.ReceiverCountryPostalCode = 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(), + // ToDo: Validator for backslash + 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..474f571e4 --- /dev/null +++ b/addenda16_test.go @@ -0,0 +1,327 @@ +// 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*CO\\" + addenda16.ReceiverCountryPostalCode = "US80014\\" + 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") + } +} + +// 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*CO\\ " + + "US80014\\ " + + " " + + "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) + } +} From 4dd05173905a5fa67198e7b1c03b7805d56ffac2 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 29 Jun 2018 16:41:46 -0400 Subject: [PATCH 26/64] #211 gofmt addenda16.go #211 gofmt addenda16.go --- addenda16.go | 1 - 1 file changed, 1 deletion(-) diff --git a/addenda16.go b/addenda16.go index f26f1c1c0..b9de7d1fa 100644 --- a/addenda16.go +++ b/addenda16.go @@ -155,4 +155,3 @@ func (addenda16 *Addenda16) TypeCode() string { func (addenda16 *Addenda16) EntryDetailSequenceNumberField() string { return addenda16.numericField(addenda16.EntryDetailSequenceNumber, 7) } - From 7b1486dc921d53997d56c700999204b2ccbed351 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 29 Jun 2018 18:23:05 -0400 Subject: [PATCH 27/64] #211 Added ToDo #211 Added ToDo --- addenda10_test.go | 2 ++ addenda11_test.go | 2 ++ addenda13_test.go | 2 ++ addenda14_test.go | 2 ++ addenda15_test.go | 2 ++ addenda16_test.go | 2 ++ 6 files changed, 12 insertions(+) diff --git a/addenda10_test.go b/addenda10_test.go index a76d802e9..53efccf74 100644 --- a/addenda10_test.go +++ b/addenda10_test.go @@ -351,6 +351,8 @@ func BenchmarkAddenda10FieldInclusionEntryDetailSequenceNumber(b *testing.B) { } } +// ToDo Add Parse test for individual fields + // 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() diff --git a/addenda11_test.go b/addenda11_test.go index a36a835d3..0e736a337 100644 --- a/addenda11_test.go +++ b/addenda11_test.go @@ -293,6 +293,8 @@ func BenchmarkAddenda11FieldInclusionEntryDetailSequenceNumber(b *testing.B) { } } +// ToDo Add Parse test for individual fields + // 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() diff --git a/addenda13_test.go b/addenda13_test.go index 29cf8b5ef..73774bd25 100644 --- a/addenda13_test.go +++ b/addenda13_test.go @@ -399,6 +399,8 @@ func BenchmarkAddenda13FieldInclusionEntryDetailSequenceNumber(b *testing.B) { } } +// ToDo Add Parse test for individual fields + // 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() diff --git a/addenda14_test.go b/addenda14_test.go index a2a3c654f..4f9db902e 100644 --- a/addenda14_test.go +++ b/addenda14_test.go @@ -399,6 +399,8 @@ func BenchmarkAddenda14FieldInclusionEntryDetailSequenceNumber(b *testing.B) { } } +// ToDo Add Parse test for individual fields + // 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() diff --git a/addenda15_test.go b/addenda15_test.go index 631d611cf..125d755ab 100644 --- a/addenda15_test.go +++ b/addenda15_test.go @@ -267,6 +267,8 @@ func BenchmarkAddenda15FieldInclusionEntryDetailSequenceNumber(b *testing.B) { } } +// ToDo Add Parse test for individual fields + // 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() diff --git a/addenda16_test.go b/addenda16_test.go index 474f571e4..9a8be7271 100644 --- a/addenda16_test.go +++ b/addenda16_test.go @@ -293,6 +293,8 @@ func BenchmarkAddenda16FieldInclusionEntryDetailSequenceNumber(b *testing.B) { } } +// ToDo Add Parse test for individual fields + // 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() From 967d525ac62bf90d100ab97a445a2681fa5d5e13 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 29 Jun 2018 22:49:28 -0400 Subject: [PATCH 28/64] #211 remove megacheck #211 remove megacheck --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) 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 From e559bfa9009b6ea2b83876afa6bf6219d538c70d Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 29 Jun 2018 23:02:56 -0400 Subject: [PATCH 29/64] #211 patseStringField Updates #211 patseStringField Updates --- addenda10.go | 3 ++- addenda13.go | 2 +- addenda14.go | 2 +- addenda15.go | 2 +- validators.go | 2 ++ 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/addenda10.go b/addenda10.go index c220caa21..e355b0d87 100644 --- a/addenda10.go +++ b/addenda10.go @@ -68,7 +68,8 @@ func (addenda10 *Addenda10) Parse(record string) { // 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 = addenda10.parseStringField(record[24:46]) + //addenda10.ForeignTraceNumber = addenda10.parseStringField(record[24:46]) + addenda10.ForeignTraceNumber = record[24:46] // 47-81 Receiving Company Name/Individual Name addenda10.Name = record[46:81] // 82-87 reserved - Leave blank diff --git a/addenda13.go b/addenda13.go index 874221db0..adede0dc8 100644 --- a/addenda13.go +++ b/addenda13.go @@ -79,7 +79,7 @@ func (addenda13 *Addenda13) Parse(record string) { // 39-40 ODFIIDNumberQualifier addenda13.ODFIIDNumberQualifier = record[38:40] // 41-74 ODFIIdentification - addenda13.ODFIIdentification = record[40:74] + addenda13.ODFIIdentification = addenda13.parseStringField(record[40:74]) // 75-77 addenda13.ODFIBranchCountryCode = record[74:77] // 78-87 reserved - Leave blank diff --git a/addenda14.go b/addenda14.go index 38cbc7299..f7874c622 100644 --- a/addenda14.go +++ b/addenda14.go @@ -75,7 +75,7 @@ func (addenda14 *Addenda14) Parse(record string) { // 39-40 RDFIIDNumberQualifier addenda14.RDFIIDNumberQualifier = record[38:40] // 41-74 RDFIIdentification - addenda14.RDFIIdentification = record[40:74] + addenda14.RDFIIdentification = addenda14.parseStringField(record[40:74]) // 75-77 addenda14.RDFIBranchCountryCode = record[74:77] // 78-87 reserved - Leave blank diff --git a/addenda15.go b/addenda15.go index dd5388e23..4548c15b3 100644 --- a/addenda15.go +++ b/addenda15.go @@ -55,7 +55,7 @@ func (addenda15 *Addenda15) Parse(record string) { // 2-3 Always 15 addenda15.typeCode = record[1:3] // 4-18 - addenda15.ReceiverIDNumber = record[3:18] + addenda15.ReceiverIDNumber = addenda15.parseStringField(record[3:18]) // 19-53 addenda15.ReceiverStreetAddress = record[18:53] // 54-87 reserved - Leave blank diff --git a/validators.go b/validators.go index e8263d902..b2d3abfda 100644 --- a/validators.go +++ b/validators.go @@ -48,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 From fc7341165d1ad2247d85f1a2845ca052e5d3b792 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Mon, 9 Jul 2018 22:37:20 -0400 Subject: [PATCH 30/64] # 211 Continue code support for IAT Removed IAT Batcher logic, just use IATBatch Extended initial logic in IATBatch to support Addenda 10-17 Added initialal test cases for IAtBatch Added Addenda17 and test cases fro Addenda17 Added Addenda10-17 properties to IATEntryDetail Modified Reader and Writer to use IATBatch instead of IATBatcher --- addenda10.go | 2 +- addenda10_test.go | 2 +- addenda11.go | 2 +- addenda11_test.go | 2 +- addenda12.go | 2 +- addenda12_test.go | 2 +- addenda13.go | 2 +- addenda13_test.go | 2 +- addenda14.go | 2 +- addenda14_test.go | 2 +- addenda15.go | 2 +- addenda15_test.go | 2 +- addenda16.go | 2 +- addenda16_test.go | 2 +- addenda17.go | 136 ++++++++++ addenda17_test.go | 186 +++++++++++++ batch.go | 6 +- batchIAT.go | 4 +- file.go | 4 +- iatBatch.go | 256 ++++++++++++------ iatBatch_test.go | 575 +++++++++++++++++++++++++++++++++++++++++ iatBatcher.go | 55 ---- iatEntryDetail.go | 10 +- iatEntryDetail_test.go | 4 +- reader.go | 9 +- writer.go | 34 ++- 26 files changed, 1143 insertions(+), 164 deletions(-) create mode 100644 addenda17.go create mode 100644 addenda17_test.go create mode 100644 iatBatch_test.go delete mode 100644 iatBatcher.go diff --git a/addenda10.go b/addenda10.go index e355b0d87..ee0afed8c 100644 --- a/addenda10.go +++ b/addenda10.go @@ -9,7 +9,7 @@ import ( "strconv" ) -// Addenda10 is a Addendumer addenda which provides business transaction information for Addenda Type +// 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 diff --git a/addenda10_test.go b/addenda10_test.go index 53efccf74..d101307b2 100644 --- a/addenda10_test.go +++ b/addenda10_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -// mockAddenda10() creates a mock Addenda10 record +// mockAddenda10 creates a mock Addenda10 record func mockAddenda10() *Addenda10 { addenda10 := NewAddenda10() addenda10.TransactionTypeCode = "ANN" diff --git a/addenda11.go b/addenda11.go index 1b3335269..98aec15ff 100644 --- a/addenda11.go +++ b/addenda11.go @@ -8,7 +8,7 @@ import ( "fmt" ) -// Addenda11 is a Addendumer addenda which provides business transaction information for Addenda Type +// 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 diff --git a/addenda11_test.go b/addenda11_test.go index 0e736a337..55867128d 100644 --- a/addenda11_test.go +++ b/addenda11_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -// mockAddenda11() creates a mock Addenda11 record +// mockAddenda11 creates a mock Addenda11 record func mockAddenda11() *Addenda11 { addenda11 := NewAddenda11() addenda11.OriginatorName = "BEK Solutions" diff --git a/addenda12.go b/addenda12.go index 6f79f0f7d..90bd946a7 100644 --- a/addenda12.go +++ b/addenda12.go @@ -8,7 +8,7 @@ import ( "fmt" ) -// Addenda12 is a Addendumer addenda which provides business transaction information for Addenda Type +// 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 diff --git a/addenda12_test.go b/addenda12_test.go index 6e099dd2e..1be3b1112 100644 --- a/addenda12_test.go +++ b/addenda12_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -// mockAddenda12() creates a mock Addenda12 record +// mockAddenda12 creates a mock Addenda12 record func mockAddenda12() *Addenda12 { addenda12 := NewAddenda12() addenda12.OriginatorCityStateProvince = "JacobsTown*PA\\" diff --git a/addenda13.go b/addenda13.go index adede0dc8..3cc20d77f 100644 --- a/addenda13.go +++ b/addenda13.go @@ -8,7 +8,7 @@ import ( "fmt" ) -// Addenda13 is a Addendumer addenda which provides business transaction information for Addenda Type +// 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 diff --git a/addenda13_test.go b/addenda13_test.go index 73774bd25..7acced8c0 100644 --- a/addenda13_test.go +++ b/addenda13_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -// mockAddenda13() creates a mock Addenda13 record +// mockAddenda13 creates a mock Addenda13 record func mockAddenda13() *Addenda13 { addenda13 := NewAddenda13() addenda13.ODFIName = "Wells Fargo" diff --git a/addenda14.go b/addenda14.go index f7874c622..008fab489 100644 --- a/addenda14.go +++ b/addenda14.go @@ -8,7 +8,7 @@ import ( "fmt" ) -// Addenda14 is a Addendumer addenda which provides business transaction information for Addenda Type +// 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 diff --git a/addenda14_test.go b/addenda14_test.go index 4f9db902e..511fde002 100644 --- a/addenda14_test.go +++ b/addenda14_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -// mockAddenda14() creates a mock Addenda14 record +// mockAddenda14 creates a mock Addenda14 record func mockAddenda14() *Addenda14 { addenda14 := NewAddenda14() addenda14.RDFIName = "Citadel Bank" diff --git a/addenda15.go b/addenda15.go index 4548c15b3..f32ac2419 100644 --- a/addenda15.go +++ b/addenda15.go @@ -8,7 +8,7 @@ import ( "fmt" ) -// Addenda15 is a Addendumer addenda which provides business transaction information for Addenda Type +// 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 diff --git a/addenda15_test.go b/addenda15_test.go index 125d755ab..07a44c10c 100644 --- a/addenda15_test.go +++ b/addenda15_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -// mockAddenda15() creates a mock Addenda15 record +// mockAddenda15 creates a mock Addenda15 record func mockAddenda15() *Addenda15 { addenda15 := NewAddenda15() addenda15.ReceiverIDNumber = "987465493213987" diff --git a/addenda16.go b/addenda16.go index b9de7d1fa..00c86c6e1 100644 --- a/addenda16.go +++ b/addenda16.go @@ -8,7 +8,7 @@ import ( "fmt" ) -// Addenda16 is a Addendumer addenda which provides business transaction information for Addenda Type +// 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 diff --git a/addenda16_test.go b/addenda16_test.go index 9a8be7271..547b8ff88 100644 --- a/addenda16_test.go +++ b/addenda16_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -// mockAddenda16() creates a mock Addenda16 record +// mockAddenda16 creates a mock Addenda16 record func mockAddenda16() *Addenda16 { addenda16 := NewAddenda16() addenda16.ReceiverCityStateProvince = "LetterTown*CO\\" 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..ea8fa2c87 --- /dev/null +++ b/addenda17_test.go @@ -0,0 +1,186 @@ +// 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.SequenceNumber = 1 + addenda17.EntryDetailSequenceNumber = 0000001 + + return addenda17 +} + +// 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") + } +} + +// ToDo: Add parse logic + +// 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/batch.go b/batch.go index 6eae8d548..07b2f9e7e 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": @@ -86,7 +90,7 @@ 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} } diff --git a/batchIAT.go b/batchIAT.go index e6a0fcbed..67ecda6a1 100644 --- a/batchIAT.go +++ b/batchIAT.go @@ -4,9 +4,11 @@ package ach +// ToDo: Deprecate + // BatchIAT holds the Batch Header and Batch Control and all Entry Records for IAT Entries type BatchIAT struct { - iatBatch + IATBatch } // NewBatchIAT returns a *BatchIAT diff --git a/file.go b/file.go index fa983e0de..8edcc7f9d 100644 --- a/file.go +++ b/file.go @@ -56,7 +56,7 @@ type File struct { ID string `json:"id"` Header FileHeader `json:"fileHeader"` Batches []Batcher `json:"batches"` - IATBatches []IATBatcher `json:"IATBatches"` + IATBatches []IATBatch`json:"IATBatches"` Control FileControl `json:"fileControl"` // NotificationOfChange (Notification of change) is a slice of references to BatchCOR in file.Batches @@ -154,7 +154,7 @@ func (f *File) AddBatch(batch Batcher) []Batcher { } // AddIATBatch appends a IATBatch to the ach.File -func (f *File) AddIATBatch(iatBatch IATBatcher) []IATBatcher { +func (f *File) AddIATBatch(iatBatch IATBatch) []IATBatch { f.IATBatches = append(f.IATBatches, iatBatch) return f.IATBatches } diff --git a/iatBatch.go b/iatBatch.go index 6a3357b6d..ecbbb6cd0 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -9,12 +9,24 @@ import ( "strconv" ) -// Batch holds the Batch Header and Batch Control and all Entry Records -type iatBatch struct { +var ( + msgBatchIATAddendaRequired = "is required for an IAT detail entry" +) + +// 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"` + 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 @@ -24,12 +36,15 @@ type iatBatch struct { } // 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) (IATBatcher, error) { - return NewBatchIAT(bh), nil +func IATNewBatch(bh *IATBatchHeader) *IATBatch { + iatBatch := new(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 { +func (batch *IATBatch) verify() error { batchNumber := batch.Header.BatchNumber // verify field inclusion in all the records of the batch. @@ -45,11 +60,6 @@ func (batch *iatBatch) verify() error { msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.Header.ServiceClassCode, batch.Control.ServiceClassCode) return &BatchError{BatchNumber: batchNumber, FieldName: "ServiceClassCode", Msg: msg} } - // Company Identification must match the Company ID from the batch header record - /* if batch.Header.CompanyIdentification != batch.Control.CompanyIdentification { - msg := fmt.Sprintf(msgBatchHeaderControlEquality, batch.Header.CompanyIdentification, batch.Control.CompanyIdentification) - return &BatchError{BatchNumber: batchNumber, FieldName: "CompanyIdentification", 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) @@ -57,7 +67,7 @@ func (batch *iatBatch) 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} } @@ -77,10 +87,6 @@ func (batch *iatBatch) verify() error { return err } - if err := batch.isOriginatorDNE(); err != nil { - return err - } - if err := batch.isTraceNumberODFI(); err != nil { return err } @@ -93,7 +99,7 @@ func (batch *iatBatch) verify() error { // 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 { +func (batch *IATBatch) build() error { // Requires a valid BatchHeader if err := batch.Header.Validate(); err != nil { return err @@ -105,7 +111,21 @@ func (batch *iatBatch) build() error { entryCount := 0 seq := 1 for i, entry := range batch.Entries { - entryCount = entryCount + 1 + len(entry.Addendum) + entryCount = entryCount + 1 + 7 + //ToDo: Add Addenda17 and Addenda18 + if entry.Addenda17 != nil { + entryCount = entryCount + 1 + } + + /*if entry.Addenda18 != nil { + entryCount = entryCount + 1 + } */ + + // 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 @@ -121,21 +141,24 @@ func (batch *iatBatch) build() error { batch.Entries[i].SetTraceNumber(batch.Header.ODFIIdentification, seq) } seq++ - addendaSeq := 1 - for x := range entry.Addendum { - // sequences don't exist in NOC or Return addenda - if a, ok := batch.Entries[i].Addendum[x].(*Addenda05); ok { - a.SequenceNumber = addendaSeq - a.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) - } - addendaSeq++ - } + + 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:]) + + //ToDo: Add Addenda17 and Addenda 18 logic for SequenceNUmber and EntryDetailSequenceNumber + + /* entry.Addenda17.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + entry.Addenda18.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:])*/ } // build a BatchControl record bc := NewBatchControl() bc.ServiceClassCode = batch.Header.ServiceClassCode - /*bc.CompanyIdentification = iatBatch.Header.CompanyIdentification*/ bc.ODFIIdentification = batch.Header.ODFIIdentification bc.BatchNumber = batch.Header.BatchNumber bc.EntryAddendaCount = entryCount @@ -147,43 +170,44 @@ func (batch *iatBatch) build() error { } // SetHeader appends an BatchHeader to the Batch -func (batch *iatBatch) SetHeader(batchHeader *IATBatchHeader) { +func (batch *IATBatch) SetHeader(batchHeader *IATBatchHeader) { batch.Header = batchHeader } // GetHeader returns the current Batch header -func (batch *iatBatch) GetHeader() *IATBatchHeader { +func (batch *IATBatch) GetHeader() *IATBatchHeader { return batch.Header } // SetControl appends an BatchControl to the Batch -func (batch *iatBatch) SetControl(batchControl *BatchControl) { +func (batch *IATBatch) SetControl(batchControl *BatchControl) { batch.Control = batchControl } // GetControl returns the current Batch Control -func (batch *iatBatch) GetControl() *BatchControl { +func (batch *IATBatch) GetControl() *BatchControl { return batch.Control } // GetEntries returns a slice of entry details for the batch -func (batch *iatBatch) GetEntries() []*IATEntryDetail { +func (batch *IATBatch) GetEntries() []*IATEntryDetail { return batch.Entries } // AddEntry appends an EntryDetail to the Batch -func (batch *iatBatch) AddEntry(entry *IATEntryDetail) { +func (batch *IATBatch) AddEntry(entry *IATEntryDetail) { batch.category = entry.Category batch.Entries = append(batch.Entries, entry) } -// IsReturn is true if the batch contains an Entry Return -func (batch *iatBatch) Category() string { +// Category returns IATBatch Category +// ToDo: Verify this process is the same as a non IAT Batch +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 { +func (batch *IATBatch) isFieldInclusion() error { if err := batch.Header.Validate(); err != nil { return err } @@ -191,10 +215,32 @@ func (batch *iatBatch) isFieldInclusion() error { if err := entry.Validate(); err != nil { return err } - for _, addenda := range entry.Addendum { - if err := addenda.Validate(); err != nil { - return nil - } + // 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 } } return batch.Control.Validate() @@ -203,10 +249,16 @@ func (batch *iatBatch) isFieldInclusion() error { // 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 { +func (batch *IATBatch) isBatchEntryCount() error { entryCount := 0 for _, entry := range batch.Entries { - entryCount = entryCount + 1 + len(entry.Addendum) + entryCount = entryCount + 1 + 7 + + //ToDo: Add logic for Addenda17 and Addenda18 + + if entry.Addenda17 != nil { + entryCount = entryCount + 1 + } } if entryCount != batch.Control.EntryAddendaCount { msg := fmt.Sprintf(msgBatchCalculatedControlEquality, entryCount, batch.Control.EntryAddendaCount) @@ -218,7 +270,7 @@ func (batch *iatBatch) isBatchEntryCount() error { // 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 { +func (batch *IATBatch) isBatchAmount() error { credit, debit := batch.calculateBatchAmounts() if debit != batch.Control.TotalDebitEntryDollarAmount { msg := fmt.Sprintf(msgBatchCalculatedControlEquality, debit, batch.Control.TotalDebitEntryDollarAmount) @@ -232,7 +284,7 @@ func (batch *iatBatch) isBatchAmount() error { return nil } -func (batch *iatBatch) calculateBatchAmounts() (credit int, debit int) { +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 @@ -246,7 +298,7 @@ func (batch *iatBatch) calculateBatchAmounts() (credit int, debit int) { // 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 { +func (batch *IATBatch) isSequenceAscending() error { lastSeq := -1 for _, entry := range batch.Entries { if entry.TraceNumber <= lastSeq { @@ -259,7 +311,7 @@ func (batch *iatBatch) isSequenceAscending() error { } // isEntryHash validates the hash by recalculating the result -func (batch *iatBatch) isEntryHash() error { +func (batch *IATBatch) isEntryHash() error { hashField := batch.calculateEntryHash() if hashField != batch.Control.EntryHashField() { msg := fmt.Sprintf(msgBatchCalculatedControlEquality, hashField, batch.Control.EntryHashField()) @@ -270,7 +322,7 @@ func (batch *iatBatch) isEntryHash() error { // 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 { +func (batch *IATBatch) calculateEntryHash() string { hash := 0 for _, entry := range batch.Entries { @@ -281,22 +333,9 @@ func (batch *iatBatch) calculateEntryHash() string { return batch.numericField(hash, 10) } -// The Originator Status Code is not equal to “2” for DNE if the Transaction Code is 23 or 33 -func (batch *iatBatch) isOriginatorDNE() error { - if batch.Header.OriginatorStatusCode != 2 { - for _, entry := range batch.Entries { - if entry.TransactionCode == 23 || entry.TransactionCode == 33 { - msg := fmt.Sprintf(msgBatchOriginatorDNE, batch.Header.OriginatorStatusCode) - return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "OriginatorStatusCode", Msg: msg} - } - } - } - return nil -} - // isTraceNumberODFI checks if the first 8 positions of the entry detail trace number // match the batch header ODFI -func (batch *iatBatch) isTraceNumberODFI() error { +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]) @@ -310,14 +349,14 @@ func (batch *iatBatch) isTraceNumberODFI() error { // ToDo: Adjustments for IAT Addenda // isAddendaSequence check multiple errors on addenda records in the batch entries -func (batch *iatBatch) isAddendaSequence() error { +func (batch *IATBatch) isAddendaSequence() error { for _, entry := range batch.Entries { - if len(entry.Addendum) > 0 { - // addenda without indicator flag of 1 - if entry.AddendaRecordIndicator != 1 { - return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "AddendaRecordIndicator", Msg: msgBatchAddendaIndicator} - } - lastSeq := -1 + + // addenda without indicator flag of 1 + if entry.AddendaRecordIndicator != 1 { + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "AddendaRecordIndicator", Msg: msgBatchAddendaIndicator} + } + /* lastSeq := -1 // check if sequence is ascending for _, addenda := range entry.Addendum { // sequences don't exist in NOC or Return addenda @@ -328,20 +367,51 @@ func (batch *iatBatch) isAddendaSequence() error { return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "SequenceNumber", Msg: msg} } lastSeq = 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} - } } - } + }*/ + + // 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} + } + //ToDo: Add Addenda17 and Addenda 18 logic for SequenceNUmber and EntryDetailSequenceNumber } return nil } // isCategory verifies that a Forward and Return Category are not in the same batch -func (batch *iatBatch) isCategory() error { +func (batch *IATBatch) isCategory() error { category := batch.GetEntries()[0].Category if len(batch.Entries) > 1 { for i := 1; i < len(batch.Entries); i++ { @@ -355,3 +425,35 @@ func (batch *iatBatch) isCategory() error { } return nil } + +func (batch *IATBatch) addendaFieldInclusion(entry *IATEntryDetail) error { + if entry.Addenda10 == nil { + msg := fmt.Sprint(msgBatchIATAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda10", Msg: msg} + } + if entry.Addenda11 == nil { + msg := fmt.Sprint(msgBatchIATAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda11", Msg: msg} + } + if entry.Addenda12 == nil { + msg := fmt.Sprint(msgBatchIATAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda12", Msg: msg} + } + if entry.Addenda13 == nil { + msg := fmt.Sprint(msgBatchIATAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda13", Msg: msg} + } + if entry.Addenda14 == nil { + msg := fmt.Sprint(msgBatchIATAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda14", Msg: msg} + } + if entry.Addenda15 == nil { + msg := fmt.Sprint(msgBatchIATAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda15", Msg: msg} + } + if entry.Addenda16 == nil { + msg := fmt.Sprint(msgBatchIATAddendaRequired) + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda16", Msg: msg} + } + return nil +} diff --git a/iatBatch_test.go b/iatBatch_test.go new file mode 100644 index 000000000..9b8eed011 --- /dev/null +++ b/iatBatch_test.go @@ -0,0 +1,575 @@ +// 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.GetEntries()[0].Addenda10 = mockAddenda10() + mockBatch.GetEntries()[0].Addenda11 = mockAddenda11() + mockBatch.GetEntries()[0].Addenda12 = mockAddenda12() + mockBatch.GetEntries()[0].Addenda13 = mockAddenda13() + mockBatch.GetEntries()[0].Addenda14 = mockAddenda14() + mockBatch.GetEntries()[0].Addenda15 = mockAddenda15() + mockBatch.GetEntries()[0].Addenda16 = mockAddenda16() + if err := mockBatch.build(); err != nil { + log.Fatal(err) + } + return mockBatch +} + +// 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) + } +} + +// 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) + } +} + +// testIATAddendaRecordIndicator validates AddendaRecordIndicator FieldInclusion +func testIATAddendaRecordIndicator(t testing.TB) { + mockBatch := mockIATBatch() + mockBatch.GetEntries()[0].AddendaRecordIndicator = 0 + 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) + } + } +} + +// TestIATAddendaRecordIndicator tests validating AddendaRecordIndicator FieldInclusion +func TestIATAddendaRecordIndicator(t *testing.T) { + testIATAddendaRecordIndicator(t) +} + +// BenchmarkIATAddendaRecordIndicator benchmarks validating AddendaRecordIndicator FieldInclusion +func BenchmarkIATAddendaRecordIndicator(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testIATAddendaRecordIndicator(b) + } +} diff --git a/iatBatcher.go b/iatBatcher.go deleted file mode 100644 index 9a60bc43e..000000000 --- a/iatBatcher.go +++ /dev/null @@ -1,55 +0,0 @@ -// 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 - -// IATBatcher abstract an IAT ACH batch type -type IATBatcher interface { - GetHeader() *IATBatchHeader - SetHeader(*IATBatchHeader) - GetControl() *BatchControl - SetControl(*BatchControl) - GetEntries() []*IATEntryDetail - AddEntry(*IATEntryDetail) - Create() error - Validate() error - // Category defines if a Forward or Return - Category() string -} - -/*// BatchError is an Error that describes batch validation issues -type IATBatchError struct { - BatchNumber int - FieldName string - Msg string -} - -func (e *BatchError) IATError() string { - return fmt.Sprintf("BatchNumber %d %s %s", e.BatchNumber, e.FieldName, e.Msg) -}*/ - -// Errors specific to parsing a Batch container -var ( -// generic messages -/* msgBatchHeaderControlEquality = "header %v is not equal to control %v" - msgBatchCalculatedControlEquality = "calculated %v is out-of-balance with control %v" - msgBatchAscending = "%v is less than last %v. Must be in ascending order" - // specific messages for error - msgBatchCompanyEntryDescription = "Company entry description %v is not valid for batch type %v" - msgBatchOriginatorDNE = "%v is not “2” for DNE with entry transaction code of 23 or 33" - msgBatchTraceNumberNotODFI = "%v in header does not match entry trace number %v" - msgBatchAddendaIndicator = "is 0 but found addenda record(s)" - msgBatchAddendaTraceNumber = "%v does not match proceeding entry detail trace number %v" - msgBatchEntries = "must have Entry Record(s) to be built" - msgBatchAddendaCount = "%v addendum found where %v is allowed for batch type %v" - msgBatchTransactionCodeCredit = "%v a credit is not allowed" - msgBatchSECType = "header SEC type code %v for batch type %v" - msgBatchTypeCode = "%v found in addenda and expecting %v for batch type %v" - msgBatchServiceClassCode = "Service Class Code %v is not valid for batch type %v" - msgBatchForwardReturn = "Forward and Return entries found in the same batch" - msgBatchAmount = "Amount must be less than %v for SEC code %v" - msgBatchCheckSerialNumber = "Check Serial Number is required for SEC code %v" - msgBatchTransactionCode = "Transaction code %v is not allowed for batch type %v" - msgBatchCardTransactionType = "Card Transaction Type %v is invalid"*/ -) diff --git a/iatEntryDetail.go b/iatEntryDetail.go index 886e28e0b..6cb39c985 100644 --- a/iatEntryDetail.go +++ b/iatEntryDetail.go @@ -62,7 +62,15 @@ type IATEntryDetail struct { // 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"` + //Addendum []Addendumer `json:"addendum,omitempty"` + Addenda10 *Addenda10 `json:"addenda10,omitempty"` + Addenda11 *Addenda11 `json:"addenda11,omitempty"` + Addenda12 *Addenda12 `json:"addenda12,omitempty"` + Addenda13 *Addenda13 `json:"addenda13,omitempty"` + Addenda14 *Addenda14 `json:"addenda14,omitempty"` + Addenda15 *Addenda15 `json:"addenda15,omitempty"` + Addenda16 *Addenda16 `json:"addenda16,omitempty"` + Addenda17 *Addenda17 `json:"addenda17,omitempty"` // Category defines if the entry is a Forward, Return, or NOC Category string `json:"category,omitempty"` // validator is composed for data validation diff --git a/iatEntryDetail_test.go b/iatEntryDetail_test.go index ba1f293e0..cc27c2e7a 100644 --- a/iatEntryDetail_test.go +++ b/iatEntryDetail_test.go @@ -66,7 +66,7 @@ func BenchmarkIATMockEntryDetail(b *testing.B) { func testParseIATEntryDetail(t testing.TB) { var line = "6221210428820007 000010000012345678901234567890123456789012345 1231380100000001" r := NewReader(strings.NewReader(line)) - r.addIATCurrentBatch(NewBatchIAT(mockIATBatchHeaderFF())) + r.addIATCurrentBatch(IATNewBatch(mockIATBatchHeaderFF())) r.IATCurrentBatch.SetHeader(mockIATBatchHeaderFF()) r.line = line if err := r.parseIATEntryDetail(); err != nil { @@ -122,7 +122,7 @@ func BenchmarkParseIATEntryDetail(b *testing.B) { func testIATEDString(t testing.TB) { var line = "6221210428820007 000010000012345678901234567890123456789012345 1231380100000001" r := NewReader(strings.NewReader(line)) - r.addIATCurrentBatch(NewBatchIAT(mockIATBatchHeaderFF())) + r.addIATCurrentBatch(IATNewBatch(mockIATBatchHeaderFF())) r.IATCurrentBatch.SetHeader(mockIATBatchHeaderFF()) r.line = line if err := r.parseIATEntryDetail(); err != nil { diff --git a/reader.go b/reader.go index c39791610..064624060 100644 --- a/reader.go +++ b/reader.go @@ -37,7 +37,7 @@ type Reader struct { // currentBatch is the current Batch entries being parsed currentBatch Batcher // IATCurrentBatch is the current IATBatch entries being parsed - IATCurrentBatch IATBatcher + IATCurrentBatch *IATBatch // line number of the file being parsed lineNum int // recordName holds the current record name being parsed. @@ -61,7 +61,7 @@ func (r *Reader) addCurrentBatch(batch Batcher) { // 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 IATBatcher) { +func (r *Reader) addIATCurrentBatch(iatBatch *IATBatch) { r.IATCurrentBatch = iatBatch } @@ -325,10 +325,7 @@ func (r *Reader) parseIATBatchHeader() error { } // Passing BatchHeader into NewBatchIAT creates a Batcher of IAT SEC code type. - iatBatch, err := IATNewBatch(bh) - if err != nil { - return r.error(err) - } + iatBatch := IATNewBatch(bh) r.addIATCurrentBatch(iatBatch) diff --git a/writer.go b/writer.go index 1822d46ad..9c4404d33 100644 --- a/writer.go +++ b/writer.go @@ -106,12 +106,36 @@ func (w *Writer) writeIATBatch(file *File) error { return err } w.lineNum++ - for _, addenda := range entry.Addendum { - if _, err := w.w.WriteString(addenda.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++ + + // ToDo: 17 and 18 } if _, err := w.w.WriteString(iatBatch.GetControl().String() + "\n"); err != nil { return err From 8b2a8f4d97b99ed772a0bdb9a323299efbc85d16 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Mon, 9 Jul 2018 22:42:20 -0400 Subject: [PATCH 31/64] #211 gofmt #211 gofmt --- iatBatch.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/iatBatch.go b/iatBatch.go index ecbbb6cd0..7050799bf 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -117,9 +117,9 @@ func (batch *IATBatch) build() error { entryCount = entryCount + 1 } - /*if entry.Addenda18 != nil { - entryCount = entryCount + 1 - } */ + /*if entry.Addenda18 != nil { + entryCount = entryCount + 1 + } */ // Verifies the required addenda* properties for an IAT entry detail are defined if err := batch.addendaFieldInclusion(entry); err != nil { From 30a1029b7ece398babc644fd277743a0d64b4149 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Mon, 9 Jul 2018 22:47:06 -0400 Subject: [PATCH 32/64] #211 file.go gofmt #211 file.go gofmt --- file.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/file.go b/file.go index 8edcc7f9d..d054806d7 100644 --- a/file.go +++ b/file.go @@ -53,11 +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"` - IATBatches []IATBatch`json:"IATBatches"` - 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 From ca5da1011ffbd93c70d11f9bd3e4f754aacd9456 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 10 Jul 2018 09:39:30 -0400 Subject: [PATCH 33/64] #211 remove batchIAT #211 remove batchIAT --- batchIAT.go | 53 ----------------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 batchIAT.go diff --git a/batchIAT.go b/batchIAT.go deleted file mode 100644 index 67ecda6a1..000000000 --- a/batchIAT.go +++ /dev/null @@ -1,53 +0,0 @@ -// 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 - -// ToDo: Deprecate - -// BatchIAT holds the Batch Header and Batch Control and all Entry Records for IAT Entries -type BatchIAT struct { - IATBatch -} - -// NewBatchIAT returns a *BatchIAT -func NewBatchIAT(bh *IATBatchHeader) *BatchIAT { - iatBatch := new(BatchIAT) - iatBatch.SetControl(NewBatchControl()) - iatBatch.SetHeader(bh) - return iatBatch -} - -// Validate checks valid NACHA batch rules. Assumes properly parsed records. -func (batch *BatchIAT) Validate() error { - // basic verification of the batch before we validate specific rules. - if err := batch.verify(); err != nil { - return err - } - // Add configuration based validation for this type. - - // Batch can have one addenda per entry record - /* if err := batch.isAddendaCount(1); err != nil { - return err - } - if err := batch.isTypeCode("05"); err != nil { - return err - }*/ - - // Add type specific validation. - // ... - return nil -} - -// Create takes Batch Header and Entries and builds a valid batch -func (batch *BatchIAT) 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() -} From 155422f68946b6dc79ad2e8ca0ee0d1ac112ea67 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Wed, 11 Jul 2018 14:05:58 -0400 Subject: [PATCH 34/64] #211 Additional Addenda Support #211 Additional Addenda Support - Update reader.go for IAT Addenda records 10-16 Update travis.yml (for now) to support over 20 for parseIATAddenda Add testIATWrite to writer_test.go Update file.go for IATBatches Minor updates to IATBatch --- .travis.yml | 2 +- addenda10.go | 2 +- file.go | 17 +++- iatBatch.go | 79 ++++++++++--------- iatBatch_test.go | 18 ++--- iatEntryDetail.go | 34 +++++++- reader.go | 196 +++++++++++++++++++++++++++++++++++++--------- writer_test.go | 73 +++++++++++++++++ 8 files changed, 332 insertions(+), 89 deletions(-) diff --git a/.travis.yml b/.travis.yml index 163aba0b0..7b1d31368 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ script: - test -z $(gofmt -s -l $GO_FILES) - go vet $GO_FILES - misspell -error -locale US . -- gocyclo -over 19 $GO_FILES +- gocyclo -over 20 $GO_FILES - golint -set_exit_status $GO_FILES after_success: - goveralls -repotoken $COVERALLS_TOKEN diff --git a/addenda10.go b/addenda10.go index ee0afed8c..3e676b01b 100644 --- a/addenda10.go +++ b/addenda10.go @@ -14,7 +14,7 @@ import ( // // Addenda10 is mandatory for IAT entries // -// The First Addenda Record identifies the Receiver of the transaction and the dollar amount of +// 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. diff --git a/file.go b/file.go index d054806d7..4ddb0786c 100644 --- a/file.go +++ b/file.go @@ -168,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} } @@ -184,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 @@ -192,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} @@ -208,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} @@ -236,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 index 7050799bf..a761f2d5f 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -10,7 +10,7 @@ import ( ) var ( - msgBatchIATAddendaRequired = "is required for an IAT detail entry" + msgIATBatchAddendaRequired = "is required for an IAT detail entry" ) // IATBatch holds the Batch Header and Batch Control and all Entry Records for an IAT batch @@ -36,8 +36,8 @@ type IATBatch struct { } // 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 := new(IATBatch) +func IATNewBatch(bh *IATBatchHeader) IATBatch { + iatBatch := IATBatch{} iatBatch.SetControl(NewBatchControl()) iatBatch.SetHeader(bh) return iatBatch @@ -112,10 +112,10 @@ func (batch *IATBatch) build() error { seq := 1 for i, entry := range batch.Entries { entryCount = entryCount + 1 + 7 - //ToDo: Add Addenda17 and Addenda18 - if entry.Addenda17 != nil { - entryCount = entryCount + 1 - } + //ToDo: Add Addenda17 and Addenda18 maximum of 2 addenda17 and 5 addenda18 + /* if entry.Addenda17 != nil { + entryCount = entryCount + 1 + }*/ /*if entry.Addenda18 != nil { entryCount = entryCount + 1 @@ -346,30 +346,13 @@ func (batch *IATBatch) isTraceNumberODFI() error { return nil } -// ToDo: Adjustments for IAT Addenda - // 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: msgBatchAddendaIndicator} } - /* lastSeq := -1 - // check if sequence is ascending - for _, addenda := range entry.Addendum { - // sequences don't exist in NOC or Return addenda - if a, ok := addenda.(*Addenda05); ok { - - if a.SequenceNumber < lastSeq { - msg := fmt.Sprintf(msgBatchAscending, a.SequenceNumber, lastSeq) - return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "SequenceNumber", Msg: msg} - } - lastSeq = a.SequenceNumber - } - }*/ - // Verify Addenda* entry detail sequence numbers are valid entryTN := entry.TraceNumberField()[8:] @@ -385,22 +368,18 @@ func (batch *IATBatch) isAddendaSequence() error { 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} @@ -428,32 +407,62 @@ func (batch *IATBatch) isCategory() error { func (batch *IATBatch) addendaFieldInclusion(entry *IATEntryDetail) error { if entry.Addenda10 == nil { - msg := fmt.Sprint(msgBatchIATAddendaRequired) + msg := fmt.Sprint(msgIATBatchAddendaRequired) return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda10", Msg: msg} } if entry.Addenda11 == nil { - msg := fmt.Sprint(msgBatchIATAddendaRequired) + msg := fmt.Sprint(msgIATBatchAddendaRequired) return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda11", Msg: msg} } if entry.Addenda12 == nil { - msg := fmt.Sprint(msgBatchIATAddendaRequired) + msg := fmt.Sprint(msgIATBatchAddendaRequired) return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda12", Msg: msg} } if entry.Addenda13 == nil { - msg := fmt.Sprint(msgBatchIATAddendaRequired) + msg := fmt.Sprint(msgIATBatchAddendaRequired) return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda13", Msg: msg} } if entry.Addenda14 == nil { - msg := fmt.Sprint(msgBatchIATAddendaRequired) + msg := fmt.Sprint(msgIATBatchAddendaRequired) return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda14", Msg: msg} } if entry.Addenda15 == nil { - msg := fmt.Sprint(msgBatchIATAddendaRequired) + msg := fmt.Sprint(msgIATBatchAddendaRequired) return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "Addenda15", Msg: msg} } if entry.Addenda16 == nil { - msg := fmt.Sprint(msgBatchIATAddendaRequired) + 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. + + // IATBatch must have the following mandatory addenda per entry detail: + // Addenda10,Addenda11,Addenda12,Addenda13,Addenda14,Addenda15,Addenda16 + + // ToDo: IATBatch can have a maximum of 2 optional Addenda17 records + // ToDo: IAtBatch can have a maximum of 5 optional Addenda18 records + + // Add type specific validation. + // ... + return nil +} diff --git a/iatBatch_test.go b/iatBatch_test.go index 9b8eed011..ca0259244 100644 --- a/iatBatch_test.go +++ b/iatBatch_test.go @@ -10,17 +10,17 @@ import ( ) // mockIATBatch -func mockIATBatch() *IATBatch { - mockBatch := &IATBatch{} +func mockIATBatch() IATBatch { + mockBatch := IATBatch{} mockBatch.SetHeader(mockIATBatchHeaderFF()) mockBatch.AddEntry(mockIATEntryDetail()) - mockBatch.GetEntries()[0].Addenda10 = mockAddenda10() - mockBatch.GetEntries()[0].Addenda11 = mockAddenda11() - mockBatch.GetEntries()[0].Addenda12 = mockAddenda12() - mockBatch.GetEntries()[0].Addenda13 = mockAddenda13() - mockBatch.GetEntries()[0].Addenda14 = mockAddenda14() - mockBatch.GetEntries()[0].Addenda15 = mockAddenda15() - mockBatch.GetEntries()[0].Addenda16 = mockAddenda16() + 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) } diff --git a/iatEntryDetail.go b/iatEntryDetail.go index 6cb39c985..1d3dd4368 100644 --- a/iatEntryDetail.go +++ b/iatEntryDetail.go @@ -61,17 +61,45 @@ type IATEntryDetail 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"` + // 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 Addenda16 *Addenda16 `json:"addenda16,omitempty"` + // Addenda16 is mandatory for IAT entries + // + // The Addenda16 record identifies key information related to the Receiver. Addenda17 *Addenda17 `json:"addenda17,omitempty"` - // Category defines if the entry is a Forward, Return, or NOC + // Addenda17 is optional for IAT entries + // + // The Addenda17 record identifies payment-related data. A maximum of two of these Addenda Records Category string `json:"category,omitempty"` // validator is composed for data validation validator diff --git a/reader.go b/reader.go index 064624060..64039d3e4 100644 --- a/reader.go +++ b/reader.go @@ -37,7 +37,7 @@ type Reader struct { // currentBatch is the current Batch entries being parsed currentBatch Batcher // IATCurrentBatch is the current IATBatch entries being parsed - IATCurrentBatch *IATBatch + IATCurrentBatch IATBatch // line number of the file being parsed lineNum int // recordName holds the current record name being parsed. @@ -61,7 +61,7 @@ func (r *Reader) addCurrentBatch(batch Batcher) { // 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) { +func (r *Reader) addIATCurrentBatch(iatBatch IATBatch) { r.IATCurrentBatch = iatBatch } @@ -144,19 +144,28 @@ func (r *Reader) parseLine() error { 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 @@ -172,6 +181,52 @@ 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. + // 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" @@ -243,6 +298,7 @@ func (r *Reader) parseAddenda() error { entry := r.currentBatch.GetEntries()[entryIndex] if entry.AddendaRecordIndicator == 1 { + switch r.line[1:3] { case "02": addenda02 := NewAddenda02() @@ -284,13 +340,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 } @@ -309,10 +374,12 @@ 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 = "IATBatchHeader" - if r.IATCurrentBatch != nil { + r.recordName = "BatchHeader" + if r.IATCurrentBatch.Header != nil { // batch header inside of current batch return r.error(&FileError{Msg: msgFileBatchInside}) } @@ -334,11 +401,12 @@ func (r *Reader) parseIATBatchHeader() error { // parseIATEntryDetail takes the input record string and parses the EntryDetailRecord values func (r *Reader) parseIATEntryDetail() error { - r.recordName = "IATEntryDetail" + r.recordName = "EntryDetail" - if r.IATCurrentBatch == nil { + if r.IATCurrentBatch.Header == nil { return r.error(&FileError{Msg: msgFileBatchOutside}) } + ed := new(IATEntryDetail) ed.Parse(r.line) if err := ed.Validate(); err != nil { @@ -348,32 +416,84 @@ func (r *Reader) parseIATEntryDetail() 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 - } +// parseAddendaRecord 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}) } - return nil -} + 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] -// 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. - // IATIndicator field - if r.line[16:29] == " " { - if err := r.parseIATEntryDetail(); err != nil { - return err + if entry.AddendaRecordIndicator == 1 { + switch r.line[1:3] { + case "10": + addenda10 := NewAddenda10() + addenda10.Parse(r.line) + if err := addenda10.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda10 = addenda10 + case "11": + addenda11 := NewAddenda11() + addenda11.Parse(r.line) + if err := addenda11.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda11 = addenda11 + case "12": + addenda12 := NewAddenda12() + addenda12.Parse(r.line) + if err := addenda12.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda12 = addenda12 + case "13": + + addenda13 := NewAddenda13() + addenda13.Parse(r.line) + if err := addenda13.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda13 = addenda13 + case "14": + addenda14 := NewAddenda14() + addenda14.Parse(r.line) + if err := addenda14.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda14 = addenda14 + case "15": + addenda15 := NewAddenda15() + addenda15.Parse(r.line) + if err := addenda15.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda15 = addenda15 + case "16": + addenda16 := NewAddenda16() + addenda16.Parse(r.line) + if err := addenda16.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda16 = addenda16 + case "17": + addenda17 := NewAddenda17() + addenda17.Parse(r.line) + if err := addenda17.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda17 = addenda17 } } else { - if err := r.parseEntryDetail(); err != nil { - return err - } + msg := fmt.Sprint(msgBatchAddendaIndicator) + return r.error(&FileError{FieldName: "AddendaRecordIndicator", Msg: msg}) } + return nil } diff --git a/writer_test.go b/writer_test.go index 2de2e9455..cc8ac30e0 100644 --- a/writer_test.go +++ b/writer_test.go @@ -104,3 +104,76 @@ 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.Create() + file.AddIATBatch(iatBatch) + + /* iatBatch2 := IATBatch{} + iatBatch2.SetHeader(mockIATBatchHeaderFF()) + iatBatch2.AddEntry(mockIATEntryDetail()) + 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.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) + } +} From 3b68afa6b56f89e0bce04ff8242c42560d4a0497 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 12 Jul 2018 09:47:51 -0400 Subject: [PATCH 35/64] #211 Updates for reading an achfile with IAT entries #211 Updates for reading an achfile with IAT entries Modified sequence numbers, debit/credit balance control, and batch header ODFIIdentification in 20110729.ach Modified File control string length in 20110805A.ach Added tests to read achfiles to reader_test.go Modified isCategory to return nil if there are no entries for a batch - could be a temporary fix Modified addenda10 to not return an error if foreign exchange amount is 0. -could be a temporary fix. Follow up - Review isCategory logic when reading an ach file. --- addenda10.go | 8 +- batch.go | 18 +- iatBatch.go | 14 +- reader_test.go | 60 ++++++ test/data/20110729A.ach | 398 ++++++++++++++++++++-------------------- test/data/20110805A.ach | 2 +- 6 files changed, 281 insertions(+), 219 deletions(-) diff --git a/addenda10.go b/addenda10.go index 3e676b01b..27d85fdc0 100644 --- a/addenda10.go +++ b/addenda10.go @@ -6,7 +6,6 @@ package ach import ( "fmt" - "strconv" ) // Addenda10 is an addenda which provides business transaction information for Addenda Type @@ -112,7 +111,7 @@ func (addenda10 *Addenda10) Validate() error { if err := addenda10.isTransactionTypeCode(addenda10.TransactionTypeCode); err != nil { return &FieldError{FieldName: "TransactionTypeCode", Value: addenda10.TransactionTypeCode, Msg: err.Error()} } - // ToDo: Foreign Exchange Amount blank ? + // ToDo: Foreign Payment Amount blank ? if err := addenda10.isAlphanumeric(addenda10.ForeignTraceNumber); err != nil { return &FieldError{FieldName: "ForeignTraceNumber", Value: addenda10.ForeignTraceNumber, Msg: err.Error()} } @@ -135,10 +134,11 @@ func (addenda10 *Addenda10) fieldInclusion() error { return &FieldError{FieldName: "TransactionTypeCode", Value: addenda10.TransactionTypeCode, Msg: msgFieldRequired} } - if addenda10.ForeignPaymentAmount == 0 { + // 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} } diff --git a/batch.go b/batch.go index 07b2f9e7e..4f1736bda 100644 --- a/batch.go +++ b/batch.go @@ -93,27 +93,21 @@ func (batch *batch) verify() error { 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 } @@ -121,7 +115,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 @@ -346,7 +343,6 @@ func (batch *batch) isTraceNumberODFI() error { return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "ODFIIdentificationField", Msg: msg} } } - return nil } @@ -410,9 +406,13 @@ func (batch *batch) isTypeCode(typeCode string) error { // isCategory verifies that a Forward and Return Category are not in the same batch func (batch *batch) isCategory() error { + // ToDo: Add temporarily - ./test/data/20110805A.ach contains a batch without a detail entry + if len(batch.GetEntries()) == 0 { + return nil + } 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/iatBatch.go b/iatBatch.go index a761f2d5f..f26695df8 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -70,23 +70,18 @@ func (batch *IATBatch) verify() error { 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 } @@ -94,7 +89,10 @@ func (batch *IATBatch) 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 @@ -391,6 +389,10 @@ func (batch *IATBatch) isAddendaSequence() error { // isCategory verifies that a Forward and Return Category are not in the same batch func (batch *IATBatch) isCategory() error { + // ToDo: Add temporarily - ./test/data/20110805A.ach contains a batch without a detail entry + if len(batch.GetEntries()) == 0 { + return nil + } category := batch.GetEntries()[0].Category if len(batch.Entries) > 1 { for i := 1; i < len(batch.Entries); i++ { diff --git a/reader_test.go b/reader_test.go index d0e57a14c..f85903f26 100644 --- a/reader_test.go +++ b/reader_test.go @@ -998,3 +998,63 @@ 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 { + t.Errorf("%T: %s", err, err) + } + if err = r.File.Validate(); err != nil { + 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 { + t.Errorf("%T: %s", err, err) + } + if err = r.File.Validate(); err != nil { + 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) + } +} diff --git a/test/data/20110729A.ach b/test/data/20110729A.ach index e2153b518..9e4932baf 100644 --- a/test/data/20110729A.ach +++ b/test/data/20110729A.ach @@ -1,175 +1,175 @@ 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 +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 5220TEST COMPANY 11234567898PPDTEST SALES110801110801 1042000010000002 822000000000000000000000000000000000000000001234567898 042000010000002 -5225 FV3 CA1234567898IATTEST BUYS USDCAD110802 1042000010000004 -6270910502340 0012200000998412345 1098765430420000 +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 +177,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 +185,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 +193,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 +201,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 +209,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 +217,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 +225,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 +233,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 +241,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 +249,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 +257,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 +265,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 +273,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..523f29e9e 100644 --- a/test/data/20110805A.ach +++ b/test/data/20110805A.ach @@ -92,4 +92,4 @@ 715A258 PO Box 160 0000002 716Metropolis*ON\ CA*01234\ 0000002 822000001600182100460000000000000000000000240123456789 042000010000005 -9000005000010000000830136685201000005101000000000000200 +9000005000010000000830136685201000005101000000000000200000000000000000000000000000000000000000 From cd0669a5515e12ba1677043f5d0eb2cf521004f5 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 12 Jul 2018 17:28:40 -0400 Subject: [PATCH 36/64] #211 Additional Code Coverage #211 Additional Code Coverage In batch.go and IATBatch.go return error in isCategory if there are no entries. Updated reader_test.go to check for entries errors. Added message msgIATBatchAddendaIndicator --- batch.go | 5 +- batch_test.go | 7 ++ iatBatch.go | 10 +- iatBatch_test.go | 240 +++++++++++++++++++++++++++++++++++++++-- iatEntryDetail_test.go | 4 +- reader.go | 2 +- reader_test.go | 34 +++++- 7 files changed, 278 insertions(+), 24 deletions(-) diff --git a/batch.go b/batch.go index 4f1736bda..dfbdd4358 100644 --- a/batch.go +++ b/batch.go @@ -406,9 +406,8 @@ func (batch *batch) isTypeCode(typeCode string) error { // isCategory verifies that a Forward and Return Category are not in the same batch func (batch *batch) isCategory() error { - // ToDo: Add temporarily - ./test/data/20110805A.ach contains a batch without a detail entry - if len(batch.GetEntries()) == 0 { - return nil + if len(batch.Entries) <= 0 { + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "entries", Msg: msgBatchEntries} } category := batch.GetEntries()[0].Category if len(batch.Entries) > 1 { 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/iatBatch.go b/iatBatch.go index f26695df8..db7b8b95b 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -10,7 +10,8 @@ import ( ) var ( - msgIATBatchAddendaRequired = "is required for an IAT detail entry" + msgIATBatchAddendaRequired = "is required for an IAT detail entry" + msgIATBatchAddendaIndicator = "is invalid for addenda record(s) found" ) // IATBatch holds the Batch Header and Batch Control and all Entry Records for an IAT batch @@ -349,7 +350,7 @@ 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: msgBatchAddendaIndicator} + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "AddendaRecordIndicator", Msg: msgIATBatchAddendaIndicator} } // Verify Addenda* entry detail sequence numbers are valid entryTN := entry.TraceNumberField()[8:] @@ -389,9 +390,8 @@ func (batch *IATBatch) isAddendaSequence() error { // isCategory verifies that a Forward and Return Category are not in the same batch func (batch *IATBatch) isCategory() error { - // ToDo: Add temporarily - ./test/data/20110805A.ach contains a batch without a detail entry - if len(batch.GetEntries()) == 0 { - return nil + if len(batch.Entries) <= 0 { + return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "entries", Msg: msgBatchEntries} } category := batch.GetEntries()[0].Category if len(batch.Entries) > 1 { diff --git a/iatBatch_test.go b/iatBatch_test.go index ca0259244..2f0a087ab 100644 --- a/iatBatch_test.go +++ b/iatBatch_test.go @@ -518,6 +518,178 @@ func BenchmarkIATServiceClassCodeMismatch(b *testing.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) + } + +} + +// testIATBatchBuildError 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() @@ -546,10 +718,10 @@ func BenchmarkIATODFIIdentificationMismatch(b *testing.B) { } } -// testIATAddendaRecordIndicator validates AddendaRecordIndicator FieldInclusion -func testIATAddendaRecordIndicator(t testing.TB) { +// testIATBatchAddendaRecordIndicator validates IATEntryDetail AddendaRecordIndicator +func testIATBatchAddendaRecordIndicator(t testing.TB) { mockBatch := mockIATBatch() - mockBatch.GetEntries()[0].AddendaRecordIndicator = 0 + mockBatch.GetEntries()[0].AddendaRecordIndicator = 2 if err := mockBatch.verify(); err != nil { if e, ok := err.(*BatchError); ok { if e.FieldName != "AddendaRecordIndicator" { @@ -561,15 +733,65 @@ func testIATAddendaRecordIndicator(t testing.TB) { } } -// TestIATAddendaRecordIndicator tests validating AddendaRecordIndicator FieldInclusion -func TestIATAddendaRecordIndicator(t *testing.T) { - testIATAddendaRecordIndicator(t) +// 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.build(); err != nil { + 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) } -// BenchmarkIATAddendaRecordIndicator benchmarks validating AddendaRecordIndicator FieldInclusion -func BenchmarkIATAddendaRecordIndicator(b *testing.B) { +// BenchmarkIATBatchControl benchmarks validating BatchControl ODFIIdentification +func BenchmarkIATBatchControl(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - testIATAddendaRecordIndicator(b) + testIATBatchControl(b) } } diff --git a/iatEntryDetail_test.go b/iatEntryDetail_test.go index cc27c2e7a..4d92a18fd 100644 --- a/iatEntryDetail_test.go +++ b/iatEntryDetail_test.go @@ -14,9 +14,9 @@ func mockIATEntryDetail() *IATEntryDetail { entry := NewIATEntryDetail() entry.TransactionCode = 22 entry.SetRDFI("121042882") - entry.AddendaRecords = 7 + entry.AddendaRecords = 007 entry.DFIAccountNumber = "123456789" - entry.Amount = 100000 + entry.Amount = 100000 // 1000.00 entry.SetTraceNumber(mockIATBatchHeaderFF().ODFIIdentification, 1) entry.Category = CategoryForward return entry diff --git a/reader.go b/reader.go index 64039d3e4..56efa2cad 100644 --- a/reader.go +++ b/reader.go @@ -491,7 +491,7 @@ func (r *Reader) parseIATAddenda() error { r.IATCurrentBatch.GetEntries()[entryIndex].Addenda17 = addenda17 } } else { - msg := fmt.Sprint(msgBatchAddendaIndicator) + msg := fmt.Sprint(msgIATBatchAddendaIndicator) return r.error(&FileError{FieldName: "AddendaRecordIndicator", Msg: msg}) } diff --git a/reader_test.go b/reader_test.go index f85903f26..1c9d86444 100644 --- a/reader_test.go +++ b/reader_test.go @@ -1008,10 +1008,23 @@ func testACHFileRead(t testing.TB) { 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) } - if err = r.File.Validate(); err != nil { + + err = r.File.Validate() + + if e, ok := err.(*FileError); ok { + if e.FieldName != "BatchCount" { + t.Errorf("%T: %s", e, e) + } + } else { t.Errorf("%T: %s", err, err) } } @@ -1038,10 +1051,23 @@ func testACHFileRead2(t testing.TB) { 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) } - if err = r.File.Validate(); err != nil { + + err = r.File.Validate() + + if e, ok := err.(*FileError); ok { + if e.FieldName != "BatchCount" { + t.Errorf("%T: %s", e, e) + } + } else { t.Errorf("%T: %s", err, err) } } From 385b025f415d339c0eff137ece5056e942ac8a1f Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 12 Jul 2018 17:33:03 -0400 Subject: [PATCH 37/64] #211 gofmt #211 gofmt --- iatEntryDetail_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iatEntryDetail_test.go b/iatEntryDetail_test.go index 4d92a18fd..122dbd056 100644 --- a/iatEntryDetail_test.go +++ b/iatEntryDetail_test.go @@ -16,7 +16,7 @@ func mockIATEntryDetail() *IATEntryDetail { entry.SetRDFI("121042882") entry.AddendaRecords = 007 entry.DFIAccountNumber = "123456789" - entry.Amount = 100000 // 1000.00 + entry.Amount = 100000 // 1000.00 entry.SetTraceNumber(mockIATBatchHeaderFF().ODFIIdentification, 1) entry.Category = CategoryForward return entry From b8336ac5d9d68e8d7e11f2e53b779edb82791e6e Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 13 Jul 2018 05:26:16 -0400 Subject: [PATCH 38/64] #211 Moved batch.entries check from isCategory to verify #211 Moved batch.entries check from isCategory to verify --- batch.go | 7 ++++--- iatBatch.go | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/batch.go b/batch.go index dfbdd4358..274b2255a 100644 --- a/batch.go +++ b/batch.go @@ -65,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 @@ -406,9 +410,6 @@ func (batch *batch) isTypeCode(typeCode string) error { // isCategory verifies that a Forward and Return Category are not in the same batch func (batch *batch) isCategory() error { - if len(batch.Entries) <= 0 { - return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "entries", Msg: msgBatchEntries} - } category := batch.GetEntries()[0].Category if len(batch.Entries) > 1 { for i := 0; i < len(batch.Entries); i++ { diff --git a/iatBatch.go b/iatBatch.go index db7b8b95b..8123d63a0 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -48,6 +48,10 @@ func IATNewBatch(bh *IATBatchHeader) IATBatch { 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 @@ -390,9 +394,6 @@ func (batch *IATBatch) isAddendaSequence() error { // isCategory verifies that a Forward and Return Category are not in the same batch func (batch *IATBatch) isCategory() error { - if len(batch.Entries) <= 0 { - return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "entries", Msg: msgBatchEntries} - } category := batch.GetEntries()[0].Category if len(batch.Entries) > 1 { for i := 1; i < len(batch.Entries); i++ { From eaf25b457ddc5e1d3293996f9c1cd91a430118a8 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 13 Jul 2018 15:25:25 -0400 Subject: [PATCH 39/64] #211 Increase Code Coverage #211 Increase Code Coverage --- addenda12_test.go | 4 +- addenda16_test.go | 8 +-- batchSHR_test.go | 2 - iatBatch_test.go | 70 +++++++++++++++++++++++- reader.go | 3 +- reader_test.go | 109 +++++++++++++++++++++++++++++-------- test/data/20110729A.ach | 2 - test/data/20110805A.ach | 2 - test/data/20180713-IAT.ach | 30 ++++++++++ writer_test.go | 26 +++++---- 10 files changed, 205 insertions(+), 51 deletions(-) create mode 100644 test/data/20180713-IAT.ach diff --git a/addenda12_test.go b/addenda12_test.go index 1be3b1112..f073ae721 100644 --- a/addenda12_test.go +++ b/addenda12_test.go @@ -12,7 +12,7 @@ import ( func mockAddenda12() *Addenda12 { addenda12 := NewAddenda12() addenda12.OriginatorCityStateProvince = "JacobsTown*PA\\" - addenda12.OriginatorCountryPostalCode = "US19305\\" + addenda12.OriginatorCountryPostalCode = "US*19305\\" addenda12.EntryDetailSequenceNumber = 00000001 return addenda12 } @@ -299,7 +299,7 @@ func testAddenda12String(t testing.TB) { // Backslash logic var line = "712" + "JacobsTown*PA\\ " + - "US19305\\ " + + "US*19305\\ " + " " + "0000001" diff --git a/addenda16_test.go b/addenda16_test.go index 547b8ff88..0dd375852 100644 --- a/addenda16_test.go +++ b/addenda16_test.go @@ -11,8 +11,8 @@ import ( // mockAddenda16 creates a mock Addenda16 record func mockAddenda16() *Addenda16 { addenda16 := NewAddenda16() - addenda16.ReceiverCityStateProvince = "LetterTown*CO\\" - addenda16.ReceiverCountryPostalCode = "US80014\\" + addenda16.ReceiverCityStateProvince = "LetterTown*AB\\" + addenda16.ReceiverCountryPostalCode = "CA*80014\\" addenda16.EntryDetailSequenceNumber = 00000001 return addenda16 } @@ -300,8 +300,8 @@ func testAddenda16String(t testing.TB) { addenda16 := NewAddenda16() // Backslash logic var line = "716" + - "LetterTown*CO\\ " + - "US80014\\ " + + "LetterTown*AB\\ " + + "CA*80014\\ " + " " + "0000001" 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/iatBatch_test.go b/iatBatch_test.go index 2f0a087ab..a42773039 100644 --- a/iatBatch_test.go +++ b/iatBatch_test.go @@ -750,8 +750,14 @@ func BenchmarkIATBatchAddendaRecordIndicator(b *testing.B) { func testIATBatchInvalidTraceNumberODFI(t testing.TB) { mockBatch := mockIATBatch() mockBatch.GetEntries()[0].SetTraceNumber("9928272", 1) - if err := mockBatch.build(); err != nil { - t.Errorf("%T: %s", err, err) + 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) + } } } @@ -795,3 +801,63 @@ func BenchmarkIATBatchControl(b *testing.B) { 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) + } +} diff --git a/reader.go b/reader.go index 56efa2cad..d42fa9ede 100644 --- a/reader.go +++ b/reader.go @@ -197,7 +197,8 @@ func (r *Reader) parseBH() error { // 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. + // 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 { diff --git a/reader_test.go b/reader_test.go index 1c9d86444..ef53df5f9 100644 --- a/reader_test.go +++ b/reader_test.go @@ -1008,24 +1008,29 @@ func testACHFileRead(t testing.TB) { defer f.Close() r := NewReader(f) _, err = r.Read() - if p, ok := err.(*ParseError); ok { - if e, ok := p.Err.(*BatchError); ok { - if e.FieldName != "entries" { - t.Errorf("%T: %s", e, e) + + 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) } - } else { - t.Errorf("%T: %s", err, err) } - err = r.File.Validate() + err2 := r.File.Validate() - if e, ok := err.(*FileError); ok { - if e.FieldName != "BatchCount" { - t.Errorf("%T: %s", e, e) + 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) } - } else { - t.Errorf("%T: %s", err, err) } } @@ -1051,24 +1056,29 @@ func testACHFileRead2(t testing.TB) { defer f.Close() r := NewReader(f) _, err = r.Read() - if p, ok := err.(*ParseError); ok { - if e, ok := p.Err.(*BatchError); ok { - if e.FieldName != "entries" { - t.Errorf("%T: %s", e, e) + + 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) } - } else { - t.Errorf("%T: %s", err, err) } - err = r.File.Validate() + err2 := r.File.Validate() - if e, ok := err.(*FileError); ok { - if e.FieldName != "BatchCount" { - t.Errorf("%T: %s", e, e) + 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) } - } else { - t.Errorf("%T: %s", err, err) } } @@ -1084,3 +1094,54 @@ func BenchmarkACHFileRead2(b *testing.B) { testACHFileRead2(b) } } + +// testACHFileRead3 validates reading a file with IAT entries +// that does not error. +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 +// does not error. +func TestACHFileRead3(t *testing.T) { + testACHFileRead3(t) +} + +// BenchmarkACHFileRead3 benchmarks validating reading a file with IAT entries +// that does not error. +func BenchmarkACHFileRead3(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + testACHFileRead3(b) + } +} diff --git a/test/data/20110729A.ach b/test/data/20110729A.ach index 9e4932baf..8ed5b6a9d 100644 --- a/test/data/20110729A.ach +++ b/test/data/20110729A.ach @@ -166,8 +166,6 @@ 622021200025998412345 0000001600OA380 ANDREA BUTLER 0098765434200017 622021200025998412345 0000001200OA383 BRANDON ARMSTRONG 0098765434200018 822000001800381600360000000000000000000172001234567898 098765430000003 -5220TEST COMPANY 11234567898PPDTEST SALES110801110801 1042000010000002 -822000000000000000000000000000000000000000001234567898 042000010000002 5225 FV3 CA1234567898IATTEST BUYS USDCAD110802 1098765430000004 627091050234007 0012200000998412345 1098765430000001 710WEB DIEGO MAY 0000001 diff --git a/test/data/20110805A.ach b/test/data/20110805A.ach index 523f29e9e..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 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/writer_test.go b/writer_test.go index cc8ac30e0..510333fdc 100644 --- a/writer_test.go +++ b/writer_test.go @@ -121,18 +121,20 @@ func testIATWrite(t testing.TB) { iatBatch.Create() file.AddIATBatch(iatBatch) - /* iatBatch2 := IATBatch{} - iatBatch2.SetHeader(mockIATBatchHeaderFF()) - iatBatch2.AddEntry(mockIATEntryDetail()) - 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.Create() - file.AddIATBatch(iatBatch2)*/ + 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.Create() + file.AddIATBatch(iatBatch2) if err := file.Create(); err != nil { t.Errorf("%T: %s", err, err) From 3e4c6de5882063ed997aa730e9f79c3387b0c6ae Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 13 Jul 2018 16:52:10 -0400 Subject: [PATCH 40/64] #211 reader_test updates #211 reader_test updates --- reader_test.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/reader_test.go b/reader_test.go index ef53df5f9..900be4ddd 100644 --- a/reader_test.go +++ b/reader_test.go @@ -1095,8 +1095,7 @@ func BenchmarkACHFileRead2(b *testing.B) { } } -// testACHFileRead3 validates reading a file with IAT entries -// that does not error. +// 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 { @@ -1131,14 +1130,12 @@ func testACHFileRead3(t testing.TB) { } } -// TestACHFileRead3 tests validating reading a file with IAT entries that -// does not error. +// 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 -// that does not error. +// BenchmarkACHFileRead3 benchmarks validating reading a file with IAT entries only func BenchmarkACHFileRead3(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { From 6804d1b23dae3580d7c30b801afb6458c369cfdb Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 13 Jul 2018 17:27:09 -0400 Subject: [PATCH 41/64] #211 iatBatch_test Add additional code coverage --- iatBatch_test.go | 93 ++++++++++++++++++++++++++++++++++++++++++ iatEntryDetail_test.go | 13 ++++++ 2 files changed, 106 insertions(+) diff --git a/iatBatch_test.go b/iatBatch_test.go index a42773039..105f1679d 100644 --- a/iatBatch_test.go +++ b/iatBatch_test.go @@ -27,6 +27,32 @@ func mockIATBatch() IATBatch { 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.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() + if err := mockBatch.build(); err != nil { + log.Fatal(err) + } + return mockBatch +} + // TestMockIATBatch validates mockIATBatch func TestMockIATBatch(t *testing.T) { iatBatch := mockIATBatch() @@ -861,3 +887,70 @@ func BenchmarkIATBatchisEntryHash(b *testing.B) { 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) + } +} diff --git a/iatEntryDetail_test.go b/iatEntryDetail_test.go index 122dbd056..0b4e89141 100644 --- a/iatEntryDetail_test.go +++ b/iatEntryDetail_test.go @@ -22,6 +22,19 @@ func mockIATEntryDetail() *IATEntryDetail { 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() From ac4331e506bd998b1b323be2679e53af3d96ab85 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Mon, 16 Jul 2018 11:19:22 -0400 Subject: [PATCH 42/64] #211 Addenda17 #211 Addenda17 --- addenda17_test.go | 10 +++++++ iatBatch.go | 32 +++++++++-------------- iatEntryDetail.go | 26 +++++++++++++----- reader.go | 4 +-- reader_test.go | 48 ++++++++++++++++++++++++++++++++++ test/data/20180716-IAT-A17.ach | 30 +++++++++++++++++++++ writer.go | 9 +++++-- writer_test.go | 17 +++++++----- 8 files changed, 140 insertions(+), 36 deletions(-) create mode 100644 test/data/20180716-IAT-A17.ach diff --git a/addenda17_test.go b/addenda17_test.go index ea8fa2c87..0942d8aa4 100644 --- a/addenda17_test.go +++ b/addenda17_test.go @@ -11,12 +11,22 @@ import ( // 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 = 0000002 + + return addenda17 +} + // TestMockAddenda17 validates mockAddenda17 func TestMockAddenda17(t *testing.T) { addenda17 := mockAddenda17() diff --git a/iatBatch.go b/iatBatch.go index 8123d63a0..13e4e8340 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -114,15 +114,8 @@ func (batch *IATBatch) build() error { entryCount := 0 seq := 1 for i, entry := range batch.Entries { - entryCount = entryCount + 1 + 7 - //ToDo: Add Addenda17 and Addenda18 maximum of 2 addenda17 and 5 addenda18 - /* if entry.Addenda17 != nil { - entryCount = entryCount + 1 - }*/ - - /*if entry.Addenda18 != nil { - entryCount = entryCount + 1 - } */ + entryCount = entryCount + 1 + 7 + len(entry.Addendum) + //ToDo: Add Addenda17 and Addenda18 maximum of 2 addenda17 and 5 addenda18 // Verifies the required addenda* properties for an IAT entry detail are defined if err := batch.addendaFieldInclusion(entry); err != nil { @@ -153,10 +146,17 @@ func (batch *IATBatch) build() error { entry.Addenda15.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) entry.Addenda16.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) - //ToDo: Add Addenda17 and Addenda 18 logic for SequenceNUmber and EntryDetailSequenceNumber + // Addenda17 and Addenda18 SequenceNumber and EntryDetailSequenceNumber + seq++ + addendaSeq := 1 + for x := range entry.Addendum { + if a, ok := batch.Entries[i].Addendum[x].(*Addenda17); ok { + a.SequenceNumber = addendaSeq + a.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + } + addendaSeq++ + } - /* entry.Addenda17.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) - entry.Addenda18.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:])*/ } // build a BatchControl record @@ -255,13 +255,7 @@ func (batch *IATBatch) isFieldInclusion() error { func (batch *IATBatch) isBatchEntryCount() error { entryCount := 0 for _, entry := range batch.Entries { - entryCount = entryCount + 1 + 7 - - //ToDo: Add logic for Addenda17 and Addenda18 - - if entry.Addenda17 != nil { - entryCount = entryCount + 1 - } + entryCount = entryCount + 1 + 7 + len(entry.Addendum) } if entryCount != batch.Control.EntryAddendaCount { msg := fmt.Sprintf(msgBatchCalculatedControlEquality, entryCount, batch.Control.EntryAddendaCount) diff --git a/iatEntryDetail.go b/iatEntryDetail.go index 1d3dd4368..b53d16e09 100644 --- a/iatEntryDetail.go +++ b/iatEntryDetail.go @@ -92,14 +92,12 @@ type IATEntryDetail struct { // The Addenda15 record identifies key information related to the Receiver. Addenda15 *Addenda15 `json:"addenda15,omitempty"` // Addenda16 - Addenda16 *Addenda16 `json:"addenda16,omitempty"` - // Addenda16 is mandatory for IAT entries - // - // The Addenda16 record identifies key information related to the Receiver. - Addenda17 *Addenda17 `json:"addenda17,omitempty"` - // Addenda17 is optional for IAT entries // - // The Addenda17 record identifies payment-related data. A maximum of two of these Addenda Records + // Addenda16 record identifies additional key information related to the Receiver. + Addenda16 *Addenda16 `json:"addenda16,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 Category string `json:"category,omitempty"` // validator is composed for data validation validator @@ -289,3 +287,17 @@ func (ed *IATEntryDetail) SecondaryOFACSreeningIndicatorField() 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/reader.go b/reader.go index d42fa9ede..a5d74b3bb 100644 --- a/reader.go +++ b/reader.go @@ -417,7 +417,7 @@ func (r *Reader) parseIATEntryDetail() error { return nil } -// parseAddendaRecord takes the input record string and create an Addenda Type appended to the last EntryDetail +// parseIATAddenda takes the input record string and create an Addenda Type appended to the last EntryDetail func (r *Reader) parseIATAddenda() error { r.recordName = "Addenda" @@ -489,7 +489,7 @@ func (r *Reader) parseIATAddenda() error { if err := addenda17.Validate(); err != nil { return r.error(err) } - r.IATCurrentBatch.GetEntries()[entryIndex].Addenda17 = addenda17 + r.IATCurrentBatch.GetEntries()[entryIndex].AddIATAddenda(addenda17) } } else { msg := fmt.Sprint(msgIATBatchAddendaIndicator) diff --git a/reader_test.go b/reader_test.go index 900be4ddd..be2214dfc 100644 --- a/reader_test.go +++ b/reader_test.go @@ -1142,3 +1142,51 @@ func BenchmarkACHFileRead3(b *testing.B) { testACHFileRead3(b) } } + +// testACHIATAddenda17 validates reading a file with IAT and Addenda 17 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) + } +} 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/writer.go b/writer.go index 9c4404d33..fb24e1859 100644 --- a/writer.go +++ b/writer.go @@ -134,8 +134,13 @@ func (w *Writer) writeIATBatch(file *File) error { return err } w.lineNum++ - - // ToDo: 17 and 18 + // 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 diff --git a/writer_test.go b/writer_test.go index 510333fdc..86771ff58 100644 --- a/writer_test.go +++ b/writer_test.go @@ -6,6 +6,8 @@ package ach import ( "bytes" + "log" + "os" "strings" "testing" ) @@ -118,6 +120,8 @@ func testIATWrite(t testing.TB) { 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.Create() file.AddIATBatch(iatBatch) @@ -133,6 +137,7 @@ func testIATWrite(t testing.TB) { iatBatch2.Entries[0].Addenda14 = mockAddenda14() iatBatch2.Entries[0].Addenda15 = mockAddenda15() iatBatch2.Entries[0].Addenda16 = mockAddenda16() + iatBatch2.Entries[0].AddIATAddenda(mockAddenda17()) iatBatch2.Create() file.AddIATBatch(iatBatch2) @@ -159,12 +164,12 @@ func testIATWrite(t testing.TB) { 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()*/ + // 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 From 23b77acdd7d40c1006fe23abcb55dead8f7352be Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Mon, 16 Jul 2018 22:03:16 -0400 Subject: [PATCH 43/64] #211 Addenda18 Support #211 Addenda18 Support --- .travis.yml | 2 +- addenda10.go | 1 - addenda13.go | 2 +- addenda14.go | 2 +- addenda16.go | 4 +- addenda18.go | 208 ++++++++++++++++++ addenda18_test.go | 325 +++++++++++++++++++++++++++++ iatBatch.go | 46 +++- iatEntryDetail.go | 7 +- reader.go | 131 +++++++----- reader_test.go | 50 ++++- test/data/20180716-IAT-A17-A18.ach | 40 ++++ validators.go | 2 +- writer_test.go | 25 ++- 14 files changed, 759 insertions(+), 86 deletions(-) create mode 100644 addenda18.go create mode 100644 addenda18_test.go create mode 100644 test/data/20180716-IAT-A17-A18.ach diff --git a/.travis.yml b/.travis.yml index 7b1d31368..163aba0b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ script: - test -z $(gofmt -s -l $GO_FILES) - go vet $GO_FILES - misspell -error -locale US . -- gocyclo -over 20 $GO_FILES +- gocyclo -over 19 $GO_FILES - golint -set_exit_status $GO_FILES after_success: - goveralls -repotoken $COVERALLS_TOKEN diff --git a/addenda10.go b/addenda10.go index 27d85fdc0..84632c222 100644 --- a/addenda10.go +++ b/addenda10.go @@ -67,7 +67,6 @@ func (addenda10 *Addenda10) Parse(record string) { // 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 = addenda10.parseStringField(record[24:46]) addenda10.ForeignTraceNumber = record[24:46] // 47-81 Receiving Company Name/Individual Name addenda10.Name = record[46:81] diff --git a/addenda13.go b/addenda13.go index 3cc20d77f..8599c9818 100644 --- a/addenda13.go +++ b/addenda13.go @@ -187,7 +187,7 @@ func (addenda13 *Addenda13) ODFIIdentificationField() string { // ODFIBranchCountryCodeField gets the ODFIBranchCountryCode field left padded func (addenda13 *Addenda13) ODFIBranchCountryCodeField() string { - return addenda13.alphaField(addenda13.ODFIBranchCountryCode, 2) + " " + return addenda13.alphaField(addenda13.ODFIBranchCountryCode, 3) } // reservedField gets reserved - blank space diff --git a/addenda14.go b/addenda14.go index 008fab489..43e378fcc 100644 --- a/addenda14.go +++ b/addenda14.go @@ -183,7 +183,7 @@ func (addenda14 *Addenda14) RDFIIdentificationField() string { // RDFIBranchCountryCodeField gets the RDFIBranchCountryCode field left padded func (addenda14 *Addenda14) RDFIBranchCountryCodeField() string { - return addenda14.alphaField(addenda14.RDFIBranchCountryCode, 2) + " " + return addenda14.alphaField(addenda14.RDFIBranchCountryCode, 3) } // reservedField gets reserved - blank space diff --git a/addenda16.go b/addenda16.go index 00c86c6e1..c8ffacd73 100644 --- a/addenda16.go +++ b/addenda16.go @@ -57,9 +57,9 @@ func (addenda16 *Addenda16) Parse(record string) { addenda16.recordType = "7" // 2-3 Always 16 addenda16.typeCode = record[1:3] - // 4-38 + // 4-38 ReceiverCityStateProvince addenda16.ReceiverCityStateProvince = record[3:38] - // 39-73 + // 39-73 ReceiverCountryPostalCode addenda16.ReceiverCountryPostalCode = record[38:73] // 74-87 reserved - Leave blank addenda16.reserved = " " diff --git a/addenda18.go b/addenda18.go new file mode 100644 index 000000000..b0c510b89 --- /dev/null +++ b/addenda18.go @@ -0,0 +1,208 @@ +// 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" +) + +// 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 = 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 = record[40:74] + // 75-77 Based on the information entered (75-77) 3 alphanumeric + addenda18.ForeignCorrespondentBankBranchCountryCode = 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..aa29a2cc3 --- /dev/null +++ b/addenda18_test.go @@ -0,0 +1,325 @@ +// 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 = 0000002 + return addenda18 +} + +func mockAddenda18C() *Addenda18 { + addenda18 := NewAddenda18() + addenda18.ForeignCorrespondentBankName = "Bank of France" + addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18.ForeignCorrespondentBankIDNumber = "456456456987987" + addenda18.ForeignCorrespondentBankBranchCountryCode = "FR" + addenda18.SequenceNumber = 2 + addenda18.EntryDetailSequenceNumber = 0000003 + return addenda18 +} + +func mockAddenda18D() *Addenda18 { + addenda18 := NewAddenda18() + addenda18.ForeignCorrespondentBankName = "Bank of Turkey" + addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18.ForeignCorrespondentBankIDNumber = "12312345678910" + addenda18.ForeignCorrespondentBankBranchCountryCode = "TR" + addenda18.SequenceNumber = 2 + addenda18.EntryDetailSequenceNumber = 0000004 + return addenda18 +} + +func mockAddenda18E() *Addenda18 { + addenda18 := NewAddenda18() + addenda18.ForeignCorrespondentBankName = "Bank of United Kingdom" + addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18.ForeignCorrespondentBankIDNumber = "1234567890123456789012345678901234" + addenda18.ForeignCorrespondentBankBranchCountryCode = "GB" + addenda18.SequenceNumber = 2 + addenda18.EntryDetailSequenceNumber = 0000005 + 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") + } +} + +// ToDo: Add parse logic + +// 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/iatBatch.go b/iatBatch.go index 13e4e8340..8f890a0ab 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -12,6 +12,11 @@ import ( 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 = "found and 7 Addendum is the maximum for SEC code IAT" + msgBatchIATAddenda17 = "found and 2 Addenda17 is the maximum for SEC code IAT" + msgBatchIATAddenda18 = "found and 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 @@ -90,7 +95,6 @@ func (batch *IATBatch) verify() error { if err := batch.isTraceNumberODFI(); err != nil { return err } - // TODO this is specific to batch SEC types and should be called by that validator if err := batch.isAddendaSequence(); err != nil { return err } @@ -115,7 +119,6 @@ func (batch *IATBatch) build() error { seq := 1 for i, entry := range batch.Entries { entryCount = entryCount + 1 + 7 + len(entry.Addendum) - //ToDo: Add Addenda17 and Addenda18 maximum of 2 addenda17 and 5 addenda18 // Verifies the required addenda* properties for an IAT entry detail are defined if err := batch.addendaFieldInclusion(entry); err != nil { @@ -154,9 +157,12 @@ func (batch *IATBatch) build() error { a.SequenceNumber = addendaSeq a.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) } + if a, ok := batch.Entries[i].Addendum[x].(*Addenda18); ok { + a.SequenceNumber = addendaSeq + a.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + } addendaSeq++ } - } // build a BatchControl record @@ -204,7 +210,7 @@ func (batch *IATBatch) AddEntry(entry *IATEntryDetail) { } // Category returns IATBatch Category -// ToDo: Verify this process is the same as a non IAT Batch + func (batch *IATBatch) Category() string { return batch.category } @@ -222,7 +228,6 @@ func (batch *IATBatch) isFieldInclusion() error { if err := batch.addendaFieldInclusion(entry); err != nil { return err } - // Verifies each Addenda* record is valid if err := entry.Addenda10.Validate(); err != nil { return err @@ -245,6 +250,7 @@ func (batch *IATBatch) isFieldInclusion() error { if err := entry.Addenda16.Validate(); err != nil { return err } + } return batch.Control.Validate() } @@ -453,12 +459,34 @@ func (batch *IATBatch) Validate() error { } // Add configuration based validation for this type. - // IATBatch must have the following mandatory addenda per entry detail: - // Addenda10,Addenda11,Addenda12,Addenda13,Addenda14,Addenda15,Addenda16 + for _, entry := range batch.Entries { - // ToDo: IATBatch can have a maximum of 2 optional Addenda17 records - // ToDo: IAtBatch can have a maximum of 5 optional Addenda18 records + // 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" { + addenda17Count = 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/iatEntryDetail.go b/iatEntryDetail.go index b53d16e09..66ee7cdd5 100644 --- a/iatEntryDetail.go +++ b/iatEntryDetail.go @@ -91,11 +91,13 @@ type IATEntryDetail struct { // // The Addenda15 record identifies key information related to the Receiver. Addenda15 *Addenda15 `json:"addenda15,omitempty"` - // Addenda16 + // 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 + // 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"` @@ -188,7 +190,6 @@ func (ed *IATEntryDetail) Validate() error { 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} diff --git a/reader.go b/reader.go index a5d74b3bb..90713267a 100644 --- a/reader.go +++ b/reader.go @@ -432,64 +432,9 @@ func (r *Reader) parseIATAddenda() error { entry := r.IATCurrentBatch.GetEntries()[entryIndex] if entry.AddendaRecordIndicator == 1 { - switch r.line[1:3] { - case "10": - addenda10 := NewAddenda10() - addenda10.Parse(r.line) - if err := addenda10.Validate(); err != nil { - return r.error(err) - } - r.IATCurrentBatch.GetEntries()[entryIndex].Addenda10 = addenda10 - case "11": - addenda11 := NewAddenda11() - addenda11.Parse(r.line) - if err := addenda11.Validate(); err != nil { - return r.error(err) - } - r.IATCurrentBatch.GetEntries()[entryIndex].Addenda11 = addenda11 - case "12": - addenda12 := NewAddenda12() - addenda12.Parse(r.line) - if err := addenda12.Validate(); err != nil { - return r.error(err) - } - r.IATCurrentBatch.GetEntries()[entryIndex].Addenda12 = addenda12 - case "13": - - addenda13 := NewAddenda13() - addenda13.Parse(r.line) - if err := addenda13.Validate(); err != nil { - return r.error(err) - } - r.IATCurrentBatch.GetEntries()[entryIndex].Addenda13 = addenda13 - case "14": - addenda14 := NewAddenda14() - addenda14.Parse(r.line) - if err := addenda14.Validate(); err != nil { - return r.error(err) - } - r.IATCurrentBatch.GetEntries()[entryIndex].Addenda14 = addenda14 - case "15": - addenda15 := NewAddenda15() - addenda15.Parse(r.line) - if err := addenda15.Validate(); err != nil { - return r.error(err) - } - r.IATCurrentBatch.GetEntries()[entryIndex].Addenda15 = addenda15 - case "16": - addenda16 := NewAddenda16() - addenda16.Parse(r.line) - if err := addenda16.Validate(); err != nil { - return r.error(err) - } - r.IATCurrentBatch.GetEntries()[entryIndex].Addenda16 = addenda16 - case "17": - addenda17 := NewAddenda17() - addenda17.Parse(r.line) - if err := addenda17.Validate(); err != nil { - return r.error(err) - } - r.IATCurrentBatch.GetEntries()[entryIndex].AddIATAddenda(addenda17) + err := r.switchIATAddenda(entryIndex) + if err != nil { + return r.error(err) } } else { msg := fmt.Sprint(msgIATBatchAddendaIndicator) @@ -498,3 +443,73 @@ func (r *Reader) parseIATAddenda() error { 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 r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda10 = addenda10 + case "11": + addenda11 := NewAddenda11() + addenda11.Parse(r.line) + if err := addenda11.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda11 = addenda11 + case "12": + addenda12 := NewAddenda12() + addenda12.Parse(r.line) + if err := addenda12.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda12 = addenda12 + case "13": + + addenda13 := NewAddenda13() + addenda13.Parse(r.line) + if err := addenda13.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda13 = addenda13 + case "14": + addenda14 := NewAddenda14() + addenda14.Parse(r.line) + if err := addenda14.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda14 = addenda14 + case "15": + addenda15 := NewAddenda15() + addenda15.Parse(r.line) + if err := addenda15.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda15 = addenda15 + case "16": + addenda16 := NewAddenda16() + addenda16.Parse(r.line) + if err := addenda16.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].Addenda16 = addenda16 + case "17": + addenda17 := NewAddenda17() + addenda17.Parse(r.line) + if err := addenda17.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].AddIATAddenda(addenda17) + case "18": + addenda18 := NewAddenda18() + addenda18.Parse(r.line) + if err := addenda18.Validate(); err != nil { + return r.error(err) + } + r.IATCurrentBatch.GetEntries()[entryIndex].AddIATAddenda(addenda18) + } + return nil +} diff --git a/reader_test.go b/reader_test.go index be2214dfc..fb756b5cf 100644 --- a/reader_test.go +++ b/reader_test.go @@ -1143,7 +1143,7 @@ func BenchmarkACHFileRead3(b *testing.B) { } } -// testACHIATAddenda17 validates reading a file with IAT and Addenda 17 entries +// 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 { @@ -1190,3 +1190,51 @@ func BenchmarkACHIATAddenda17(b *testing.B) { 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) + } +} diff --git a/test/data/20180716-IAT-A17-A18.ach b/test/data/20180716-IAT-A17-A18.ach new file mode 100644 index 000000000..53240e527 --- /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 00030000001 +718Bank of Spain 01987987987123123 ES 00040000001 +718Bank of France 01456456456987987 FR 00050000001 +718Bank of Turkey 0112312345678910 TR 00060000001 +718Bank of United Kingdom 011234567890123456789012345678901234GB 00070000001 +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 00030000001 +718Bank of Spain 01987987987123123 ES 00040000001 +718Bank of France 01456456456987987 FR 00050000001 +718Bank of Turkey 0112312345678910 TR 00060000001 +718Bank of United Kingdom 011234567890123456789012345678901234GB 00070000001 +82200000150012104288000000002000000000000000 231380100000002 +9000002000004000000300024208576000000002000000000100000 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 \ No newline at end of file diff --git a/validators.go b/validators.go index b2d3abfda..5ff0e8505 100644 --- a/validators.go +++ b/validators.go @@ -247,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 diff --git a/writer_test.go b/writer_test.go index 86771ff58..11073d99a 100644 --- a/writer_test.go +++ b/writer_test.go @@ -6,8 +6,6 @@ package ach import ( "bytes" - "log" - "os" "strings" "testing" ) @@ -122,6 +120,11 @@ func testIATWrite(t testing.TB) { 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) @@ -138,6 +141,12 @@ func testIATWrite(t testing.TB) { 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) @@ -164,12 +173,12 @@ func testIATWrite(t testing.TB) { 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() + /* // 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 From b210a0a279aa501d675ddc36466227eaca707af9 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Mon, 16 Jul 2018 22:08:15 -0400 Subject: [PATCH 44/64] #211 gofmt and govet #211 gofmt and govet --- addenda18_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/addenda18_test.go b/addenda18_test.go index aa29a2cc3..ab53d141a 100644 --- a/addenda18_test.go +++ b/addenda18_test.go @@ -64,7 +64,6 @@ func mockAddenda18E() *Addenda18 { return addenda18 } - // TestMockAddenda18 validates mockAddenda18 func TestMockAddenda18(t *testing.T) { addenda18 := mockAddenda18() @@ -241,7 +240,6 @@ func BenchmarkAddenda18ForeignCorrespondentBankBranchCountryCodeAlphaNumeric(b * } } - //testAddenda18ForeignCorrespondentBankIDNumberAlphaNumeric validates ForeignCorrespondentBankIDNumber is alphanumeric func testAddenda18ForeignCorrespondentBankIDNumberAlphaNumeric(t testing.TB) { addenda18 := mockAddenda18() From e144a8935fd304a85aa0ec895330d3b961d87fcc Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Mon, 16 Jul 2018 22:11:32 -0400 Subject: [PATCH 45/64] #211 golint #211 golint --- iatBatch.go | 1 - 1 file changed, 1 deletion(-) diff --git a/iatBatch.go b/iatBatch.go index 8f890a0ab..6284730e3 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -210,7 +210,6 @@ func (batch *IATBatch) AddEntry(entry *IATEntryDetail) { } // Category returns IATBatch Category - func (batch *IATBatch) Category() string { return batch.category } From 84ea4cc151bc6427bee9cf2c2a4fbf22884a97fa Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 17 Jul 2018 09:34:08 -0400 Subject: [PATCH 46/64] # 211 SequenceNumber adjustment # 211 SequenceNumber adjustment --- test/data/20180716-IAT-A17-A18.ach | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/data/20180716-IAT-A17-A18.ach b/test/data/20180716-IAT-A17-A18.ach index 53240e527..99059a3be 100644 --- a/test/data/20180716-IAT-A17-A18.ach +++ b/test/data/20180716-IAT-A17-A18.ach @@ -10,11 +10,11 @@ 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 00030000001 -718Bank of Spain 01987987987123123 ES 00040000001 -718Bank of France 01456456456987987 FR 00050000001 -718Bank of Turkey 0112312345678910 TR 00060000001 -718Bank of United Kingdom 011234567890123456789012345678901234GB 00070000001 +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 @@ -27,11 +27,11 @@ 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 00030000001 -718Bank of Spain 01987987987123123 ES 00040000001 -718Bank of France 01456456456987987 FR 00050000001 -718Bank of Turkey 0112312345678910 TR 00060000001 -718Bank of United Kingdom 011234567890123456789012345678901234GB 00070000001 +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 From 45a3257ffc625f8c7c2c47f4817755c6dbb0dbc6 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 17 Jul 2018 09:41:26 -0400 Subject: [PATCH 47/64] #211 Adennda17 and Addenda18 SequenceNumber and EntryDetailSequenceNumber --- iatBatch.go | 48 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/iatBatch.go b/iatBatch.go index 6284730e3..9412292bd 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -139,7 +139,6 @@ func (batch *IATBatch) build() error { if currentTraceNumberODFI != batchHeaderODFI { batch.Entries[i].SetTraceNumber(batch.Header.ODFIIdentification, seq) } - seq++ entry.Addenda10.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) entry.Addenda11.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) @@ -151,17 +150,20 @@ func (batch *IATBatch) build() error { // Addenda17 and Addenda18 SequenceNumber and EntryDetailSequenceNumber seq++ - addendaSeq := 1 + addenda17Seq := 1 + addenda18Seq := 1 for x := range entry.Addendum { if a, ok := batch.Entries[i].Addendum[x].(*Addenda17); ok { - a.SequenceNumber = addendaSeq + 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 = addendaSeq + a.SequenceNumber = addenda18Seq a.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) + addenda18Seq++ } - addendaSeq++ } } @@ -386,7 +388,39 @@ func (batch *IATBatch) isAddendaSequence() error { msg := fmt.Sprintf(msgBatchAddendaTraceNumber, entry.Addenda16.EntryDetailSequenceNumberField(), entry.TraceNumberField()[8:]) return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TraceNumber", Msg: msg} } - //ToDo: Add Addenda17 and Addenda 18 logic for SequenceNUmber and EntryDetailSequenceNumber + + lastAddenda17Seq := -1 + lastAddenda18Seq := -1 + // check if sequence is ascending + for _, IATAddenda := range entry.Addendum { + // sequences don't exist in NOC or Return addenda + 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 } @@ -476,7 +510,7 @@ func (batch *IATBatch) Validate() error { addenda17Count = addenda17Count + 1 } if IATAddenda.TypeCode() == "18" { - addenda17Count = addenda18Count + 1 + addenda18Count = addenda18Count + 1 } } if addenda17Count > 2 { From 2f84e470387acae314bbbd61eb9a65de7f3bff7f Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 17 Jul 2018 09:59:49 -0400 Subject: [PATCH 48/64] #211 Add call to Validate for Addendumer #211 Add call to Validate for Addendumer --- iatBatch.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/iatBatch.go b/iatBatch.go index 9412292bd..175122e98 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -135,11 +135,12 @@ func (batch *IATBatch) build() error { return err } - // Add a sequenced TraceNumber if one is not already set. Have to keep original trance number Return and NOC entries + // 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:]) @@ -148,7 +149,7 @@ func (batch *IATBatch) build() error { entry.Addenda15.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) entry.Addenda16.EntryDetailSequenceNumber = batch.parseNumField(batch.Entries[i].TraceNumberField()[8:]) - // Addenda17 and Addenda18 SequenceNumber and EntryDetailSequenceNumber + // Set TraceNumber for Addendumer Addenda17 and Addenda18 SequenceNumber and EntryDetailSequenceNumber seq++ addenda17Seq := 1 addenda18Seq := 1 @@ -158,7 +159,6 @@ func (batch *IATBatch) build() error { 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:]) @@ -166,7 +166,6 @@ func (batch *IATBatch) build() error { } } } - // build a BatchControl record bc := NewBatchControl() bc.ServiceClassCode = batch.Header.ServiceClassCode @@ -251,6 +250,12 @@ func (batch *IATBatch) isFieldInclusion() error { 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() @@ -346,7 +351,6 @@ func (batch *IATBatch) isTraceNumberODFI() error { return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "ODFIIdentificationField", Msg: msg} } } - return nil } @@ -389,11 +393,10 @@ func (batch *IATBatch) isAddendaSequence() error { return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "TraceNumber", Msg: msg} } + // check if sequence is ascending for addendumer - Addenda17 and Addenda18 lastAddenda17Seq := -1 lastAddenda18Seq := -1 - // check if sequence is ascending for _, IATAddenda := range entry.Addendum { - // sequences don't exist in NOC or Return addenda if a, ok := IATAddenda.(*Addenda17); ok { if a.SequenceNumber < lastAddenda17Seq { From 431abb6ea7b02130495c54706dde0e9c17c8a357 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 17 Jul 2018 12:04:55 -0400 Subject: [PATCH 49/64] #211 iatBatch Code Coverage #211 iatBatch Code Coverage --- iatBatch_test.go | 308 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) diff --git a/iatBatch_test.go b/iatBatch_test.go index 105f1679d..2164ae36c 100644 --- a/iatBatch_test.go +++ b/iatBatch_test.go @@ -31,7 +31,9 @@ func mockIATBatch() IATBatch { 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() @@ -39,7 +41,16 @@ func mockIATBatchManyEntries() IATBatch { 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() @@ -47,12 +58,49 @@ func mockIATBatchManyEntries() IATBatch { 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() @@ -954,3 +1002,263 @@ func BenchmarkIATBatchIsCategory(b *testing.B) { testIATBatchIsCategory(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) + } +} From e4a28714d5da061c38f15de472bce5b109b58136 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 17 Jul 2018 15:38:56 -0400 Subject: [PATCH 50/64] #211 iatBatch Code Coverage #211 iatBatch Code Coverage --- addenda18_test.go | 17 ++++-- iatBatch.go | 11 ++-- iatBatch_test.go | 135 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 7 deletions(-) diff --git a/addenda18_test.go b/addenda18_test.go index ab53d141a..3c7ed8447 100644 --- a/addenda18_test.go +++ b/addenda18_test.go @@ -37,7 +37,7 @@ func mockAddenda18C() *Addenda18 { addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" addenda18.ForeignCorrespondentBankIDNumber = "456456456987987" addenda18.ForeignCorrespondentBankBranchCountryCode = "FR" - addenda18.SequenceNumber = 2 + addenda18.SequenceNumber = 3 addenda18.EntryDetailSequenceNumber = 0000003 return addenda18 } @@ -48,7 +48,7 @@ func mockAddenda18D() *Addenda18 { addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" addenda18.ForeignCorrespondentBankIDNumber = "12312345678910" addenda18.ForeignCorrespondentBankBranchCountryCode = "TR" - addenda18.SequenceNumber = 2 + addenda18.SequenceNumber = 4 addenda18.EntryDetailSequenceNumber = 0000004 return addenda18 } @@ -59,11 +59,22 @@ func mockAddenda18E() *Addenda18 { addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" addenda18.ForeignCorrespondentBankIDNumber = "1234567890123456789012345678901234" addenda18.ForeignCorrespondentBankBranchCountryCode = "GB" - addenda18.SequenceNumber = 2 + addenda18.SequenceNumber = 5 addenda18.EntryDetailSequenceNumber = 0000005 return addenda18 } +func mockAddenda18F() *Addenda18 { + addenda18 := NewAddenda18() + addenda18.ForeignCorrespondentBankName = "Antarctica" + addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" + addenda18.ForeignCorrespondentBankIDNumber = "123456789012345678901" + addenda18.ForeignCorrespondentBankBranchCountryCode = "AQ" + addenda18.SequenceNumber = 6 + addenda18.EntryDetailSequenceNumber = 0000006 + return addenda18 +} + // TestMockAddenda18 validates mockAddenda18 func TestMockAddenda18(t *testing.T) { addenda18 := mockAddenda18() diff --git a/iatBatch.go b/iatBatch.go index 175122e98..7af9465be 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -13,9 +13,9 @@ 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 = "found and 7 Addendum is the maximum for SEC code IAT" - msgBatchIATAddenda17 = "found and 2 Addenda17 is the maximum for SEC code IAT" - msgBatchIATAddenda18 = "found and 5 Addenda18 is the maximum for SEC code IAT" + 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" ) @@ -159,6 +159,7 @@ func (batch *IATBatch) build() error { 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:]) @@ -166,6 +167,7 @@ func (batch *IATBatch) build() error { } } } + // build a BatchControl record bc := NewBatchControl() bc.ServiceClassCode = batch.Header.ServiceClassCode @@ -256,7 +258,6 @@ func (batch *IATBatch) isFieldInclusion() error { return err } } - } return batch.Control.Validate() } @@ -351,6 +352,7 @@ func (batch *IATBatch) isTraceNumberODFI() error { return &BatchError{BatchNumber: batch.Header.BatchNumber, FieldName: "ODFIIdentificationField", Msg: msg} } } + return nil } @@ -396,6 +398,7 @@ func (batch *IATBatch) isAddendaSequence() error { // 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 { diff --git a/iatBatch_test.go b/iatBatch_test.go index 2164ae36c..a2f9070ef 100644 --- a/iatBatch_test.go +++ b/iatBatch_test.go @@ -1003,6 +1003,32 @@ func BenchmarkIATBatchIsCategory(b *testing.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() @@ -1262,3 +1288,112 @@ func BenchmarkIATBatchValidateAddenda17(b *testing.B) { 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) + } +} From 78996792206e83927429212a610f5e00b3cd890e Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 17 Jul 2018 15:43:20 -0400 Subject: [PATCH 51/64] #211 error with goveralls on github #211 error with goveralls on github --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 163aba0b0..35573104b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,6 @@ env: global: 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 github.com/client9/misspell/cmd/misspell - go get -u golang.org/x/lint/golint - go get github.com/fzipp/gocyclo From 16dec66bd09b722a24a4131975e74594d3969960 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 17 Jul 2018 17:27:26 -0400 Subject: [PATCH 52/64] #211 EntryDetailSequenceNumber #211 EntryDetailSequenceNumber --- addenda17_test.go | 2 +- addenda18_test.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/addenda17_test.go b/addenda17_test.go index 0942d8aa4..5e4289856 100644 --- a/addenda17_test.go +++ b/addenda17_test.go @@ -22,7 +22,7 @@ func mockAddenda17B() *Addenda17 { addenda17 := NewAddenda17() addenda17.PaymentRelatedInformation = "Transfer of money from one country to another" addenda17.SequenceNumber = 2 - addenda17.EntryDetailSequenceNumber = 0000002 + addenda17.EntryDetailSequenceNumber = 0000001 return addenda17 } diff --git a/addenda18_test.go b/addenda18_test.go index 3c7ed8447..2dd65fdb9 100644 --- a/addenda18_test.go +++ b/addenda18_test.go @@ -27,7 +27,7 @@ func mockAddenda18B() *Addenda18 { addenda18.ForeignCorrespondentBankIDNumber = "987987987123123" addenda18.ForeignCorrespondentBankBranchCountryCode = "ES" addenda18.SequenceNumber = 2 - addenda18.EntryDetailSequenceNumber = 0000002 + addenda18.EntryDetailSequenceNumber = 0000001 return addenda18 } @@ -38,7 +38,7 @@ func mockAddenda18C() *Addenda18 { addenda18.ForeignCorrespondentBankIDNumber = "456456456987987" addenda18.ForeignCorrespondentBankBranchCountryCode = "FR" addenda18.SequenceNumber = 3 - addenda18.EntryDetailSequenceNumber = 0000003 + addenda18.EntryDetailSequenceNumber = 0000001 return addenda18 } @@ -49,7 +49,7 @@ func mockAddenda18D() *Addenda18 { addenda18.ForeignCorrespondentBankIDNumber = "12312345678910" addenda18.ForeignCorrespondentBankBranchCountryCode = "TR" addenda18.SequenceNumber = 4 - addenda18.EntryDetailSequenceNumber = 0000004 + addenda18.EntryDetailSequenceNumber = 0000001 return addenda18 } @@ -60,7 +60,7 @@ func mockAddenda18E() *Addenda18 { addenda18.ForeignCorrespondentBankIDNumber = "1234567890123456789012345678901234" addenda18.ForeignCorrespondentBankBranchCountryCode = "GB" addenda18.SequenceNumber = 5 - addenda18.EntryDetailSequenceNumber = 0000005 + addenda18.EntryDetailSequenceNumber = 0000001 return addenda18 } @@ -71,7 +71,7 @@ func mockAddenda18F() *Addenda18 { addenda18.ForeignCorrespondentBankIDNumber = "123456789012345678901" addenda18.ForeignCorrespondentBankBranchCountryCode = "AQ" addenda18.SequenceNumber = 6 - addenda18.EntryDetailSequenceNumber = 0000006 + addenda18.EntryDetailSequenceNumber = 0000001 return addenda18 } From a435bdcb991d0702a8dd804bc77b8f3c862150f1 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 17 Jul 2018 20:47:23 -0400 Subject: [PATCH 53/64] #211 iatBatch Code Coverage #211 iatBatch Code Coverage --- iatBatch_test.go | 254 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) diff --git a/iatBatch_test.go b/iatBatch_test.go index a2f9070ef..007b649f9 100644 --- a/iatBatch_test.go +++ b/iatBatch_test.go @@ -1397,3 +1397,257 @@ func BenchmarkIATBatchEntryAddendum(b *testing.B) { 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) + } +} From ff7a1db08996bb367b83996c12299fe947c4d266 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Tue, 17 Jul 2018 20:55:53 -0400 Subject: [PATCH 54/64] #211 revert removal of goveralls #211 revert removal of goveralls --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 35573104b..163aba0b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ env: global: 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 github.com/client9/misspell/cmd/misspell - go get -u golang.org/x/lint/golint - go get github.com/fzipp/gocyclo From baac7a632abfd2a0acccadcd5ff0912ef3cd0620 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Wed, 18 Jul 2018 10:04:41 -0400 Subject: [PATCH 55/64] #211 iatBatch code coverage #211 iatBatch code coverage --- iatBatch_test.go | 249 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 240 insertions(+), 9 deletions(-) diff --git a/iatBatch_test.go b/iatBatch_test.go index 007b649f9..e224df458 100644 --- a/iatBatch_test.go +++ b/iatBatch_test.go @@ -734,7 +734,7 @@ func BenchmarkIATBatchFieldInclusion(b *testing.B) { } -// testIATBatchBuildError validates IATBatch build error +// testIATBatchBuild validates IATBatch build error func testIATBatchBuild(t testing.TB) { mockBatch := IATBatch{} mockBatch.SetHeader(mockIATBatchHeaderFF()) @@ -1514,12 +1514,10 @@ func testIATBatchAddenda18EDSequenceNumber(t testing.TB) { 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" @@ -1527,7 +1525,6 @@ func testIATBatchAddenda18EDSequenceNumber(t testing.TB) { addenda18.ForeignCorrespondentBankBranchCountryCode = "TR" addenda18.SequenceNumber = 1 addenda18.EntryDetailSequenceNumber = 0000001 - addenda18B := NewAddenda18() addenda18B.ForeignCorrespondentBankName = "Bank of United Kingdom" addenda18B.ForeignCorrespondentBankIDNumberQualifier = "01" @@ -1535,7 +1532,6 @@ func testIATBatchAddenda18EDSequenceNumber(t testing.TB) { addenda18B.ForeignCorrespondentBankBranchCountryCode = "GB" addenda18B.SequenceNumber = 2 addenda18B.EntryDetailSequenceNumber = 0000001 - mockBatch.Entries[0].Addenda10 = mockAddenda10() mockBatch.Entries[0].Addenda11 = mockAddenda11() mockBatch.Entries[0].Addenda12 = mockAddenda12() @@ -1588,12 +1584,10 @@ func testIATBatchAddenda18Sequence(t testing.TB) { 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" @@ -1601,7 +1595,6 @@ func testIATBatchAddenda18Sequence(t testing.TB) { addenda18.ForeignCorrespondentBankBranchCountryCode = "TR" addenda18.SequenceNumber = 1 addenda18.EntryDetailSequenceNumber = 0000001 - addenda18B := NewAddenda18() addenda18B.ForeignCorrespondentBankName = "Bank of United Kingdom" addenda18B.ForeignCorrespondentBankIDNumberQualifier = "01" @@ -1609,7 +1602,6 @@ func testIATBatchAddenda18Sequence(t testing.TB) { addenda18B.ForeignCorrespondentBankBranchCountryCode = "GB" addenda18B.SequenceNumber = 2 addenda18B.EntryDetailSequenceNumber = 0000001 - mockBatch.Entries[0].Addenda10 = mockAddenda10() mockBatch.Entries[0].Addenda11 = mockAddenda11() mockBatch.Entries[0].Addenda12 = mockAddenda12() @@ -1651,3 +1643,242 @@ func BenchmarkIATBatchAddenda18Sequence(b *testing.B) { 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" + 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) + } + +} From 80348a2e22c207900f41aac9f44d9988a944e540 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Wed, 18 Jul 2018 10:36:03 -0400 Subject: [PATCH 56/64] #211 iatBatch #211 iatBatch comment code in isCategory regarding NOC --- iatBatch.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/iatBatch.go b/iatBatch.go index 7af9465be..22eccb13d 100644 --- a/iatBatch.go +++ b/iatBatch.go @@ -436,9 +436,10 @@ func (batch *IATBatch) isCategory() error { category := batch.GetEntries()[0].Category if len(batch.Entries) > 1 { for i := 1; i < len(batch.Entries); i++ { - if batch.Entries[i].Category == CategoryNOC { + // 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} } From d8044278cdc3e553e969f63f6daba2a7e2849341 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Wed, 18 Jul 2018 15:11:39 -0400 Subject: [PATCH 57/64] #211 IAT Addenda Record Parsing tests #211 IAT Addenda Record Parsing tests --- addenda10.go | 5 +++-- addenda10_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++--- addenda11.go | 5 +++-- addenda11_test.go | 41 +++++++++++++++++++++++++++++++++++++-- addenda12.go | 5 +++-- addenda12_test.go | 39 +++++++++++++++++++++++++++++++++++++ addenda13.go | 5 +++-- addenda13_test.go | 48 +++++++++++++++++++++++++++++++++++++++++++--- addenda14.go | 5 +++-- addenda14_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++-- addenda15.go | 3 ++- addenda15_test.go | 41 +++++++++++++++++++++++++++++++++++++-- addenda16.go | 5 +++-- addenda16_test.go | 39 +++++++++++++++++++++++++++++++++++++ addenda17_test.go | 38 ++++++++++++++++++++++++++++++++++-- addenda18.go | 7 ++++--- addenda18_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++-- iatBatch_test.go | 2 +- 18 files changed, 398 insertions(+), 33 deletions(-) diff --git a/addenda10.go b/addenda10.go index 84632c222..1d6cf7cec 100644 --- a/addenda10.go +++ b/addenda10.go @@ -6,6 +6,7 @@ package ach import ( "fmt" + "strings" ) // Addenda10 is an addenda which provides business transaction information for Addenda Type @@ -67,9 +68,9 @@ func (addenda10 *Addenda10) Parse(record string) { // 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 = record[24:46] + addenda10.ForeignTraceNumber = strings.TrimSpace(record[24:46]) // 47-81 Receiving Company Name/Individual Name - addenda10.Name = record[46:81] + 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 diff --git a/addenda10_test.go b/addenda10_test.go index d101307b2..e5bfe9df6 100644 --- a/addenda10_test.go +++ b/addenda10_test.go @@ -27,6 +27,51 @@ func TestMockAddenda10(t *testing.T) { } } +// 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() @@ -351,12 +396,10 @@ func BenchmarkAddenda10FieldInclusionEntryDetailSequenceNumber(b *testing.B) { } } -// ToDo Add Parse test for individual fields - // 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" + var line = "710ANN000000000000100000928383-23938 BEK Enterprises 0000001" addenda10.Parse(line) if addenda10.String() != line { diff --git a/addenda11.go b/addenda11.go index 98aec15ff..831ba1775 100644 --- a/addenda11.go +++ b/addenda11.go @@ -6,6 +6,7 @@ package ach import ( "fmt" + "strings" ) // Addenda11 is an addenda which provides business transaction information for Addenda Type @@ -54,9 +55,9 @@ func (addenda11 *Addenda11) Parse(record string) { // 2-3 Always 11 addenda11.typeCode = record[1:3] // 4-38 - addenda11.OriginatorName = record[3:38] + addenda11.OriginatorName = strings.TrimSpace(record[3:38]) // 39-73 - addenda11.OriginatorStreetAddress = record[38: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 diff --git a/addenda11_test.go b/addenda11_test.go index 55867128d..283563545 100644 --- a/addenda11_test.go +++ b/addenda11_test.go @@ -25,6 +25,45 @@ func TestMockAddenda11(t *testing.T) { } } +// 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() @@ -293,8 +332,6 @@ func BenchmarkAddenda11FieldInclusionEntryDetailSequenceNumber(b *testing.B) { } } -// ToDo Add Parse test for individual fields - // 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() diff --git a/addenda12.go b/addenda12.go index 90bd946a7..054cda81a 100644 --- a/addenda12.go +++ b/addenda12.go @@ -6,6 +6,7 @@ package ach import ( "fmt" + "strings" ) // Addenda12 is an addenda which provides business transaction information for Addenda Type @@ -59,9 +60,9 @@ func (addenda12 *Addenda12) Parse(record string) { // 2-3 Always 12 addenda12.typeCode = record[1:3] // 4-38 - addenda12.OriginatorCityStateProvince = record[3:38] + addenda12.OriginatorCityStateProvince = strings.TrimSpace(record[3:38]) // 39-73 - addenda12.OriginatorCountryPostalCode = record[38: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 diff --git a/addenda12_test.go b/addenda12_test.go index f073ae721..4f8556e78 100644 --- a/addenda12_test.go +++ b/addenda12_test.go @@ -25,6 +25,45 @@ func TestMockAddenda12(t *testing.T) { } } +// 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() diff --git a/addenda13.go b/addenda13.go index 8599c9818..3a154f1a6 100644 --- a/addenda13.go +++ b/addenda13.go @@ -6,6 +6,7 @@ package ach import ( "fmt" + "strings" ) // Addenda13 is an addenda which provides business transaction information for Addenda Type @@ -75,13 +76,13 @@ func (addenda13 *Addenda13) Parse(record string) { // 2-3 Always 13 addenda13.typeCode = record[1:3] // 4-38 ODFIName - addenda13.ODFIName = record[3:38] + 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 = record[74: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 diff --git a/addenda13_test.go b/addenda13_test.go index 7acced8c0..f454d391d 100644 --- a/addenda13_test.go +++ b/addenda13_test.go @@ -19,6 +19,51 @@ func mockAddenda13() *Addenda13 { 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() @@ -399,13 +444,10 @@ func BenchmarkAddenda13FieldInclusionEntryDetailSequenceNumber(b *testing.B) { } } -// ToDo Add Parse test for individual fields - // 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 { diff --git a/addenda14.go b/addenda14.go index 43e378fcc..1c9ff163f 100644 --- a/addenda14.go +++ b/addenda14.go @@ -6,6 +6,7 @@ package ach import ( "fmt" + "strings" ) // Addenda14 is an addenda which provides business transaction information for Addenda Type @@ -71,13 +72,13 @@ func (addenda14 *Addenda14) Parse(record string) { // 2-3 Always 14 addenda14.typeCode = record[1:3] // 4-38 RDFIName - addenda14.RDFIName = record[3:38] + 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 = record[74: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 diff --git a/addenda14_test.go b/addenda14_test.go index 511fde002..9954290ba 100644 --- a/addenda14_test.go +++ b/addenda14_test.go @@ -27,6 +27,51 @@ func TestMockAddenda14(t *testing.T) { } } +// 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() @@ -399,8 +444,6 @@ func BenchmarkAddenda14FieldInclusionEntryDetailSequenceNumber(b *testing.B) { } } -// ToDo Add Parse test for individual fields - // 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() diff --git a/addenda15.go b/addenda15.go index f32ac2419..0cdd8665f 100644 --- a/addenda15.go +++ b/addenda15.go @@ -6,6 +6,7 @@ package ach import ( "fmt" + "strings" ) // Addenda15 is an addenda which provides business transaction information for Addenda Type @@ -57,7 +58,7 @@ func (addenda15 *Addenda15) Parse(record string) { // 4-18 addenda15.ReceiverIDNumber = addenda15.parseStringField(record[3:18]) // 19-53 - addenda15.ReceiverStreetAddress = record[18: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 diff --git a/addenda15_test.go b/addenda15_test.go index 07a44c10c..e384885df 100644 --- a/addenda15_test.go +++ b/addenda15_test.go @@ -25,6 +25,45 @@ func TestMockAddenda15(t *testing.T) { } } +// 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() @@ -267,8 +306,6 @@ func BenchmarkAddenda15FieldInclusionEntryDetailSequenceNumber(b *testing.B) { } } -// ToDo Add Parse test for individual fields - // 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() diff --git a/addenda16.go b/addenda16.go index c8ffacd73..2c7c165e7 100644 --- a/addenda16.go +++ b/addenda16.go @@ -6,6 +6,7 @@ package ach import ( "fmt" + "strings" ) // Addenda16 is an addenda which provides business transaction information for Addenda Type @@ -58,9 +59,9 @@ func (addenda16 *Addenda16) Parse(record string) { // 2-3 Always 16 addenda16.typeCode = record[1:3] // 4-38 ReceiverCityStateProvince - addenda16.ReceiverCityStateProvince = record[3:38] + addenda16.ReceiverCityStateProvince = strings.TrimSpace(record[3:38]) // 39-73 ReceiverCountryPostalCode - addenda16.ReceiverCountryPostalCode = record[38:73] + 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 diff --git a/addenda16_test.go b/addenda16_test.go index 0dd375852..9fbbf84b9 100644 --- a/addenda16_test.go +++ b/addenda16_test.go @@ -25,6 +25,45 @@ func TestMockAddenda16(t *testing.T) { } } +// 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() diff --git a/addenda17_test.go b/addenda17_test.go index 5e4289856..de7002e37 100644 --- a/addenda17_test.go +++ b/addenda17_test.go @@ -27,6 +27,42 @@ func mockAddenda17B() *Addenda17 { 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() @@ -38,8 +74,6 @@ func TestMockAddenda17(t *testing.T) { } } -// ToDo: Add parse logic - // testAddenda17String validates that a known parsed file can be return to a string of the same value func testAddenda17String(t testing.TB) { addenda17 := NewAddenda17() diff --git a/addenda18.go b/addenda18.go index b0c510b89..bfa597dac 100644 --- a/addenda18.go +++ b/addenda18.go @@ -6,6 +6,7 @@ package ach import ( "fmt" + "strings" ) // Addenda18 is an addenda which provides business transaction information for Addenda Type @@ -72,16 +73,16 @@ func (addenda18 *Addenda18) Parse(record string) { // 2-3 Always 18 addenda18.typeCode = record[1:3] // 4-83 Based on the information entered (04-38) 35 alphanumeric - addenda18.ForeignCorrespondentBankName = record[3:38] + 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 = record[40:74] + addenda18.ForeignCorrespondentBankIDNumber = strings.TrimSpace(record[40:74]) // 75-77 Based on the information entered (75-77) 3 alphanumeric - addenda18.ForeignCorrespondentBankBranchCountryCode = record[74:77] + addenda18.ForeignCorrespondentBankBranchCountryCode = strings.TrimSpace(record[74:77]) // 78-83 - Blank space addenda18.reserved = " " // 84-87 SequenceNumber is consecutively assigned to each Addenda18 Record following diff --git a/addenda18_test.go b/addenda18_test.go index 2dd65fdb9..e1e59f3b9 100644 --- a/addenda18_test.go +++ b/addenda18_test.go @@ -66,7 +66,7 @@ func mockAddenda18E() *Addenda18 { func mockAddenda18F() *Addenda18 { addenda18 := NewAddenda18() - addenda18.ForeignCorrespondentBankName = "Antarctica" + addenda18.ForeignCorrespondentBankName = "Bank of Antarctica" addenda18.ForeignCorrespondentBankIDNumberQualifier = "01" addenda18.ForeignCorrespondentBankIDNumber = "123456789012345678901" addenda18.ForeignCorrespondentBankBranchCountryCode = "AQ" @@ -98,7 +98,50 @@ func TestMockAddenda18(t *testing.T) { } } -// ToDo: Add parse logic +// 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) { diff --git a/iatBatch_test.go b/iatBatch_test.go index e224df458..3aaa03c69 100644 --- a/iatBatch_test.go +++ b/iatBatch_test.go @@ -1784,7 +1784,7 @@ func testIATBatchAddenda18Count(t testing.TB) { mockBatch.Entries[0].AddIATAddenda(mockAddenda18E()) addenda18F := NewAddenda18() - addenda18F.ForeignCorrespondentBankName = "Russian Federation" + addenda18F.ForeignCorrespondentBankName = "Russian Federation Bank" addenda18F.ForeignCorrespondentBankIDNumberQualifier = "01" addenda18F.ForeignCorrespondentBankIDNumber = "123123456789943874" addenda18F.ForeignCorrespondentBankBranchCountryCode = "RU" From 18ec18d6ecde58904d7b39a4a66a628955bf06d3 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 19 Jul 2018 09:46:19 -0400 Subject: [PATCH 58/64] #211 reader_test #211 reader_test --- reader.go | 18 ++--- reader_test.go | 108 +++++++++++++++++++++++++++ test/data/IAT-InvalidAddenda10.ach | 30 ++++++++ test/data/IAT-InvalidBatchHeader.ach | 30 ++++++++ test/data/IAT-InvalidEntryDetail.ach | 30 ++++++++ 5 files changed, 207 insertions(+), 9 deletions(-) create mode 100644 test/data/IAT-InvalidAddenda10.ach create mode 100644 test/data/IAT-InvalidBatchHeader.ach create mode 100644 test/data/IAT-InvalidEntryDetail.ach diff --git a/reader.go b/reader.go index 90713267a..3af43a330 100644 --- a/reader.go +++ b/reader.go @@ -450,21 +450,21 @@ func (r *Reader) switchIATAddenda(entryIndex int) error { addenda10 := NewAddenda10() addenda10.Parse(r.line) if err := addenda10.Validate(); err != nil { - return r.error(err) + return err } r.IATCurrentBatch.GetEntries()[entryIndex].Addenda10 = addenda10 case "11": addenda11 := NewAddenda11() addenda11.Parse(r.line) if err := addenda11.Validate(); err != nil { - return r.error(err) + return err } r.IATCurrentBatch.GetEntries()[entryIndex].Addenda11 = addenda11 case "12": addenda12 := NewAddenda12() addenda12.Parse(r.line) if err := addenda12.Validate(); err != nil { - return r.error(err) + return err } r.IATCurrentBatch.GetEntries()[entryIndex].Addenda12 = addenda12 case "13": @@ -472,42 +472,42 @@ func (r *Reader) switchIATAddenda(entryIndex int) error { addenda13 := NewAddenda13() addenda13.Parse(r.line) if err := addenda13.Validate(); err != nil { - return r.error(err) + return err } r.IATCurrentBatch.GetEntries()[entryIndex].Addenda13 = addenda13 case "14": addenda14 := NewAddenda14() addenda14.Parse(r.line) if err := addenda14.Validate(); err != nil { - return r.error(err) + return err } r.IATCurrentBatch.GetEntries()[entryIndex].Addenda14 = addenda14 case "15": addenda15 := NewAddenda15() addenda15.Parse(r.line) if err := addenda15.Validate(); err != nil { - return r.error(err) + return err } r.IATCurrentBatch.GetEntries()[entryIndex].Addenda15 = addenda15 case "16": addenda16 := NewAddenda16() addenda16.Parse(r.line) if err := addenda16.Validate(); err != nil { - return r.error(err) + return err } r.IATCurrentBatch.GetEntries()[entryIndex].Addenda16 = addenda16 case "17": addenda17 := NewAddenda17() addenda17.Parse(r.line) if err := addenda17.Validate(); err != nil { - return r.error(err) + return err } r.IATCurrentBatch.GetEntries()[entryIndex].AddIATAddenda(addenda17) case "18": addenda18 := NewAddenda18() addenda18.Parse(r.line) if err := addenda18.Validate(); err != nil { - return r.error(err) + return err } r.IATCurrentBatch.GetEntries()[entryIndex].AddIATAddenda(addenda18) } diff --git a/reader_test.go b/reader_test.go index fb756b5cf..8ea0b7265 100644 --- a/reader_test.go +++ b/reader_test.go @@ -1238,3 +1238,111 @@ func BenchmarkACHIATAddenda1718(b *testing.B) { 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) + } +} 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-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 From e21af84dca05dcd27a54a0cdd8ac5b479eb6b9cb Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 19 Jul 2018 09:55:37 -0400 Subject: [PATCH 59/64] #211 IAT BatchControl Test #211 IAT BatchControl Test --- reader_test.go | 37 +++++++++++++++++++++++++++ test/data/IAT-InvalidBatchControl.ach | 30 ++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 test/data/IAT-InvalidBatchControl.ach diff --git a/reader_test.go b/reader_test.go index 8ea0b7265..cc4d3e82b 100644 --- a/reader_test.go +++ b/reader_test.go @@ -1346,3 +1346,40 @@ func BenchmarkACHFileIATAddenda10(b *testing.B) { 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) + } +} \ 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..9949f7e3e --- /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 \ No newline at end of file From 81c34759d36b9fac097987b99eef69781869419d Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 19 Jul 2018 10:44:53 -0400 Subject: [PATCH 60/64] #211 reader_test #211 reader_test --- reader_test.go | 39 ++++++++++++++++++++++++++++++-- test/data/IAT-BatchHeaderErr.ach | 31 +++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 test/data/IAT-BatchHeaderErr.ach diff --git a/reader_test.go b/reader_test.go index cc4d3e82b..c0253152b 100644 --- a/reader_test.go +++ b/reader_test.go @@ -1347,7 +1347,6 @@ func BenchmarkACHFileIATAddenda10(b *testing.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") @@ -1382,4 +1381,40 @@ func BenchmarkACHFileIATBC(b *testing.B) { for i := 0; i < b.N; i++ { testACHFileIATBC(b) } -} \ No newline at end of file +} + +// 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) + } + } +} + +// TestACHFileIATBC 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/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 From 78c11247bd1f82006867f24e0acd98487c1fae85 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 19 Jul 2018 11:19:58 -0400 Subject: [PATCH 61/64] #211 Removed unnecessary ToDo #211 Removed unnecessary ToDo --- addenda16.go | 1 - addenda16_test.go | 2 -- batchCIE_test.go | 2 -- batchPOS_test.go | 2 -- iatEntryDetail_test.go | 1 - test/data/IAT-InvalidBatchControl.ach | 2 +- 6 files changed, 1 insertion(+), 9 deletions(-) diff --git a/addenda16.go b/addenda16.go index 2c7c165e7..a53728213 100644 --- a/addenda16.go +++ b/addenda16.go @@ -74,7 +74,6 @@ func (addenda16 *Addenda16) String() string { addenda16.recordType, addenda16.typeCode, addenda16.ReceiverCityStateProvinceField(), - // ToDo: Validator for backslash addenda16.ReceiverCountryPostalCodeField(), addenda16.reservedField(), addenda16.EntryDetailSequenceNumberField()) diff --git a/addenda16_test.go b/addenda16_test.go index 9fbbf84b9..41c5abce1 100644 --- a/addenda16_test.go +++ b/addenda16_test.go @@ -332,8 +332,6 @@ func BenchmarkAddenda16FieldInclusionEntryDetailSequenceNumber(b *testing.B) { } } -// ToDo Add Parse test for individual fields - // 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() 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/iatEntryDetail_test.go b/iatEntryDetail_test.go index 0b4e89141..b6ceed1b8 100644 --- a/iatEntryDetail_test.go +++ b/iatEntryDetail_test.go @@ -47,7 +47,6 @@ func testMockIATEntryDetail(t testing.TB) { if entry.RDFIIdentification != "12104288" { t.Error("RDFIIdentification dependent default value has changed") } - // ToDo: Add checkDigit test if entry.AddendaRecords != 7 { t.Error("AddendaRecords default dependent value has changed") } diff --git a/test/data/IAT-InvalidBatchControl.ach b/test/data/IAT-InvalidBatchControl.ach index 9949f7e3e..88616111c 100644 --- a/test/data/IAT-InvalidBatchControl.ach +++ b/test/data/IAT-InvalidBatchControl.ach @@ -27,4 +27,4 @@ 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 -9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 \ No newline at end of file +9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 From 3bdda8480008226e12794fb353c6635ed6d4611f Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Thu, 19 Jul 2018 11:35:31 -0400 Subject: [PATCH 62/64] 211 writer_test 211 writer_test --- README.md | 6 ++++ writer_test.go | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/README.md b/README.md index 32bbcbcaf..a88553acb 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ ACH is under active development but already in production for multiple companies * 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. @@ -261,6 +262,11 @@ Which will generate a well formed ACH flat file. 82200000020010200101000000000000000000000799123456789 234567890000002 9000002000001000000040020400202000000017500000000000799 ``` + +### Create an IAT file + + + # Getting Help channel | info diff --git a/writer_test.go b/writer_test.go index 11073d99a..389e067f8 100644 --- a/writer_test.go +++ b/writer_test.go @@ -193,3 +193,101 @@ func BenchmarkIATWrite(b *testing.B) { 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) + } +} From 63ad758387ff3d0dcb4b353bb410cde5332bc692 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 20 Jul 2018 13:44:09 -0400 Subject: [PATCH 63/64] #211 typo #211 typo --- reader_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reader_test.go b/reader_test.go index c0253152b..4e51a9807 100644 --- a/reader_test.go +++ b/reader_test.go @@ -1406,7 +1406,7 @@ func testACHFileIATBH(t testing.TB) { } } -// TestACHFileIATBC tests validating error when reading an invalid IAT Batch Header +// TestACHFileIATBH tests validating error when reading an invalid IAT Batch Header func TestACHFileIATBH(t *testing.T) { testACHFileIATBH(t) } From 0b9f31ed191a82ce677c1636fae3061d57b155f0 Mon Sep 17 00:00:00 2001 From: Brooke E Kline Jr Date: Fri, 20 Jul 2018 14:00:47 -0400 Subject: [PATCH 64/64] #211 README updates #211 README --- README.md | 122 +++++++++++++++++++++++++++++++++++++++++++++- addenda17_test.go | 1 - 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a88553acb..ec6e0a10f 100644 --- a/README.md +++ b/README.md @@ -263,9 +263,127 @@ Which will generate a well formed ACH flat file. 9000002000001000000040020400202000000017500000000000799 ``` -### Create an IAT file +### 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 diff --git a/addenda17_test.go b/addenda17_test.go index de7002e37..08de4c377 100644 --- a/addenda17_test.go +++ b/addenda17_test.go @@ -14,7 +14,6 @@ func mockAddenda17() *Addenda17 { addenda17.PaymentRelatedInformation = "This is an international payment" addenda17.SequenceNumber = 1 addenda17.EntryDetailSequenceNumber = 0000001 - return addenda17 }