Repository: moov-io/ach
Pull request #1407 (2024-04-13 14:37:24)

feat: preserve IAT BatchControl.CompanyIdentification when parsing from json
The main motivation for this PR is the fact that, using the `achcli` tool to convert from json to ach, there's no way to set the `BatchControl.CompanyIdentification` for IAT files, since they don't have the `CompanyIdentification` in the `BatchHeader`. Unless I overlooked something, this seemed like a straightforward fix and I added enough tests for the use case I am trying to cover.

(@adamdecaf contribution guide asked to tag maintainers)


Patch:
diff --git a/iatBatch.go b/iatBatch.go
index 253bf816..301ce4a1 100644
--- a/iatBatch.go
+++ b/iatBatch.go
@@ -110,6 +110,9 @@ func (iatBatch *IATBatch) verify() error {
 		return iatBatch.Error("BatchNumber",
 			NewErrBatchHeaderControlEquality(iatBatch.Header.BatchNumber, iatBatch.Control.BatchNumber))
 	}
+	if err := iatBatch.Control.isAlphanumeric(iatBatch.Control.CompanyIdentification); err != nil {
+		return fieldError("CompanyIdentification", err, iatBatch.Control.CompanyIdentification)
+	}
 	if _, err := iatBatch.isBatchEntryCount(); err != nil {
 		return err
 	}
@@ -220,10 +223,16 @@ func (iatBatch *IATBatch) build() error {
 		}
 	}
 
+	originalControl := iatBatch.GetControl()
+
 	// build a BatchControl record
 	bc := NewBatchControl()
 	iatBatch.Control = bc
 
+	if originalControl != nil {
+		bc.CompanyIdentification = originalControl.CompanyIdentification
+	}
+
 	bc.ServiceClassCode = iatBatch.Header.ServiceClassCode
 	bc.ODFIIdentification = iatBatch.Header.ODFIIdentification
 	bc.BatchNumber = iatBatch.Header.BatchNumber


Test patch:
diff --git a/file_test.go b/file_test.go
index de5d35a0..65aef5ee 100644
--- a/file_test.go
+++ b/file_test.go
@@ -664,6 +664,30 @@ func TestFile__IATdatetimeParse(t *testing.T) {
 	}
 }
 
+func TestFile__IATEmptyCompanyIdentificationParse(t *testing.T) {
+	file, err := ReadJSONFile(filepath.Join("test", "testdata", "iat-debit.json"))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	bc := file.IATBatches[0].GetControl()
+	if bc == nil || bc.CompanyIdentification != "" {
+		t.Errorf("file.IATBatches[0].Control.CompanyIdentification=expected no Company Identification, got %v", bc.CompanyIdentification)
+	}
+}
+
+func TestFile__IATcompanyIdentificationParse(t *testing.T) {
+	file, err := ReadJSONFile(filepath.Join("test", "testdata", "iat-debit-company-identification.json"))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	bc := file.IATBatches[0].GetControl()
+	if bc == nil || bc.CompanyIdentification != "231380102" {
+		t.Errorf("file.IATBatches[0].Control.CompanyIdentification=expected Company Identification 231380102, got %v", bc.CompanyIdentification)
+	}
+}
+
 func TestFile__JsonBypassOrigin(t *testing.T) {
 	path := filepath.Join("test", "testdata", "json-bypass-origin.json")
 	opts := &ValidateOpts{
diff --git a/iatBatch_test.go b/iatBatch_test.go
index 84234ba0..0ab87e4b 100644
--- a/iatBatch_test.go
+++ b/iatBatch_test.go
@@ -817,6 +817,43 @@ func TestIATBatchControl(t *testing.T) {
 	testIATBatchControl(t)
 }
 
+// testIATBatchControlWithCompanyIdentification validates BatchControl CompanyIdentification
+func testIATBatchControlWithCompanyIdentification(t *testing.T) {
+	mockBatch := mockIATBatch(t)
+	mockBatch.Control.CompanyIdentification = "121042882"
+	err := mockBatch.verify()
+	if err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+}
+
+// testIATBatchControlInvalidCompanyIdentification attempts to validate an invalid BatchControl CompanyIdentification
+func testIATBatchControlInvalidCompanyIdentification(t *testing.T) {
+	mockBatch := mockIATBatch(t)
+	mockBatch.Control.CompanyIdentification = "1234®6789"
+	err := mockBatch.Control.Validate()
+	if !base.Match(err, ErrNonAlphanumeric) {
+		t.Errorf("%T: %s", err, err)
+	}
+}
+
+// testIATBatchControlEmptyCompanyIdentification validates an empty BatchControl CompanyIdentification
+func testIATBatchControlEmptyCompanyIdentification(t *testing.T) {
+	mockBatch := mockIATBatch(t)
+	mockBatch.Control.CompanyIdentification = ""
+	err := mockBatch.verify()
+	if err != nil {
+		t.Errorf("%T: %s", err, err)
+	}
+}
+
+// TestIATBatchControlCompanyIdentification tests validating BatchControl CompanyIdentification
+func TestIATBatchControlCompanyIdentification(t *testing.T) {
+	testIATBatchControlWithCompanyIdentification(t)
+	testIATBatchControlInvalidCompanyIdentification(t)
+	testIATBatchControlEmptyCompanyIdentification(t)
+}
+
 // BenchmarkIATBatchControl benchmarks validating BatchControl ODFIIdentification
 func BenchmarkIATBatchControl(b *testing.B) {
 	b.ReportAllocs()
diff --git a/test/testdata/iat-debit-company-identification.json b/test/testdata/iat-debit-company-identification.json
new file mode 100644
index 00000000..46d061ea
--- /dev/null
+++ b/test/testdata/iat-debit-company-identification.json
@@ -0,0 +1,156 @@
+{
+  "id": "iat-datetime",
+  "fileHeader": {
+    "id": "iat-datetime",
+    "immediateDestination": "121042882",
+    "immediateOrigin": "231380104",
+    "fileCreationDate": "190807",
+    "fileCreationTime": "1513",
+    "fileIDModifier": "A",
+    "immediateDestinationName": "Bank",
+    "immediateOriginName": "My Bank Name"
+  },
+  "batches": null,
+  "IATBatches": [
+    {
+      "id": "iat-datetime",
+      "IATBatchHeader": {
+        "id": "iat-datetime",
+        "serviceClassCode": 225,
+        "foreignExchangeIndicator": "FF",
+        "foreignExchangeReferenceIndicator": 3,
+        "foreignExchangeReference": "",
+        "ISODestinationCountryCode": "US",
+        "originatorIdentification": "123456789",
+        "standardEntryClassCode": "IAT",
+        "companyEntryDescription": "TRADEPAYMT",
+        "ISOOriginatingCurrencyCode": "CAD",
+        "ISODestinationCurrencyCode": "USD",
+        "effectiveEntryDate": "2019-09-23T09:14:55.794Z",
+        "ODFIIdentification": "23138010",
+        "batchNumber": 1
+      },
+      "IATEntryDetails": [
+        {
+          "id": "iat-datetime",
+          "transactionCode": 27,
+          "RDFIIdentification": "12104288",
+          "checkDigit": "2",
+          "AddendaRecords": 7,
+          "amount": 100000,
+          "DFIAccountNumber": "123456789                          ",
+          "OFACScreeningIndicator": " ",
+          "SecondaryOFACScreeningIndicator": " ",
+          "addendaRecordIndicator": 1,
+          "traceNumber": "231380100000001",
+          "addenda10": {
+            "id": "iat-datetime",
+            "typeCode": "10",
+            "transactionTypeCode": "ANN",
+            "foreignPaymentAmount": 100000,
+            "foreignTraceNumber": "928383-23938",
+            "name": "BEK Enterprises",
+            "entryDetailSequenceNumber": 1
+          },
+          "addenda11": {
+            "id": "iat-datetime",
+            "typeCode": "11",
+            "originatorName": "BEK Solutions",
+            "originatorStreetAddress": "15 West Place Street",
+            "entryDetailSequenceNumber": 1
+          },
+          "addenda12": {
+            "id": "iat-datetime",
+            "typeCode": "12",
+            "originatorCityStateProvince": "JacobsTown*PA\\",
+            "originatorCountryPostalCode": "US*19305\\",
+            "entryDetailSequenceNumber": 1
+          },
+          "addenda13": {
+            "id": "iat-datetime",
+            "typeCode": "13",
+            "ODFIName": "Wells Fargo",
+            "ODFIIDNumberQualifier": "01",
+            "ODFIIdentification": "231380104",
+            "ODFIBranchCountryCode": "US",
+            "entryDetailSequenceNumber": 1
+          },
+          "addenda14": {
+            "id": "iat-datetime",
+            "typeCode": "14",
+            "RDFIName": "Citadel Bank",
+            "RDFIIDNumberQualifier": "01",
+            "RDFIIdentification": "121042882",
+            "RDFIBranchCountryCode": "CA",
+            "entryDetailSequenceNumber": 1
+          },
+          "addenda15": {
+            "id": "iat-datetime",
+            "typeCode": "15",
+            "receiverIDNumber": "987465493213987",
+            "receiverStreetAddress": "2121 Front Street",
+            "entryDetailSequenceNumber": 1
+          },
+          "addenda16": {
+            "id": "iat-datetime",
+            "typeCode": "16",
+            "receiverCityStateProvince": "LetterTown*AB\\",
+            "receiverCountryPostalCode": "CA*80014\\",
+            "entryDetailSequenceNumber": 1
+          },
+          "addenda17": [
+            {
+              "id": "iat-datetime",
+              "typeCode": "17",
+              "paymentRelatedInformation": "This is an international payment",
+              "sequenceNumber": 1,
+              "entryDetailSequenceNumber": 1
+            }
+          ],
+          "addenda18": [
+            {
+              "id": "iat-datetime",
+              "typeCode": "18",
+              "foreignCorrespondentBankName": "Bank of France",
+              "foreignCorrespondentBankIDNumberQualifier": "01",
+              "foreignCorrespondentBankIDNumber": "456456456987987",
+              "foreignCorrespondentBankBranchCountryCode": "FR",
+              "sequenceNumber": 1,
+              "entryDetailSequenceNumber": 1
+            }
+          ]
+        }
+      ],
+      "batchControl": {
+        "id": "iat-datetime",
+        "serviceClassCode": 225,
+        "entryAddendaCount": 10,
+        "entryHash": 12104288,
+        "totalDebit": 100000,
+        "totalCredit": 0,
+        "companyIdentification": "231380102",
+        "ODFIIdentification": "23138010",
+        "batchNumber": 1
+      }
+    }
+  ],
+  "fileControl": {
+    "id": "iat-datetime",
+    "batchCount": 1,
+    "blockCount": 2,
+    "entryAddendaCount": 10,
+    "entryHash": 12104288,
+    "totalDebit": 100000,
+    "totalCredit": 0
+  },
+  "fileADVControl": {
+    "id": "iat-datetime",
+    "batchCount": 0,
+    "entryAddendaCount": 0,
+    "entryHash": 0,
+    "totalDebit": 0,
+    "totalCredit": 0
+  },
+  "NotificationOfChange": null,
+  "ReturnEntries": null
+}