Odyssey
aACH.prg
1 <?php
2 /**
3  * @package aACH.prg -- The Admin side of ACH handling: download batches, canceling.
4  *
5  * Notes:
6  * For creating and downloading an ach batch, the follow process is used
7  * 1. Show all the unique transactions that are not in a batch
8  * 2. User selects the ones for the next batch.
9  * 3. transhdr entries are marked with the processed information
10  * 4. A new window/tab is opened and the batch report is shown and the ach file is downloaded
11  * 5. The batch creation grid is refreshed.
12  * If user views history later, they can click an icon to download the ach file or view the batch report again
13  * ACH batch file and report are saved in /home/<cu>/admin/ach/
14  *
15  * The batch id is an encoded string of the batch date/time.
16  *
17  * The <cu>transhdr.transactioncode table has specific meaning
18  * - For internal transfers it is the code sent to the core
19  * - For ACH transactions use for determining the Service Class Code and Standard Entry Class Codes:
20  * Service Class Code:
21  * 1X = credit with respect to the RDFI
22  * 2X = debit with respect to RDFI
23  * 3X = mixed credit/debit with respect to RDFI
24  * Standard Entry Class Code
25  * X = P (PPD), C (CCD), W (WEB)
26  * PPD/CCD are for ACH feature; WEB is for External Transfer feature
27  * NOTE: It is possible in the future that the transhdr.transactioncode is just a flag to make sure
28  * the correct SSC and SEC are created, or to better identify certain transactions (e.g. micro
29  * deposits) for specialized handling. (Addendum: As of 10/05/2017 I think we are at this point. The code
30  * building the file will reflect this. "W" means it comes from the Scheduled/Transfer feature, "P" means
31  * it came from the micro deposit creation from the External Account feature, and "C" means it came from
32  * the "commercial" ACH feature.)
33  * This is because we might create two entries under one company for most external transfers but two
34  * separate companies with one entry each for an external transfer to make a loan payment at the CU. For
35  * this later case it is a "WEB" for the external account debit and a "PPD" for the loan payment. Additionally
36  * the micro deposit offset needs to move the money into the CU Admin configured account so we need to
37  * specifically be able to identify that <cu>transhdr entry.
38  * NOTE: When determining the local account number for the ACH record take the sub-account and add to the end
39  * of the account number and make sure any non-alphanumeric characters are removed.
40  */
41 require_once( "../../banking/library/hcuACH.i" );
42 require_once("$sharedLibrary/sFeatureMnu.i");
43 require_once( "../library/aGroupSupport.i" );
44 
45 // include a file we need for gathering admin info
46 // for the MakeACHBatch / CreateAchFiles functions.
47 require_once( dirname(__FILE__) . "/../library/aAdminSettings.i" );
48 
49 const DFI_ACCOUNT_MAX_SIZE = 17;
50 
51 // Get advanced permissions
52 $advPerm = checkPerm($Cn, "aACHAdv", $Cu)[1] == 1;
53 
54 $self = "$menu_link?ft=$ft";
55 
56  $admVars = array();
57  $admOk = array(
58  "operation" => array("filter" => FILTER_SANITIZE_STRING),
59  "page" => array("filter" => FILTER_SANITIZE_STRING),
60  "trans_id" => array("filter" => FILTER_SANITIZE_NUMBER_INT)
61  );
62  HCU_ImportVars( $admVars, "", $admOk );
63 
64  $operation = trim( isset( $admVars["operation"]) ? $admVars["operation"] : "" );
65 
66  $page = trim( isset( $admVars["page"]) ? $admVars["page"] : "" );
67 
68  // set up a common "environment"
69  $ADM_ENV = array();
70  $ADM_ENV["SYSENV"] = $SYSENV;
71  $ADM_ENV["Cu"] = $Cu;
72  $ADM_ENV["Cn"] = $Cn;
73  $ADM_ENV["dbh"] = $dbh;
74  $ADM_ENV["self"] = $self;
75  $ADM_ENV["menu_link"] = $menu_link;
76  $ADM_ENV["advPerm"] = $advPerm;
77 
78  // The max time allowed for being able to remove
79  // transactions from a batch after its creation.
80  // Current value is number of days.
81 
82  // The removeFromBatchAllowed value will be sent
83  // to the client with the batch detail records
84  // when called: GetBatchDetail().
85  $ADM_ENV["removeFromBatchTime"] = 14;
86 
87  // operations are the .data activities called by a kendo datasource or other ajax mechanism
88  if ($operation != "") {
89  try {
90  if (!in_array($ft, array(100,99))) {
91  throw new exception("Data call isn't using expected ft!", 12);
92  }
93 
94  $aryReply = array();
95  $aryResult = array();
96  $aryInfo = array();
97  $aryError = array();
98 
99  switch($operation) {
100  case "achGetBatches":
101  $return = GetCompletedBatches( $ADM_ENV );
102 
103  if ($return['code'] != '000') {
104  throw new Exception ( HCU_JsonEncode($return["errors"]) );
105  }
106 
107  // return the data
108  $aryResult[$operation] = $return["data"];
109  break;
110  case "achMakeBatch":
111  if (!$advPerm) {
112  // Need to keep structure of other thrown errors.
113  throw new exception ( HCU_JsonEncode(array("Need advanced permissions to make a batch.")) );
114  }
115  $admOk = array(
116  "trans_list" => array("filter" => FILTER_SANITIZE_STRING)
117  );
118  HCU_ImportVars( $admVars, "", $admOk );
119  $returnMake = MakeACHBatch( $ADM_ENV, $admVars );
120 
121  if ($returnMake['code'] != '000') {
122  throw new Exception ( HCU_JsonEncode($returnMake["errors"]) );
123  } else {
124  $aryInfo[] = "Create ACH upload file. Processed time: " . $returnMake["data"]["batch_time"];
125 
126  // set return data to update grid
127  // batch file create / notifications may fail but
128  // should not hold up the process
129  $aryResult[$operation] = $returnMake["data"];
130 
131  // now create the batch files
132  $admVars["batch_id"] = $returnMake["data"]["batch_id"];
133  $returnCreate = CreateAchFiles( $ADM_ENV, $admVars );
134 
135  if ($returnCreate['code'] != '000') {
136  // must use merge to save the previous success mesage
137  $returnErrors = explode('^', $returnCreate['errors']);
138  $aryInfo = array_merge($aryInfo, $returnErrors);
139  $aryInfo[] = "Use the <a href=\"main.prg?ft=100\">History</a> screen to attempt re-building the ACH upload file.";
140  } else {
141  // set return data to update grid
142  // notifications may fail but should not hold up the process
143  //$aryResult[$operation] = $returnMake["data"];
144 
145  // now send batch notification emails
146  $returnNotify = SendACHNotifications( $ADM_ENV, $admVars['trans_list'] );
147  if ($returnNotify['code'] !== '000') {
148  // do not throw error here, put the error into the info array
149  // and display in dialog box after return.
150  $aryInfo[] = $returnNotify['errors'];
151  if ($returnNotify['code'] == 103) {
152  $aryInfo[] = "This feature requires an \"ACH Notifications\" email on the <a href=\"main.prg?ft=46\">CU Email Notifications</a> screen.";
153  } else if ($returnNotify['code'] == 104) {
154  $aryInfo[] = "This feature requires an \"ACH Notifications\" email template on the <a href=\"main.prg?ft=54\">Settings</a> screen.";
155  }
156  }
157  }
158  }
159  break;
160  case "achGetBatchDetail":
161  $admOk = array(
162  "batch_id" => array("filter" => FILTER_SANITIZE_STRING)
163  );
164  HCU_ImportVars( $admVars, "", $admOk );
165 
166  $return = GetBatchDetail( $ADM_ENV, $admVars );
167 
168  if ($return['code'] != '000') {
169  throw new Exception ( HCU_JsonEncode($return["errors"]) );
170  }
171 
172  // return the data
173  $aryResult[$operation] = $return["data"];
174  break;
175  case "achGetReady":
176  $return = GetApprovedACHTrans( $ADM_ENV );
177 
178  if ($return['code'] != '000') {
179  throw new Exception ( HCU_JsonEncode($return["errors"]) );
180  }
181 
182  // return the data
183  $aryResult[$operation] = $return["data"];
184  break;
185  case "achGetDetail":
186  $transId = isset( $admVars["trans_id"]) ? $admVars["trans_id"] : 0;
187  $return = GetACHTransDetail( $ADM_ENV, $transId );
188 
189  if ($return['code'] != '000') {
190  throw new Exception ( HCU_JsonEncode($return["errors"]) );
191  }
192 
193  // return the data
194  $aryResult[$operation] = $return["data"];
195  break;
196  case "achCancel":
197  $admOk = array(
198  "trans_list" => array("filter" => FILTER_SANITIZE_STRING)
199  );
200  HCU_ImportVars( $admVars, "", $admOk );
201 
202  if (!$advPerm) {
203  // Need to keep structure of other thrown errors.
204  throw new exception ( HCU_JsonEncode(array("Need advanced permissions to cancel a batch.")) );
205  }
206 
207  $return = CancelACHTrans( $ADM_ENV, $admVars["trans_list"] );
208 
209  if ($return['code'] != '000') {
210  throw new Exception ( HCU_JsonEncode($return["errors"]) );
211  }
212 
213  // return the data
214  $aryResult[$operation] = $return["data"];
215  break;
216  case "ach_download_file":
217  // download the ach file
218  // NOTE: Exists when done.
219  $admOk = array(
220  "batch_id" => array("filter" => FILTER_SANITIZE_STRING),
221  "download_type" => array("filter" => FILTER_SANITIZE_STRING)
222  );
223  HCU_ImportVars( $admVars, "", $admOk );
224 
225  if (!$advPerm) {
226  // Need to keep structure of other thrown errors.
227  ShowErrorPage(array("Need advanced permissions to download ACH file."));
228  exit;
229  }
230 
231  $return = DownloadAchFile( $ADM_ENV, $admVars );
232 
233  if ($return['code'] != '000') {
234  // need to show an error screen and exit
235  ShowErrorPage( $return["errors"] );
236  exit;
237  }
238 
239  // return the data as an output file so the browser will save it
240  $filename = $return["data"]["filename"];
241  $outString = $return["data"]["output"];
242  header("Content-length: " . strlen($outString));
243  header("Content-type: application/octetstream");
244  header("Content-disposition: attachment; filename=\"$filename\"");
245  print ( $outString );
246 
247  // since done, exit so no more output
248  exit;
249  break;
250  case "achCreateFile":
251  // create the ach file and report if they don't exist
252  // note: don't download or anything
253  $admOk = array(
254  "batch_id" => array("filter" => FILTER_SANITIZE_STRING)
255  );
256  HCU_ImportVars( $admVars, "", $admOk );
257 
258  if (!$advPerm) {
259  // Need to keep structure of other thrown errors.
260  throw new exception ( HCU_JsonEncode(array("Need advanced permissions to create file.")) );
261  }
262 
263  $return = ACH_CreateFile($ADM_ENV, $admVars);
264  if ($return['code'] !== '000') {
265  // There is a single error thrown from the BuildACHBusiness
266  // function in CreateACHFiles that has two parts separated
267  // by the '^' symbol. Splitting will allow each part to be
268  // printed on a new line in the homecu error popout.
269  $errors = explode("^", $return['errors']);
270  throw new Exception( HCU_JsonEncode($errors) );
271  }
272 
273  // success message
274  $aryInfo = $return['data']['message'];
275  $aryResult[$operation] = $return['data'];
276  break;
277  case "achSendNotifications":
278  $admOk = array(
279  "batch_id" => array("filter" => FILTER_SANITIZE_STRING)
280  );
281  HCU_ImportVars( $admVars, "", $admOk );
282 
283  if (!$advPerm) {
284  // Need to keep structure of other thrown errors.
285  throw new exception ( HCU_JsonEncode(array("Need advanced permissions to send notifications.")) );
286  }
287 
288  // retrieve processed headers for restart
289  $transHdrIds = GetProcessedHeaders( $ADM_ENV, $admVars['batch_id'] );
290  $return = SendACHNotifications( $ADM_ENV, $transHdrIds);
291  if ($return['code'] !== '000') {
292  // depending on the code returned use specific error messages
293  // code 103 means no email setup in cu email notifications, add link to this screen
294  // code 104 means no email template was found, add link to the settings screen
295  $returnErrors = array();
296  $returnErrors[] = $return['errors'];
297  if ($return['code'] == 103) {
298  $returnErrors[] = "This feature requires an \"ACH Notifications\" email on the <a href=\"main.prg?ft=46\">CU Email Notifications</a> screen.";
299  } else if ($return['code'] == 104) {
300  $returnErrors[] = "This feature requires an \"ACH Notifications\" email template on the <a href=\"main.prg?ft=54\">Settings</a> screen.";
301  }
302 
303  throw new Exception( HCU_JsonEncode($returnErrors) );
304  }
305 
306  // Return the id to show it worked
307  $aryResult[$operation] = array("batch_id"=>$admVars['batch_id']);
308  $aryInfo[] = "Email notifications have been sent";
309  break;
310  case "achRemoveBatchTransactions":
311  // batch_id: processed_date
312  // trans_list: list of transactions ids to remove from the batch
313  $admOk = array(
314  "batch_id" => array("filter" => FILTER_SANITIZE_STRING),
315  "trans_list" => array("filter" => FILTER_SANITIZE_STRING)
316  );
317  HCU_ImportVars( $admVars, "", $admOk );
318 
319  if (!$advPerm) {
320  // Need to keep structure of other thrown errors.
321  throw new exception ( HCU_JsonEncode(array("Need advanced permissions to remove transactions.")) );
322  }
323 
324  $return = ACH_RemoveBatchTransactions($ADM_ENV, $admVars);
325  if ($return['code'] !== '000') {
326  throw new Exception( HCU_JsonEncode($return['errors']) );
327  }
328 
329  // success message
330  $aryInfo[] = $return['data']['message'];
331  $aryResult[$operation] = $return['data'];
332  break;
333  default:
334  throw new Exception ("Operation not recognized: $operation.");
335  break;
336  }
337  } catch (Exception $ex) {
338  $aryReply["errors"] = HCU_JsonDecode( $ex->getMessage() );
339 
340  // if returning error, not replying with data
341  $aryResult = array();
342 
343  // if returning error, not returning status
344  $aryInfo = array();
345  }
346 
347  if ( count( $aryInfo ) ) {
348  $aryReply["info"] = $aryInfo;
349  }
350 
351  if ( count( $aryResult ) ) {
352  $aryReply["data"] = $aryResult;
353  }
354 
355  header('Content-type: application/json');
356 
357  print HCU_JsonEncode( array( "results" => $aryReply ) );
358 
359  // no more processing
360  exit;
361  } else {
362  try {
363  // the new email notifications are for commercial use only so
364  // do not show email notification fields at all on the ach tab
365  // if the credit union doesn't have access.
366 
367  // use the feature menu to determine this fact.
368  $ADM_ENV['commercialAccess'] = false;
369  $featureList = FetchMenuFeatureList( array("dbh" => $dbh), array("Cu"=>$Cu) );
370  if (in_array("ACHPMT", $featureList['data']) || in_array("ACHCOL", $featureList['data'])) {
371  $ADM_ENV['commercialAccess'] = true;
372  }
373 
374  switch($page) {
375  case "show_items":
376  if ($ft != 99) {
377  throw new exception("Invalid access.", 14);
378  }
379  PrintActionPage( $ADM_ENV, $admVars );
380  break;
381  case "ach_show_report":
382  if ($ft != 101) {
383  ShowErrorPage("Invalid access");
384  }
385  $admOk = array(
386  "batch_id" => array("filter" => FILTER_SANITIZE_STRING),
387  "download" => array("filter" => FILTER_SANITIZE_STRING)
388  );
389  HCU_ImportVars( $admVars, "", $admOk );
390 
391  // these variables are global in main.prg and are used in hcuDispFunctions to set the include environment
392  $ADM_ENV["cloudfrontDomainName"] = $cloudfrontDomainName;
393  $ADM_ENV["homecuKendoVersion"] = $homecuKendoVersion;
394  $ADM_ENV["bootstrapVersion"] = $bootstrapVersion;
395  $ADM_ENV["fontawesomeVersion"] = $fontawesomeVersion;
396  $ADM_ENV["loadStyleSheet"] = $loadStyleSheet;
397  $ADM_ENV["chome"] = $ADM_ENV["Cu"];
398 
399  PrintAchReport( $ADM_ENV, $admVars );
400  break;
401  case "validate":
402  if ($ft != 100) {
403  throw new exception("Invalid access.", 14);
404  }
405  $admOk = array(
406  "file" => array("filter" => FILTER_SANITIZE_STRING),
407  "show_raw" => array("filter" => FILTER_VALIDATE_BOOLEAN)
408  );
409  HCU_ImportVars( $admVars, "", $admOk );
410 
411  PrintValidate( $ADM_ENV, $admVars );
412  break;
413  case "make_batch":
414  if ($ft != 100) {
415  throw new exception("Invalid access.", 14);
416  }
417  MakeBatch($self, $dbh, $Cu, $authval);
418  break;
419  default:
420  if ($ft != 100) {
421  throw new exception("Invalid access: $page", 13);
422  }
423  PrintMainPage( $ADM_ENV, $admVars );
424  break;
425  }
426  }
427  catch (exception $ex)
428  {
429 // unresolved: is this good enough?
430  $message = $ex->getMessage();
431  $code = $ex->getCode();
432 
433  print "<p>An unexpected error occured. If this error keeps happening please contact Support.</p>";
434 
435  if ( strlen( $message ) ) {
436  print "<p>Message: $message</p>";
437  }
438 
439  if ( strlen( $code ) ) {
440  print "<p>Code: $code</p>";
441 
442  if ( $code == 13 ) {
443  print "<p>Creating a new ACH upload file will open a report in a new window. <br />Make sure pop-ups are allowed for this website.</p>";
444  }
445  }
446  }
447  }
448 
449  // No more output after this!
450 
451 /****************** functions ******************/
452 // Show an error message. This is for handling errors that occur during a download and a new page might
453 // show up if the actual download doesn't happen.
454 function ShowErrorPage( $pErrors ) {
455 
456  $errorString = "";
457 
458  if ( is_array( $pErrors ) ) {
459  for ( $i = 0; $i < count( $pErrors ); $i++ ) {
460  $errorString .= $pErrors[$i] . "<br>";
461  }
462  } else {
463  $errorString = $pErrors;
464  }
465 
466  print <<< EOF
467  <!DOCTYPE html>
468  <html>
469  <head>
470  <title>Error</title>
471  </head>
472  <body>
473  <div class="error-block">
474  <div class="error-header">The following error(s) occured</div>
475  <p id="error">$errorString</p>
476  <p>Please try your operation again. If this error persists please contact Support.</p>
477  </div>
478  <style>
479  .error-header {
480  border-radius: 4px 4px 0 0;
481  border-bottom-style: solid;
482  border-bottom-width: 1px;
483  border-color: #dfd0d0;
484  font-size: 1.2em;
485  position: relative;
486  margin: -2px 0 10px -2px;
487  padding: .3em;
488  width: 100%;
489  height: 1.1em;
490  min-height: 16px;
491  }
492  .error-block {
493  color: #7f5050;
494  border-color: #dfd0d0;
495  border-radius: 4px;
496  border-style: solid;
497  border-width: 1px;
498  background-color: #fff0f0;
499  width: 600px;
500  height: 150px;
501  }
502  #error {
503  margin-left: 1em;
504  font-weight: bold;
505  }
506  p {
507  margin-left: 1em;
508  }
509  </style>
510  </body>
511  </html>
512 EOF;
513 } // end ShowErrorPage
514 
515 /**
516  * ACH_CreateFile
517  * This function will restart the batch file process and automatically
518  * start the email notification process if the files are generated successfully.
519  *
520  * @param $pAdmEnv array environment variables
521  * @param $pAdmVars array contains the batch_id
522  *
523  * @return $returnInfo array batch record
524  */
525 function ACH_CreateFile($pAdmEnv, $pAdmVars) {
526  $returnInfo = array("code" => "000", "errors" => "", "data" => array());
527 
528  try {
529  // Decode batch_id
530  $batch = HCU_PayloadDecode($pAdmEnv['Cu'], $pAdmVars['batch_id']);
531  $pAdmVars['batch_date'] = $batch['batch_date'];
532 
533  // Re-create batch files
534  $return = CreateAchFiles( $pAdmEnv, $pAdmVars );
535  if ($return['code'] != '000') {
536  throw new Exception ( $return['errors'], $return['code'] );
537  }
538 
539  $returnInfo['data']['message'] = array();
540  $returnInfo['data']['message'][] = "ACH Files created successfully";
541 
542  // Start email notifications
543  // Retrieve processed headers
544  $transHdrIds = GetProcessedHeaders( $pAdmEnv, $pAdmVars['batch_id'] );
545  $return = SendACHNotifications( $pAdmEnv, $transHdrIds);
546  if ($return['code'] !== '000') {
547  // do not throw error here, put the error into the info array
548  // and display in dialog box after return.
549  $returnInfo['data']['message'][] = $return['errors'];
550  if ($return['code'] == 103) {
551  $returnInfo['data']['message'][] = "This feature requires an \"ACH Notifications\" email on the <a href=\"main.prg?ft=46\">CU Email Notifications</a> screen.";
552  } else if ($return['code'] == 104) {
553  $returnInfo['data']['message'][] = "This feature requires an \"ACH Notifications\" email template on the <a href=\"main.prg?ft=54\">Settings</a> screen.";
554  }
555  }
556 
557  // Get updated batch record
558  $return = GetCompletedBatchRecord($pAdmEnv, $pAdmVars);
559  if ($return['code'] !== '000') {
560  throw new Exception( $return['errors'], $return['code'] );
561  }
562 
563  $returnInfo['data']['batch_record'] = $return['data']['batch_record'];
564  $returnInfo['data']['batch_id'] = $pAdmVars['batch_id'];
565  } catch (Exception $ex) {
566  $returnInfo["errors"] = $ex->getMessage();
567  $returnInfo["code"] = $ex->getCode();
568  }
569 
570  return $returnInfo;
571 }
572 
573 /**
574  * ACH_RemoveBatchTransactions
575  * This function will remove any selected transactions from a specific batch
576  * and gerneate a new ach and report file for the remaining transactions of
577  * the batch.
578  *
579  * @param $pAdmEnv array environment variables
580  * @param $pAdmVars array contains the batch_id
581  *
582  * @return $returnInfo array batch record
583  */
584 function ACH_RemoveBatchTransactions($pAdmEnv, $pAdmVars) {
585  $returnInfo = array("code" => "000", "errors" => "", "data" => array());
586 
587  try {
588  // decode batch_id
589  $batch = HCU_PayloadDecode($pAdmEnv['Cu'], $pAdmVars['batch_id']);
590  $pAdmVars['batch_date'] = $batch['batch_date'];
591 
592  // Not allowed to remove transactions after certain time
593  // from the creation of the batch.
594  $removeTransactionsTime = time() - $pAdmEnv['removeFromBatchTime'] * 24 * 60 * 60;
595  $removeBatchTime = strtotime($batch['batch_date']);
596  if ($removeBatchTime < $removeTransactionsTime) {
597  // Batch is too old, do not allow removal of transactions
598  $error = "Transactions cannot be removed from an ACH upload file that is more than 14 days old.";
599 
600  throw new Exception($error);
601  }
602 
603  // Remove specified transaction ids from the batch
604  $return = RemoveBatchTransactions($pAdmEnv, $pAdmVars);
605  if ($return['code'] !== '000') {
606  throw new Exception($return['errors']);
607  }
608 
609  // Remove old ach and report files for this batch
610  $return = RemoveBatchFiles($pAdmEnv, $pAdmVars);
611  if ($return['code'] !== '000') {
612  throw new Exception($return['errors']);
613  }
614 
615  // If the admin user removed all transactions from a particular batch
616  // the CreateAchFiles function will throw an error because no processed
617  // transactions were found.
618 
619  // The logic for the error cannot be changed because the Actions and History
620  // screens both have user actions that create files manually for a specific
621  // batch that should have transactions but for some reason may not.
622 
623  // Skip the error because it is reasonable to remove all transactions from
624  // a batch. The file will not be created.
625  $return = CreateAchFiles($pAdmEnv, $pAdmVars);
626  if ($return['code'] !== '000' && $return['code'] !== 300) {
627  throw new Exception($return['errors']);
628  }
629 
630  // Get updated batch record
631  $return = GetCompletedBatchRecord($pAdmEnv, $pAdmVars);
632  if ($return['code'] !== '000') {
633  throw new Exception($return['errors']);
634  }
635 
636  $returnInfo['data']['batch_record'] = $return['data']['batch_record'];
637  $returnInfo['data']['batch_id'] = $pAdmVars['batch_id'];
638  $returnInfo["data"]["message"] = "Remove transactions. ACH upload file creation date: " . $return['data']['batch_date'];
639  } catch (Exception $ex) {
640  $returnInfo["errors"] = $ex->getMessage();
641  $returnInfo["code"] = $ex->getCode();
642  }
643 
644  return $returnInfo;
645 } // end ACH_RemoveBatchTransactions
646 
647 /**
648  * GetCompletedBatchRecord:
649  * this function can be used to get a single completed
650  * batch record with the same format as the GetCompletedBatches
651  * function.
652  *
653  * The function requires a decoded, processed date timestamp
654  *
655  * @param $pAdmEnv array environment variables
656  * @param $pAdmVars array contains the processed_date timestamp
657  *
658  * @return $returnInfo array batch record
659  */
660 function GetCompletedBatchRecord($pAdmEnv, $pAdmVars) {
661  $returnInfo = array("code" => "000", "errors" => "", "data" => array());
662 
663  try {
664  $cu = strtolower( $pAdmEnv["Cu"] );
665 
666  $batchPreName = "Batch";
667  $batchPath = "/home/{$cu}/admin/ach/";
668 
669  // get the credit union's timezone
670  $tz = GetCreditUnionTimezone( $pAdmEnv["dbh"], $pAdmEnv["Cu"] );
671  $dateTime = new DateTime( $pAdmVars['batch_date'] );
672  $dateTime->setTimezone(new DateTimeZone($tz));
673  $batchDay = $dateTime->format("ymd");
674  $batchHour = $dateTime->format("His");
675  $batchDate = $dateTime->format("m/d/y H:i:s T");
676 
677  $baseFileName = "{$batchPreName}_{$batchDay}_{$batchHour}";
678  $batchAch = file_exists( "{$batchPath}{$baseFileName}.ach" );
679  $batchReport = file_exists( "{$batchPath}{$baseFileName}.txt" );
680 
681  $sql = "
682  SELECT
683  th.processed_date, th.processed_by,
684  COUNT(DISTINCT th.id) AS count,
685  SUM(CASE WHEN (th.feature_code = 'ACHPMT' OR th.feature_code = 'ACHCOL') AND td.email_notify = 1 THEN 1 ELSE 0 END) AS notify
686  FROM {$pAdmEnv['Cu']}transhdr AS th
687  JOIN {$pAdmEnv['Cu']}transdtl AS td ON th.id = td.transhdr_id
688  WHERE th.processed_date = '{$pAdmVars['batch_date']}'
689  GROUP BY processed_date, processed_by
690  ORDER BY processed_date DESC";
691  $sqlRs = db_query( $sql, $pAdmEnv["dbh"] );
692  if ( !$sqlRs ) {
693  throw new Exception( "ACH Query Error - getting approved ach txns" );
694  }
695 
696  $sqlRecord = db_fetch_assoc($sqlRs, 0);
697 
698  // return same style record as the fulle GetCompletedBatches function
699  // for a single record.
700  $returnInfo['data']['batch_date'] = $batchDate;
701  $returnInfo['data']['batch_record'] = array();
702  if ($sqlRecord) {
703  $returnInfo['data']['batch_record']['batch_id'] = $pAdmVars['batch_id'];
704  $returnInfo['data']['batch_record']['processed_by'] = $sqlRecord['processed_by'];
705  $returnInfo['data']['batch_record']['count'] = intval($sqlRecord['count']);
706  $returnInfo['data']['batch_record']['notify'] = intval($sqlRecord['notify']);
707  $returnInfo['data']['batch_record']['display'] = $batchDate;
708  $returnInfo['data']['batch_record']['has_ach_file'] = file_exists( "{$batchPath}{$baseFileName}.ach" );
709  $returnInfo['data']['batch_record']['has_ach_report'] = file_exists( "{$batchPath}{$baseFileName}.txt" );
710  }
711  } catch (Exception $ex) {
712  $returnInfo["errors"] = $ex->getMessage();
713  $returnInfo["code"] = $ex->getCode();
714  }
715 
716  return $returnInfo;
717 } // end GetCompletedBatchRecord
718 
719 /**
720  * RemoveBatchFiles: Remove the current ach and report files associated
721  * with a specified batch.
722  *
723  * @param $pAdmEnv array environment variables
724  * @param $pAdmVars array contains the processed_date
725  * for retrieving the file names.
726  *
727  * @return $returnInfo array error/success information
728  */
729 function RemoveBatchFiles($pAdmEnv, $pAdmVars) {
730  $returnInfo = array("code" => "000", "errors" => "", "data" => array());
731 
732  try {
733  $cu = strtolower( $pAdmEnv["Cu"] );
734 
735  $batchDate = $pAdmVars['batch_date'];
736  $batchPreName = "Batch";
737  $batchPath = "/home/{$cu}/admin/ach/";
738 
739  // get the credit union's timezone
740  $tz = GetCreditUnionTimezone( $pAdmEnv["dbh"], $pAdmEnv["Cu"] );
741 
742  // use the batch timestamp to figure out the creation date/time and filename
743  $dateTime = new DateTime( $batchDate );
744  $dateTime->setTimezone(new DateTimeZone($tz));
745  $batchDay = $dateTime->format("ymd");
746  $batchHour = $dateTime->format("His");
747 
748  // verify the directory exists
749  if ( !file_exists( $batchPath ) ) {
750  throw new Exception( "Remove ACH Files Error - Directory does not exist - please contact Support", 201 );
751  }
752 
753  $achBaseName = "{$batchPreName}_{$batchDay}_{$batchHour}";
754 
755  $achFileName = "{$achBaseName}.ach";
756  $achFilePath = "{$batchPath}{$achFileName}";
757 
758  $achReportName = "{$achBaseName}.txt";
759  $achReportPath = "{$batchPath}{$achReportName}";
760 
761  if ( file_exists($achFilePath) ) {
762  if ( !unlink($achFilePath) ) {
763  throw new Exception("Error Removing ACH Upload File - Failed to remove ACH upload file.", 202);
764  }
765  }
766 
767  if ( file_exists($achReportPath) ) {
768  if ( !unlink($achReportPath) ) {
769  throw new Exception("Error Removing ACH Upload Report File - Failed to remove ACH upload report file.", 203);
770  }
771  }
772 
773  // return formatted date for success message.
774  $returnInfo['data']['batch_time'] = $dateTime->format("m/d/y H:i:s T");
775  } catch (Exception $ex) {
776  $returnInfo["errors"] = $ex->getMessage();
777  $returnInfo["code"] = $ex->getCode();
778  }
779 
780  return $returnInfo;
781 } // end RemoveBatchFiles
782 
783 /**
784  * RemoveBatchTransactions:
785  * Remove a list of transactions from a specified batch.
786  *
787  * @param $pAdmEnv array environment variables
788  * @param $pAdmVars array contains list of transaction
789  * ids to remove and the processed_date.
790  *
791  * @return $returnInfo array error/success information
792  */
793  function RemoveBatchTransactions($pAdmEnv, $pAdmVars) {
794  $returnInfo = array("code" => "000", "errors" => "", "data" => array());
795 
796  try {
797  $batchDate = $pAdmVars['batch_date'];
798  $batchRemove = trim($pAdmVars['trans_list']);
799 
800  $sql = "
801  UPDATE {$pAdmEnv['Cu']}transhdr SET
802  processed_status = NULL,
803  processed_date = NULL,
804  processed_by = NULL
805  WHERE processed_date = '{$batchDate}'
806  AND id IN ({$batchRemove})";
807  $sqlRs = db_query($sql, $pAdmEnv['dbh']);
808  if (!$sqlRs) {
809  throw new Exception("Failed to remove transactions", 200);
810  }
811  } catch (Exception $ex) {
812  $returnInfo["errors"] = $ex->getMessage();
813  $returnInfo["code"] = $ex->getCode();
814  }
815 
816  return $returnInfo;
817  } // end RemoveBatchTransactions
818 
819 /**
820  * SendACHNotifications
821  * send email notifications to all parties involved in batch transactions
822  * for the batch creation process.
823  *
824  * @param $pAdmEnv: array: environment data
825  * @param $pTransHdrIds: string: comma separated list of transhdr id values
826  * @return $returnInfo: array: error, or nothing
827  *
828  */
829 function SendACHNotifications( $pAdmEnv, $pTransHdrIds ) {
830  $returnInfo = array("code" => "000", "errors" => "", "data" => array());
831 
832  try {
833  // the ach notifications email and ach notification template are
834  // only necessary for ACHPMT/ACHCOL at this time
835  $achNotifyEmail = "";
836  $achNotifyTemplate = "";
837 
838  // Check cuadmnotify for email value of achnotify
839  // Cannot send email notifications if this email doesn't exist.
840  $sql = "SELECT email FROM cuadmnotify WHERE cu='{$pAdmEnv['Cu']}' AND role = 'achnotify'";
841  $sqlRs = db_query( $sql, $pAdmEnv['dbh'] );
842  if (!$sqlRs) {
843  throw new Exception( "ACH Query Error - unable to get ACH notifications email: 100", 100 );
844  }
845  $sqlData = db_fetch_assoc($sqlRs, 0);
846  if (is_array($sqlData)) {
847  // Email was found for ACH Notifications
848  $achNotifyEmail = $sqlData['email'];
849  }
850 
851  // Check cuadmin.settings for achnotify value
852  // Cannot send email notifications if this value doesn't exist, it is
853  // the email template.
854  $sql = "SELECT settings::json->>'achnotify' AS template FROM cuadmin WHERE cu='{$pAdmEnv['Cu']}'";
855  $sqlRs = db_query( $sql, $pAdmEnv['dbh'] );
856  if (!$sqlRs) {
857  throw new Exception( "ACH Query Error - unable to get ACH notifications email template: 101", 101 );
858  }
859  $sqlData = db_fetch_assoc($sqlRs, 0);
860  if (HCU_array_key_value("template", $sqlData)) {
861  // Email template was found for ACH Notifications
862  $achNotifyTemplate = $sqlData['template'];
863  }
864 
865  // Get list of transhdr id's
866  $idList = array();
867  $inputList = explode( ",", $pTransHdrIds );
868  for ( $i = 0; $i < count( $inputList ); $i++ ) {
869  if ( ctype_digit( $inputList[$i] ) ) {
870  $idList[] = $inputList[$i];
871  }
872  }
873 
874  $idString = implode( ",", $idList );
875 
876  // Send email to each partner that has email_notify = 1
877  if ( strlen( $idString ) > 0 ) {
878  // Get all transaction details
879  $sql = "
880  SELECT dtl.id, dtl.amount, dtl.transdata, hdr.processed_date, hdr.feature_code, grp.group_name
881  FROM {$pAdmEnv['Cu']}transdtl dtl
882  LEFT JOIN {$pAdmEnv['Cu']}transhdr hdr ON hdr.id = dtl.transhdr_id
883  LEFT JOIN {$pAdmEnv['Cu']}user usr ON usr.user_id = hdr.posted_by
884  LEFT JOIN {$pAdmEnv['Cu']}group grp ON grp.group_id = usr.group_id
885  WHERE dtl.transhdr_id IN ($idString)
886  AND dtl.email_notify = 1";
887  $sqlRs = db_query( $sql, $pAdmEnv['dbh'] );
888  if ($sqlRs) {
889  $dataRow = 0;
890  while ($row = db_fetch_array($sqlRs, $dataRow++)) {
891  $row['transdata'] = HCU_JsonDecode($row['transdata']);
892 
893  // since notifications are only for ach commercial currently,
894  // do not send email if the feature_code is not ACHPMT or ACHCOL.
895  if ($row['feature_code'] == "ACHPMT" || $row['feature_code'] == "ACHCOL") {
896  if ($achNotifyEmail == "") {
897  throw new Exception( "ACH Notifications Error - ACH notifications email was not found: 103", 103 );
898  }
899 
900  if ($achNotifyTemplate == "") {
901  throw new Exception( "ACH Notifications Error - ACH notifications template was not found: 104", 104 );
902  }
903 
904  // Replace content placeholders with transaction data
905  // for each partner.
906  $achNotifyMessage = BuildACHNotification( $pAdmEnv, $row, $achNotifyTemplate );
907  if ($achNotifyMessage['code'] !== '000') {
908  throw new Exception( "ACH Notifications Error - Unable to build ACH notifications email: 105", 105 );
909  }
910 
911  // Send email
912  $notify = new ErrorMail;
913  $notify->header = "Content-Type: text/html";
914  $notify->mailto = $achNotifyMessage['data']['email'];
915  $notify->mailfrom = $achNotifyEmail;
916  $notify->subject = "Funds Transfer Notice";
917  $notify->msgbody = $achNotifyMessage['data']['message'];
918  $notify->callingfunction = __FUNCTION__;
919  $notify->file = __FILE__;
920  $notify->cu = $pAdmEnv['Cu'];
921  $notify->SendMail();
922 
923  // Eventually we may use an amazon api for emails
924  // this will give us some error checking ability.
925  //
926  // When error handling is available, check if the email
927  // has failed, do not set the transdtl.email_notify to 2.
928  $sqlUpd = "UPDATE {$pAdmEnv['Cu']}transdtl SET email_notify=2 WHERE id={$row['id']}";
929  $sqlUpdRs = db_query( $sqlUpd, $pAdmEnv['dbh'] );
930  if (!$sqlUpdRs) {
931  throw new Exception( "ACH Query Error - unable to update ACH upload file details: 106", 106 );
932  }
933  }
934  }
935 
936  // Because we have no error handling on the ErrorMail class
937  // we do not need to check if any of the above emails have failed.
938  //
939  // Set the transhdr processed_status to 30
940  //
941  // When error handling is available, check if any emails have failed
942  // and for which transhdr id, do not update this transhdr processed_status
943  // to 30.
944  $sql = "UPDATE {$pAdmEnv['Cu']}transhdr SET processed_status=30 WHERE id IN ($idString)";
945  $sqlRs = db_query( $sql, $pAdmEnv['dbh'] );
946  if (!$sqlRs) {
947  throw new Exception( "ACH Query Error - unable to update ACH upload file processed: 107", 107 );
948  }
949  } else {
950  throw new Exception( "ACH Query Error - unable to get partner emails: 108", 108 );
951  }
952  }
953  } catch (Exception $ex) {
954  $returnInfo["errors"] = $ex->getMessage();
955  $returnInfo["code"] = $ex->getCode();
956  }
957 
958  return $returnInfo;
959 } // end SendACHNotifications
960 
961 /**
962  * GetProcessedHeaders
963  * this function retrieves all transaction headers
964  * associated with a specific batch_id (processed_date)
965  *
966  * @param $pAdmEnv: array: environment data
967  * @param $pProcessedDate: array: processed date for the batch
968  * @return $idString: array: string of transhdr id values
969  *
970  */
971 function GetProcessedHeaders( $pAdmEnv, $pProcessedDate ) {
972  $batch = HCU_PayloadDecode($pAdmEnv['Cu'], $pProcessedDate);
973  $batchDate = $batch['batch_date'];
974 
975  $sql = "SELECT id FROM {$pAdmEnv['Cu']}transhdr WHERE processed_date = '$batchDate'";
976  $sqlRs = db_query( $sql, $pAdmEnv['dbh'] );
977  if (!$sqlRs) {
978  throw new Exception( "ACH Query Error - unable to get ACH upload file information", 102 );
979  }
980 
981  $rowIndex = 0;
982  $inputList = array();
983  while ($row = db_fetch_assoc( $sqlRs, $rowIndex++ )) {
984  $inputList[] = $row['id'];
985  }
986 
987  $idString = implode(",", $inputList);
988  return $idString;
989 } // GetProcessedHeaders
990 
991 /**
992  * BuildACHNotification
993  * this function will replace all placeholder data in the achnotify
994  * email tempalte with the given data values.
995  *
996  * @param $pAdmEnv: array: environment data
997  * @param $pTransdata: array: data to replace the placeholders in template
998  * @param $pTemplate: string: email template containing placeholders that
999  * need to be replaced with data value.
1000  * @return $returnInfo: array: array containing errors or updated content
1001  */
1002 function BuildACHNotification( $pAdmEnv, $pTransData, $pTemplate ) {
1003  $returnInfo = array("code" => "000", "errors" => "", "data" => array());
1004 
1005  // this array will contain the data values in the same order as the placeholder strings
1006  $stringData = array();
1007 
1008  // this array contains placeholder strings that will be replaced with $stringData
1009  $stringReplacements = array(
1010  // Transaction data
1011  "{{company}}", "{{transactiontype}}", "{{amount}}", "{{date}}",
1012  // Remote account data
1013  "{{accountname}}", "{{routing}}", "{{accountnumber}}", "{{accounttype}}"
1014  );
1015 
1016  try {
1017  // fill in data to replace placeholders
1018  $stringData[] = $pTransData['group_name'];
1019  $stringData[] = is_array($pTransData['transdata']['acct_source']) ? "withdrawal" : "deposit";
1020  $stringData[] = $pTransData['amount'];
1021 
1022  $date = new DateTime( $pTransData['processed_date'] );
1023  $stringData[] = date_format($date, "m/d/Y");
1024 
1025  $acctInfo = is_array($pTransData['transdata']['acct_source']) ?
1026  $pTransData['transdata']['acct_source'] :
1027  $pTransData['transdata']['acct_dest'];
1028 
1029  $stringData[] = $acctInfo['remote_entity']['name'];
1030  $stringData[] = $acctInfo['rdfi']['rdfi_routing'];
1031 
1032  $stringData[] = strlen($acctInfo['rdfi']['rdfi_account']) > 2 ?
1033  "ending in " . substr($acctInfo['rdfi']['rdfi_account'], -2) :
1034  "ending in " . $acctInfo['rdfi']['rdfi_account'];
1035  $stringData[] = $acctInfo['rdfi']['rdfi_account_type'] == 10 ? "Checking" : "Savings";
1036 
1037  // replace template placeholders
1038  $emailBody = $pTemplate;
1039  for ($i = 0; $i < count($stringReplacements); $i++) {
1040  // replace each placeholder, each placeholder should be the same index as it's data
1041  $emailBody = str_replace($stringReplacements[$i], $stringData[$i], $emailBody);
1042  }
1043 
1044  // Return partner email with email body
1045  $returnInfo['data']['email'] = $acctInfo['remote_entity']['email'];
1046  $returnInfo['data']['message'] = $emailBody;
1047 
1048  } catch (Exception $ex) {
1049  $returnInfo["errors"] = $ex->getMessage();
1050  $returnInfo["code"] = "999";
1051  }
1052 
1053  return $returnInfo;
1054 } // end BuildACHNotification
1055 
1056 /**
1057  * CancelACHTrans
1058  * Marks an approved ACH transaction as cancelled by the Admin user.
1059  *
1060  * @param object $pAdmEnv -- structure with common and environment information
1061  * @param string $pTransList -- the transactions to cancel (comma separated list
1062  *
1063  * @return object $returnInfo - structure with data and error info.
1064  */
1065 function CancelACHTrans( $pAdmEnv, $pTransList ) {
1066  $returnInfo = array("code" => "000", "errors" => "", "data" => array());
1067 
1068  try {
1069  // first, make sure ids are all valid
1070  $idList = array();
1071  $inputList = explode( ",", $pTransList );
1072  for ( $i = 0; $i < count( $inputList ); $i++ ) {
1073  if ( ctype_digit( $inputList[$i] ) ) {
1074  $idList[] = $inputList[$i];
1075  }
1076  }
1077 
1078  $idString = implode( ",", $idList );
1079 
1080  if ( strlen( $idString ) > 0 ) {
1081  // the banking user can see the cancellation in their User Activity screen
1082  $sql = "SELECT now() AT TIME ZONE 'UTC'";
1083  $rs = db_query( $sql, $pAdmEnv["dbh"] );
1084  if ($rs) {
1085  list( $processedStamp ) = db_fetch_array( $rs, 0 );
1086  } else {
1087  throw new Exception( "ACH Query Error - unable to get timestamp" );
1088  }
1089 
1090  $sql = "UPDATE {$pAdmEnv["Cu"]}transhdr SET processed_status = 99,
1091  processed_date = '$processedStamp',
1092  processed_by = '{$pAdmEnv["Cn"]}'
1093  WHERE id in ($idString) ";
1094  $rs = db_query( $sql, $pAdmEnv["dbh"] );
1095  if ( !$rs ) {
1096  throw new Exception( "ACH Query Error - cancel" );
1097  }
1098 
1099  // return list of Ids cancelled
1100  $returnInfo["data"] = $idString;
1101  } else {
1102  throw new Exception( "ACH Cancel Error - invalid list" );
1103  }
1104  } catch (Exception $ex) {
1105  $logInfo = array( "error" => $ex->getMessage(), "code" => $ex->getCode() );
1106 
1107  $returnInfo["errors"] = $ex->getMessage();
1108  $returnInfo["code"] = "999";
1109  }
1110 
1111  return $returnInfo;
1112 } // end CancelACHTrans
1113 
1114 /**
1115  * MakeACHBatch
1116  * Marks the given list of transaction ids as part of the same batch by the Admin user.
1117  * NOTE: The batch is denoted by all transactons with the same processed timestamp.
1118  *
1119  * @param object $pAdmEnv -- structure with common and environment information
1120  * @param array $pAdmVars -- the passed parameters for the operation
1121  *
1122  * @return object $returnInfo - structure with data and error info.
1123  */
1124 function MakeACHBatch( $pAdmEnv, $pAdmVars ) {
1125  $returnInfo = array("code" => "000", "errors" => array(), "data" => array());
1126 
1127  try {
1128  // must check the cu routing number first
1129  // if this doesn't exist, do not create the batch
1130  $adminAchSettings = AdminReadSettings($pAdmEnv, $pAdmEnv["dbh"], $pAdmEnv["Cu"]);
1131 
1132  $cuRouting = trim( $adminAchSettings["settings"]["routing"] );
1133  if ( strlen( $cuRouting ) != 9 ) {
1134  throw new Exception( "ACH Settings - invalid CU routing number", 300 );
1135  }
1136 
1137  // first, make sure ids are all valid
1138  $transIdList = $pAdmVars["trans_list"];
1139  $idList = array();
1140  $inputList = explode( ",", $transIdList );
1141  for ( $i = 0; $i < count( $inputList ); $i++ ) {
1142  if ( ctype_digit( $inputList[$i] ) ) {
1143  $idList[] = $inputList[$i];
1144  }
1145  }
1146 
1147  $idString = implode( ",", $idList );
1148 
1149  if ( strlen( $idString ) > 0 ) {
1150  $tz = GetCreditUnionTimezone( $pAdmEnv["dbh"], $pAdmEnv["Cu"] );
1151 
1152  // get a UTC timestamp
1153  $sql = "SELECT now() AT TIME ZONE 'UTC'";
1154  $rs = db_query( $sql, $pAdmEnv["dbh"] );
1155  if ($rs) {
1156  list( $processedStamp ) = db_fetch_array( $rs, 0 );
1157  } else {
1158  throw new Exception( "ACH Query Error - unable to get timestamp" );
1159  }
1160 
1161  $sql = "UPDATE {$pAdmEnv["Cu"]}transhdr SET processed_by = '{$pAdmEnv["Cn"]}',
1162  processed_date = '$processedStamp',
1163  processed_status = 10
1164  WHERE id in ($idString) ";
1165  $rs = db_query( $sql, $pAdmEnv["dbh"] );
1166  if ( !$rs ) {
1167  throw new Exception( "ACH Query Error - recording ACH upload file" );
1168  }
1169 
1170  // return the batch identifier (timestamp)
1171  $payload = array( "batch_date" => $processedStamp );
1172  $encodedBatchId = HCU_PayloadEncode( $pAdmEnv["Cu"], $payload );
1173  $returnInfo["data"]["batch_id"] = $encodedBatchId;
1174 
1175  // convert to readable time
1176  $myDateTime = new DateTime($processedStamp);
1177  $myDateTime->setTimezone(new DateTimeZone($tz));
1178  $formatted = $myDateTime->format("m/d/y H:i:s T");
1179 
1180  $returnInfo["data"]["batch_time"] = $formatted;
1181 
1182  } else {
1183  throw new Exception( "Error Building ACH Upload File - invalid transaction list" );
1184  }
1185  } catch (Exception $ex) {
1186  $logInfo = array( "error" => $ex->getMessage(), "code" => $ex->getCode() );
1187 
1188  $returnInfo["errors"][] = $ex->getMessage();
1189  // 300 = cu routing number invalid
1190  if ($ex->getCode() == 300) {
1191  $returnInfo["errors"][] = "This feature requires a valid routing number on the <a href=\"main.prg?ft=54\">Settings</a> screen.";
1192  }
1193  $returnInfo["code"] = "999";
1194  }
1195 
1196  return $returnInfo;
1197 } // end MakeACHBatch
1198 
1199 /**
1200  * GetCompletedBatches
1201  * Get a list of timestamps for completed ACH batches.
1202  *
1203  * @param object $pAdmEnv -- structure with common and environment information
1204  *
1205  * @return
1206  */
1207 function GetCompletedBatches( $pAdmEnv ) {
1208  $returnInfo = array("code" => "000", "errors" => array(), "data" => array());
1209 
1210  try {
1211  $cu = strtolower( $pAdmEnv["Cu"] );
1212 
1213  $batchPreName = "Batch";
1214  $batchPath = "/home/{$cu}/admin/ach/";
1215 
1216  // verify the directory exists
1217  if ( !file_exists( $batchPath ) ) {
1218  throw new Exception( "Get Completed ACH Files Error - Directory does not exist - please contact Support" );
1219  }
1220 
1221  // get the credit union's timezone
1222  $tz = GetCreditUnionTimezone( $pAdmEnv["dbh"], $pAdmEnv["Cu"] );
1223 
1224  // get all the processed batches - processed_date is the identifier for the batch
1225  $sql = "
1226  SELECT
1227  th.processed_date, th.processed_by,
1228  COUNT(DISTINCT th.id) AS count,
1229  SUM(CASE WHEN (th.feature_code = 'ACHPMT' OR th.feature_code = 'ACHCOL') AND td.email_notify = 1 THEN 1 ELSE 0 END) AS notify
1230  FROM {$cu}transhdr AS th
1231  JOIN {$cu}transdtl AS td ON th.id = td.transhdr_id
1232  WHERE th.processed_date IS NOT NULL
1233  AND th.processed_status >= 10
1234  AND th.processed_status <= 30
1235  AND th.feature_code IN ('TRNEXT', 'ACHCOL', 'ACHPMT')
1236  GROUP BY processed_date, processed_by
1237  ORDER BY processed_date DESC";
1238  $rs = db_query( $sql, $pAdmEnv["dbh"] );
1239  if ( !$rs ) {
1240  throw new Exception( "ACH Query Error - getting approved ach txns" );
1241  }
1242 
1243  // cycle through the rows
1244  $row = 0;
1245  $returnData = array();
1246  while ( $txRow = db_fetch_assoc( $rs, $row++ ) ) {
1247  // figure out the file name
1248  $myDateTime = new DateTime( $txRow["processed_date"] );
1249 
1250  // convert to local time for the filename
1251  $myDateTime->setTimezone(new DateTimeZone($tz));
1252  $batchDay = $myDateTime->format("ymd");
1253  $batchHour = $myDateTime->format("His");
1254 
1255  $baseFileName = "{$batchPreName}_{$batchDay}_{$batchHour}";
1256 
1257  // convert to readable time
1258  $formatted = $myDateTime->format( "m/d/y H:i:s T ");
1259 
1260  $payload = array( "batch_date" => $txRow["processed_date"] );
1261  $encodedBatchId = HCU_PayloadEncode( $pAdmEnv["Cu"], $payload );
1262 
1263  $returnData[] = array( "batch_id" => $encodedBatchId,
1264  "processed_by" => $txRow["processed_by"],
1265  "count" => $txRow["count"],
1266  "notify" => $txRow["notify"],
1267  "display" => $formatted,
1268  "has_ach_file" => file_exists( "{$batchPath}{$baseFileName}.ach" ),
1269  "has_ach_report" => file_exists( "{$batchPath}{$baseFileName}.txt" )
1270  );
1271  }
1272 
1273  $returnInfo["data"] = $returnData;
1274  } catch (Exception $ex) {
1275  $logInfo = array( "error" => $ex->getMessage(), "code" => $ex->getCode() );
1276 
1277  $returnInfo["errors"] = $ex->getMessage();
1278  $returnInfo["code"] = "999";
1279  }
1280 
1281  return $returnInfo;
1282 } // end GetCompletedBatches
1283 
1284 /**
1285  * GetBatchDetail
1286  * Get all the transactions in the given batch.
1287  *
1288  * @param object $pAdmEnv -- structure with common and environment information
1289  * @param array $pAdmVars -- the passed parameters for the operation
1290  *
1291  * @return data or error
1292  */
1293 function GetBatchDetail( $pAdmEnv, $pAdmVars ) {
1294  $returnInfo = array("code" => "000", "errors" => array(), "data" => array());
1295 
1296  try {
1297  $payloadInfo = HCU_PayloadDecode( $pAdmEnv["Cu"], $pAdmVars["batch_id"] );
1298  $batchTimestamp = $payloadInfo["batch_date"];
1299 
1300  // Not allowed to remove transactions after certain time
1301  // from the creation of the batch. A boolean value will be
1302  // sent back to the client with the batch detail records: line 1304.
1303  $removeTransactionsTime = time() - $pAdmEnv['removeFromBatchTime'] * 24 * 60 * 60;
1304  $removeBatchTime = strtotime($payloadInfo["batch_date"]);
1305 
1306  // gotta get them all - Left joins in case user is gone
1307  $sql = "SELECT id, feature_code, effective_date, u1.user_name AS poster, posted_date,
1308  u2.user_name AS approver, approved_date, accountnumber,
1309  transactioncode, memo, group_name, (select sum(amount) from {$pAdmEnv["Cu"]}transdtl d where d.transhdr_id = th.id) as amount
1310  FROM {$pAdmEnv["Cu"]}transhdr th
1311  LEFT JOIN {$pAdmEnv["Cu"]}user u1 ON u1.user_id = th.posted_by
1312  LEFT JOIN {$pAdmEnv["Cu"]}user u2 ON u2.user_id = th.approved_by
1313  INNER JOIN {$pAdmEnv["Cu"]}group g ON g.group_id = u1.group_id
1314  WHERE processed_date = '$batchTimestamp'
1315  AND processed_status >= 10
1316  ORDER BY group_name, feature_code, id";
1317  $rs = db_query( $sql, $pAdmEnv["dbh"] );
1318  if ( !$rs ) {
1319  throw new Exception( "ACH Query Error - getting approved ach txns" );
1320  }
1321 
1322  // cycle through the rows
1323  $row = 0;
1324  $returnData = array();
1325  while ( $txRow = db_fetch_assoc( $rs, $row++ ) ) {
1326  // do any data processing
1327  $timestamp = strtotime( $txRow["effective_date"] );
1328  $account = trim( $txRow["accountnumber"] );
1329  $returnData[] = array( "id" => $txRow["id"],
1330  "feature" => trim( $txRow["feature_code"] ),
1331  "eff_date" => date( "m/d/Y", $timestamp ),
1332  "group" => $txRow["group_name"],
1333  "account" => $account,
1334  "amount" => $txRow["amount"],
1335  "memo" => $txRow["memo"]
1336  );
1337  }
1338 
1339  $returnInfo["data"]["records"] = $returnData;
1340  $returnInfo["data"]["removeFromBatchAllowed"] = !($removeBatchTime < $removeTransactionsTime);
1341  } catch (Exception $ex) {
1342  $logInfo = array( "error" => $ex->getMessage(), "code" => $ex->getCode() );
1343 
1344  $returnInfo["errors"] = $ex->getMessage();
1345  $returnInfo["code"] = "999";
1346  }
1347 
1348  return $returnInfo;
1349 } // end GetBatchDetail
1350 
1351 /**
1352  * DownloadAchFile
1353  * Download the given ach file. It could be an ACH file or ACH report.
1354  *
1355  * @param object $pAdmEnv -- structure with common and environment information
1356  * @param array $pAdmVars -- the passed parameters for the operation
1357  *
1358  * @return data or error
1359  */
1360 function DownloadAchFile( $pAdmEnv, $pAdmVars ) {
1361  $returnInfo = array("code" => "000", "errors" => array(), "data" => array());
1362 
1363  try {
1364  $cu = strtolower( $pAdmEnv["Cu"] );
1365 
1366  $payloadInfo = HCU_PayloadDecode( $pAdmEnv["Cu"], $pAdmVars["batch_id"] );
1367  $batchTimeString = $payloadInfo["batch_date"];
1368 
1369  // get the credit union's timezone
1370  $tz = GetCreditUnionTimezone( $pAdmEnv["dbh"], $pAdmEnv["Cu"] );
1371 
1372  // use the batch timestamp to figure out the creation date/time and filename
1373  $myDateTime = new DateTime( $batchTimeString );
1374  $myDateTime->setTimezone(new DateTimeZone($tz));
1375  $batchDay = $myDateTime->format("ymd");
1376  $batchHour = $myDateTime->format("His");
1377 
1378  // use the batch timestamp to figure out the creation date/time and filename
1379  $batchPreName = "Batch";
1380  $batchPath = "/home/{$cu}/admin/ach/";
1381 
1382  $fileType = ( $pAdmVars["download_type"] === "ach" ) ? "ach" : "txt";
1383  $achFileName = "{$batchPreName}_{$batchDay}_{$batchHour}.{$fileType}";
1384 
1385  // get the file
1386  $fileString = file_get_contents( "{$batchPath}{$achFileName}" );
1387  if ( $fileString === false ) {
1388  throw new Exception( "Error Building ACH Upload File - ACH upload file not found" );
1389  }
1390 
1391  $returnInfo["data"]["filename"] = $achFileName;
1392  $returnInfo["data"]["output"] = $fileString;
1393  } catch (Exception $ex) {
1394  $logInfo = array( "error" => $ex->getMessage(), "code" => $ex->getCode() );
1395 
1396  $returnInfo["errors"] = $ex->getMessage();
1397  $returnInfo["code"] = "999";
1398  }
1399 
1400  return $returnInfo;
1401 } // end DownloadAchFile
1402 
1403 // Helper function to get all the ach-related transaction headers.
1404 function GetACHTransactionHeaders( $pAdmEnv, $pBatchTimeString ) {
1405 
1406  $retInfo = array();
1407 
1408  // get all the group/company info
1409  $sql = "SELECT 0 as processed, group_name, tax_id, th.*
1410  FROM {$pAdmEnv["Cu"]}transhdr th
1411  LEFT JOIN {$pAdmEnv["Cu"]}user u1 ON u1.user_id = th.posted_by
1412  INNER JOIN {$pAdmEnv["Cu"]}group g ON g.group_id = u1.group_id
1413  WHERE processed_date = '$pBatchTimeString'
1414  AND processed_status >= 10
1415  AND processed_status <= 30
1416  ORDER BY group_name";
1417  $hdrRS = db_query( $sql, $pAdmEnv["dbh"] );
1418  if ( $hdrRS ) {
1419  $hdrRow = 0;
1420  while ( $rowHdr = db_fetch_assoc( $hdrRS, $hdrRow++ ) ) {
1421  $retInfo[] = $rowHdr;
1422  }
1423  }
1424 
1425  return $retInfo;
1426 } // GetACHTransactionHeaders
1427 
1428 /**
1429  * AddMicroDeposits
1430  * Helper function to create the micro-deposits entry. If there are any, a single "Company"
1431  * gets created with the offsetting internal entry and all the micro-deposits to external
1432  * accounts. The transactioncode of "1P" will be used for the TRNEXT microdeposits.
1433  *
1434  * @param object $pAdmEnv -- structure with common and environment information
1435  * @param array &$pTransactionHeaders -- all the headers from the <cu>transhdr table.
1436  * @param array $pInputParams -- values used for constructing the micro deposit ach records.
1437  * @param array $pAchValues -- values for the ach records needing to be returned updated.
1438  * @param array $pOutputData -- the output of this call (assumed to be initialized before calling this function).
1439  * @param array $pReportLines -- lines of report information.
1440  *
1441  * @return success or error
1442  */
1443 function AddMicroDeposits( $pAdmEnv, &$pTransactionHeaders, $pInputParams, &$pAchValues, &$pOutputData, &$pReportLines ) {
1444  /**
1445  * Service Class Code:
1446  *
1447  * 220: Credit to remote acocunt
1448  * 225: Debit to local account
1449  */
1450  $returnInfo = array( "code" => "000", "error" => "" );
1451 
1452  try {
1453  // see if there are any micro deposits
1454  $microDepositList = array();
1455  for ( $i = 0; $i < count( $pTransactionHeaders ); $i++ ) {
1456  if ( $pTransactionHeaders[$i]["feature_code"] == "TRNEXT" &&
1457  $pTransactionHeaders[$i]["transactioncode"] == "1P" ) {
1458  $microDepositList[] = $pTransactionHeaders[$i];
1459  }
1460  }
1461 
1462  $microList = array();
1463  for ( $i = 0; $i < count( $microDepositList ); $i++ ) {
1464  // get the detail for each micro deposit
1465  $sql = "SELECT * FROM {$pAdmEnv["Cu"]}transdtl WHERE transhdr_id = {$microDepositList[$i]["id"]}";
1466  $dtlRS = db_query( $sql, $pAdmEnv["dbh"] );
1467  if ( !$dtlRS ) {
1468  throw new Exception( "ACH Query Error - getting microdeposit ach txns" );
1469  }
1470 
1471  $microRow = 0;
1472  while ( $microDtl = db_fetch_assoc( $dtlRS, $microRow++ ) ) {
1473  $microList[] = $microDtl;
1474  }
1475  }
1476 
1477  $companyEntryAddendaCount = 0;
1478  if ( count( $microList ) > 0 ) {
1479  // we are making a batch entry
1480  $pAchValues["batchNumber"]++;
1481 
1482  $entriesList = array();
1483 
1484  $companyID = $pInputParams['CompanyIDType'] . $pInputParams["OriginatingRouting"];
1485 
1486  // create a Company header
1487  $companyHeader = array( "RecordTypeCode" => "5",
1488  "ServiceClassCode" => "220",
1489  "CompanyName" => MakeValidAchString( $pInputParams["CompanyName"], 16 ),
1490  "CompanyData" => str_repeat( " ", 20 ),
1491  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
1492  "StandardEntryClassCode" => "WEB",
1493  "CompanyEntryDescription" => MakeValidAchString( $pInputParams["CompanyEntryDescription"], 10 ),
1494  "CompanyDescriptiveDate" => str_repeat( " ", 6 ),
1495  "EffectiveEntryDate" => MakeValidAchString( $pInputParams["EffectiveEntryDate"], 6 ),
1496  "SettlementDate" => str_repeat( " ", 3 ),
1497  "OriginatorStatusCode" => "1",
1498  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
1499  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
1500  $entriesList[] = $companyHeader;
1501 
1502  $totalMicroAmount = 0;
1503  $companyControlHash = 0;
1504  for ( $i = 0; $i < count( $microList ); $i++ ) {
1505  // create an Entry for each micro deposit
1506 
1507  // decode the transaction data
1508  $transData = HCU_JsonDecode( $microList[$i]["transdata"], true );
1509 
1510  // set up information in the correct format; the routing number used is the first 8 digits, the 9th is the check digit
1511  if ( strlen( $transData["acct_dest"]["rdfi"]["rdfi_routing"] ) != 9 ) {
1512  throw new Exception( "ACH File Creation - invalid routing number (location 111)" );
1513  }
1514 
1515  $receivingDFIRouting = substr( $transData["acct_dest"]["rdfi"]["rdfi_routing"], 0, 8 );
1516  $receivingCheckDigit = substr( $transData["acct_dest"]["rdfi"]["rdfi_routing"], -1 );
1517  $account = str_replace( " ", "", $transData["acct_dest"]["rdfi"]["rdfi_account"] );
1518  $receivingDFIAccount = MakeValidAchString( $account, 17 );
1519  $receivingAccountType = $transData["acct_dest"]["rdfi"]["rdfi_account_type"];
1520  $receivingTxnType = $transData["acct_dest"]["rdfi"]["rdfi_txn_type"];
1521 
1522  $actualAmount = trim( $microList[$i]["amount"] );
1523  $totalMicroAmount += $actualAmount;
1524 
1525  $roundAmount = round( $actualAmount * 100 );
1526  $amount = str_pad( $roundAmount, 10, "0", STR_PAD_LEFT );
1527 
1528  // just use the CU routing number here
1529  $individualID = MakeValidAchString( $pInputParams["OriginatingRouting"], 15 );
1530 
1531  $individualName = MakeValidAchString( $transData["acct_dest"]["remote_entity"]["name"], 22 );
1532 
1533  // 10 = checking, 20 = savings
1534  $transactionCode = $receivingAccountType == 10 ? "22" : ( $receivingAccountType == 20 ? "32" : "00" );
1535 
1536  if ( $transactionCode == "00" ) {
1537  throw new Exception( "Error Building ACH Upload File (micro deposit) - could not determine transaction code" );
1538  }
1539 
1540  $discretionaryData = str_pad("S", 2, " ", STR_PAD_RIGHT);
1541 
1542  // an entry detail for each
1543  $pAchValues["traceNumber"]++;
1544  $pAchValues["fileEntryAddendaCount"]++;
1545  $companyEntryAddendaCount++;
1546  $entryDetail = array ( "RecordTypeCode" => "6",
1547  "TransactionCode" => $transactionCode,
1548  "ReceivingDFI" => $receivingDFIRouting,
1549  "CheckDigit" => $receivingCheckDigit,
1550  "DFIAccountNumber" => $receivingDFIAccount,
1551  "Amount" => $amount,
1552  "IndividualIdentificationNumber" => $individualID,
1553  "IndividualName" => strtoupper( $individualName ),
1554  "DiscretionaryData" => $discretionaryData,
1555  "AddendaRecordIndicator" => "0",
1556  "TraceNumber" => substr( $pInputParams["OriginatingRouting"], 0, 8 ) . substr( "000000" . $pAchValues["traceNumber"], -7 ) );
1557 
1558  $entriesList[] = $entryDetail;
1559 
1560  // add to the control hashes
1561  $companyControlHash += $receivingDFIRouting;
1562  $pAchValues["fileControlHash"] += $receivingDFIRouting;
1563 
1564  $formattedAmount = number_format( $actualAmount, 2, ".", "," );
1565  $pReportLines[] = " Micro deposit credit of $formattedAmount to " . str_replace(' ', '', "$receivingDFIRouting$receivingCheckDigit") . " / " . trim( $receivingDFIAccount );
1566  }
1567 
1568  // Add Control record for WEB transactions
1569  // create a Company control record // the company hash can only be 10 characters long
1570  $entryHash = substr( str_repeat( "0", 10 ) . $companyControlHash, -10 );
1571 
1572  // Get final micr amount formatted for control records
1573  $finalMicroAmount = round( $totalMicroAmount * 100 );
1574 
1575  // output the company control record
1576  $companyControl = array ( "RecordTypeCode" => "8",
1577  "ServiceClassCode" => "220",
1578  "EntryAddendaCount" => str_pad( $companyEntryAddendaCount, 6, "0", STR_PAD_LEFT ),
1579  "EntryHash" => $entryHash,
1580  "TotalDebitEntryDollarAmount" => str_pad( 0, 12, "0", STR_PAD_LEFT ),
1581  "TotalCreditEntryDollarAmount" => str_pad( $finalMicroAmount, 12, "0", STR_PAD_LEFT ),
1582  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
1583  "MessageAuthenticationCode" => str_repeat( " ", 19 ),
1584  "Reserved" => str_repeat( " ", 6 ),
1585  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
1586  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
1587 
1588  $entriesList[] = $companyControl;
1589  $pAchValues["fileBatchCount"]++;
1590 
1591  // New header type 5 record for the GL account
1592  $pAchValues["batchNumber"]++;
1593  $companyHeader = array( "RecordTypeCode" => "5",
1594  "ServiceClassCode" => "225",
1595  "CompanyName" => MakeValidAchString( $pInputParams["CompanyName"], 16 ),
1596  "CompanyData" => str_repeat( " ", 20 ),
1597  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
1598  "StandardEntryClassCode" => "CCD",
1599  "CompanyEntryDescription" => MakeValidAchString( $pInputParams["CompanyEntryDescription"], 10 ),
1600  "CompanyDescriptiveDate" => str_repeat( " ", 6 ),
1601  "EffectiveEntryDate" => MakeValidAchString( $pInputParams["EffectiveEntryDate"], 6 ),
1602  "SettlementDate" => str_repeat( " ", 3 ),
1603  "OriginatorStatusCode" => "1",
1604  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
1605  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
1606  $entriesList[] = $companyHeader;
1607 
1608  $companyControlHash = 0;
1609 
1610  // create an offsetting Entry from the special fund
1611  $receivingDFIRouting = substr( $pInputParams["OriginatingRouting"], 0, 8 );
1612  $receivingCheckDigit = substr( $pInputParams["OriginatingRouting"], -1 );
1613 
1614  $individualID = MakeValidAchString( $pInputParams["OriginatingRouting"], 15 );
1615  $individualName = MakeValidAchString( $pInputParams["CompanyName"], 22 );
1616 
1617  $pAchValues["traceNumber"]++;
1618  $pAchValues["fileEntryAddendaCount"]++;
1619  $entryDetail = array ( "RecordTypeCode" => "6",
1620  "TransactionCode" => "47", // assuming debiting GL account!
1621  "ReceivingDFI" => $receivingDFIRouting,
1622  "CheckDigit" => $receivingCheckDigit,
1623  "DFIAccountNumber" => MakeValidAchString( $pInputParams["accountMicroDeposit"], 17 ),
1624  "Amount" => str_pad( $finalMicroAmount, 10, "0", STR_PAD_LEFT ),
1625  "IndividualIdentificationNumber" => $individualID,
1626  "IndividualName" => strtoupper( $individualName ),
1627  "DiscretionaryData" => " ",
1628  "AddendaRecordIndicator" => "0",
1629  "TraceNumber" => substr( $pInputParams["OriginatingRouting"], 0, 8 ) . substr( "000000" . $pAchValues["traceNumber"], -7 ) );
1630  $entriesList[] = $entryDetail;
1631 
1632  $formattedAmount = number_format( $totalMicroAmount, 2, ".", "," );
1633  $pReportLines[] = " Total from CU account: $formattedAmount from " . trim( $pInputParams["OriginatingRouting"] ) . " / " . trim( $pInputParams["accountMicroDeposit"] );
1634 
1635  // add to the control hashes
1636  $companyControlHash += $receivingDFIRouting;
1637  $pAchValues["fileControlHash"] += $receivingDFIRouting;
1638 
1639  // create a Company control record // the company hash can only be 10 characters long
1640  $entryHash = substr( str_repeat( "0", 10 ) . $companyControlHash, -10 );
1641 
1642  // output the company control record
1643  $companyEntryAddendaCount = 1;
1644  $companyControl = array ( "RecordTypeCode" => "8",
1645  "ServiceClassCode" => "225",
1646  "EntryAddendaCount" => str_pad( $companyEntryAddendaCount, 6, "0", STR_PAD_LEFT ),
1647  "EntryHash" => $entryHash,
1648  "TotalDebitEntryDollarAmount" => str_pad( $finalMicroAmount, 12, "0", STR_PAD_LEFT ),
1649  "TotalCreditEntryDollarAmount" => str_pad( 0, 12, "0", STR_PAD_LEFT ),
1650  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
1651  "MessageAuthenticationCode" => str_repeat( " ", 19 ),
1652  "Reserved" => str_repeat( " ", 6 ),
1653  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
1654  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
1655 
1656  $entriesList[] = $companyControl;
1657  $pAchValues["fileBatchCount"]++;
1658 
1659  // micro deposits are always credits and debits
1660  $pAchValues["totalFileCredit"] += $finalMicroAmount;
1661  $pAchValues["totalFileDebit"] += $finalMicroAmount;
1662 
1663  // make the output records
1664  for ( $i = 0; $i < count( $entriesList ); $i++ ) {
1665  $item = array_values( $entriesList[$i] );
1666 
1667  $outputLine = implode( "", $item );
1668  if ( strlen( $outputLine ) != 94 ) {
1669  throw new Exception( "ACH Output Error - micro deposit block size" );
1670  }
1671 
1672  $pOutputData[] = $outputLine;
1673  }
1674 
1675  // mark all the micro deposit record headers as handled
1676  for ( $i = 0; $i < count( $pTransactionHeaders ); $i++ ) {
1677  if ( $pTransactionHeaders[$i]["feature_code"] == "TRNEXT" &&
1678  $pTransactionHeaders[$i]["transactioncode"] == "1P" ) {
1679  $pTransactionHeaders[$i]["processed"] = true;
1680  }
1681  }
1682 
1683  } else {
1684  $pReportLines[] = " None";
1685  }
1686 
1687  } catch (Exception $ex) {
1688  $logInfo = array( "error" => $ex->getMessage(), "code" => $ex->getCode() );
1689 
1690  $returnInfo["error"] = $ex->getMessage();
1691  $returnInfo["code"] = "999";
1692  }
1693 
1694  return $returnInfo;
1695 } // end AddMicroDeposits
1696 
1697 /**
1698  * AddMicroDepositOffsets
1699  * Helper function to create the micro-deposits offsetting entry. If there are any, a single "Company"
1700  * gets created with the offsetting internal entry and all the micro deposit offset debits to external
1701  * accounts. The transactioncode of "2P" will be used for the TRNEXT offsetting microdeposit (NOTE: not all
1702  * sites will be configured to have an offsetting microdeposit).
1703  *
1704  * @param object $pAdmEnv -- structure with common and environment information
1705  * @param array &$pTransactionHeaders -- all the headers from the <cu>transhdr table.
1706  * @param array $pInputParams -- values used for constructing the micro deposit ach records.
1707  * @param array $pAchValues -- values for the ach records needing to be returned updated.
1708  * @param array $pOutputData -- the output of this call (assumed to be initialized before calling this function).
1709  * @param array $pReportLines -- lines of report information.
1710  *
1711  * @return success or error
1712  */
1713 function AddMicroDepositOffsets( $pAdmEnv, &$pTransactionHeaders, $pInputParams, &$pAchValues, &$pOutputData, &$pReportLines ) {
1714  /**
1715  * Service Class Code:
1716  *
1717  * 220: Credit to local acocunt
1718  * 225: Debit to remote account
1719  */
1720  $returnInfo = array( "code" => "000", "error" => "" );
1721 
1722  try {
1723  // see if there are any micro deposits
1724  $microDepositList = array();
1725  for ( $i = 0; $i < count( $pTransactionHeaders ); $i++ ) {
1726  if ( $pTransactionHeaders[$i]["feature_code"] == "TRNEXT" &&
1727  $pTransactionHeaders[$i]["transactioncode"] == "2P" ) {
1728  $microDepositList[] = $pTransactionHeaders[$i];
1729  }
1730  }
1731 
1732  $microList = array();
1733  for ( $i = 0; $i < count( $microDepositList ); $i++ ) {
1734  // get the detail for each micro deposit
1735  $sql = "SELECT * FROM {$pAdmEnv["Cu"]}transdtl WHERE transhdr_id = {$microDepositList[$i]["id"]}";
1736  $dtlRS = db_query( $sql, $pAdmEnv["dbh"] );
1737  if ( !$dtlRS ) {
1738  throw new Exception( "ACH Query Error - getting microdeposit offset ach txns" );
1739  }
1740 
1741  // should just be one each!
1742  $microDtl = db_fetch_assoc( $dtlRS, 0 );
1743  $microList[] = $microDtl;
1744  }
1745 
1746  $companyEntryAddendaCount = 0;
1747  if ( count( $microList ) > 0 ) {
1748  // we are making a batch entry
1749  $pAchValues["batchNumber"]++;
1750 
1751  $entriesList = array();
1752 
1753  $companyID = $pInputParams['CompanyIDType'] . $pInputParams["OriginatingRouting"];
1754 
1755  // create a Company header
1756  $companyHeader = array( "RecordTypeCode" => "5",
1757  "ServiceClassCode" => "225",
1758  "CompanyName" => MakeValidAchString( $pInputParams["CompanyName"], 16 ),
1759  "CompanyData" => str_repeat( " ", 20 ),
1760  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
1761  "StandardEntryClassCode" => "WEB",
1762  "CompanyEntryDescription" => MakeValidAchString( $pInputParams["CompanyEntryDescription"], 10 ),
1763  "CompanyDescriptiveDate" => str_repeat( " ", 6 ),
1764  "EffectiveEntryDate" => MakeValidAchString( $pInputParams["EffectiveEntryDate"], 6 ),
1765  "SettlementDate" => str_repeat( " ", 3 ),
1766  "OriginatorStatusCode" => "1",
1767  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
1768  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
1769  $entriesList[] = $companyHeader;
1770 
1771  $totalMicroAmount = 0;
1772  $companyControlHash = 0;
1773  for ( $i = 0; $i < count( $microList ); $i++ ) {
1774  // create an Entry for each micro deposit
1775 
1776  // decode the transaction data
1777  $transData = HCU_JsonDecode( $microList[$i]["transdata"], true );
1778 
1779  // set up information in the correct format
1780  if ( strlen( $transData["acct_source"]["rdfi"]["rdfi_routing"] ) != 9 ) {
1781  throw new Exception( "ACH File Creation - invalid routing number (location 112)" );
1782  }
1783 
1784  $receivingDFIRouting = substr( $transData["acct_source"]["rdfi"]["rdfi_routing"], 0, 8 );
1785  $receivingCheckDigit = substr( $transData["acct_source"]["rdfi"]["rdfi_routing"], -1 );
1786  $account = str_replace( " ", "", $transData["acct_source"]["rdfi"]["rdfi_account"] );
1787  $receivingDFIAccount = MakeValidAchString( $account, 17 );
1788  $receivingAccountType = $transData["acct_source"]["rdfi"]["rdfi_account_type"];
1789  $receivingTxnType = $transData["acct_source"]["rdfi"]["rdfi_txn_type"];
1790 
1791  $actualAmount = trim( $microList[$i]["amount"] );
1792  $totalMicroAmount += $actualAmount;
1793 
1794  $roundAmount = round( $actualAmount * 100 );
1795  $amount = str_pad( $roundAmount, 10, "0", STR_PAD_LEFT );
1796 
1797  // just use the CU routing number here
1798  $individualID = MakeValidAchString( $pInputParams["OriginatingRouting"], 15 );
1799 
1800  $individualName = MakeValidAchString( $transData["acct_source"]["remote_entity"]["name"], 22 );
1801 
1802  // 10 = checking, 20 = savings
1803  $transactionCode = $receivingAccountType == 10 ? "27" : ( $receivingAccountType == 20 ? "37" : "00" );
1804 
1805  if ( $transactionCode == "00" ) {
1806  throw new Exception( "ACH Batch Build (micro deposit offset) - could not determine transaction code" );
1807  }
1808 
1809  $discretionaryData = str_pad("S", 2, " ", STR_PAD_RIGHT);
1810 
1811  // an entry detail for each
1812  $pAchValues["traceNumber"]++;
1813  $pAchValues["fileEntryAddendaCount"]++;
1814  $companyEntryAddendaCount++;
1815  $entryDetail = array ( "RecordTypeCode" => "6",
1816  "TransactionCode" => $transactionCode,
1817  "ReceivingDFI" => $receivingDFIRouting,
1818  "CheckDigit" => $receivingCheckDigit,
1819  "DFIAccountNumber" => $receivingDFIAccount,
1820  "Amount" => $amount,
1821  "IndividualIdentificationNumber" => $individualID,
1822  "IndividualName" => strtoupper( $individualName ),
1823  "DiscretionaryData" => $discretionaryData,
1824  "AddendaRecordIndicator" => "0",
1825  "TraceNumber" => substr( $pInputParams["OriginatingRouting"], 0, 8 ) . substr( "000000" . $pAchValues["traceNumber"], -7 ) );
1826  $entriesList[] = $entryDetail;
1827 
1828  // add to the control hashes
1829  $companyControlHash += $receivingDFIRouting;
1830  $pAchValues["fileControlHash"] += $receivingDFIRouting;
1831 
1832  $formattedAmount = number_format( $actualAmount, 2, ".", "," );
1833  $pReportLines[] = " Micro deposit debit of $formattedAmount to " . str_replace(' ', '', "$receivingDFIRouting$receivingCheckDigit") . " / " . trim( $receivingDFIAccount );
1834  }
1835 
1836  // Add Control record for WEB transactions
1837  // create a Company control record // the company hash can only be 10 characters long
1838  $entryHash = substr( str_repeat( "0", 10 ) . $companyControlHash, -10 );
1839 
1840  // Get final micr amount formatted for control records
1841  $finalMicroAmount = round( $totalMicroAmount * 100 );
1842 
1843  // output the company control record
1844  $companyControl = array ( "RecordTypeCode" => "8",
1845  "ServiceClassCode" => "225",
1846  "EntryAddendaCount" => str_pad( $companyEntryAddendaCount, 6, "0", STR_PAD_LEFT ),
1847  "EntryHash" => $entryHash,
1848  "TotalDebitEntryDollarAmount" => str_pad( $finalMicroAmount, 12, "0", STR_PAD_LEFT ),
1849  "TotalCreditEntryDollarAmount" => str_pad( 0, 12, "0", STR_PAD_LEFT ),
1850  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
1851  "MessageAuthenticationCode" => str_repeat( " ", 19 ),
1852  "Reserved" => str_repeat( " ", 6 ),
1853  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
1854  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
1855 
1856  $entriesList[] = $companyControl;
1857  $pAchValues["fileBatchCount"]++;
1858 
1859  // New header type 5 record for the GL account
1860  $pAchValues["batchNumber"]++;
1861  $companyHeader = array( "RecordTypeCode" => "5",
1862  "ServiceClassCode" => "220",
1863  "CompanyName" => MakeValidAchString( $pInputParams["CompanyName"], 16 ),
1864  "CompanyData" => str_repeat( " ", 20 ),
1865  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
1866  "StandardEntryClassCode" => "CCD",
1867  "CompanyEntryDescription" => MakeValidAchString( $pInputParams["CompanyEntryDescription"], 10 ),
1868  "CompanyDescriptiveDate" => str_repeat( " ", 6 ),
1869  "EffectiveEntryDate" => MakeValidAchString( $pInputParams["EffectiveEntryDate"], 6 ),
1870  "SettlementDate" => str_repeat( " ", 3 ),
1871  "OriginatorStatusCode" => "1",
1872  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
1873  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
1874  $entriesList[] = $companyHeader;
1875 
1876  $companyControlHash = 0;
1877 
1878  // create an offsetting Entry from the special fund
1879  $receivingDFIRouting = substr( $pInputParams["OriginatingRouting"], 0, 8 );
1880  $receivingCheckDigit = substr( $pInputParams["OriginatingRouting"], -1 );
1881 
1882  $individualID = MakeValidAchString( $pInputParams["OriginatingRouting"], 15 );
1883  $individualName = MakeValidAchString( $pInputParams["CompanyName"], 22 );
1884 
1885  $finalMicroAmount = round( $totalMicroAmount * 100 );
1886 
1887  $pAchValues["traceNumber"]++;
1888  $pAchValues["fileEntryAddendaCount"]++;
1889  $entryDetail = array ( "RecordTypeCode" => "6",
1890  "TransactionCode" => "42", // assuming crediting GL account!
1891  "ReceivingDFI" => $receivingDFIRouting,
1892  "CheckDigit" => $receivingCheckDigit,
1893  "DFIAccountNumber" => MakeValidAchString( $pInputParams["accountMicroDeposit"], 17 ),
1894  "Amount" => str_pad( $finalMicroAmount, 10, "0", STR_PAD_LEFT ),
1895  "IndividualIdentificationNumber" => $individualID,
1896  "IndividualName" => strtoupper( $individualName ),
1897  "DiscretionaryData" => " ",
1898  "AddendaRecordIndicator" => "0",
1899  "TraceNumber" => substr( $pInputParams["OriginatingRouting"], 0, 8 ) . substr( "000000" . $pAchValues["traceNumber"], -7 ) );
1900  $entriesList[] = $entryDetail;
1901 
1902  $formattedAmount = number_format( $totalMicroAmount, 2, ".", "," );
1903  $pReportLines[] = " Total into CU account: $formattedAmount to " . trim( $pInputParams["OriginatingRouting"] ) . " / " . trim( $pInputParams["accountMicroDeposit"] );
1904 
1905  // add to the control hashes
1906  $companyControlHash += $receivingDFIRouting;
1907  $pAchValues["fileControlHash"] += $receivingDFIRouting;
1908 
1909  // create a Company control record // the company hash can only be 10 characters long
1910  $entryHash = substr( str_repeat( "0", 10 ) . $companyControlHash, -10 );
1911 
1912  $companyEntryAddendaCount = 1;
1913  // output the company control record
1914  $companyControl = array ( "RecordTypeCode" => "8",
1915  "ServiceClassCode" => "220",
1916  "EntryAddendaCount" => str_pad( $companyEntryAddendaCount, 6, "0", STR_PAD_LEFT ),
1917  "EntryHash" => $entryHash,
1918  "TotalDebitEntryDollarAmount" => str_pad( 0, 12, "0", STR_PAD_LEFT ),
1919  "TotalCreditEntryDollarAmount" => str_pad( $finalMicroAmount, 12, "0", STR_PAD_LEFT ),
1920  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
1921  "MessageAuthenticationCode" => str_repeat( " ", 19 ),
1922  "Reserved" => str_repeat( " ", 6 ),
1923  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
1924  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
1925 
1926  $entriesList[] = $companyControl;
1927 
1928  // update any return information (values)
1929  $pAchValues["fileBatchCount"]++;
1930 
1931  // micro deposits are always credits and debits
1932  $pAchValues["totalFileCredit"] += $finalMicroAmount;
1933  $pAchValues["totalFileDebit"] += $finalMicroAmount;
1934 
1935  // make the output records
1936  for ( $i = 0; $i < count( $entriesList ); $i++ ) {
1937  $item = array_values( $entriesList[$i] );
1938 
1939  $outputLine = implode( "", $item );
1940  if ( strlen( $outputLine ) != 94 ) {
1941  throw new Exception( "ACH Output Error - micro deposit block size" );
1942  }
1943 
1944  $pOutputData[] = $outputLine;
1945  }
1946 
1947  // mark all the micro deposit record headers as handled
1948  for ( $i = 0; $i < count( $pTransactionHeaders ); $i++ ) {
1949  if ( $pTransactionHeaders[$i]["feature_code"] == "TRNEXT" &&
1950  $pTransactionHeaders[$i]["transactioncode"] == "2P" ) {
1951  $pTransactionHeaders[$i]["processed"] = true;
1952  }
1953  }
1954 
1955  } else {
1956  $pReportLines[] = " None";
1957  }
1958 
1959  } catch (Exception $ex) {
1960  $logInfo = array( "error" => $ex->getMessage(), "code" => $ex->getCode() );
1961 
1962  $returnInfo["error"] = $ex->getMessage();
1963  $returnInfo["code"] = "999";
1964  }
1965 
1966  return $returnInfo;
1967 } // end AddMicroDepositOffsets
1968 
1969 /**
1970  * BuildACHTransfer
1971  * Creates entries in the ACH output file for handling ACH transfers (from the Transfer feature). This
1972  * will create a "WEB" StandardEntryClassCode for both the debit and credit transfers or a "WEB" for
1973  * the external account and a "PPD" if the local account is a loan payment.
1974  * NOTE: regular transfers won't utilize any addenda records.
1975  *
1976  * @param object $pAdmEnv -- structure with common and environment information
1977  * @param array $pTxnHeader -- all the headers from the <cu>transhdr table.
1978  * @param array $pInputParams -- values used for constructing the ach records.
1979  * @param array $pAchValues -- values for the ach records needing to be returned updated.
1980  * @param array $pOutputData -- the output of this call (assumed to be initialized before calling this function).
1981  * @param array $pReportLines -- lines of report information.
1982  *
1983  * @return success or error
1984  */
1985 function BuildACHTransfer( $pAdmEnv, $pTxnHeader, $pInputParams, &$pAchValues, &$pOutputData, &$pReportLines ) {
1986  $returnInfo = array( "code" => "000", "error" => "" );
1987 
1988  try {
1989  // ensure the date entered is a business date
1990  $effDate = ACH_EnsureBusinessDay( $pAdmEnv, $pTxnHeader["effective_date"] );
1991 
1992  // avoid daylight savings time change issues.
1993  $effectiveDate = date( "ymd", strtotime( $effDate . " 3:00" ) );
1994 
1995  $adminAchSettings = AdminReadSettings($pAdmEnv, $pAdmEnv["dbh"], $pAdmEnv["Cu"]);
1996 
1997  $transferDirection = substr( $pTxnHeader["transactioncode"], 0, 1 );
1998 
1999  // get all the detail for each company
2000  // NOTE: For regular ACH Transfers there should only be one detail.
2001  $sql = "SELECT * FROM {$pAdmEnv["Cu"]}transdtl WHERE transhdr_id = {$pTxnHeader["id"]}";
2002  $dtlRS = db_query( $sql, $pAdmEnv["dbh"] );
2003  if ( !$dtlRS ) {
2004  throw new Exception( "ACH Query Error - getting batch ach txns" );
2005  }
2006 
2007  $detailRows = array();
2008  $dtlRow = 0;
2009  while ( $rowDtl = db_fetch_assoc( $dtlRS, $dtlRow++ ) ) {
2010  // decode the transaction data
2011  $transData = HCU_JsonDecode( $rowDtl["transdata"], true );
2012 
2013  if ( empty( $transData ) ) {
2014  throw new Exception( "ACH Error - transaction data is missing or corrupt" );
2015  }
2016 
2017  $rowDtl["acct_source"] = $transData["acct_source"];
2018  $rowDtl["acct_dest"] = $transData["acct_dest"];
2019 
2020  $detailRows[] = $rowDtl;
2021  }
2022 
2023  if ( count( $detailRows ) != 1 ) {
2024  throw new Exception( "ACH Error - incorrect detail for ACH transfer. Need one detail row, found: " . count( $detailRows ) );
2025  }
2026 
2027  // make the access simpler
2028  $transferRow = $detailRows[0];
2029 
2030  // get the CompanyEntryDescription / IndividualName
2031  $entryName = trim( $pTxnHeader["memo"] );
2032  if ( strlen( $entryName ) == 0 ) {
2033  $entryName = "Online Funds Transfer";
2034  }
2035 
2036  $companyID = $pInputParams['CompanyIDType'] . $pInputParams["OriginatingRouting"];
2037 
2038  // test if special case of payment to a loan (this variable is also used later)
2039  $paymentSource = $transferRow["acct_source"];
2040  $paymentDestination = $transferRow["acct_dest"];
2041  if ( false ) {
2042  // this is the case of a loan payment; make two separate transfers of one entry each
2043 
2044  // first entry is a "WEB" debit from the remote account
2045 
2046  // build a company header
2047  $serviceClassCode = "225";
2048  $pAchValues["batchNumber"]++;
2049 
2050  $companyHeader = array( "RecordTypeCode" => "5",
2051  "ServiceClassCode" => $serviceClassCode,
2052  "CompanyName" => MakeValidAchString( $pInputParams["CompanyName"], 16 ),
2053  "CompanyData" => str_repeat( " ", 20 ),
2054  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
2055  "StandardEntryClassCode" => "WEB",
2056  "CompanyEntryDescription" => MakeValidAchString( $entryName, 10 ),
2057  "CompanyDescriptiveDate" => str_repeat( " ", 6 ),
2058  "EffectiveEntryDate" => $effectiveDate,
2059  "SettlementDate" => str_repeat( " ", 3 ),
2060  "OriginatorStatusCode" => "1",
2061  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
2062  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
2063 
2064  // add to the output
2065  $header = array_values( $companyHeader );
2066 
2067  $outputLine = implode( "", $header );
2068  if ( strlen( $outputLine ) != 94 ) {
2069  throw new Exception( "ACH Output Error - company header block size (location 101)" );
2070  }
2071  $pOutputData[] = $outputLine;
2072 
2073  if ( strlen( $transData["acct_source"]["rdfi"]["rdfi_routing"] ) != 9 ) {
2074  throw new Exception( "ACH File Creation - invalid routing number (location 112)" );
2075  }
2076 
2077  $receivingDFIRouting = substr( $paymentSource["rdfi"]["rdfi_routing"], 0, 8 );
2078  $receivingCheckDigit = substr( $paymentSource["rdfi"]["rdfi_routing"], -1 );
2079  $account = str_replace( " ", "", $paymentSource["rdfi"]["rdfi_account"] );
2080  $receivingDFIAccount = MakeValidAchString( $account, 17 );
2081 
2082  $printableSourceAccount = "{$paymentSource["rdfi"]["rdfi_routing"]} / $account";
2083 
2084  $roundAmount = round( $transferRow["amount"] * 100 );
2085  $amount = str_pad( $roundAmount, 10, "0", STR_PAD_LEFT );
2086 
2087  // use the end of the name because last name is more important
2088  $fullName = $paymentSource["remote_entity"]["name"];
2089  $individualID = MakeValidAchString( substr( $fullName, -15 ), 15 );
2090 
2091  $individualName = MakeValidAchString( $entryName, 22 );
2092 
2093  // add to the total debit
2094  $pAchValues["totalFileDebit"] += $roundAmount;
2095 
2096  // an entry detail remote debit
2097  $pAchValues["traceNumber"]++;
2098  $pAchValues["fileEntryAddendaCount"]++;
2099 
2100  $entryDetail = array ( "RecordTypeCode" => "6",
2101  "TransactionCode" => "27",
2102  "ReceivingDFI" => $receivingDFIRouting,
2103  "CheckDigit" => $receivingCheckDigit,
2104  "DFIAccountNumber" => $receivingDFIAccount,
2105  "Amount" => $amount,
2106  "IndividualIdentificationNumber" => $individualID,
2107  "IndividualName" => $individualName,
2108  "DiscretionaryData" => " ",
2109  "AddendaRecordIndicator" => "0",
2110  "TraceNumber" => substr( $pInputParams["OriginatingRouting"], 0, 8 ) . substr( "000000" . $pAchValues["traceNumber"], -7 ) );
2111 
2112  $pAchValues["fileControlHash"] += $receivingDFIRouting;
2113 
2114  // add to the output
2115  $entry = array_values( $entryDetail );
2116 
2117  $outputLine = implode( "", $entry );
2118  if ( strlen( $outputLine ) != 94 ) {
2119  throw new Exception( "ACH Output Error - company header block size (location 104)" );
2120  }
2121  $pOutputData[] = $outputLine;
2122 
2123  // the company hash can only be 10 characters long
2124  $entryHash = substr( str_repeat( "0", 10 ) . $receivingDFIRouting, -10 );
2125 
2126  // the control record for this first company/entry
2127  $companyControl = array ( "RecordTypeCode" => "8",
2128  "ServiceClassCode" => $serviceClassCode,
2129  "EntryAddendaCount" => str_pad( "1", 6, "0", STR_PAD_LEFT ),
2130  "EntryHash" => $entryHash,
2131  "TotalDebitEntryDollarAmount" => str_pad( $amount, 12, "0", STR_PAD_LEFT ),
2132  "TotalCreditEntryDollarAmount" => str_pad( "0", 12, "0", STR_PAD_LEFT ),
2133  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
2134  "MessageAuthenticationCode" => str_repeat( " ", 19 ),
2135  "Reserved" => str_repeat( " ", 6 ),
2136  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
2137  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
2138 
2139  // add to the output
2140  $control = array_values( $companyControl );
2141 
2142  $outputLine = implode( "", $control );
2143  if ( strlen( $outputLine ) != 94 ) {
2144  throw new Exception( "ACH Output Error - company control block size (location 108)" );
2145  }
2146  $pOutputData[] = $outputLine;
2147 
2148  // the 2nd company/entry is a PPD payment to the loan at this CU
2149 
2150  // build a company header
2151  $serviceClassCode = "220";
2152  $pAchValues["batchNumber"]++;
2153 
2154  $companyHeader = array( "RecordTypeCode" => "5",
2155  "ServiceClassCode" => $serviceClassCode,
2156  "CompanyName" => MakeValidAchString( $pInputParams["CompanyName"], 16 ),
2157  "CompanyData" => str_repeat( " ", 20 ),
2158  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
2159  "StandardEntryClassCode" => "PPD",
2160  "CompanyEntryDescription" => MakeValidAchString( $entryName, 10 ),
2161  "CompanyDescriptiveDate" => str_repeat( " ", 6 ),
2162  "EffectiveEntryDate" => $effectiveDate,
2163  "SettlementDate" => str_repeat( " ", 3 ),
2164  "OriginatorStatusCode" => "1",
2165  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
2166  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
2167 
2168  // add to the output
2169  $header = array_values( $companyHeader );
2170 
2171  $outputLine = implode( "", $header );
2172  if ( strlen( $outputLine ) != 94 ) {
2173  throw new Exception( "ACH Output Error - company header block size (location 102)" );
2174  }
2175  $pOutputData[] = $outputLine;
2176 
2177  if ( strlen( $pInputParams["OriginatingRouting"] ) != 9 ) {
2178  throw new Exception( "ACH File Creation - invalid routing number (location 113)" );
2179  }
2180 
2181  $receivingDFIRouting = substr( $pInputParams["OriginatingRouting"], 0, 8 );
2182  $receivingCheckDigit = substr( $pInputParams["OriginatingRouting"], -1 );
2183  $destParts = explode( "|", $paymentDestination );
2184  $account = str_replace( " ", "", $destParts[1] );
2185  $subAccount = str_replace( " ", "", $destParts[2] );
2186  $receivingDFIAccount = MakeValidAchString( $account . $subAccount, 17 );
2187 
2188  $printableDestAccount = "$account-$subAccount";
2189 
2190  $actualAmount = trim( $transferRow["amount"] );
2191  $roundAmount = round( $actualAmount * 100 );
2192  $amount = str_pad( $roundAmount, 10, "0", STR_PAD_LEFT );
2193 
2194  // use the individual name and number from earlier
2195 
2196  // add to the total credit
2197  $pAchValues["totalFileCredit"] += $roundAmount;
2198 
2199  // an entry detail remote debit
2200  $pAchValues["traceNumber"]++;
2201  $pAchValues["fileEntryAddendaCount"]++;
2202 
2203  $entryDetail = array ( "RecordTypeCode" => "6",
2204  "TransactionCode" => "52",
2205  "ReceivingDFI" => $receivingDFIRouting,
2206  "CheckDigit" => $receivingCheckDigit,
2207  "DFIAccountNumber" => $receivingDFIAccount,
2208  "Amount" => $amount,
2209  "IndividualIdentificationNumber" => $individualID,
2210  "IndividualName" => $individualName,
2211  "DiscretionaryData" => " ",
2212  "AddendaRecordIndicator" => "0",
2213  "TraceNumber" => substr( $pInputParams["OriginatingRouting"], 0, 8 ) . substr( "000000" . $pAchValues["traceNumber"], -7 ) );
2214 
2215  $pAchValues["fileControlHash"] += $receivingDFIRouting;
2216 
2217  // add to the output
2218  $entry = array_values( $entryDetail );
2219 
2220  $outputLine = implode( "", $entry );
2221  if ( strlen( $outputLine ) != 94 ) {
2222  throw new Exception( "ACH Output Error - entry detail block size (location 105)" );
2223  }
2224  $pOutputData[] = $outputLine;
2225 
2226  // the company hash can only be 10 characters long
2227  $entryHash = substr( str_repeat( "0", 10 ) . $receivingDFIRouting, -10 );
2228 
2229  // the control record for this first company/entry
2230  $companyControl = array ( "RecordTypeCode" => "8",
2231  "ServiceClassCode" => $serviceClassCode,
2232  "EntryAddendaCount" => str_pad( "1", 6, "0", STR_PAD_LEFT ),
2233  "EntryHash" => $entryHash,
2234  "TotalDebitEntryDollarAmount" => str_pad( "0", 12, "0", STR_PAD_LEFT ),
2235  "TotalCreditEntryDollarAmount" => str_pad( $amount, 12, "0", STR_PAD_LEFT ),
2236  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
2237  "MessageAuthenticationCode" => str_repeat( " ", 19 ),
2238  "Reserved" => str_repeat( " ", 6 ),
2239  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
2240  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
2241 
2242  // add to the output
2243  $control = array_values( $companyControl );
2244 
2245  $outputLine = implode( "", $control );
2246  if ( strlen( $outputLine ) != 94 ) {
2247  throw new Exception( "ACH Output Error - company control block size (location 109)" );
2248  }
2249  $pOutputData[] = $outputLine;
2250 
2251  // add to the file batch count these two entries
2252  $pAchValues["fileBatchCount"] += 2;
2253 
2254  // add info to the report for this company
2255  $formattedAmount = number_format( $actualAmount, 2, ".", "," );
2256  $pReportLines[] = " Transfer: $fullName Automated loan payment $formattedAmount from $printableSourceAccount to $printableDestAccount effective $effectiveDate";
2257  } else {
2258  // this is the normal case where we have a single WEB with one each source and destination entries
2259  $serviceClassCode = "200"; // mixed credit and debit
2260 
2261  // build a company header
2262  $pAchValues["batchNumber"]++;
2263  $companyControlHash = 0;
2264 
2265  $companyHeader = array( "RecordTypeCode" => "5",
2266  "ServiceClassCode" => $serviceClassCode,
2267  "CompanyName" => MakeValidAchString( $pInputParams["CompanyName"], 16 ),
2268  "CompanyData" => str_repeat( " ", 20 ),
2269  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
2270  "StandardEntryClassCode" => "WEB",
2271  "CompanyEntryDescription" => MakeValidAchString( $entryName, 10 ),
2272  "CompanyDescriptiveDate" => str_repeat( " ", 6 ),
2273  "EffectiveEntryDate" => $effectiveDate,
2274  "SettlementDate" => str_repeat( " ", 3 ),
2275  "OriginatorStatusCode" => "1",
2276  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
2277  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
2278 
2279  // add to the output
2280  $header = array_values( $companyHeader );
2281 
2282  $outputLine = implode( "", $header );
2283  if ( strlen( $outputLine ) != 94 ) {
2284  throw new Exception( "ACH Output Error - company header block size (location 103)" );
2285  }
2286  $pOutputData[] = $outputLine;
2287 
2288  // the routing and account numbers depend on the transfer direction
2289  if ( $transferDirection == 1 ) {
2290  // Local to Remote
2291  if ( strlen( $pInputParams["OriginatingRouting"] ) != 9 ) {
2292  throw new Exception( "ACH File Creation - invalid routing number (location 114)" );
2293  }
2294 
2295  $sourceDFIRouting = substr( $pInputParams["OriginatingRouting"], 0, 8 );
2296  $sourceCheckDigit = substr( $pInputParams["OriginatingRouting"], -1 );
2297 
2298  // $sourceParts is used later
2299  $sourceParts = explode( "|", $paymentSource );
2300  $account = str_replace( " ", "", $sourceParts[1] );
2301  $subAccount = str_replace( " ", "", $sourceParts[2] );
2302  $sourceDFIAccount = FormatDFIAccount($account, $subAccount, $adminAchSettings["settings"]["sub_account_mask"]);
2303 
2304  $printableSourceAccount = "$account-$subAccount";
2305 
2306  if ( strlen( $transData["acct_dest"]["rdfi"]["rdfi_routing"] ) != 9 ) {
2307  throw new Exception( "ACH File Creation - invalid routing number (location 115)" );
2308  }
2309 
2310  $destDFIRouting = substr( $paymentDestination["rdfi"]["rdfi_routing"], 0, 8 );
2311  $destCheckDigit = substr( $paymentDestination["rdfi"]["rdfi_routing"], -1 );
2312  $account = str_replace( " ", "", $paymentDestination["rdfi"]["rdfi_account"] );
2313  $destDFIAccount = MakeValidAchString( $account, 17 );
2314 
2315  $printableDestAccount = "{$paymentDestination["rdfi"]["rdfi_routing"]} / $account";
2316 
2317  // use the end of the name because last name is more important
2318  $fullName = $paymentDestination["remote_entity"]["name"];
2319  $individualID = MakeValidAchString( substr( $fullName, -15 ), 15 );
2320 
2321  // need to test if local account is a checking or savings
2322  // NOTE: source account could be a loan (e.g. line of credit)
2323  if ( $sourceParts[0] == "L" ) {
2324  // this is not allowed
2325  throw new Exception( "ACH File Creation - cannot debit a loan account: $paymentSource" );
2326  }
2327 
2328  // see if there is source (local) meta data
2329  $localACHType = $transData["source_meta"]["deposit_ach_type"];
2330  if ( $localACHType > 0 ) {
2331  $isChecking = $localACHType == 10;
2332  } else {
2333  $sql = "SELECT deposittype
2334  FROM {$pAdmEnv["Cu"]}accountbalance
2335  WHERE accountnumber = '{$sourceParts[1]}'
2336  AND accounttype = '{$sourceParts[2]}'
2337  AND certnumber = {$sourceParts[3]}";
2338  $acctRS = db_query( $sql, $pAdmEnv["dbh"] );
2339  if ( !$acctRS ) {
2340  throw new Exception( "ACH Query Error - getting account deposit type" );
2341  }
2342 
2343  $acctRow = db_fetch_assoc( $acctRS, 0 );
2344  // only "Y" is checking
2345  $isChecking = $acctRow["deposittype"] == "Y";
2346  }
2347 
2348  $sourceTransactionCode = $isChecking ? "27" : "37";
2349  $destTransactionCode = $paymentDestination["rdfi"]["rdfi_account_type"] == 10 ? "22" : "32";
2350  } else {
2351  // Remote to Local
2352  if ( strlen( $transData["acct_source"]["rdfi"]["rdfi_routing"] ) != 9 ) {
2353  throw new Exception( "ACH File Creation - invalid routing number (location 116)" );
2354  }
2355 
2356  $sourceDFIRouting = substr( $paymentSource["rdfi"]["rdfi_routing"], 0, 8 );
2357  $sourceCheckDigit = substr( $paymentSource["rdfi"]["rdfi_routing"], -1 );
2358  $account = str_replace( " ", "", $paymentSource["rdfi"]["rdfi_account"] );
2359  $sourceDFIAccount = MakeValidAchString( $account, 17 );
2360 
2361  $printableSourceAccount = "{$paymentSource["rdfi"]["rdfi_routing"]} / $account";
2362 
2363  if ( strlen( $pInputParams["OriginatingRouting"] ) != 9 ) {
2364  throw new Exception( "ACH File Creation - invalid routing number (location 117)" );
2365  }
2366 
2367  $destDFIRouting = substr( $pInputParams["OriginatingRouting"], 0, 8 );
2368  $destCheckDigit = substr( $pInputParams["OriginatingRouting"], -1 );
2369 
2370  // $destParts is used later
2371  $destParts = explode( "|", $paymentDestination );
2372  $account = str_replace( " ", "", $destParts[1] );
2373  $subAccount = str_replace( " ", "", $destParts[2] );
2374  $destDFIAccount = FormatDFIAccount($account, $subAccount, $adminAchSettings["settings"]["sub_account_mask"]);
2375 
2376 
2377  $printableDestAccount = "$account-$subAccount";
2378 
2379  // use the end of the name because last name is more important
2380  $fullName = $paymentSource["remote_entity"]["name"];
2381  $individualID = MakeValidAchString( substr( $fullName, -15 ), 15 );
2382 
2383  $sourceTransactionCode = $paymentSource["rdfi"]["rdfi_account_type"] == 10 ? "27" : "37";
2384 
2385 
2386  // see if there is destination (local) meta data
2387  $localACHType = $transData["dest_meta"]["deposit_ach_type"];
2388  if ( $localACHType > 0 ) {
2389  $isChecking = $localACHType == 10;
2390  } else if( $destParts[0] != "L" ) {
2391  // need to test if local account is a checking or savings
2392  $sql = "SELECT deposittype
2393  FROM {$pAdmEnv["Cu"]}accountbalance
2394  WHERE accountnumber = '{$destParts[1]}'
2395  AND accounttype = '{$destParts[2]}'
2396  AND certnumber = {$destParts[3]}";
2397  $acctRS = db_query( $sql, $pAdmEnv["dbh"] );
2398  if ( !$acctRS ) {
2399  throw new Exception( "ACH Query Error - getting batch ach txns" );
2400  }
2401 
2402  $acctRow = db_fetch_assoc( $acctRS, 0 );
2403  // only "Y" is checking
2404  $isChecking = $acctRow["deposittype"] == "Y";
2405  } else {
2406  // just to make sure we have a value
2407  $isChecking = false;
2408  }
2409 
2410  // for loans, use code 52
2411  if( $destParts[0] == "L" ) {
2412  $destTransactionCode = "52";
2413  } else {
2414  $destTransactionCode = $isChecking ? "22" : "32";
2415  }
2416  }
2417 
2418  $actualAmount = trim( $transferRow["amount"] );
2419  $roundAmount = round( $actualAmount * 100 );
2420  $amount = str_pad( $roundAmount, 10, "0", STR_PAD_LEFT );
2421 
2422  $individualName = MakeValidAchString( $entryName, 22 );
2423  // Set Discretionary Data as "S" for Single non-recurring transaction
2424  // and "R" for recurring transaction.
2425  $transMeta = HCU_JsonDecode($pTxnHeader['transmeta']);
2426  $transRecurring = (HCU_array_key_value("recurr", $transMeta) == "yes") ? "R" : "S";
2427  $discretionaryData = str_pad($transRecurring, 2, " ", STR_PAD_RIGHT);
2428 
2429  // add to the total credit and debit
2430  $pAchValues["totalFileCredit"] += $roundAmount;
2431  $pAchValues["totalFileDebit"] += $roundAmount;
2432 
2433  // an entry detail source
2434  $pAchValues["traceNumber"]++;
2435  $pAchValues["fileEntryAddendaCount"]++;
2436 
2437  $entryDetail = array ( "RecordTypeCode" => "6",
2438  "TransactionCode" => $sourceTransactionCode,
2439  "ReceivingDFI" => $sourceDFIRouting,
2440  "CheckDigit" => $sourceCheckDigit,
2441  "DFIAccountNumber" => $sourceDFIAccount,
2442  "Amount" => $amount,
2443  "IndividualIdentificationNumber" => $individualID,
2444  "IndividualName" => $individualName,
2445  "DiscretionaryData" => $discretionaryData,
2446  "AddendaRecordIndicator" => "0",
2447  "TraceNumber" => substr( $pInputParams["OriginatingRouting"], 0, 8 ) . substr( "000000" . $pAchValues["traceNumber"], -7 ) );
2448 
2449  // add to the output
2450  $entry = array_values( $entryDetail );
2451 
2452  $outputLine = implode( "", $entry );
2453  if ( strlen( $outputLine ) != 94 ) {
2454  throw new Exception( "ACH Output Error - entry detail block size (location 106)" );
2455  }
2456  $pOutputData[] = $outputLine;
2457 
2458  $companyControlHash += $sourceDFIRouting;
2459  $pAchValues["fileControlHash"] += $sourceDFIRouting;
2460 
2461  // an entry detail destination
2462  $pAchValues["traceNumber"]++;
2463  $pAchValues["fileEntryAddendaCount"]++;
2464 
2465  $entryDetail = array ( "RecordTypeCode" => "6",
2466  "TransactionCode" => $destTransactionCode,
2467  "ReceivingDFI" => $destDFIRouting,
2468  "CheckDigit" => $destCheckDigit,
2469  "DFIAccountNumber" => $destDFIAccount,
2470  "Amount" => $amount,
2471  "IndividualIdentificationNumber" => $individualID,
2472  "IndividualName" => $individualName,
2473  "DiscretionaryData" => $discretionaryData,
2474  "AddendaRecordIndicator" => "0",
2475  "TraceNumber" => substr( $pInputParams["OriginatingRouting"], 0, 8 ) . substr( "000000" . $pAchValues["traceNumber"], -7 ) );
2476 
2477  // add to the output
2478  $entry = array_values( $entryDetail );
2479 
2480  $outputLine = implode( "", $entry );
2481  if ( strlen( $outputLine ) != 94 ) {
2482  throw new Exception( "ACH Output Error - entry detail block size (location 107)" );
2483  }
2484  $pOutputData[] = $outputLine;
2485 
2486  $companyControlHash += $destDFIRouting;
2487  $pAchValues["fileControlHash"] += $destDFIRouting;
2488 
2489  // the company hash can only be 10 characters long
2490  $entryHash = substr( str_repeat( "0", 10 ) . $companyControlHash, -10 );
2491 
2492  // the control record for this first company/entry
2493  $companyControl = array ( "RecordTypeCode" => "8",
2494  "ServiceClassCode" => $serviceClassCode,
2495  "EntryAddendaCount" => str_pad( "2", 6, "0", STR_PAD_LEFT ),
2496  "EntryHash" => $entryHash,
2497  "TotalDebitEntryDollarAmount" => str_pad( $amount, 12, "0", STR_PAD_LEFT ),
2498  "TotalCreditEntryDollarAmount" => str_pad( $amount, 12, "0", STR_PAD_LEFT ),
2499  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
2500  "MessageAuthenticationCode" => str_repeat( " ", 19 ),
2501  "Reserved" => str_repeat( " ", 6 ),
2502  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
2503  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
2504 
2505  // add to the output
2506  $control = array_values( $companyControl );
2507 
2508  $outputLine = implode( "", $control );
2509  if ( strlen( $outputLine ) != 94 ) {
2510  throw new Exception( "ACH Output Error - company control block size (location 110)" );
2511  }
2512  $pOutputData[] = $outputLine;
2513 
2514  // add to the file batch count this one entry
2515  $pAchValues["fileBatchCount"]++;
2516 
2517  // add info to the report for this company
2518  $formattedAmount = number_format( $actualAmount, 2, ".", "," );
2519 
2520  if ( $destTransactionCode == "52" ) {
2521  $pReportLines[] = " Transfer: $fullName Automated loan payment $formattedAmount from $printableSourceAccount to $printableDestAccount effective $effectiveDate";
2522  } else {
2523  $pReportLines[] = " Transfer: $fullName WEB transfer $formattedAmount from $printableSourceAccount to $printableDestAccount effective $effectiveDate";
2524  }
2525  }
2526 
2527  } catch (Exception $ex) {
2528  $logInfo = array( "error" => $ex->getMessage(), "code" => $ex->getCode() );
2529 
2530  $returnInfo["error"] = $ex->getMessage();
2531  $returnInfo["code"] = "999";
2532  }
2533 
2534  return $returnInfo;
2535 } // end BuildACHTransfer
2536 
2537 /**
2538  * BuildACHBusiness
2539  * Creates entries in the ACH output file for handling "Commercial" ACHCOL and ACHPMT features.
2540  *
2541  * @param object $pAdmEnv -- structure with common and environment information
2542  * @param array $pTxnHeader -- all the headers from the <cu>transhdr table.
2543  * @param array $pInputParams -- values used for constructing the ach records.
2544  * @param array $pAchValues -- values for the ach records needing to be returned updated.
2545  * @param array $pOutputData -- the output of this call (assumed to be initialized before calling this function).
2546  * @param array $pReportLines -- lines of report information.
2547  *
2548  * @return success or error
2549  */
2550 function BuildACHBusiness( $pAdmEnv, $pTxnHeader, $pInputParams, &$pAchValues, &$pOutputData, &$pReportLines ) {
2551  $returnInfo = array( "code" => "000", "error" => "" );
2552 
2553  try {
2554  $pAchValues["batchNumber"]++;
2555 
2556  // avoid daylight savings time change issues.
2557  $effectiveDate = date( "ymd", strtotime( $pTxnHeader["effective_date"] . " 3:00" ) );
2558  $adminAchSettings = AdminReadSettings($pAdmEnv, $pAdmEnv["dbh"], $pAdmEnv["Cu"]);
2559 
2560  // The service class code is used in the company header/control records, types (5/8) to
2561  // determine the type of transactions included in the company record.
2562  // 200: type 6 records consist of both credit and debit records
2563  // 220: type 6 records consist of only credit records
2564  // 225: type 6 records consist of only debt records
2565 
2566  // The service class code is hard-coded to 200 at this time due to the fact that we are
2567  // including a single local type 6 record and the one or more remote type 6 records within
2568  // a single company header and control record.
2569  //
2570  // Example: ACH Collection: $30.87
2571  // company header record, service class code: 200
2572  // remote record(s): 30.87 (debit from remote)
2573  // local record: 30.87 (credit to local)
2574  // company control record, service class code: 200; credit amount: 30.87, debit amount: 30.87
2575 
2576  // It is possible in the future we may need to generate separate header/control records for
2577  // the remote type 6 records and the local type 6 records. This would be the case for specfying
2578  // the service class codes of 220 and 225.
2579  //
2580  // Example: ACH Collection $30.87
2581  // company header record, service class code: 225 (debit from remote)
2582  // remote record(s): 30.87
2583  // company control record, service class code: 225; credit amount: 0, debit amount: 30.87
2584  //
2585  // company header record, service class code: 220 (credit to local)
2586  // local record: 30.87
2587  // company control record, service class code: 220; credit amount: 30.87, debit amount: 0
2588  $serviceClassCode = "200";
2589 
2590  // Transaction direction:
2591  // 1: Local To Remote -> ACH Payment
2592  // 2: Remote to Local -> ACH Collection
2593  $transferDirection = substr( $pTxnHeader["transactioncode"], 0, 1 );
2594 
2595  // P -> PPD, C -> CCD, W -> WEB
2596  $standardEntryClass = substr( $pTxnHeader["transactioncode"], 1, 1 );
2597  if ( $standardEntryClass == "C" ) {
2598  $secCode = "CCD";
2599  } else if ( $standardEntryClass == "P" ) {
2600  $secCode = "PPD";
2601  } else if ( $standardEntryClass == "W" ) {
2602  $secCode = "WEB";
2603  } else {
2604  throw new Exception( "Error Building ACH Upload File - Could not determine Standard Entry Class (201)" );
2605  }
2606 
2607  // for now using the CU's ACH name;
2608  $companyName = $pTxnHeader["group_name"];
2609 
2610  // Identifies the Originator
2611  $companyID = trim($pTxnHeader['tax_id']);
2612  if (strlen($companyID) != 10) {
2613  // since the group hub link is only displayed on error
2614  // when the tax id is missing, the encoding should be
2615  // inside the error block.
2616  $groupContext = GroupContext($pAdmEnv, $pAdmEnv['Cu']);
2617  $groupContext['g_name'] = $companyName;
2618  $group = GroupSelect($pAdmEnv, $pAdmEnv['dbh'], $groupContext);
2619 
2620  // must decode all primary users for the group or the hub will complain
2621  for ($i = 0; $i < count($group['group']['g_primary']); $i++) {
2622  $payload = urldecode($group['group']['g_primary'][$i]['p_payload']);
2623  $primary = HCU_PayloadDecode($pAdmEnv['Cu'], $payload);
2624 
2625  $group['group']['g_primary'][$i] = $primary;
2626  $group['group']['g_primary'][$i]['p_name'] = $primary['user_name'];
2627  }
2628  $groupEncrypt = GroupEncrypt($pAdmEnv, $pAdmEnv['Cu'], $group);
2629  $groupEncrypt = urlencode($groupEncrypt);
2630 
2631  // because we can only throw a single string, encode it.
2632  // KEEP THE '^', This will be used to split the string to prevent the use of
2633  // json encode and decode over and over again.
2634  $companyIDError = "Error Building ACH Upload File - Missing Company Identification: 109^This feature requires a \"Company Tax ID\" set up on the <a href=\"main.prg?ft=102101&payload={$groupEncrypt}\">{$companyName}</a> Group Hub";
2635  throw new Exception( $companyIDError, 109 );
2636  }
2637 
2638  $companyICD = substr($companyID, 0, -9);
2639  if ($companyICD != "1" && $companyICD != "9" && $companyICD != " ") {
2640  throw new Exception( "Error Building ACH Upload File - Invalid Identification Code Designator (202)" );
2641  }
2642 
2643  // company entry description is the ACH Transaction type
2644  // depending on the transaction code above which can be
2645  // 1P/2P (PPD) or 1C/2C (CCD) -> 1 (L2R), 2(R2L)
2646  $companyEntryDescription = $transferDirection == "1" ? "ACH Payment" : "ACH Collection";
2647 
2648  // build a company header
2649  $companyHeader = array( "RecordTypeCode" => "5",
2650  "ServiceClassCode" => $serviceClassCode,
2651  "CompanyName" => MakeValidAchString( $companyName, 16 ),
2652  "CompanyData" => str_repeat( " ", 20 ),
2653  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
2654  "StandardEntryClassCode" => $secCode,
2655  "CompanyEntryDescription" => MakeValidAchString( $companyEntryDescription, 10 ),
2656  "CompanyDescriptiveDate" => str_repeat( " ", 6 ),
2657  "EffectiveEntryDate" => $effectiveDate,
2658  "SettlementDate" => str_repeat( " ", 3 ),
2659  "OriginatorStatusCode" => "1",
2660  "OriginatingDFI" => MakeValidAchString( substr( $pInputParams["OriginatingRouting"], 0, 8 ), 8 ),
2661  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
2662 
2663  $header = array_values( $companyHeader );
2664 
2665  $outputLine = implode( "", $header );
2666  if ( strlen( $outputLine ) != 94 ) {
2667  throw new Exception( "ACH Output Error - company header block size (203)" );
2668  }
2669  $pOutputData[] = $outputLine;
2670 
2671  // add info to the report for this company
2672  $pReportLines[] = " Group/Company: $companyName";
2673 
2674  // get all the detail for each company
2675  $sql = "SELECT * FROM {$pAdmEnv["Cu"]}transdtl WHERE transhdr_id = {$pTxnHeader["id"]}";
2676  $dtlRS = db_query( $sql, $pAdmEnv["dbh"] );
2677  if ( !$dtlRS ) {
2678  throw new Exception( "ACH Query Error - getting ACH transactions (204)" );
2679  }
2680 
2681  if (db_num_rows($dtlRS) == 0) {
2682  throw new Exception( "ACH Error - detail not found for ACH transfer (205)" );
2683  }
2684 
2685  // Local account information.
2686  // Initialize this above the main loop because we only
2687  // need it set one time.
2688  //
2689  // This will allow all remote records of a transaction
2690  // to print first, then print a single record for local
2691  // information.
2692  $localAccountInfo = array();
2693  $localAccountInfoSet = false;
2694  $localAccountTotal = 0;
2695  $localAccountTotalFormatted = "";
2696 
2697  // To make the report file more intuitive to the credit union
2698  // we need to print the local information first. So we set up
2699  // a local report lines array, fill it up with the remote records
2700  // then append it after we print the local information line.
2701  $remoteReportLines = array();
2702 
2703  $dtlRow = 0;
2704  $companyTotalCredit = 0;
2705  $companyTotalDebit = 0;
2706  $companyControlHash = 0;
2707  $companyEntryAddendaCount = 0;
2708  while ( $rowDtl = db_fetch_assoc( $dtlRS, $dtlRow++ ) ) {
2709  // Initialize remote data array
2710  $remoteAccountInfo = array();
2711 
2712  // decode the transaction data
2713  $transData = HCU_JsonDecode( $rowDtl["transdata"], true );
2714 
2715  if (empty($transData)) {
2716  throw new Exception( "ACH Error - transaction data is missing or corrupt (206)" );
2717  }
2718 
2719  // Check for valid routing number
2720  if ( strlen( $pInputParams["OriginatingRouting"] ) != 9 ) {
2721  throw new Exception( "Error Building ACH Upload File - invalid routing number (207)" );
2722  }
2723 
2724  // Only need to set the local account information one time
2725  // since it is the same for every single transdtl record of
2726  // a batch transaction.
2727  if (!$localAccountInfoSet) {
2728 
2729  $localParts = array();
2730  // Local to Remote: ACH Payment
2731  // Local information will be in source_* fields of trans_data
2732  if ($transferDirection == "1") {
2733  $localParts = $transData['source_key'];
2734  $localType = $transData['source_meta']['deposit_ach_type'];
2735  $localAccountInfo['TransactionCode'] = $localType == 10 ? "27" : ($localType == 20 ? "37" : "00");
2736  } else {
2737  // Remote to Local: ACH Collaction
2738  // Local information will be in dest_* fields of trans_data
2739  $localParts = $transData['dest_key'];
2740  $localType = $transData['dest_meta']['deposit_ach_type'];
2741  $localAccountInfo['TransactionCode'] = $localType == 10 ? "22" : ($localType == 20 ? "32" : "00");
2742  }
2743 
2744  // check for bad transaction code on local account
2745  if ($localAccountInfo['TransactionCode'] == "00") {
2746  throw new Exception("Error Building ACH Upload File - could not determine transaction code (208)");
2747  }
2748 
2749  $localAccount = str_replace( " ", "", $localParts['accountnumber'] );
2750  $localSub = str_replace( " ", "", $localParts['accounttype'] );
2751  $localAccountInfo['ReceivingDFI'] = substr( $pInputParams["OriginatingRouting"], 0, 8 );
2752  $localAccountInfo['CheckDigit'] = substr( $pInputParams["OriginatingRouting"], -1 );
2753  $localAccountInfo['DFIAccountNumber'] = FormatDFIAccount($localAccount, $localSub, $adminAchSettings["settings"]["sub_account_mask"]);
2754 
2755  // Set up local account name
2756  $localName = $localAccount . "-" . $localSub;
2757  $localName = strtoupper($localName);
2758  $localAccountInfo['IndividualName'] = MakeValidAchString($localName, 22);
2759 
2760  // The individual ID is used as an optional 15 character field
2761  // used to identify the local/remote account for the current record.
2762  $localAccountInfo['IndividualIdentificationNumber'] = str_repeat( " ", 15 );
2763  $localAccountInfo['DiscretionaryData'] = " ";
2764  $localAccountInfo['AddendaRecordIndicator'] = 0;
2765 
2766  $localAccountInfoSet = true;
2767  }
2768 
2769  $remoteParts = array();
2770 
2771  // Local to Remote: ACH Payment
2772  // Remote information will be in dest_* fields of trans_data
2773  if ($transferDirection == "1") {
2774  $remoteParts = $transData['acct_dest'];
2775  $remoteType = $remoteParts['rdfi']['rdfi_account_type'];
2776  $remoteAccountInfo['TransactionCode'] = $remoteType == 10 ? "22" : ($remoteType == 20 ? "32" : "00");
2777  } else {
2778  // Remote to Local: ACH Collection
2779  // Remote information will be in source_* fields of trans_data
2780  $remoteParts = $transData['acct_source'];
2781  $remoteType = $remoteParts['rdfi']['rdfi_account_type'];
2782  $remoteAccountInfo['TransactionCode'] = $remoteType == 10 ? "27" : ($remoteType == 20 ? "37" : "00");
2783  }
2784 
2785  // check for bad transaction code on remote account
2786  if ($remoteAccountInfo['TransactionCode'] == "00") {
2787  throw new Exception("Error Building ACH Upload File - could not determine transaction code (209)");
2788  }
2789 
2790  // Check for valid routing number
2791  if ( strlen( $remoteParts["rdfi"]["rdfi_routing"] ) != 9 ) {
2792  throw new Exception( "Error Building ACH Upload File - invalid routing number (210)" );
2793  }
2794 
2795  $remoteAccountInfo['ReceivingDFI'] = substr( $remoteParts["rdfi"]["rdfi_routing"], 0, 8 );
2796  $remoteAccountInfo['CheckDigit'] = substr( $remoteParts["rdfi"]["rdfi_routing"], -1 );
2797  $remoteAccount = str_replace( " ", "", $remoteParts["rdfi"]["rdfi_account"] );
2798  $remoteAccountInfo['DFIAccountNumber'] = MakeValidAchString( $remoteAccount, 17 );
2799 
2800  // Set up partner name
2801  $remoteName = $remoteParts['remote_entity']['name'];
2802  $remoteName = strtoupper($remoteName);
2803  $remoteAccountInfo['IndividualName'] = MakeValidAchString($remoteName, 22);
2804 
2805  // The individual ID is used as an optional 15 character field
2806  // used to identify the local/remote account for the current record.
2807  $remoteAccountInfo['IndividualIdentificationNumber'] = str_repeat( " ", 15 );
2808  $remoteAccountInfo['DiscretionaryData'] = " ";
2809 
2810  // Set up addenda record
2811  $remoteAddenda = trim($remoteParts['rdfi']['addenda']);
2812  if (strlen($remoteAddenda) > 0) {
2813  $remoteAccountInfo['Addenda'] = MakeValidAchString($remoteAddenda, 80);
2814  $remoteAccountInfo['AddendaRecordIndicator'] = 1;
2815  } else {
2816  $remoteAccountInfo['AddendaRecordIndicator'] = 0;
2817  }
2818 
2819  // Set up remote amount information
2820  $remoteActual = trim($rowDtl['amount']);
2821  $remoteActualFormatted = number_format($remoteActual, 2, ".", ",");
2822  $remoteActualFormatted = str_pad($remoteActualFormatted, 10, " ", STR_PAD_RIGHT);
2823 
2824  $remoteAmount = round($remoteActual * 100);
2825  $remoteAccountInfo['Amount'] = str_pad( $remoteAmount, 10, "0", STR_PAD_LEFT );
2826 
2827  // Add to local account total
2828  $localAccountTotal += $remoteActual;
2829  $localAccountTotalFormatted = number_format($localAccountTotal, 2, ".", ",");
2830  $localAccountTotalFormatted = str_pad($localAccountTotalFormatted, 10, " ", STR_PAD_RIGHT);
2831 
2832  $localAccountAmount = round($localAccountTotal * 100);
2833  $localAccountInfo['Amount'] = str_pad($localAccountAmount, 10, "0", STR_PAD_LEFT);
2834 
2835  // add total credit and debit amounts
2836  $companyTotalCredit += $remoteAmount;
2837  $pAchValues["totalFileCredit"] += $remoteAmount;
2838 
2839  $companyTotalDebit += $remoteAmount;
2840  $pAchValues["totalFileDebit"] += $remoteAmount;
2841 
2842  // an entry detail for each
2843  $companyEntryAddendaCount++;
2844  $pAchValues['fileEntryAddendaCount']++;
2845  $pAchValues["traceNumber"]++;
2846 
2847  $entryDetail = array (
2848  "RecordTypeCode" => "6",
2849  "TransactionCode" => $remoteAccountInfo['TransactionCode'],
2850  "ReceivingDFI" => $remoteAccountInfo['ReceivingDFI'],
2851  "CheckDigit" => $remoteAccountInfo['CheckDigit'],
2852  "DFIAccountNumber" => $remoteAccountInfo['DFIAccountNumber'],
2853  "Amount" => $remoteAccountInfo['Amount'],
2854  "IndividualIdentificationNumber" => $remoteAccountInfo['IndividualIdentificationNumber'],
2855  "IndividualName" => $remoteAccountInfo['IndividualName'],
2856  "DiscretionaryData" => $remoteAccountInfo['DiscretionaryData'],
2857  "AddendaRecordIndicator" => $remoteAccountInfo['AddendaRecordIndicator'],
2858  "TraceNumber" => substr( $pInputParams["OriginatingRouting"], 0, 8 ) . substr( "000000" . $pAchValues["traceNumber"], -7 ) );
2859 
2860  $entry = array_values( $entryDetail );
2861  $outputLine = implode( "", $entry );
2862  if ( strlen( $outputLine ) != 94 ) {
2863  throw new Exception( "ACH Output Error - entry detail block size (211)" );
2864  }
2865  $pOutputData[] = $outputLine;
2866 
2867  $rName = $remoteAccountInfo['IndividualName'];
2868  $rAccount =
2869  $remoteAccountInfo['ReceivingDFI'] .
2870  $remoteAccountInfo['CheckDigit'] . " / " .
2871  $remoteAccountInfo['DFIAccountNumber'];
2872  $rAmount = "$" . $remoteActualFormatted;
2873  $remoteReportLines[] = " Partner: {$rName} {$rAmount} {$rAccount}";
2874 
2875  // see if an addenda
2876  if ( HCU_array_key_exists("Addenda", $remoteAccountInfo) ) {
2877  $companyEntryAddendaCount++;
2878  $pAchValues["fileEntryAddendaCount"]++;
2879 
2880  // only one addenda line per entry detail, max
2881  $addendaRecord = array(
2882  "RecordTypeCode" => "7",
2883  "AddendaTypeCode" => "05",
2884  "PaymentRelatedInformation" => $remoteAccountInfo['Addenda'],
2885  "AddendaSequenceNumber" => "0001",
2886  "EntryDetailSequenceNumber" => str_pad( $pAchValues["traceNumber"], 7, "0", STR_PAD_LEFT ) );
2887 
2888  $addenda = array_values( $addendaRecord );
2889  $outputLine = implode( "", $addenda );
2890  if ( strlen( $outputLine ) != 94 ) {
2891  throw new Exception( "ACH Output Error - addenda entry block size (212)" );
2892  }
2893  $pOutputData[] = $outputLine;
2894  $remoteReportLines[] = " Addenda: {$remoteAccountInfo['Addenda']}";
2895  }
2896 
2897  // add to the control hashes
2898  $companyControlHash += $remoteAccountInfo['ReceivingDFI'];
2899  $pAchValues["fileControlHash"] += $remoteAccountInfo['ReceivingDFI'];
2900  }
2901 
2902  $companyEntryAddendaCount++;
2903  $pAchValues["fileEntryAddendaCount"]++;
2904 
2905  // Local accuont detail record
2906  $entryDetail = array (
2907  "RecordTypeCode" => "6",
2908  "TransactionCode" => $localAccountInfo['TransactionCode'],
2909  "ReceivingDFI" => $localAccountInfo['ReceivingDFI'],
2910  "CheckDigit" => $localAccountInfo['CheckDigit'],
2911  "DFIAccountNumber" => $localAccountInfo['DFIAccountNumber'],
2912  "Amount" => $localAccountInfo['Amount'],
2913  "IndividualIdentificationNumber" => $localAccountInfo['IndividualIdentificationNumber'],
2914  "IndividualName" => $localAccountInfo['IndividualName'],
2915  "DiscretionaryData" => $localAccountInfo['DiscretionaryData'],
2916  "AddendaRecordIndicator" => $localAccountInfo['AddendaRecordIndicator'],
2917  "TraceNumber" => substr( $pInputParams["OriginatingRouting"], 0, 8 ) . substr( "000000" . $pAchValues["traceNumber"], -7 ) );
2918 
2919  $entry = array_values( $entryDetail );
2920  $outputLine = implode( "", $entry );
2921  if ( strlen( $outputLine ) != 94 ) {
2922  throw new Exception( "ACH Output Error - entry detail block size (213)" );
2923  }
2924  $pOutputData[] = $outputLine;
2925 
2926  // add to the control hashes
2927  $companyControlHash += $localAccountInfo['ReceivingDFI'];
2928  $pAchValues["fileControlHash"] += $localAccountInfo['ReceivingDFI'];
2929 
2930  // Print local report line
2931  // Need to get printable versions of the necesary strings because they
2932  // are all padded to fit specific lengths.
2933  $lName = trim($localAccountInfo['IndividualName']);
2934  $lAmount = "$" . trim($localAccountTotalFormatted);
2935  if ($transferDirection == "1") {
2936  $pReportLines[] = " Transfer: ACH Payment, {$lAmount} effective {$effectiveDate} from {$lName} to:";
2937  } else {
2938  $pReportLines[] = " Transfer: ACH Collection, {$lAmount} effective {$effectiveDate} to {$lName} from:";
2939  }
2940 
2941  // Print remote information to report file
2942  // append remoteReportLines array to $pReportLines
2943  $pReportLines = array_merge($pReportLines, $remoteReportLines);
2944 
2945  // Add empty line between transactions
2946  $pReportLines[] = "";
2947 
2948  // the company hash can only be 10 characters long
2949  $entryHash = substr( str_repeat( "0", 10 ) . $companyControlHash, -10 );
2950 
2951  // output the company control record
2952  $companyControl = array (
2953  "RecordTypeCode" => "8",
2954  "ServiceClassCode" => $serviceClassCode,
2955  "EntryAddendaCount" => str_pad( $companyEntryAddendaCount, 6, "0", STR_PAD_LEFT ),
2956  "EntryHash" => $entryHash,
2957  "TotalDebitEntryDollarAmount" => str_pad( $companyTotalDebit, 12, "0", STR_PAD_LEFT ),
2958  "TotalCreditEntryDollarAmount" => str_pad( $companyTotalCredit, 12, "0", STR_PAD_LEFT ),
2959  "CompanyIdentification" => MakeValidAchString( $companyID, 10 ),
2960  "MessageAuthenticationCode" => str_repeat( " ", 19 ),
2961  "Reserved" => str_repeat( " ", 6 ),
2962  "OriginatingDFI" => substr( $pInputParams["OriginatingRouting"], 0, 8 ),
2963  "BatchNumber" => str_pad( $pAchValues["batchNumber"], 7, "0", STR_PAD_LEFT ) );
2964 
2965  $control = array_values( $companyControl );
2966 
2967  $outputLine = implode( "", $control );
2968  if ( strlen( $outputLine ) != 94 ) {
2969  throw new Exception( "ACH Output Error - company control block size (214)" );
2970  }
2971  $pOutputData[] = $outputLine;
2972 
2973  // add to the file batch count (doesn't include addenda)
2974  $pAchValues["fileBatchCount"]++;
2975  } catch (Exception $ex) {
2976  $logInfo = array( "error" => $ex->getMessage(), "code" => $ex->getCode() );
2977 
2978  $returnInfo["error"] = $ex->getMessage();
2979  $returnInfo["code"] = $ex->getCode() == 0 ? "999" : $ex->getCode();
2980  }
2981 
2982  return $returnInfo;
2983 } // end BuildACHBusiness
2984 
2985 // Helper function to strip out not allowed characters from string and pad the
2986 // correct amount of characters on end.
2987 // Returns the validated string.
2988 function MakeValidAchString( $inputString, $length ) {
2989  return substr( str_pad( preg_replace( ACH_NOT_ALLOWED, "", $inputString ), $length ), 0, $length );
2990 } // end MakeValidAchString
2991 
2992 
2993 /**
2994  * Concatenate and format the account number and sub-account number
2995  *
2996  * @param string $accountNumber Account number
2997  * @param string $subAccountNumber Sub-account number
2998  * @param string $mask Sub-account number mask to use
2999  *
3000  * @return string Formatted DFI string
3001  */
3002 function FormatDFIAccount($accountNumber, $subAccountNumber, $mask = '') {
3003  $maskLength = strlen($mask);
3004 
3005  if (strlen($subAccountNumber) >= $maskLength) {
3006  return MakeValidAchString("$accountNumber$subAccountNumber", 17 );
3007  }
3008 
3009  $accountLengthDelta = strlen("$accountNumber$mask") - DFI_ACCOUNT_MAX_SIZE;
3010  if ($accountLengthDelta > 0) {
3011  $mask = substr($mask, $accountLengthDelta);
3012  }
3013 
3014  $maskedNumber = substr("$mask$subAccountNumber", -1 * $maskLength);
3015  $maskedNumber = strtr($maskedNumber, '+', ' ');
3016 
3017  return MakeValidAchString("$accountNumber$maskedNumber", 17);
3018 }
3019 /**
3020  * CreateAchFiles
3021  * Get all the transactions in the given batch and supply the output for a valid ACH file.
3022  * NOTE: for any string, remove any non-allowed characters (e.g. in memos, addenda, etc).
3023  *
3024  * @param object $pAdmEnv -- structure with common and environment information
3025  * @param array $pAdmVars -- the passed parameters for the operation
3026  *
3027  * @return data or error
3028  */
3029 function CreateAchFiles( $pAdmEnv, $pAdmVars ) {
3030  $returnInfo = array("code" => "000", "errors" => array(), "data" => array());
3031 
3032  // output data in lines 94 bytes long, blocks of 10 (9-fill to make even blocks)
3033  $outputData = array();
3034 
3035  // report data is free-form, but assume text output
3036  $reportLines = array();
3037 
3038  try {
3039  // need this for sanitizing strings
3040  require_once( dirname(__FILE__) . "/../library/aACHValidate.i" );
3041 
3042  $cu = strtolower( $pAdmEnv["Cu"] );
3043  $CU = strtoupper( $pAdmEnv["Cu"] );
3044 
3045  $batchPreName = "Batch";
3046  $batchPath = "/home/{$cu}/admin/ach/";
3047  $blockingFactor = 10;
3048 
3049  // verify the directory exists
3050  if ( !file_exists( $batchPath ) ) {
3051  throw new Exception( "Create ACH Files Error - Directory does not exist - please contact Support" );
3052  }
3053 
3054  $payloadInfo = HCU_PayloadDecode( $pAdmEnv["Cu"], $pAdmVars["batch_id"] );
3055  $batchTimeString = $payloadInfo["batch_date"];
3056 
3057  // get the credit union's timezone
3058  $creditUnionTz = GetCreditUnionTimezone( $pAdmEnv["dbh"], $pAdmEnv["Cu"] );
3059 
3060  // use the batch timestamp to figure out the creation date/time and filename
3061  $myDateTime = new DateTime( $batchTimeString );
3062  $myDateTime->setTimezone(new DateTimeZone($creditUnionTz));
3063  $batchDay = $myDateTime->format("ymd");
3064  $batchHour = $myDateTime->format("His");
3065 
3066  // guess this is the letter we want
3067  $fileModifier = "A";
3068 
3069  // get all files created today
3070  $results = glob( "{$batchPath}{$batchPreName}_{$batchDay}_*.ach" );
3071  for ( $i = 0; $i < count( $results ); $i++ ) {
3072  $fileModifier++; // B, C, D ... - problems if over 26
3073  }
3074 
3075  $achBaseName = "{$batchPreName}_{$batchDay}_{$batchHour}";
3076  $achFileName = "{$achBaseName}.ach";
3077  $achReportName = "{$achBaseName}.txt";
3078 
3079  $reportLines[] = "Report for $achBaseName";
3080 
3081  // make sure the filename doesn't already exist
3082 // unresolved - search $results
3083 
3084  // get the CU's routing number
3085  // There will be only one routing number for a credit union.
3086  $adminAchSettings = AdminReadSettings($pAdmEnv, $pAdmEnv["dbh"], $CU);
3087 
3088  $cuRouting = trim( $adminAchSettings["settings"]["routing"] );
3089  if ( strlen( $cuRouting ) != 9 ) {
3090  throw new Exception( "ACH Settings - invalid CU routing number" );
3091  }
3092 
3093  // micro deposit source account
3094  $accountMicroDeposit = $adminAchSettings["settings"]["account"];
3095 
3096  // use the ACH settings name for the origin name
3097  $originName = MakeValidAchString( $adminAchSettings["settings"]["name"], 23 );
3098 
3099 /* UNRESOLVED QUESTIONS:
3100  * 14. How long to keep batches around?
3101  *
3102  * Answered:
3103  * 1. Do we gather just for an effective date, or all dates? Just gather them all; let upstream worry about warehousing.
3104  * 2. What if effective date is in the past? Use as is. Possibly might have to adjust to current date.
3105  * 3. NEED for File Header: Immediate Destination, Immediate Destination Name,
3106  * Immeditate Origination Name, FileID Modifier. DONE
3107  * 5. How handle reversals? Reversals are handled through the core so we don't worry about them.
3108  * 6. What Standard Entry Class Codes do we use? PPD and CCD (ach), WEB (external transfer).
3109  * 7. What Service Class Codes do we use? Currently 220, 225 - base it on the transhdr.transaction code 1st character.
3110  * 8. How determine the Transaction Code (entry detail)? Base it on flags in transdtl.transdata for checking or savings,
3111  * credit or debit.
3112  * 9. When Email notify? Set processed status when creating batch, then send email and finalize the processed status.
3113  * 10. Do we want to handle multiple addenda for a txn? No. No matter how much addenda, only get one line.
3114  * 11. Do we want to re-create a batch identically, with different create date/time, or store created? Store file on file
3115  * system; allow for re-download.
3116  * 15. ACH Report? Yes, generate and save on disk with the ach file. Let user re-download either report or file. Show report
3117  * screen and download file from that screen.
3118  * 16. If have entry but missing files, allow for re-build of ach and report files? Yes; build exactly the same, if possible.
3119  * This feature lets the ach file be removed, data updated, and the batch re-built;
3120  * 17. Use local time timestamp for batch file name, or utc? Local time values, utc stored in database.
3121  * 13. Need filtering of data on inputs to avoid invalid Alphameric data. Doing a str_replace().
3122  * 4. NEED for Company Header: Company name - using CU name from admin settings,
3123  * Company ID - using CU routing number,
3124  * Company Entry Description - using CU name from admin settings,
3125  * Company descriptive date - leaving blank.
3126  * Using CU name, routing info
3127  * 12. Need for Entry Detail (individual): Individual ID Number. Using CU routing number for now and external
3128  * account name for IndividualName.
3129  */
3130  // create a file header
3131  $myRouting = substr( str_pad( trim( $adminAchSettings["settings"]["routing"] ), 10, " ", STR_PAD_LEFT ), 0, 10 );
3132 
3133  $fileCreateDate = new DateTimeImmutable("now", new DateTimeZone($creditUnionTz));
3134 
3135  $fileHeader = array( "RecordTypeCode" => "1",
3136  "PriorityCode" => "01",
3137  "ImmediateDestination" => $myRouting, // cu's routing/transit number
3138  "ImmediateOrigin" => $myRouting, // same routing number for origin and destination
3139  "FileCreateDate" => $fileCreateDate->format("ymd"), // current day (yymmdd)
3140  "FileCreateTime" => $fileCreateDate->format("Hi"), // current time (hhmm)
3141  "FileIDModifier" => $fileModifier, // new letter for each file on the given day
3142  "RecordSize" => "094",
3143  "BlockingFactor" => $blockingFactor,
3144  "FormatCode" => "1",
3145  "ImmediateDestinationName" => str_pad( "Federal Reserve Bank", 23 ),
3146  "ImmediateOriginName" => $originName,
3147  "ReferenceCode" => str_repeat( " ", 8 ) );
3148 
3149  $header = array_values( $fileHeader );
3150 
3151  $outputLine = implode( "", $header );
3152  if ( strlen( $outputLine ) != 94 ) {
3153  throw new Exception( "ACH Output Error - file header block size" );
3154  }
3155  $outputData[] = $outputLine;
3156 
3157 // unresolved - need to group and count by type of ACH for the report
3158  $transactionHeaders = GetACHTransactionHeaders( $pAdmEnv, $batchTimeString );
3159  if ( !count( $transactionHeaders ) ) {
3160  throw new Exception( "Error Building ACH Upload File - no ACH transactions for upload", 300 );
3161  }
3162 
3163  // have the important values and counters all be in one variable
3164  $values = array();
3165 
3166  // these get adjusted along the way
3167  $values["batchNumber"] = 0;
3168  $values["totalFileCredit"] = 0;
3169  $values["totalFileDebit"] = 0;
3170  $values["fileControlHash"] = 0;
3171  $values["fileEntryAddendaCount"] = 0;
3172  $values["fileBatchCount"] = 0; // number of "8" records
3173 
3174  // trace number is unique and ascending across all type "6" records
3175  $values["traceNumber"] = 0;
3176 
3177  // first handle the micro-deposits
3178  $passedParams = array( "CompanyName" => $originName,
3179  "OriginatingRouting" => $cuRouting,
3180  "CompanyEntryDescription" => $originName,
3181  "EffectiveEntryDate" => (new DateTime("now", new DateTimeZone($creditUnionTz)))->format('ymd'),
3182  "accountMicroDeposit" => $accountMicroDeposit,
3183  "CompanyIDType" => $adminAchSettings['settings']['type']
3184  );
3185 
3186  $reportLines[] = "";
3187  $reportLines[] = "Micro Deposits";
3188  $retInfo = AddMicroDeposits( $pAdmEnv, $transactionHeaders, $passedParams, $values, $outputData, $reportLines );
3189  if ( $retInfo["code"] != "000" ) {
3190  throw new Exception( $retInfo["error"] );
3191  }
3192 
3193  // add the offsetting entries for getting the micro deposits back
3194  $reportLines[] = "";
3195  $reportLines[] = "Micro Deposit Offsetting Entries";
3196  $retInfo = AddMicroDepositOffsets( $pAdmEnv, $transactionHeaders, $passedParams, $values, $outputData, $reportLines );
3197  if ( $retInfo["code"] != "000" ) {
3198  throw new Exception( $retInfo["error"] );
3199  }
3200 
3201  // remove the micro-deposit specific parameter
3202  unset( $passedParams["accountMicroDeposit"] );
3203 
3204  // cycle through the header rows, handling the TRNEXT external transfers
3205  $reportLines[] = "";
3206  $reportLines[] = "External Transfers";
3207  $hasOne = false;
3208  $hdrRow = 0;
3209  for ( $hdrRow = 0; $hdrRow < count( $transactionHeaders ); $hdrRow++ ) {
3210  $rowHdr = $transactionHeaders[$hdrRow];
3211 
3212  if ( $rowHdr["processed"] > 0 ) continue;
3213 
3214  if ( $rowHdr["feature_code"] == "TRNEXT" ) {
3215  $retInfo = BuildACHTransfer( $pAdmEnv, $rowHdr, $passedParams, $values, $outputData, $reportLines );
3216  if ( $retInfo["code"] != "000" ) {
3217  throw new Exception( $retInfo["error"] );
3218  }
3219 
3220  // flag as processed
3221  $transactionHeaders[$hdrRow]["processed"] = true;
3222 
3223  $hasOne = true;
3224  }
3225  }
3226  if ( !$hasOne ) {
3227  $reportLines[] = " None";
3228  }
3229 
3230  // now cycle through handling the ACHCOL and ACH PMT transfers
3231  $reportLines[] = "";
3232  $reportLines[] = "ACH Transactions";
3233  $hasOne = false;
3234  $hdrRow = 0;
3235  for ( $hdrRow = 0; $hdrRow < count( $transactionHeaders ); $hdrRow++ ) {
3236  $rowHdr = $transactionHeaders[$hdrRow];
3237 
3238  if ( $rowHdr["processed"] > 0 ) continue;
3239 
3240  if ( $rowHdr["feature_code"] == "ACHCOL" ||
3241  $rowHdr["feature_code"] == "ACHPMT" ) {
3242  $achBusinessReturn = BuildACHBusiness( $pAdmEnv, $rowHdr, $passedParams, $values, $outputData, $reportLines );
3243  if ($achBusinessReturn['code'] !== "000") {
3244  throw new Exception($achBusinessReturn['error'], $achBusinessReturn['code']);
3245  }
3246  // flag as processed
3247  $transactionHeaders[$hdrRow]["processed"] = true;
3248 
3249  $hasOne = true;
3250  }
3251  }
3252  if ( !$hasOne ) {
3253  $reportLines[] = " None";
3254  }
3255 
3256  // see if any unprocessed - this should just be found during development
3257  for ( $hdrRow = 0; $hdrRow < count( $transactionHeaders ); $hdrRow++ ) {
3258  $rowHdr = $transactionHeaders[$hdrRow];
3259 
3260  if ( $rowHdr["processed"] > 0 ) continue;
3261 
3262  // don't know how to handle anything else
3263  throw new Exception( "Error Building ACH Upload File - Unprocessed transaction: " . print_r( $rowHdr, true ) );
3264  }
3265 
3266  // the file hash can only be 10 characters long
3267  $entryHash = substr( str_repeat( "0", 10 ) . $values["fileControlHash"], -10 );
3268 
3269  // for the total blocks, count what we have plus one more for the file control record
3270  // remember: a block is the number of blockingFactor blocks and includes the 999 filler records
3271  $realRecordCount = count( $outputData ) + 1; // add the file control record
3272  $fillerCount = $blockingFactor - ($realRecordCount % $blockingFactor);
3273  $totalBlockCount = floor( ( $realRecordCount + $fillerCount ) / $blockingFactor ); // round down and avoid floating point zeros issues
3274  // note: adding filler won't adjust the block count since it is just completing the block
3275 
3276  // output the file control record
3277  $fileControl = array ( "RecordTypeCode" => "9",
3278  "BatchCount" => str_pad( $values["fileBatchCount"], 6, "0", STR_PAD_LEFT ),
3279  "BlockCount" => str_pad( $totalBlockCount, 6, "0", STR_PAD_LEFT ),
3280  "EntryAddendaCount" => str_pad( $values["fileEntryAddendaCount"], 8, "0", STR_PAD_LEFT ),
3281  "EntryHash" => $entryHash,
3282  "TotalDebitEntryDollarAmount" => str_pad( $values["totalFileDebit"], 12, "0", STR_PAD_LEFT ),
3283  "TotalCreditEntryDollarAmount" => str_pad( $values["totalFileCredit"], 12, "0", STR_PAD_LEFT ),
3284  "Reserved" => str_repeat( " ", 39 ) );
3285 
3286  $control = array_values( $fileControl );
3287 
3288  $outputLine = implode( "", $control );
3289  if ( strlen( $outputLine ) != 94 ) {
3290  throw new Exception( "ACH Output Error - file control block size" );
3291  }
3292  $outputData[] = $outputLine;
3293 
3294  // add the 999 blocks until the right number of blocks
3295  for ( $i = 0; $i < $fillerCount; $i++ ) {
3296  $outputData[] = str_repeat( "9", 94 );
3297  }
3298 
3299  // the report is returned to the caller
3300  $reportString = implode( "\n", $reportLines );
3301 
3302  // save a copy of the current directory
3303  $currDir = getcwd();
3304 
3305  if ( chdir( $batchPath ) ) {
3306  // save the ach batch
3307  $fp = fopen( $achFileName, "w" );
3308  if ( $fp ) {
3309  // now convert the output array to a string
3310  $outputString = implode( "\n", $outputData );
3311 
3312  $bytesWritten = fputs( $fp, $outputString );
3313  fclose( $fp );
3314 
3315  if ( $bytesWritten != strlen( $outputString ) ) {
3316  throw new Exception( "ACH Output Error - could not write to output file" );
3317  }
3318  } else {
3319  throw new Exception( "ACH Output Error - could not open output file" );
3320  }
3321 
3322  // save the batch report
3323  $fp = fopen( $achReportName, "w" );
3324  if ( $fp ) {
3325  $bytesWritten = fputs( $fp, $reportString );
3326  fclose( $fp );
3327 
3328  if ( $bytesWritten != strlen( $reportString ) ) {
3329  throw new Exception( "ACH Output Error - could not write to report file" );
3330  }
3331  } else {
3332  throw new Exception( "ACH Output Error - could not open report file" );
3333  }
3334  } else {
3335  throw new Exception( "ACH Output Error - could not reach file directory" );
3336  }
3337  chdir( $currDir );
3338 
3339  // iterate the transaction headers and set the processed_status to 20
3340  // this value will signify the file was generated successfully.
3341  $hdrRow = 0;
3342  $hdrList = array();
3343  for ( $hdrRow = 0; $hdrRow < count( $transactionHeaders ); $hdrRow++ ) {
3344  $hdrList[] = $transactionHeaders[$hdrRow]['id'];
3345  }
3346  $hdrString = implode(",", $hdrList);
3347 
3348  // update hdr processed_status using this list of hdr id's
3349  $sql = "UPDATE {$pAdmEnv['Cu']}transhdr SET processed_status = 20 WHERE id IN ($hdrString)";
3350  $sqlRs = db_query( $sql, $pAdmEnv['dbh'] );
3351  if (!$sqlRs) {
3352  throw new Exception( "ACH Output Error - unable to update transaction process" );
3353  }
3354 
3355  $returnInfo["data"]["batch_id"] = $pAdmVars["batch_id"];
3356  } catch (Exception $ex) {
3357  $logInfo = array( "error" => $ex->getMessage(), "code" => $ex->getCode() );
3358 
3359  $returnInfo["errors"] = $ex->getMessage();
3360  $returnInfo["code"] = $ex->getCode() == 0 ? "999" : $ex->getCode();
3361  }
3362 
3363  return $returnInfo;
3364 } // end CreateAchFiles
3365 
3366 
3367 /**
3368  * GetApprovedACHTrans
3369  * Get any approved but not yet processed ACH transactions for the given CU.
3370  *
3371  * @param object $pAdmEnv -- structure with common and environment information
3372  *
3373  * @return
3374  */
3375 function GetApprovedACHTrans( $pAdmEnv ) {
3376  $returnInfo = array("code" => "000", "errors" => array(), "data" => array());
3377 
3378  try {
3379  $achFeatures = "'ACHCOL', 'ACHPMT', 'ACHPYRL', 'TRNEXT'";
3380 
3381  // gotta get them all - Left joins in case user is gone
3382  $sql = "SELECT id, feature_code, effective_date, u1.user_name AS poster, posted_date,
3383  u2.user_name AS approver, approved_date, accountnumber,
3384  transactioncode, memo, group_name, (select sum(amount) from {$pAdmEnv["Cu"]}transdtl d where d.transhdr_id = th.id) as amount
3385  FROM {$pAdmEnv["Cu"]}transhdr th
3386  LEFT JOIN {$pAdmEnv["Cu"]}user u1 ON u1.user_id = th.posted_by
3387  LEFT JOIN {$pAdmEnv["Cu"]}user u2 ON u2.user_id = th.approved_by
3388  INNER JOIN {$pAdmEnv["Cu"]}group g ON g.group_id = u1.group_id
3389  WHERE approved_status = 10
3390  AND processed_status IS NULL
3391  AND feature_code in ($achFeatures)
3392  ORDER BY effective_date, group_name, feature_code, id";
3393  $rs = db_query( $sql, $pAdmEnv["dbh"] );
3394  if ( !$rs ) {
3395  throw new Exception( "ACH Query Error - getting approved ach txns" );
3396  }
3397 
3398  // cycle through the rows
3399  $row = 0;
3400  $returnData = array();
3401  while ( $txRow = db_fetch_assoc( $rs, $row++ ) ) {
3402  // do any data processing
3403  $timestamp = strtotime( $txRow["effective_date"] );
3404  $account = trim( $txRow["accountnumber"] );
3405  $returnData[] = array( "id" => $txRow["id"],
3406  "feature" => trim( $txRow["feature_code"] ),
3407  "eff_date" => date( "m/d/Y", $timestamp ),
3408  "group" => $txRow["group_name"],
3409  "account" => $account,
3410  "amount" => $txRow["amount"],
3411  "memo" => $txRow["memo"]
3412  );
3413  }
3414 
3415  $returnInfo["data"] = $returnData;
3416  } catch (Exception $ex) {
3417  $logInfo = array( "error" => $ex->getMessage(), "code" => $ex->getCode() );
3418 
3419  $returnInfo["errors"] = $ex->getMessage();
3420  $returnInfo["code"] = "999";
3421  }
3422 
3423  return $returnInfo;
3424 } // end GetApprovedACHTrans
3425 
3426 /**
3427  * GetACHTransDetail
3428  * Get the detail for the given ACH transaction.
3429  *
3430  * @param object $pAdmEnv -- structure with common and environment information
3431  * @param integer $pTransId -- the transaction to cancel
3432  *
3433  * @return
3434  */
3435 function GetACHTransDetail( $pAdmEnv, $pTransId ) {
3436  $returnInfo = array("code" => "000", "errors" => array(), "data" => array());
3437 
3438  try {
3439  // get the necessary header info
3440  $sql = "SELECT th.feature_code, th.effective_date, th.accountnumber, th.transactioncode,
3441  th.memo, g.group_name,
3442  (SELECT user_name
3443  FROM {$pAdmEnv["Cu"]}user
3444  WHERE group_id = u.group_id
3445  AND is_group_primary = TRUE
3446  LIMIT 1) as group_owner
3447  FROM {$pAdmEnv["Cu"]}transhdr th
3448  INNER JOIN {$pAdmEnv["Cu"]}user u ON u.user_id = th.posted_by
3449  INNER JOIN {$pAdmEnv["Cu"]}group g ON g.group_id = u.group_id
3450  WHERE id = $pTransId";
3451  $rs = db_query( $sql, $pAdmEnv["dbh"] );
3452  if ( !$rs ) {
3453  throw new Exception( "ACH Query Error - getting ach detail txn header" );
3454  }
3455 
3456  $headerRow = db_fetch_assoc( $rs, 0 );
3457 
3458  $featureCode = trim( $headerRow["feature_code"] );
3459 
3460  $timestamp = strtotime( $headerRow["effective_date"] );
3461 
3462  $account = trim( $headerRow["accountnumber"] );
3463 
3464  $headerData = array( //"id" => $headerRow["id"],
3465  "feature" => $featureCode,
3466  "eff_date" => date( "m/d/Y", $timestamp ),
3467  "account" => $account,
3468  "group_name" => trim( $headerRow["group_name"] ),
3469  "group_owner" => trim( $headerRow["group_owner"] ),
3470  "memo" => trim( $headerRow["memo"] )
3471  );
3472 
3473  // get all the transaction detail
3474  $sql = "SELECT * FROM {$pAdmEnv["Cu"]}transdtl
3475  WHERE transhdr_id = $pTransId
3476  ORDER BY id";
3477  $rs = db_query( $sql, $pAdmEnv["dbh"] );
3478  if ( !$rs ) {
3479  throw new Exception( "ACH Query Error - getting ach txn detail" );
3480  }
3481 
3482  // where the remote account info is depends on type of ACH
3483  if ( substr( $headerRow["transactioncode"], 0, 1 ) == 1 ) {
3484  $whichRemote = "acct_dest";
3485  $whichLocal = "acct_source";
3486  } else {
3487  $whichRemote = "acct_source";
3488  $whichLocal = "acct_dest";
3489  }
3490 
3491  // cycle through the rows
3492  $row = 0;
3493  $returnDetail = array();
3494  $localAccount = "";
3495  while ( $detRow = db_fetch_assoc( $rs, $row++ ) ) {
3496  // do any data processing
3497  $transData = HCU_JsonDecode( $detRow["transdata"], true );
3498  // if don't have it yet, get the local account
3499  if ( $localAccount == "" ) {
3500  $localAccount = $transData[$whichLocal];
3501  }
3502 
3503  // both parts of the account should exist
3504  $dfiAccount = "{$transData[$whichRemote]["rdfi"]["rdfi_routing"]} / {$transData[$whichRemote]["rdfi"]["rdfi_account"]}";
3505 
3506  $remoteAddress1 = trim( HCU_array_key_value('address1', $transData[$whichRemote]["remote_entity"]) );
3507  $remoteAddress2 = trim( HCU_array_key_value('address2', $transData[$whichRemote]["remote_entity"]) );
3508  $remoteAddress = "Not Listed";
3509  if ( strlen( $remoteAddress1 ) > 0 ) {
3510  if ( strlen( $remoteAddress2 ) > 0 ) {
3511  $remoteAddress = "$remoteAddress1 / $remoteAddress2";
3512  } else {
3513  $remoteAddress = $remoteAddress1;
3514  }
3515  } else if ( strlen( $remoteAddress2 ) ) {
3516  $remoteAddress = $remoteAddress2;
3517  }
3518 
3519  $detailData = array( "amount" => $detRow["amount"],
3520  "dfi_account" => $dfiAccount,
3521  "remote_name" => HCU_array_key_value('name', $transData[$whichRemote]["remote_entity"]),
3522  "remote_address" => $remoteAddress,
3523  "addenda" => HCU_array_key_value('addenda', $transData[$whichRemote]["rdfi"])
3524  );
3525  $returnDetail[] = $detailData;
3526  }
3527 
3528  // get the full local account if have the info
3529  if ( $localAccount != "" && $localAccount != "micro" ) {
3530  $parts = explode( "|", $localAccount );
3531  $type = $parts[0] == "D" ? "(Deposit)" : ($type == "L" ? "(Loan)" : "(Unknown)");
3532  $account = trim( $parts[1] );
3533  $subAccount = trim( $parts[2] );
3534  $accountString = trim( "$account - $subAccount $type" );
3535  $headerData["account"] = $accountString;
3536  } else if ( $localAccount == "micro" ) {
3537  $headerData["account"] = "Micro Deposit";
3538  }
3539 
3540  $returnInfo["data"]["header"] = $headerData;
3541  $returnInfo["data"]["detail"] = $returnDetail;
3542 
3543  } catch (Exception $ex) {
3544  $logInfo = array( "error" => $ex->getMessage(), "code" => $ex->getCode() );
3545 
3546  $returnInfo["errors"] = $ex->getMessage();
3547  $returnInfo["code"] = "999";
3548  }
3549 
3550  return $returnInfo;
3551 } // end GetACHTransDetail
3552 
3553 function BuildFeatureNameLookup( $pDbh ) {
3554  // get a lookup array for the feature names
3555  require_once(dirname(__FILE__) . '/../../shared/library/sFeatureMnu.i');
3556  $result = GetFeatureList( $pDbh );
3557  // unresolved: check result[code]?
3558  $menuFeatures = $result["data"];
3559 
3560  // these are the ACH types
3561  $achFeatureList = array( "ACHCOL" => "Missing menu entry",
3562  "ACHPMT" => "Missing menu entry",
3563  "ACHPYRL" => "Missing menu entry",
3564  "TRNEXT" => "Missing menu entry" );
3565 // Unreasolved: We have more ACH features than menu features. Ach can be used for:
3566 // payment (single or batch), collection (single or batch), payroll, transfer, micro deposit
3567 
3568  // go through and get display names
3569  $lookupList = array_keys( $achFeatureList );
3570  for ( $i = 0; $i < count( $lookupList ); $i++ ) {
3571  $lookup = $lookupList[$i];
3572  for ( $m = 0; $m < count( $menuFeatures ); $m++ ) {
3573  if ( $menuFeatures[$m]["featurecode"] == $lookup ) {
3574  $achFeatureList[$lookup] = $menuFeatures[$m]["description"];
3575  break;
3576  }
3577  }
3578  }
3579 
3580  return $achFeatureList;
3581 } // end BuildFeatureNameLookup
3582 /**************** end functions *****************/
3583 
3584 /**
3585  * function PrintActionPage($pAdmEnv, $pAdmVars)
3586  * Prints out the page that allows selecting of batch items. Prints to the admin screen.
3587  * Let the user choose which unprocessed ACH transactions to choose and see txn detail. The
3588  * user can go to a 2nd page to see the result of the batch creation and download the file.
3589  *
3590  * @param object $pAdmEnv -- structure with common and environment information
3591  * @param array $pAdmVars -- the passed parameters for the operation
3592  */
3593 function PrintActionPage( $pAdmEnv, $pAdmVars ) {
3594  // get the feature names
3595  $dbh = $pAdmEnv["dbh"];
3596  $achFeatureList = BuildFeatureNameLookup( $dbh );
3597 
3598  $self = $pAdmEnv["self"];
3599  $advPerm = $pAdmEnv["advPerm"];
3600  ?>
3601  <script type="text/javascript">
3602  <?php getShowWaitFunctions(); ?>
3603 
3604  var activeWindows= [];
3605  var achActionGrid;
3606  var dsACHHelper;
3607  var achTransList = []; // initialize list for display grid
3608  var cancelDialog;
3609  var batchDialog;
3610  var infoDialog;
3611  var detailWindow;
3612  var cachedDetailInfo = []; // cache to accumulate detail
3613  var lastDetailInfo; // the last detail requested that needs to be shown
3614  var window_stack = [];
3615  var featureList = <?php echo HCU_JsonEncode( $achFeatureList ) ?>;
3616  var achDetailModel;
3617 
3618  $(document).ready(function() {
3619  achDetailModel = kendo.observable({
3620  achFeature: "",
3621  groupOwner: "",
3622  groupName: "",
3623  account: "",
3624  effDate: "",
3625  memo: "",
3626  detail: "[]",
3627  SetDetail: function( detailInfo ) {
3628  this.set( "achFeature", ( detailInfo.header.feature in featureList ) ? featureList[detailInfo.header.feature] : 'Unknown' );
3629  this.set( "groupOwner", detailInfo.header.group_owner );
3630  this.set( "groupName", detailInfo.header.group_name );
3631  this.set( "account", detailInfo.header.account );
3632  this.set( "effDate", detailInfo.header.eff_date );
3633  this.set( "memo", detailInfo.header.memo );
3634  this.set( "detail", JSON.stringify( detailInfo.detail ) );
3635  }
3636  });
3637 
3638  dsACHHelper = new kendo.data.DataSource({
3639  autoSync: false,
3640  batch: false,
3641  transport: {
3642  read: {
3643  url: "<?php echo $self; ?>",
3644  dataType: "json",
3645  contentType: "application/x-www-form-urlencoded",
3646  type: "POST",
3647  cache: false
3648  }
3649  },
3650  schema: {
3651  parse: function(response) {
3652  // not showing data, so return empty array
3653  var display = [];
3654  return display;
3655  }
3656  },
3657  requestStart: function( e ) {
3658  $.homecuValidator.displayMessage( "", $.homecuValidator.settings.statusError );
3659  showWaitWindow();
3660  },
3661  requestEnd: function( e ) {
3662  var results = null;
3663 
3664  try {
3665  hideWaitWindow();
3666  if (e.hasOwnProperty("response")) {
3667  if (e.response.hasOwnProperty("results")) {
3668  var results = e.response.results;
3669  if (results.hasOwnProperty("errors")) {
3670  e.preventDefault();
3671 
3672  // during the batching process the email notifications may fail
3673  // this should not hold up the whole process,
3674  // update the grid and direct user to history to restart the
3675  // notifications process.
3676  if ( results.hasOwnProperty("data") ) {
3677 
3678  <?php if ($advPerm) { ?>
3679  // data formakebatch
3680  if (results.data.hasOwnProperty("achMakeBatch")) {
3681  ShowBatchResults( results.data.achMakeBatch.batch_id );
3682  // refresh the display
3683  GetAvailableACHTxns();
3684  }
3685  <?php } ?>
3686  // data for others (future?)
3687  } else {
3688  dsACHHelper.cancelChanges();
3689  }
3690  throw results.errors;
3691  } else {
3692  if ( results.data ) {
3693  if ( results.data.achGetReady ) {
3694  ShowAvailableACH( results.data.achGetReady );
3695  }
3696  if ( results.data.achGetDetail ) {
3697  ShowAchDetail( results.data.achGetDetail );
3698  }
3699 
3700  <?php if ($advPerm) { ?>
3701  if ( results.data.achCancel ) {
3702  HandleCancelResult( results.data.achCancel );
3703  }
3704  if ( results.data.achMakeBatch ) {
3705  ShowBatchResults( results.data.achMakeBatch.batch_id );
3706  // refresh the display
3707  GetAvailableACHTxns();
3708  }
3709  <?php } ?>
3710  }
3711  if (results.info) {
3712  // check if any extra info that needs to go into a dialog box.
3713  // the notification process can fail if the batch is successful,
3714  // show the success message in the popout, put the extra failure info
3715  // into a dialog.
3716 
3717  // splice out success message first
3718  var successMsg = results.info.splice(0, 1);
3719  $.homecuValidator.displayMessage( successMsg, $.homecuValidator.settings.statusInfo);
3720 
3721  // build warning dialog content
3722  var warningContent = "";
3723  for (var i = 0; i < results.info.length; i++) {
3724  if (i == 0) {
3725  // this is the initial warning, make bold
3726  warningContent += "<p><strong>" + results.info[i] + "</strong></p>";
3727  } else {
3728  // this is the information for the initial warning, regular text
3729  warningContent += "<p>" + results.info[i] + "</p>";
3730  }
3731  }
3732 
3733  if (warningContent !== "") {
3734  // slow down the popup so not everything will happen at the exact same time
3735  setTimeout(function() {
3736  infoDialog.content(warningContent);
3737  infoDialog.center();
3738  infoDialog.open();
3739  }, 750);
3740  }
3741  }
3742  }
3743  } else {
3744  throw "Unexpected results from server";
3745  }
3746  } else {
3747  throw "Invalid results from server";
3748  }
3749  } catch (error) {
3750  var theMessage;
3751  if ( error ) {
3752  theMessage = error.message ? error.message : error;
3753  } else {
3754  theMessage = "Unknown server error occurred during the operation";
3755  }
3756  $.homecuValidator.displayMessage( theMessage, $.homecuValidator.settings.statusError );
3757  }
3758  },
3759 
3760  // code to run if the request fails; the raw request and
3761  // status codes are passed to the function
3762  error: function( e ) {
3763  }
3764  });
3765 
3766  <?php if ($advPerm) { ?>
3767  // start disabled and enable when there are selections
3768  $("#actionsDDL").kendoDropDownList({
3769  enable: false,
3770  dataTextField: "name",
3771  dataValueField: "value",
3772  dataSource: [{name: "Create ACH Upload File", value: "batch"}, {name: "Cancel Transactions", value: "cancel"}],
3773  optionLabel: {name: "Choose Action...", value: ""},
3774  change: function(e) {
3775  // get the partner information from the server
3776  var action = this.value();
3777 
3778  if ( action === "cancel" ) {
3779  cancelDialog.open().center();
3780  } else if ( action === "batch" ) {
3781  batchDialog.open().center();
3782  }
3783 
3784  // reset the dropdown
3785  this.select(0);
3786  }
3787  });
3788  <?php } ?>
3789 
3790  achActionGrid = $("#achActionGrid").kendoGrid({
3791  dataSource: achTransList,
3792  sortable: true,
3793  selectable: "cell",
3794  noRecords: {
3795  template: "<div class='vsgSecondary'>No available ACH transactions were found</div>"
3796  },
3797  columns: [
3798  <?php if ($advPerm) { ?>
3799  {field: "checked", headerTemplate: "<input type='checkbox' id='checkAllForBatch'>", width: "45px",
3800  sortable: false,
3801  template: "<label><input type='checkbox' class='in-batch-checkbox' name='inBatch' value='1'></label>"},
3802  <?php } ?>
3803  {field: "eff_date", title: "Effective Date", width: "110px", headerAttributes: { "class": "textAlignCenter"}, attributes: {"class": "textAlignCenter"}},
3804  {field: "feature", title: "Feature", width: "110px",
3805  headerAttributes: { "class": "textAlignCenter"}, attributes: {"class": "textAlignCenter"},
3806  template: "#= ( featureList[feature] === undefined ) ? feature : featureList[feature] #"},
3807  {field: "group", title: "Group", width: "100px", headerAttributes: { "class": "textAlignCenter"}, attributes: {"class": "textAlignCenter"}},
3808  {field: "account", title: "Account", width: "130px", headerAttributes: { "class": "textAlignCenter"}, attributes: {"class": "textAlignCenter"}},
3809  {field: "amount", title: "Amount", width: "110px",
3810  headerAttributes: { "class": "textAlignRight"}, attributes: {"class": "textAlignRight"},
3811  template: '#= kendo.toString(amount, "c") #'},
3812  {field: "memo", title: "Memo"}
3813  ],
3814  change: function( e ) {
3815  e.preventDefault();
3816 
3817  var selectedCell = this.select();
3818  var selectedRow = selectedCell.closest("tr");
3819  var cellIndex = selectedCell.index();
3820 
3821  var dataItem = achActionGrid.dataItem(selectedRow);
3822 
3823  if ( cellIndex < 1 ) {
3824 
3825  <?php if ($advPerm) { ?>
3826  var checkbox = selectedRow.find("td:first input");
3827  checkbox.prop("checked", !checkbox.prop("checked"));
3828 
3829  // save the checked state for just the one id
3830  SetAchInBatch( dataItem.id, checkbox.prop("checked") );
3831 
3832  TestActions();
3833  <?php } ?>
3834  } else {
3835  GetAchDetail( dataItem.id );
3836  }
3837 
3838  selectedCell.removeClass( "k-state-selected" );
3839  },
3840  <?php if ($advPerm) { ?>
3841  dataBound: function( e ) {
3842  $("#checkAllForBatch").click( function() {
3843  var state = $("#checkAllForBatch").prop( "checked" );
3844  $(".in-batch-checkbox").prop( "checked", state );
3845 
3846  // set them all
3847  SetAchInBatch( 0, state );
3848 
3849  TestActions();
3850  });
3851 
3852  $(".in-batch-checkbox").click( function(e) {
3853  var selectedRow = $(e.target).closest("tr");
3854 
3855  var dataItem = achActionGrid.dataItem(selectedRow);
3856 
3857  // just read the value because here the user clicked the box directly
3858  var checkbox = selectedRow.find("td:first input");
3859 
3860  // save the checked state for just the one id
3861  SetAchInBatch( dataItem.id, checkbox.prop("checked") );
3862 
3863  TestActions();
3864  });
3865  }
3866  <?php } ?>
3867  }).data("kendoGrid");
3868 
3869  // set up the tooltips this way so the defaults can be used
3870  var toolTipProps = homecuTooltip.defaults;
3871  //this filter selects the 2nd, 3rd, 4th, and 6th column's cells
3872  toolTipProps.filter = "td:nth-child(3),td:nth-child(4),td:nth-child(5),td:nth-child(7)";
3873  toolTipProps.position = "top",
3874  toolTipProps.maxWidth = 400,
3875  toolTipProps.showAfter = 20,
3876  toolTipProps.content = function(e) {
3877  var element = e.target[0];
3878  if ( element.offsetWidth < element.scrollWidth ) {
3879  return "<span style='display:block; width:100%;'>" + e.target.text() + "</span>";
3880  } else {
3881  return "";
3882  }
3883  };
3884  toolTipProps.show = function(e) {
3885  if( this.content.text() !== "") {
3886  $('[role="tooltip"]').css("visibility", "visible");
3887  }
3888  };
3889  toolTipProps.hide = function() {
3890  $('[role="tooltip"]').css("visibility", "hidden");
3891  };
3892 
3893  $("#achActionGrid").kendoTooltip(toolTipProps).data("kendoTooltip");
3894 
3895  achDetailGrid = $("#achDetailGrid").kendoGrid({
3896  dataSource: new kendo.data.DataSource({
3897  transport: {
3898  read: function( options ) {
3899  var data = JSON.parse( achDetailModel.detail )
3900  options.success(data);
3901  }
3902  }
3903  }),
3904  sortable: true,
3905  selectable: false,
3906  noRecords: {
3907  template: "<div class='vsgSecondary'>No available ACH transactions were found</div>"
3908  },
3909  columns: [
3910  {field: "remote_name", title: "Name" },
3911  {field: "remote_address", title: "Address",
3912  template: '# if (remote_address == "Not Listed" ) { # <span class="vsgSecondary">#= remote_address #</span> #} else {# #= remote_address # # } #' },
3913  {field: "amount", title: "Amount", width: "100px",
3914  headerAttributes: { style: "text-align: center"},
3915  template: '<div style="text-align:right;">#= kendo.toString(amount, "c") #</div>' },
3916  {field: "dfi_account", title: "Account" },
3917  {field: "addenda", title: "Addenda" }
3918  ]
3919  }).data("kendoGrid");
3920 
3921  <?php if ($advPerm) { ?>
3922  infoDialog = $("#infoDialog").kendoDialog({
3923  title: "ACH Batch Warning",
3924  modal: true,
3925  visible: false,
3926  resizeable: false,
3927  maxWidth: 750,
3928  show: function() {
3929  window_stack.push(function(e) {
3930  infoDialog.close(e);
3931  });
3932  },
3933  close: function() {
3934  window_stack.pop();
3935  },
3936  actions: [
3937  { text: "Ok", primary: true,
3938  action: function(e) {}
3939  }
3940  ]
3941  }).data("kendoDialog");
3942 
3943  cancelDialog = $("#cancelDialog").kendoDialog({
3944  visible: false,
3945  title: "Cancel ACH Transactions",
3946  content: "<p><strong>Continuing will cancel the selected transactions.</strong></p><p>Are you sure you want to continue?</p>",
3947  show: function(e) {
3948  // add the close function to the window stack
3949  window_stack.push(function(e) {
3950  cancelDialog.close(e);
3951  });
3952  },
3953  close: function(e) {
3954  // remove the close function from the window stack
3955  window_stack.pop();
3956  },
3957  actions: [{text: "Cancel"},
3958  {
3959  text: "Continue",
3960  action: function(e){
3961  // get list of txns to cancel
3962  var list = GetCheckedTxns();
3963  CancelACHTxns( list );
3964 
3965  // Returning false will prevent the closing of the dialog
3966  return true;
3967  },
3968  primary: true
3969  }]
3970  }).data("kendoDialog");
3971 
3972  batchDialog = $("#batchDialog").kendoDialog({
3973  visible: false,
3974  title: "Create ACH Upload File",
3975  content: "<p><strong>Continuing will place all the selected transactions into a single ACH upload file.</strong></p><p>Are you sure you want to continue?</p>",
3976  show: function(e) {
3977  // add the close function to the window stack
3978  window_stack.push(function(e) {
3979  batchDialog.close(e);
3980  });
3981  },
3982  close: function(e) {
3983  // remove the close function from the window stack
3984  window_stack.pop();
3985  },
3986  actions: [{text: "Cancel"},
3987  {
3988  text: "Continue",
3989  action: function(e){
3990  // get list of txns to cancel
3991  var list = GetCheckedTxns();
3992  BatchACHTxns( list );
3993 
3994  // Returning false will prevent the closing of the dialog
3995  return true;
3996  },
3997  primary: true
3998  }]
3999  }).data("kendoDialog");
4000  <?php } ?>
4001 
4002  detailWindow = $("#achDetailWindow").kendoWindow({
4003  modal: true,
4004  visible: false,
4005  resizable: false,
4006  minWidth: 300,
4007  maxWidth: 768,
4008  maxHeight: 900,
4009  title: "ACH Transaction Details",
4010  activate: function(e) {
4011  // add the close function to the window stack
4012  window_stack.push(function(e) {
4013  detailWindow.close(e);
4014  });
4015 
4016  $("#detailClose").click( function() {
4017  detailWindow.close();
4018  } );
4019 
4020  // bind the edit model to the template
4021  kendo.bind($("#achDetailWindow"), achDetailModel);
4022 
4023  },
4024  open: function(e) {
4025  this.wrapper.css({ top: 100 });
4026 
4027  // refresh the grid
4028  achDetailGrid.dataSource.read();
4029  },
4030  close: function(e) {
4031  // remove the close function from the window stack
4032  window_stack.pop();
4033  }
4034  }).data("kendoWindow");
4035 
4036  // set up the validator
4037  $.homecuValidator.setup({formErrorTitle: "Error Occured", formStatusField: "formACHStatus"});
4038 
4039  // read the initial data
4040  GetAvailableACHTxns();
4041 
4042  // Handle clicking on the overlay
4043  $(document).on("click", ".k-overlay", function (e) {
4044  if(window_stack.length > 0) {
4045  // get the close function from the stack, without poping it (the close will pop it)
4046  var fn = window_stack[window_stack.length - 1];
4047  var ret = fn(e);
4048  }
4049  });
4050 
4051  }); // end ready function
4052 
4053  <?php if ($advPerm) { ?>
4054  function SetAchInBatch( id, state ) {
4055  for ( var i = 0; i < achTransList.length; i++ ) {
4056  if ( achTransList[i].id == id || id == 0 ) {
4057  achTransList[i].inBatch = state;
4058 
4059  if ( id > 0 ) {
4060  break;
4061  }
4062  }
4063  }
4064  } // end SetAchInBatch
4065 
4066  function GetCheckedTxns() {
4067  var checkedTxns = [];
4068  for ( var i = 0; i < achTransList.length; i++ ) {
4069  if ( achTransList[i].inBatch ) {
4070  checkedTxns.push( achTransList[i].id * 1 );
4071  }
4072  }
4073 
4074  return checkedTxns.toString();
4075  }
4076 
4077  function TestActions() {
4078  // test if any checked
4079  var checked = false;
4080  $(".in-batch-checkbox").each( function() {
4081  var state = $(this).prop( "checked" );
4082  if ( state ) {
4083  checked = true;
4084  return false;
4085  }
4086  });
4087 
4088  var dropdownlist = $("#actionsDDL").data("kendoDropDownList");
4089  dropdownlist.enable(checked);
4090  } // end TestActions
4091  <?php } ?>
4092 
4093  function GetAvailableACHTxns() {
4094  var request = { operation: "achGetReady" };
4095 
4096  dsACHHelper.read( request );
4097  }
4098 
4099  <?php if ($advPerm) { ?>
4100  function CancelACHTxns( list ) {
4101  if ( list.length > 0 ) {
4102  var request = { operation: "achCancel", trans_list: list };
4103 
4104  dsACHHelper.read( request );
4105  }
4106  }
4107  function BatchACHTxns( list ) {
4108  if ( list.length > 0 ) {
4109  var request = { operation: "achMakeBatch", trans_list: list };
4110 
4111  dsACHHelper.read( request );
4112  }
4113  }
4114  <?php } ?>
4115 
4116  function GetAchDetail( id ) {
4117  // see if the detail is in the cache
4118  var foundInCache = false;
4119  for ( var i = 0; i < cachedDetailInfo.length; i++ ) {
4120  if ( cachedDetailInfo[i].header.id == id ) {
4121  lastDetailInfo = cachedDetailInfo[i];
4122  foundInCache = true;
4123  detailWindow.open().center();
4124  break;
4125  }
4126  }
4127 
4128  if ( !foundInCache ) {
4129  // get from server
4130  var request = { operation: "achGetDetail", trans_id: id };
4131 
4132  dsACHHelper.read( request );
4133  }
4134  }
4135  function ShowAchDetail( detailInfo ) {
4136  // save the detail info in a "cache"
4137  cachedDetailInfo.push( detailInfo );
4138 
4139  achDetailModel.SetDetail( detailInfo );
4140 
4141  // update the datasource
4142 
4143 
4144  detailWindow.open().center();
4145  }
4146 
4147  function ShowAvailableACH( achList ) {
4148  achTransList = [];
4149  for ( var i = 0; i < achList.length; i++ ) {
4150  var newObject = {id: achList[i].id,
4151  account: achList[i].account,
4152  amount: achList[i].amount * 1,
4153  eff_date: achList[i].eff_date,
4154  feature: achList[i].feature,
4155  group: achList[i].group,
4156  memo: achList[i].memo,
4157  inBatch: false,
4158  cancel: false};
4159 
4160  achTransList.push( newObject );
4161  }
4162 
4163  achActionGrid.dataSource.data(achTransList);
4164 
4165 
4166  } // end ShowAvailableACH
4167 
4168  <?php if ($advPerm) { ?>
4169  function HandleCancelResult( idString ) {
4170  var idList = idString.split( "," );
4171 
4172  // remove all txns in list from array
4173  var tempTransList = [];
4174  for ( var i = 0; i < achTransList.length; i++ ) {
4175  var index = idList.indexOf( achTransList[i].id );
4176  if ( index < 0 ) {
4177  tempTransList.push( achTransList[i] );
4178  }
4179  }
4180 
4181  achTransList = tempTransList;
4182 
4183  // update the grid
4184  var grid = $('#achActionGrid').data("kendoGrid");
4185  grid.dataSource.data(achTransList);
4186 
4187  // clear the checkbox
4188  $("#checkAllForBatch").prop( "checked", false );
4189 
4190  // re-disable the action drop-down list
4191  var dropdownlist = $("#actionsDDL").data("kendoDropDownList");
4192  dropdownlist.enable(false);
4193  }
4194  <?php } ?>
4195 
4196  function ShowBatchResults( batchId ) {
4197  if ( batchId.length > 0 ) {
4198  $("#achBatchId").val( batchId );
4199  $("#achBatchOperation").attr( "name", "page" );
4200  $("#achBatchOperation").val( "ach_show_report" );
4201  $("#achBatchDownload").val( "download" );
4202  $("#achPostForm").attr( "action", "<?php echo $pAdmEnv["menu_link"] ?>?ft=101" );
4203  $("#achPostForm").attr( "target", "achreport" );
4204  $("#achPostForm").submit();
4205  } else {
4206  // an error must have happened
4207  }
4208 
4209  // re-disable the action drop-down list
4210  var dropdownlist = $("#actionsDDL").data("kendoDropDownList");
4211  dropdownlist.enable(false);
4212  } // end ShowBatchResults
4213 
4214  </script>
4215  <?php PrintCommonStyle() ?>
4216  <style>
4217  #achBody .k-grid td {
4218  white-space: nowrap;
4219  text-overflow: ellipsis;
4220  }
4221  #achBody .k-grid .k-grid-content tr:hover {
4222  cursor: pointer;
4223  }
4224  .hcu-scrolling-dialog {
4225  overflow-y: auto;
4226  }
4227  </style>
4228  <div id="achDetailWindow" class="container-fluid hcu-scrolling-dialog" style="display:none;">
4229  <div class="h4"><div data-bind="text: achFeature" ></div></div>
4230  <div class="form-horizontal well well-sm">
4231  <div class="row">
4232  <div class="col-xs-3"><label>Group Owner</label></div>
4233  <div class="col-xs-9"><div data-bind="text: groupOwner" class="vsgSecondary"></div></div>
4234  </div>
4235  <div class="row">
4236  <div class="col-xs-3"><label>Group Name</label></div>
4237  <div class="col-xs-9"><div data-bind="text: groupName" class="vsgSecondary"></div></div>
4238  </div>
4239  <div class="row">
4240  <div class="col-xs-3"><label>Account</label></div>
4241  <div class="col-xs-9"><div data-bind="text: account" class="vsgSecondary"></div></div>
4242  </div>
4243  <div class="row">
4244  <div class="col-xs-3"><label>Effective Date</label></div>
4245  <div class="col-xs-9"><div data-bind="text: effDate" class="vsgSecondary"></div></div>
4246  </div>
4247  <div class="row">
4248  <div class="col-xs-3"><label>Memo</label></div>
4249  <div class="col-xs-9"><div data-bind="text: memo" class="vsgSecondary"></div></div>
4250  </div>
4251  <div id="achDetailGrid"></div>
4252  </div>
4253  <div class="hcu-template">
4254  <div class="hcu-edit-buttons k-state-default">
4255  <span class="hcu-icon-delete">
4256  </span>
4257  <a id="detailClose" style="cursor: pointer;" >Close</a>
4258  </div>
4259  </div>
4260  </div>
4261  <form id="achPostForm" method="post">
4262  <input type="hidden" id="achBatchId" name="batch_id" value="">
4263  <input type="hidden" id="achBatchOperation" name="operation" value="">
4264  <input type="hidden" id="achBatchDownload" name="download" value="">
4265  </form>
4266 
4267  <div id="cancelDialog"></div>
4268  <div id="batchDialog"></div>
4269  <div id="infoDialog"></div>
4270  <div id="achBody" class="container-fluid">
4271  <div id="formACHStatus" class="homecu-formStatus k-block k-error-colored" style="display:none;"></div>
4272  <br>
4273  <div class="form-horizontal well well-sm">
4274  <div class="row">
4275  <div class="col-xs-12"><h2>ACH Actions</h2></div>
4276  </div>
4277  <?php if ($advPerm) { ?>
4278  <div class="row">
4279  <div class="col-xs-3">&nbsp;</div>
4280  <div class="col-xs-3"><input id="actionsDDL" class="hcu-all-100"></div>
4281  </div>
4282  <div class="row">
4283  <div class="col-xs-12 ">&nbsp;</div>
4284  </div>
4285  <div class="row">
4286  <div class="col-xs-12 small vsgSecondary">A new ACH upload file report will automatically display in a new window. Make sure pop-ups are allowed for this website.</div>
4287  </div>
4288  <div class="row">
4289  <div class="col-xs-12 small vsgSecondary">Please check NACHA guidelines for specific timing requirements.</div>
4290  </div>
4291  <?php } else { ?>
4292  <div class="row">
4293  <div class="col-xs-12 small vsgSecondary">You have basic permissions. Creating an ACH file or canceling transactions is not available to you.</div>
4294  </div>
4295  <?php } ?>
4296  <div class="row">
4297  <div class="col-xs-12"><div id="achActionGrid"></div></div>
4298  </div>
4299  </div>
4300  </div>
4301 <?php
4302 
4303 } // end PrintActionPage
4304 
4305 /**
4306  * function PrintMainPage( $pAdmEnv, $pAdmVars)
4307  * Prints out the main page to the admin screen. Show ACH batch history. User can select one to
4308  * see the batch detail. Also links to download the ACH file or show the ACH report for any given
4309  * batch.
4310  *
4311  * @param object $pAdmEnv -- structure with common and environment information
4312  * @param array $pAdmVars -- the passed parameters for the operation
4313  */
4314 function PrintMainPage( $pAdmEnv, $pAdmVars ) {
4315  // get the feature names
4316  $achFeatureList = BuildFeatureNameLookup( $pAdmEnv["dbh"] );
4317 
4318  $self = $pAdmEnv["self"];
4319  $advPerm = $pAdmEnv["advPerm"];
4320  ?>
4321  <script type="text/javascript">
4322  <?php getShowWaitFunctions(); ?>
4323 
4324  var activeWindows = [];
4325  var achHistoryGrid;
4326  var achBatchItemGrid;
4327  var dsACHHelper;
4328  var achBatchList = []; // initialize list for display grid
4329  var achBatchItems = []; // for batch detail grid
4330  var achCurrSelectedBatchItem;
4331  var cancelDialog;
4332  var batchDetailWindow;
4333  var cachedDetailInfo = []; // cache to accumulate detail
4334  var lastDetailInfo; // the last detail requested that needs to be shown
4335  var window_stack = [];
4336  var featureList = <?php echo HCU_JsonEncode( $achFeatureList ) ?>;
4337  var commercialAccess = <?php echo $pAdmEnv['commercialAccess'] == 1 ? "true" : "false"; ?>;
4338  var removeFromBatchAllowed = false;
4339  var removeConfirm;
4340 
4341  $(document).ready(function() {
4342  dsACHHelper = new kendo.data.DataSource({
4343  autoSync: false,
4344  batch: false,
4345  transport: {
4346  read: {
4347  url: "<?php echo $self; ?>",
4348  dataType: "json",
4349  contentType: "application/x-www-form-urlencoded",
4350  type: "POST",
4351  cache: false
4352  }
4353  },
4354  schema: {
4355  parse: function(response) {
4356  // not showing data, so return empty array
4357  var display = [];
4358  return display;
4359  }
4360  },
4361  requestStart: function( e ) {
4362  $.homecuValidator.displayMessage( "", $.homecuValidator.settings.statusError );
4363  showWaitWindow();
4364  },
4365  requestEnd: function( e ) {
4366  var results = null;
4367 
4368  try {
4369  hideWaitWindow();
4370  if (e.hasOwnProperty("response")) {
4371  if (e.response.hasOwnProperty("results")) {
4372  var results = e.response.results;
4373  if (results.hasOwnProperty("errors")) {
4374  e.preventDefault();
4375  dsACHHelper.cancelChanges();
4376  throw results.errors;
4377  } else {
4378  if ( results.data ) {
4379  if ( results.data.achRemoveBatchTransactions ) {
4380  UpdateACHResults( results.data.achRemoveBatchTransactions, "achRemove" );
4381  batchDetailWindow.close();
4382  }
4383  if ( results.data.achSendNotifications ) {
4384  UpdateACHResults( results.data.achSendNotifications, "achNotify" );
4385  }
4386  if ( results.data.achGetBatches ) {
4387  ShowACHBatches( results.data.achGetBatches );
4388  }
4389  if ( results.data.achCreateFile ) {
4390  UpdateACHResults( results.data.achCreateFile, "achFile" );
4391  }
4392  if ( results.data.achGetBatchDetail ) {
4393  ShowACHBatchDetail( results.data.achGetBatchDetail );
4394  }
4395  }
4396  if (results.info) {
4397  // check if any extra info that needs to go into a dialog box.
4398  // the notification process can fail if the batch is successful,
4399  // show the success message in the popout, put the extra failure info
4400  // into a dialog.
4401 
4402  // splice out success message first
4403  var successMsg = results.info.splice(0, 1);
4404  $.homecuValidator.displayMessage( successMsg, $.homecuValidator.settings.statusInfo);
4405 
4406  // build warning dialog content
4407  var warningContent = "";
4408  for (var i = 0; i < results.info.length; i++) {
4409  if (i == 0) {
4410  // this is the initial warning, make bold
4411  warningContent += "<p><strong>" + results.info[i] + "</strong></p>";
4412  } else {
4413  // this is the information for the initial warning, regular text
4414  warningContent += "<p>" + results.info[i] + "</p>";
4415  }
4416  }
4417 
4418  if (warningContent !== "") {
4419  // slow down the popup so not everything will happen at the exact same time
4420  setTimeout(function() {
4421  infoDialog.content(warningContent);
4422  infoDialog.center();
4423  infoDialog.open();
4424  }, 750);
4425  }
4426  }
4427  }
4428  } else {
4429  throw "Unexpected results from server";
4430  }
4431  } else {
4432  throw "Invalid results from server";
4433  }
4434  } catch (error) {
4435  var theMessage;
4436  if ( error ) {
4437  theMessage = error.message ? error.message : error;
4438  } else {
4439  theMessage = "Unknown server error occurred during the operation";
4440  }
4441  $.homecuValidator.displayMessage( theMessage, $.homecuValidator.settings.statusError );
4442  }
4443  },
4444 
4445  // code to run if the request fails; the raw request and
4446  // status codes are passed to the function
4447  error: function( e ) {
4448  }
4449  });
4450 
4451  achHistoryGrid = $("#achHistoryGrid").kendoGrid({
4452  dataSource: achBatchList,
4453  pageable: {
4454  pageSize: 20
4455  },
4456  sortable: true,
4457  selectable: "cell",
4458  noRecords: {
4459  template: "<div class='vsgSecondary'>No available ACH upload files were found</div>"
4460  },
4461  columns: [
4462  {field: "batch_time", title: "Processed Date", width: "120px"},
4463  {field: "processed_by", title: "Processed By", width: "120px"},
4464  {field: "count", title: "Item Count", width: "70px"},
4465  <?php if ($advPerm) { ?>
4466  {title: "", template: "<div class='fa #= ((hasACHFile) ? 'fa-download' : 'fa-gears')#'></div>", width: "20px"},
4467  <?php } ?>
4468  {title: "", template: "<div class='fa fa-file-text-o' #= ((!hasACHReport) ? 'style=\"opacity: 0.38;\"' : '')#></div>", width: "20px"},
4469  <?php if ($advPerm) { ?>
4470  {title: "", template: "<div class='fa fa-envelope' #= ((!showSendNotify) ? 'style=\"opacity: 0.38;\"' : 'style=\"color: \\#f0ad4e;\"')#></div>",
4471  width: "20px", hidden: !commercialAccess}
4472  <?php } ?>
4473  ],
4474  change: function( e ) {
4475  e.preventDefault();
4476  var selectedCell = this.select();
4477  var cellIndex = selectedCell[0].cellIndex;
4478  var selectedRow = selectedCell.closest("tr");
4479 
4480  // save the batch id for later use
4481  achCurrSelectedBatchItem = achHistoryGrid.dataItem(selectedRow);
4482 
4483  switch(cellIndex) {
4484  <?php if ($advPerm) { ?>
4485  case 3:
4486  if ( achCurrSelectedBatchItem.hasACHFile ) {
4487  // download with a form post so browser handles the file save
4488  // set the batch id
4489  $("#achBatchId").val( achCurrSelectedBatchItem.batch_id );
4490  $("#achBatchOperation").attr( "name", "operation" );
4491  $("#achPostForm").attr( "action", "<?php echo $pAdmEnv["self"] ?>" );
4492  $("#achPostForm").attr( "target", "" );
4493  $("#achBatchOperation").val( "ach_download_file" );
4494  $("#achPostForm").submit();
4495  } else {
4496  // create it via ajax
4497  CreateACHFile( achCurrSelectedBatchItem.batch_id );
4498  }
4499  break;
4500  case 5:
4501  // only allow click if the process status is 20
4502  // this mean the batch has been created and the batch file exists
4503  // have not been sent for some reason.
4504  if ( achCurrSelectedBatchItem.showSendNotify ) {
4505  SendNotifications( achCurrSelectedBatchItem.batch_id );
4506  }
4507  break;
4508  case 4:
4509  <?php } else { ?>
4510  case 3:
4511  <?php } ?>
4512  // viewing ach report - make sure report exists
4513  if ( achCurrSelectedBatchItem.hasACHReport ) {
4514  // open new window to see the report
4515  $("#achBatchId").val( achCurrSelectedBatchItem.batch_id );
4516  $("#achBatchOperation").attr( "name", "page" );
4517  $("#achBatchOperation").val( "ach_show_report" );
4518  $("#achPostForm").attr( "action", "<?php echo $pAdmEnv["menu_link"] ?>?ft=101" );
4519  $("#achPostForm").attr( "target", "achreport" );
4520  $("#achPostForm").submit();
4521  }
4522  break;
4523  default:
4524  // get batch detail
4525  GetBatchDetail( achCurrSelectedBatchItem.batch_id );
4526  break;
4527  }
4528 
4529  selectedCell.removeClass( "k-state-selected" );
4530  },
4531  dataBound: function( e ) {
4532 
4533  }
4534  }).data("kendoGrid");
4535 
4536  // set up the grid
4537  achBatchItemGrid = $("#achBatchItemGrid").kendoGrid({
4538  dataSource: new kendo.data.DataSource({
4539  transport: {
4540  read: function( options ) {
4541  var data = achBatchItems;
4542  options.success(data);
4543  }
4544  }
4545  }),
4546  sortable: true,
4547  selectable: "row",
4548  noRecords: {
4549  template: "<div class='vsgSecondary'>No available ACH transactions were found</div>"
4550  },
4551  columns: [
4552  <?php if ($advPerm) { ?>
4553  {field: "remove", title: "Remove", width: "30px",
4554  sortable: false,
4555  template: "<input type=\"checkbox\" onclick=\"SelectRemove(this);\">",
4556  headerTemplate: "<input type=\"checkbox\" onclick=\"SelectRemoveAll(this);\">"
4557  },
4558  <?php } ?>
4559  {field: "group", title: "Group" },
4560  {field: "feature", title: "Type",
4561  template: "#= ( featureList[feature] === undefined ) ? feature : featureList[feature] #"},
4562  {field: "amount", title: "Amount" },
4563  {field: "account", title: "Account" },
4564  {field: "memo", title: "Memo" }
4565  ],
4566  change: function(e) {
4567  e.preventDefault();
4568 
4569  var selectedRow = this.select();
4570  <?php
4571  // this is part of the remove transactions from batch
4572  // feature. only users with advanced permissions are allowed.
4573  if ($advPerm) { ?>
4574  if (removeFromBatchAllowed) {
4575  var selectedInput = this.select().find("td:eq(0) input[type=\"checkbox\"]");
4576  selectedInput.click();
4577  }
4578  <?php } ?>
4579 
4580  selectedRow.removeClass( "k-state-selected" );
4581  },
4582  dataBound: function( e ) {
4583  // because the sort re-binds all data, we must reset the checkboxes
4584  // and disable the remove button
4585  this.wrapper.find("input[type=\"checkbox\"]").each(function(e) {
4586  $(this).prop("checked", false);
4587  });
4588  }
4589  }).data("kendoGrid");
4590 
4591  batchDetailWindow = $("#batchItems").kendoWindow({
4592  modal: true,
4593  visible: false,
4594  resizable: false,
4595  minWidth: 300,
4596  maxWidth: 768,
4597  maxHeight: 900,
4598  title: "ACH Upload File Details.",
4599  activate: function(e) {
4600  // add the close function to the window stack
4601  window_stack.push(function(e) {
4602  batchDetailWindow.close(e);
4603  });
4604 
4605  // setup validator to show in this window
4606  $.homecuValidator.setup({
4607  formStatusField: "batchItemForm"
4608  });
4609 
4610  $("#lnkRemove").off();
4611  $("#lnkRemove").click( function() {
4612  // this is part of the remove transactions from batch
4613  // feature. only master users are allowed.
4614  var transListRemove = GetSelectedTransactions();
4615  <?php if ($advPerm) { ?>
4616  if (transListRemove != "") {
4617  removeConfirm.open();
4618  }
4619  <?php } ?>
4620  } );
4621 
4622  $("#batchItemsClose").off();
4623  $("#batchItemsClose").click(function(e) {
4624  // close window
4625  batchDetailWindow.close();
4626  });
4627 
4628  // set the title
4629  this.title( "ACH Upload File Details: " + achCurrSelectedBatchItem.batch_time );
4630  },
4631  open: function(e) {
4632  this.wrapper.css({ top: 100 });
4633 
4634  // refresh the grid
4635  achBatchItemGrid.dataSource.read();
4636 
4637  <?php if ($advPerm) { ?>
4638  if (removeFromBatchAllowed) {
4639  achBatchItemGrid.showColumn(0);
4640  $("#removeTranSecondaryText").show();
4641  $("#lnkRemove").show();
4642  } else {
4643  achBatchItemGrid.hideColumn(0);
4644  $("#removeTranSecondaryText").hide();
4645  $("#lnkRemove").hide();
4646  }
4647  <?php } ?>
4648  },
4649  close: function(e) {
4650  //disable link
4651  $("#lnkRemove").addClass("vsgDisabled");
4652 
4653  // remove the close function from the window stack
4654  window_stack.pop();
4655 
4656  $.homecuValidator.setup({
4657  formStatusField: "formACHStatus"
4658  });
4659  }
4660  }).data("kendoWindow");
4661 
4662  <?php if ($advPerm) { ?>
4663  removeConfirm = $("#removeConfirmDialog").kendoDialog({
4664  title: "Remove Transactions",
4665  content: $("#removeConfirmDialogTemplate").html(),
4666  minWidth: 300,
4667  maxWidth: 500,
4668  visible: false,
4669  resizable: false,
4670  open: function(e) {
4671  window_stack.push(function(e) {
4672  removeConfirm.close(e);
4673  });
4674  },
4675  close: function(e) {
4676  window_stack.pop();
4677  },
4678  actions: [
4679  { text: "No",
4680  action: function(e) { }
4681  },
4682  { text: "Yes", primary: true,
4683  action: RemoveTransactions
4684  }
4685  ]
4686  }).data("kendoDialog");
4687  <?php } ?>
4688 
4689  infoDialog = $("#infoDialog").kendoDialog({
4690  title: "ACH Batch Warning",
4691  modal: true,
4692  visible: false,
4693  resizeable: false,
4694  maxWidth: 750,
4695  show: function() {
4696  window_stack.push(function(e) {
4697  infoDialog.close(e);
4698  });
4699  },
4700  close: function() {
4701  window_stack.pop();
4702  },
4703  actions: [
4704  { text: "Ok", primary: true,
4705  action: function(e) {}
4706  }
4707  ]
4708  }).data("kendoDialog");
4709 
4710  // get the completed batches
4711  GetACHBatches();
4712 
4713  // Handle clicking on the overlay
4714  $(document).on("click", ".k-overlay", function (e) {
4715  if(window_stack.length > 0) {
4716  // get the close function from the stack, without poping it (the close will pop it)
4717  var fn = window_stack[window_stack.length - 1];
4718  var ret = fn(e);
4719  }
4720  });
4721 
4722  }); // end ready function
4723 
4724  function GetSelectedTransactions() {
4725  var transListRemove = "";
4726 
4727  achBatchItemGrid.wrapper.find("tbody tr").each(function(e, i) {
4728  var isChecked = $(this).find("td:eq(0) input").prop("checked");
4729  var dataItem = achBatchItemGrid.dataItem(i);
4730  if (isChecked) {
4731  transListRemove += dataItem.id;
4732  transListRemove += ",";
4733  }
4734  });
4735 
4736  transListRemove = transListRemove.replace(/(,$)/g, '');
4737  transListRemove = transListRemove.trim();
4738  return transListRemove;
4739  }
4740 
4741  function RemoveTransactions() {
4742  var batchId = achCurrSelectedBatchItem.batch_id;
4743  var transListRemove = GetSelectedTransactions();
4744 
4745  var request = {
4746  operation: "achRemoveBatchTransactions",
4747  batch_id: batchId,
4748  trans_list: transListRemove
4749  };
4750 
4751  dsACHHelper.read( request );
4752 
4753  }
4754 
4755  function SelectRemoveAll(e) {
4756  var input = $(e);
4757  var inputChecked = input.prop("checked");
4758 
4759  achBatchItemGrid.wrapper.find("tbody tr").each(function(e) {
4760  $(this).find("td:eq(0) input[type=\"checkbox\"]").prop("checked", inputChecked);
4761  });
4762 
4763  if (inputChecked) {
4764  $("#lnkRemove").removeClass("vsgDisabled");
4765  } else {
4766  $("#lnkRemove").addClass("vsgDisabled");
4767  }
4768  }
4769 
4770  function SelectRemove(e) {
4771  // get header input
4772  var headerInput = achBatchItemGrid.wrapper.find("thead tr th:eq(0) input[type=\"checkbox\"]");
4773  var headerChecked = headerInput.prop("checked");
4774 
4775  // get number of total records and number of total checked records
4776  var numRows = numChecked = 0;
4777  achBatchItemGrid.wrapper.find("tbody tr").each(function(e) {
4778  var isChecked = $(this).find("td:eq(0) input[type=\"checkbox\"]").prop("checked");
4779 
4780  if (isChecked) {
4781  numChecked ++;
4782  }
4783  numRows ++;
4784  });
4785 
4786  // set header checkbox based on all rwos checked
4787  if (numChecked == numRows) {
4788  headerInput.prop("checked", true);
4789  } else {
4790  headerInput.prop("checked", false);
4791  }
4792 
4793  if (numChecked > 0) {
4794  $("#lnkRemove").removeClass("vsgDisabled");
4795  } else {
4796  $("#lnkRemove").addClass("vsgDisabled");
4797  }
4798  }
4799 
4800  function GetACHBatches() {
4801  var request = { operation: "achGetBatches" };
4802 
4803  dsACHHelper.read( request );
4804  }
4805 
4806  function ShowACHBatches( achList ) {
4807  achBatchList = [];
4808  for ( var i = 0; i < achList.length; i++ ) {
4809  // the user may only resend notificaiton emails IF:
4810  // 1: the credit union has ACH features set up for the banking menu
4811  // 2: the ach batch file was successfully created
4812  // 3: the ach batch process status is 20: another check meaning the batch
4813  // file has been created successfully.
4814  var setShowSendNotify =
4815  (achList[i].notify > 0) &&
4816  (achList[i].has_ach_file);
4817 
4818  var newObject = {batch_id: achList[i].batch_id,
4819  count: achList[i].count,
4820  processed_by: achList[i].processed_by,
4821  batch_time: achList[i].display,
4822  hasACHFile: achList[i].has_ach_file,
4823  hasACHReport: achList[i].has_ach_report,
4824  showSendNotify: setShowSendNotify
4825  };
4826  achBatchList.push( newObject );
4827  }
4828 
4829  achHistoryGrid.dataSource.data(achBatchList);
4830 
4831  } // end ShowACHBatches
4832  function GetBatchDetail( id ) {
4833  var request = { operation: "achGetBatchDetail", batch_id: id };
4834 
4835  dsACHHelper.read( request );
4836  }
4837  function ShowACHBatchDetail( batchData ) {
4838  achBatchItems = batchData.records;
4839  removeFromBatchAllowed = batchData.removeFromBatchAllowed;
4840 
4841  // open the window and show all the data
4842  batchDetailWindow.open().center();
4843 
4844  } // end ShowACHBatches
4845 
4846  <?php if ($advPerm) { ?>
4847  function CreateACHFile( id ) {
4848  var request = { operation: "achCreateFile", batch_id: id };
4849 
4850  dsACHHelper.read( request );
4851  } // end CreateACHFile
4852  function UpdateACHResults( data, updateType ) {
4853  // if we got here there was no error
4854  var batchId = data.hasOwnProperty("batch_id") ? data.batch_id : "";
4855  var batchRecord = data.hasOwnProperty("batch_record") ? data.batch_record : [];
4856 
4857  var removeIndex = -1;
4858  for ( var i = 0; i < achBatchList.length; i++ ) {
4859  if ( achBatchList[i].batch_id == batchId ) {
4860  // This needs to be updated to use the
4861  // batchRecord. It will require changes
4862  // the server code.
4863  if ( updateType == "achNotify" ) {
4864  achBatchList[i].showSendNotify = false;
4865  achBatchList[i].notify = 0;
4866  }
4867 
4868  if ( updateType == "achRemove" || updateType == "achFile" ) {
4869  // batchRecord is empty array if all transactions
4870  // were removed. Remove this record from the grid.
4871  if (batchRecord.length == 0) {
4872  achBatchList.splice(i, 1);
4873  break;
4874  }
4875 
4876  // If record is not removed, update data
4877  achBatchList[i].count = batchRecord.count;
4878  achBatchList[i].notify = batchRecord.notify;
4879  achBatchList[i].hasACHFile = batchRecord.has_ach_file;
4880  achBatchList[i].hasACHReport = batchRecord.has_ach_report;
4881  achBatchList[i].showSendNotify = batchRecord.notify > 0;
4882  }
4883  }
4884  }
4885 
4886  achHistoryGrid.dataSource.data(achBatchList);
4887  } // end UpdateACHResults
4888 
4889  function SendNotifications( id ) {
4890  var request = { operation: "achSendNotifications", batch_id: id };
4891 
4892  dsACHHelper.read( request );
4893  }
4894  <?php } ?>
4895 
4896  </script>
4897  <?php PrintCommonStyle() ?>
4898  <style>
4899  #achBody .k-grid td {
4900  white-space: nowrap;
4901  text-overflow: ellipsis;
4902  }
4903  #achBody .k-grid .k-grid-content tr:hover {
4904  cursor: pointer;
4905  }
4906  .hcu-scrolling-dialog {
4907  overflow-y: auto;
4908  }
4909  </style>
4910  <?php if ($advPerm) { ?>
4911  <script type="text/template" id="removeConfirmDialogTemplate">
4912  <div>
4913  <p>
4914  <strong>
4915  You are about to remove these transactions from this ACH upload file.<br><br>
4916  This action cannot be undone. If you have already successfully submitted the ACH upload file you should not be doing this.<br><br>
4917  New ACH upload and report files will be generated automatically.
4918  </strong>
4919  </p>
4920  <p>Do you wish to continue?</p>
4921  </div>
4922  </script>
4923  <?php } ?>
4924  <div id="infoDialog"></div>
4925  <?php if ($advPerm) { ?>
4926  <div id="removeConfirmDialog"></div>
4927  <?php } ?>
4928  <div id="batchItems" class="container-fluid hcu-scrolling-dialog">
4929  <div id="batchItemForm"></div>
4930  <div class="row">
4931  <div class="col-xs-12 hcu-secondary">
4932  <?php if ($advPerm) { ?>
4933  <div class="small vsgSecondary" id="removeTranSecondaryText">
4934  <span>To remove transactions from this ACH upload file, use the checkboxes to select the records you wish to remove and click "Remove" below.</span>
4935  </div>
4936  <?php } ?>
4937  </div>
4938  </div>
4939  <div id="achBatchItemGrid"></div>
4940  <div class="hcu-template">
4941  <div class="hcu-edit-buttons k-state-default">
4942  <?php if ($advPerm) { ?>
4943  <span class="hcu-icon-delete">
4944  <a href="#" id="lnkRemove" class="vsgDisabled">Remove</a>
4945  </span>
4946  <?php } ?>
4947  <button href="##" id="batchItemsClose" class="k-button k-primary">
4948  Close
4949  </button>
4950  </div>
4951  </div>
4952  </div>
4953  <form id="achPostForm" action="<?php echo $self; ?>" method="post">
4954  <input type="hidden" id="achBatchId" name="batch_id" value="">
4955  <input type="hidden" id="achBatchOperation" name="operation" value="ach_create_file">
4956  <input type="hidden" name="download_type" value="ach">
4957  </form>
4958  <div id="achBody" class="container-fluid">
4959  <div id="formACHStatus" class="homecu-formStatus k-block k-error-colored" style="display:none;"></div>
4960  <br>
4961 
4962  <div class="form-horizontal well well-sm">
4963  <div class="row">
4964  <div class="col-xs-12"><h2>ACH History</h2></div>
4965  </div>
4966  <div class="row">
4967  <div class="col-xs-12 hcu-secondary">
4968  <div class="small vsgSecondary">Click row to see detail.</div>
4969  <!-- only show this label if credit union has access to commercial ach -->
4970  <?php if ($advPerm) { ?>
4971  <?php if ($pAdmEnv['commercialAccess']): ?>
4972  <div class="small vsgSecondary">Click the <span class="fa fa-envelope" style="color: #f0ad4e;"></span> to restart the email notification process.</div>
4973  <?php endif; ?>
4974  <?php } else { ?>
4975  <div class="small vsgSecondary">You have basic permissions. The downloading and resending email actions are not available to you.</div>
4976  <?php } ?>
4977  </div>
4978  </div>
4979  <div class="row">
4980  <div class="col-xs-12"><div id="achHistoryGrid"></div></div>
4981  </div>
4982  <?php
4983  if ( getenv( "DEVMODE" ) == 1 ) {
4984  ?>
4985  <style>
4986  div.k-dropzone em {
4987  visibility: visible;
4988  }
4989  </style>
4990  <div class="k-block" style="margin: 1em; border: 5px ridge lightblue; border-radius: 10px;">
4991  <div style="margin:2px;">ACH File Validator (developer tool)</div>
4992  <form method="post" action="<?php echo $self; ?>" >
4993  <input type="hidden" name="page" value="validate">
4994  <div class="demo-section k-content">
4995  <input name="achFile" id="files" type="file" />
4996  <label>Show Raw File Output <input type="checkbox" name="show_raw" id="show_raw" /></label>
4997  <p style="text-align: right">
4998  <input type="submit" value="Submit" class="k-button k-primary" />
4999  </p>
5000  </div>
5001  </form>
5002  </div>
5003  <script>
5004  $(document).ready(function() {
5005  $("#files").kendoUpload({
5006  multiple: false,
5007  validation: {
5008  allowedExtensions: [".ACH", ".ach", ".txt"],
5009  },
5010  localization: {
5011  select: 'Select File To Upload',
5012  remove: '',
5013  cancel: ''
5014  }
5015  });
5016  });
5017  </script>
5018  <?php
5019  }
5020  ?>
5021  </div>
5022  </div>
5023 <?php
5024 
5025 } // end PrintMainPage
5026 
5027 
5028 /**
5029  * function PrintAchReport( $pAdmEnv, $pAdmVars )
5030  * This prints out the report page. It is inteneded to be a stand-alone page with limited interaction
5031  * with the rest of admin. User closes to be done with it. Print and Download are the other two choices.
5032  *
5033  * @param object $pAdmEnv -- structure with common and environment information
5034  * @param array $pAdmVars -- the passed parameters for the operation
5035  */
5036 function PrintAchReport( $pAdmEnv, $pAdmVars ) {
5037  try {
5038  // read the ach report file
5039  $cu = strtolower( $pAdmEnv["Cu"] );
5040  $advPerm = $pAdmEnv["advPerm"];
5041 
5042  $payloadInfo = HCU_PayloadDecode( $pAdmEnv["Cu"], $pAdmVars["batch_id"] );
5043 
5044  // use the batch timestamp to figure out the creation date/time and filename
5045  $myDateTime = new DateTime( $payloadInfo["batch_date"] );
5046 
5047  // convert to local time for the filename
5048  $tz = GetCreditUnionTimezone( $pAdmEnv["dbh"], $pAdmEnv["Cu"] );
5049 
5050  $myDateTime->setTimezone(new DateTimeZone($tz));
5051  $batchDay = $myDateTime->format("ymd");
5052  $batchHour = $myDateTime->format("His");
5053  $batchPreName = "Batch";
5054  $batchPath = "/home/{$cu}/admin/ach/";
5055 
5056  $achFileName = "{$batchPreName}_{$batchDay}_{$batchHour}.txt";
5057 
5058  // check file exists
5059  if (!file_exists("{$batchPath}{$achFileName}")) {
5060  ShowErrorPage( "Unable to find ACH report file" );
5061  exit;
5062  }
5063 
5064  // get the file
5065  $fileString = file_get_contents( "{$batchPath}{$achFileName}" );
5066  if ( $fileString === false ) {
5067  ShowErrorPage( "Unable to find ACH report file" );
5068  exit;
5069  }
5070 
5071  } catch (Exception $ex) {
5072  ShowErrorPage( "Unexpected error trying to show ACH report: {$ex->getMessage()}" );
5073  }
5074 
5075 ?>
5076  <html>
5077  <head>
5078  <title><?php echo $achFileName ?></title>
5079 <?php
5080  // add the include files to get the environment
5081  setIncludeFiles(false, false, GetAdminDefaultKendoStyle(), false, $pAdmEnv);
5082 
5083 ?>
5084  </head>
5085  <style>
5086  @media print {
5087  .ach-report-body {
5088  background-color: white;
5089  overflow: hidden;
5090  }
5091  .ach-report-wrapper {
5092  margin: 1em;
5093  overflow: hidden;
5094  }
5095  .ach-pre-scrollable pre {
5096  background-color: white;
5097  border: none;
5098  page-break-inside: auto;
5099  }
5100  }
5101  @media screen {
5102  .ach-report-body {
5103  background-color: white;
5104  overflow: hidden;
5105  }
5106  .ach-report-wrapper {
5107  margin: 1em;
5108  overflow: hidden;
5109  }
5110  .ach-pre-scrollable {
5111  overflow: auto;
5112  -ms-word-wrap: normal;
5113  word-wrap: normal;
5114  overflow-wrap: normal;
5115  white-space: pre;
5116  max-height: 90%;
5117  height: 90%;
5118  }
5119  .ach-pre-scrollable pre {
5120  background-color: white;
5121  margin-bottom: 4em;
5122  }
5123  }
5124  </style>
5125  <body class="ach-report-body">
5126  <script type="text/javascript">
5127  $(document).ready(function() {
5128  $("#printReport").click(function() {
5129  window.print();
5130  });
5131  $("#closeReport").click(function() {
5132  window.close();
5133  });
5134  <?php if ($advPerm) { ?>
5135  $("#downloadReport").click(function() {
5136  $("#achReportForm").submit();
5137  });
5138  <?php } ?>
5139 <?php
5140  if ( isset( $pAdmVars["download"] ) && $pAdmVars["download"] == "download" ) {
5141 ?>
5142  $("#achPostForm").submit();
5143 <?php
5144  }
5145 ?>
5146  });
5147  </script>
5148  <?php if ($advPerm) { ?>
5149  <form id="achReportForm" action="<?php echo $pAdmEnv["menu_link"]; ?>?ft=100" method="post">
5150  <input type="hidden" id="achBatchId" name="batch_id" value="<?php echo $pAdmVars["batch_id"]; ?>">
5151  <input type="hidden" id="achBatchOperation" name="operation" value="ach_download_file">
5152  <input type="hidden" name="download_type" value="report">
5153  </form>
5154  <?php } ?>
5155  <div class="container-fluid ach-report-wrapper">
5156  <div class="form-horizontal well well-sm hidden-print">
5157  <div class="row">
5158  <div class="col-xs-12 h4 text-center">Odyssey ACH Report</div>
5159  </div>
5160  <div class="row">
5161  <div class="col-xs-12 text-center">
5162  <div class="btn-group" role="group" aria-label="...">
5163  <?php if ($advPerm) { ?>
5164  <button id="downloadReport" class="btn">Download</button>
5165  <?php } ?>
5166  <button id="closeReport" class="btn">Close</button>
5167  <button id="printReport" class="btn">Print</button>
5168  </div>
5169  </div>
5170  </div>
5171  </div>
5172  <div class="ach-pre-scrollable">
5173  <pre>
5174  <?php echo $fileString ?>
5175  </pre>
5176  </div>
5177  </div>
5178 <?php
5179 
5180  if ( isset($pAdmVars["download"]) && $pAdmVars["download"] == "download" ) {
5181  // start the download
5182 ?>
5183  <form id="achPostForm" action="<?php echo $pAdmEnv["menu_link"]; ?>?ft=100" method="post">
5184  <input type="hidden" id="achBatchId" name="batch_id" value="<?php echo $pAdmVars["batch_id"]; ?>">
5185  <input type="hidden" id="achBatchOperation" name="operation" value="ach_download_file">
5186  <input type="hidden" name="download_type" value="ach">
5187  </form>
5188 <?php
5189  }
5190 ?>
5191  </body>
5192  </html>
5193 <?php
5194 } // end PrintAchReport
5195 
5196 /**
5197  * function PrintValidate( $pAdmEnv, $pAdmVars )
5198  * This is a validator that will examine an ACH file.
5199  *
5200  * @param object $pAdmEnv -- structure with common and environment information
5201  * @param array $pAdmVars -- the passed parameters for the operation
5202  */
5203 function PrintValidate( $pAdmEnv, $pAdmVars ) {
5204 ?>
5205  <script type="text/javascript">
5206  $(document).ready(function() {
5207  $(".returnLink").click( function() {
5208  $("#redirectForm").submit();
5209  } );
5210  });
5211  </script>
5212  <style>
5213  </style>
5214  <div class="container-fluid">
5215  <form id="redirectForm" action="<?php echo $pAdmEnv["self"]; ?>" method="post">
5216  <input type="hidden" name="page" value="">
5217  </form>
5218  <div><a class="returnLink" style="cursor:pointer;">Return</a></div>
5219  <div class="">
5220  <div id="formValidateDiv" class="k-block k-error-colored" style="display:none">
5221  </div>
5222  </div>
5223 <?php
5224  $name = "Filename Not Found";
5225  $found = false;
5226 
5227  try {
5228  if ( $_FILES['achFile']['error'] == UPLOAD_ERR_OK ) {
5229  $tmp_name = $_FILES["achFile"]["tmp_name"];
5230  // basename() may prevent filesystem traversal attacks;
5231  // further validation/sanitation of the filename may be appropriate
5232  $name = basename($_FILES["achFile"]["name"]);
5233 
5234  // sanitize the name (i.e. only use it if it is safe
5235  if ( !preg_match("`^[-0-9A-Z_\.]+$`i",$name) ) {
5236  $name = "Your ACH File";
5237  }
5238 
5239  // read the contents
5240  $fileContents = file( $tmp_name, FILE_IGNORE_NEW_LINES );
5241 
5242  // remove the file since we don't need it
5243  unlink( $tmp_name );
5244 
5245  $found = true;
5246  }
5247 
5248  require_once( dirname(__FILE__) . "/../library/aACHValidate.i" );
5249 
5250  // initialize arrays
5251  $fileHeaderArray = array();
5252  $companyHeaderArray = array();
5253  $entryDetailArray = array();
5254 
5255  $printWarningList = array();
5256  $printErrorList = array();
5257  if ( !$found ) {
5258  $printErrorList[] = "ACH File Validate Error - Error uploading file. Please try again.";
5259  }
5260 
5261  // quick check to make sure file has valid record types
5262  $fileRecordCount = 0;
5263  $validRecordTypes = ["1", "5", "6", "7", "8", "9"];
5264  for ( $i = 0; $i < count( $fileContents ); $i++ ) {
5265  $recordType = substr( $fileContents[$i], 0, 1 );
5266 
5267  if ( !in_array( $recordType, $validRecordTypes ) ) {
5268  $printErrorList[] = "Unexpected ACH Record Type: $recordType";
5269  break;
5270  }
5271 
5272  // sanitize to make sure only valid characters are in file
5273  if ( !preg_match( ALPHAMERIC_STRING, $fileContents[$i] ) ) {
5274  $printErrorList[] = "Invalid ACH character on line $i";
5275  $printErrorList[] = "**{$fileContents[$i]}**";
5276  // don't do more tests if there was an invalid character
5277  break;
5278  }
5279 
5280  $testString = preg_replace( "/[\r\n]+/","", $fileContents[$i] );
5281  if ( strlen( $testString ) != 94 ) {
5282  $checkString = str_replace( " ", "_", $testString );
5283  $printErrorList[] = "ACH Record length not 94 characters. Record: $i (zero-based)" . strlen( $checkString );
5284  $printErrorList[] = "ACH Record: " . $checkString;
5285  break;
5286  }
5287 
5288  $fileRecordCount++; // this is tested at the end
5289  }
5290 
5291  // set the line counter to handle the variable amount of data
5292  $fileContentsLine = 0;
5293 
5294  if ( !count( $printErrorList ) ) {
5295  // Line 0 - file header
5296  $extractionResults = GetFileHeaderRecord( $pAdmEnv, $fileContents[$fileContentsLine++] );
5297  if ( $extractionResults["code"] != "000" ) {
5298  $printErrorList[] = "ACH File extraction error";
5299  $printErrorList[] = $extractionResults["error"][0]; // only one expected
5300  $printWarningList[] = $extractionResults["warning"][0]; // only one expected
5301  } else {
5302  $fileHeaderArray = $extractionResults["data"];
5303 
5304  $results = ValidateFileHeaderRecord( $pAdmEnv, $fileHeaderArray );
5305  if ( count( $results["error"] ) ) {
5306  $printErrorList[] = "Invalid ACH File - File Header Record";
5307 
5308  for ( $e = 0; $e < count($results["error"]); $e++ ) {
5309  $printErrorList[] = $results["error"][$e];
5310  }
5311  }
5312  if ( count( $results["warning"] ) ) {
5313  $printWarningList[] = "ACH File Header Record Warnings";
5314 
5315  for ( $e = 0; $e < count($results["warning"]); $e++ ) {
5316  $printWarningList[] = $results["warning"][$e];
5317  }
5318  }
5319 
5320  if ( count( $fileHeaderArray ) ) {
5321  print "<div class='k-block' style='background-color: lightcyan;'>";
5322  print "<div class='h4'>File Header</div>";
5323 
5324  $arrayKeys = array_keys( $fileHeaderArray );
5325  for ( $i = 0; $i < count( $arrayKeys ); $i++ ) {
5326  print "<b>{$arrayKeys[$i]}</b>: <span style='background-color:white'>{$fileHeaderArray[$arrayKeys[$i]]}</span><br>";
5327  }
5328  print "</ul>";
5329  print "</div>";
5330  }
5331  }
5332  }
5333 
5334  // initialize some counters for validating the complete file
5335  $fileBatchCount = 0;
5336  $fileEntryAddendaCount = 0;
5337  $fileEntryHash = 0;
5338  $fileTotalDebitEntryDollarAmount = 0;
5339  $fileTotalCreditEntryDollarAmount = 0;
5340 
5341  // do this loop for each company
5342  do {
5343  $companyHeaderArray = null;
5344  if ( !count( $printErrorList ) ) {
5345  // Line 1 - company header
5346  $extractionResults = GetCompanyHeaderRecord( $pAdmEnv, $fileContents[$fileContentsLine++] );
5347  if ( $extractionResults["code"] != "000" ) {
5348  $printErrorList[] = "ACH File extraction error";
5349  $printErrorList[] = $extractionResults["error"];
5350  } else {
5351  $companyHeaderArray = $extractionResults["data"];
5352 
5353  $results = ValidateCompanyHeaderRecord( $pAdmEnv, $companyHeaderArray );
5354  if ( count( $results["error"] ) ) {
5355  $printErrorList[] = "Invalid ACH File - Company Header Record";
5356 
5357  for ( $e = 0; $e < count($results["error"]); $e++ ) {
5358  $printErrorList[] = $results["error"][$e];
5359  }
5360  }
5361  if ( count( $results["warning"] ) ) {
5362  $printWarningList[] = "ACH Company Header Record Warnings";
5363 
5364  for ( $e = 0; $e < count($results["warning"]); $e++ ) {
5365  $printWarningList[] = $results["warning"][$e];
5366  }
5367  }
5368 
5369  if ( count( $companyHeaderArray ) ) {
5370  print "<div class='k-block k-info-colored'>";
5371  print "<div class='h4'>Company Header</div>";
5372  $arrayKeys = array_keys( $companyHeaderArray );
5373  for ( $i = 0; $i < count( $arrayKeys ); $i++ ) {
5374  print "<b>{$arrayKeys[$i]}</b>: <span style='background-color:white'>{$companyHeaderArray[$arrayKeys[$i]]}</span><br>";
5375  }
5376  print "</ul>";
5377  print "</div>";
5378  }
5379  }
5380  }
5381 
5382  // indent all the company info
5383  print "<div style='margin-left: 2em'>";
5384 
5385  // do this loop for each record for the company
5386  $companyEntryAddendaCount = 0;
5387  $companyEntryHash = 0;
5388  $companyTotalDebitEntryDollarAmount = 0;
5389  $companyTotalCreditEntryDollarAmount = 0;
5390  do {
5391  if ( !count( $printErrorList ) ) {
5392  // entry detail
5393  $extractionResults = GetEntryDetailRecord( $pAdmEnv, $fileContents[$fileContentsLine++] );
5394  if ( $extractionResults["code"] != "000" ) {
5395  $printErrorList[] = "ACH File extraction error";
5396  $printErrorList[] = $extractionResults["error"];
5397  } else {
5398  $entryDetailArray = $extractionResults["data"];
5399 
5400  $results = ValidateEntryDetailRecord( $pAdmEnv, $entryDetailArray, $companyHeaderArray );
5401  if ( count( $results["error"] ) ) {
5402  $printErrorList[] = "Invalid ACH File - Entry Detail Record";
5403 
5404  for ( $e = 0; $e < count($results["error"]); $e++ ) {
5405  $printErrorList[] = $results["error"][$e];
5406  }
5407  }
5408  if ( count( $results["warning"] ) ) {
5409  $printWarningList[] = "ACH Entry Detail Record Warnings";
5410 
5411  for ( $e = 0; $e < count($results["warning"]); $e++ ) {
5412  $printWarningList[] = $results["warning"][$e];
5413  }
5414  }
5415  if ( count( $entryDetailArray ) ) {
5416  print "<div class='k-block'>";
5417  print "<div class='h4'>Entry Detail</div>";
5418  $arrayKeys = array_keys( $entryDetailArray );
5419  for ( $i = 0; $i < count( $arrayKeys ); $i++ ) {
5420  print "<b>{$arrayKeys[$i]}</b>: <span style='background-color:white'>{$entryDetailArray[$arrayKeys[$i]]}</span><br>";
5421  }
5422  print "</ul>";
5423  print "</div>";
5424  }
5425  }
5426  }
5427 
5428  // add to the totals for validation
5429  if ( !count( $printErrorList ) ) {
5430  $companyEntryAddendaCount++;
5431 
5432  $companyEntryHash += $entryDetailArray["ReceivingDFI"];
5433  if ( $entryDetailArray["TransactionCode"] == "22" ||
5434  $entryDetailArray["TransactionCode"] == "32" ||
5435  $entryDetailArray["TransactionCode"] == "42" ||
5436  $entryDetailArray["TransactionCode"] == "52" ) {
5437  $companyTotalCreditEntryDollarAmount += $entryDetailArray["Amount"];
5438  } else {
5439  $companyTotalDebitEntryDollarAmount += $entryDetailArray["Amount"];
5440  }
5441  }
5442 
5443  $addendaDetailArray = array();
5444  if ( !count( $printErrorList ) &&
5445  $entryDetailArray["AddendaRecordIndicator"] == "1" ) {
5446  $extractionResults = GetAddendaRecord( $pAdmEnv, $fileContents[$fileContentsLine++] );
5447  if ( $extractionResults["code"] != "000" ) {
5448  $printErrorList[] = "ACH File extraction error";
5449  $printErrorList[] = $extractionResults["error"];
5450  } else {
5451  $addendaDetailArray = $extractionResults["data"];
5452 
5453  $entryDetailSequenceNumber = substr( $entryDetailArray["TraceNumber"], -7 );
5454  $results = ValidateAddendaRecord( $pAdmEnv, $addendaDetailArray, $entryDetailSequenceNumber );
5455  if ( count( $results["error"] ) ) {
5456  $printErrorList[] = "Invalid ACH File - Addenda Record";
5457 
5458  for ( $e = 0; $e < count($results["error"]); $e++ ) {
5459  $printErrorList[] = $results["error"][$e];
5460  }
5461  }
5462  if ( count( $results["warning"] ) ) {
5463  $printWarningList[] = "ACH Entry Detail Record Warnings";
5464 
5465  for ( $e = 0; $e < count($results["warning"]); $e++ ) {
5466  $printWarningList[] = $results["warning"][$e];
5467  }
5468  }
5469  if ( count( $addendaDetailArray ) ) {
5470  print "<div class='k-block' style='background-color:BlanchedAlmond;'>";
5471  print "<div class='h4'>Addenda Detail</div>";
5472  $arrayKeys = array_keys( $addendaDetailArray );
5473  for ( $i = 0; $i < count( $arrayKeys ); $i++ ) {
5474  print "<b>{$arrayKeys[$i]}</b>: <span style='background-color:white'>{$addendaDetailArray[$arrayKeys[$i]]}</span><br>";
5475  }
5476  print "</ul>";
5477  print "</div>";
5478  }
5479  }
5480  }
5481 
5482  // add to the totals for validation
5483  if ( !count( $printErrorList ) &&
5484  isset( $addendaDetailArray["RecordTypeCode"] ) &&
5485  $addendaDetailArray["RecordTypeCode"] == "7") {
5486  $companyEntryAddendaCount++;
5487  }
5488 
5489  // all records have the first caracter as the type code, so look to see what is next
5490  $nextTypeCode = substr( $fileContents[$fileContentsLine], 0 , 1 );
5491  } while ( !count( $printErrorList ) && $nextTypeCode != 8 );
5492 
5493  // this should be a company control record
5494  if ( !count( $printErrorList ) ) {
5495  // entry detail
5496  $extractionResults = GetCompanyControlRecord( $pAdmEnv, $fileContents[$fileContentsLine++] );
5497  if ( $extractionResults["code"] != "000" ) {
5498  $printErrorList[] = "ACH File extraction error";
5499  $printErrorList[] = $extractionResults["error"];
5500  } else {
5501  $companyControlArray = $extractionResults["data"];
5502 
5503  $results = ValidateCompanyControlRecord( $pAdmEnv, $companyControlArray );
5504  if ( count( $results["error"] ) ) {
5505  $printErrorList[] = "Invalid ACH File - Company Control Record";
5506 
5507  for ( $e = 0; $e < count($results["error"]); $e++ ) {
5508  $printErrorList[] = $results["error"][$e];
5509  }
5510  }
5511  if ( count( $results["warning"] ) ) {
5512  $printWarningList[] = "ACH Company Control Record Warnings";
5513 
5514  for ( $e = 0; $e < count($results["warning"]); $e++ ) {
5515  $printWarningList[] = $results["warning"][$e];
5516  }
5517  }
5518  }
5519  if ( count( $companyControlArray ) ) {
5520  print "<div class='k-block k-success-colored'>";
5521  print "<div class='h4'>Company Control Record</div>";
5522  $arrayKeys = array_keys( $companyControlArray );
5523  for ( $i = 0; $i < count( $arrayKeys ); $i++ ) {
5524  print "<b>{$arrayKeys[$i]}</b>: <span style='background-color:white'>{$companyControlArray[$arrayKeys[$i]]}</span><br>";
5525  }
5526  print "</ul>";
5527  print "</div>";
5528  }
5529  }
5530 
5531  // now validate the company totals
5532  if ( !count( $printErrorList ) ) {
5533  if ( intval( $companyEntryAddendaCount ) != intval( $companyControlArray["EntryAddendaCount"] ) ) {
5534  $printErrorList[] = "Company control record Entry Addenda Count does not match: $companyEntryAddendaCount vs {$companyControlArray["EntryAddendaCount"]}";
5535  }
5536 
5537  $hashTest = str_pad( $companyEntryHash, 10, "0", STR_PAD_LEFT );
5538  // use "exactly equal" because want to check type and length, too.
5539  if ( $hashTest !== $companyControlArray["EntryHash"] ) {
5540  $printErrorList[] = "Company control record Entry Hash does not match";
5541  }
5542 
5543  if ( $companyTotalDebitEntryDollarAmount != $companyControlArray["TotalDebitEntryDollarAmount"] ) {
5544  print "Test: Debit: $companyTotalDebitEntryDollarAmount, Credit: $companyTotalCreditEntryDollarAmount<br>";
5545  print_r( $companyControlArray );
5546  $printErrorList[] = "Company control record Total Debit Entry Dollar Amount does not match {$companyTotalDebitEntryDollarAmount} vs {$companyControlArray["TotalDebitEntry"]}";
5547  }
5548 
5549  if ( $companyTotalCreditEntryDollarAmount != $companyControlArray["TotalCreditEntryDollarAmount"] ) {
5550  print "Test: Debit: $companyTotalDebitEntryDollarAmount, Credit: $companyTotalCreditEntryDollarAmount<br>";
5551  print_r( $companyControlArray );
5552  $printErrorList[] = "Company control record Total Credit Entry Dollar Amount does not match {$companyTotalCreditEntryDollarAmount} vs {$companyControlArray["TotalCreditEntry"]}";
5553  }
5554 
5555  if ( $companyHeaderArray["CompanyID"] != $companyControlArray["CompanyID"] ) {
5556  $printErrorList[] = "Company control record Company Identifier does not match";
5557  }
5558 
5559  if ( $companyHeaderArray["BatchNumber"] != $companyControlArray["BatchNumber"] ) {
5560  $printErrorList[] = "Company control record Batch Number does not match";
5561  }
5562  }
5563 
5564  if ( !count( $printErrorList ) ) {
5565  // add to the counters
5566  $fileBatchCount++;
5567  $fileEntryAddendaCount += $companyEntryAddendaCount;
5568  $fileEntryHash += $companyEntryHash;
5569  $fileTotalDebitEntryDollarAmount += $companyTotalDebitEntryDollarAmount;
5570  $fileTotalCreditEntryDollarAmount += $companyTotalCreditEntryDollarAmount;
5571  }
5572 
5573  // end indentation
5574  print "</div>";
5575 
5576  $nextTypeCode = substr( $fileContents[$fileContentsLine], 0 , 1 );
5577  } while (!count( $printErrorList ) && $nextTypeCode != 9 );
5578 
5579  // this should be a file control record
5580  if ( !count( $printErrorList ) ) {
5581  // entry detail
5582  $extractionResults = GetFileControlRecord( $pAdmEnv, $fileContents[$fileContentsLine++] );
5583  if ( $extractionResults["code"] != "000" ) {
5584  $printErrorList[] = "ACH File extraction error";
5585  $printErrorList[] = $extractionResults["error"];
5586  } else {
5587  $fileControlArray = $extractionResults["data"];
5588 
5589  $results = ValidateFileControlRecord( $pAdmEnv, $fileControlArray );
5590  if ( count( $results["error"] ) ) {
5591  $printErrorList[] = "Invalid ACH File - File Control Record";
5592 
5593  for ( $e = 0; $e < count($results["error"]); $e++ ) {
5594  $printErrorList[] = $results["error"][$e];
5595  }
5596  }
5597  if ( count( $results["warning"] ) ) {
5598  $printWarningList[] = "ACH File Control Record Warnings";
5599 
5600  for ( $e = 0; $e < count($results["warning"]); $e++ ) {
5601  $printWarningList[] = $results["warning"][$e];
5602  }
5603  }
5604  }
5605  if ( count( $fileControlArray ) ) {
5606  print "<div class='k-block' style='background-color: lightcyan;'>";
5607  print "<div class='h4'>File Control Record</div>";
5608  $arrayKeys = array_keys( $fileControlArray );
5609  for ( $i = 0; $i < count( $arrayKeys ); $i++ ) {
5610  print "<b>{$arrayKeys[$i]}</b>: <span style='background-color:white'>{$fileControlArray[$arrayKeys[$i]]}</span><br>";
5611  }
5612  print "</ul>";
5613  print "</div>";
5614  }
5615  }
5616 
5617  // validate the whole file totals
5618  if ( !count( $printErrorList ) ) {
5619  if ( $fileBatchCount != $fileControlArray["BatchCount"] ) {
5620  $printErrorList[] = "File Control record Batch Count does not match";
5621  $printErrorList[] = "Counted: $fileBatchCount, Control record: {$fileControlArray["BatchCount"]}";
5622  }
5623 
5624  // a "block" is 10 records
5625  $fileBlockCount = round( $fileRecordCount / 10 );
5626  if ( $fileBlockCount != (int) $fileControlArray["BlockCount"] ) {
5627  $printErrorList[] = "File Control record Block Count does not match: counted $fileBlockCount; file {$fileControlArray["BlockCount"]}";
5628  }
5629 
5630  if ( $fileEntryAddendaCount != $fileControlArray["EntryAddendaCount"] ) {
5631  $printErrorList[] = "File Control record Entry Addenda Count does not match";
5632  }
5633 
5634  if ( strlen( $hashTest ) > 10 ) {
5635  $hashTest = substr( $fileEntryHash, -10 );
5636  } else {
5637  $hashTest = str_pad( $fileEntryHash, 10, "0", STR_PAD_LEFT );
5638  }
5639 
5640  // use "exactly equal" because want to check type and length, too.
5641  if ( $hashTest !== $fileControlArray["EntryHash"] ) {
5642  $printErrorList[] = "File Control record Entry Hash does not match";
5643  }
5644 
5645  if ( $fileTotalDebitEntryDollarAmount != $fileControlArray["TotalDebitEntryDollarAmount"] ) {
5646  $printErrorList[] = "File Control record Total Debit Entry does not match";
5647  }
5648 
5649  if ( $fileTotalCreditEntryDollarAmount != $fileControlArray["TotalCreditEntryDollarAmount"] ) {
5650  $printErrorList[] = "File Control record Total Credit Entry does not match";
5651  }
5652 
5653  }
5654 
5655  } catch (Exception $ex ) {
5656  $printErrorList[] = $ex->getMessage();
5657  }
5658 
5659  $errorString = "";
5660  if ( count( $printErrorList ) ) {
5661  $errorString .= "The following <b>errors</b> occurred: ";
5662  $errorString .= "<ul>";
5663  for ( $i = 0; $i < count( $printErrorList ); $i++ ) {
5664  $errorString .= "<li>{$printErrorList[$i]}</li>";
5665  }
5666  $errorString .= "Validation stops if error found, so check the last record validated.";
5667  $errorString .= "</ul>";
5668  }
5669  if ( count( $printWarningList ) ) {
5670  $errorString .= "The following <b>warnings</b> occurred: ";
5671  $errorString .= "<ul>";
5672  for ( $i = 0; $i < count( $printWarningList ); $i++ ) {
5673  $errorString .= "<li>{$printWarningList[$i]}</li>";
5674  }
5675  $errorString .= "</ul>";
5676  }
5677 
5678  if ( isset( $pAdmVars["show_raw"] ) && $pAdmVars["show_raw"] ) {
5679  ?>
5680  <div><a class="returnLink" style="cursor:pointer;">Return</a></div>
5681  <div> Raw Data</div>
5682  <div> File <?php echo $name ?></div>
5683  <pre><?php
5684  for ( $i = 0; $i < count( $fileContents ); $i++ ) {
5685  print "{$fileContents[$i]}<br>";
5686  }
5687  ?></pre>
5688  <?php
5689  }
5690  ?>
5691  <div><a class="returnLink" style="cursor:pointer;">Return</a></div>
5692  <script>
5693  $(document).ready( function() {
5694  <?php
5695  if ( strlen( $errorString ) ) {
5696  ?>
5697  $("#formValidateDiv").html( "<?php echo $errorString ?>" ).show();
5698  <?php
5699  }
5700  ?>
5701  } );
5702  </script>
5703  </div>
5704 <?php }
5705 
5706  /**
5707  * function PrintCommonStyle()
5708  * prints out styling common to a majority of the pages.
5709  */
5710  function PrintCommonStyle()
5711  { ?>
5712  <style>
5713  .grid_12 {
5714  margin-bottom: 5px;
5715  }
5716 
5717  /* Give same styling as if type="text" */
5718  input[type="email"], input[type="password"] {
5719  border-style: solid;
5720  border-width: 1px;
5721  font-family: inherit;
5722  font-size: 100%;
5723  }
5724 
5725  #showHelpDialog {
5726  text-align: left;
5727  }
5728 
5729  #login-entry {
5730  margin-left: auto;
5731  margin-right: auto;
5732  }
5733 
5734  .homecu-formStatus {
5735  margin-bottom: 10px;
5736  }
5737  </style>
5738  <?php }
SendMail()
Definition: errormail.i:111