Odyssey
schedliveproc.php
1 #!/usr/local/bin/php
2 <?php
3 
4 #use LWP::Simple qw(get $ua);
5 //use LWP::UserAgent;
6 
7 //$ua = LWP::UserAgent->new(
8 // ssl_opts => { SSL_verify_mode => 'SSL_VERIFY_NONE'},
9 //);
10 
11 # pgliverecur.php
12 #
13 # Fetches packets for live homebanking members with recurring transactions
14 #
15 # If provided, -p packetstamp is used as the fetch 'if_mod_since' parameter
16 # Otherwise, if_mod_since is set to current timestamp - 24 hours
17 # Fetcher cutoff is set to 'if_mod_since' - 1 day
18 #
19 # Loads alert tables (alertus, alertab, alertah, alertlb, alertlh) with data
20 # alertus contains user info for members with active repeating transactions
21 # alertab, alertah, alertlb, alertlh have homebanking data
22 #
23 # Processes recurring transactions and notifies members
24 #
25 // ** SET HOMECU FLAGS
26  $serviceMinimal = true;
27  $serviceShowInfo = false;
28  $serviceLoadMenu = false;
29  $serviceShowMenu = false;
30  $serviceLoadCuInfo = false;
31 
32  // ** INCLUDE MAIN GLOBAL SCRIPT -- Handles security / global variable values
33  // hcuService will be returning a status object: e.g. ["homecuErrors":{[{"message":"message1"}...{"message":"messageN"}}]
34  require_once('/var/www/html/banking/library/hcuService.i');
35 
36  require_once('/var/www/html/shared/library/sAPIAppl.i');
37  require_once('/var/www/html/shared/library/sAPIAppl.std.i');
38  require_once('/var/www/html/banking/library/hcuTransferScheduled.i');
39  require_once('/var/www/html/banking/library/hcuTransfer.i');
40 
41  // INCLUDE the cu_sms.pi script which will have the function for sending out text messages
42  require_once('/var/www/html/shared/library/cu_sms.i');
43  require_once("/var/www/html/shared/library/cu_data.i"); // For mask functions.
44 
45  // set up the logger
46  $logger = $HB_ENV["SYSENV"]["logger"];
47 
48 # # # # # # # # # # # # # # # #
49 # Import Server Variables
50 # # # # # # # # # # # # # # # #
51 //require "/usr/local/bin/dbserver.cfg" if -f "/usr/local/bin/dbserver.cfg" and -r "/usr/local/bin/dbserver.cfg"; # Import: $ownuname, $grpuname, $wwwhost, $dbhost, $dbport, $dbname, $dbuser, $dbpasswd, $dbplat
52 //if ( file_exists( "/usr/local/bin/dbserver.cfg" ) && is_readable( "/usr/local/bin/dbserver.cfg" ) ) {
53 // require "/usr/local/bin/dbserver.cfg";
54 //} else {
55 // die( "Needed file is missing: /usr/local/bin/dbserver.cfg\n" );
56 //}
57 
58  $ownuname = 'www0';
59  $grpuname = 'www0';
60 
61  $wwwhost = 'www0.homecu.net';
62 
63  $dbhost = getenv('DATABASE_HOST');
64  $dbport = getenv('DATABASE_PORT');
65  $dbname = getenv('DATABASE_NAME');
66  $dbuser = getenv('DATABASE_USER');
67  $dbpasswd = getenv('DATABASE_PASSWORD');
68  $dbplat = 'Postgres';
69  $pReplyEmail = "nobody@homecu.net"; // used for the From when mailing CU
70 
71  # ################
72  # SET LONG CODE AVAILABLE NUMBERS (for alerts messaging)
73  $gRoundRobinCounter = 0;
74  $LONGCODE_ROUNDROBIN = array('+12672458136', '+12082982149', '+16017142198', '+12082547302', '+16017142199', '+13306492200');
75 
76 
77 //use vars qw($opt_c $opt_l $opt_n $opt_m $opt_p $opt_s $opt_f $opt_d $FAILRPTAFTER $WARNFAILRPT);
78 # a = action - either "alerts" or "scheduled"
79 # c = limit fetching and processing to this particular cu code
80 # x = exclude this cu code when fetching and processing
81 # l = limit number of packets retrieved
82 # n = nap interval between requests
83 # m = fetch and process for this member only
84 # p = 'if_mod_since' pktstamp to pass to fetcher
85 # s = skip loading tables
86 # f = fetch only - no messages sent
87 # t = limit fetching and processing to this particular time zone
88 # d = database host to use - fully-qualified host.homecu.net name
89 # r = runslot override - used to override the calculated runslot
90 
91 //use Getopt::Std;
92 //unless (getopts('c:h:l:n:m:p:t:x:d:sf')) {
93 // die "Usage: $ARGV[0] [-d dbhost] [-c cucode] [-x cucode] [-t timezone] [-l limit] [-n nap] [-m member] [-p pktstamp] [-s] [-f]\n";
94 //}
95  // characters followed by a colon (':') are required values, followed by two colons ("::") are optional values
96  $showUsage = false;
97  $allowedOptions = "c::l::n::m::p::t::x::d::s::f::r::";
98  $longOptions = array( "help::", "dryrun::" );
99  $commandOptions = getopt( $allowedOptions, $longOptions );
100  if ( $commandOptions === FALSE || isset( $commandOptions["help"] ) ) {
101  $showUsage = true;
102  }
103 
104  // get the action (last parameter)
105  $action = trim( $argv[$argc-1] );
106 
107  // set some option variables based on what was passed in
108  $opt_c = isset( $commandOptions["c"] ) ? $commandOptions["c"] : "";
109  $opt_l = isset( $commandOptions["l"] ) ? $commandOptions["l"] : "";
110  $opt_n = isset( $commandOptions["n"] ) ? $commandOptions["n"] : "";
111  $opt_m = isset( $commandOptions["m"] ) ? $commandOptions["m"] : "";
112  $opt_p = isset( $commandOptions["p"] ) ? $commandOptions["p"] : "";
113  $opt_s = isset( $commandOptions["s"] ) ? true : false;
114  $opt_f = isset( $commandOptions["f"] ) ? true : false;
115  $opt_d = isset( $commandOptions["d"] ) ? $commandOptions["d"] : "";
116  $opt_x = isset( $commandOptions["x"] ) ? $commandOptions["x"] : "";
117  $opt_t = isset( $commandOptions["t"] ) ? $commandOptions["t"] : "";
118  $opt_r = isset( $commandOptions["r"] ) ? trim( $commandOptions["r"] ) : "";
119 
120  $dryRun = isset( $commandOptions["dryrun"] );
121 
122  $dryRunPrefix = $dryRun ? "Dry run: " : "";
123 
124  // enforce some command line rules
125  if ( $action == "" || !($action == "alerts" || $action == "scheduled") ) {
126  print "Missing action. Supported actions: alerts OR scheduled\n";
127  $showUsage = true;
128  }
129  if ( "$opt_d" == "" && "$dbhost" == "" ) {
130  print "Missing database host.\n";
131  $showUsage = true;
132  }
133  if ( "$opt_r" != "" && !$opt_s ) {
134  print "[-r runslot (DAYHHMM)] should be used with the [-s] (skip load) option.\n";
135  $showUsage = true;
136  }
137 
138  if ( $showUsage ) {
139  // show usage and exit
140  die( "Usage: {$argv[0]} [-dDbhost] [-cCucode] [-xCucode] [-tTimezone] [-lLimit] [-nNap] [-mMember] [-pPktstamp] [-rRunslot] [-s] [-f] [--dryrun] <action>\n[runslot] is expected to be used with -s option\n<action> can be 'alerts' or 'scheduled'\n" );
141  }
142 
143  if ( "$opt_d" != "" ) { $dbhost = "$opt_d"; }
144 
145  // this is the directory we have rights to and monitor will eventually be able to see
146  $WORKINGDIRECTORY = "/home/homecu/html";
147  # Change Directory to where this script has permissions
148  chdir( $WORKINGDIRECTORY );
149 
150  date_default_timezone_set ( "America/Denver" );
151 
152  $DMSMAIL = 'support@homecu.net';
153  $ALERTMAIL = 'support@homecu.net';
154 
155  $STARTDATE = date( "D M d, Y G:i" );
156  $TODAY = date ( "Y-m-d" ); // date formatted like sql date field
157  $STDOUT = fopen('php://stdout', 'w');
158  fwrite( $STDOUT, "$STARTDATE $TODAY\n" );
159 
160  /*
161  * run-slot logic
162  */
163  list( $s, $m, $h, $mday, $mon, $year, $wday, $yday, $isdst) = localtime();
164 
165  # CREATE THE RUNSLOT value -- IT will be the key used in tables
166  $weekdays = array("SUN", "MON", "TUE", "WED", "THR", "FRI", "SAT");
167  # Minute segments -- sections of quarter hour
168  $minsegs = array("00", "15", "30", "45");
169 
170 
171  # Create a string for the hour/min segment
172  $runslotPrefix = $action == "alerts" ? "A" : "S";
173  // * Set the platform identifier. This is used in the query to the core.
174  $HB_ENV['platform'] = ($action == "alerts" ? "ALERT" : "SCHED");
175 
176  if ( strlen( $opt_r ) > 0 ) {
177  $runslotseg = substr( $opt_r, -4 );
178  $runslot = $runslotPrefix . $opt_r;
179  } else {
180  $runslotseg = sprintf("%02d", $h) . sprintf("%02d", $minsegs[intval($m / 15)]);
181  $runslot = $runslotPrefix . $weekdays[$wday] . $runslotseg;
182  }
183 
184  $PIDFILE = $action == "alerts" ? $WORKINGDIRECTORY . "/runalerts{$runslot}.pid" : $WORKINGDIRECTORY . "/runrecur{$runslot}.pid";
185  if ( file_exists( "$PIDFILE" ) ) {
186  $errorMessage = "Prior PID file $PIDFILE still exists -- giving up";
187  if ( $dryRun ) {
188  print "Dry Run: Would send error email to HomeCU: $errorMessage\n";
189  } else {
190  dmserror( $DMSMAIL, 'WARN', "$errorMessage\n" );
191  }
192  exit (1);
193  }
194 
195  $PID = fopen( $PIDFILE, "w+" );
196  fwrite( $PID, getmypid() . "\n" );
197  fclose( $PID );
198 
199  list ($s, $m, $h, $mday, $mon, $year, $wday, $yday, $isdst) = localtime();
200  $T = sprintf( "%4d%02d%02d%02d%02d%02d", 1900+$year,$mon+1,$mday,$h,$m,$s );
201 
202  if ( $dryRun ) {
203  $STAT = fopen('php://stdout', 'w');
204  fprintf( $STAT, "Dry Run: outputting to standard output\n" );
205  } else {
206  if ( $action == "alerts") {
207  $filename = "alerts$T.txt";
208  $STAT = fopen( $filename, "w+" );
209  } else if ( $action == "scheduled" ) {
210  $filename = "recurltx$T.txt";
211  $STAT = fopen( $filename, "w+" );
212  } else {
213  dmserror( $DMSMAIL, 'FATAL', "Unknown action: $action\n" );
214  }
215 
216  fwrite( $STDOUT, "Results will be in {$WORKINGDIRECTORY}/$filename\n" );
217  }
218 
219  fflush( $STAT );
220 
221  //`find /home/httpd/hcupriv -name "recur*" -mtime +90 -exec rm -f {} \\;`;
222  foreach( glob( "recur*") as $filename ) {
223  // make sure file is at least 90 days old
224  if ( filemtime( $filename ) < (time() - 90 * 24 * 3600) ) {
225  if ( $dryRun ) {
226  fprintf( $STAT, "Dry Run: removing old recurring file $filename\n" );
227  } else {
228  unlink( $filename );
229  }
230  }
231  }
232  foreach( glob( "alert*") as $filename ) {
233  // make sure file is at least 90 days old
234  if ( filemtime( $filename ) < (time() - 90 * 24 * 3600) ) {
235  if ( $dryRun ) {
236  fprintf( $STAT, "Dry Run: removing old alerts file $filename\n" );
237  } else {
238  unlink( $filename );
239  }
240  }
241  }
242 
243  $cucode = ($opt_c ? "'" . strtoupper($opt_c) . "'" : "");
244  $xcucode = ($opt_x ? "'" . strtoupper($opt_x) . "'": "");
245  $opt_t = str_replace( " ", "", $opt_t );
246  $opt_t = ucfirst(strtolower($opt_t));
247  $limit = ($opt_l > 0 ? $opt_l : "");
248  $nap = ( $opt_n < 0 ? 0 : intval( $opt_n ) );
249  $member = ($opt_m ? $opt_m : "");
250  $skipload = ($opt_s ? 1 : "");
251  $fetchonly = ($opt_f ? true : false);
252  $pktstamp = (intval($opt_p) > 0 ? $opt_p : time() - 86400);
253  //$alertdate = `date +'%a %b %d %Y %X %Z'`;
254  $alertdate = date( "D M d Y H:i:s T");
255 
256  $opt_m = str_replace( " ", "", $opt_m );
257  // "s" is alias for cu_scheduledtxn
258  $memberSQL = ($opt_m ? " and s.accountnumber = '$opt_m' " : "");
259  // "a" is alias for cuadmin
260  if ( $opt_t == "Eastern" ) {
261  $timezoneSQL = " and a.tz in ('Eastern','East-Indiana','Indiana-Starke','Michigan','America/Port_of_Spain','America/St_Thomas','America/Barbados') ";
262  } elseif ( $opt_t == "Pacific" ) {
263  $timezoneSQL = " and a.tz in ('Pacific','Alaska','Arizona') ";
264  } else {
265  $timezoneSQL = ($opt_t ? " and a.tz = '$opt_t' " : "");
266  }
267 
268  // 09/19: This clarifies the changing state of $xcucode. We want it to be an array member,
269  // not a select string part. See design doc
270  // https://docs.google.com/document/d/1wqTezsy-9l2q01sInvbxSuxCx-zq0wRV_kA0VtrWsj8
271  $xcu_arr = [];
272  $sql_xcucodes = $xcucode;
273  $xcucode_arr = (! empty($xcucode))? [str_replace("'", '', $xcucode)] : [];
274  // if timezone is set
275  // AND a single Credit union is NOT listed
276  // then I want to load the list of credit unions to exclude from the blacklist file
277  if (($opt_t != '') && $xcucode == '') {
278  // ReturnBlackList
279  // This function will load the list of credit unions to be excluded from the alerts.blacklist.conf file
280  // and return in a string formatted as 'CUCODE1', 'CUCODE2', 'CUCODE3'
281  // 09-19: Now returns array, see design document at above link as to the "why"
282  $xcu_arr = ReturnBlackList();
283  $sql_xcucodes = "'" . implode("', '", $xcu_arr) . "'";
284  $returnString = "\nExcluded Blacklisted: $sql_xcucodes\n\n";
285 
286  } else if ( ($opt_t == "") && $cucode == "" ) {
287  # If timezone is BLANK
288  # AND cucode is currently blank --
289  # THEN build cucode from the ReturnRunSlotList Function
290  # Pass the runslot segment to the function 'HH:MM'
291  $cucode = ReturnRunSlotList( $runslotseg );
292  }
293 
294  $ignore_cu_arr = ReturnIgnoreList($dbh);
295  if (count($ignore_cu_arr) > 0) {
296  // if CU is empty OR CU is specified AND in ignore list - add to blacklist and report
297  if ( $cucode == "" || ($cucode != "" && in_array($cucode, $ignore_cu_arr))) {
298 
299  $sql_xcucodes = "'" . implode("', '", array_unique(array_merge($xcucode_arr, $xcu_arr, $ignore_cu_arr))) . "'";
300  $returnString = "\nExcluded Blacklisted: "
301  . "'" . implode("', '", $xcu_arr) . "'" . "\n";
302  if (count($xcucode_arr) > 0) {
303  $returnString .= "Command line X option exclusions: "
304  . "'" . implode("', '", $xcucode_arr) . "'" . "\n";
305  }
306  $returnString .= "In Migration CUs Ignored: "
307  . "'" . implode("', '", $ignore_cu_arr) . "'\n\n";
308 
309  // this is for the person running it to see on the console
310  if ( !$dryRun ) {
311  fprintf( $STDOUT, $returnString );
312  }
313  fprintf( $STAT, $returnString );
314  }
315  }
316 
317  ############ print out processing parameters ########
318  fprintf( $STAT, "Command options: \n" );
319  fprintf( $STAT, "\tCU Code: $cucode\n" );
320  fprintf( $STAT, "\tlimit: $opt_l\n" );
321  fprintf( $STAT, "\tnap: $opt_n\n" );
322  fprintf( $STAT, "\tmember: $opt_m\n" );
323  fprintf( $STAT, "\tpktstamp: $opt_p\n" );
324  fprintf( $STAT, "\tskip load: $skipload\n" );
325  fprintf( $STAT, "\tfetch only: $fetchonly\n" );
326  fprintf( $STAT, "\tdbhost: $opt_d\n" );
327  fprintf( $STAT, "\texclude CU: $sql_xcucodes\n" );
328  fprintf( $STAT, "\ttimezone: $opt_t\n" );
329  fprintf( $STAT, "\trunslot: $opt_r\n" );
330  fprintf( $STAT, "\tdry run: " . ($dryRun ? "true" : "false") . "\n" );
331  fprintf( $STAT, "\nUsing run slot: $runslot\n\n" );
332 
333  // these are only applicable if loading the dataset to process
334  if ( !$skipload ) {
335  if ( $opt_t ) {
336  $string = "Processing Timezone $opt_t\n";
337  fprintf( $STAT, $string );
338  if ( !$dryRun ) {
339  fprintf( $STDOUT, $string );
340  }
341  }
342  if ( $opt_c ) {
343  $string = "Processing CU $opt_c\n";
344  fprintf( $STAT, $string );
345  if ( !$dryRun ) {
346  fprintf( $STDOUT, $string );
347  }
348  }
349  if ( $opt_x ) {
350  $string = "Excluding CU $opt_x\n";
351  fprintf( $STAT, $string );
352  if ( !$dryRun ) {
353  fprintf( $STDOUT, $string );
354  }
355  }
356  if ( $opt_m ) {
357  $string = "Processing Member $opt_m\n";
358  fprintf( $STAT, $string );
359  if ( !$dryRun ) {
360  fprintf( $STDOUT, $string );
361  }
362  }
363  if ( $opt_l > 0 ) {
364  $string = "Limit=$limit\n";
365  fprintf( $STAT, $string );
366  if ( !$dryRun ) {
367  fprintf( $STDOUT, $string );
368  }
369  }
370  if ( $opt_n ) {
371  $string = "Nap=$nap\n";
372  fprintf( $STAT, $string );
373  if ( !$dryRun ) {
374  fprintf( $STDOUT, $string );
375  }
376  }
377  if ( $opt_p ) {
378  $string = "Using pktstamp $pktstamp\n";
379  fprintf( $STAT, $string );
380  if ( !$dryRun ) {
381  fprintf( $STDOUT, $string );
382  }
383  }
384  }
385 
386  if ( $action == "alerts" ) {
387  // for alerts look back 7 days
388  $cutoff = date( "Ymd", time() - 7 * 24 * 60 * 60 );
389  } else {
390  // for scheduled txns cutoff is in the future to avoid getting history (+5 days)
391  $cutoff = date( "Ymd", time() + 5 * 24 * 60 * 60 );
392  }
393 
394  ############# configuration variables ###########
395  $config = array();
396  $config["skipload"] = $skipload;
397  $config["runslot"] = $runslot;
398  $config["cucode"] = $cucode;
399  $config["xcucode"] = $sql_xcucodes;
400  $config["member"] = $member;
401  $config["limit"] = $limit;
402  $config["nap"] = $nap;
403  $config["fetchonly"] = $fetchonly;
404  $config["pktstamp"] = $pktstamp;
405  $config["cutoff"] = $cutoff;
406 
407  ####################### Globals
408  $gatheredStats = array();
409  $fetchStats = array();
410 
411  ####################### Open Database
412 
413  // database was opened via hcuService.i but verify we have a connection
414  if ( !$dbh ) {
415  dmserror( $DMSMAIL, 'FATAL', "Database connection failed" );
416  }
417 
418  fprintf( $STAT, "Connected to database.\n" );
419 
420  $starttime = time();
421 
422  $numProcessed = 0;
423  ############# Process Stuff
424  if ( $action == "alerts" ) {
425  if ($opt_t == "" && $opt_c == "" && $cucode == "") {
426  fprintf($STAT, "No valid Credit Unions found with current parameters.\n");
427  } else {
428  $numProcessed = ProcessAlerts($config);
429  }
430  } else if ( $action == "scheduled" ) {
431  $numProcessed = ProcessScheduledTransfers( $config );
432  }
433 
434  $now = date( "D M d H:i:s Y" );
435  fprintf( $STAT, "{$dryRunPrefix}Finished Processing $now\n" );
436 
437  $endtime = time();
438  $time = $endtime - $starttime;
439  $processedString = "\n{$dryRunPrefix}Processed $numProcessed accounts in $time seconds\n\n";
440  fprintf( $STAT, $processedString );
441 
442  // this is for the person running it to see on the console
443  if ( !$dryRun ) {
444  fprintf( $STDOUT, $processedString );
445  }
446 
447  if ( count( $fetchStats ) > 0 ) {
448  fprintf( $STAT, "Fetch statistics:\n");
449  foreach( $fetchStats as $statCode => $count ) {
450  fprintf( $STAT, "\t$statCode: $count\n");
451  }
452  } else {
453  fprintf( $STAT, "No Fetch stats gathered.\n");
454  }
455  fprintf( $STAT, "\n");
456 
457  if ( $action == "alerts" ) {
458  PrintGatheredAlertStats( $STAT, $gatheredStats );
459  } else if ( $action == "scheduled" ) {
460  PrintGatheredTransferStats( $STAT, $gatheredStats );
461  }
462 
463 /*
464 We are trying to combine cross account logic with regular account logic, so hopefully don't need this part
465 
466  $memsql = "SELECT trim(cu), trim(from_account) as from_account, trim(to_account) as to_account
467  FROM cutronus
468  WHERE runslot = '$runslot'
469  ORDER BY shuffle ";
470  $memsql .= ( $limit != "" ? " limit $limit " : "" );
471 
472  #print STAT "Using sql " . $memsql . "\n";
473 
474  $listRS = db_query( $memsql, $dbh );
475  if ( $listRS ) {
476  dmserror($DMSMAIL,'FATAL',"Failed Selecting Cross Accounts to Process\n" . db_last_error( $listRS ) );
477  }
478 
479  $now = date( "D M d H:i:s Y" );
480  fprintf( $STAT, "Fetching Cross Account Packets Begin $now\n";
481 
482  $stats = array();
483  $row = 0;
484  while ( $dataRow = db_fetch_row( $listRS, $row++ ) ) {
485  if ( $nap > 0 ) { sleep( $nap ); }
486 
487  $cu = $dataRow[0];
488  $fromAccount = $dataRow[1];
489  $toAccount = $dataRow[2];
490 
491  ($status, $asofdate, $reason) = batch_XS();
492 
493  if ($reason > '' ) { $showcount .= $reason; }
494 
495  $stats{"Status_${status}"}++;
496 
497  if ($status == '200' || $status == '201' || $status == '202') {
498  if ( $cucode != "") sleep( $nap + 300 );
499  }
500 
501  if ($status eq '001' || $status eq '999') {
502  print STAT "$row\tCU $Cu Member $Cn\tFailed XA! $status - $reason\n";
503  }
504 
505  if ($status eq '001') {
506  # -- I want to send an email to the Credit Union
507  # They have a user w/repeating transactions that is NO longer setup
508  # in the Credit Union but are still setup in HomeBanking
509  my $email_msg = "RECURRING TRANSACTION FAILED\n\nMember $Cn does not exist or is not setup on your core system.\n\n\n\n($Cu:$Cn)";
510  my $ERR_CUMAIL = '';
511  # FETCH THE CU EMAIL ACCOUNT
512  my $sql = "SELECT rtrim(email)
513  FROM cuadmnotify
514  WHERE cu = '$Cu'
515  AND role = 'transfernotify'";
516  my $admalertresult = $conn->exec($sql);
517  unless (PGRES_TUPLES_OK eq $admalertresult->resultStatus) {
518  dmserror($DMSMAIL,'WARN',$conn->errorMessage);
519  } else {
520  ($ERR_CUMAIL) = $admalertresult->fetchrow;
521  }
522  $ERR_CUMAIL = ($ERR_CUMAIL gt '' ? $ERR_CUMAIL : $DMSMAIL);
523 
524  SmsMailTransfer( $ERR_CUMAIL, $ERR_CUMAIL, $email_msg, '' );
525  }
526 }
527 print STAT "Fetching Cross Account Packets Complete " . scalar localtime() . "\n";
528 $endtime = time;
529 $time = $endtime - $starttime;
530 print STAT "\nProcessed $row accounts in $time seconds\n\n";
531 while(($stat,$count) = each %stats) {
532  print STAT "$stat: $count\n";
533 }
534 */
535 
536 
537  fclose( $STAT );
538 
539  unlink($PIDFILE);
540 exit;
541 
542 /**
543  * This function will perform all the ScheduledTransfers processing. All the configuration variables are passed in.
544  * There are global variables used, as well.
545  */
546 function ProcessScheduledTransfers( $config ) {
547  global $dbh;
548  global $dryRun, $dryRunPrefix;
549  global $HB_ENV;
550  global $STAT;
551  global $TODAY;
552  global $DMSMAIL;
553  global $memberSQL, $timezoneSQL;
554  global $gatheredStats, $fetchStats;
555  global $pReplyEmail; // Without this, MailCU does not run.
556 
557  # ####################### #
558  # FAILED REPEATED TRANSFER MORATORIUM IN DAYS
559  # ####################### #
560  $FAILRPTAFTER = '33';
561 
562  # ####################### #
563  # FAILED REPEATED TRANSFER WARNING MSG IN DAYS
564  # ####################### #
565  $WARNFAILRPT = '10';
566 
567  // cuEmailCache is just to gather the email so don't need to get it again
568  $cuEmailCache = array();
569 
570  // NOTE: setting of scheduled transfers to inactive is not done if doing skipload because it is usually a debugging effort
571  if ( !$config["skipload"] ) {
572  // just delete the entries being added
573  $memSQL = "DELETE FROM cutronus WHERE runslot = '{$config["runslot"]}';
574  DELETE FROM cutronab WHERE runslot = '{$config["runslot"]}';
575  DELETE FROM cutronah WHERE runslot = '{$config["runslot"]}';
576  DELETE FROM cutronlb WHERE runslot = '{$config["runslot"]}';
577  DELETE FROM cutronlh WHERE runslot = '{$config["runslot"]}'; ";
578  if ( $dryRun ) {
579  fprintf( $STAT, "Dry run: would be deleting from tables\n $memSQL\n" );
580  } else {
581  $rs = db_query( $memSQL, $dbh );
582  if ( !$rs ) {
583  dmserror( $DMSMAIL, 'FATAL', "Failed Deleting From Tables\n" . db_last_error( $rs ) );
584  }
585  }
586 
587  $now = date( "D M d H:i:s Y" );
588  fprintf( $STAT, "Truncate Tables Complete $now\n" );
589 
590  $CU2_PROCRECUR = 32; # Process Repeating Transfers flag
591 
592  # #####################
593  #
594  # SET UNSUCCESSFUL UPDATES TO INACTIVE
595  #
596  # select all active transfers that have failed for more than FAILRPTAFTER
597  # stopdate must NOT be exceeded
598  # then
599  # Gather Email / members for credit union that transfer is set to inactive
600  # Email member that transfer is set to inacive
601  # SET stopdate to current day
602  # SET status to I
603  #
604  # AFTER process of all records email each CU with list of Accounts
605  #
606  # #####################
607 
608  $failSQL = "SELECT s.id, s.user_id, trim(s.cu), s.next_trigger_date, s.status, trim(a.pname)
609  FROM cu_scheduledtxn s
610  LEFT JOIN cuadmin a on a.cu = s.cu
611  WHERE date_part('day', current_date::timestamp - next_trigger_date::timestamp) > {$FAILRPTAFTER}
612  AND (s.end_date IS NULL OR s.end_date >= s.next_trigger_date)
613  AND a.livebatch = 'L'
614  AND s.status = 'A'
615  AND coalesce(a.offlinestat, 'N') not in ('Y','U')
616  AND (a.flagset2::bigint & {$CU2_PROCRECUR}::bigint) = $CU2_PROCRECUR
617 
618  ORDER BY s.cu, s.id; ";
619 
620  $failRS = db_query( $failSQL, $dbh );
621  $row = 0;
622  $currCU = '';
623  $cuList = array();
624 
625  # LOOP through any returned rows
626  while ( $failRec = db_fetch_row( $failRS, $row++ ) ) {
627 
628  $inactiveMsg = "";
629  $failCUMail = "";
630 
631  # Get CU Code
632  $currCU = $failRec[2];
633 
634  # #############
635  # Check for Credit Union
636  if ( !isset( $cuList[$currCU] ) ) {
637  # CU Code has not been added yet
638  # FETCH THE CU EMAIL ACCOUNT
639  $failCUMail = FetchCUEmail( $dbh, $currCU );
640 
641  // make sure we got an email
642  $failCUMail = ($failCUMail != "" ? $failCUMail : $DMSMAIL);
643 
644  # Add Email for CU
645  $cuList[$currCU]["email"] = $failCUMail;
646  // start the account list
647  $cuList[$currCU]["accounts"] = "";
648  } else {
649  # SET THE failCUMail
650  $failCUMail = $cuList[$currCU]["email"];
651  }
652 
653  // add the CU email to the cache in case need it again
654  if ( !isset( $cuEmailCache[$currCU] ) ) {
655  $cuEmailCache[$currCU]["email"] = $failCUMail;
656  }
657 
658  // get user info
659  $userInfo = FetchUserInfo( $dbh, $currCU, $failRec[1] );
660 
661  // add User Name to the CU Email LIst
662  $cuList[$currCU]["accounts"] .= "Notice: A repeating transfer for user {$userInfo["user_name"]} was set to inactive.\n";
663 
664  if ( $dryRun ) {
665  fprintf( $STAT, "Dry Run: Would send inactive email to {$userInfo["email"] } (at $currCU):\n{$userInfo["email"] }\n" );
666  } else {
667  // send email to user
668  if ( validateEmail( $userInfo["email"] ) ) {
669  # Email appears to be valid -- Send email with credit union name
670  MailUserInactive( $failCUMail, $failRec[5], $userInfo["email"] );
671  }
672  }
673 
674  if ( !$dryRun ) {
675  # Update Record in the database
676  $updSQL = "UPDATE cu_scheduledtxn
677  SET status = 'I',
678  end_date = current_date
679  WHERE id = '" . $failRec[0] . "'; ";
680 
681  $rs = db_query( $updSQL, $dbh );
682  if ( !$rs ) {
683  dmserror( $DMSMAIL, 'FATAL', "Failed Updating Deactivate Txns\n" . db_last_error() . "\n$updSQL");
684  }
685  }
686 
687  $updateMessage = "{$dryRunPrefix}Updating repeating transfer {$failRec[0]} to inactive, user {$userInfo["user_name"]} of {$currCU}\n";
688  fprintf( $STAT, $updateMessage );
689  }
690 
691  # #############
692  # Send email to Credit Union
693 
694  # ##########################
695  # EMAIL ANY CUs that have members drop off
696  # Loop through cuList
697 
698  # Loop through each cucode mentioned
699  if ( count( $cuList ) > 0 ) {
700  foreach( $cuList as $thisCU => $info ) {
701  if ( $dryRun ) {
702  fprintf( $STAT, "Dry Run: Email CU that account(s) dropped off: $thisCU, {$cuList[$thisCU]["accounts"]}\n" );
703  } else {
704  $message = "Hi,\n\nThe following user's had recurring transfer(s) were set to inactive.\n\n";
705  $message .= "{$cuList[$thisCU]["accounts"]}\n\n\n";
706  $message .= "HomeCU";
707 
708  MailCU( $pReplyEmail, $cuList[$thisCU]["email"], $message );
709  }
710  }
711  }
712 
713 
714 
715  #
716  // Get the unique list of users and accounts; we need the user to know where to send an email if a failure; it can be unique
717  // because the core will fail if there is a balance issue if there are multiple transfers from the same account; need to get
718  // cross-account authority from the core because some cores aren't good about checking that (go figure).
719  // Get both the from and to account so can tell if cross account or not
720  // Lower code will get the 'liveserver' value from cuadmin, so just using cuadmin to see if online
721 
722  if ( !$dryRun ) {
723  // save the run in the cutronus table (accountnumber is used for alerts)
724  $insert = "INSERT INTO cutronus (runslot, cu, user_id, sched_id, accountnumber) ";
725  $select = "SELECT distinct '{$config["runslot"]}', s.cu, s.user_id, id, ''
726  FROM cu_scheduledtxn s
727  JOIN cuadmin a on a.cu = s.cu
728  AND a.livebatch = 'L'
729  AND coalesce(a.offlinestat, 'N') not in ('Y','U')
730  WHERE (a.flagset2::bigint & {$CU2_PROCRECUR}::bigint) = $CU2_PROCRECUR
731  AND s.next_trigger_date <= '{$TODAY}'
732  AND s.status = 'A'
733  AND s.approved_status = 10
734  AND (s.end_date is NULL OR s.end_date >= s.next_trigger_date)
735  AND s.feature_code IN ('TRN', 'TRNM2M', 'TRNEXT', 'ACHCOL', 'ACHPMT')
736  $memberSQL $timezoneSQL";
737 
738  if ( $config["cucode"] != "" ) { $select .= " AND s.cu IN ({$config["cucode"]}) "; }
739  if ( $config["xcucode"] != "" ) { $select .= " AND s.cu NOT IN ({$config["xcucode"]}) "; }
740 
741  $shuffle = "UPDATE cutronus SET shuffle = random() where runslot = '{$config["runslot"]}'; ";
742 
743  // note the ';' before the shuffle query
744  $sql = "$insert $select; $shuffle";
745  $memRS = db_query( $sql, $dbh );
746 
747  if ( !$memRS ) {
748  dmserror( $DMSMAIL,'FATAL',"Failed populating cutronus table with accounts to process\n" . db_last_error() . "\n$sql\n" );
749  }
750 
751  fprintf( $STAT, " SQL to gather scheduled txns:\n\t\t\t $select\n" );
752  }
753 
754  $now = date( "D M d H:i:s Y" );
755  fprintf( $STAT, "{$dryRunPrefix}Load Tables for Regular Accounts Complete $now\n" );
756  // fprintf( $STAT, " with SQL: $select\n" );
757  } // end !$config["skipload"]
758 
759  // for a dry run, use a form of the query from earlier, that would have populated the cutronus table
760  if ( $dryRun ) {
761  $memsql = "SELECT s.cu, s.user_id, s.feature_code, s.txn_data, s.id, a.pname, an1.email,
762  s.approved_by, s.start_date, s.end_date, s.next_trigger_date, s.interval_count, s.repeating_parameters
763  FROM cu_scheduledtxn s
764  INNER JOIN cuadmin a on a.cu = s.cu
765  AND a.livebatch = 'L' and coalesce(a.offlinestat, 'N') not in ('Y','U')
766  AND (a.flagset2::bigint & {$CU2_PROCRECUR}::bigint) = $CU2_PROCRECUR
767  INNER JOIN cuadmnotify an1 ON an1.cu = s.cu
768  AND role = 'transfernotify'
769  WHERE s.next_trigger_date <= '{$TODAY}'
770  AND s.status = 'A'
771  AND (s.end_date is NULL OR s.end_date >= s.next_trigger_date)
772  AND s.feature_code IN ('TRN', 'TRNM2M', 'TRNEXT', 'ACHCOL', 'ACHPMT')
773  $memberSQL $timezoneSQL";
774 
775  if ( $config["cucode"] != "" ) { $memsql .= " AND s.cu IN ({$config["cucode"]}) "; }
776  if ( $config["xcucode"] != "" ) { $memsql .= " AND s.cu NOT IN ({$config["xcucode"]}) "; }
777  } else {
778  // We need a separate query for the user email because the credit union name is part of the table name
779  // The txn_data is a json array that needs to be processed different based on feature code
780  $memsql = "SELECT tron.cu, tron.user_id, s.feature_code, s.txn_data, s.id, a.pname, an1.email,
781  s.approved_by, s.start_date, s.end_date, s.next_trigger_date, s.interval_count, s.repeating_parameters
782  FROM cutronus tron
783  INNER JOIN cu_scheduledtxn s ON s.id = tron.sched_id
784  INNER JOIN cuadmin a on a.cu = s.cu
785  INNER JOIN cuadmnotify an1 ON an1.cu = s.cu
786  AND role = 'transfernotify'
787  WHERE runslot = '{$config["runslot"]}' ";
788 
789  if ( $config["cucode"] != "" ) { $memsql .= " AND tron.cu IN ({$config["cucode"]}) "; }
790  if ( $config["xcucode"] != "" ) { $memsql .= " AND tron.cu NOT IN ({$config["xcucode"]}) "; }
791 
792  // NOTE: need to handle member account filter while processing the records since it is buried in json array and dependant on transfer type
793  $memsql .= " order by shuffle ";
794  }
795 
796  if ( $config["limit"] != "" ) { $memsql .= " limit {$config["limit"]} "; }
797 
798  fprintf( $STAT, " Using sql to process scheduled txns:\n\t\t\t" . $memsql . "\n" );
799 
800  $memRS = db_query( $memsql, $dbh );
801 
802  if ( !$memRS ) {
803  dmserror( $DMSMAIL,'FATAL',"Failed Selecting Accounts to Process\n" . db_last_error() );
804  }
805 
806  $now = date( "D M d H:i:s Y" );
807  fprintf( $STAT, "{$dryRunPrefix}Fetching Account Packets Begin $now\n" );
808 
809  $showcount = "";
810 
811  /* This loop will
812  * - validate the user has access to the account
813  * - validate the transfer rights (internal/external, deposit/withdrawal) allow the transfer
814  * - validate the core allows (if flag set that core needs to check authority)
815  * - get each "from" account's balances (except for external -> local) (NOTE: really just need the misc1 field for applicable loans)
816  */
817  $row = 0;
818  $numProcessed = 0;
819  while ( $schedRow = db_fetch_assoc( $memRS, $row++ ) ) {
820  // get the values we need - cu, user_id, from_account, to_account, feature_code, type
821  $currCU = trim( $schedRow["cu"] );
822  $userId = $schedRow["user_id"];
823  $featureCode = trim( $schedRow["feature_code"] );
824  $txnData = HCU_JsonDecode( $schedRow["txn_data"], false );
825  $scheduleId = $schedRow["id"];
826  $pName = trim( $schedRow["pname"] );
827  $adminEmail = trim( $schedRow["email"] );
828  $approvedBy = $schedRow["approved_by"];
829  $startDate = $schedRow["start_date"];
830  $endDate = $schedRow["end_date"];
831  $nextTriggerDate = $schedRow["next_trigger_date"];
832  $intervalCount = $schedRow["interval_count"];
833  $repeatingParameters = HCU_JsonDecode( $schedRow["repeating_parameters"], false );
834  $fromAccountKey = Array();
835  $toAccountKey = Array();
836  $lockStatus = '';
837 
838  // get the type of transfer
839  $type = isset( $txnData["txn"]["type"] ) ? $txnData["txn"]["type"] : "";
840 
841  $localTransCode = trim($txnData["txn"]["transactioncode"]);
842 
843  // set the environment for this credit union
844  LoadCUAdmin( $dbh, $currCU, $HB_ENV );
845 
846  /**
847  * SET COMMON VALUES THAT WOULD NORMALLY BE IN COOKIE -- BUT LOAD FROM LoadCuAdmin results *
848  */
849  $HB_ENV['Fset'] = $HB_ENV['flagset'];
850  $HB_ENV['Fset2'] = $HB_ENV['flagset2'];
851  $HB_ENV['Fset3'] = $HB_ENV['flagset3'];
852  /* User Flags -- Should not change functionality */
853 
854  // adding flag so we don't load cross accounts during scheduled transaction processing
855  $HB_ENV["Fmsg_tx"] = GetMsgTxValue('MSGTX_TMP_XAX_LD');
856  $HB_ENV['Ml'] = '';
857  $HB_ENV['Fplog'] = '';
858 
859  // count doesn't need a message
860  AddToGatheredTransferStats( $gatheredStats, $currCU, "count", "" );
861 
862  if ( $txnData == "" ) {
863  $errorMessage = "Error extracting scheduled transaction data. CU: {$currCU}, User Id: {$usreId}";
864  if ( $dryRun ) {
865  fprintf( $STAT, "Dry Run: Would send error email to HomeCU: $errorMessage\n" );
866  } else {
867  dmserror( $DMSMAIL, 'FATAL', $errorMessage );
868  }
869  }
870 
871  if ( $repeatingParameters == "" ) {
872  $errorMessage = "Error with scheduled transaction repeating parameters. CU: {$currCU}, User Id: {$usreId}, id: $scheduleId";
873  if ( $dryRun ) {
874  fprintf( $STAT, "Dry Run: Would send error email to HomeCU: $errorMessage\n" );
875  } else {
876  dmserror( $DMSMAIL, 'FATAL', $errorMessage );
877  }
878  }
879 
880  // if only handlng certain accounts test that now
881  if ( $config["member"] != "") {
882  if ( $txnData["txn"]["txFromAcct"] != $config["member"] ) continue;
883  }
884 
885  // validate the user has access to the account and rights to do the transfer
886  // TRN - both accounts need to exist and have withdrawal (from) and deposit (to) rights
887  // TRNEXT - one account needs to exist and have appropriate rights
888  // TRNM2M/ACHPMT - from account needs to exist and have withdrawal right
889  // ACHCOL - to account needs to exist and have deposit rights
890  if ( $featureCode == "TRN" ) {
891  // let both from and to accounts through, but need to create the account info in the form T|XXX|YYY|Z
892  // we shouldn't have a transfer on certificate so we are okay with a zero for the certificate
893  $fromAccountId = $txnData["txn"]["from"];
894  $toAccountId = $txnData["txn"]["to"];
895 
896  $fromAccountKey = BuildAccountKey($txnData, "from");
897  $toAccountKey = BuildAccountKey($txnData, "to");
898 
899  } else if ( $featureCode == "TRNEXT" ) {
900  if ( $type == "X2L" ) {
901  // external to local - don't check from account
902  $fromAccountId = "";
903  $toAccountId = $txnData["txn"]["to"];
904  $toAccountKey = BuildAccountKey($txnData, "to");
905  } else {
906  // local to external - don't check to account
907  $fromAccountId = $txnData["txn"]["from"];
908  $toAccountId = "";
909  $fromAccountKey = BuildAccountKey($txnData, "from");
910  }
911  } else if ( $featureCode == "TRNM2M" || $featureCode == "ACHPMT" ) {
912  // local to external - don't check to account
913  $fromAccountId = $txnData["txn"]["from"];
914  $toAccountId = "";
915  $fromAccountKey = BuildAccountKey($txnData, "from");
916  } else if ( $featureCode == "ACHCOL" ) {
917  // external to local - don't check from account
918  $fromAccountId = "";
919  $toAccountId = $txnData["txn"]["to"];
920  $toAccountKey = BuildAccountKey($txnData, "to");
921  }
922 
923  $validAccess = UserHasRightsToTransfer( $dbh, $currCU, $userId, $fromAccountKey, $toAccountKey, $localTransCode);
924 
925  if ( !$validAccess ) {
926  $failMsg = "\nWarning: you no longer have rights to an account used in your repeating transfer.\n";
927  $failMsg .= "If this isn't corrected within {$FAILRPTAFTER} days the transfer will be set inactive.\n";
928 
929  // get user info
930  $userInfo = FetchUserInfo( $dbh, $currCU, $userId );
931 
932  // add the CU email to the cache in case need it again
933  if ( isset( $cuEmailCache[$currCU]["email"] ) ) {
934  $failCUMail = $cuEmailCache[$currCU]["email"];
935  } else {
936  $failCUMail = FetchCUEmail( $dbh, $currCU );
937 
938  // make sure we got an email
939  $failCUMail = ($failCUMail != "" ? $failCUMail : $DMSMAIL);
940 
941  $cuEmailCache[$currCU]["email"] = $failCUMail;
942  }
943 
944  if ( validateEmail( $userInfo["email"] ) ) {
945  if ( $dryRun ) {
946  fprintf( $STAT, "Dry Run: Would send error email to user: $failMsg\n" );
947  } else {
948  # Email appears to be valid -- Send email with credit union name
949  MailUserWarning( $failCUMail, $currCU, $userInfo["email"], $failMsg );
950  }
951  }
952 
953  $accountInfoForMessage = "FROM: $fromAccountId TO: $toAccountId";
954  $message = "User {$userInfo["user_name"]} no longer has rights to account ($accountInfoForMessage)\n";
955  AddToGatheredTransferStats( $gatheredStats, $currCU, "rights", $message );
956 
957  // skip to the next entry
958  continue;
959  }
960 
961  if (HCU_array_key_value("fromtype", $txnData['txn']) != 'X') {
962  // check if the from account is locked or readonly -- IF NOT type 'X'
963  //$parts = explode( "|", $fromAccountId );
964  $account = HCU_array_key_value("frommember", $txnData['txn']); /// $parts[1];
965  $lockStatus = CheckIfAccountLocked( $dbh, $currCU, $account );
966  }
967  if ( $lockStatus == "" &&
968  (HCU_array_key_value("totype", $txnData['txn']) != 'X') &&
969  (HCU_array_key_value("totype", $txnData['txn']) != 'M') ) {
970  // from account is okay, check the to account
971  //$parts = explode( "|", $toAccountId );
972  //$account = $parts[1];
973  $account = HCU_array_key_value("tomember", $txnData['txn']); /// $parts[1];
974  $lockStatus = CheckIfAccountLocked( $dbh, $currCU, $account );
975  }
976 
977  if ( $lockStatus != "" ) {
978  $lockType = $lockStatus == "R" ? "Readonly" : "Locked";
979  $failMsg = "\nWarning: an account used in your repeating transfer is marked $lockType.\n";
980  $failMsg .= "If this isn't corrected within {$FAILRPTAFTER} days the transfer will be set inactive.\n";
981 
982  // get user info
983  $userInfo = FetchUserInfo( $dbh, $currCU, $userId );
984 
985  // add the CU email to the cache in case need it again
986  if ( isset( $cuEmailCache[$currCU]["email"] ) ) {
987  $failCUMail = $cuEmailCache[$currCU]["email"];
988  } else {
989  $failCUMail = FetchCUEmail( $dbh, $currCU );
990 
991  // make sure we got an email
992  $failCUMail = ($failCUMail != "" ? $failCUMail : $DMSMAIL);
993 
994  $cuEmailCache[$currCU]["email"] = $failCUMail;
995  }
996 
997  if ( validateEmail( $userInfo["email"] ) ) {
998  if ( $dryRun ) {
999  fprintf( $STAT, "Dry Run: Would send error email to user: $failMsg\n" );
1000  } else {
1001  # Email appears to be valid -- Send email with credit union name
1002  MailUserWarning( $failCUMail, $currCU, $userInfo["email"], $failMsg );
1003  }
1004  }
1005 
1006  $accountInfoForMessage = "FROM: $fromAccountId TO: $toAccountId";
1007  $message = "User {$userInfo["user_name"]} is using a $lockType account ($accountInfoForMessage)\n";
1008  AddToGatheredTransferStats( $gatheredStats, $currCU, "rights", $message );
1009 
1010  // skip to the next entry
1011  continue;
1012  }
1013 
1014  if ( in_array( $featureCode, array( "TRN", "TRNM2M" ) ) ) {
1015  // make sure we have a From account
1016  if ( $fromAccountId == "" ) {
1017  // we need to have had a From account, so this is an error path
1018  $errorMessage = "Invalid transaction - missing From account\n";
1019  if ( $dryRun ) {
1020  fprintf( $STAT, "Dry Run: Would send error email to HomeCU: $errorMessage\n" );
1021  continue;
1022  } else {
1023  dmserror( $DMSMAIL,'FATAL',"$errorMessage\n" );
1024  }
1025  }
1026 
1027  // see if already gathered data on this From account
1028  if ( HaveAccountData( $dbh, $currCU, $fromAccountId, $config["runslot"] ) ) {
1029  // pretend we have good status for later logic
1030  $status = "000";
1031  } else {
1032  // TODO: WHEN FUNCTIONALITY EXISTS, CHECK CROSS-ACCOUNT AUTHORITY WITH CORE
1033 
1034  // TODO: check the appliance down list??
1035 
1036  // since we are about to contact the core, check the nap setting
1037  if ( $config["nap"] > 0 ) {
1038  sleep( $config["nap"] );
1039  }
1040 
1041  // Get the account balance only for the internal from account
1042 
1043  // set up some environment since calling banking function
1044  $HB_ENV["Cu"] = $currCU;
1045  $HB_ENV["cu"] = $currCU;
1046  $HB_ENV["Uid"] = $userId;
1047  // NOTE: dryRun will fetch and evaluate but not process
1048  // set the email to "NULL" because we don't know if we have the actual member or just a user
1049  $memberData = array( "account" => $fromAccountKey['accountnumber'], "cutoff" => $config["cutoff"], "packetstamp" => $config["pktstamp"], "email" => "NULL" );
1050 
1051  $fetchResp = FetchMemberDataTRON( $HB_ENV, $memberData );
1052 
1053  // For the packet status use the value returned from the core
1054  $status = $fetchResp["status"];
1055 
1056  if ( !isset( $fetchStats["Status_{$status}"] ) ) {
1057  $fetchStats["Status_{$status}"] = 1;
1058  } else {
1059  $fetchStats["Status_{$status}"]++;
1060  }
1061 
1062  // for the error code use the value returned from the function
1063  $code = $fetchResp["code"];
1064 
1065  if ( $code == '200' || $code == '201' || $code == '202' ) {
1066  // if just doing one credit union, give it a little quiet time
1067  if ( $cucode != "" ) { sleep( $config["nap"] + 300 ); }
1068  } else if ( $code != '000' ) {
1069  $reason = trim( $fetchResp["data"]["requestDesc"] );
1070  if ( !strlen( $reason ) ) {
1071  $reason = "Unknown reason";
1072  }
1073 
1074  fprintf( $STAT, "$row\tCU $currCU Member Account {$fromAccountKey['accountnumber']}\tFailed! $code - $reason\n" );
1075  }
1076 
1077  if ( $code == '001' ) {
1078  # -- I want to send an email to the Credit Union
1079  # They have a user w/repeating transaction(s) that is/are NO longer set up
1080  # in the Credit Union but is/are still set up in HomeBanking
1081  $emailMsg = "RECURRING TRANSACTION FAILED\n\n";
1082  $emailMsg .= "Member {$fromAccountKey['accountnumber']} does not exist or is not set up on your core system.\n\n\n\n";
1083  $emailMsg .= "($currCU: {$fromAccountKey['accountnumber']})";
1084 
1085  # FETCH THE CU EMAIL ACCOUNT
1086  $errCUMail = FetchCUEmail( $dbh, $currCU );
1087 
1088  $errCUMail = ($errCUMail > "" ? $errCUMail : $DMSMAIL);
1089 
1090  if ( $dryRun ) {
1091  fprintf( $STAT, "Dry Run: Would send error email to CU: $emailMsg\n" );
1092  } else {
1093  MailCU( $errCUMail, $errCUMail, $emailMsg );
1094  }
1095  } else if ( $code == "000" ) {
1096  $accountData = $fetchResp["data"]["requestData"];
1097  // update the cutronab, cutronah, cutronlb, cutronlh tables with the data just retrieved
1098  $loadResp = UpdateTRONTables( $HB_ENV, $config["runslot"], $memberData, $accountData );
1099 
1100  if ( $loadResp["status"]["code"] != "000" ) {
1101  $errorMessage = "Failure loading TRON tables: {$loadResp["status"]["error"]}.";
1102 
1103  if ( $dryRun ) {
1104  fprintf( $STAT, "Dry Run: Would send error email to HomeCU: $errorMessage\n" );
1105  } else {
1106  dmserror( $DMSMAIL, 'FATAL', $errorMessage );
1107  }
1108 
1109  // need to skip this transaction because of the error
1110  continue;
1111  }
1112  }
1113  }
1114  }
1115 
1116  // All transfers features will do this transfer section
1117 
1118  // TODO: If looking for potential performance improvements, this is where we would have a child process handle the actual scheduling of the transaction.
1119  if ( !$config["fetchonly"] ) {
1120  // need the authority account
1121 
1122  /**** TEMPORARY - Set up the txn data as to how it will be after things are cleaned up
1123  {"txn": { "from":"D|1103|0|0", "txFrom": "1103", "txFromSuffix": "0", "txFromType": "D", "to":"D|1103|10|0", "toacct": "1103", "tosuffix": "10", "totype": "D", "amount":11.11, "transactioncode": "AT", "deposittype":"D", "memo": "" } }
1124  ****/
1125  if ( !is_array( $txnData ) ) {
1126  $errorMessage = "Error processing scheduled transaction data. CU: {$currCU}, User Id: {$userId} No transaction data.";
1127  if ( $dryRun ) {
1128  fprintf( $STAT, "Dry Run: Would send error email to HomeCU: $errorMessage\n" );
1129  } else {
1130  dmserror( $DMSMAIL, 'FATAL', "$errorMessage\n" );
1131  }
1132  }
1133 
1134  $pName = ( $pName == "" ) ? "HomeCU" : $pName;
1135 
1136  // set up the "banking" environment
1137  $HB_ENV["Cu"] = $currCU;
1138  $HB_ENV["cu"] = $currCU;
1139  $HB_ENV["Uid"] = $userId;
1140 
1141  $txnData["txn_id"] = $scheduleId;
1142  $txnData["approved_by"] = $approvedBy;
1143  $txnData["frequency"] = $repeatingParameters['interval'];
1144  $txnData["interval"] = $intervalCount;
1145 
1146  $transferResult = HandleTransferRequest( $dbh, $HB_ENV, $featureCode, $txnData, $dryRun );
1147 
1148  // get user info
1149  $userInfo = FetchUserInfo( $dbh, $currCU, $userId );
1150 
1151  if ( $transferResult["status"]["code"] != "000" ) {
1152  // show there was an error
1153  $data = "From account: {$txnData["txn"]["from"]}, To account: {$txnData["txn"]["to"]}, Amount: {$txnData["txn"]["amount"]}";
1154  $message = "Unable to post transaction $scheduleId for CU: $currCU, user: {$userInfo["user_name"]}\n";
1155  $message .= "\t\t\t\tRequest=$featureCode\tData=({$data})\n";
1156  $message .= "\t\t\t\tResponse: {$transferResult["status"]["error"]}\n";
1157 
1158  // add this error to the stats
1159  AddToGatheredTransferStats( $gatheredStats, $currCU, "failed", $message );
1160  } else {
1161  // now, finally, update the schedule's next trigger date
1162  $interval = $repeatingParameters["interval"];
1163 
1164  // NOTE: There are logic errors in TxNextInterval and the parameters will need to change when that issue is fixed.
1165  $trigger = TxNextInterval( $startDate, $interval, $intervalCount + 1 );
1166  // if the next trigger date is after the end date then null is returned - this is handled in the function
1167 
1168  // create a success message
1169  $message = "Updating next transaction date ($trigger) txid $scheduleId for user {$userInfo["user_name"]}.";
1170 
1171  if ( !$dryRun ) {
1172  // update the next trigger date
1173  $updated = UpdateNextTriggerDate( $dbh, $scheduleId, $trigger );
1174 
1175  if ( !$updated ) {
1176  dmserror( $DMSMAIL, 'FATAL', "Error updating next trigger date for schedule id $scheduleId (cu: $currCU)\n" );
1177  }
1178  }
1179 
1180  // add this success to the stats
1181  AddToGatheredTransferStats( $gatheredStats, $currCU, "success", $message );
1182  }
1183 
1184  } // end if ( !fetchonly )
1185 
1186  $numProcessed++;
1187  } // end while
1188 
1189  return $numProcessed;
1190 } // end ProcessScheduledTransfers
1191 
1192 /**
1193 * Added: 04/16/2018
1194 * This function retrieves the offlinestat and offlineblurb attributes from the cuadmin table
1195 * for the current CU
1196 * Args:
1197 * pDbh - database handler
1198 * currCU - current CU admin
1199 * Returns:
1200 * offlinestat (Y/N) and offlineblurb values from the database
1201 * Logic:
1202 * First, if offlinestat is "", offlinestat is set to "N"
1203 * Then, if offlinestat != "N", offlinestat is set to "Y", denoting anything that is not "N" is "Y"
1204 */
1205 function GetCUAdminOfflineStatus($pDbh, $currCU) {
1206  $offlineStatSql = "SELECT trim(offlinestat), trim(offlineblurb)
1207  FROM cuadmin
1208  WHERE cu = '$currCU'";
1209 
1210  $rs = db_query($offlineStatSql, $pDbh);
1211 
1212  if ( !$rs ) {
1213  dmserror( $DMSMAIL, "FATAL", "Unable to obtain offline status from database: {$currCU}.\n" . db_last_error() . "\n$offlineStatSql\n" );
1214  //in case the execution passes, we set the flags to terminate it from the calling scope
1215  $returnOfflineStat = array( "offlinestat" => "unknown", "offlineblurb" => "unknown" );
1216  } else {
1217  $cuAdminRow = db_fetch_row( $rs );
1218  $thisOfflineStat = $cuAdminRow[0];
1219 
1220  # initial status "" is considered as "N"
1221  if ( $thisOfflineStat == "") {
1222  $thisOfflineStat = "N";
1223  }
1224  // anything other than "N" is considered "Y" i.e. U and R => Y
1225  if ( $thisOfflineStat != "N") {
1226  $thisOfflineStat = "Y";
1227  }
1228 
1229  $returnOfflineStat = array( "offlinestat" => $thisOfflineStat, "offlineblurb" => $cuAdminRow[1] );
1230  }
1231 
1232  return $returnOfflineStat;
1233 } // end GetCUAdminOfflineStatus
1234 
1235 /**
1236  * This function will perform all the ScheduledTransfers processing. All the configuration variables are passed in.
1237  * There are global variables used, as well.
1238  */
1239 function ProcessAlerts( $config ) {
1240  global $dbh;
1241  global $dryRun, $dryRunPrefix;
1242  global $HB_ENV;
1243  global $STAT;
1244  global $TODAY;
1245  global $DMSMAIL, $ALERTMAIL;
1246  global $memberSQL, $timezoneSQL;
1247  global $gatheredStats, $fetchStats;
1248 
1249  $CU2_ALLOW_MBR_ALERTS = 33554432; # Process Alerts flag
1250  $CU2_ESCHEMA = 2097152; # Expanded loan history schema
1251 
1252  // Update: 04/19/2018
1253  // Gotten rid of the dms_ia_head file fetching process.
1254  // We now directly fetch the offline status of the CUs from the database
1255  // rather than reading the list of offline CUs list from
1256  // '/home/homecu/logs/dms_ia_head/dms_ia_head.log.not_online_up' log file
1257 
1258  if ( !$config["skipload"] ) {
1259  // delete from alert temporary tables for the runslot
1260  // delete from temporary banking data tables for runslot
1261  $memSQL = "DELETE FROM cutronus WHERE runslot = '{$config["runslot"]}';
1262  DELETE FROM cutronab WHERE runslot = '{$config["runslot"]}';
1263  DELETE FROM cutronah WHERE runslot = '{$config["runslot"]}';
1264  DELETE FROM cutronlb WHERE runslot = '{$config["runslot"]}';
1265  DELETE FROM cutronlh WHERE runslot = '{$config["runslot"]}'; ";
1266  if ( $dryRun ) {
1267  fprintf( $STAT, "Dry run: would be deleting from tables\n $memSQL\n" );
1268  } else {
1269  $rs = db_query( $memSQL, $dbh );
1270  if ( !$rs ) {
1271  dmserror( $DMSMAIL, 'FATAL', "Failed Deleting From Tables\n" . db_last_error( $rs ) );
1272  }
1273 
1274  fprintf( $STAT, "Deleting from tables complete.\n" );
1275  }
1276 
1277  // query all the alert member accounts into the temporary table
1278  # ###
1279  # Sort of complex logic based on how several settings can determine the status of cucode
1280  # cucode can be set by either the runtime slot list OR on command line with -c
1281  # In theory the -t option is mutually exclusive to either -c or the runslot list
1282  #
1283  # Therefore if cucode is blank AND if timezone is NOT set
1284  # Then set query for cu = '', this will prevent the query from returning ALL the CUs on the server
1285  # Problem was discovered when a runslot was being executed but no files existed. The cu query was left off
1286  # so ALL CUs were returned
1287  #
1288  #
1289 
1290  if ( !$dryRun ) {
1291  // unresolved - I removed the timezone check as secondary to cu list - not sure why it was there
1292  $cuSQL = ($config["cucode"] != "" ? " AND a.cu IN ({$config["cucode"]}) " : "");
1293  $xcuSQL= ($config["xcucode"] != "" ? " AND a.cu NOT IN ({$config["xcucode"]}) " : "");
1294 
1295  // save the run in the cutronus table (accountnumber is used for alerts)
1296  $insert = "INSERT INTO cutronus (runslot, cu, user_id, sched_id, accountnumber) ";
1297 
1298  // lastalert is used for balance and loan but not transaction alerts; boolean flag for checknumber
1299  $select = "SELECT DISTINCT '{$config["runslot"]}', alert.cu, 0, 0, alert.accountnumber
1300  FROM cu_alerts alert
1301  JOIN cuadmin a on a.cu = alert.cu
1302  AND a.livebatch = 'L'
1303  AND coalesce(a.offlinestat, 'N') not in ('Y','U')
1304  AND (a.flagset2::bigint & {$CU2_ALLOW_MBR_ALERTS}::bigint) = {$CU2_ALLOW_MBR_ALERTS}
1305  $timezoneSQL $cuSQL $xcuSQL
1306  WHERE ((alert.alerttype IN ('B','T','L') AND (lastalert < CURRENT_DATE OR alert.lastalert IS NULL)) OR
1307  (alert.alerttype = 'C' AND alert.lastalert IS NULL)) ";
1308 
1309 
1310  $shuffle = "UPDATE cutronus SET shuffle = random() where runslot = '{$config["runslot"]}'; ";
1311 
1312  // note the ';' before the shuffle query
1313  $sql = "$insert $select; $shuffle";
1314  $memRS = db_query( $sql, $dbh );
1315 
1316  if ( !$memRS ) {
1317  dmserror( $DMSMAIL,'FATAL',"Failed populating cutronus table with accounts to process\n" . db_last_error() . "\n$sql\n" );
1318  }
1319 
1320  fprintf( $STAT, " SQL to gather alerts:\n\t\t\t $select\n" );
1321  }
1322 
1323  $now = date( "D M d H:i:s Y" );
1324  fprintf( $STAT, "Load Tables Complete $now\n" );
1325  }
1326 
1327  // Update 04/18/2018: We get rid of use of
1328  // /home/homecu/logs/dms_ia_head/dms_ia_head.log.not_online_up file
1329  // Previously: see if internet appliance "down" file exits
1330  // if ( false && !file_exists( $dms_ia_file ) ) {
1331  // printf( $STAT, "Internet Appliance down list $dms_ia_file not found!\n" );
1332  // dmserror( $DMSMAIL, "WARN", "Internet Appliance down list $dms_ia_file not found!\n" );
1333  // }
1334 
1335  // process credit union member accounts one at a time, randomly, tracking CU stats
1336  // NOTE: Cannot initially get user table info because need CU name as part of it.
1337  $memsql = "SELECT trim(tron.cu) as cu, trim(tron.accountnumber) as accountnumber
1338  FROM cutronus tron
1339  WHERE tron.runslot = '{$config["runslot"]}'";
1340 
1341  if ( $config["cucode"] != "" ) { $memsql .= " AND tron.cu in ({$config["cucode"]}) "; }
1342  if ( $config["xcucode"] != "" ) { $memsql .= " AND tron.cu not in ({$config["xcucode"]}) "; }
1343  if ( $config["member"] != "" ) { $memsql .= " AND accountnumber='{$config["member"]}' "; }
1344 
1345  $memsql .= " ORDER BY shuffle ";
1346 
1347  if ( $config["limit"] != "" ) { $memsql .= " limit {$config["limit"]} "; }
1348 
1349  fprintf( $STAT, "Using sql: $memsql\n" );
1350 
1351  $memRS = db_query( $memsql, $dbh );
1352 
1353  $cuInfoCache = array();
1354  $numProcessed = 0;
1355  $row = 0;
1356  while ( $alertRow = db_fetch_assoc( $memRS, $row++ ) ) {
1357  // start the timer for the gathering of this CU/account
1358  $thisTimeStart = microtime(true);
1359  $thisMessages = array();
1360 
1361  // gather credit union specific information - email, name, etc.
1362  $currCU = strtoupper( $alertRow["cu"] );
1363  $accountNumber = $alertRow["accountnumber"];
1364 
1365  // retrieve offlinestat, offlineblurb for currCU
1366  $offlineStat = GetCUAdminOfflineStatus($dbh, $currCU);
1367 
1368  if ( $offlineStat["offlinestat"] == "unknown" ) {
1369  dmserror( $DMSMAIL, "FATAL", "Unable to obtain offlinestat from database: {$currCU}.\n". db_last_error(). "\n" );
1370  }
1371 
1372  $isCUOffline = false;
1373 
1374  // continue the while loop if the offlinestat of the currCU is not N
1375  if ( $offlineStat["offlinestat"] != "N" ) {
1376  fprintf( $STAT, "{$currCU} is offline (status: '".$offlineStat["offlinestat"]."') ".$offlineStat["offlineblurb"]."'\n" );
1377  $thisMessages[] = "$row\tCU $currCU Account $accountNumber\tSkipped! Connection is down\n";
1378 
1379  $thisStat = array( "time" => 0, "messages" => $thisMessages );
1380  AddToGatheredAlertStats( $gatheredStats, $currCU, "gathering", $thisStat );
1381  // set the flat to indicate that the currCU is offline, just in case we don't want to continue here and wan't to do
1382  // further processing conditioning on this flag
1383  $isCUOffline = true;
1384  // continue with other record in $alertRow
1385  continue;
1386  }
1387 
1388  // Update 04/19/2018: conditioning on the isCUOffline flag as well
1389  if ( !$isCUOffline && !isset( $cuInfoCache[$currCU]) ) {
1390  // add the info for next time the credit union is used
1391 
1392  $cuSQL = "SELECT trim(pname) as pname, trim(user_name) as user_name,
1393  flagset, flagset2, flagset3, trim(orgname) as orgname
1394  FROM cuadmin
1395  WHERE cu = '$currCU' ";
1396 
1397  $rs = db_query( $cuSQL, $dbh );
1398 
1399  if ( !$rs ) {
1400  $errorMessage = "Failed getting admin info: {$config["runslot"]} \n" . db_last_error() . " SQL: $cuSQL\n";
1401  dmserror( $DMSMAIL, 'WARN', $errorMessage );
1402 
1403  // cannot really go on
1404  continue;
1405  }
1406 
1407  $adminRow = db_fetch_assoc( $rs, 0 );
1408 
1409  // get the notification addresses
1410  $sql = "SELECT trim(email) as email, trim(role) as role FROM cuadmnotify
1411  WHERE cu = '$currCU'
1412  AND role IN ('alert', 'txtbanking') ";
1413  $rs = db_query( $sql, $dbh );
1414 
1415  $row2 = 0;
1416  $alertNotify = "";
1417  $txtBankingNotify = "";
1418  while ( $notifyRow = db_fetch_assoc( $rs, $row2++ ) ) {
1419  if ( $notifyRow["role"] == "alert" ) {
1420  $alertNotify = trim( $notifyRow["email"] );
1421  } else {
1422  $txtBankingNotify = trim( $notifyRow["email"] );
1423  }
1424  }
1425 
1426  // need at least the "alert" email
1427  if ( strlen( $alertNotify ) == 0 ) {
1428  dmserror( $DMSMAIL, 'WARN', "Failed getting ALERTMAIL {$config["runslot"]} \n" . db_last_error() . " SQL: $sql\n" );
1429 
1430  // use HomeCU email
1431  $alertNotify = $ALERTMAIL;
1432  }
1433 
1434  $cuInfoCache[$currCU] = array(
1435  "alert" => $alertNotify,
1436  "txtbanking" => $txtBankingNotify,
1437  "flagset" => $adminRow["flagset"],
1438  "flagset2" => $adminRow["flagset2"],
1439  "flagset3" => $adminRow["flagset3"],
1440  "pname" => $adminRow["pname"],
1441  "user_name" => $adminRow["user_name"],
1442  "orgname" => $adminRow["orgname"]
1443  );
1444 
1445  }
1446 
1447  // set the environment for this credit union
1448  LoadCUAdmin( $dbh, $currCU, $HB_ENV );
1449 
1450  // some errors require us to skip processing but we still want to record the message
1451  $thisSkip = $isCUOffline;
1452 
1453  // see if already gathered data on this From account
1454  if ( HaveAccountData( $dbh, $currCU, $accountNumber, $config["runslot"] ) ) {
1455  // pretend we have good status for later logic
1456  $status = "000";
1457  } else {
1458  $lockLevel = CheckIfAccountLocked( $dbh, $currCU, $accountNumber );
1459 
1460  // if readonly, still can get alerted
1461  if ( $lockLevel == "L" ) {
1462  $thisMessages[] = "$row\tCU $currCU Account $accountNumber\tSkipped! Account is locked ($lockLevel)\n";
1463  } else {
1464  // TODO: WHEN FUNCTIONALITY EXISTS, CHECK CROSS-ACCOUNT AUTHORITY WITH CORE
1465 
1466  // Update: 04/19/2018: just an extra conditioning to check whether to skip the current alert row with currCU
1467  if ( !$thisSkip ) {
1468 
1469  // since we are about to contact the core, check the nap setting
1470  if ( $config["nap"] > 0 ) {
1471  sleep( $config["nap"] );
1472  }
1473 
1474  // set up some environment since calling banking function
1475  $HB_ENV["Cu"] = $currCU;
1476  $HB_ENV["cu"] = $currCU;
1477 
1478  // NOTE: dryRun will fetch and evaluate but not process
1479  // set the email to "NULL" because we don't know if we have the actual member or just a user
1480  $memberData = array( "account" => $accountNumber, "cutoff" => $config["cutoff"], "packetstamp" => $config["pktstamp"], "email" => "NULL" );
1481 
1482  $fetchResp = FetchMemberDataTRON( $HB_ENV, $memberData );
1483 
1484  // For the packet status use the value returned from the core
1485  $status = $fetchResp["status"];
1486 
1487  if ( !isset( $fetchStats["Status_{$status}"] ) ) {
1488  $fetchStats["Status_{$status}"] = 1;
1489  } else {
1490  $fetchStats["Status_{$status}"]++;
1491  }
1492 
1493  // log each member access
1494  $reason = $fetchResp["data"]["requestDesc"];
1495  $now = date( "D M d H:i:s Y" );
1496  $thisMessages[] = "$row\tCU $currCU Account $accountNumber\t $status - $reason $now\n";
1497 
1498  // for the error code use the value returned from the function
1499  $code = $fetchResp["code"];
1500 
1501  if ( $code == '200' || $code == '201' || $code == '202' ) {
1502  // if just doing one credit union, give it a little quiet time
1503  if ( $config["cucode"] != "" ) { sleep( $config["nap"] + 300 ); }
1504  } else if ( $code != '000' ) {
1505  $reason = trim( $fetchResp["data"]["requestDesc"] );
1506  if ( !strlen( $reason ) ) {
1507  $reason = "Unknown reason";
1508  }
1509 
1510  $thisMessages[] = "$row\tCU $currCU Member Account {$accountNumber}\tFailed! $code - $reason\n";
1511  $thisSkip = true;
1512  }
1513 
1514  if ( $code == '001' ) {
1515  # -- I want to send an email to the Credit Union
1516  # They have a user with alerts on an account that is/are NO longer set up
1517  # in the Credit Union but is/are still set up in HomeBanking
1518  $emailMsg = "PROCESSING ALERTS FAILED\n\n";
1519  $emailMsg .= "Member Account {$accountNumber} does not exist or is not set up on your core system.\n\n\n\n";
1520 
1521  # FETCH THE CU EMAIL ACCOUNT
1522  $errCUMail = FetchCUEmail( $dbh, $currCU );
1523 
1524  $errCUMail = ($errCUMail > "" ? $errCUMail : $DMSMAIL);
1525 
1526  if ( $dryRun ) {
1527  fprintf( $STAT, "Dry Run: Would send error email to CU: $emailMsg\n" );
1528  } else {
1529  MailCU( $errCUMail, $errCUMail, $emailMsg );
1530  }
1531  } else if ( $code == "000" ) {
1532  $accountData = $fetchResp["data"]["requestData"];
1533  // update the cutronab, cutronah, cutronlb, cutronlh tables with the data just retrieved
1534  $loadResp = UpdateTRONTables( $HB_ENV, $config["runslot"], $memberData, $accountData );
1535 
1536  if ( $loadResp["status"]["code"] != "000" ) {
1537  $errorMessage = "Failure loading TRON tables: {$loadResp["status"]["error"]}.";
1538 
1539  if ( $dryRun ) {
1540  fprintf( $STAT, "Dry Run: Would send error email to HomeCU: $errorMessage\n" );
1541  } else {
1542  dmserror( $DMSMAIL, 'FATAL', $errorMessage );
1543  }
1544 
1545  // need to skip this transaction because of the error (only for dry run)
1546  continue;
1547  }
1548  }
1549  }
1550  }
1551  }
1552 
1553  $thisTimeEnd = microtime(true);
1554  $thisTime = $thisTimeEnd - $thisTimeStart;
1555 
1556  $thisStat = array( "time" => $thisTime, "messages" => $thisMessages );
1557  AddToGatheredAlertStats( $gatheredStats, $currCU, "gathering", $thisStat );
1558 
1559  // no processing if fetchonly
1560  if ( !$thisSkip && !$config["fetchonly"] ) {
1561  // process all users of this CU's member account
1562  // NOTE: this will process all the alerts for the account
1563  $results = ProcessAlertsForAccount( $HB_ENV, $cuInfoCache[$currCU], $currCU, $accountNumber, $config );
1564 
1565  // gather the returned stats/messages
1566  AddToGatheredAlertStats( $gatheredStats, $currCU, "sent_stats", $results );
1567  AddToGatheredAlertStats( $gatheredStats, $currCU, "sending", $results );
1568  }
1569 
1570  $numProcessed++;
1571  }
1572 
1573  return $numProcessed;
1574 } // end ProcessAlerts
1575 
1576 
1577 // Process the alerts for the given credit union member account. It is assumed the
1578 // correct amount of the latest account data was obtained before calling this function.
1579 function ProcessAlertsForAccount( $pHbEnv, $cuInfo, $currCU, $account, $config ) {
1580  global $STDOUT;
1581  global $STAT;
1582  global $DMSMAIL;
1583 
1584  $alertReturn = array( "time" => 0, "user_count" => 0, "alert_count" => 0, "messages" => array(), "alert_stats" => array() );
1585 
1586  $thisTimeStart = microtime(true);
1587 
1588  $dbh = $pHbEnv["dbh"];
1589  $orgname = $cuInfo["orgname"];
1590 
1591  // global configuration flag set in admin
1592  $CU_CALCRUNBAL = 64;
1593 
1594  $ALERTFROMADDR = "noreply@homecu.net";
1595  $ALERTFROMNAME = $orgname;
1596 
1597  // get any alert messages the CU has set up
1598  $shortAlertMsg = array();
1599  $longAlertMsg = array();
1600  $numAlertMsgs = 0;
1601  $sql = "SELECT message, sms_message
1602  FROM cualertmsgs
1603  WHERE cu ='$currCU'
1604  AND (startdate <= CURRENT_DATE AND CURRENT_DATE <= stopdate)
1605  AND runstat = 1 ";
1606 
1607  $rs = db_query( $sql, $dbh );
1608 
1609  if ( !$rs ) {
1610  $errorMsg = "Failed getting AlertMsgs {$config["runslot"]} \n" . db_last_error() . " SQL: $sql\n";
1611  dmserror( $DMSMAIL, 'WARN', $errorMsg );
1612 
1613  // save the error
1614  $alertReturn["messages"][] = $errorMsg;
1615  } else {
1616  $row = 0;
1617  while ( $msgRow = db_fetch_assoc( $rs, $row++ ) ) {
1618  $longAlertMsg[] = $row["message"];
1619  $shortAlertMsg[] = $row["sms_message"];
1620 
1621  $numAlertMsgs++;
1622  }
1623 }
1624 
1625  // set up all the alert select and update queries for this user
1626  $select = array();
1627  $update = array();
1628 
1629  // the select fields need to all be in a specific order
1630  $select["CurrentBalance"] =
1631  "SELECT alert.id, alert.user_id, alert.emailtype, trim(alert.notifyto) as notifyto,
1632  trim(alert.provider_id) as provider_id, trim(alert.notifymsg) as notifymsg,
1633  0 as incamt, 0 as amount,
1634  alert.incbal, acct.amount as balance, alert.alerttype
1635  FROM cu_alerts alert
1636  JOIN cutronab acct ON acct.cu = alert.cu
1637  AND acct.accountnumber = alert.accountnumber
1638  AND acct.accounttype = alert.accounttype
1639  AND acct.certnumber = alert.certnumber
1640  AND acct.runslot = '{$config["runslot"]}'
1641  AND acct.amount < alert.notifyamt
1642  WHERE alert.cu = '$currCU'
1643  AND alert.accountnumber='$account'
1644  AND (alert.lastalert IS NULL OR alert.lastalert < CURRENT_DATE)
1645  AND coalesce( alert.useavailbal, 0 ) = 0
1646  AND alert.alerttype = 'B' ";
1647 
1648  $update["CurrentBalance"] =
1649  "UPDATE cu_alerts SET lastalert = CURRENT_TIMESTAMP
1650  FROM cutronab acct
1651  WHERE acct.cu = cu_alerts.cu
1652  AND acct.accountnumber = cu_alerts.accountnumber
1653  AND acct.accounttype = cu_alerts.accounttype
1654  AND acct.certnumber = cu_alerts.certnumber
1655  AND acct.runslot = '{$config["runslot"]}'
1656  AND acct.amount < cu_alerts.notifyamt
1657  AND (cu_alerts.lastalert IS NULL OR cu_alerts.lastalert < CURRENT_DATE)
1658  AND cu_alerts.cu = '$currCU'
1659  AND cu_alerts.accountnumber = '$account'
1660  AND coalesce( cu_alerts.useavailbal, 0 ) = 0
1661  AND cu_alerts.alerttype = 'B' ";
1662 
1663  #########
1664  # Available Balance Alerts
1665  #########
1666  $select["AvailableBalance"] =
1667  "SELECT alert.id, alert.user_id, alert.emailtype, trim(alert.notifyto) as notifyto,
1668  trim(alert.provider_id) as provider_id, trim(alert.notifymsg) as notifymsg,
1669  0 as incamt, 0 as amount,
1670  alert.incbal, acct.available as balance, alert.alerttype
1671  FROM cu_alerts as alert
1672  JOIN cutronab as acct ON acct.cu = alert.cu
1673  AND acct.accountnumber = alert.accountnumber
1674  AND acct.accounttype = alert.accounttype
1675  AND acct.certnumber = alert.certnumber
1676  AND acct.runslot = '{$config["runslot"]}'
1677  AND acct.available < alert.notifyamt
1678  WHERE alert.cu = '$currCU'
1679  AND alert.accountnumber ='$account'
1680  AND (alert.lastalert IS NULL OR alert.lastalert < CURRENT_DATE)
1681  AND coalesce( alert.useavailbal, 0 ) > 0
1682  AND alert.alerttype = 'B' ";
1683 
1684  $update["AvailableBalance"] =
1685  "UPDATE cu_alerts SET lastalert = CURRENT_TIMESTAMP
1686  FROM cutronab acct
1687  WHERE acct.cu = cu_alerts.cu
1688  AND acct.accountnumber = cu_alerts.accountnumber
1689  AND acct.accounttype = cu_alerts.accounttype
1690  AND acct.certnumber = cu_alerts.certnumber
1691  AND acct.runslot = '{$config["runslot"]}'
1692  AND acct.available < cu_alerts.notifyamt
1693  AND (cu_alerts.lastalert IS NULL OR cu_alerts.lastalert < CURRENT_DATE)
1694  AND cu_alerts.cu = '{$currCU}'
1695  AND cu_alerts.accountnumber = '$account'
1696  AND coalesce( cu_alerts.useavailbal, 0 ) > 0
1697  AND cu_alerts.alerttype = 'B' ";
1698 
1699  #########
1700  # Check Alerts
1701  #########
1702  $select["Check"] =
1703  "SELECT alert.id, alert.user_id, alert.emailtype, trim(alert.notifyto) as notifyto,
1704  trim(alert.provider_id) as provider_id, trim(alert.notifymsg) as notifymsg,
1705  alert.incamt, acct.amount,
1706  0 incbal, 0 balance, alert.alerttype
1707  FROM cu_alerts as alert
1708  JOIN cutronah as acct ON acct.cu = alert.cu
1709  AND acct.accountnumber = alert.accountnumber
1710  AND acct.accounttype = alert.accounttype
1711  AND acct.certnumber = alert.certnumber
1712  AND trim(acct.checknumber) = trim(alert.notifychknum)
1713  AND acct.runslot = '{$config["runslot"]}'
1714  WHERE alert.cu = '{$currCU}'
1715  AND alert.accountnumber = '$account'
1716  AND alert.lastalert IS NULL
1717  AND alert.alerttype = 'C' ";
1718  $update["Check"] =
1719  "UPDATE cu_alerts SET lastalert = CURRENT_TIMESTAMP
1720  FROM cutronah as acct
1721  WHERE acct.cu = cu_alerts.cu
1722  AND acct.accountnumber = cu_alerts.accountnumber
1723  AND acct.accounttype = cu_alerts.accounttype
1724  AND acct.certnumber = cu_alerts.certnumber
1725  AND acct.runslot = '{$config["runslot"]}'
1726  AND acct.checknumber = cu_alerts.notifychknum
1727  AND cu_alerts.lastalert is null
1728  AND cu_alerts.cu = '{$currCU}'
1729  AND cu_alerts.accountnumber ='$account'
1730  AND cu_alerts.alerttype = 'C' ";
1731 
1732  $select["Payment"] =
1733  "SELECT alert.id, alert.user_id, alert.emailtype, trim(alert.notifyto) as notifyto,
1734  trim(alert.provider_id) as provider_id, trim(alert.notifymsg) as notifymsg,
1735  0 as incamt, 0 as amount,
1736  0 as incbal, 0 as balance, alert.alerttype
1737  FROM cu_alerts as alert
1738  JOIN cutronlb as loan ON loan.cu = alert.cu
1739  AND loan.accountnumber = alert.accountnumber
1740  AND loan.loannumber = alert.accounttype
1741  AND loan.runslot = '{$config["runslot"]}'
1742  AND (loan.nextduedate - (coalesce(alert.notifyloandaysprior, 0) || ' days')::interval) < CURRENT_TIMESTAMP
1743  AND loan.currentbalance > 0
1744  WHERE alert.cu = '{$currCU}'
1745  AND alert.accountnumber = '$account'
1746  AND (alert.lastalert IS NULL OR alert.lastalert < CURRENT_DATE)
1747  AND alert.alerttype = 'L' ";
1748  $update["Payment"] =
1749  "UPDATE cu_alerts SET lastalert = CURRENT_TIMESTAMP
1750  FROM cutronlb as loan
1751  WHERE loan.cu = cu_alerts.cu
1752  AND loan.accountnumber = cu_alerts.accountnumber
1753  AND loan.loannumber = cu_alerts.accounttype
1754  AND loan.runslot = '{$config["runslot"]}'
1755  AND (loan.nextduedate - (coalesce(cu_alerts.notifyloandaysprior, 0) || ' days')::interval) < CURRENT_TIMESTAMP
1756  AND (cu_alerts.lastalert IS NULL OR cu_alerts.lastalert < CURRENT_DATE)
1757  AND loan.currentbalance > 0
1758  AND cu_alerts.cu = '{$currCU}'
1759  AND cu_alerts.accountnumber='$account'
1760  AND cu_alerts.alerttype = 'L' ";
1761 
1762  # ##################
1763  # mws 2/11/2015 -
1764  # removed the comparison to ~*, this is a regular expression compare and the string in alert.notifydesc could contain
1765  # regular expression items ie ( ) {} [], this caused a problem when the member typed (:
1766  # also, the extra processing for re is not desired. found an equivalent with ilike
1767  # ~* alert.notifydesc
1768  # ilike '%' || alert.notifydesc || '%'
1769  # ##################
1770  // Just get the most recent trace number for the subaccount because all trace numbers were
1771  // examine as part of the query and wouldn't match the next time the query ran anyway.
1772  $select["Transaction"] =
1773  "SELECT alert.id, alert.user_id, alert.emailtype, trim(alert.notifyto) as notifyto,
1774  trim(alert.provider_id) as provider_id,
1775  case when coalesce(inctransdesc, 0) = 1
1776  then trim(acct.description)
1777  else trim(alert.notifymsg)
1778  end as notifymsg,
1779  alert.incamt, acct.amount,
1780  alert.incbal, acct.balance, alert.alerttype
1781  FROM cu_alerts as alert
1782  JOIN cutronah as acct ON acct.cu = alert.cu
1783  AND acct.accountnumber = alert.accountnumber
1784  AND acct.accounttype = alert.accounttype
1785  AND acct.certnumber = alert.certnumber
1786  AND acct.description ilike '%' || alert.notifydesc || '%'
1787  AND acct.runslot = '{$config["runslot"]}'
1788  WHERE alert.cu = '{$currCU}'
1789  AND alert.accountnumber = '$account'
1790  AND (coalesce( alert.notifyrange, 0 ) = 0
1791  OR (coalesce( alert.notifyrange, 0 ) > 0 AND abs(acct.amount) > alert.notifyamtmin AND abs(acct.amount) < alert.notifyamtmax))
1792  AND ((alert.notifytranstype = 'B')
1793  OR (alert.notifytranstype = 'D' AND acct.amount > 0)
1794  OR (alert.notifytranstype = 'W' AND acct.amount < 0))
1795  AND (alert.lasttrace IS NULL OR alert.lasttrace < acct.tracenumber)
1796  AND alert.alerttype = 'T' ";
1797 
1798  $update["Transaction"] =
1799  "UPDATE cu_alerts SET
1800  lastalert = CURRENT_TIMESTAMP,
1801  lasttrace = (SELECT max(tracenumber)
1802  FROM cutronah as hist
1803  WHERE hist.cu = acct.cu
1804  AND hist.accountnumber = acct.accountnumber
1805  AND hist.accounttype = acct.accounttype
1806  AND hist.certnumber = acct.certnumber
1807  AND hist.runslot = '{$config["runslot"]}')
1808  FROM cutronah as acct
1809  WHERE acct.cu = cu_alerts.cu
1810  AND acct.runslot = '{$config["runslot"]}'
1811  AND acct.accountnumber = cu_alerts.accountnumber
1812  AND acct.accounttype = cu_alerts.accounttype
1813  AND acct.certnumber = cu_alerts.certnumber
1814  AND acct.description ilike '%' || cu_alerts.notifydesc || '%'
1815  AND (coalesce( cu_alerts.notifyrange, 0 ) = 0
1816  OR (abs(acct.amount) > cu_alerts.notifyamtmin AND abs(acct.amount) < cu_alerts.notifyamtmax))
1817  AND ((cu_alerts.notifytranstype = 'B')
1818  OR (cu_alerts.notifytranstype = 'D' AND acct.amount > 0)
1819  OR (cu_alerts.notifytranstype = 'W' AND acct.amount < 0))
1820  AND (cu_alerts.lasttrace IS NULL OR cu_alerts.lasttrace < acct.tracenumber)
1821  AND cu_alerts.cu = '{$currCU}'
1822  AND cu_alerts.accountnumber='$account'
1823  AND cu_alerts.alerttype = 'T' ";
1824 
1825  // use an array to hold unique users for later counting
1826  $uniqueUserList = array();
1827 
1828  #
1829  # Since the logic for each alert type is so close, we churn thru
1830  # a hash of selects/updates to process the alerts.
1831  #
1832  #print STAT `date +'%a %b %d %T %Y %Z'`;
1833  foreach ( $select as $key => $query ) {
1834  $alertReturn["alert_stats"]["sent.$key"] = 0;
1835  $alertReturn["alert_stats"]["upd.$key"] = 0;
1836 
1837  $alertReturn["messages"][] = "Processing $currCU $account $key Alerts";
1838 
1839  $rs = db_query( $query, $dbh );
1840 
1841  if ( !$rs ) {
1842  $errorMsg = "Failed getting $key Alerts {$config["runslot"]} \n" . db_last_error() . " SQL: $query\n";
1843  dmserror( $DMSMAIL, 'WARN', $errorMsg );
1844 
1845  $alertReturn["messages"][] = $errorMsg;
1846 
1847  // keep going
1848  continue;
1849  }
1850 
1851  $alertIdList = array();
1852  $msgPick = 0;
1853  $row = 0;
1854  while ( $alertRow = db_fetch_assoc( $rs, $row++ ) ) {
1855  $userId = $alertRow["user_id"];
1856  if ( !in_array( $userId, $uniqueUserList ) ) {
1857  $uniqueUserList[] = $userId;
1858  }
1859 
1860  $alertReturn["alert_stats"]["sent.$key"]++;
1861 
1862  $alertType = $alertRow["alerttype"];
1863 
1864  $msg = $alertRow["notifymsg"];
1865 
1866  // what we show depends on alert type
1867  if ( $alertType != "L" ) {
1868  if ( $alertRow["incamt"] > 0 ) {
1869  $msg .= "\nAmount = {$alertRow["amount"]}";
1870  }
1871 
1872  if ( $alertType == "B" ) {
1873  $msg .= "\nBalance = {$alertRow["balance"]}";
1874  } else if ( $alertType == "T" ) {
1875  // only include the balance if the CU_CALCURUNBAL flag is not set
1876  // -- otherwise we haven't got a good balance to show
1877  if ( ($cuInfo["flagset"] & $CU_CALCRUNBAL) != $CU_CALCRUNBAL ) {
1878  if ( $alertRow["incbal"] > 0 ) {
1879  $msg .= "\nBalance = {$alertRow["balance"]}";
1880  }
1881  }
1882  }
1883  }
1884 
1885  $to = $alertRow["notifyto"];
1886  $alertLongCode = $cuInfo["txtbanking"];
1887  $alertMail = $cuInfo["alert"];
1888 
1889  $mask= GetMask($dbh, $pHbEnv["Cu"]);
1890  $mask= $mask["code"] != 0 ? array("start" => -2) : $mask["data"]; // Just return the default mask in this case.
1891  $accountPortion= ApplyMask($account, $mask);
1892 
1893  if ( $alertRow["emailtype"] == 'W' ) {
1894  // wireless/SMS
1895 
1896  // customize the message based on type
1897  $preamble = "";
1898  switch( $alertType ) {
1899  case "B":
1900  $preamble .= "Low Balance";
1901  break;
1902  case "C":
1903  $preamble .= "Check Number";
1904  break;
1905  case "L":
1906  $preamble .= "Loan Payment Due";
1907  break;
1908  case "T":
1909  $preamble .= "Transaction Found";
1910  break;
1911  }
1912  $msg = "Account ending {$accountPortion} {$preamble}\n{$msg}";
1913 
1914  if ( isset( $shortAlertMsg[$msgPick] ) ) {
1915  $msg .= "\n" . $shortAlertMsg[$msgPick];
1916  }
1917 
1918 
1919  # Determine if I am sending this via Long code or mail
1920 
1921  // * Are we using AWS SMS or Long Code?
1922 
1923  if ($cuInfo['flagset3'] & GetFlagsetValue('CU3_LONGCODE_MFA')) {
1924  // ** Use a LONG CODE of some sort, if CU has a custom, then use it.
1925  // ** Otherwise use the Roundrobin list
1926  # check if CU has a longcode to use
1927  if ( preg_match( "/^[+]{0,1}[0-9]{1,12}$/", $alertLongCode ) && $alertLongCode != '' ) {
1928  SmsLongcode( 'SMS', $alertLongCode, $to, $msg, $account, '' );
1929  } else {
1930  # use our longcode
1931  SmsInternalLongcode( $cuInfo["alert"], $to, $msg, $account, $ALERTFROMNAME );
1932  }
1933  } else {
1934  // ** Send via AWS, but use SMSLongCode function to standardize message
1935  SmsLongcode( 'AWS', $alertLongCode, $to, $msg, $account, "$ALERTFROMNAME" );
1936  }
1937  } else {
1938  // html email
1939 
1940  // customize the message based on type
1941  $preamble = "";
1942  switch( $alertRow["alerttype"] ) {
1943  case "B":
1944  $preamble .= "The balance is below the specified amount.\n";
1945  break;
1946  case "C":
1947  $preamble .= "A check with the specified check number has been found.\n";
1948  break;
1949  case "L":
1950  $preamble .= "There is a loan payment due.\n";
1951  break;
1952  case "T":
1953  $preamble .= "A transaction has been found with the specified phrase.\n";
1954  break;
1955  }
1956 
1957  // add some words around the message
1958  $msg = "Account ending with $accountPortion has an alert.\n"
1959  . "<br /><br />\n"
1960  . "$preamble\n"
1961  . "<br /><br />\n"
1962  . "<pre>$msg</pre>\n";
1963 
1964  if ( isset( $longAlertMsg[$msgPick] ) ) {
1965  $msg .= "<hr>" . $longAlertMsg[$msgPick++];
1966  }
1967 
1968  HtmlMail( $alertMail, $to, $msg, $ALERTFROMADDR, $ALERTFROMNAME);
1969  }
1970 
1971  // count this alert
1972  $alertReturn["alert_count"]++;
1973 
1974  // rotate through the alert messages
1975  if ( $msgPick >= $numAlertMsgs ) {
1976  $msgPick = 0;
1977  }
1978  }
1979 
1980  if ( $rs ) {
1981  // update Last Alert Date for the affected alerts
1982 # print STAT "Updating $key Alerts. \n";
1983 
1984  $rs = db_query( $update[$key], $dbh );
1985 
1986 # print $conn->errorMessage,"\n"
1987  if ( !$rs ) {
1988  $errorMsg = "Failed updating $key Alerts {$config["runslot"]} \n" . db_last_error() . "SQL: {$update[$key]}\n";
1989  dmserror( $DMSMAIL, 'WARN', $errorMsg );
1990 
1991  $alertReturn["messages"][] = $errorMsg;
1992 
1993  // don't stop
1994  } else {
1995 # fprintf( $STAT, "$currCU $key Alert(s) Updated. $update[$key]\n" );
1996  // NOTE: the update count may not equal the sent count because the sent count is for the
1997  // number of transactions found and the update count is the number of alert entries found.
1998  $alertReturn["alert_stats"]["upd.$key"] += db_affected_rows( $rs );
1999  }
2000  }
2001  } # End foreach
2002 
2003  // add the user count
2004  $alertReturn["user_count"] = count( $uniqueUserList );
2005 
2006  $thisTimeEnd = microtime(true);
2007 
2008  $alertReturn["time"] = $thisTimeEnd - $thisTimeStart;
2009 
2010  return $alertReturn;
2011 } // end ProcessAlertsForAccount
2012 
2013 /* See if we already retrieved member account data for this account/runslot.
2014  *
2015  * All accounts should have a share account which shows up in the cutronab table so only check that.
2016  * Accounts come in the form "D|1103|5|0" or as a simple account number. Since all subaccounts are
2017  * read with the balance info (cutronab is balances) we only need to check if the account number exists.
2018  */
2019 function HaveAccountData( $pDbh, $pCurrCU, $pAccount, $pRunslot ) {
2020  $haveAccount = false;
2021 
2022  if ( strpos( $pAccount, "|" ) > 0 ) {
2023  $parts = explode( "|", $pAccount );
2024  $type = $parts[0];
2025  $account = $parts[1];
2026  $subAccount = $parts[2];
2027  } else {
2028  $account = $pAccount;
2029  }
2030 
2031  $sql = "SELECT count(*) as count
2032  FROM cutronab
2033  WHERE cu = '$pCurrCU'
2034  AND runslot = '$pRunslot'
2035  AND accountnumber = '$account'";
2036 
2037  $rs = db_query( $sql, $pDbh );
2038  if ( $rs ) {
2039  $accountRow = db_fetch_row( $rs, 0 );
2040 
2041  $haveAccount = $accountRow[0] > 0;
2042  }
2043 
2044  return $haveAccount;
2045 } // end HaveAccountData
2046 
2047 /* Take the raw data returned from the core and load into the TRON tables. Remove current runslot
2048  * data first.
2049  */
2050 function UpdateTRONTables( $pEnv, $pRunSlot, $pMemberData, $pRawData ) {
2051  $returnError = array(
2052  "status" => array( "code"=>'000', "error" => "" )
2053  );
2054 
2055  try {
2056  // Update tables cu_alertab, cu_alertah, cu_alertlb, cu_alertlh.
2057  $dbh = HCU_array_key_value('dbh', $pEnv);
2058 
2059  /* START TRANSACTION */
2060  $startRs = db_work( $dbh, HOMECU_WORK_BEGIN );
2061 
2062  // ** Traverse to the Inquiry Node
2063  $pktInquiryNode = $pRawData->Inquiry;
2064 
2065  /*
2066  * Removed cutronlh from the data insert.
2067  * This table has extra columns {fee, escrow} that not all credit union interfaces
2068  * provide. However, this table is NOT used for the "Transaction" alerts. Decided
2069  * to just not insert the tables.
2070  * This only has an impact if we ever decide to include loan history for the
2071  * "Transaction" alert searches.
2072  */
2073  $updTableList = array( "cutronab", "cutronah", "cutronlb" );
2074  $updTableMeta = array( "cutronab" => array( "node" => 'AccountBalance' ),
2075  "cutronah" => array( "node" => "AccountHistory"),
2076  "cutronlb" => array( "node" => "LoanBalance") );
2077 
2078  /* ** TABLE LOOP ** */
2079  foreach ( $updTableList as $updTableName ) {
2080 
2081  // ** First Delete existing rows
2082  $sql = "DELETE FROM $updTableName
2083  WHERE runslot = '$pRunSlot'
2084  AND cu = '{$pEnv["Cu"]}'
2085  AND accountnumber = '" . prep_save( $pMemberData["account"], 12 ) . "'";
2086  $result = db_query( $sql, $dbh );
2087 
2088  // start the data copy
2089  $cp = "copy {$updTableName} from stdin with null as ''";
2090  $result = db_query( $cp, $dbh );
2091 
2092  // ** Search for the expected <node> in the packet
2093  $tblPktData = $pktInquiryNode->{ HCU_array_key_value( 'node', HCU_array_key_value( $updTableName, $updTableMeta ) ) };
2094 
2095  $dataRecords = explode("\n", $tblPktData);
2096 
2097  while (list($key, $line) = each($dataRecords)) {
2098  if ( trim( $line ) == "" ) {
2099  continue;
2100  }
2101 
2102  // ** massage some bits in the data -- sanitize special characters
2103  $line = preg_replace("/ ?\t ?/", "\t", $line);
2104  $line = preg_replace("/ $/", "", $line);
2105  $line = preg_replace("/\\0/", " ", $line);
2106 
2107  // add the runslot to the beginning
2108  # ADD RUNSLOT PRIOR TO CU CODE
2109  $line = "{$pRunSlot}\t{$pEnv["Cu"]}\t" . $line;
2110 
2111  pg_put_line( $dbh, $line . "\n" );
2112  }
2113 
2114  // ** Terminate the Copy with a \.
2115  $endCopyRs = pg_put_line( $dbh, "\\.\n" );
2116 
2117  if ( !$endCopyRs ) {
2118  // ** Error was found
2119  throw new Exception ("Error with the final pg_put_line");
2120  }
2121 
2122  $resultsRs = pg_end_copy( $dbh );
2123  if ( !$resultsRs ) {
2124  throw new Exception ("Error Performing the Copy");
2125  }
2126  }
2127 
2128  /* UPDATE TRANSACTION */
2129  $commitRs = db_work( $dbh, HOMECU_WORK_COMMIT );
2130  if ( !$commitRs ) {
2131  // ** FAILED COMMIT
2132  throw new Exception ("Failed to Commit Work");
2133  }
2134  } catch( Exception $e ) {
2135  $commitRs = db_work( $dbh, HOMECU_WORK_ROLLBACK );
2136 
2137  $returnError["status"]["code"] = "999";
2138  $returnError["status"]["error"] = $e->getMessage();
2139  }
2140 
2141 
2142 return $returnError;
2143 } // end UpdateTRONTables
2144 
2145 /* Set up a transfer request. The request will be put into the <cu>transhdr/dtl tables and approved.
2146  * The core will be sent the request if it is not an ACH transfer.
2147  *
2148  * NOTE: look at hcuACH.i for transaction code meetings for ach-related transactions
2149  *
2150  * @param array $pHbEnv -- The current HB_ENV array
2151  * @param array $pTxnData -- Array of data from the cu_scheduledtxn.txn_data field.
2152  *
2153  * @return structure with error or success
2154  */
2155 function HandleTransferRequest( $pDbh, $pEnv, $pFeatureCode, $pTxnData, $pDryRun ) {
2156  global $DMSMAIL;
2157  global $STAT;
2158 
2159  $returnError = array(
2160  "status" => array( "code"=>'000', "error" => "" )
2161  );
2162 
2163  try {
2164  $transData = array();
2165  $fromMember = "";
2166  $transactionCode = "";
2167  $referenceId = 0;
2168  $emailNotify = 0;
2169  $inTransaction = false;
2170  $pTxnData["txn"]["memo"] = trim($pTxnData["txn"]["memo"]) != "" ? $pTxnData["txn"]["memo"] : "AUTO HB XFER";
2171 
2172  $transactionMemo = ""; // this is a memo for the overall transaction, not an addenda on an ACH
2173  // build the transdata json as the regular processing would have built it
2174  if ( $pFeatureCode == "TRN" ) {
2175  $srcAcctId = explode("|", $pTxnData["txn"]["from"]);
2176  $srcAcctKey = BuildAccountKey($pTxnData, "from");
2177 
2178  $dstAcctId = explode("|", $pTxnData["txn"]["to"]);
2179  $dstAcctKey = BuildAccountKey($pTxnData, "to");
2180 
2181  // add transdata
2182  $transData["acct_source"] = $pTxnData["txn"]["from"];
2183  $transData["acct_dest"] = $pTxnData["txn"]["to"];
2184  $transData['source_key'] = $srcAcctKey;
2185  $transData['dest_key'] = $dstAcctKey;
2186 
2187  // add any memo
2188  $transactionMemo = $pTxnData["txn"]["memo"];
2189 
2190  // add any misc1 field
2191  $misc1 = isset( $pTxnData["txn"]["misc1"] ) && strlen( $pTxnData["txn"]["misc1"] ) > 0 ? $pTxnData["txn"]["misc1"] : "";
2192  $transData['misc'] = $misc1;
2193 
2194  // get the from member account
2195  $parts = explode( "|", $pTxnData["txn"]["from"] );
2196  $fromMember = $srcAcctKey['accountnumber'];
2197 
2198  // reference id is the user_id of who made the transfer
2199  $referenceId = $pEnv["Uid"];
2200  } else if ( $pFeatureCode == "TRNEXT" ) {
2201  $localAcctKey = array();
2202 
2203  if ( $pTxnData["txn"]["type"] == "L2X" ) {
2204  $localAcct = $pTxnData["txn"]["from"];
2205  $localAcctId = explode("|", $pTxnData["txn"]["from"]);
2206 
2207  $localAcctKey = BuildAccountKey($pTxnData, "from");
2208  $remoteAcct = BuildExtAcctData( $pDbh, $pEnv, $pTxnData["txn"]["to"], "CR" );
2209 
2210  // reference id is the achpartner entry
2211  $referenceId = $pTxnData["txn"]["to"];
2212  } else {
2213  $localAcct = $pTxnData["txn"]["to"];
2214  $localAcctId = explode("|", $pTxnData["txn"]["to"]);
2215 
2216  $localAcctKey = BuildAccountKey($pTxnData, "to");
2217  $remoteAcct = BuildExtAcctData( $pDbh, $pEnv, $pTxnData["txn"]["from"], "DB" );
2218 
2219  // reference id is the achpartner entry
2220  $referenceId = $pTxnData["txn"]["from"] ;
2221  }
2222 
2223  // add any memo (this is not the addenda)
2224  $transactionMemo = $pTxnData["txn"]["memo"];
2225 
2226  if ( $pTxnData["txn"]["type"] == "L2X" ) {
2227  $transData["acct_source"] = $localAcct;
2228  $transData["acct_dest"] = $remoteAcct;
2229  $transData['source_key'] = $localAcctKey;
2230 
2231  // get the "from" member even though it isn't needed for authority with local -> external
2232  $parts = explode( "|", $localAcct );
2233  $fromMember = $localAcctKey['accountnumber'];
2234  } else {
2235  $transData["acct_source"] = $remoteAcct;
2236  $transData["acct_dest"] = $localAcct;
2237  $transData['dest_key'] = $localAcctKey;
2238 
2239  // no "from" member on external -> local
2240  $fromMember = "";
2241  }
2242  } else if ( $pFeatureCode == "TRNM2M" ) {
2243  $localAcct = $pTxnData["txn"]["from"];
2244  $localAcctId = explode("|", $pTxnData["txn"]["from"]);
2245 
2246  $localAcctKey = BuildAccountKey($pTxnData, "from");
2247  $transData["acct_source"] = $pTxnData["txn"]["from"];
2248  $transData['source_key'] = $localAcctKey;
2249 
2250  // get the from member account
2251  $parts = explode( "|", $pTxnData["txn"]["from"] );
2252  $fromMember = $localAcctKey['accountnumber'];
2253 
2254  // need "acct_dest"
2255  $remoteAcct = BuildM2MAcctData( $pDbh, $pEnv, $pTxnData["txn"]["to"], "DB" );
2256 
2257  $destId = "M|{$remoteAcct["rdfi"]["rdfi_account"]}|{$remoteAcct["rdfi"]["rdfi_account_type"]}";
2258  $remoteAcctKey = BuildM2MAccountKey($remoteAcct);
2259  $transData["acct_dest"] = $destId;
2260  $transData['dest_key'] = $remoteAcctKey;
2261 
2262  // add any memo
2263  $transactionMemo = $pTxnData["txn"]["memo"];
2264 
2265  // name goes in the "misc" field to match an immediate M2M transfer
2266  $transData['misc'] = $remoteAcctKey["name"];
2267 
2268  // reference id is the M2M entry (if ad-hoc supported, then it would be 0)
2269  $referenceId = $pTxnData["txn"]["to"] ;
2270  } else if ( $pFeatureCode == "ACHCOL" || $pFeatureCode == "ACHPMT" ) {
2271  $localAcctKey = array();
2272  if ( $pTxnData["txn"]["type"] == "L2R" ) {
2273  $localAcct = $pTxnData["txn"]["from"];
2274  $localAcctId = explode("|", $localAcct);
2275 
2276  $localAcctKey['accountnumber'] = $pTxnData['txn']['frommember'];
2277  $localAcctKey['recordtype'] = $localAcctId[0];
2278  if ($localAcctId[0] == "D") {
2279  $localAcctKey['accounttype'] = $localAcctId[2];
2280  $localAcctKey['certnumber'] = $localAcctId[3];
2281  } else if ($localAcctId[0] == "L") {
2282  $localAcctKey['loannumber'] = $localAcctId[2];
2283  }
2284  $remoteAcct = BuildACHData( $pDbh, $pEnv, $pTxnData["txn"]["to"] );
2285 
2286  // this is a credit to the remote account
2287  $remoteAcct["rdfi"]["rdfi_txn_type"] = "CR";
2288 
2289  // reference id is the achpartner entry
2290  $referenceId = $pTxnData["txn"]["to"];
2291  } else {
2292  $localAcct = $pTxnData["txn"]["to"];
2293  $localAcctId = explode("|", $localAcct);
2294  $localAcctKey['accountnumber'] = $pTxnData['txn']['tomember'];
2295  $localAcctKey['recordtype'] = $localAcctId[0];
2296  if ($localAcctId[0] == "D") {
2297  $localAcctKey['accounttype'] = $localAcctId[2];
2298  $localAcctKey['certnumber'] = $localAcctId[3];
2299  } else if ($localAcctId[0] == "L") {
2300  $localAcctKey['loannumber'] = $localAcctId[2];
2301  }
2302  $remoteAcct = BuildACHData( $pDbh, $pEnv, $pTxnData["txn"]["from"] );
2303 
2304  // this is a debit from the remote account
2305  $remoteAcct["rdfi"]["rdfi_txn_type"] = "DB";
2306 
2307  // reference id is the achpartner entry
2308  $referenceId = $pTxnData["txn"]["from"] ;
2309  }
2310 
2311  // get email notify value
2312  $emailNotify = $remoteAcct['remote_entity']['notify'] == "t" ? 1 : 0;
2313  // unset, this data is unnecessary for the transaction itself
2314  unset($remoteAcct['remote_entity']['notify']);
2315 
2316  // add any memo
2317  $transactionMemo = $pTxnData["txn"]["memo"];
2318 
2319  if ( isset( $pTxnData["txn"]["addenda"] ) && $pTxnData["txn"]["addenda"] != "" ) {
2320  $remoteAcct["rdfi"]["addenda"] = $pTxnData["txn"]["addenda"];
2321  }
2322 
2323  if ( $pTxnData["txn"]["type"] == "L2R" ) {
2324  $transData["acct_source"] = $localAcct;
2325  $transData["acct_dest"] = $remoteAcct;
2326  $transData['source_key'] = $localAcctKey;
2327  $transData['dest_key'] = array();
2328 
2329  // get the "from" member even though it isn't needed for authority with local -> external
2330  $parts = explode( "|", $localAcct );
2331  $fromMember = $localAcctKey['accountnumber'];
2332  } else {
2333  $transData["acct_source"] = $remoteAcct;
2334  $transData["acct_dest"] = $localAcct;
2335  $transData['source_key'] = array();
2336  $transData['dest_key'] = $localAcctKey;
2337 
2338  // no "from" member on external -> local
2339  $fromMember = "";
2340  }
2341  } else {
2342  // error - unknown feature
2343  $errorMessage = "Unknown feature code: $pFeatureCode.";
2344 
2345  // reference id is ?? if ad-hoc
2346  $returnError["status"]["code"] = "999";
2347  $returnError["status"]["error"] = $errorMessage;
2348 
2349 
2350  dmserror( $DMSMAIL, 'FATAL', $errorMessage );
2351  return $returnError;
2352  }
2353 
2354  if ( $pDryRun ) {
2355  // not making changes, but let person running the script know
2356 
2357  // read the user information
2358  $userSql = "SELECT user_name
2359  FROM {$pEnv["Cu"]}user
2360  WHERE user_id = {$pEnv["Uid"]} ";
2361 
2362  $userRS = db_query( $userSql, $pDbh );
2363  $userRow = db_fetch_row( $userRS, 0 );
2364 
2365  fprintf( $STAT, "Dry Run: Recording transfer for CU: {$pEnv["Cu"]}, User: {$userRow[0]}, Feature: $pFeatureCode, Amount: {$pTxnData["txn"]['amount']}, TxnId: {$pTxnData["txn_id"]} \n" );
2366  } else {
2367 
2368  // get some allowed amounts for validation
2369  $permissionInputs = array( "feature" => $pFeatureCode );
2370  $permissionInputs["amount"] = $pTxnData["txn"]['amount'];
2371 
2372  switch ($pFeatureCode) {
2373  case "TRN":
2374  $permissionInputs["account"] = $pTxnData["txn"]["frommember"];
2375  $permissionInputs["accounttype"] = $pTxnData["txn"]["fromsuffix"];
2376  break;
2377  case "TRNEXT":
2378  if ($pTxnData["txn"]["type"] == "X2L") { // If coming from external account, mimic transfer screen which sets the accountnumber to user_id and the accounttype to extaccount id.
2379  $permissionInputs["account"] = $pEnv["Uid"];
2380  $permissionInputs["accounttype"] = $pTxnData["txn"]["from"];
2381  } else {
2382  $sourceParts = explode("|", $pTxnData["txn"]["from"]);
2383  $permissionInputs["account"] = $sourceParts[1];
2384  $permissionInputs["accounttype"] = $sourceParts[2];
2385  }
2386  break;
2387  case "TRNM2M":
2388  $sourceParts = explode("|", $pTxnData["txn"]["from"]);
2389  $permissionInputs["account"] = $sourceParts[1];
2390  $permissionInputs["accounttype"] = $sourceParts[2];
2391  break;
2392  // TODO: Solve at a later time.
2393  case "ACHCOL":
2394  $sourceParts = explode("|", $pTxnData["txn"]["to"]);
2395  $permissionInputs["account"] = $sourceParts[1];
2396  $permissionInputs["accounttype"] = $sourceParts[2];
2397  break;
2398  case "ACHPMT":
2399  $sourceParts = explode("|", $pTxnData["txn"]["from"]);
2400  $permissionInputs["account"] = $sourceParts[1];
2401  $permissionInputs["accounttype"] = $sourceParts[2];
2402  break;
2403  }
2404 
2405  $limitTestResult = Perm_CheckLimits( $pDbh, $pEnv, $permissionInputs );
2406 
2407  if ($limitTestResult["status"]["code"] != "000") {
2408  throw new Exception( "Scheduled Transfer Error: " . $limitTestResult["status"]["error"]);
2409  }
2410 
2411  // for ach payment transactions we must check the
2412  // available funds.
2413  if ($pFeatureCode == "ACHPMT") {
2414  $memberInfoParams = array(
2415  "member"=>$localAcctKey['accountnumber'],
2416  "email"=>""
2417  );
2418 
2419  // get list of member accounts
2420  $memberInfo = FindMemberAccounts($pEnv, $memberInfoParams);
2421  if ($memberInfo['code'] != "000") {
2422  throw new Exception($memberInfo['error']);
2423  }
2424 
2425  $memberAccts = $memberInfo['data']['accounts'];
2426  $acctAvailable = 0;
2427  // iterate member account list to find the account available
2428  // balance.
2429 
2430  $acctFound = false;
2431  if (HCU_array_key_exists("deposit", $memberAccts)) {
2432  foreach ($memberAccts['deposit'] as $key => $value) {
2433 
2434  $acctNum = $value['accountnumber'];
2435  $acctType = $value['accounttype'];
2436  $acctCert = $value['certnumber'];
2437  // match account by accountnumber, accounttype and certnumber
2438  // recordtype can be hard match because loan accounts are not
2439  // allowed for ach transactions.
2440  $acctFound =
2441  ($acctNum == $localAcctKey['accountnumber']) &&
2442  ($acctType == $localAcctKey['accounttype']) &&
2443  ($acctCert == $localAcctKey['certnumber']);
2444 
2445  // get available balance and break loop
2446  if ($acctFound) {
2447  $acctAvailable = doubleval($value['available']);
2448  break;
2449  }
2450  }
2451  }
2452 
2453  $acctAmount = doubleval($pTxnData['txn']['amount']);
2454  if ($acctAvailable < $acctAmount) {
2455  throw new Exception("Scheduled Transfer Error: Transfer exceeds available funds.");
2456  }
2457  }
2458 
2459  // start a transaction
2460  $txnRs = db_work( $pDbh, HOMECU_WORK_BEGIN);
2461  $inTransaction = true;
2462 
2463  // record the transfer in the <cu>transhdr/transdtl table
2464 
2465  // all transactions use this info
2466  $recordVariables = array();
2467  $recordVariables["txFeatureCode"] = $pFeatureCode;
2468  $recordVariables["txPostedBy"] = $pEnv["Uid"];
2469  // ** Set the Authority Account for the transaction
2470  // This will always be the "from" member, but only used with calls to the core
2471  $recordVariables["txAuthAccount"] = $fromMember;
2472  $recordVariables["txTransCode"] = trim( $pTxnData["txn"]["transactioncode"] );
2473  $recordVariables["txComment"] = trim( $transactionMemo );
2474 
2475  $recordVariables["txReferenceId"] = $referenceId;
2476  $recordVariables["txAmount"] = $pTxnData["txn"]['amount'];
2477  $recordVariables["txNotify"] = $emailNotify;
2478 
2479  // put the transfer data in to a JSON string
2480  $jsonTransData = HCU_JsonEncode( $transData );
2481  $recordVariables["txData"] = $jsonTransData;
2482 
2483  // Setup transaction metadata.
2484  // Single or Recurring transaction, and the current interval. In this case
2485  // the record can have a frequency of One Time, which is Single.
2486  $transMeta = array();
2487  $transMeta["source"] = $pTxnData["txn_id"];
2488  $transMeta["recurr"] = $pTxnData["frequency"] == "OneTime" ? "no" : "yes";
2489  $transMeta["interval"] = $pTxnData["interval"];
2490  $jsonTransMeta = HCU_JsonEncode($transMeta);
2491  $recordVariables["txMeta"] = $jsonTransMeta;
2492 
2493  // the pHBEnv is needed for CU and logging
2494  $transferResult = RecordTransfer( $pDbh, $pEnv, $recordVariables );
2495 
2496  if ( $transferResult["status"]["code"] == "000" ) {
2497  // save the header id
2498  $transHeaderId = $transferResult["data"]["id"];
2499  $approvalInfo = array( "txId" => $transHeaderId,
2500  "txApprover" => $pTxnData["approved_by"] );
2501 
2502  // scheduled transactions always approve
2503  $transferResult = MarkTransferApproved( $pDbh, $pEnv, $approvalInfo );
2504 
2505  // regular and M2M transfers post to the core right away; ACH-related wait for the admin process
2506  if ( $transferResult["status"]["code"] == "000" &&
2507  ( $pFeatureCode == "TRN" || $pFeatureCode == "TRNM2M" ) ) {
2508  // populate all the transfer data
2509  $transferRecord = array();
2510  // use the source/dest determined above (since M2M is a calculated value)
2511  $transferRecord["acct_source"] = $transData["acct_source"];
2512  $transferRecord["acct_dest"] = $transData["acct_dest"];
2513  $transferRecord["misc"] = $transData["misc"];
2514  $transferRecord["accountnumber"] = $fromMember;
2515  $transferRecord["transactioncode"] = $pTxnData["txn"]["transactioncode"];
2516  $transferRecord["memo"] = trim( $transactionMemo );
2517  $transferRecord["amount"] = $pTxnData["txn"]["amount"];
2518 
2519  $transferResult = PostInternalTransfer( $pEnv, $transferRecord );
2520 
2521  // if no error, mark it processed
2522  if ( $transferResult["status"]["code"] === "000" ) {
2523  // set up the query to mark the transaction as processed ("*sched*" means processed by scheduler processor)
2524  $sql = "UPDATE {$pEnv["Cu"]}transhdr SET
2525  processed_by = '*sched*',
2526  processed_date = now(),
2527  processed_status = 20
2528  WHERE id = $transHeaderId";
2529 
2530  $rs = db_query( $sql, $pDbh );
2531  }
2532  }
2533  }
2534 
2535  if ( $transferResult["status"]["code"] !== "000" ) {
2536  $errorMessage = "Feature: $pFeatureCode, UserId: {$pEnv["Uid"]}; Error Message: ";
2537  for ( $i = 0; $i < count( $transferResult["status"]["errors"] ); $i++ ) {
2538  $errorMessage .= $transferResult["status"]["errors"][$i] . " ";
2539  }
2540  throw new Exception( "Scheduled Transfer Error: $errorMessage" );
2541  }
2542 
2543  // if we made it here, let's commit the transaction
2544  $commitRs = db_work( $pDbh, HOMECU_WORK_COMMIT );
2545  if ( !$commitRs ) {
2546  // ** FAILED COMMIT
2547  throw new Exception ("Failed to Commit Work");
2548  }
2549  }
2550 
2551  } catch( Exception $e ) {
2552  $errorMessage = $e->getMessage();
2553 
2554  $returnError["status"]["code"] = "999";
2555  $returnError["status"]["error"] = $errorMessage;
2556 
2557  if ( $inTransaction ) {
2558  db_work( $pDbh, HOMECU_WORK_ROLLBACK );
2559  }
2560  }
2561 
2562  return $returnError;
2563 } // end HandleTransferRequest
2564 
2565 // Build a structure of external account data that would mimic the transdtl.transdata remote account info.
2566 function BuildExtAcctData( $pDbh, $pEnv, $pAcctId, $pTxnType ) {
2567  $extInfo = array();
2568 
2569  // get the remote info from the external accounts table
2570  $extSQL = "SELECT display_name, remote_info
2571  FROM {$pEnv["Cu"]}extaccount
2572  WHERE id = {$pAcctId}";
2573 
2574  $rs = db_query( $extSQL, $pDbh );
2575  if ( !$rs ) {
2576  dmserror( $DMSMAIL, "WARN", "Unable to read {$pEnv["Cu"]}extaccount for userId {$pEnv["Uid"]}, id $pAcctId.\n" . db_last_error() );
2577  } else {
2578  $extRow = db_fetch_assoc( $rs );
2579 
2580  $extData = HCU_JsonDecode( $extRow["remote_info"], false );
2581  if ( !$extData ) {
2582  dmserror( $DMSMAIL, "WARN", "Unable to parse json for {$pEnv["Cu"]}extaccount (for userId {$pEnv["Uid"]}), id $pAcctId.\n" . db_last_error() );
2583  } else {
2584  /* Here is the structures we are working with:
2585  *
2586  * this is in <cu>extaccount
2587  * {"rdfi":{"routing":"930840239","account":"8989","type":"10","name":"Mike Streymore"},
2588  *
2589  * this is in <cu>transdtl.transdata
2590  * {"rdfi":{"rdfi_routing":"930840239","rdfi_account":"8989","rdfi_account_type":"10","addenda":""},"remote_entity":{"name":"Mike Streymore"}}
2591  */
2592  $extDFI = array( "rdfi_routing" => $extData["rdfi"]["routing"],
2593  "rdfi_account" => $extData["rdfi"]["account"],
2594  "rdfi_account_type" => $extData["rdfi"]["type"],
2595  "rdfi_txn_type" => $pTxnType,
2596  "addenda" => "" ); // addenda is placeholder if scheduled txn has a memo
2597  $remoteEntity = array ( "name" => $extData["rdfi"]["name"],
2598  "entry_id" => $pAcctId,
2599  "display_name" => $extRow["display_name"] );
2600 
2601  $extInfo["rdfi"] = $extDFI;
2602  $extInfo["remote_entity"] = $remoteEntity;
2603  }
2604  }
2605 
2606  return $extInfo;
2607 } // end BuildExtAcctData
2608 
2609 // Build a structure of M2M account data that would mimic the transdtl.transdata m2m account info.
2610 function BuildM2MAcctData( $pDbh, $pEnv, $pAcctId, $pTxnType ) {
2611  $extInfo = array();
2612 
2613  // get the remote info from the external accounts table
2614  $extSQL = "SELECT display_name, remote_info
2615  FROM {$pEnv["Cu"]}extaccount
2616  WHERE id = {$pAcctId}";
2617 
2618  $rs = db_query( $extSQL, $pDbh );
2619  if ( !$rs ) {
2620  dmserror( $DMSMAIL, "WARN", "Unable to read {$pEnv["Cu"]}extaccount for userId {$pEnv["Uid"]}, id $pAcctId.\n" . db_last_error() );
2621  } else {
2622  $extRow = db_fetch_assoc( $rs );
2623 
2624  $extData = HCU_JsonDecode( $extRow["remote_info"], false );
2625  if ( !$extData ) {
2626  dmserror( $DMSMAIL, "WARN", "Unable to parse json for {$pEnv["Cu"]}extaccount (for userId {$pEnv["Uid"]}), id $pAcctId.\n" . db_last_error() );
2627  } else {
2628  /* Here is the structures we are working with:
2629  *
2630  * this is in <cu>extaccount
2631  * {"rdfi":{"routing":"930840239","account":"8989","type":"10","name":"Mike Streymore"},
2632  *
2633  * this is in <cu>transdtl.transdata
2634  * {"rdfi":{"rdfi_routing":"930840239","rdfi_account":"8989","rdfi_account_type":"10","addenda":""},"remote_entity":{"name":"Mike Streymore"}}
2635  */
2636  $extDFI = array( "rdfi_account" => $extData["rdfi"]["account"],
2637  "rdfi_account_type" => $extData["rdfi"]["type"],
2638  "rdfi_txn_type" => $pTxnType,
2639  "addenda" => "" ); // addenda is placeholder if scheduled txn has a memo
2640  $remoteEntity = array ( "name" => $extData["rdfi"]["name"],
2641  "entry_id" => $pAcctId,
2642  "display_name" => $extRow["display_name"] );
2643 
2644  $extInfo["rdfi"] = $extDFI;
2645  $extInfo["remote_entity"] = $remoteEntity;
2646  }
2647  }
2648 
2649  return $extInfo;
2650 } // end BuildM2MAcctData
2651 
2652 // Build a structure for ACH data that would mimic the transdtl.transdata remote account info.
2653 function BuildACHData( $pDbh, $pEnv, $pAcctId ) {
2654  $achInfo = array();
2655 
2656  // get the remote info from the external accounts table
2657  $achSQL = "SELECT ach_name, address, dfi_data, email_notify
2658  FROM {$pEnv["Cu"]}achpartner
2659  WHERE id = {$pAcctId}";
2660 
2661  $rs = db_query( $achSQL, $pDbh );
2662  if ( !$rs ) {
2663  dmserror( $DMSMAIL, "WARN", "Unable to read {$pEnv["Cu"]}achpartner for userId {$pEnv["Uid"]}, id $pAcctId.\n" . db_last_error() );
2664  } else {
2665  $achRow = db_fetch_assoc( $rs );
2666 
2667  $achDFI = HCU_JsonDecode( $achRow["dfi_data"], false );
2668  $achAddress = HCU_JsonDecode( $achRow["address"], false );
2669  if ( !$achDFI || !$achAddress ) {
2670  dmserror( $DMSMAIL, "WARN", "Unable to parse json for {$pEnv["Cu"]}achpartner (for userId {$pEnv["Uid"]}), id $pAcctId.\n" . db_last_error() );
2671  } else {
2672  /* Here is the structures we are working with:
2673  *
2674  * this is in <cu>achpartner
2675  * {"address1":"123 Woods St.","address2":"","city":"Boise","state":"ID","zip":"83705","country":"","email":"mike2@homecu.net"}
2676  * {"dfi_routing":"456389323","dfi_account":"123383","dfi_account_type":"10"}
2677  *
2678  * this is in <cu>transdtl.transdata
2679  * {{"acct_source":{"rdfi":{"rdfi_routing":"456389343","rdfi_account":"123383","rdfi_account_type":"10","rdfi_txn_type":"DB","addenda":"Testing addenda"},"remote_entity":{"name":"Mike Wabash","address1":"123 Woods St.","address2":"","email":"mike2@homecu.net"}},"acct_dest":"D|1103|10|0"}
2680  */
2681  $achData = array( "rdfi_routing" => $achDFI["dfi_routing"],
2682  "rdfi_account" => $achDFI["dfi_account"],
2683  "rdfi_account_type" => $achDFI["dfi_account_type"],
2684  "rdfi_txn_type" => "", // placeholder so calling routine can put in the txn type
2685  "addenda" => "" ); // addenda is placeholder if scheduled txn has an addenda
2686  $remoteEntity = array ( "name" => $achRow["ach_name"],
2687  "address1" => $achAddress["address1"],
2688  "address2" => $achAddress["address2"],
2689  "email" => $achAddress["email"],
2690  "notify" => $achRow['email_notify'] );
2691 
2692  $achInfo["rdfi"] = $achData;
2693  $achInfo["remote_entity"] = $remoteEntity;
2694  }
2695  }
2696 
2697  return $achInfo;
2698 } // end BuildACHData
2699 
2700 // Make sure the user has ability to access the given account and rights to do the transfer.
2701 // Accounts come in the form "D|1103|5|0" or an empty string if it is an external account.
2702 
2703 /**
2704  * Verify if the user still has rights to this account for transferring
2705  *
2706  * @param integer $dbh - Current database handle
2707  * @param string $cu - Credit Union code
2708  * @param integer $userId - User Id for the user in question
2709  * @param array $pFromAcctKey - This is the FROM Account key used to lookup record (may be empty)
2710  * @param array $pToAcctkey - This is the TO Account key used to lookup record (may be empty)
2711  * @param string $pTransCode - The transaction code for this scheduled transaction. This can change how the
2712  * to account is formed. Needed for XA and XP transactions.
2713  *
2714  * @return boolean
2715  * {true, false} - Does the user have rights to the appropriate accounts to make this transfer
2716  */
2717 function UserHasRightsToTransfer( $dbh, $cu, $userId, $pFromAcctKey, $pToAcctKey, $pTransCode ) {
2718  $hasRights = false;
2719 
2720  try {
2721 
2722  if (count($pFromAcctKey) == 0 && count($pToAcctKey) == 0) {
2723  throw new Exception ("No Valid Accounts");
2724  }
2725 
2726  /* Cross Account Transaction Codes */
2727  $tranCodeXLookup = Array("XA", "XP");
2728 
2729  /* ** VERIFY FROM ACCOUNT ** */
2730  if (count($pFromAcctKey)) {
2731  $fromAcct = FindUserAccountExists($dbh, $cu, $userId, $pFromAcctKey['recordtype'], $pFromAcctKey['accountnumber'],
2732  ($pFromAcctKey['recordtype'] == "D" ? $pFromAcctKey['accounttype'] : $pFromAcctKey['loannumber']),
2733  ($pFromAcctKey['recordtype'] == "D" ? $pFromAcctKey['certnumber'] : '')
2734  );
2735  if ($fromAcct === false) {
2736  dmserror( $GLOBALS['DMSMAIL'], "WARN", "Unable to read {$cu}useraccount for userId $userId, account {$pFromAcctKey['accountnumber']}, accounttype {$pFromAcctKey['accounttype']}, recordtype {$pFromAcctKey['recordtype']}. \n" . db_last_error() );
2737  $hasFromRights = false;
2738  } else {
2739  // ** Read the data
2740  // test for rights
2741  if (count($pToAcctKey) == 0) {
2742  // internal -> external, so test ext_withdraw
2743  $hasFromRights = HCU_array_key_value("ext_withdraw", $fromAcct['data']) === 't';
2744  } else {
2745  // internal -> internal, so test int_withdraw
2746  $hasFromRights = HCU_array_key_value("int_withdraw", $fromAcct['data']) === 't';
2747  }
2748 
2749  }
2750  } else {
2751  $hasFromRights = true;
2752  }
2753 
2754  /* ** VERIFY TO ACCOUNT ** */
2755  if (count($pToAcctKey)) {
2756 
2757  if (in_array($pTransCode, $tranCodeXLookup)) {
2758  /*
2759  * Cross Account Lookup
2760  * Build the information slightly different
2761  *
2762  * To lookup in the useraccounts table, always send the from accountnumber as the accountnumber,
2763  * AND construct the useraccounts.accounttype from sub-account#accountnumber (of cross account)
2764  *
2765  */
2766 
2767  $localAcctNbr = $pFromAcctKey['accountnumber'];
2768  $localAcctSfx = ($pToAcctKey['recordtype'] == "D" ? $pToAcctKey['accounttype'] : $pToAcctKey['loannumber']) . '#' . $pToAcctKey['accountnumber'];
2769  $localAcctCrt = '';
2770  $localRecType = ($pToAcctKey['recordtype'] == "D" ? 'T' : 'P');
2771  } else {
2772  $localAcctNbr = $pToAcctKey['accountnumber'];
2773  $localAcctSfx = ($pToAcctKey['recordtype'] == "D" ? $pToAcctKey['accounttype'] : $pToAcctKey['loannumber']);
2774  $localAcctCrt = ($pToAcctKey['recordtype'] == "D" ? $pToAcctKey['certnumber'] : '');
2775  $localRecType = $pToAcctKey['recordtype'];
2776 
2777  }
2778  $toAcct = FindUserAccountExists($dbh, $cu, $userId, $localRecType, $localAcctNbr, $localAcctSfx, $localAcctCrt);
2779  if ($toAcct === false) {
2780  dmserror( $GLOBALS['DMSMAIL'], "WARN", "Unable to read {$cu}useraccount for userId $userId, account {$localAcctNbr}, accounttype {$localAcctSfx}, recordtype {$localAcctCrt}. \n" . db_last_error() );
2781  $hasToRights = false;
2782  } else {
2783  // ** Read the data
2784  // test for rights
2785  if (count($pFromAcctKey) == 0) {
2786  // internal -> external, so test ext_withdraw
2787  $hasToRights = HCU_array_key_value("ext_deposit", $toAcct['data']) === 't';
2788  } else {
2789  // internal -> internal, so test int_withdraw
2790  $hasToRights = HCU_array_key_value("int_deposit", $toAcct['data']) === 't';
2791  }
2792 
2793  }
2794  } else {
2795  $hasToRights = true;
2796  }
2797 
2798  // need both From and To rights to be successful
2799  $hasRights = $hasFromRights && $hasToRights;
2800  } catch( Exception $e ) {
2801  $hasRights = false;
2802  }
2803 
2804  return $hasRights;
2805 } // end UserHasRightsToTransfer
2806 
2807 
2808 // Update the given schedule record's next trigger date.
2809 // NOTE: Also updates interval_count
2810 // Return a boolean true on success, false on failure.
2811 function UpdateNextTriggerDate( $pDbh, $pScheduleId, $pTrigger ) {
2812  $updateResult = false;
2813 
2814  // need to set to NULL if no trigger date
2815  $quotedTriggerDate = $pTrigger ? "'$pTrigger'" : "NULL";
2816 
2817  $sql = "UPDATE cu_scheduledtxn
2818  SET next_trigger_date = $quotedTriggerDate, interval_count = interval_count + 1
2819  WHERE id = $pScheduleId";
2820 
2821  $rs = db_query( $sql, $pDbh );
2822 
2823  $updateResult = $rs != false;
2824 
2825  return $updateResult;
2826 } // end UpdateNextTriggerDate
2827 
2828 // for getting data from appliance, get just the balances, put into cu_alerts so only have one set of tables and can use the runslot logic.
2829 // write a function in sApiAppl.i file to get just the balances (mimic current function in sApplApi.std.i).
2830 // we will stop using the alertxx tables and use the cu_alertxx tables instead (to use runslot logic).
2831 
2832 // Send message to the given list; The "from" is only the first in the $from list.
2833 function SmsMailTransfer( $from, $to, $message, $acct ) {
2834 
2835  // need to use comma separators
2836  $to = str_replace( ";", ",", $to );
2837 
2838  $fromParts = explode( ";", $from );
2839  $fromMail = $fromParts[0];
2840 
2841  $date = date( "D M d H:i:s Y" );
2842 
2843  $headers = "From: $fromMail\r\n";
2844 
2845  $subject = "Credit Union Recurring Transaction";
2846 
2847  $body = "$message\r\n\r\n$date";
2848 
2849  $notify = new ErrorMail;
2850  $notify->mailto = $to;
2851  $notify->mailfrom = $fromMail;
2852  $notify->subject = $subject;
2853  $notify->msgbody = $body;
2854  $notify->callingfunction = __FUNCTION__;
2855  $notify->file = __FILE__;
2856  $notify->SendMail();
2857 
2858 } // end SmsMailTransfer
2859 
2860 // Return the next long code from the roundrobin list.
2861 function GetNextLongCode() {
2862  global $gRoundRobinCounter;
2863  global $LONGCODE_ROUNDROBIN;
2864 
2865  $idx = $gRoundRobinCounter % count( $LONGCODE_ROUNDROBIN );
2866  $gRoundRobinCounter++;
2867 
2868  return $LONGCODE_ROUNDROBIN[$idx];
2869 } // end GetNextLongCode
2870 
2871 // Send the text message using our round-robin, next long code.
2872 function SmsInternalLongcode( $from, $to, $message, $acct, $orgname ) {
2873  $nextLongCode = GetNextLongCode();
2874 
2875  SmsLongcode( 'SMS', $nextLongCode, $to, $message, $acct, "($from) " . $orgname );
2876 } // end SmsInternalLongcode
2877 
2878 // Send a message to a single address from a single address.
2879 function HtmlMail( $from, $to, $message, $hdrFromAddr, $hdrFromName) {
2880 
2881  $date = date( "D M d H:i:s Y" );
2882 
2883  $subject = "Credit Union Alert";
2884 
2885  $body = "<html><body>$message<br /><br /><span style='font-style: italic'>$date</span></body></html>";
2886 
2887  $notify = new ErrorMail;
2888  $notify->mailto = $to;
2889  $notify->mailfrom = $hdrFromAddr;
2890  $notify->mailfromname = $hdrFromName;
2891  $notify->subject = $subject;
2892  $notify->htmlMsgbody = $body;
2893  $notify->callingfunction = __FUNCTION__;
2894  $notify->file = __FILE__;
2895  $notify->SendMail();
2896 } // end HtmlMail
2897 
2898 // Send the text message using a longcode.
2899 function SmsLongcode( $pType, $from, $to, $message, $acct, $orgname ) {
2900 
2901  # SEND using LONG CODE
2902  $smsMsg = ($orgname != '' ? "Account Alert from $orgname\n\n" . $message : "Credit Union Alert\n" . $message);
2903 
2904  #
2905  # -- Need to escape new lines so the perl long code script will keep the string on one line
2906  # When broken up it causes problems on the command line call
2907 // $message = preg_replace( "/\n/", "\\n", $message);
2908 
2909  if ($pType == 'SMS') {
2910  // use the global values for the api key and url
2911  SendLongCodeSMS ($GLOBALS['HOMECU_LONGCODE_API_KEY'], $GLOBALS['HOMECU_LONGCODE_URL'], $from, $to, $smsMsg);
2912  } elseif($pType == 'AWS') {
2913  // ** -- ATTEMPT TO SEND AS AWS SMS
2914  $msg_response = SendAwsSMS($to, $orgname, $smsMsg);
2915  }
2916 
2917 } // end SmsLongcode
2918 
2919 // Fetch the CU's notify email account
2920 function FetchCUEmail( $dbh, $cu ) {
2921  global $DMSMAIL;
2922 
2923  $cuEmail = "";
2924 
2925  $sql = "SELECT rtrim(email)
2926  FROM cuadmnotify
2927  WHERE cu = '$cu'
2928  AND role = 'transfernotify'";
2929  $rs = db_query( $sql, $dbh );
2930  if ( !$rs ) {
2931  dmserror( $DMSMAIL, "WARN", "Unable to get CU's transfernotify email: {$cu}. \n" . db_last_error() );
2932  } else {
2933  $cuRow = db_fetch_row( $rs );
2934  $cuEmail = $cuRow[0];
2935  }
2936 
2937  return $cuEmail;
2938 } // end FetchCUEmail
2939 
2940 // Fetch information from the user record
2941 function FetchUserInfo( $dbh, $cu, $userId ) {
2942  global $DMSMAIL;
2943 
2944  $returnInfo = array();
2945  $sql = "SELECT user_name, email
2946  FROM {$cu}user
2947  WHERE user_id = $userId";
2948  $rs = db_query( $sql, $dbh );
2949  if ( !$rs ) {
2950  dmserror( $DMSMAIL, "WARN", "Unable to get user name and email: {$cu}, user_id: {$userId}. \n" . db_last_error() );
2951  $returnInfo = array( "user_name" => "unknown", "email" => "unknown" );
2952  } else {
2953  $cuRow = db_fetch_row( $rs );
2954  $returnInfo = array( "user_name" => $cuRow[0], "email" => $cuRow[1] );
2955  }
2956 
2957  return $returnInfo;
2958 } // end FetchUserInfo
2959 
2960 // Mail the supplied message to the given credit union email address.
2961 function MailCU( $pFromMail, $pCUMail, $pMessage ) {
2962  // can only have one from email
2963  $fromParts = explode( ";", $pFromMail );
2964  $fromMail = $fromParts[0];
2965 
2966  // handle multiple destination emails; need to use comma separators
2967  $to = str_replace( ";", ",", $pCUMail );
2968 
2969  $date = date( "D M d H:i:s Y" );
2970 
2971  $subject = "HomeCU Member Processing Issue";
2972 
2973  $body = "$pMessage\r\n\r\n$date";
2974 
2975  $notify = new ErrorMail;
2976  $notify->mailto = $to;
2977  $notify->mailfrom = $fromMail;
2978  $notify->subject = $subject;
2979  $notify->msgbody = $body;
2980  $notify->callingfunction = __FUNCTION__;
2981  $notify->file = __FILE__;
2982  $notify->SendMail();
2983 
2984 } // end MailCU
2985 
2986 // Send email to user that a recurring transaction was made inactive.
2987 function MailUserInactive( $CUMAIL, $cuName, $userEmail ) {
2988 
2989  $parts = explode( ";", $CUMAIL );
2990  $fromMail = $parts[0];
2991 
2992  $date = date( "D M d H:i:s Y" );
2993  $subject = "$cuName recurring transfers";
2994 
2995  // make an informational message
2996  $body = "This notice is to inform you your recurring transfer was set to inactive.\n\n";
2997  $body .= "If you want to manually transfer these funds please call the credit union to reschedule your recurring transfer. \n\n";
2998  $body .= "$cuName\n\n$date";
2999 
3000  $notify = new ErrorMail;
3001  $notify->header = "Content-Type: text/html";
3002  $notify->mailto = $userEmail;
3003  $notify->mailfrom = $fromMail;
3004  $notify->subject = $subject;
3005  $notify->msgbody = $body;
3006  $notify->callingfunction = __FUNCTION__;
3007  $notify->file = __FILE__;
3008  $notify->cu = $cuName;
3009  $notify->SendMail();
3010 } // end MailUserInactive
3011 
3012 // Send email to user with the given warning message. The CU and current date will be added to the message.
3013 function MailUserWarning( $CUMAIL, $cuName, $userEmail, $message ) {
3014 
3015  $parts = explode( ";", $CUMAIL );
3016  $fromMail = $parts[0];
3017 
3018  $date = date( "D M d H:i:s Y" );
3019  $subject = "$cuName recurring transfers";
3020 
3021  // make an informational message
3022  $body = "$message\n\n$cuName\n\n$date";
3023 
3024  $notify = new ErrorMail;
3025  $notify->mailto = $userEmail;
3026  $notify->mailfrom = $fromMail;
3027  $notify->subject = $subject;
3028  $notify->msgbody = $body;
3029  $notify->callingfunction = __FUNCTION__;
3030  $notify->file = __FILE__;
3031  $notify->cu = $cuName;
3032  $notify->SendMail();
3033 } // end MailUserWarning
3034 
3035 // Report the given error
3036 function dmserror( $email, $errorType, $errorMessage ) {
3037  global $PIDFILE;
3038 
3039  // 9/16/2019: Do not send an email when there is an error.
3040  // They are not useful to support.
3041  // handle multiple email addresses
3042  $email = str_replace( ";", ",", $email );
3043 
3044  $subject = "Subject: HomeCU Live Background Processing Error";
3045 
3046  // make an informational message
3047  $body = "An error occured in " . __FILE__ . " during background processing.\n\n";
3048  $body .= "Error message: $errorMessage\n\n";
3049  $body .= "Timestamp: " . date( "D M d, Y G:i" );
3050 
3051  $notify = new ErrorMail;
3052  $notify->mailto = $email;
3053  $notify->subject = $subject;
3054  $notify->msgbody = $body;
3055  $notify->callingfunction = __FUNCTION__;
3056  $notify->file = __FILE__;
3057  $notify->SendMail();
3058 
3059  $stderr = fopen( "php://stderr", "w" );
3060 
3061  if ( !$stderr ) {
3062  $stderr = fopen( "php://stdout", "w" );
3063  }
3064 
3065  fwrite( $stderr, "\nERROR OCCURRED DURING BACKGROUND PROCESSING!\n\n" );
3066  fwrite( $stderr, "$errorMessage\n" );
3067 
3068  if ( $errorType == "FATAL" ) {
3069  unlink($PIDFILE);
3070  exit(1);
3071  }
3072 }
3073 
3074 # #################
3075 # LOAD CREDIT UNION BLACK LIST
3076 # data is one line per credit union
3077 # #################
3078 /**
3079  * Open blacklist alerts file and build an array.
3080  * 09/19 Used to crete an SQL in clause, was getting concatenated without
3081  * a comma to contents of SQL query. Now just returns the array.
3082  * https://github.com/homecu/odyssey/issues/3069
3083  * @return array
3084  */
3085 function ReturnBlackList() {
3086  global $WORKINGDIRECTORY;
3087 
3088  // Where is the blacklist file located
3089  $blacklistFile= $WORKINGDIRECTORY . "/alerts.blacklist.conf";
3090  $returnCUList = [];
3091 
3092  // OPEN THE FILE / RETRIEVE CONTENTS
3093  if (file_exists( $blacklistFile)) {
3094  $cuList = file($blacklistFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
3095  # LOOP THROUGH EACH RECORD IN BLACKLIST
3096  for ( $i = 0; $i < count($cuList); $i++ ) {
3097 
3098  // Always use Uppercase value of cucode
3099  $returnCUList[] = trim(strtoupper($cuList[$i]));
3100  }
3101  }
3102 
3103  return $returnCUList;
3104 
3105 }
3106 
3107 # ######################
3108 # LOAD CREDIT UNION INCLUDE LIST
3109 # one line per credit union in format CUCODE ; time1; time2; ... timeN;
3110 # ######################
3111 # Usage $x = ReturnRunSlotList('0815');
3112 # param1 - currRunSlot - string - this is the 4 character runslot to match in the include file
3113 # returns list of quoted CU codes.
3114 function ReturnRunSlotList( $pCurrRunSlot ) {
3115  global $WORKINGDIRECTORY;
3116 
3117  $runslitFile = $WORKINGDIRECTORY . "/alerts.runslotlist.conf";
3118  $returnCUList = "";
3119 
3120 
3121  # OPEN THE FILE / RETRIEVE CONTENTS
3122  if ( file_exists( $runslitFile ) ) {
3123  $slotList = file( $runslitFile );
3124  # LOOP through each record in the INCLUSION File
3125  for ( $i = 0; $i < count( $slotList ); $i++ ) {
3126  $line = trim( $slotList[$i] );
3127 
3128  $parts = explode( ";", $line );
3129 
3130  # Match the appropriate time slot
3131  # each line of file should be formatted as
3132  # Be sure to put the credit union in the blacklist file if you do NOT want it to run with timezone
3133  # CUCODE ;0600;0815;
3134  # CUCODE ;0945;
3135  # CUCODE ;0430;1100;
3136  $cuCode = trim( $parts[0] );
3137 
3138  for ( $s = 1; $s < count( $parts ); $s++ ) {
3139  if ( trim( $parts[$s] ) == trim( $pCurrRunSlot ) ) {
3140  $returnCUList .= ( $returnCUList != "" ? ", " : "" ) . "'" . strtoupper( $cuCode ) . "'";
3141  // break out of this loop
3142  break;
3143  }
3144  }
3145  }
3146  }
3147 
3148  return $returnCUList;
3149 } // end ReturnRunSlotList
3150 
3151 // Add the given message to the gathered stats slot for the given credit union.
3152 function AddToGatheredTransferStats( &$pHolderArray, $pCurrCU, $pSlot, $pMessage ) {
3153  if ( !isset( $pHolderArray[$pCurrCU ] ) ) {
3154  // set up a new slot
3155  $pHolderArray[$pCurrCU] = array( "count" => 0,
3156  "failed" => array( "count" => 0, "messages" => array() ),
3157  "rights" => array( "count" => 0, "messages" => array() ),
3158  "success" => array( "count" => 0, "messages" => array() ),
3159  "unknown" => 0 );
3160  }
3161 
3162  if ( $pSlot == "count" ) {
3163  $pHolderArray[$pCurrCU]["count"]++;
3164  } else if ( in_array( $pSlot, array( "failed", "rights", "success" ) ) ) {
3165  // this would handle failed, rights, success
3166  $pHolderArray[$pCurrCU][$pSlot]["count"]++;
3167  $pHolderArray[$pCurrCU][$pSlot]["messages"][] = $pMessage;
3168  } else {
3169  $pHolderArray[$pCurrCU]["unknown"]++;
3170  }
3171 
3172  return;
3173 } // end AddToGatheredTransferStats
3174 
3175 // Paired function with AddToGatheredTransferStats to print out the stats.
3176 function PrintGatheredTransferStats( $pOutputHandle, $pHolderArray ) {
3177  $output = $pOutputHandle ? $pOutputHandle : fopen('php://stdout', 'w');
3178 
3179  // format the output a little
3180  $padding = str_repeat( " ", 15 );
3181 
3182  // print the count
3183  fprintf( $output, "Credit Union transactions processed:\n" );
3184  fprintf( $output, "CU\t\tCount\tMessages\n" );
3185  foreach( $pHolderArray as $thisCU => $cuStats ) {
3186  $thisCUPrint = substr( $thisCU . $padding, 0, 15 );
3187  fprintf( $output, "$thisCUPrint\t{$pHolderArray[$thisCU]["count"]}\n" );
3188 
3189  // show any failed messages
3190  if ( isset( $pHolderArray[$thisCU]["failed"] ) && $pHolderArray[$thisCU]["failed"]["count"] > 0 ) {
3191  $failedInfo = $pHolderArray[$thisCU]["failed"];
3192  fprintf( $output, "\n$padding\t\tNumber of failures: {$failedInfo["count"]}\n" );
3193 
3194  for ( $i = 0; $i < count( $failedInfo["messages"] ); $i++ ) {
3195  fprintf( $output, "$padding\t\t{$failedInfo["messages"][$i]}\n" );
3196  }
3197  }
3198 
3199  // show any success messages
3200  if ( isset( $pHolderArray[$thisCU]["success"] ) && $pHolderArray[$thisCU]["success"]["count"] > 0 ) {
3201  $successInfo = $pHolderArray[$thisCU]["success"];
3202  fprintf( $output, "\n$padding\t\tNumber of successful updates: {$successInfo["count"]}\n" );
3203 
3204  for ( $i = 0; $i < count( $successInfo["messages"] ); $i++ ) {
3205  fprintf( $output, "$padding\t\t{$successInfo["messages"][$i]}\n" );
3206  }
3207  }
3208 
3209  // show any access rights issues
3210  if ( isset( $pHolderArray[$thisCU]["rights"] ) && $pHolderArray[$thisCU]["rights"]["count"] > 0 ) {
3211  $rightsInfo = $pHolderArray[$thisCU]["rights"];
3212  fprintf( $output, "\n$padding\t\tNumber of access rights issues: {$rightsInfo["count"]}\n" );
3213 
3214  for ( $i = 0; $i < count( $rightsInfo["messages"] ); $i++ ) {
3215  fprintf( $output, "$padding\t\t{$rightsInfo["messages"][$i]}\n" );
3216  }
3217  }
3218 
3219  // show any unknown calls
3220  if ( isset( $pHolderArray[$thisCU]["unkonwn"] ) && $pHolderArray[$thisCU]["unknown"] > 0 ) {
3221  $unknownCount = $pHolderArray[$thisCU]["unknown"];
3222  fprintf( $output, "\n$padding\t\tNumber of unknown statistic calls (need to fix): {$unknownCount}\n" );
3223  }
3224 
3225  }
3226 
3227  return;
3228 } // end PrintGatheredTransferStats
3229 
3230 // Add the given message to the gathered stats slot for the given credit union.
3231 // This assumes a specific structure that is returned from the ProcessAlertsForAccount() function.
3232 function AddToGatheredAlertStats( &$pHolderArray, $pCurrCU, $pCategory, $pResults ) {
3233  if ( !isset( $pHolderArray[$pCurrCU ] ) ) {
3234  // set up a new slot
3235  $pHolderArray[$pCurrCU] = array( "gathering" => array( "time" => 0, "count" => 0, "messages" => array() ),
3236  "sending" => array( "time" => 0, "user_count" => 0, "alert_count" => 0, "messages" => array() ),
3237  "sent_stats" => array(),
3238  "unknown" => 0 );
3239  }
3240 
3241  if ( $pCategory == "gathering" ) {
3242  // gathering is per credit union account
3243  $pHolderArray[$pCurrCU]["gathering"]["count"]++;
3244 
3245  $pHolderArray[$pCurrCU]["gathering"]["time"] += $pResults["time"];
3246 
3247  // add any messages
3248  if ( count( $pResults["messages"] ) > 0 ) {
3249  $pHolderArray[$pCurrCU]["gathering"]["messages"] = array_merge( $pHolderArray[$pCurrCU]["gathering"]["messages"], $pResults["messages"] );
3250  }
3251  } else if ( $pCategory == "sending" ) {
3252  // sending is all users of the account and all alerts sent
3253  $pHolderArray[$pCurrCU]["sending"]["user_count"] += $pResults["user_count"];
3254  $pHolderArray[$pCurrCU]["sending"]["alert_count"] += $pResults["alert_count"];
3255 
3256  $pHolderArray[$pCurrCU]["sending"]["time"] += $pResults["time"];
3257 
3258  // add any messages
3259  if ( count( $pResults["messages"] ) > 0 ) {
3260  $pHolderArray[$pCurrCU]["sending"]["messages"] = array_merge( $pHolderArray[$pCurrCU]["sending"]["messages"], $pResults["messages"] );
3261  }
3262  } else if ( $pCategory == "sent_stats" ) {
3263  // add any specific alert counts
3264  $arrayKeys = array_keys( $pResults["alert_stats"] );
3265  for ( $i = 0; $i < count( $arrayKeys ); $i++ ) {
3266  // initialize if not used yet
3267  if ( !isset( $pHolderArray[$pCurrCU]["sent_stats"][$arrayKeys[$i]] ) ) {
3268  $pHolderArray[$pCurrCU]["sent_stats"][$arrayKeys[$i]] = 0;
3269  }
3270 
3271  // add the current count to the existing
3272  $pHolderArray[$pCurrCU]["sent_stats"][$arrayKeys[$i]] += $pResults["alert_stats"][$arrayKeys[$i]];
3273  }
3274  } else {
3275  $pHolderArray[$pCurrCU]["unknown"]++;
3276  }
3277 
3278  return;
3279 } // end AddToGatheredAlertStats
3280 
3281 // Paired function with AddToGatheredAlertStats to print out the stats.
3282 function PrintGatheredAlertStats( $pOutputHandle, $pHolderArray ) {
3283  $output = $pOutputHandle ? $pOutputHandle : fopen('php://stdout', 'w');
3284 
3285  // print the count
3286  fprintf( $output, "Credit Union Alerts Processed:\n" );
3287 
3288  foreach( $pHolderArray as $thisCU => $cuStats ) {
3289  fprintf( $output, "{$thisCU}\n" );
3290 
3291  $timeGathering = round( $pHolderArray[$thisCU]["gathering"]["time"], 1 );
3292  fprintf( $output, "\tGathered Count\t{$pHolderArray[$thisCU]["gathering"]["count"]}\t{$timeGathering} seconds\n" );
3293 
3294  // show the gathering messages if there are any
3295  if ( isset( $pHolderArray[$thisCU]["gathering"]["messages"] ) &&
3296  count( $pHolderArray[$thisCU]["gathering"]["messages"] ) > 0 ) {
3297  fprintf( $output, "\tGathering Messages\n" );
3298 
3299  for ( $i = 0; $i < count( $pHolderArray[$thisCU]["gathering"]["messages"] ); $i++ ) {
3300  fprintf( $output, "\t\t{$pHolderArray[$thisCU]["gathering"]["messages"][$i]}\n" );
3301  }
3302  }
3303 
3304  $timeSending = round( $pHolderArray[$thisCU]["sending"]["time"], 1 );
3305  fprintf( $output, "\tSent Alerts\t{$pHolderArray[$thisCU]["sending"]["alert_count"]}\tSent Users\t{$pHolderArray[$thisCU]["sending"]["user_count"]}\t{$timeSending} seconds\n" );
3306 
3307  // show the gathering messages if there are any
3308  if ( isset( $pHolderArray[$thisCU]["sending"]["messages"] ) &&
3309  count( $pHolderArray[$thisCU]["sending"]["messages"] ) > 0 ) {
3310  fprintf( $output, "\tSending Messages\n" );
3311 
3312  for ( $i = 0; $i < count( $pHolderArray[$thisCU]["sending"]["messages"] ); $i++ ) {
3313  fprintf( $output, "\t\t{$pHolderArray[$thisCU]["sending"]["messages"][$i]}\n" );
3314  }
3315  }
3316 
3317 
3318  $nameWidth = 25;
3319  $statisticTitle = substr( "Statistics" . str_repeat( " ", $nameWidth ), 0, $nameWidth );
3320  fprintf( $output, "\t{$statisticTitle}\tCount\n" );
3321 
3322  $stats = array_keys( $pHolderArray[$thisCU]["sent_stats"] );
3323  for ( $i = 0; $i < count( $stats ); $i++ ) {
3324  // align the first column
3325  $statistic = substr( $stats[$i] . str_repeat( " ", $nameWidth ), 0, $nameWidth );
3326  fprintf( $output, "\t{$statistic}\t{$pHolderArray[$thisCU]["sent_stats"][$stats[$i]]}\n" );
3327  }
3328  }
3329 
3330  return;
3331 } // end PrintGatheredAlertStats
3332 
3333 
3334 /**
3335  * This function will return either the {dest_key} or {source_key} information from the
3336  * transdtl table for this record.
3337  * It will accept the txn_data and parse the correct information from the record.
3338  * NOTE: The accounts passed in are expected to be local accounts.
3339  *
3340  * @param array $pTxnData - The txn_data for this transdtl record
3341  * @param string $pKey - The particular key to retrieve. Options are {from, to}
3342  *
3343  * @return array
3344  * [recordtype]
3345  * [accountnumber]
3346  * FOR DEPOSITS
3347  * [accounttype]
3348  * [certnumber]
3349  * FOR LOANS
3350  * [loannumber]
3351  */
3352 function BuildAccountKey($pTxnData, $pKey) {
3353 
3354  $acctKey = Array();
3355 
3356  if (HCU_array_key_exists("txn", $pTxnData)) {
3357  $lookup = Array(
3358  "acctid" => ($pKey == "from" ? "from" : "to"),
3359  "member" => ($pKey == "from" ? "frommember" : "tomember"),
3360  "accounttype" => ($pKey == "from" ? "fromsuffix" : "tosuffix"),
3361  "recordtype" => ($pKey == "from" ? "fromtype" : "totype")
3362  );
3363 
3364  $acctId = HCU_AcctIdExplode(HCU_array_key_value($lookup["acctid"], $pTxnData["txn"]));
3365 
3366  $acctKey['accountnumber'] = HCU_array_key_value($lookup["member"], $pTxnData['txn']);
3367 
3368  $acctKey['recordtype'] = HCU_array_key_value($lookup["recordtype"], $pTxnData['txn']);
3369 
3370  if ($acctKey['recordtype'] == "D") {
3371  $acctKey['accounttype'] = HCU_array_key_value($lookup["accounttype"], $pTxnData['txn']);
3372  $acctKey['certnumber'] = $acctId["segment4"];
3373 
3374  } else if ($acctKey['recordtype'] == "L" || $acctKey['recordtype'] == "C") {
3375  $acctKey['loannumber'] = HCU_array_key_value($lookup["accounttype"], $pTxnData['txn']);
3376  // ** For Credit card records the recordtype should be 'L'
3377  $acctKey['recordtype'] = "L";
3378  }
3379 
3380  }
3381 
3382  return $acctKey;
3383 }
3384 
3385 /**
3386  * This function will return either the {dest_key} information from the
3387  * transdtl table for this record.
3388  * It will accept the remote account data and parse the correct information from that.
3389  * NOTE: The account passed in are expected to be an M2M account.
3390  *
3391  * @param array $pTxnData - The txn_data for this transdtl record
3392  * @param string $pKey - The particular key to retrieve. Options are {from, to}
3393  *
3394  * @return array
3395  * [accountnumber]
3396  * [accounttype]
3397  * [display_name]
3398  * [name]
3399  * [addenda]
3400  * [ext_id]
3401  */
3402 function BuildM2MAccountKey( $pAcctData ) {
3403  $acctKey = Array();
3404 
3405  $acctKey['accountnumber'] = $pAcctData["rdfi"]["rdfi_account"];
3406  $acctKey['accounttype'] = $pAcctData["rdfi"]["rdfi_account_type"];
3407  $acctKey['display_name'] = $pAcctData["remote_entity"]["display_name"];
3408  $acctKey['name'] = $pAcctData["remote_entity"]["name"];
3409  $acctKey['addenda'] = $pAcctData["rdfi"]["addenda"];
3410  $acctKey['ext_id'] = $pAcctData["remote_entity"]["entry_id"];
3411 
3412 
3413  return $acctKey;
3414 } // end BuildM2MAccountKey
3415 
3416 /**
3417  * function ReturnIgnoreList($dbh)
3418  * Returns a comma delimited list of CUs in migration determined
3419  * by migration status found witin the system_options field
3420  * in the cuinfo table.
3421  * 09/19 Getting concatenated with blacklist without comma, just return the array.
3422  *
3423  * @param $dbh - database connection
3424  * @throws Exception
3425  * @return array $returnCUList - list of CUs in migration
3426  */
3427 function ReturnIgnoreList($dbh) {
3428 
3429  // constants - taken from cu_top.i, can't include since running this file from crontab
3430  $SYS_TYPE_CLOSED = 64;
3431  $SYS_TYPE_UPG_TEST = 128; // ** TESTING/SETUP
3432  $SYS_TYPE_UPG_BETA = 256; // ** BETA
3433  $returnCUList = [];
3434 
3435  $sql = "select user_name from cuinfo" .
3436  " where (coalesce(system_options, 0) & $SYS_TYPE_CLOSED) = 0" .
3437  " and ((coalesce(system_options, 0) & $SYS_TYPE_UPG_TEST) <> 0" .
3438  " or (coalesce(system_options, 0) & $SYS_TYPE_UPG_BETA) <> 0)" .
3439  " order by user_name";
3440 
3441  $sth = db_query($sql, $dbh);
3442  if (!$sth) {
3443  throw new Exception ("Select query failed.", 2);
3444  }
3445 
3446  $results = db_fetch_all($sth);
3447  $results = $results === false ? array() : $results;
3448 
3449  if (count($results) > 0) {
3450  foreach ( $results as $cuName) {
3451  // Always use Uppercase value of cucode
3452  $returnCUList[] = trim(strtoupper($cuName["user_name"]));
3453  }
3454  }
3455 
3456  return $returnCUList;
3457 
3458 }