Odyssey
AlliedPay_API.i
1 <?php
2 /*
3  * 08-19 Copyright HomeCU LLC
4  * SSO API to Allied Payments
5  * Some documentation:
6  * https://docs.alliedpayment.com
7  * Critical to connection is a valid routing number, even a fake one
8  * https://developer.wepay.com/docs/articles/testing
9  *
10  */
11 
12 /**
13  * @param array $parms by ref
14  * @throws Exception
15  */
16 function apn_config(&$parms) {
17 
18  $parms['devMode'] = (! HCU_array_key_value('devMode', $parms) ? 0 : HCU_array_key_value('devMode', $parms));
19  if ($parms['devMode']) {
20  // External Services / User Management
21  $parms['apnDomain'] = HCU_array_key_value('devDomain', $parms);
22  // HomeCU login
23  $parms['apnPubkey'] = HCU_array_key_value('devPubkey', $parms);
24  // HomeCU password
25  $parms['apnPrivkey'] = HCU_array_key_value('devPrivkey', $parms);
26  // Mobile Services / Deposits & History
27  $parms['apnURL'] = HCU_array_key_value('devURL', $parms);
28 
29  } else {
30  // External Services / User Management
31  $parms['apnDomain'] = HCU_array_key_value('prdDomain', $parms);
32  // HomeCU login
33  $parms['apnPubkey'] = HCU_array_key_value('prdPubkey', $parms);
34  // HomeCU password
35  $parms['apnPrivkey'] = HCU_array_key_value('prdPrivkey', $parms);
36  // Mobile Services / Deposits & History
37  $parms['apnURL'] = HCU_array_key_value('prdURL', $parms);
38  }
39 
40  if (
41  empty($parms['apnDomain']) ||
42  empty($parms['apnPubkey']) ||
43  empty($parms['apnPrivkey']) ||
44  empty($parms['apnURL'])
45  ) {
46  throw new Exception("Missing Parameters", 100);
47  }
48 }
49 
50 /**
51  * @param string $apnDomain
52  * @param string $mbrAccount
53  * @param array $mir
54  * @param array $accounts
55  * @return array
56  */
57 function apn_buildSSOPayload($apnDomain, $mbrAccount, $mir, $accounts) {
58 
59  try {
60  $return['status']['response'] = true;
61  $return['status']['message'] = 'Success';
62  $req_arr = [
63  'Application' => 'BILLPAY',
64  'UserName' => $mbrAccount,
65  'FinancialInstitutionId' => $apnDomain
66  ];
67 
68  if ($mir['class'] == 'B') {
69  $req_arr['PrimaryCompanyName'] = $mir['lastname'];
70 
71  } else {
72  $req_arr['FirstName'] = $mir['firstname'];
73  $req_arr['LastName'] = $mir['lastname'];
74  }
75 
76  $req_arr['Email'] = $mir['email'];
77  $req_arr['Accounts'] = [];
78  $req_arr['Address1'] = $mir['address1'];
79  $req_arr['City'] = $mir['city'];
80  $req_arr['State'] = $mir['state'];
81  $req_arr['Zip'] = $mir['zip'];
82 
83  if (HCU_array_key_exists('phonenumbers',$mir)) {
84  $req_arr['PhoneNumbers'] = $mir['phonenumbers'];
85  }
86 
87  $mirtype = (HCU_array_key_value('class', $mir) == 'B') ? 'Business' : 'Personal';
88 
89  if (!is_array($accounts) || !count($accounts)) {
90  throw new Exception('No Eligible Payment Accounts Found');
91  }
92 
93  // Was an else here - if you throw and exception you never get to the else
94  foreach ($accounts as $acctID => $account) {
95 
96  // insert $account info into $req_arr['Accounts'] array
97  // 08-19 Not sure why this was commented out so leaving it as is
98  // $account['AccountCIF'] = $mbrAccount;
99 
100  $a = [
101  'AccountNumber' => $account['AccountNumber'],
102  'AccountName' => $account['AccountName'],
103  // force treat as numeric
104  'AvailableAccountBalance' => $account['AvailableAccountBalance'] * 1,
105  'RoutingNumber' => $account['RoutingNumber'],
106  'AccountOwnerType' => $mirtype,
107  'AccountType' => $account['AccountType']
108  ];
109 
110  $account['AccountOwnerType'] = $mirtype;
111  // '$values' <- This is the key required by Allied. It is not an error.
112  $req_arr['Accounts']['$values'][] = $a;
113  }
114 
115  $return['data'] = json_encode($req_arr);
116 
117  } catch (Exception $e) {
118  $return['status']['response'] = false;
119  $return['status']['message'] = $e->getMessage() . " (" . $e->getLine() . ")";
120  $return['data'] = [];
121  }
122 
123  return $return;
124 }
125 
126 /**
127  * Use $parms configuration settings to build list of accounts
128  * @param resource object $dbh
129  * @param string $Cu
130  * @param string $Cn
131  * @param array $parms
132  * @return array
133  */
134 function apn_selectAccounts($dbh, $Cu, $Cn, $parms) {
135 
136  try {
137  $return['status']['response'] = true;
138  $return['status']['message'] = "Success";
139  // list of accounts goes here
140  $return['data'] = [];
141 
142  if (!HCU_array_key_value('rtn', $parms)) {
143  throw new Exception('Missing Routing Number');
144  }
145 
146  // In other SSO's we have made MICR optional, here it is confirmed as required for Allied
147  $rtn = HCU_array_key_value('rtn', $parms);
148  $acctsql = (HCU_array_key_value('acctsql', $parms) ? $parms['acctsql'] : "trim(micraccount)");
149  $balwhere = (HCU_array_key_value('balwhere', $parms) ? $parms['balwhere'] : "and deposittype = 'Y' and trim(micraccount) <> ''");
150  $sendjoint = (HCU_array_key_value('sendjoint', $parms) ? $parms['sendjoint'] : 0);
151 
152  $sql = "select {$acctsql} as account, trim(description) as name, available,
153  case when deposittype = 'Y' then 'checking' else 'savings' end as atype
154  from {$Cu}accountbalance where accountnumber='$Cn'
155  and deposittype in ('Y','S','N') $balwhere";
156 
157  if (!$sendjoint) {
158  $sql .= " and accounttype not like '%@%' ";
159  }
160  $sql .= " order by accounttype;";
161 
162  $sth = db_query($sql, $dbh);
163 
164  if (! ($sth) || (db_num_rows($sth) == 0)) {
165  throw new Exception('No Eligible Payment Accounts Found');
166  }
167 
168  for ($row = 0; $drow = db_fetch_array($sth, $row); $row++) {
169 
170  $return['data'][] = [
171  'AccountNumber' => HCU_array_key_value('account', $drow),
172  'AccountName' => HCU_array_key_value('name', $drow),
173  'AvailableAccountBalance' => HCU_array_key_value('available', $drow),
174  'RoutingNumber' => $rtn,
175  'AccountType' => HCU_array_key_value('atype', $drow)
176  ];
177  }
178 
179  } catch (Exception $e) {
180  $return['status']['response'] = false;
181  $return['status']['message'] = $e->getMessage() . " (" . $e->getLine() . ")";
182  $return['data'] = [];
183  }
184 
185  return $return;
186 }
187 
188 /**
189  * @param string $apnURL
190  * @param string $apnDomain
191  * @param string $apnPubkey
192  * @param string $apnPrivkey
193  * @param $mbrAccount
194  * @return array
195  */
196 function apn_buildAuthHdr($apnURL, $apnDomain, $apnPubkey, $apnPrivkey, $mbrAccount) {
197 
198  try {
199  $return['status']['response'] = true;
200  $return['status']['message'] = 'Success';
201 
202  if (empty($apnURL) || empty($apnDomain) || empty($apnPubkey) || empty($apnPrivkey) || empty($mbrAccount)) {
203  throw new Exception('Missing Parameters');
204  }
205 
206  $authdate = apn_timestamp();
207  $sigstring = "{$apnURL}\r\n{$authdate}\r\n";
208  $signature = hash_hmac('SHA1', $sigstring, $apnPrivkey, TRUE);
209  $signature = base64_encode($signature);
210  $authstring = "Authorization: TIMESTAMP username={$mbrAccount};domain={$apnDomain};"
211  . "timestamp={$authdate};signature={$signature};publicKey={$apnPubkey}";
212  $return['data'] = $authstring;
213 
214  } catch (Exception $e) {
215  $return['status']['response'] = false;
216  $return['status']['message'] = $e->getMessage() . " (" . $e->getLine() . ")";
217  $return['data'] = [];
218  }
219 
220  return $return;
221 }
222 
223 /**
224  * php date always gives 000000 for microtime - nd 'c' format gets +00:00 for GMT dates.
225  * Looking for format as 2018-08-02T20:05:43.273Z so we improvise to insert the microseconds.
226  * @return string
227  */
228 function apn_timestamp() {
229 
230  $t = microtime(true);
231  $t = substr($t, strpos($t, '.'), 4);
232  $d = gmdate('c');
233  $d = str_replace('+00:00', "{$t}Z", $d);
234  return $d;
235 }
236 
237 /**
238  * Curl the response from Allied.
239  * @param array $parms
240  * @param string $reqURL
241  * @param string $reqMethod
242  * @param array $reqHeaders
243  * @param string $reqData JSON
244  * @return array
245  */
246 function apn_embcurl($parms, $reqURL, $reqMethod, $reqHeaders, $reqData = '') {
247 
248  $curlopts = [
249  CURLOPT_RETURNTRANSFER => 1,
250  CURLOPT_SSL_VERIFYPEER => 0,
251  CURLOPT_SSL_VERIFYHOST => 0,
252  CURLOPT_HEADER => FALSE,
253  CURLOPT_URL => $reqURL
254  ];
255 
256  $ch = curl_init();
257  curl_setopt_array($ch, $curlopts);
258 
259  if ($reqMethod != 'GET') {
260  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $reqMethod);
261  }
262  if (strlen($reqData) > 0) {
263  curl_setopt($ch, CURLOPT_POSTFIELDS, $reqData);
264  }
265 
266  curl_setopt($ch, CURLOPT_HTTPHEADER, $reqHeaders);
267  $response = curl_exec($ch);
268 
269  if ($parms["logging"] == "enabled") {
270  $log = [
271  'env' => ($parms['environment']) ?? 'No Environment in params',
272  'ch' => print_r($ch, true),
273  'headers' => $reqHeaders,
274  'method' => $reqMethod,
275  'data' => $reqData,
276  'url' => $reqURL,
277  'response' => $response,
278  'curlinfo' => print_r(curl_getinfo($ch),true)
279  ];
280  LogSSOActivity(AlliedLogParams($log));
281  }
282 
283  $respHTTP = curl_getinfo($ch, CURLINFO_HTTP_CODE);
284 
285  if ($respHTTP >= 400 && $respHTTP < 600) {
286  // HTTP Response 4xx client error or 5xx server error
287  $respArr = [
288  "error" => [
289  "status" => "hcuH" . $respHTTP,
290  "message" => "Connection Failed HTTP Error"
291  ]
292  ];
293  } elseif (curl_errno($ch)) {
294  $respArr =[
295  "error" =>[
296  "status" => "hcuC" . curl_errno($ch),
297  "message" => "Curl Error"
298  ]
299  ];
300  } elseif (!isset($response) || $response == '') {
301  $respArr = [
302  "error" => [
303  "status" => "hcuE" . curl_errno($ch),
304  "message" => "Empty Response"
305  ]
306  ];
307  } else {
308  $respArr = json_decode($response, TRUE);
309  }
310 
311  curl_close($ch);
312 
313  return $respArr;
314 }
315 
316 /**
317  * Helper for apn_embcurl, when there are too many params pass the param as an array.
318  * @param array $log_params
319  * @return array
320  */
321 function AlliedLogParams($log_params) {
322 
323  // get the environment info passed in
324  $logParms = $log_params['env'];
325  // the id used across all communications in session
326  $logParms["token"] = '';
327  // setting logPoint here makes all the log entries look alike
328  // not very helpful for support when vendor uses multiple calls.
329  // set logPoint before curl call, allow clarity for possibly multiple calls
330  // $logParms["logPoint"] = "ALLIED SSO apn_embcurl";
331  // the id for this transaction
332  $logParms["txnId"] = time();
333  // the request
334  $logParms["request"] = "curl ";
335 
336  if ($log_params['method'] != 'GET') {
337  $logParms["request"] .= "-X {$log_params['method']} ";
338  }
339 
340  if (is_array($log_params['headers'])) {
341  foreach ($log_params['headers'] as $hdr) {
342  $logParms['request'] .= "-H '$hdr' ";
343  }
344  }
345 
346  // the request
347  if (strlen($log_params['data']) > 0) {
348  $logParms['request'] .= "-d '{$log_params['data']}' ";
349  }
350  // Request URL
351  $logParms['request'] .= "'{$log_params['url']}' ";
352  $logParms["reply"] = $log_params['curlinfo'];
353  $logParms["reply"] .= "\n{$log_params['response']}";
354 
355  return $logParms;
356 }
357 
358 /**
359  * Populate MIR data, some special keys used so can't use populateMIR()
360  * @param array $MIR mir packet info from core or local load
361  * @param string $Ml homebanking email to use if mir.email is empty
362  * @param array $reqMIR required items can't be empty in mir
363  * @param string $datefmt format to use for DOB
364  * @param string $phones 'flat' or 'split'
365  * 'flat' = fill ['PHONE'] with first non-null of HomePhone, CellPhone, WorkPhone
366  * 'split' = split each phone # into sub-array of optional [Area], required [Pre] & [Num]
367  * 'named' = array of number (string), name (string), and isTextCapable (boolean).
368  * note that we don't know isTextCapable so it will be false
369  * 08/19: per the code below, true for cell false for other phones
370  * @param boolean $noEmpty unset empty mir values
371  * @return array
372  * @throws Exception
373  */
374 function apn_populateMIR($MIR, $Ml, $reqMIR, $datefmt = 'Y-m-d', $phones = 'flat', $noEmpty = false) {
375 
376  try {
377  /* strip slashes and dashes and non-digits from phone numbers
378  * and zip code and tax id; rearrange DOB into mmddyyyy
379  * then check to see if we have enough valid data to
380  * satisfy the request
381  */
382  if ($phones == 'flat') {
383 
384  $MIR['data']['phone'] = preg_replace('/\D/', '', $MIR['data']['homephone']);
385  if (trim($MIR['data']['phone']) == '') {
386  $MIR['data']['phone'] = preg_replace('/\D/', '', $MIR['data']['cellphone']);
387  }
388  if (trim($MIR['data']['phone']) == '') {
389  $MIR['data']['phone'] = preg_replace('/\D/', '', $MIR['data']['workphone']);
390  }
391 
392  } elseif ($phones == 'split') {
393 
394  $MIR['data']['homephone'] = preg_replace('/\D/', '', $MIR['data']['homephone']);
395  switch (strlen($MIR['data']['homephone'])) {
396  case 10:
397  $MIR['data']['homephone'] = [
398  'area' => substr($MIR['data']['homephone'], 0, 3),
399  'pre' => substr($MIR['data']['homephone'], 3, 3),
400  'num' => substr($MIR['data']['homephone'], 6, 4)
401  ];
402  break;
403  case 7:
404  $MIR['data']['homephone'] = [
405  'pre' => substr($MIR['data']['homephone'], 3, 3),
406  'num' => substr($MIR['data']['homephone'], 6, 4)
407  ];
408  break;
409  default:
410  unset($MIR['data']['homephone']);
411  break;
412  }
413 
414  $MIR['data']['cellphone'] = preg_replace('/\D/', '', $MIR['data']['cellphone']);
415 
416  switch (strlen($MIR['data']['cellphone'])) {
417  case 10:
418  $MIR['data']['cellphone'] = [
419  'area' => substr($MIR['data']['cellphone'], 0, 3),
420  'pre' => substr($MIR['data']['cellphone'], 3, 3),
421  'num' => substr($MIR['data']['cellphone'], 6, 4)
422  ];
423  break;
424  case 7:
425  $MIR['data']['cellphone'] = [
426  'pre' => substr($MIR['data']['cellphone'], 3, 3),
427  'num' => substr($MIR['data']['cellphone'], 6, 4)
428  ];
429  break;
430  default:
431  unset($MIR['data']['cellphone']);
432  break;
433  }
434 
435  $MIR['data']['workphone'] = preg_replace('/\D/', '', $MIR['data']['workphone']);
436 
437  switch (strlen($MIR['data']['workphone'])) {
438  case 10:
439  $MIR['data']['workphone'] = [
440  'area' => substr($MIR['data']['workphone'], 0, 3),
441  'pre' => substr($MIR['data']['workphone'], 3, 3),
442  'num' => substr($MIR['data']['workphone'], 6, 4)
443  ];
444  break;
445  case 7:
446  $MIR['data']['workphone'] = [
447  'pre' => substr($MIR['data']['workphone'], 3, 3),
448  'num' => substr($MIR['data']['workphone'], 6, 4)
449  ];
450  break;
451  default:
452  unset($MIR['data']['workphone']);
453  break;
454  }
455 
456  } elseif ($phones == 'named') {
457 
458  $MIR['data']['phonenumbers'] = [];
459  $MIR['data']['homephone'] = preg_replace('/\D/', '', $MIR['data']['homephone']);
460 
461  switch (strlen($MIR['data']['homephone'])) {
462  case 10:
463  case 7:
464  $MIR['data']['phonenumbers'][] = [
465  'number' => $MIR['data']['homephone'],
466  'name' => 'Home',
467  'isTextCapable' => FALSE
468  ];
469  break;
470  default:
471  unset($MIR['data']['homephone']);
472  break;
473  }
474 
475  $MIR['data']['cellphone'] = preg_replace('/\D/', '', $MIR['data']['cellphone']);
476 
477  switch (strlen($MIR['data']['cellphone'])) {
478  case 10:
479  case 7:
480  // assume cell phone would be text capable
481  $MIR['data']['phonenumbers'][] = [
482  'number' => $MIR['data']['cellphone'],
483  'name' => 'Cell',
484  'isTextCapable' => TRUE
485  ];
486  break;
487  default:
488  unset($MIR['data']['cellphone']);
489  break;
490  }
491 
492  $MIR['data']['workphone'] = preg_replace('/\D/', '', $MIR['data']['workphone']);
493 
494  switch (strlen($MIR['data']['workphone'])) {
495  case 10:
496  case 7:
497  $MIR['data']['phonenumbers'][] = [
498  'number' => $MIR['data']['workphone'],
499  'name' => 'Work',
500  'isTextCapable' => FALSE
501  ];
502  break;
503  default:
504  unset($MIR['data']['workphone']);
505  break;
506  }
507 
508  if (!count($MIR['data']['phonenumbers'])) {
509  unset($MIR['data']['phonenumbers']);
510  }
511  // End named
512  }
513 
514  $MIR['data']['dob'] = format_date($MIR['data']['dob'], $datefmt);
515 
516  $rmlist = [' ', '-'];
517  $MIR['data']['ssn'] = str_replace($rmlist, '', $MIR['data']['ssn']);
518  $MIR['data']['zip'] = str_replace($rmlist, '', $MIR['data']['zip']);
519 
520  if (strlen($MIR['data']['zip']) < 5) {
521  unset($MIR['data']['zip']);
522  }
523 
524  $rmlist = ["#", "&", "/", "%", ",", ":", "=", "?", "'"];
525 
526  $EMAIL = (empty($MIR['data']['email']) ? $Ml : $MIR['data']['email']);
527  $MIR['data']['email'] = str_replace($rmlist, "", $EMAIL);
528  $MIR['data']['firstname'] = str_replace($rmlist, "", $MIR['data']['firstname']);
529  $MIR['data']['middlename'] = str_replace($rmlist, "", $MIR['data']['middlename']);
530  $MIR['data']['lastname'] = str_replace($rmlist, "", $MIR['data']['lastname']);
531  $MIR['data']['address1'] = str_replace($rmlist, "", $MIR['data']['address1']);
532  $MIR['data']['address2'] = str_replace($rmlist, "", $MIR['data']['address2']);
533  $MIR['data']['city'] = str_replace($rmlist, "", $MIR['data']['city']);
534  $MIR['data']['state'] = str_replace($rmlist, "", $MIR['data']['state']);
535  $MIR['data']['accountnumber'] = str_replace($rmlist, "", $MIR['data']['accountnumber']);
536  # default country code to US. Assume CU will specify for other countries
537  if (trim($MIR['data']['cc']) == '') {
538  $MIR['data']['cc'] = 'US';
539  }
540 
541  // if firstname is missing but lastname is present, treat as a business account
542  if (! HCU_array_key_value('firstname', $MIR['data']) && HCU_array_key_value('lastname', $MIR['data'])) {
543  $MIR['data']['class'] = 'B';
544  // clear firstname from the 'required values' list
545  unset($reqMIR['firstname']);
546  }
547 
548  if ($noEmpty) {
549  // D.R.Y. some of the redundant code
550  $keys = [
551  'email',
552  'firstname',
553  'middlename',
554  'lastname',
555  'address1',
556  'address2',
557  'city',
558  'state',
559  'accountnumber',
560  'dob',
561  'zip'
562  ];
563 
564  foreach ($keys as $field) {
565  if (trim($MIR['data'][$field]) == false) {
566  unset($MIR['data'][$field]);
567  }
568  }
569  }
570 
571  $missing = array_diff_key($reqMIR, $MIR['data']);
572  if (sizeof($missing)) {
573  throw new Exception("Incomplete Member Info (" . join(", ", array_keys($missing)) . ")");
574  }
575 
576  $return['status']['response'] = true;
577  $return['status']['message'] = 'Success';
578  $return['data'] = $MIR['data'];
579 
580  } catch (Exception $e) {
581  $return['status']['response'] = false;
582  $return['status']['message'] = $e->getMessage();
583  // $return['status']['message'] = "(" . $e->getline() . ") " . $e->getMessage();
584  $return['data'] = [];
585  }
586 
587  return $return;
588 }
589