Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.49% covered (danger)
0.49%
2 / 405
0.00% covered (danger)
0.00%
0 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
Polaris
0.49% covered (danger)
0.49%
2 / 405
0.00% covered (danger)
0.00%
0 / 28
5179.58
0.00% covered (danger)
0.00%
0 / 1
 init
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
7.23
 makeRequest
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
30
 formatJSONTime
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 encodeJSONTime
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getMyHolds
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
12
 getStatus
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
56
 getStatuses
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getConfig
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getHolding
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 placeHold
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
30
 getPickUpLocations
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 getDefaultPickUpLocation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPurchaseHistory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNewItems
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 findReserves
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 patronLogin
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 getMyFines
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 getMyProfile
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 getMyTransactions
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 renewMyItems
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
20
 getRenewDetails
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 cancelHolds
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 getCancelHoldDetails
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCheckoutHistory
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
30
 getHoldCount
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 suspendHolds
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
12
 getSuspendHoldDetails
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 reactivateHolds
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3/**
4 * Polaris ILS Driver
5 *
6 * PHP version 8
7 *
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License version 2,
11 * as published by the Free Software Foundation.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
21 *
22 * @category VuFind
23 * @package  ILS_Drivers
24 * @author   BookSite <vufind-tech@lists.sourceforge.net>
25 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
26 * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
27 */
28
29namespace VuFind\ILS\Driver;
30
31use VuFind\Exception\ILS as ILSException;
32
33use function count;
34use function intval;
35use function strlen;
36
37/**
38 * VuFind Connector for Polaris
39 *
40 * Based on Polaris 1.4 API
41 *
42 * @category VuFind
43 * @package  ILS_Drivers
44 * @author   BookSite <vufind-tech@lists.sourceforge.net>
45 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
46 * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
47 */
48class Polaris extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface
49{
50    use \VuFindHttp\HttpServiceAwareTrait;
51
52    /**
53     * Web services host
54     *
55     * @var string
56     */
57    protected $ws_host;
58
59    /**
60     * Web services application path
61     *
62     * @var string
63     */
64    protected $ws_app;
65
66    /**
67     * Web services ID
68     *
69     * @var string
70     */
71    protected $ws_api_id;
72
73    /**
74     * Web services key
75     *
76     * @var string
77     */
78    protected $ws_api_key;
79
80    /**
81     * Default pick up location
82     *
83     * @var string
84     */
85    protected $defaultPickUpLocation;
86
87    /**
88     * Web services requesting organization ID
89     *
90     * @var string
91     */
92    protected $ws_requestingorgid;
93
94    /**
95     * Initialize the driver.
96     *
97     * Validate configuration and perform all resource-intensive tasks needed to
98     * make the driver active.
99     *
100     * @throws ILSException
101     * @return void
102     */
103    public function init()
104    {
105        if (empty($this->config) || !isset($this->config['PAPI'])) {
106            throw new ILSException('Configuration needs to be set.');
107        }
108
109        // Define Polaris PAPI parameters
110        $this->ws_host    = $this->config['PAPI']['ws_host'];
111        $this->ws_app     = $this->config['PAPI']['ws_app'];
112        $this->ws_api_id  = $this->config['PAPI']['ws_api_id'];
113        $this->ws_api_key = $this->config['PAPI']['ws_api_key'];
114        $this->ws_requestingorgid    = $this->config['PAPI']['ws_requestingorgid'];
115        $this->defaultPickUpLocation
116            = $this->config['Holds']['defaultPickUpLocation'] ?? null;
117    }
118
119    /**
120     * Make Request
121     *
122     * Makes a request to the Polaris Restful API
123     *
124     * @param string $api_query      Query string for request
125     * @param string $http_method    HTTP method (default = GET)
126     * @param string $patronpassword Patron password (optional)
127     * @param bool   $json           Optional JSON attachment
128     *
129     * @throws ILSException
130     * @return obj
131     */
132    protected function makeRequest(
133        $api_query,
134        $http_method = 'GET',
135        $patronpassword = '',
136        $json = false
137    ) {
138        // auth has to be in GMT, otherwise use config-level TZ
139        $site_config_TZ = date_default_timezone_get();
140        date_default_timezone_set('GMT');
141        $date = date('D, d M Y H:i:s T');
142        date_default_timezone_set($site_config_TZ);
143
144        $url = $this->ws_host . $this->ws_app . $api_query;
145
146        $signature_text = $http_method . $url . $date . $patronpassword;
147        $signature = base64_encode(
148            hash_hmac('sha1', $signature_text, $this->ws_api_key, true)
149        );
150
151        $auth_token = "PWS {$this->ws_api_id}:$signature";
152        $http_headers = [
153            'Content-type: application/json',
154            'Accept: application/json',
155            "PolarisDate: $date",
156            "Authorization: $auth_token",
157        ];
158
159        try {
160            $client = $this->httpService->createClient($url);
161
162            // Attach JSON if necessary
163            $json_data = null;
164            if ($json !== false) {
165                $json_data = json_encode($json);
166                $client->setRawBody($json_data);
167                $client->setEncType('application/json');
168            }
169
170            // httpService doesn't explicitly support PUT, so add this:
171            if ($http_method == 'PUT') {
172                $http_headers[] = 'Content-Length: ' . strlen($json_data);
173            }
174            $client->setHeaders($http_headers);
175            $client->setMethod($http_method);
176            $result = $client->send();
177        } catch (\Exception $e) {
178            $this->throwAsIlsException($e);
179        }
180
181        if (!$result->isSuccess()) {
182            throw new ILSException('HTTP error');
183        }
184
185        return json_decode($result->getBody());
186    }
187
188    /**
189     * Return human-readable date from text like Date(1360051200000-0800)
190     *
191     * @param string $jsontime Input
192     *
193     * @return string
194     */
195    public function formatJSONTime($jsontime)
196    {
197        preg_match('/Date\((\d+)\-(\d){2}(\d){2}\)/', $jsontime, $matches);
198        if (count($matches) > 0) {
199            $matchestmp = intval($matches[1] / 1000);
200            $date = date('n-j-Y', $matchestmp);
201        } else {
202            $date = 'n/a';
203        }
204        return $date;
205    }
206
207    /**
208     * Encode from human-readable date to text like Date(1360051200000-0800)
209     *
210     * @param string $date Input
211     *
212     * @return string
213     */
214    public function encodeJSONTime($date)
215    {
216        // auth has to be in GMT, otherwise use config-level TZ
217        //$site_config_TZ = date_default_timezone_get();
218        //date_default_timezone_set('GMT');
219        $unix_time = strtotime($date);
220        //date_default_timezone_set($site_config_TZ);
221
222        $json_time = '/Date(' . $unix_time . '000)/';
223        return $json_time;
224    }
225
226    /**
227     * Get Patron Holds
228     *
229     * This is responsible for retrieving all holds by a specific patron.
230     *
231     * @param array $patron The patron array from patronLogin
232     *
233     * @return mixed                Array of the patron's holds on success.
234     */
235    public function getMyHolds($patron)
236    {
237        $holds = [];
238        $response = $this->makeRequest(
239            "patron/{$patron['cat_username']}/holdrequests/all",
240            'GET',
241            $patron['cat_password']
242        );
243        $holds_response_array = $response->PatronHoldRequestsGetRows;
244        foreach ($holds_response_array as $holds_response) {
245            // only display item if it is NOT expired
246            if ($holds_response->StatusID > 8) {
247                continue;
248            }
249
250            $create = $this->formatJSONTime($holds_response->ActivationDate);
251            $expire = $this->formatJSONTime($holds_response->ExpirationDate);
252
253            $holds[] = [
254                'type'     => $holds_response->StatusDescription,
255                'id'       => $holds_response->BibID,
256                'location' => $holds_response->PickupBranchName,
257                'reqnum'   => $holds_response->HoldRequestID,
258                'expire'   => $expire,
259                'create'   => $create,
260                'position' => $holds_response->QueuePosition,
261                'title'    => $holds_response->Title,
262            ];
263        }
264        return $holds;
265    }
266
267    /**
268     * Get Status
269     *
270     * This is responsible for retrieving the status information of a certain
271     * record.
272     *
273     * @param string $id The record id to retrieve the holdings for
274     *
275     * @return mixed     On success, an associative array with the following keys:
276     * id, availability (boolean), status, location, reserve, callnumber.
277     */
278    public function getStatus($id)
279    {
280        $holding = [];
281        $response = $this->makeRequest("bib/$id/holdings");
282        $holdings_response_array = $response->BibHoldingsGetRows;
283
284        $copy_count = 0;
285        foreach ($holdings_response_array as $holdings_response) {
286            //$holdings_response = $holdings_response_array[0];
287            $copy_count++;
288
289            $availability = 0;
290            if (
291                ($holdings_response->CircStatus == 'In')
292                || ($holdings_response->CircStatus == 'Just Returned')
293                || ($holdings_response->CircStatus == 'On Shelf')
294                || ($holdings_response->CircStatus == 'Available - Check shelves')
295            ) {
296                $availability = 1;
297            }
298
299            $duedate = '';
300            if ($holdings_response->DueDate) {
301                $duedate = date('n-j-Y', strtotime($holdings_response->DueDate));
302            }
303
304            $holding[] = [
305                'availability' => $availability,
306                'id'         => $id,
307                'status'     => $holdings_response->CircStatus,
308                'location'   => $holdings_response->LocationName,
309                //'reserve'  => 'No',
310                'callnumber' => $holdings_response->CallNumber,
311                'duedate'    => $duedate,
312                //'number'   => $holdings_response->ItemsIn,
313                'number'     => $copy_count,
314                'barcode'         => $holdings_response->Barcode,
315                'shelf_location'  => $holdings_response->ShelfLocation,
316                'collection_name' => $holdings_response->CollectionName,
317                //'per_item_holdable' => $per_item_holdable,
318                //'designation' => $designation,
319                'holdable' => $holdings_response->Holdable,
320            ];
321        }
322        return $holding;
323    }
324
325    /**
326     * Get Statuses
327     *
328     * This is responsible for retrieving the status information for a
329     * collection of records.
330     *
331     * @param array $ids The array of record ids to retrieve the status for
332     *
333     * @return mixed         An array of getStatus() return values on success.
334     */
335    public function getStatuses($ids)
336    {
337        $items = [];
338        $count = 0;
339        foreach ($ids as $id) {
340            $items[$count] = $this->getStatus($id);
341            $count++;
342        }
343        return $items;
344    }
345
346    /**
347     * Public Function which retrieves renew, hold and cancel settings from the
348     * driver ini file.
349     *
350     * @param string $function The name of the feature to be checked
351     * @param array  $params   Optional feature-specific parameters (array)
352     *
353     * @return array An array with key-value pairs.
354     *
355     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
356     */
357    public function getConfig($function, $params = [])
358    {
359        if (isset($this->config[$function])) {
360            $functionConfig = $this->config[$function];
361        } else {
362            $functionConfig = false;
363        }
364        return $functionConfig;
365    }
366
367    /**
368     * Get Holding
369     *
370     * This is responsible for retrieving the holding information of a certain
371     * record.
372     *
373     * @param string $id      The record id to retrieve the holdings for
374     * @param array  $patron  Patron data
375     * @param array  $options Extra options (not currently used)
376     *
377     * @return mixed         On success, an associative array with the following
378     * keys: id, availability (boolean), status, location, reserve, callnumber,
379     * duedate, number, barcode.
380     *
381     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
382     */
383    public function getHolding($id, array $patron = null, array $options = [])
384    {
385        return $this->getStatus($id);
386    }
387
388    /**
389     * Place Hold
390     *
391     * Attempts to place a hold or recall on a particular item and returns
392     * an array with result details.
393     *
394     * @param array $holdDetails An array of item and patron data
395     *
396     * @return mixed An array of data on the request including
397     * whether or not it was successful and a system message (if available)
398     */
399    public function placeHold($holdDetails)
400    {
401        // what do workstation & userid really mean in this context?
402        $workstationid = '1';
403        $userid = '1';
404
405        // all activations are for now(), for now.
406        // microtime is msec or sec?? seems to have changed
407        $activationdate = '/Date(' . intval(microtime(true) * 1000) . ')/';
408        if (empty($holdDetails['barcode'])) {
409            $holdDetails['barcode'] = '';
410        }
411
412        $jsonrequest = [
413            'PatronID'     => $holdDetails['patron']['id'],
414            'BibID'        => $holdDetails['id'],
415            'ItemBarcode'  => $holdDetails['barcode'],
416            'VolumeNumber' => '',
417            'Designation'  => '',
418            'PickupOrgID'     => $holdDetails['pickUpLocation'],
419            'IsBorrowByMail'  => '0',
420            'PatronNotes'     => $holdDetails['comment'],
421            'ActivationDate'  => $activationdate,
422            'WorkstationID'   => $workstationid,
423            'UserID'          => $userid,
424            'RequestingOrgID' => $this->ws_requestingorgid,
425            'TargetGUID'      => '',
426        ];
427
428        $response = $this->makeRequest('holdrequest', 'POST', '', $jsonrequest);
429
430        if ($response->StatusValue == 1) {
431            return [ 'success' => true,  'sysMessage' => $response->Message ];
432        } elseif ($response->StatusValue == 5) {
433            // auto say "yes" to Conditional: Accept even with existing holds
434            // response
435            $reply_jsonrequest = [
436                // apparent bug in API, TxnGroupQualifer missing final "i"
437                'TxnGroupQualifier' => $response->TxnGroupQualifer,
438                'TxnQualifier' => $response->TxnQualifier,
439                'RequestingOrgID' => $this->ws_requestingorgid,
440                'Answer' => 1,
441                'State' => 5,
442            ];
443
444            $reply_response = $this->makeRequest(
445                "holdrequest/{$response->RequestGUID}",
446                'PUT',
447                '',
448                $reply_jsonrequest
449            );
450
451            if ($reply_response->StatusValue == 1) {
452                // auto-reply success
453                return [ 'success' => true,  'sysMessage' => $response->Message ];
454            } else {
455                return [ 'success' => false, 'sysMessage' => $response->Message ];
456            }
457        } else {
458            return [ 'success' => false, 'sysMessage' => $response->Message ];
459        }
460    }
461
462    /**
463     * Get Pick Up Locations
464     *
465     * This is responsible for gettting a list of valid library locations for
466     * holds / recall retrieval
467     *
468     * @param array $patron      Patron information returned by the patronLogin
469     * method.
470     * @param array $holdDetails Optional array, only passed in when getting a list
471     * in the context of placing or editing a hold. When placing a hold, it contains
472     * most of the same values passed to placeHold, minus the patron data. When
473     * editing a hold it contains all the hold information returned by getMyHolds.
474     * May be used to limit the pickup options or may be ignored. The driver must
475     * not add new options to the return array based on this data or other areas of
476     * VuFind may behave incorrectly.
477     *
478     * @throws ILSException
479     * @return array             An array of associative arrays with locationID
480     * and locationDisplay keys
481     *
482     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
483     */
484    public function getPickUpLocations($patron = false, $holdDetails = null)
485    {
486        $locations = [];
487        if (isset($this->ws_pickUpLocations)) {
488            // hardcoded pickup locations in the .ini file? or...
489            foreach ($this->ws_pickUpLocations as $code => $library) {
490                $locations[] = [
491                    'locationID'      => $code,
492                    'locationDisplay' => $library,
493                ];
494            }
495        } else {
496            // we get them from the API
497            $response = $this->makeRequest('organizations/branch');
498            $locations_response_array = $response->OrganizationsGetRows;
499            foreach ($locations_response_array as $location_response) {
500                $locations[] = [
501                    'locationID'      => $location_response->OrganizationID,
502                    'locationDisplay' => $location_response->Name,
503                ];
504            }
505        }
506        return $locations;
507    }
508
509    /**
510     * Get Default Pick Up Location
511     *
512     * Returns the default pick up location set in VoyagerRestful.ini
513     *
514     * @param array $patron      Patron information returned by the patronLogin
515     * method.
516     * @param array $holdDetails Optional array, only passed in when getting a list
517     * in the context of placing a hold; contains most of the same values passed to
518     * placeHold, minus the patron data.    May be used to limit the pickup options
519     * or may be ignored.
520     *
521     * @return string           The default pickup location for the patron.
522     *
523     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
524     */
525    public function getDefaultPickUpLocation($patron = false, $holdDetails = null)
526    {
527        return $this->defaultPickUpLocation;
528    }
529
530    /**
531     * Get Purchase History
532     *
533     * This is responsible for retrieving the acquisitions history data for the
534     * specific record (usually recently received issues of a serial).
535     *
536     * @param string $id The record id to retrieve the info for
537     *
538     * @return mixed         An array with the acquisitions data on success.
539     */
540    public function getPurchaseHistory($id)
541    {
542        return [];
543    }
544
545    /**
546     * Get New Items
547     *
548     * Retrieve the IDs of items recently added to the catalog.
549     *
550     * @param int $page    Page number of results to retrieve (counting starts at 1)
551     * @param int $limit   The size of each page of results to retrieve
552     * @param int $daysOld The maximum age of records to retrieve in days (max. 30)
553     * @param int $fundId  optional fund ID to use for limiting results (use a value
554     * returned by getFunds, or exclude for no limit); note that "fund" may be a
555     * misnomer - if funds are not an appropriate way to limit your new item
556     * results, you can return a different set of values from getFunds. The
557     * important thing is that this parameter supports an ID returned by getFunds,
558     * whatever that may mean.
559     *
560     * @return array             Associative array with 'count' and 'results' keys
561     *
562     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
563     */
564    public function getNewItems($page, $limit, $daysOld, $fundId = null)
565    {
566        return ['count' => 0, 'results' => []];
567    }
568
569    /**
570     * Find Reserves
571     *
572     * Obtain information on course reserves.
573     *
574     * @param string $course ID from getCourses (empty string to match all)
575     * @param string $inst   ID from getInstructors (empty string to match all)
576     * @param string $dept   ID from getDepartments (empty string to match all)
577     *
578     * @return mixed An array of associative arrays representing reserve items.
579     *
580     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
581     */
582    public function findReserves($course, $inst, $dept)
583    {
584        return [];
585    }
586
587    /**
588     * Patron Login
589     *
590     * This is responsible for authenticating a patron against the catalog.
591     *
592     * @param string $username The patron username
593     * @param string $password The patron password
594     *
595     * @return mixed           Associative array of patron info on successful login,
596     * null on unsuccessful login.
597     */
598    public function patronLogin($username, $password)
599    {
600        // username == barcode
601        $response = $this->makeRequest("patron/$username", 'GET', "$password");
602
603        if (!$response->ValidPatron) {
604            return null;
605        }
606
607        $user = [];
608
609        $user['id']           = $response->PatronID;
610        $user['firstname']    = null;
611        $user['lastname']     = null;
612        $user['cat_username'] = $response->PatronBarcode;
613        $user['cat_password'] = $password;
614        $user['email']        = null;
615        $user['major']        = null;
616        $user['college']      = null;
617
618        return $user;
619    }
620
621    /**
622     * Get Patron Fines
623     *
624     * This is responsible for retrieving all fines by a specific patron.
625     *
626     * @param array $patron The patron array from patronLogin
627     *
628     * @return mixed        Array of the patron's fines on success.
629     */
630    public function getMyFines($patron)
631    {
632        $fineList = [];
633
634        $response = $this->makeRequest(
635            "patron/{$patron['cat_username']}/account/outstanding",
636            'GET',
637            $patron['cat_password']
638        );
639        $fines_response_array = $response->PatronAccountGetRows;
640
641        foreach ($fines_response_array as $fines_response) {
642            $fineList[] = [
643            // fees in vufind are in pennies
644            'amount'   => $fines_response->TransactionAmount * 100,
645            'checkout' => $this->formatJSONTime($fines_response->CheckOutDate),
646            'fine'     => $fines_response->FeeDescription,
647            'balance'  => $fines_response->OutstandingAmount * 100,
648            'duedate'    => $this->formatJSONTime($fines_response->DueDate),
649            'createdate' => $this->formatJSONTime($fines_response->TransactionDate),
650            'id'    => $fines_response->BibID,
651            'title' => $fines_response->Title,
652            ];
653        }
654
655        return $fineList;
656    }
657
658    /**
659     * Get Patron Profile
660     *
661     * This is responsible for retrieving the profile for a specific patron.
662     *
663     * @param array $patron The patron array
664     *
665     * @throws ILSException
666     * @return array Array of the patron's profile data on success.
667     */
668    public function getMyProfile($patron)
669    {
670        // firstname, lastname, address1, address2, zip, phone, group
671        $response = $this->makeRequest(
672            "patron/{$patron['cat_username']}/basicdata",
673            'GET',
674            $patron['cat_password']
675        );
676        $profile_response = $response->PatronBasicData;
677        $profile = [
678          'firstname' => $profile_response->NameFirst,
679          'lastname'  => $profile_response->NameLast,
680          'phone'     => $profile_response->PhoneNumber,
681        ];
682        return $profile;
683    }
684
685    /**
686     * Get Patron Transactions
687     *
688     * This is responsible for retrieving all transactions (i.e. checked out items)
689     * by a specific patron.
690     *
691     * @param array $patron The patron array from patronLogin
692     *
693     * @return mixed Array of associative arrays of the patron's transactions on
694     * success.
695     */
696    public function getMyTransactions($patron)
697    {
698        // duedate, id, barcode, renew (count), request (pending count),
699        // volume (vol number), publication_year, renewable, message, title, item_id
700        // polaris apis: PatronItemsOutGet
701        $transactions = [];
702        $response = $this->makeRequest(
703            "patron/{$patron['cat_username']}/itemsout/all",
704            'GET',
705            $patron['cat_password']
706        );
707
708        foreach ($response->PatronItemsOutGetRows as $trResponse) {
709            // any more renewals available?
710            if (($trResponse->RenewalLimit - $trResponse->RenewalCount) > 0) {
711                $renewable = true;
712            } else {
713                $renewable = false;
714            }
715            $transactions[] = [
716                'duedate' => $this->formatJSONTime($trResponse->DueDate),
717                'id'      => $trResponse->BibID,
718                'barcode' => $trResponse->Barcode,
719                'renew'   => $trResponse->RenewalCount,
720                'renewLimit' => $trResponse->RenewalLimit,
721                'renewable' => $renewable,
722                'title'   => $trResponse->Title,
723                'item_id' => $trResponse->ItemID,
724            ];
725        }
726        return $transactions;
727    }
728
729    /**
730     * Renew My Items
731     *
732     * Function for attempting to renew a patron's items. The data in
733     * $renewDetails['details'] is determined by getRenewDetails().
734     *
735     * @param array $renewDetails An array of data required for renewing items
736     * including the Patron ID and an array of renewal IDS
737     *
738     * @return array              An array of renewal information keyed by item ID
739     */
740    public function renewMyItems($renewDetails)
741    {
742        $renew_ids = $renewDetails['details'];
743        $patron = $renewDetails['patron'];
744        $count = 0;
745        $item_response = [];
746        $item_blocks = [];
747
748        foreach ($renew_ids as $renew_id) {
749            $jsonrequest = [];
750            $jsonrequest['Action'] = 'renew';
751            $jsonrequest['LogonBranchID']      = '1';
752            $jsonrequest['LogonUserID']        = '1';
753            $jsonrequest['LogonWorkstationID'] = '1';
754            $jsonrequest['RenewData']['IgnoreOverrideErrors'] = 'true';
755
756            $response = $this->makeRequest(
757                "patron/{$patron['cat_username']}/itemsout/$renew_id",
758                'PUT',
759                $patron['cat_password'],
760                $jsonrequest
761            );
762            if ($response->PAPIErrorCode == 0) {
763                $count++;
764                $item_response[$renew_id] = [
765                'success'  => true,
766                'new_date' => $this->formatJSONTime(
767                    $response->ItemRenewResult->DueDateRows[0]->DueDate
768                ),
769                'item_id'  =>
770                    $response->ItemRenewResult->DueDateRows[0]->ItemRecordID,
771                ];
772            } elseif ($response->PAPIErrorCode == -2) {
773                $item_blocks[$renew_id]
774                    = $response->ItemRenewResult->BlockRows[0]->ErrorDesc;
775                $item_response[$renew_id] = [
776                'success'  => -1,
777                'new_date' => false,
778                'item_id' => $response->ItemRenewResult->BlockRows[0]->ItemRecordID,
779                'sysMessage' => $response->ItemRenewResult->BlockRows[0]->ErrorDesc,
780                ];
781            }
782        }
783        $result = [
784            'count' => $count, 'details' => $item_response,
785            'blocks' => $item_blocks,
786        ];
787
788        return $result;
789    }
790
791    /**
792     * Get Renew Details
793     *
794     * In order to renew an item, Voyager requires the patron details and an item
795     * id. This function returns the item id as a string which is then used
796     * as submitted form data in checkedOut.php. This value is then extracted by
797     * the RenewMyItems function.
798     *
799     * @param array $checkOutDetails An array of item data
800     *
801     * @return string Data for use in a form field
802     */
803    public function getRenewDetails($checkOutDetails)
804    {
805        $renewDetails = $checkOutDetails['item_id'];
806        return $renewDetails;
807    }
808
809    /**
810     * Cancel Holds
811     *
812     * Attempts to Cancel a hold or recall on a particular item. The
813     * data in $cancelDetails['details'] is determined by getCancelHoldDetails().
814     *
815     * @param array $cancelDetails An array of item and patron data
816     *
817     * @return array An array of data on each request including whether or not it
818     * was successful and a system message (if available)
819     */
820    public function cancelHolds($cancelDetails)
821    {
822        $hold_ids = $cancelDetails['details'];
823        $patron = $cancelDetails['patron'];
824        $count = 0;
825        $item_response = [];
826
827        foreach ($hold_ids as $hold_id) {
828            $response = $this->makeRequest(
829                "patron/{$patron['cat_username']}/holdrequests/$hold_id/cancelled"
830                . '?wsid=1&userid=1',
831                'PUT',
832                $patron['cat_password']
833            );
834
835            if ($response->PAPIErrorCode == 0) {
836                $count++;
837                $item_response[$hold_id] = [
838                'success' => true,
839                'status'  => 'hold_cancel_success',
840                ];
841            } else {
842                $item_response[$hold_id] = [
843                'success' => false,
844                'status'  => 'hold_cancel_fail',
845                'sysMessage' => 'Failure calling ILS to cancel hold',
846                ];
847            }
848        }
849
850        $result = [ 'count' => $count, 'items' => $item_response ];
851        return $result;
852    }
853
854    /**
855     * Get Cancel Hold Details
856     *
857     * @param array $holdDetails A single hold array from getMyHolds
858     * @param array $patron      Patron information from patronLogin
859     *
860     * @return string Data for use in a form field (just request id is all Polaris
861     * needs)
862     *
863     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
864     */
865    public function getCancelHoldDetails($holdDetails, $patron = [])
866    {
867        return $holdDetails['reqnum'];
868    }
869
870    /**
871     * Get Checkout History
872     *
873     * Returns the patrons checkout / reading history
874     *
875     * @param array $patron The patron array from patronLogin
876     *
877     * @return mixed Array of the patron's checkouts on success.
878     */
879    public function getCheckoutHistory($patron)
880    {
881        // get number of pages, only get most recent max 200 items (last 2 pages)
882        // TODO: use real pagination, not just recent items.
883        $items_per_page = 100;
884
885        $response = $this->makeRequest(
886            "patron/{$patron['cat_username']}/readinghistory?rowsperpage=1&page=-1",
887            'GET',
888            $patron['cat_password']
889        );
890
891        // error code returns number of results
892        $count = $response->PAPIErrorCode;
893
894        if ($count == 0) {
895            return;
896        }
897
898        $pages = ceil($count / $items_per_page);
899
900        $penultimate_page = $pages - 1;
901
902        if ($penultimate_page > 0) {
903            $page_offset = $penultimate_page;
904        } else {
905            $page_offset = $pages;
906        }
907
908        $checkouts = [];
909        while ($page_offset <= $pages) {
910            $response = $this->makeRequest(
911                "patron/{$patron['cat_username']}/readinghistory?rowsperpage="
912                . "$items_per_page&page=$page_offset",
913                'GET',
914                $patron['cat_password']
915            );
916
917            $checkout_history_array = $response->PatronReadingHistoryGetRows;
918            foreach ($checkout_history_array as $checkout_response) {
919                $date = $this->formatJSONTime($checkout_response->CheckOutDate);
920                $checkouts[] = [
921                   'id' => $checkout_response->BibID,
922                   'title' => $checkout_response->Title,
923                   'format' => $checkout_response->FormatDescription,
924                   'location' => $checkout_response->LoaningBranchName,
925                   'date' => $date,
926                   'author' => $checkout_response->Author,
927                   ];
928            }
929            $page_offset++;
930        }
931        // show most recent checkouts first
932        $checkouts = array_reverse($checkouts);
933
934        return $checkouts;
935    }
936
937    /**
938     * Get Hold Count
939     *
940     * Returns the count of a hold based on API call to bibid
941     *
942     * @param array $id bib id
943     *
944     * @return string count of holds
945     */
946    public function getHoldCount($id)
947    {
948        $response = $this->makeRequest("bib/$id");
949        $holdings_response_array = $response->BibGetRows;
950        $hold_count = 0;
951        foreach ($holdings_response_array as $response) {
952            if ($response->ElementID == '8') {
953                // that's the current holds field, could also be pulled by label
954                // instead?
955                if ($response->Value > 0) {
956                    $hold_count = $response->Value;
957                }
958                break;
959            }
960        }
961        return $hold_count;
962    }
963
964    /**
965     * Suspend Holds
966     *
967     * Attempts to Suspend a hold or recall on a particular item. The
968     * data in $suspendDetails['details'] is determined by getSuspendHoldDetails().
969     *
970     * @param array $suspendDetails An array of item and patron data
971     *
972     * @return array An array of data on each request including whether or not it
973     * was successful and a system message (if available)
974     */
975    public function suspendHolds($suspendDetails)
976    {
977        $hold_ids = $suspendDetails['details'];
978        $patron = $suspendDetails['patron'];
979
980        $jsondate = $this->encodeJSONTime($suspendDetails['date']);
981
982        $count = 0;
983        $item_response = [];
984
985        foreach ($hold_ids as $hold_id) {
986            $jsonrequest = [
987                 'UserID' => '1',
988                 'ActivationDate' => "$jsondate",
989                ];
990
991            $response = $this->makeRequest(
992                "patron/{$patron['cat_username']}/holdrequests/$hold_id/inactive",
993                'PUT',
994                $patron['cat_password'],
995                $jsonrequest
996            );
997
998            if ($response->PAPIErrorCode == 0) {
999                $count++;
1000                $item_response[$hold_id] = [
1001                  'success' => true,
1002                  'status'  => 'hold_suspend_success',
1003                ];
1004            } else {
1005                $item_response[$hold_id] = [
1006                'success' => false,
1007                'status'  => 'hold_suspend_fail',
1008                'sysMessage' => 'Failure calling ILS to suspend hold',
1009                ];
1010            }
1011        }
1012
1013        $result = [ 'count' => $count, 'items' => $item_response ];
1014        return $result;
1015    }
1016
1017    /**
1018     * Get Suspend Hold Details
1019     *
1020     * @param array $holdDetails An array of item data
1021     *
1022     * @return string Data for use in a form field (just request id is all Polaris
1023     * needs)
1024     */
1025    public function getSuspendHoldDetails($holdDetails)
1026    {
1027        return $holdDetails['reqnum'];
1028    }
1029
1030    /**
1031     * Reactivate Holds
1032     *
1033     * Attempts to Reactivate a hold or recall on a particular item. The
1034     * data in $reactivateDetails['details'] is determined by
1035     * getReactivateHoldDetails().
1036     *
1037     * @param array $reactivateDetails An array of item and patron data
1038     *
1039     * @return array An array of data on each request including whether or not it
1040     * was successful and a system message (if available)
1041     */
1042    public function reactivateHolds($reactivateDetails)
1043    {
1044        $hold_ids = $reactivateDetails['details'];
1045        $patron = $reactivateDetails['patron'];
1046
1047        $date = date('d/M/Y');
1048        $jsondate = $this->encodeJSONTime($date);
1049
1050        $count = 0;
1051        $item_response = [];
1052
1053        foreach ($hold_ids as $hold_id) {
1054            $jsonrequest = [
1055                 'UserID' => '1',
1056                 'ActivationDate' => "$jsondate",
1057                 ];
1058
1059            $response = $this->makeRequest(
1060                "patron/{$patron['cat_username']}/holdrequests/$hold_id/active",
1061                'PUT',
1062                $patron['cat_password'],
1063                $jsonrequest
1064            );
1065
1066            if ($response->PAPIErrorCode == 0) {
1067                $count++;
1068                $item_response[$hold_id] = [
1069                  'success' => true,
1070                  'status'  => 'hold_reactivate_success',
1071                ];
1072            } else {
1073                $item_response[$hold_id] = [
1074                'success' => false,
1075                'status'  => 'hold_reactivate_fail',
1076                'sysMessage' => 'Failure calling ILS to reactivate hold',
1077                ];
1078            }
1079        }
1080
1081        $result = [ 'count' => $count, 'items' => $item_response ];
1082        return $result;
1083    }
1084}