Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.64% covered (danger)
3.64%
42 / 1154
1.28% covered (danger)
1.28%
1 / 78
CRAP
0.00% covered (danger)
0.00%
0 / 1
Demo
3.64% covered (danger)
3.64%
42 / 1154
1.28% covered (danger)
1.28%
1 / 78
80826.62
0.00% covered (danger)
0.00%
0 / 1
 __construct
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 init
56.25% covered (warning)
56.25%
9 / 16
0.00% covered (danger)
0.00%
0 / 1
11.10
 isFailing
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getFakeLoc
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getFakeServices
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 getFakeStatus
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 getFakeCallNum
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getFakeCallNumPrefix
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getRandomBibId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getRandomBibIdAndTitle
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getRecordSource
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkIntermittentFailure
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 checkRenewBlock
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRequestBlocks
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getAccountBlocks
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getRandomHolding
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
210
 getRandomItemIdentifier
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 createRequestList
0.00% covered (danger)
0.00%
0 / 65
0.00% covered (danger)
0.00%
0 / 1
306
 getStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSuppressedRecords
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSession
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getSimulatedStatus
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 setStatus
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 getStatuses
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getHolding
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
156
 getPurchaseHistory
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 patronLogin
71.43% covered (warning)
71.43%
15 / 21
0.00% covered (danger)
0.00%
0 / 1
5.58
 getMyProfile
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
2
 getMyFines
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
90
 getMyHolds
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getMyStorageRetrievalRequests
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getMyILLRequests
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getTransactionList
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 calculateDueStatus
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getRandomTransactionList
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
72
 getMyTransactions
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
 getHistoricTransactionList
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getRandomHistoricTransactionList
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
20
 getMyTransactionHistory
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
132
 purgeTransactionHistory
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 getPickUpLocations
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 getHoldDefaultRequiredDate
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getDefaultPickUpLocation
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultRequestGroup
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getRequestGroups
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getFunds
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDepartments
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getInstructors
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getCourses
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getRandomBibIds
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getNewItems
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getCourseId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getDepartmentId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getInstructorId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 findReserves
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 cancelHolds
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
 updateHolds
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
90
 cancelStorageRetrievalRequests
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
 getCancelStorageRetrievalRequestDetails
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 renewMyItems
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
30
 getRenewDetails
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkRequestIsValid
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 placeHold
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 1
110
 checkStorageRetrievalRequestIsValid
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 placeStorageRetrievalRequest
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 1
156
 checkILLRequestIsValid
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 placeILLRequest
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
182
 getILLPickupLibraries
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getILLPickupLocations
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
12
 cancelILLRequests
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
 getCancelILLRequestDetails
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 changePassword
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getConfig
11.27% covered (danger)
11.27%
8 / 71
0.00% covered (danger)
0.00%
0 / 1
150.93
 getRecentlyReturnedBibs
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getTrendingBibs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getProxiedUsers
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getProxyingUsers
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUrlsForRecord
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3/**
4 * Advanced Dummy ILS Driver -- Returns sample values based on Solr index.
5 *
6 * Note that some sample values (holds, transactions, fines) are stored in
7 * the session. You can log out and log back in to get a different set of
8 * values.
9 *
10 * PHP version 8
11 *
12 * Copyright (C) Villanova University 2007, 2022.
13 * Copyright (C) The National Library of Finland 2014.
14 *
15 * This program is free software; you can redistribute it and/or modify
16 * it under the terms of the GNU General Public License version 2,
17 * as published by the Free Software Foundation.
18 *
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with this program; if not, write to the Free Software
26 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
27 *
28 * @category VuFind
29 * @package  ILS_Drivers
30 * @author   Greg Pendlebury <vufind-tech@lists.sourceforge.net>
31 * @author   Ere Maijala <ere.maijala@helsinki.fi>
32 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
33 * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
34 */
35
36namespace VuFind\ILS\Driver;
37
38use ArrayObject;
39use Laminas\Http\Request as HttpRequest;
40use Laminas\Session\Container as SessionContainer;
41use VuFind\Date\DateException;
42use VuFind\Exception\ILS as ILSException;
43use VuFind\ILS\Logic\AvailabilityStatus;
44use VuFind\ILS\Logic\AvailabilityStatusInterface;
45use VuFindSearch\Command\RandomCommand;
46use VuFindSearch\Query\Query;
47use VuFindSearch\Service as SearchService;
48
49use function array_key_exists;
50use function array_slice;
51use function count;
52use function in_array;
53use function is_callable;
54use function strlen;
55
56/**
57 * Advanced Dummy ILS Driver -- Returns sample values based on Solr index.
58 *
59 * @category VuFind
60 * @package  ILS_Drivers
61 * @author   Greg Pendlebury <vufind-tech@lists.sourceforge.net>
62 * @author   Ere Maijala <ere.maijala@helsinki.fi>
63 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
64 * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
65 */
66class Demo extends AbstractBase implements \VuFind\I18n\HasSorterInterface
67{
68    use \VuFind\I18n\HasSorterTrait;
69
70    /**
71     * Catalog ID used to distinquish between multiple Demo driver instances with the
72     * MultiBackend driver
73     *
74     * @var string
75     */
76    protected $catalogId = 'demo';
77
78    /**
79     * Connection used when getting random bib ids from Solr
80     *
81     * @var SearchService
82     */
83    protected $searchService;
84
85    /**
86     * Total count of records in the Solr index (used for random bib lookup)
87     *
88     * @var int
89     */
90    protected $totalRecords;
91
92    /**
93     * Container for storing persistent simulated ILS data.
94     *
95     * @var SessionContainer[]
96     */
97    protected $session = [];
98
99    /**
100     * Factory function for constructing the SessionContainer.
101     *
102     * @var callable
103     */
104    protected $sessionFactory;
105
106    /**
107     * HTTP Request object (if available).
108     *
109     * @var ?HttpRequest
110     */
111    protected $request;
112
113    /**
114     * Should we return bib IDs in MyResearch responses?
115     *
116     * @var bool
117     */
118    protected $idsInMyResearch = true;
119
120    /**
121     * Should we support Storage Retrieval Requests?
122     *
123     * @var bool
124     */
125    protected $storageRetrievalRequests = true;
126
127    /**
128     * Should we support ILLRequests?
129     *
130     * @var bool
131     */
132    protected $ILLRequests = true;
133
134    /**
135     * Date converter object
136     *
137     * @var \VuFind\Date\Converter
138     */
139    protected $dateConverter;
140
141    /**
142     * Failure probability settings
143     *
144     * @var array
145     */
146    protected $failureProbabilities = [];
147
148    /**
149     * Courses for use in course reserves.
150     *
151     * @var array
152     */
153    protected $courses = ['Course A', 'Course B', 'Course C'];
154
155    /**
156     * Departments for use in course reserves.
157     *
158     * @var array
159     */
160    protected $departments = ['Dept. A', 'Dept. B', 'Dept. C'];
161
162    /**
163     * Instructors for use in course reserves.
164     *
165     * @var array
166     */
167    protected $instructors = ['Instructor A', 'Instructor B', 'Instructor C'];
168
169    /**
170     * Item and pick up locations
171     *
172     * @var array
173     */
174    protected $locations = [
175        [
176            'locationID' => 'A',
177            'locationDisplay' => 'Campus A',
178        ],
179        [
180            'locationID' => 'B',
181            'locationDisplay' => 'Campus B',
182        ],
183        [
184            'locationID' => 'C',
185            'locationDisplay' => 'Campus C',
186        ],
187    ];
188
189    /**
190     * Default pickup location
191     *
192     * @var string
193     */
194    protected $defaultPickUpLocation;
195
196    /**
197     * Constructor
198     *
199     * @param \VuFind\Date\Converter $dateConverter  Date converter object
200     * @param SearchService          $ss             Search service
201     * @param callable               $sessionFactory Factory function returning
202     * SessionContainer object for fake data to simulate consistency and reduce Solr
203     * hits
204     * @param HttpRequest            $request        HTTP request object (optional)
205     */
206    public function __construct(
207        \VuFind\Date\Converter $dateConverter,
208        SearchService $ss,
209        $sessionFactory,
210        HttpRequest $request = null
211    ) {
212        $this->dateConverter = $dateConverter;
213        $this->searchService = $ss;
214        if (!is_callable($sessionFactory)) {
215            throw new \Exception('Invalid session factory passed to constructor.');
216        }
217        $this->sessionFactory = $sessionFactory;
218        $this->request = $request;
219    }
220
221    /**
222     * Initialize the driver.
223     *
224     * Validate configuration and perform all resource-intensive tasks needed to
225     * make the driver active.
226     *
227     * @throws ILSException
228     * @return void
229     */
230    public function init()
231    {
232        if (isset($this->config['Catalog']['id'])) {
233            $this->catalogId = $this->config['Catalog']['id'];
234        }
235        if (isset($this->config['Catalog']['idsInMyResearch'])) {
236            $this->idsInMyResearch = $this->config['Catalog']['idsInMyResearch'];
237        }
238        if (isset($this->config['Catalog']['storageRetrievalRequests'])) {
239            $this->storageRetrievalRequests
240                = $this->config['Catalog']['storageRetrievalRequests'];
241        }
242        if (isset($this->config['Catalog']['ILLRequests'])) {
243            $this->ILLRequests = $this->config['Catalog']['ILLRequests'];
244        }
245        if (isset($this->config['Failure_Probabilities'])) {
246            $this->failureProbabilities = $this->config['Failure_Probabilities'];
247        }
248        $this->defaultPickUpLocation
249            = $this->config['Holds']['defaultPickUpLocation'] ?? '';
250        if ($this->defaultPickUpLocation === 'user-selected') {
251            $this->defaultPickUpLocation = false;
252        }
253        $this->checkIntermittentFailure();
254    }
255
256    /**
257     * Check for a simulated failure. Returns true for failure, false for
258     * success.
259     *
260     * @param string $method  Name of method that might fail
261     * @param int    $default Default probability (if config is empty)
262     *
263     * @return bool
264     */
265    protected function isFailing($method, $default = 0)
266    {
267        // Method may come in like Class::Method, we just want the Method part
268        $parts = explode('::', $method);
269        $key = array_pop($parts);
270        $probability = $this->failureProbabilities[$key] ?? $default;
271        return rand(1, 100) <= $probability;
272    }
273
274    /**
275     * Generate a fake location name.
276     *
277     * @param bool $returnText If true, return location text; if false, return ID
278     *
279     * @return string
280     */
281    protected function getFakeLoc($returnText = true)
282    {
283        $locations = $this->locations;
284        $loc = rand() % count($locations);
285        return $returnText
286            ? $locations[$loc]['locationDisplay']
287            : $locations[$loc]['locationID'];
288    }
289
290    /**
291     * Generate fake services.
292     *
293     * @return array
294     */
295    protected function getFakeServices()
296    {
297        // Load service configuration; return empty array if no services defined.
298        $services = isset($this->config['Records']['services'])
299            ? (array)$this->config['Records']['services']
300            : [];
301        if (empty($services)) {
302            return [];
303        }
304
305        // Make it more likely we have a single service than many:
306        $count = rand(1, 5) == 1 ? rand(1, count($services)) : 1;
307        $keys = (array)array_rand($services, $count);
308        $fakeServices = [];
309
310        foreach ($keys as $key) {
311            if ($key !== null) {
312                $fakeServices[] = $services[$key];
313            }
314        }
315
316        return $fakeServices;
317    }
318
319    /**
320     * Generate a fake status message.
321     *
322     * @return string
323     */
324    protected function getFakeStatus()
325    {
326        $loc = rand() % 10;
327        switch ($loc) {
328            case 10:
329                return 'Missing';
330            case 9:
331                return 'On Order';
332            case 8:
333                return 'Invoiced';
334            case 7:
335                return 'Uncertain';
336            default:
337                return 'Available';
338        }
339    }
340
341    /**
342     * Generate a fake call number.
343     *
344     * @return string
345     */
346    protected function getFakeCallNum()
347    {
348        $codes = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
349        $a = $codes[rand() % strlen($codes)];
350        $b = rand() % 899 + 100;
351        $c = rand() % 9999;
352        return $a . $b . '.' . $c;
353    }
354
355    /**
356     * Generate a fake call number prefix sometimes.
357     *
358     * @return string
359     */
360    protected function getFakeCallNumPrefix()
361    {
362        $codes = '0123456789';
363        $prefix = substr(str_shuffle($codes), 1, rand(0, 1));
364        if (!empty($prefix)) {
365            return 'Prefix: ' . $prefix;
366        }
367        return '';
368    }
369
370    /**
371     * Get a random ID from the Solr index.
372     *
373     * @return string
374     */
375    protected function getRandomBibId()
376    {
377        [$id] = $this->getRandomBibIdAndTitle();
378        return $id;
379    }
380
381    /**
382     * Get a random ID and title from the Solr index.
383     *
384     * @return array [id, title]
385     */
386    protected function getRandomBibIdAndTitle()
387    {
388        $source = $this->getRecordSource();
389        $query = $this->config['Records']['query'] ?? '*:*';
390        $command = new RandomCommand($source, new Query($query), 1);
391        $result = $this->searchService->invoke($command)->getResult();
392        if (count($result) === 0) {
393            throw new \Exception("Problem retrieving random record from $source.");
394        }
395        $record = current($result->getRecords());
396        return [$record->getUniqueId(), $record->getTitle()];
397    }
398
399    /**
400     * Get the name of the search backend providing records.
401     *
402     * @return string
403     */
404    protected function getRecordSource()
405    {
406        return $this->config['Records']['source'] ?? DEFAULT_SEARCH_BACKEND;
407    }
408
409    /**
410     * Should we simulate a system failure?
411     *
412     * @return void
413     * @throws ILSException
414     */
415    protected function checkIntermittentFailure()
416    {
417        if ($this->isFailing(__METHOD__, 0)) {
418            throw new ILSException('Simulating low-level system failure');
419        }
420    }
421
422    /**
423     * Are renewals blocked?
424     *
425     * @return bool
426     */
427    protected function checkRenewBlock()
428    {
429        return $this->isFailing(__METHOD__, 25);
430    }
431
432    /**
433     * Check whether the patron is blocked from placing requests (holds/ILL/SRR).
434     *
435     * @param array $patron Patron data from patronLogin().
436     *
437     * @return mixed A boolean false if no blocks are in place and an array
438     * of block reasons if blocks are in place
439     *
440     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
441     */
442    public function getRequestBlocks($patron)
443    {
444        return $this->isFailing(__METHOD__, 10)
445            ? ['simulated request block'] : false;
446    }
447
448    /**
449     * Check whether the patron has any blocks on their account.
450     *
451     * @param array $patron Patron data from patronLogin().
452     *
453     * @return mixed A boolean false if no blocks are in place and an array
454     * of block reasons if blocks are in place
455     *
456     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
457     */
458    public function getAccountBlocks($patron)
459    {
460        return $this->isFailing(__METHOD__, 10)
461            ? ['simulated account block'] : false;
462    }
463
464    /**
465     * Generates a random, fake holding array
466     *
467     * @param string $id     set id
468     * @param string $number set number for multiple items
469     * @param array  $patron Patron data
470     *
471     * @return array
472     */
473    protected function getRandomHolding($id, $number, array $patron = null)
474    {
475        $status = $this->getFakeStatus();
476        $location = $this->getFakeLoc();
477        $locationhref = ($location === 'Campus A') ? 'http://campus-a' : false;
478        switch ($status) {
479            case 'Uncertain':
480                $availability = AvailabilityStatusInterface::STATUS_UNCERTAIN;
481                break;
482            case 'Available':
483                if (rand(1, 2) === 1) {
484                    // Legacy boolean value
485                    $availability = true;
486                } else {
487                    $availability = AvailabilityStatusInterface::STATUS_AVAILABLE;
488                    $status = 'Item in Library';
489                }
490                break;
491            default:
492                if (rand(1, 2) === 1) {
493                    // Legacy boolean value
494                    $availability = false;
495                } else {
496                    $availability = AvailabilityStatusInterface::STATUS_UNAVAILABLE;
497                }
498                break;
499        }
500        $result = [
501            'id'           => $id,
502            'source'       => $this->getRecordSource(),
503            'item_id'      => $number,
504            'number'       => $number,
505            'barcode'      => sprintf('%08d', rand() % 50000),
506            'availability' => $availability,
507            'status'       => $status,
508            'location'     => $location,
509            'locationhref' => $locationhref,
510            'reserve'      => rand(1, 4) === 1 ? 'Y' : 'N',
511            'callnumber'   => $this->getFakeCallNum(),
512            'callnumber_prefix' => $this->getFakeCallNumPrefix(),
513            'duedate'      => '',
514            'is_holdable'  => true,
515            'addLink'      => $patron ? true : false,
516            'level'        => 'copy',
517            'storageRetrievalRequest' => 'auto',
518            'addStorageRetrievalRequestLink' => $patron ? 'check' : false,
519            'ILLRequest'   => 'auto',
520            'addILLRequestLink' => $patron ? 'check' : false,
521            'services'     => $status == 'Available' ? $this->getFakeServices() : [],
522        ];
523
524        switch (rand(1, 5)) {
525            case 1:
526                $result['location'] = 'Digital copy available';
527                $result['locationhref'] = 'http://digital';
528                $result['__electronic__'] = true;
529                $result['availability'] = true;
530                $result['status'] = '';
531                break;
532            case 2:
533                $result['location'] = 'Electronic Journals';
534                $result['locationhref'] = 'http://electronic';
535                $result['__electronic__'] = true;
536                $result['availability'] = true;
537                $result['status'] = 'Available from ' . rand(2010, 2019);
538        }
539
540        return $result;
541    }
542
543    /**
544     * Generate an associative array containing some sort of ID (for cover
545     * generation).
546     *
547     * @return array
548     */
549    protected function getRandomItemIdentifier()
550    {
551        switch (rand(1, 4)) {
552            case 1:
553                return ['isbn' => '1558612742'];
554            case 2:
555                return ['oclc' => '55114477'];
556            case 3:
557                return ['issn' => '1133-0686'];
558        }
559        return ['upc' => '733961100525'];
560    }
561
562    /**
563     * Generate a list of holds, storage retrieval requests or ILL requests.
564     *
565     * @param string $requestType Request type (Holds, StorageRetrievalRequests or
566     * ILLRequests)
567     *
568     * @return ArrayObject List of requests
569     */
570    protected function createRequestList($requestType)
571    {
572        // How many items are there?  %10 - 1 = 10% chance of none,
573        // 90% of 1-9 (give or take some odd maths)
574        $items = rand() % 10 - 1;
575
576        $requestGroups = $this->getRequestGroups(null, null);
577
578        $list = new ArrayObject();
579        for ($i = 0; $i < $items; $i++) {
580            $location = $this->getFakeLoc(false);
581            $randDays = rand() % 10;
582            $currentItem = [
583                'location' => $location,
584                'create'   => $this->dateConverter->convertToDisplayDate(
585                    'U',
586                    strtotime("now - {$randDays} days")
587                ),
588                'expire'   => $this->dateConverter->convertToDisplayDate(
589                    'U',
590                    strtotime('now + 30 days')
591                ),
592                'item_id' => $i,
593                'reqnum' => $i,
594            ];
595            // Inject a random identifier of some sort:
596            $currentItem += $this->getRandomItemIdentifier();
597            if ($i == 2 || rand() % 5 == 1) {
598                // Mimic an ILL request
599                $currentItem['id'] = "ill_request_$i";
600                $currentItem['title'] = "ILL Hold Title $i";
601                $currentItem['institution_id'] = 'ill_institution';
602                $currentItem['institution_name'] = 'ILL Library';
603                $currentItem['institution_dbkey'] = 'ill_institution';
604            } else {
605                if ($this->idsInMyResearch) {
606                    [$currentItem['id'], $currentItem['title']]
607                        = $this->getRandomBibIdAndtitle();
608                    $currentItem['source'] = $this->getRecordSource();
609                } else {
610                    $currentItem['title'] = 'Demo Title ' . $i;
611                }
612            }
613
614            if ($requestType == 'Holds') {
615                $pos = rand() % 5;
616                if ($pos > 1) {
617                    $currentItem['position'] = $pos;
618                    $currentItem['available'] = false;
619                    $currentItem['in_transit'] = (rand() % 2) === 1;
620                } else {
621                    $currentItem['available'] = true;
622                    $currentItem['in_transit'] = false;
623                    if (rand() % 3 != 1) {
624                        $lastDate = strtotime('now + 3 days');
625                        $currentItem['last_pickup_date'] = $this->dateConverter
626                            ->convertToDisplayDate('U', $lastDate);
627                    }
628                }
629                $pos = rand(0, count($requestGroups) - 1);
630                $currentItem['requestGroup'] = $requestGroups[$pos]['name'];
631                $currentItem['cancel_details'] = $currentItem['updateDetails']
632                    = (!$currentItem['available'] && !$currentItem['in_transit'])
633                    ? $currentItem['reqnum'] : '';
634                if (rand(0, 3) === 1) {
635                    $currentItem['proxiedBy'] = 'Fictional Proxy User';
636                }
637            } else {
638                $status = rand() % 5;
639                $currentItem['available'] = $status == 1;
640                $currentItem['canceled'] = $status == 2;
641                $currentItem['processed'] = ($status == 1 || rand(1, 3) == 3)
642                    ? $this->dateConverter->convertToDisplayDate('U', time())
643                    : '';
644                if ($requestType == 'ILLRequests') {
645                    $transit = rand() % 2;
646                    if (
647                        !$currentItem['available']
648                        && !$currentItem['canceled']
649                        && $transit == 1
650                    ) {
651                        $currentItem['in_transit'] = $location;
652                    } else {
653                        $currentItem['in_transit'] = false;
654                    }
655                }
656            }
657
658            $list->append($currentItem);
659        }
660        return $list;
661    }
662
663    /**
664     * Get Status
665     *
666     * This is responsible for retrieving the status information of a certain
667     * record.
668     *
669     * @param string $id The record id to retrieve the holdings for
670     *
671     * @return mixed     On success, an associative array with the following keys:
672     * id, availability (boolean), status, location, reserve, callnumber.
673     */
674    public function getStatus($id)
675    {
676        return $this->getSimulatedStatus($id);
677    }
678
679    /**
680     * Get suppressed records.
681     *
682     * @return array ID numbers of suppressed records in the system.
683     */
684    public function getSuppressedRecords()
685    {
686        return $this->config['Records']['suppressed'] ?? [];
687    }
688
689    /**
690     * Get the session container (constructing it on demand if not already present)
691     *
692     * @param string $patron ID of current patron
693     *
694     * @return SessionContainer
695     */
696    protected function getSession($patron = null)
697    {
698        $sessionKey = md5($this->catalogId . '/' . ($patron ?? 'default'));
699
700        // SessionContainer not defined yet? Build it now:
701        if (!isset($this->session[$sessionKey])) {
702            $this->session[$sessionKey] = ($this->sessionFactory)($sessionKey);
703        }
704        $result = $this->session[$sessionKey];
705        // Special case: check for clear_demo request parameter to reset:
706        if ($this->request && $this->request->getQuery('clear_demo')) {
707            $result->exchangeArray([]);
708        }
709
710        return $result;
711    }
712
713    /**
714     * Get Simulated Status (support method for getStatus/getHolding)
715     *
716     * This is responsible for retrieving the status information of a certain
717     * record.
718     *
719     * @param string $id     The record id to retrieve the holdings for
720     * @param array  $patron Patron data
721     *
722     * @return mixed     On success, an associative array with the following keys:
723     * id, availability (boolean), status, location, reserve, callnumber.
724     *
725     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
726     */
727    protected function getSimulatedStatus($id, array $patron = null)
728    {
729        $id = (string)$id;
730
731        if ($json = $this->config['StaticHoldings'][$id] ?? null) {
732            foreach (json_decode($json, true) as $i => $status) {
733                if ($status['use_status_class'] ?? false) {
734                    $availability = $status['availability'] ?? false;
735                    if ($status['use_unknown_message'] ?? false) {
736                        $availability = AvailabilityStatusInterface::STATUS_UNKNOWN;
737                    }
738                    $status['availability'] = new AvailabilityStatus(
739                        $availability,
740                        $status['status'] ?? '',
741                        $status['extraStatusInformation'] ?? []
742                    );
743                    unset($status['status']);
744                    unset($status['use_unknown_message']);
745                }
746                $this->setStatus($id, $status, $i > 0, $patron);
747            }
748        }
749
750        // Do we have a fake status persisted in the session?
751        $session = $this->getSession($patron['id'] ?? null);
752        if (isset($session->statuses[$id])) {
753            return $session->statuses[$id];
754        }
755
756        // Create fake entries for a random number of items
757        $holding = [];
758        $records = rand() % 15;
759        for ($i = 1; $i <= $records; $i++) {
760            $holding[] = $this->setStatus($id, [], true, $patron);
761        }
762        return $holding;
763    }
764
765    /**
766     * Set Status
767     *
768     * @param string $id      id for record
769     * @param array  $holding associative array with options to specify
770     *      number, barcode, availability, status, location,
771     *      reserve, callnumber, duedate, is_holdable, and addLink
772     * @param bool   $append  add another record or replace current record
773     * @param array  $patron  Patron data
774     *
775     * @return array
776     */
777    protected function setStatus(string $id, $holding = [], $append = true, $patron = null)
778    {
779        $session = $this->getSession($patron['id'] ?? null);
780        $i = isset($session->statuses[$id])
781            ? count($session->statuses[$id]) + 1 : 1;
782        $holding = array_merge($this->getRandomHolding($id, $i, $patron), $holding);
783
784        // if statuses is already stored
785        if ($session->statuses) {
786            // and this id is part of it
787            if ($append && isset($session->statuses[$id])) {
788                // add to the array
789                $session->statuses[$id][] = $holding;
790            } else {
791                // if we're over-writing or if there's nothing stored for this id
792                $session->statuses[$id] = [$holding];
793            }
794        } else {
795            // brand new status storage!
796            $session->statuses = [$id => [$holding]];
797        }
798        return $holding;
799    }
800
801    /**
802     * Get Statuses
803     *
804     * This is responsible for retrieving the status information for a
805     * collection of records.
806     *
807     * @param array $ids The array of record ids to retrieve the status for
808     *
809     * @return array An array of getStatus() return values on success.
810     */
811    public function getStatuses($ids)
812    {
813        $this->checkIntermittentFailure();
814
815        if ($this->isFailing(__METHOD__, 0)) {
816            return array_map(
817                function ($id) {
818                    return [
819                        [
820                            'id' => $id,
821                            'error' => 'Simulated failure',
822                        ],
823                    ];
824                },
825                $ids
826            );
827        }
828
829        return array_map([$this, 'getStatus'], $ids);
830    }
831
832    /**
833     * Get Holding
834     *
835     * This is responsible for retrieving the holding information of a certain
836     * record.
837     *
838     * @param string $id      The record id to retrieve the holdings for
839     * @param array  $patron  Patron data
840     * @param array  $options Extra options
841     *
842     * @return array On success, an associative array with the following keys:
843     * id, availability (boolean), status, location, reserve, callnumber,
844     * duedate, number, barcode.
845     */
846    public function getHolding($id, array $patron = null, array $options = [])
847    {
848        $this->checkIntermittentFailure();
849
850        if ($this->isFailing(__METHOD__, 0)) {
851            return [
852                'id' => $id,
853                'error' => 'Simulated failure',
854            ];
855        }
856
857        // Get basic status info:
858        $status = $this->getSimulatedStatus($id, $patron);
859
860        $issue = 1;
861        // Add notes and summary:
862        foreach (array_keys($status) as $i) {
863            $itemNum = $i + 1;
864            $noteCount = rand(1, 3);
865            $status[$i]['holdings_notes'] = [];
866            $status[$i]['item_notes'] = [];
867            for ($j = 1; $j <= $noteCount; $j++) {
868                $status[$i]['holdings_notes'][] = "Item $itemNum holdings note $j"
869                    . ($j === 1 ? ' https://vufind.org/?f=1&b=2#sample_link' : '');
870                $status[$i]['item_notes'][] = "Item $itemNum note $j";
871            }
872            $summCount = rand(1, 3);
873            $status[$i]['summary'] = [];
874            for ($j = 1; $j <= $summCount; $j++) {
875                $status[$i]['summary'][] = "Item $itemNum summary $j";
876            }
877            $volume = intdiv($issue, 4) + 1;
878            $seriesIssue = $issue % 4;
879            $issue = $issue + 1;
880            $status[$i]['enumchron'] = "volume $volume, issue $seriesIssue";
881            if (rand(1, 100) <= ($this->config['Holdings']['boundWithProbability'] ?? 25)) {
882                $status[$i]['bound_with_records'] = [];
883                $boundWithCount = 3;
884                for ($j = 0; $j < $boundWithCount; $j++) {
885                    $randomRecord = array_combine(['bibId', 'title'], $this->getRandomBibIdAndTitle());
886                    $status[$i]['bound_with_records'][] = $randomRecord;
887                }
888                $boundWithIndex = rand(0, $boundWithCount + 1);
889                array_splice($status[$i]['bound_with_records'], $boundWithIndex, 0, [
890                    [
891                        'title' => 'The Title on This Page',
892                        'bibId' => $id,
893                    ],
894                ]);
895            }
896        }
897
898        // Filter out electronic holdings from the normal holdings list:
899        $status = array_filter(
900            $status,
901            function ($a) {
902                return !($a['__electronic__'] ?? false);
903            }
904        );
905
906        // Slice out a chunk if pagination is enabled.
907        $slice = null;
908        if ($options['itemLimit'] ?? null) {
909            // For sensible pagination, we need to sort by location:
910            $callback = function ($a, $b) {
911                return $this->getSorter()->compare($a['location'], $b['location']);
912            };
913            usort($status, $callback);
914            $slice = array_slice(
915                $status,
916                $options['offset'] ?? 0,
917                $options['itemLimit']
918            );
919        }
920
921        // Electronic holdings:
922        $statuses = $this->getStatus($id);
923        $electronic = [];
924        foreach ($statuses as $item) {
925            if ($item['__electronic__'] ?? false) {
926                // Don't expose internal __electronic__ flag upstream:
927                unset($item['__electronic__']);
928                $electronic[] = $item;
929            }
930        }
931
932        // Send back final value:
933        return [
934            'total' => count($status),
935            'holdings' => $slice ?: $status,
936            'electronic_holdings' => $electronic,
937        ];
938    }
939
940    /**
941     * Get Purchase History
942     *
943     * This is responsible for retrieving the acquisitions history data for the
944     * specific record (usually recently received issues of a serial).
945     *
946     * @param string $id The record id to retrieve the info for
947     *
948     * @return array     An array with the acquisitions data on success.
949     */
950    public function getPurchaseHistory($id)
951    {
952        $this->checkIntermittentFailure();
953        $issues = rand(0, 3);
954        $retval = [];
955        for ($i = 0; $i < $issues; $i++) {
956            $retval[] = ['issue' => 'issue ' . ($i + 1)];
957        }
958        return $retval;
959    }
960
961    /**
962     * Patron Login
963     *
964     * This is responsible for authenticating a patron against the catalog.
965     *
966     * @param string $username The patron username
967     * @param string $password The patron password
968     *
969     * @throws ILSException
970     * @return mixed           Associative array of patron info on successful login,
971     * null on unsuccessful login.
972     */
973    public function patronLogin($username, $password)
974    {
975        $this->checkIntermittentFailure();
976
977        $user = [
978            'id'           => trim($username),
979            'firstname'    => 'Lib',
980            'lastname'     => 'Rarian',
981            'cat_username' => trim($username),
982            'cat_password' => trim($password),
983            'email'        => 'Lib.Rarian@library.not',
984            'major'        => null,
985            'college'      => null,
986        ];
987
988        $loginMethod = $this->config['Catalog']['loginMethod'] ?? 'password';
989        if ('email' === $loginMethod) {
990            $user['email'] = $username;
991            $user['cat_password'] = '';
992            return $user;
993        }
994
995        if (isset($this->config['Users'])) {
996            if (
997                !isset($this->config['Users'][$username])
998                || $password !== $this->config['Users'][$username]
999            ) {
1000                return null;
1001            }
1002        }
1003
1004        return $user;
1005    }
1006
1007    /**
1008     * Get Patron Profile
1009     *
1010     * This is responsible for retrieving the profile for a specific patron.
1011     *
1012     * @param array $patron The patron array
1013     *
1014     * @return array        Array of the patron's profile data on success.
1015     */
1016    public function getMyProfile($patron)
1017    {
1018        $this->checkIntermittentFailure();
1019        $age = rand(13, 113);
1020        $birthDate = new \DateTime();
1021        $birthDate->sub(new \DateInterval("P{$age}Y"));
1022        $patron = [
1023            'firstname'       => 'Lib-' . $patron['cat_username'],
1024            'lastname'        => 'Rarian',
1025            'address1'        => 'Somewhere...',
1026            'address2'        => 'Over the Rainbow',
1027            'zip'             => '12345',
1028            'city'            => 'City',
1029            'country'         => 'Country',
1030            'phone'           => '1900 CALL ME',
1031            'mobile_phone'    => '1234567890',
1032            'group'           => 'Library Staff',
1033            'expiration_date' => 'Someday',
1034            'birthdate'       => $birthDate->format('Y-m-d'),
1035        ];
1036        return $patron;
1037    }
1038
1039    /**
1040     * Get Patron Fines
1041     *
1042     * This is responsible for retrieving all fines by a specific patron.
1043     *
1044     * @param array $patron The patron array from patronLogin
1045     *
1046     * @return mixed        Array of the patron's fines on success.
1047     *
1048     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1049     */
1050    public function getMyFines($patron)
1051    {
1052        $this->checkIntermittentFailure();
1053        $session = $this->getSession($patron['id'] ?? null);
1054        if (!isset($session->fines)) {
1055            // How many items are there? %20 - 2 = 10% chance of none,
1056            // 90% of 1-18 (give or take some odd maths)
1057            $fines = rand() % 20 - 2;
1058
1059            $fineList = [];
1060            for ($i = 0; $i < $fines; $i++) {
1061                // How many days overdue is the item?
1062                $day_overdue = rand() % 30 + 5;
1063                // Calculate checkout date:
1064                $checkout = strtotime('now - ' . ($day_overdue + 14) . ' days');
1065                // 1 in 10 chance of this being a "Manual Fee":
1066                if (rand(1, 10) === 1) {
1067                    $fine = 2.50;
1068                    $type = 'Manual Fee';
1069                } else {
1070                    // 50c a day fine
1071                    $fine = $day_overdue * 0.50;
1072                    // After 20 days it becomes 'Long Overdue'
1073                    $type = $day_overdue > 20 ? 'Long Overdue' : 'Overdue';
1074                }
1075
1076                $fineList[] = [
1077                    'amount'   => $fine * 100,
1078                    'checkout' => $this->dateConverter
1079                        ->convertToDisplayDate('U', $checkout),
1080                    'createdate' => $this->dateConverter
1081                        ->convertToDisplayDate('U', time()),
1082                    'fine'     => $type,
1083                    // Additional description for long overdue fines:
1084                    'description' => 'Manual Fee' === $type ? 'Interlibrary loan request fee' : '',
1085                    // 50% chance they've paid half of it
1086                    'balance'  => (rand() % 100 > 49 ? $fine / 2 : $fine) * 100,
1087                    'duedate'  => $this->dateConverter->convertToDisplayDate(
1088                        'U',
1089                        strtotime("now - $day_overdue days")
1090                    ),
1091                ];
1092                // Some fines will have no id or title:
1093                if (rand() % 3 != 1) {
1094                    if ($this->idsInMyResearch) {
1095                        [$fineList[$i]['id'], $fineList[$i]['title']]
1096                            = $this->getRandomBibIdAndTitle();
1097                        $fineList[$i]['source'] = $this->getRecordSource();
1098                    } else {
1099                        $fineList[$i]['title'] = 'Demo Title ' . $i;
1100                    }
1101                }
1102            }
1103            $session->fines = $fineList;
1104        }
1105        return $session->fines;
1106    }
1107
1108    /**
1109     * Get Patron Holds
1110     *
1111     * This is responsible for retrieving all holds by a specific patron.
1112     *
1113     * @param array $patron The patron array from patronLogin
1114     *
1115     * @return mixed        Array of the patron's holds on success.
1116     *
1117     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1118     */
1119    public function getMyHolds($patron)
1120    {
1121        $this->checkIntermittentFailure();
1122        $session = $this->getSession($patron['id'] ?? null);
1123        if (!isset($session->holds)) {
1124            $session->holds = $this->createRequestList('Holds');
1125        }
1126        return $session->holds;
1127    }
1128
1129    /**
1130     * Get Patron Storage Retrieval Requests
1131     *
1132     * This is responsible for retrieving all call slips by a specific patron.
1133     *
1134     * @param array $patron The patron array from patronLogin
1135     *
1136     * @return mixed        Array of the patron's holds
1137     *
1138     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1139     */
1140    public function getMyStorageRetrievalRequests($patron)
1141    {
1142        $this->checkIntermittentFailure();
1143        $session = $this->getSession($patron['id'] ?? null);
1144        if (!isset($session->storageRetrievalRequests)) {
1145            $session->storageRetrievalRequests
1146                = $this->createRequestList('StorageRetrievalRequests');
1147        }
1148        return $session->storageRetrievalRequests;
1149    }
1150
1151    /**
1152     * Get Patron ILL Requests
1153     *
1154     * This is responsible for retrieving all ILL requests by a specific patron.
1155     *
1156     * @param array $patron The patron array from patronLogin
1157     *
1158     * @return mixed        Array of the patron's ILL requests
1159     *
1160     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1161     */
1162    public function getMyILLRequests($patron)
1163    {
1164        $this->checkIntermittentFailure();
1165        $session = $this->getSession($patron['id'] ?? null);
1166        if (!isset($session->ILLRequests)) {
1167            $session->ILLRequests = $this->createRequestList('ILLRequests');
1168        }
1169        return $session->ILLRequests;
1170    }
1171
1172    /**
1173     * Construct a transaction list for getMyTransactions; may be random or
1174     * pre-set depending on Demo.ini settings.
1175     *
1176     * @return array
1177     */
1178    protected function getTransactionList()
1179    {
1180        $this->checkIntermittentFailure();
1181        // If Demo.ini includes a fixed set of transactions, load those; otherwise
1182        // build some random ones.
1183        return isset($this->config['Records']['transactions'])
1184            ? json_decode($this->config['Records']['transactions'], true)
1185            : $this->getRandomTransactionList();
1186    }
1187
1188    /**
1189     * Calculate the due status for a due date.
1190     *
1191     * @param int $due Due date as Unix timestamp
1192     *
1193     * @return string
1194     */
1195    protected function calculateDueStatus($due)
1196    {
1197        $dueRelative = $due - time();
1198        if ($dueRelative < 0) {
1199            return 'overdue';
1200        } elseif ($dueRelative < 24 * 60 * 60) {
1201            return 'due';
1202        }
1203        return false;
1204    }
1205
1206    /**
1207     * Construct a random set of transactions for getMyTransactions().
1208     *
1209     * @return array
1210     */
1211    protected function getRandomTransactionList()
1212    {
1213        // How many items are there?  %10 - 1 = 10% chance of none,
1214        // 90% of 1-9 (give or take some odd maths)
1215        $trans = rand() % 10 - 1;
1216
1217        $transList = [];
1218        for ($i = 0; $i < $trans; $i++) {
1219            // When is it due? +/- up to 15 days
1220            $due_relative = rand() % 30 - 15;
1221            // Due date
1222            $rawDueDate = strtotime(
1223                'now ' . ($due_relative >= 0 ? '+' : '') . $due_relative . ' days'
1224            );
1225
1226            // Times renewed    : 0,0,0,0,0,1,2,3,4,5
1227            $renew = rand() % 10 - 5;
1228            if ($renew < 0) {
1229                $renew = 0;
1230            }
1231
1232            // Renewal limit
1233            $renewLimit = $renew + rand() % 3;
1234
1235            // Pending requests : 0,0,0,0,0,1,2,3,4,5
1236            $req = rand() % 10 - 5;
1237            if ($req < 0) {
1238                $req = 0;
1239            }
1240
1241            // Create a generic transaction:
1242            $transList[] = $this->getRandomItemIdentifier() + [
1243                // maintain separate display vs. raw due dates (the raw
1244                // one is used for renewals, in case the user display
1245                // format is incompatible with date math).
1246                'duedate' => $this->dateConverter->convertToDisplayDate(
1247                    'U',
1248                    $rawDueDate
1249                ),
1250                'rawduedate' => $rawDueDate,
1251                'dueStatus' => $this->calculateDueStatus($rawDueDate),
1252                'barcode' => sprintf('%08d', rand() % 50000),
1253                'renew'   => $renew,
1254                'renewLimit' => $renewLimit,
1255                'request' => $req,
1256                'item_id' => $i,
1257                'renewable' => $renew < $renewLimit,
1258            ];
1259            if ($i == 2 || rand() % 5 == 1) {
1260                // Mimic an ILL loan
1261                $transList[$i] += [
1262                    'id'      => "ill_institution_$i",
1263                    'title'   => "ILL Loan Title $i",
1264                    'institution_id' => 'ill_institution',
1265                    'institution_name' => 'ILL Library',
1266                    'institution_dbkey' => 'ill_institution',
1267                    'borrowingLocation' => 'ILL Service Desk',
1268                ];
1269            } else {
1270                $transList[$i]['borrowingLocation'] = $this->getFakeLoc();
1271                if ($this->idsInMyResearch) {
1272                    [$transList[$i]['id'], $transList[$i]['title']]
1273                        = $this->getRandomBibIdAndTitle();
1274                    $transList[$i]['source'] = $this->getRecordSource();
1275                } else {
1276                    $transList[$i]['title'] = 'Demo Title ' . $i;
1277                }
1278            }
1279        }
1280        return $transList;
1281    }
1282
1283    /**
1284     * Get Patron Transactions
1285     *
1286     * This is responsible for retrieving all transactions (i.e. checked out items)
1287     * by a specific patron.
1288     *
1289     * @param array $patron The patron array from patronLogin
1290     * @param array $params Parameters
1291     *
1292     * @return mixed        Array of the patron's transactions on success.
1293     *
1294     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1295     */
1296    public function getMyTransactions($patron, $params = [])
1297    {
1298        $this->checkIntermittentFailure();
1299        $session = $this->getSession($patron['id'] ?? null);
1300        if (!isset($session->transactions)) {
1301            $session->transactions = $this->getTransactionList();
1302        }
1303        // Order
1304        $transactions = $session->transactions;
1305        if (!empty($params['sort'])) {
1306            $sort = explode(
1307                ' ',
1308                !empty($params['sort']) ? $params['sort'] : 'date_due desc',
1309                2
1310            );
1311
1312            $descending = isset($sort[1]) && 'desc' === $sort[1];
1313
1314            usort(
1315                $transactions,
1316                function ($a, $b) use ($sort, $descending) {
1317                    if ('title' === $sort[0]) {
1318                        $cmp = $this->getSorter()->compare(
1319                            $a['title'] ?? '',
1320                            $b['title'] ?? ''
1321                        );
1322                    } else {
1323                        $cmp = $a['rawduedate'] - $b['rawduedate'];
1324                    }
1325                    return $descending ? -$cmp : $cmp;
1326                }
1327            );
1328        }
1329
1330        if (isset($params['limit'])) {
1331            $limit = $params['limit'] ?? 50;
1332            $offset = isset($params['page']) ? ($params['page'] - 1) * $limit : 0;
1333            $transactions = array_slice($transactions, $offset, $limit);
1334        }
1335
1336        return [
1337            'count' => count($session->transactions),
1338            'records' => $transactions,
1339        ];
1340    }
1341
1342    /**
1343     * Construct a historic transaction list for getMyTransactionHistory; may be
1344     * random or pre-set depending on Demo.ini settings.
1345     *
1346     * @return array
1347     */
1348    protected function getHistoricTransactionList()
1349    {
1350        $this->checkIntermittentFailure();
1351        // If Demo.ini includes a fixed set of transactions, load those; otherwise
1352        // build some random ones.
1353        return isset($this->config['Records']['historicTransactions'])
1354            ? json_decode($this->config['Records']['historicTransactions'], true)
1355            : $this->getRandomHistoricTransactionList();
1356    }
1357
1358    /**
1359     * Construct a random set of transactions for getMyTransactionHistory().
1360     *
1361     * @return array
1362     */
1363    protected function getRandomHistoricTransactionList()
1364    {
1365        // How many items are there?  %10 - 1 = 10% chance of none,
1366        // 90% of 1-150 (give or take some odd maths)
1367        $trans = rand() % 10 - 1 > 0 ? rand() % 15 : 0;
1368
1369        $transList = [];
1370        for ($i = 0; $i < $trans; $i++) {
1371            // Checkout date
1372            $relative = rand() % 300;
1373            $checkoutDate = strtotime("now -$relative days");
1374            // Due date (7-30 days from checkout)
1375            $dueDate = $checkoutDate + 60 * 60 * 24 * (rand() % 23 + 7);
1376            // Return date (1-40 days from checkout and < now)
1377            $returnDate = min(
1378                [$checkoutDate + 60 * 60 * 24 * (rand() % 39 + 1), time()]
1379            );
1380
1381            // Create a generic transaction:
1382            $transList[] = $this->getRandomItemIdentifier() + [
1383                'checkoutDate' => $this->dateConverter->convertToDisplayDate(
1384                    'U',
1385                    $checkoutDate
1386                ),
1387                'dueDate' => $this->dateConverter->convertToDisplayDate(
1388                    'U',
1389                    $dueDate
1390                ),
1391                'returnDate' => $this->dateConverter->convertToDisplayDate(
1392                    'U',
1393                    $returnDate
1394                ),
1395                // Raw dates for sorting
1396                '_checkoutDate' => $checkoutDate,
1397                '_dueDate' => $dueDate,
1398                '_returnDate' => $returnDate,
1399                'barcode' => sprintf('%08d', rand() % 50000),
1400                'row_id' => $i,
1401            ];
1402            if ($this->idsInMyResearch) {
1403                [$transList[$i]['id'], $transList[$i]['title']]
1404                    = $this->getRandomBibIdAndTitle();
1405                $transList[$i]['source'] = $this->getRecordSource();
1406            } else {
1407                $transList[$i]['title'] = 'Demo Title ' . $i;
1408            }
1409        }
1410        return $transList;
1411    }
1412
1413    /**
1414     * Get Patron Loan History
1415     *
1416     * This is responsible for retrieving all historic transactions for a specific
1417     * patron.
1418     *
1419     * @param array $patron The patron array from patronLogin
1420     * @param array $params Parameters
1421     *
1422     * @return mixed        Array of the patron's historic transactions on success.
1423     *
1424     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1425     */
1426    public function getMyTransactionHistory($patron, $params)
1427    {
1428        $this->checkIntermittentFailure();
1429        $session = $this->getSession($patron['id'] ?? null);
1430        if (!isset($session->historicLoans)) {
1431            $session->historicLoans = $this->getHistoricTransactionList();
1432        }
1433
1434        // Sort and splice the list
1435        $historicLoans = $session->historicLoans;
1436        if (isset($params['sort'])) {
1437            switch ($params['sort']) {
1438                case 'checkout asc':
1439                    $sorter = function ($a, $b) {
1440                        return strcmp($a['_checkoutDate'], $b['_checkoutDate']);
1441                    };
1442                    break;
1443                case 'return desc':
1444                    $sorter = function ($a, $b) {
1445                        return strcmp($b['_returnDate'], $a['_returnDate']);
1446                    };
1447                    break;
1448                case 'return asc':
1449                    $sorter = function ($a, $b) {
1450                        return strcmp($a['_returnDate'], $b['_returnDate']);
1451                    };
1452                    break;
1453                case 'due desc':
1454                    $sorter = function ($a, $b) {
1455                        return strcmp($b['_dueDate'], $a['_dueDate']);
1456                    };
1457                    break;
1458                case 'due asc':
1459                    $sorter = function ($a, $b) {
1460                        return strcmp($a['_dueDate'], $b['_dueDate']);
1461                    };
1462                    break;
1463                default:
1464                    $sorter = function ($a, $b) {
1465                        return strcmp($b['_checkoutDate'], $a['_checkoutDate']);
1466                    };
1467                    break;
1468            }
1469
1470            usort($historicLoans, $sorter);
1471        }
1472
1473        $limit = isset($params['limit']) ? (int)$params['limit'] : 50;
1474        $start = isset($params['page'])
1475            ? ((int)$params['page'] - 1) * $limit : 0;
1476
1477        $historicLoans = array_splice($historicLoans, $start, $limit);
1478
1479        return [
1480            'count' => count($session->historicLoans),
1481            'transactions' => $historicLoans,
1482        ];
1483    }
1484
1485    /**
1486     * Purge Patron Transaction History
1487     *
1488     * @param array  $patron The patron array from patronLogin
1489     * @param ?array $ids    IDs to purge, or null for all
1490     *
1491     * @throws ILSException
1492     * @return array Associative array of the results
1493     */
1494    public function purgeTransactionHistory(array $patron, ?array $ids): array
1495    {
1496        $this->checkIntermittentFailure();
1497        $session = $this->getSession($patron['id'] ?? null);
1498        if (null === $ids) {
1499            $session->historicLoans = [];
1500            $status = 'loan_history_all_purged';
1501        } else {
1502            $session->historicLoans = array_filter(
1503                $session->historicLoans ?? [],
1504                function ($loan) use ($ids) {
1505                    return !in_array($loan['row_id'], $ids);
1506                }
1507            );
1508            $status = 'loan_history_selected_purged';
1509        }
1510        return [
1511            'success' => true,
1512            'status' => $status,
1513            'sys_message' => '',
1514        ];
1515    }
1516
1517    /**
1518     * Get Pick Up Locations
1519     *
1520     * This is responsible get a list of valid library locations for holds / recall
1521     * retrieval
1522     *
1523     * @param array $patron      Patron information returned by the patronLogin
1524     * method.
1525     * @param array $holdDetails Optional array, only passed in when getting a list
1526     * in the context of placing or editing a hold. When placing a hold, it contains
1527     * most of the same values passed to placeHold, minus the patron data. When
1528     * editing a hold it contains all the hold information returned by getMyHolds.
1529     * May be used to limit the pickup options or may be ignored. The driver must
1530     * not add new options to the return array based on this data or other areas of
1531     * VuFind may behave incorrectly.
1532     *
1533     * @return array        An array of associative arrays with locationID and
1534     * locationDisplay keys
1535     *
1536     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1537     */
1538    public function getPickUpLocations($patron = false, $holdDetails = null)
1539    {
1540        $this->checkIntermittentFailure();
1541        $result = $this->locations;
1542        if (($holdDetails['reqnum'] ?? '') == 1) {
1543            $result[] = [
1544                'locationID' => 'D',
1545                'locationDisplay' => 'Campus D',
1546            ];
1547        }
1548
1549        if (isset($this->config['Holds']['excludePickupLocations'])) {
1550            $excluded
1551                = explode(':', $this->config['Holds']['excludePickupLocations']);
1552            $result = array_filter(
1553                $result,
1554                function ($loc) use ($excluded) {
1555                    return !in_array($loc['locationID'], $excluded);
1556                }
1557            );
1558        }
1559
1560        return $result;
1561    }
1562
1563    /**
1564     * Get Default "Hold Required By" Date (as Unix timestamp) or null if unsupported
1565     *
1566     * @param array $patron   Patron information returned by the patronLogin method.
1567     * @param array $holdInfo Contains most of the same values passed to
1568     * placeHold, minus the patron data.
1569     *
1570     * @return int|null
1571     *
1572     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1573     */
1574    public function getHoldDefaultRequiredDate($patron, $holdInfo)
1575    {
1576        $this->checkIntermittentFailure();
1577        // 5 years in the future (but similate intermittent failure):
1578        return !$this->isFailing(__METHOD__, 50)
1579            ? mktime(0, 0, 0, date('m'), date('d'), date('Y') + 5) : null;
1580    }
1581
1582    /**
1583     * Get Default Pick Up Location
1584     *
1585     * Returns the default pick up location set in HorizonXMLAPI.ini
1586     *
1587     * @param array $patron      Patron information returned by the patronLogin
1588     * method.
1589     * @param array $holdDetails Optional array, only passed in when getting a list
1590     * in the context of placing a hold; contains most of the same values passed to
1591     * placeHold, minus the patron data. May be used to limit the pickup options
1592     * or may be ignored.
1593     *
1594     * @return false|string      The default pickup location for the patron or false
1595     * if the user has to choose.
1596     *
1597     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1598     */
1599    public function getDefaultPickUpLocation($patron = false, $holdDetails = null)
1600    {
1601        $this->checkIntermittentFailure();
1602        return $this->defaultPickUpLocation;
1603    }
1604
1605    /**
1606     * Get Default Request Group
1607     *
1608     * Returns the default request group
1609     *
1610     * @param array $patron      Patron information returned by the patronLogin
1611     * method.
1612     * @param array $holdDetails Optional array, only passed in when getting a list
1613     * in the context of placing a hold; contains most of the same values passed to
1614     * placeHold, minus the patron data. May be used to limit the request group
1615     * options or may be ignored.
1616     *
1617     * @return false|string      The default request group for the patron.
1618     *
1619     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1620     */
1621    public function getDefaultRequestGroup($patron = false, $holdDetails = null)
1622    {
1623        $this->checkIntermittentFailure();
1624        if ($this->isFailing(__METHOD__, 50)) {
1625            return false;
1626        }
1627        $requestGroups = $this->getRequestGroups(0, 0);
1628        return $requestGroups[0]['id'];
1629    }
1630
1631    /**
1632     * Get request groups
1633     *
1634     * @param int   $bibId       BIB ID
1635     * @param array $patron      Patron information returned by the patronLogin
1636     * method.
1637     * @param array $holdDetails Optional array, only passed in when getting a list
1638     * in the context of placing a hold; contains most of the same values passed to
1639     * placeHold, minus the patron data. May be used to limit the request group
1640     * options or may be ignored.
1641     *
1642     * @return array  False if request groups not in use or an array of
1643     * associative arrays with id and name keys
1644     *
1645     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1646     */
1647    public function getRequestGroups(
1648        $bibId = null,
1649        $patron = null,
1650        $holdDetails = null
1651    ) {
1652        $this->checkIntermittentFailure();
1653        return [
1654            [
1655                'id' => 1,
1656                'name' => 'Main Library',
1657            ],
1658            [
1659                'id' => 2,
1660                'name' => 'Branch Library',
1661            ],
1662        ];
1663    }
1664
1665    /**
1666     * Get Funds
1667     *
1668     * Return a list of funds which may be used to limit the getNewItems list.
1669     *
1670     * @return array An associative array with key = fund ID, value = fund name.
1671     */
1672    public function getFunds()
1673    {
1674        $this->checkIntermittentFailure();
1675        return ['Fund A', 'Fund B', 'Fund C'];
1676    }
1677
1678    /**
1679     * Get Departments
1680     *
1681     * Obtain a list of departments for use in limiting the reserves list.
1682     *
1683     * @return array An associative array with key = dept. ID, value = dept. name.
1684     */
1685    public function getDepartments()
1686    {
1687        $this->checkIntermittentFailure();
1688        return $this->departments;
1689    }
1690
1691    /**
1692     * Get Instructors
1693     *
1694     * Obtain a list of instructors for use in limiting the reserves list.
1695     *
1696     * @return array An associative array with key = ID, value = name.
1697     */
1698    public function getInstructors()
1699    {
1700        $this->checkIntermittentFailure();
1701        return $this->instructors;
1702    }
1703
1704    /**
1705     * Get Courses
1706     *
1707     * Obtain a list of courses for use in limiting the reserves list.
1708     *
1709     * @return array An associative array with key = ID, value = name.
1710     */
1711    public function getCourses()
1712    {
1713        $this->checkIntermittentFailure();
1714        return $this->courses;
1715    }
1716
1717    /**
1718     * Get a set of random bib IDs
1719     *
1720     * @param int $limit Maximum number of IDs to return (max 30)
1721     *
1722     * @return string[]
1723     */
1724    protected function getRandomBibIds($limit): array
1725    {
1726        $count = rand(0, $limit > 30 ? 30 : $limit);
1727        $results = [];
1728        for ($x = 0; $x < $count; $x++) {
1729            $randomId = $this->getRandomBibId();
1730
1731            // avoid duplicate entries in array:
1732            if (!in_array($randomId, $results)) {
1733                $results[] = $randomId;
1734            }
1735        }
1736        return $results;
1737    }
1738
1739    /**
1740     * Get New Items
1741     *
1742     * Retrieve the IDs of items recently added to the catalog.
1743     *
1744     * @param int $page    Page number of results to retrieve (counting starts at 1)
1745     * @param int $limit   The size of each page of results to retrieve
1746     * @param int $daysOld The maximum age of records to retrieve in days (max. 30)
1747     * @param int $fundId  optional fund ID to use for limiting results (use a value
1748     * returned by getFunds, or exclude for no limit); note that "fund" may be a
1749     * misnomer - if funds are not an appropriate way to limit your new item
1750     * results, you can return a different set of values from getFunds. The
1751     * important thing is that this parameter supports an ID returned by getFunds,
1752     * whatever that may mean.
1753     *
1754     * @return array       Associative array with 'count' and 'results' keys
1755     *
1756     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1757     */
1758    public function getNewItems($page, $limit, $daysOld, $fundId = null)
1759    {
1760        $this->checkIntermittentFailure();
1761        // Pick a random number of results to return -- don't exceed limit or 30,
1762        // whichever is smaller (this can be pretty slow due to the random ID code).
1763        $results = $this->config['Records']['new_items']
1764            ?? $this->getRandomBibIds(30);
1765        $retVal = ['count' => count($results), 'results' => []];
1766        foreach ($results as $result) {
1767            $retVal['results'][] = ['id' => $result];
1768        }
1769        return $retVal;
1770    }
1771
1772    /**
1773     * Determine a course ID for findReserves.
1774     *
1775     * @param string $course Course ID (or empty for a random choice)
1776     *
1777     * @return string
1778     */
1779    protected function getCourseId(string $course = ''): string
1780    {
1781        return empty($course) ? (string)rand(0, count($this->courses) - 1) : $course;
1782    }
1783
1784    /**
1785     * Determine a department ID for findReserves.
1786     *
1787     * @param string $dept Department ID (or empty for a random choice)
1788     *
1789     * @return string
1790     */
1791    protected function getDepartmentId(string $dept = ''): string
1792    {
1793        return empty($dept) ? (string)rand(0, count($this->departments) - 1) : $dept;
1794    }
1795
1796    /**
1797     * Determine an instructor ID for findReserves.
1798     *
1799     * @param string $inst Instructor ID (or empty for a random choice)
1800     *
1801     * @return string
1802     */
1803    protected function getInstructorId(string $inst = ''): string
1804    {
1805        return empty($inst) ? (string)rand(0, count($this->instructors) - 1) : $inst;
1806    }
1807
1808    /**
1809     * Find Reserves
1810     *
1811     * Obtain information on course reserves.
1812     *
1813     * @param string $course ID from getCourses (empty string to match all)
1814     * @param string $inst   ID from getInstructors (empty string to match all)
1815     * @param string $dept   ID from getDepartments (empty string to match all)
1816     *
1817     * @return mixed An array of associative arrays representing reserve items.
1818     *
1819     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1820     */
1821    public function findReserves($course, $inst, $dept)
1822    {
1823        $this->checkIntermittentFailure();
1824        // Pick a random number of results to return -- don't exceed 30.
1825        $count = rand(0, 30);
1826        $results = [];
1827        for ($x = 0; $x < $count; $x++) {
1828            $randomId = $this->getRandomBibId();
1829
1830            // avoid duplicate entries in array:
1831            if (!in_array($randomId, $results)) {
1832                $results[] = $randomId;
1833            }
1834        }
1835
1836        $retVal = [];
1837        foreach ($results as $current) {
1838            $retVal[] = [
1839                'BIB_ID' => $current,
1840                'INSTRUCTOR_ID' => $this->getInstructorId($inst),
1841                'COURSE_ID' => $this->getCourseId($course),
1842                'DEPARTMENT_ID' => $this->getDepartmentId($dept),
1843            ];
1844        }
1845        return $retVal;
1846    }
1847
1848    /**
1849     * Cancel Holds
1850     *
1851     * Attempts to Cancel a hold or recall on a particular item.
1852     *
1853     * @param array $cancelDetails An array of item and patron data
1854     *
1855     * @return array               An array of data on each request including
1856     * whether or not it was successful and a system message (if available)
1857     */
1858    public function cancelHolds($cancelDetails)
1859    {
1860        $this->checkIntermittentFailure();
1861        // Rewrite the holds in the session, removing those the user wants to
1862        // cancel.
1863        $newHolds = new ArrayObject();
1864        $retVal = ['count' => 0, 'items' => []];
1865        $session = $this->getSession($cancelDetails['patron']['id'] ?? null);
1866        foreach ($session->holds as $current) {
1867            if (!in_array($current['reqnum'], $cancelDetails['details'])) {
1868                $newHolds->append($current);
1869            } else {
1870                if (!$this->isFailing(__METHOD__, 50)) {
1871                    $retVal['count']++;
1872                    $retVal['items'][$current['item_id']] = [
1873                        'success' => true,
1874                        'status' => 'hold_cancel_success',
1875                    ];
1876                } else {
1877                    $newHolds->append($current);
1878                    $retVal['items'][$current['item_id']] = [
1879                        'success' => false,
1880                        'status' => 'hold_cancel_fail',
1881                        'sysMessage' =>
1882                            'Demonstrating failure; keep trying and ' .
1883                            'it will work eventually.',
1884                    ];
1885                }
1886            }
1887        }
1888
1889        $session->holds = $newHolds;
1890        return $retVal;
1891    }
1892
1893    /**
1894     * Update holds
1895     *
1896     * This is responsible for changing the status of hold requests
1897     *
1898     * @param array $holdsDetails The details identifying the holds
1899     * @param array $fields       An associative array of fields to be updated
1900     * @param array $patron       Patron array
1901     *
1902     * @return array Associative array of the results
1903     */
1904    public function updateHolds(
1905        array $holdsDetails,
1906        array $fields,
1907        array $patron
1908    ): array {
1909        $results = [];
1910        $session = $this->getSession($patron['id']);
1911        foreach ($session->holds as &$currentHold) {
1912            if (
1913                !isset($currentHold['updateDetails'])
1914                || !in_array($currentHold['updateDetails'], $holdsDetails)
1915            ) {
1916                continue;
1917            }
1918            if ($this->isFailing(__METHOD__, 25)) {
1919                $results[$currentHold['reqnum']]['success'] = false;
1920                $results[$currentHold['reqnum']]['status']
1921                    = 'Simulated error; try again and it will work eventually.';
1922                continue;
1923            }
1924            if (array_key_exists('frozen', $fields)) {
1925                if ($fields['frozen']) {
1926                    $currentHold['frozen'] = true;
1927                    if (isset($fields['frozenThrough'])) {
1928                        $currentHold['frozenThrough'] = $this->dateConverter
1929                            ->convertToDisplayDate('U', $fields['frozenThroughTS']);
1930                    } else {
1931                        $currentHold['frozenThrough'] = '';
1932                    }
1933                } else {
1934                    $currentHold['frozen'] = false;
1935                    $currentHold['frozenThrough'] = '';
1936                }
1937            }
1938            if (isset($fields['pickUpLocation'])) {
1939                $currentHold['location'] = $fields['pickUpLocation'];
1940            }
1941            $results[$currentHold['reqnum']]['success'] = true;
1942        }
1943
1944        return $results;
1945    }
1946
1947    /**
1948     * Cancel Storage Retrieval Request
1949     *
1950     * Attempts to Cancel a Storage Retrieval Request on a particular item. The
1951     * data in $cancelDetails['details'] is determined by
1952     * getCancelStorageRetrievalRequestDetails().
1953     *
1954     * @param array $cancelDetails An array of item and patron data
1955     *
1956     * @return array               An array of data on each request including
1957     * whether or not it was successful and a system message (if available)
1958     */
1959    public function cancelStorageRetrievalRequests($cancelDetails)
1960    {
1961        $this->checkIntermittentFailure();
1962        // Rewrite the items in the session, removing those the user wants to
1963        // cancel.
1964        $newRequests = new ArrayObject();
1965        $retVal = ['count' => 0, 'items' => []];
1966        $session = $this->getSession($cancelDetails['patron']['id'] ?? null);
1967        foreach ($session->storageRetrievalRequests as $current) {
1968            if (!in_array($current['reqnum'], $cancelDetails['details'])) {
1969                $newRequests->append($current);
1970            } else {
1971                if (!$this->isFailing(__METHOD__, 50)) {
1972                    $retVal['count']++;
1973                    $retVal['items'][$current['item_id']] = [
1974                        'success' => true,
1975                        'status' => 'storage_retrieval_request_cancel_success',
1976                    ];
1977                } else {
1978                    $newRequests->append($current);
1979                    $retVal['items'][$current['item_id']] = [
1980                        'success' => false,
1981                        'status' => 'storage_retrieval_request_cancel_fail',
1982                        'sysMessage' =>
1983                            'Demonstrating failure; keep trying and ' .
1984                            'it will work eventually.',
1985                    ];
1986                }
1987            }
1988        }
1989
1990        $session->storageRetrievalRequests = $newRequests;
1991        return $retVal;
1992    }
1993
1994    /**
1995     * Get Cancel Storage Retrieval Request Details
1996     *
1997     * In order to cancel a hold, Voyager requires the patron details an item ID
1998     * and a recall ID. This function returns the item id and recall id as a string
1999     * separated by a pipe, which is then submitted as form data in Hold.php. This
2000     * value is then extracted by the CancelHolds function.
2001     *
2002     * @param array $request An array of request data
2003     * @param array $patron  Patron information from patronLogin
2004     *
2005     * @return string Data for use in a form field
2006     *
2007     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2008     */
2009    public function getCancelStorageRetrievalRequestDetails($request, $patron)
2010    {
2011        return $request['reqnum'];
2012    }
2013
2014    /**
2015     * Renew My Items
2016     *
2017     * Function for attempting to renew a patron's items. The data in
2018     * $renewDetails['details'] is determined by getRenewDetails().
2019     *
2020     * @param array $renewDetails An array of data required for renewing items
2021     * including the Patron ID and an array of renewal IDS
2022     *
2023     * @return array              An array of renewal information keyed by item ID
2024     */
2025    public function renewMyItems($renewDetails)
2026    {
2027        $this->checkIntermittentFailure();
2028        // Simulate an account block at random.
2029        if ($this->checkRenewBlock()) {
2030            return [
2031                'blocks' => [
2032                    'Simulated account block; try again and it will work eventually.',
2033                ],
2034                'details' => [],
2035            ];
2036        }
2037
2038        // Set up successful return value.
2039        $finalResult = ['blocks' => false, 'details' => []];
2040
2041        // Grab transactions from session so we can modify them:
2042        $session = $this->getSession($renewDetails['patron']['id'] ?? null);
2043        $transactions = $session->transactions;
2044        foreach ($transactions as $i => $current) {
2045            // Only renew requested items:
2046            if (in_array($current['item_id'], $renewDetails['details'])) {
2047                if (!$this->isFailing(__METHOD__, 50)) {
2048                    $transactions[$i]['rawduedate'] += 21 * 24 * 60 * 60;
2049                    $transactions[$i]['dueStatus']
2050                        = $this->calculateDueStatus($transactions[$i]['rawduedate']);
2051                    $transactions[$i]['duedate']
2052                        = $this->dateConverter->convertToDisplayDate(
2053                            'U',
2054                            $transactions[$i]['rawduedate']
2055                        );
2056                    $transactions[$i]['renew'] = $transactions[$i]['renew'] + 1;
2057                    $transactions[$i]['renewable']
2058                        = $transactions[$i]['renew']
2059                        < $transactions[$i]['renewLimit'];
2060
2061                    $finalResult['details'][$current['item_id']] = [
2062                        'success' => true,
2063                        'new_date' => $transactions[$i]['duedate'],
2064                        'new_time' => '',
2065                        'item_id' => $current['item_id'],
2066                    ];
2067                } else {
2068                    $finalResult['details'][$current['item_id']] = [
2069                        'success' => false,
2070                        'new_date' => false,
2071                        'item_id' => $current['item_id'],
2072                        'sysMessage' =>
2073                            'Demonstrating failure; keep trying and ' .
2074                            'it will work eventually.',
2075                    ];
2076                }
2077            }
2078        }
2079
2080        // Write modified transactions back to session; in-place changes do not
2081        // work due to ArrayObject eccentricities:
2082        $session->transactions = $transactions;
2083
2084        return $finalResult;
2085    }
2086
2087    /**
2088     * Get Renew Details
2089     *
2090     * In order to renew an item, Voyager requires the patron details and an item
2091     * id. This function returns the item id as a string which is then used
2092     * as submitted form data in checkedOut.php. This value is then extracted by
2093     * the RenewMyItems function.
2094     *
2095     * @param array $checkOutDetails An array of item data
2096     *
2097     * @return string Data for use in a form field
2098     */
2099    public function getRenewDetails($checkOutDetails)
2100    {
2101        return $checkOutDetails['item_id'];
2102    }
2103
2104    /**
2105     * Check if hold or recall available
2106     *
2107     * This is responsible for determining if an item is requestable
2108     *
2109     * @param string $id     The Bib ID
2110     * @param array  $data   An Array of item data
2111     * @param array  $patron An array of patron data
2112     *
2113     * @return mixed An array of data on the request including
2114     * whether or not it is valid and a status message. Alternatively a boolean
2115     * true if request is valid, false if not.
2116     *
2117     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2118     */
2119    public function checkRequestIsValid($id, $data, $patron)
2120    {
2121        $this->checkIntermittentFailure();
2122        if ($this->isFailing(__METHOD__, 10)) {
2123            return [
2124                'valid' => false,
2125                'status' => rand() % 3 != 0
2126                    ? 'hold_error_blocked' : 'Demonstrating a custom failure',
2127            ];
2128        }
2129        return [
2130            'valid' => true,
2131            'status' => 'request_place_text',
2132        ];
2133    }
2134
2135    /**
2136     * Place Hold
2137     *
2138     * Attempts to place a hold or recall on a particular item and returns
2139     * an array with result details.
2140     *
2141     * @param array $holdDetails An array of item and patron data
2142     *
2143     * @return mixed An array of data on the request including
2144     * whether or not it was successful and a system message (if available)
2145     */
2146    public function placeHold($holdDetails)
2147    {
2148        $this->checkIntermittentFailure();
2149        // Simulate failure:
2150        if ($this->isFailing(__METHOD__, 50)) {
2151            return [
2152                'success' => false,
2153                'sysMessage' =>
2154                    'Demonstrating failure; keep trying and ' .
2155                    'it will work eventually.',
2156            ];
2157        }
2158
2159        $session = $this->getSession($holdDetails['patron']['id'] ?? null);
2160        if (!isset($session->holds)) {
2161            $session->holds = new ArrayObject();
2162        }
2163        $lastHold = count($session->holds) - 1;
2164        $nextId = $lastHold >= 0
2165            ? $session->holds[$lastHold]['item_id'] + 1 : 0;
2166
2167        // Figure out appropriate expiration date:
2168        $expire = !empty($holdDetails['requiredByTS'])
2169            ? $this->dateConverter->convertToDisplayDate(
2170                'Y-m-d',
2171                gmdate('Y-m-d', $holdDetails['requiredByTS'])
2172            ) : null;
2173
2174        $requestGroup = '';
2175        foreach ($this->getRequestGroups(null, null) as $group) {
2176            if (
2177                isset($holdDetails['requestGroupId'])
2178                && $group['id'] == $holdDetails['requestGroupId']
2179            ) {
2180                $requestGroup = $group['name'];
2181                break;
2182            }
2183        }
2184        if ($holdDetails['startDateTS']) {
2185            // Suspend until the previous day:
2186            $frozen = true;
2187            $frozenThrough = $this->dateConverter->convertToDisplayDate(
2188                'U',
2189                \DateTime::createFromFormat(
2190                    'U',
2191                    $holdDetails['startDateTS']
2192                )->modify('-1 DAY')->getTimestamp()
2193            );
2194        } else {
2195            $frozen = false;
2196            $frozenThrough = '';
2197        }
2198        $reqNum = sprintf('%06d', $nextId);
2199        $proxiedFor = null;
2200        if (!empty($holdDetails['proxiedUser'])) {
2201            $proxies = $this->getProxiedUsers($holdDetails['patron']);
2202            $proxiedFor = $proxies[$holdDetails['proxiedUser']];
2203        }
2204        $session->holds->append(
2205            [
2206                'id'       => $holdDetails['id'],
2207                'source'   => $this->getRecordSource(),
2208                'location' => $holdDetails['pickUpLocation'],
2209                'expire'   => $expire,
2210                'create'   =>
2211                    $this->dateConverter->convertToDisplayDate('U', time()),
2212                'reqnum'   => $reqNum,
2213                'item_id'  => $nextId,
2214                'volume'   => '',
2215                'processed' => '',
2216                'requestGroup' => $requestGroup,
2217                'frozen'   => $frozen,
2218                'frozenThrough' => $frozenThrough,
2219                'updateDetails' => $reqNum,
2220                'cancel_details' => $reqNum,
2221                'proxiedFor' => $proxiedFor,
2222            ]
2223        );
2224
2225        return ['success' => true];
2226    }
2227
2228    /**
2229     * Check if storage retrieval request available
2230     *
2231     * This is responsible for determining if an item is requestable
2232     *
2233     * @param string $id     The Bib ID
2234     * @param array  $data   An Array of item data
2235     * @param array  $patron An array of patron data
2236     *
2237     * @return mixed An array of data on the request including
2238     * whether or not it is valid and a status message. Alternatively a boolean
2239     * true if request is valid, false if not.
2240     *
2241     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2242     */
2243    public function checkStorageRetrievalRequestIsValid($id, $data, $patron)
2244    {
2245        $this->checkIntermittentFailure();
2246        if (!$this->storageRetrievalRequests || $this->isFailing(__METHOD__, 10)) {
2247            return [
2248                'valid' => false,
2249                'status' => rand() % 3 != 0
2250                    ? 'storage_retrieval_request_error_blocked'
2251                    : 'Demonstrating a custom failure',
2252            ];
2253        }
2254        return [
2255            'valid' => true,
2256            'status' => 'storage_retrieval_request_place_text',
2257        ];
2258    }
2259
2260    /**
2261     * Place a Storage Retrieval Request
2262     *
2263     * Attempts to place a request on a particular item and returns
2264     * an array with result details.
2265     *
2266     * @param array $details An array of item and patron data
2267     *
2268     * @return mixed An array of data on the request including
2269     * whether or not it was successful and a system message (if available)
2270     */
2271    public function placeStorageRetrievalRequest($details)
2272    {
2273        $this->checkIntermittentFailure();
2274        if (!$this->storageRetrievalRequests) {
2275            return [
2276                'success' => false,
2277                'sysMessage' => 'Storage Retrieval Requests are disabled.',
2278            ];
2279        }
2280
2281        // Make sure pickup location is valid
2282        $pickUpLocation = $details['pickUpLocation'] ?? null;
2283        $validLocations = array_column($this->getPickUpLocations(), 'locationID');
2284        if (
2285            null !== $pickUpLocation
2286            && !in_array($pickUpLocation, $validLocations)
2287        ) {
2288            return [
2289                'success' => false,
2290                'sysMessage' => 'storage_retrieval_request_invalid_pickup',
2291            ];
2292        }
2293
2294        // Simulate failure:
2295        if ($this->isFailing(__METHOD__, 50)) {
2296            return [
2297                'success' => false,
2298                'sysMessage' =>
2299                    'Demonstrating failure; keep trying and ' .
2300                    'it will work eventually.',
2301            ];
2302        }
2303
2304        $session = $this->getSession($details['patron']['id'] ?? null);
2305        if (!isset($session->storageRetrievalRequests)) {
2306            $session->storageRetrievalRequests = new ArrayObject();
2307        }
2308        $lastRequest = count($session->storageRetrievalRequests) - 1;
2309        $nextId = $lastRequest >= 0
2310            ? $session->storageRetrievalRequests[$lastRequest]['item_id'] + 1
2311            : 0;
2312
2313        // Figure out appropriate expiration date:
2314        if (
2315            !isset($details['requiredBy'])
2316            || empty($details['requiredBy'])
2317        ) {
2318            $expire = strtotime('now + 30 days');
2319        } else {
2320            try {
2321                $expire = $this->dateConverter->convertFromDisplayDate(
2322                    'U',
2323                    $details['requiredBy']
2324                );
2325            } catch (DateException $e) {
2326                // Expiration date is invalid
2327                return [
2328                    'success' => false,
2329                    'sysMessage' => 'storage_retrieval_request_date_invalid',
2330                ];
2331            }
2332        }
2333        if ($expire <= time()) {
2334            return [
2335                'success' => false,
2336                'sysMessage' => 'storage_retrieval_request_date_past',
2337            ];
2338        }
2339
2340        $session->storageRetrievalRequests->append(
2341            [
2342                'id'       => $details['id'],
2343                'source'   => $this->getRecordSource(),
2344                'location' => $details['pickUpLocation'],
2345                'expire'   =>
2346                    $this->dateConverter->convertToDisplayDate('U', $expire),
2347                'create'   =>
2348                    $this->dateConverter->convertToDisplayDate('U', time()),
2349                'processed' => rand() % 3 == 0
2350                    ? $this->dateConverter->convertToDisplayDate('U', $expire) : '',
2351                'reqnum'   => sprintf('%06d', $nextId),
2352                'item_id'  => $nextId,
2353            ]
2354        );
2355
2356        return ['success' => true];
2357    }
2358
2359    /**
2360     * Check if ILL request available
2361     *
2362     * This is responsible for determining if an item is requestable
2363     *
2364     * @param string $id     The Bib ID
2365     * @param array  $data   An Array of item data
2366     * @param array  $patron An array of patron data
2367     *
2368     * @return mixed An array of data on the request including
2369     * whether or not it is valid and a status message. Alternatively a boolean
2370     * true if request is valid, false if not.
2371     *
2372     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2373     */
2374    public function checkILLRequestIsValid($id, $data, $patron)
2375    {
2376        $this->checkIntermittentFailure();
2377        if (!$this->ILLRequests || $this->isFailing(__METHOD__, 10)) {
2378            return [
2379                'valid' => false,
2380                'status' => rand() % 3 != 0
2381                    ? 'ill_request_error_blocked' : 'Demonstrating a custom failure',
2382            ];
2383        }
2384        return [
2385            'valid' => true,
2386            'status' => 'ill_request_place_text',
2387        ];
2388    }
2389
2390    /**
2391     * Place ILL Request
2392     *
2393     * Attempts to place an ILL request on a particular item and returns
2394     * an array with result details
2395     *
2396     * @param array $details An array of item and patron data
2397     *
2398     * @return mixed An array of data on the request including
2399     * whether or not it was successful and a system message (if available)
2400     */
2401    public function placeILLRequest($details)
2402    {
2403        $this->checkIntermittentFailure();
2404        if (!$this->ILLRequests) {
2405            return [
2406                'success' => false,
2407                'sysMessage' => 'ILL requests are disabled.',
2408            ];
2409        }
2410        // Simulate failure:
2411        if ($this->isFailing(__METHOD__, 50)) {
2412            return [
2413                'success' => false,
2414                'sysMessage' =>
2415                    'Demonstrating failure; keep trying and ' .
2416                    'it will work eventually.',
2417            ];
2418        }
2419
2420        $session = $this->getSession($details['patron']['id'] ?? null);
2421        if (!isset($session->ILLRequests)) {
2422            $session->ILLRequests = new ArrayObject();
2423        }
2424        $lastRequest = count($session->ILLRequests) - 1;
2425        $nextId = $lastRequest >= 0
2426            ? $session->ILLRequests[$lastRequest]['item_id'] + 1
2427            : 0;
2428
2429        // Figure out appropriate expiration date:
2430        if (
2431            !isset($details['requiredBy'])
2432            || empty($details['requiredBy'])
2433        ) {
2434            $expire = strtotime('now + 30 days');
2435        } else {
2436            try {
2437                $expire = $this->dateConverter->convertFromDisplayDate(
2438                    'U',
2439                    $details['requiredBy']
2440                );
2441            } catch (DateException $e) {
2442                // Expiration Date is invalid
2443                return [
2444                    'success' => false,
2445                    'sysMessage' => 'ill_request_date_invalid',
2446                ];
2447            }
2448        }
2449        if ($expire <= time()) {
2450            return [
2451                'success' => false,
2452                'sysMessage' => 'ill_request_date_past',
2453            ];
2454        }
2455
2456        // Verify pickup library and location
2457        $pickupLocation = '';
2458        $pickupLocations = $this->getILLPickupLocations(
2459            $details['id'],
2460            $details['pickUpLibrary'],
2461            $details['patron']
2462        );
2463        foreach ($pickupLocations as $location) {
2464            if ($location['id'] == $details['pickUpLibraryLocation']) {
2465                $pickupLocation = $location['name'];
2466                break;
2467            }
2468        }
2469        if (!$pickupLocation) {
2470            return [
2471                'success' => false,
2472                'sysMessage' => 'ill_request_place_fail_missing',
2473            ];
2474        }
2475
2476        $session->ILLRequests->append(
2477            [
2478                'id'       => $details['id'],
2479                'source'   => $this->getRecordSource(),
2480                'location' => $pickupLocation,
2481                'expire'   =>
2482                    $this->dateConverter->convertToDisplayDate('U', $expire),
2483                'create'   =>
2484                    $this->dateConverter->convertToDisplayDate('U', time()),
2485                'processed' => rand() % 3 == 0
2486                    ? $this->dateConverter->convertToDisplayDate('U', $expire) : '',
2487                'reqnum'   => sprintf('%06d', $nextId),
2488                'item_id'  => $nextId,
2489            ]
2490        );
2491
2492        return ['success' => true];
2493    }
2494
2495    /**
2496     * Get ILL Pickup Libraries
2497     *
2498     * This is responsible for getting information on the possible pickup libraries
2499     *
2500     * @param string $id     Record ID
2501     * @param array  $patron Patron
2502     *
2503     * @return bool|array False if request not allowed, or an array of associative
2504     * arrays with libraries.
2505     *
2506     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2507     */
2508    public function getILLPickupLibraries($id, $patron)
2509    {
2510        $this->checkIntermittentFailure();
2511        if (!$this->ILLRequests) {
2512            return false;
2513        }
2514
2515        $details = [
2516            [
2517                'id' => 1,
2518                'name' => 'Main Library',
2519                'isDefault' => true,
2520            ],
2521            [
2522                'id' => 2,
2523                'name' => 'Branch Library',
2524                'isDefault' => false,
2525            ],
2526        ];
2527
2528        return $details;
2529    }
2530
2531    /**
2532     * Get ILL Pickup Locations
2533     *
2534     * This is responsible for getting a list of possible pickup locations for a
2535     * library
2536     *
2537     * @param string $id        Record ID
2538     * @param string $pickupLib Pickup library ID
2539     * @param array  $patron    Patron
2540     *
2541     * @return bool|array False if request not allowed, or an array of locations.
2542     *
2543     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2544     */
2545    public function getILLPickupLocations($id, $pickupLib, $patron)
2546    {
2547        $this->checkIntermittentFailure();
2548        switch ($pickupLib) {
2549            case 1:
2550                return [
2551                    [
2552                        'id' => 1,
2553                        'name' => 'Circulation Desk',
2554                        'isDefault' => true,
2555                    ],
2556                    [
2557                        'id' => 2,
2558                        'name' => 'Reference Desk',
2559                        'isDefault' => false,
2560                    ],
2561                ];
2562            case 2:
2563                return [
2564                    [
2565                        'id' => 3,
2566                        'name' => 'Main Desk',
2567                        'isDefault' => false,
2568                    ],
2569                    [
2570                        'id' => 4,
2571                        'name' => 'Library Bus',
2572                        'isDefault' => true,
2573                    ],
2574                ];
2575        }
2576        return [];
2577    }
2578
2579    /**
2580     * Cancel ILL Request
2581     *
2582     * Attempts to Cancel an ILL request on a particular item. The
2583     * data in $cancelDetails['details'] is determined by
2584     * getCancelILLRequestDetails().
2585     *
2586     * @param array $cancelDetails An array of item and patron data
2587     *
2588     * @return array               An array of data on each request including
2589     * whether or not it was successful and a system message (if available)
2590     */
2591    public function cancelILLRequests($cancelDetails)
2592    {
2593        $this->checkIntermittentFailure();
2594        // Rewrite the items in the session, removing those the user wants to
2595        // cancel.
2596        $newRequests = new ArrayObject();
2597        $retVal = ['count' => 0, 'items' => []];
2598        $session = $this->getSession($cancelDetails['patron']['id'] ?? null);
2599        foreach ($session->ILLRequests as $current) {
2600            if (!in_array($current['reqnum'], $cancelDetails['details'])) {
2601                $newRequests->append($current);
2602            } else {
2603                if (!$this->isFailing(__METHOD__, 50)) {
2604                    $retVal['count']++;
2605                    $retVal['items'][$current['item_id']] = [
2606                        'success' => true,
2607                        'status' => 'ill_request_cancel_success',
2608                    ];
2609                } else {
2610                    $newRequests->append($current);
2611                    $retVal['items'][$current['item_id']] = [
2612                        'success' => false,
2613                        'status' => 'ill_request_cancel_fail',
2614                        'sysMessage' =>
2615                            'Demonstrating failure; keep trying and ' .
2616                            'it will work eventually.',
2617                    ];
2618                }
2619            }
2620        }
2621
2622        $session->ILLRequests = $newRequests;
2623        return $retVal;
2624    }
2625
2626    /**
2627     * Get Cancel ILL Request Details
2628     *
2629     * @param array $request An array of request data
2630     * @param array $patron  Patron information from patronLogin
2631     *
2632     * @return string Data for use in a form field
2633     *
2634     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2635     */
2636    public function getCancelILLRequestDetails($request, $patron)
2637    {
2638        return $request['reqnum'];
2639    }
2640
2641    /**
2642     * Change Password
2643     *
2644     * Attempts to change patron password (PIN code)
2645     *
2646     * @param array $details An array of patron id and old and new password:
2647     *
2648     * 'patron'      The patron array from patronLogin
2649     * 'oldPassword' Old password
2650     * 'newPassword' New password
2651     *
2652     * @return array An array of data on the request including
2653     * whether or not it was successful and a system message (if available)
2654     *
2655     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2656     */
2657    public function changePassword($details)
2658    {
2659        $this->checkIntermittentFailure();
2660        if (!$this->isFailing(__METHOD__, 33)) {
2661            return ['success' => true, 'status' => 'change_password_ok'];
2662        }
2663        return [
2664            'success' => false,
2665            'status' => 'An error has occurred',
2666            'sysMessage' =>
2667                'Demonstrating failure; keep trying and it will work eventually.',
2668        ];
2669    }
2670
2671    /**
2672     * Public Function which specifies renew, hold and cancel settings.
2673     *
2674     * @param string $function The name of the feature to be checked
2675     * @param array  $params   Optional feature-specific parameters (array)
2676     *
2677     * @return array An array with key-value pairs.
2678     *
2679     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2680     */
2681    public function getConfig($function, $params = [])
2682    {
2683        $this->checkIntermittentFailure();
2684        if ($function == 'Holds') {
2685            return $this->config['Holds']
2686                ?? [
2687                    'HMACKeys' => 'id:item_id:level',
2688                    'extraHoldFields' =>
2689                        'comments:requestGroup:pickUpLocation:requiredByDate',
2690                    'defaultRequiredDate' => 'driver:0:2:0',
2691                ];
2692        }
2693        if ($function == 'Holdings') {
2694            return [
2695                'itemLimit' => $this->config['Holdings']['itemLimit'] ?? null,
2696            ];
2697        }
2698        if (
2699            $function == 'StorageRetrievalRequests'
2700            && $this->storageRetrievalRequests
2701        ) {
2702            return [
2703                'HMACKeys' => 'id',
2704                'extraFields' => 'comments:pickUpLocation:requiredByDate:item-issue',
2705                'helpText' => 'This is a storage retrieval request help text'
2706                    . ' with some <span style="color: red">styling</span>.',
2707            ];
2708        }
2709        if ($function == 'ILLRequests' && $this->ILLRequests) {
2710            return [
2711                'enabled' => true,
2712                'HMACKeys' => 'number',
2713                'extraFields' =>
2714                    'comments:pickUpLibrary:pickUpLibraryLocation:requiredByDate',
2715                'defaultRequiredDate' => '0:1:0',
2716                'helpText' => 'This is an ILL request help text'
2717                    . ' with some <span style="color: red">styling</span>.',
2718            ];
2719        }
2720        if ($function == 'changePassword') {
2721            return $this->config['changePassword']
2722                ?? ['minLength' => 4, 'maxLength' => 20];
2723        }
2724        if ($function == 'getMyTransactionHistory') {
2725            if (empty($this->config['TransactionHistory']['enabled'])) {
2726                return false;
2727            }
2728            $config = [
2729                'sort' => [
2730                    'checkout desc' => 'sort_checkout_date_desc',
2731                    'checkout asc' => 'sort_checkout_date_asc',
2732                    'return desc' => 'sort_return_date_desc',
2733                    'return asc' => 'sort_return_date_asc',
2734                    'due desc' => 'sort_due_date_desc',
2735                    'due asc' => 'sort_due_date_asc',
2736                ],
2737                'default_sort' => 'checkout desc',
2738                'purge_all' => $this->config['TransactionHistory']['purgeAll'] ?? true,
2739                'purge_selected' => $this->config['TransactionHistory']['purgeSelected'] ?? true,
2740            ];
2741            if ($this->config['Loans']['paging'] ?? false) {
2742                $config['max_results']
2743                    = $this->config['Loans']['max_page_size'] ?? 100;
2744            }
2745            return $config;
2746        }
2747        if ('getMyTransactions' === $function) {
2748            if (empty($this->config['Loans']['paging'])) {
2749                return [];
2750            }
2751            return [
2752                'max_results' => $this->config['Loans']['max_page_size'] ?? 100,
2753                'sort' => [
2754                    'due desc' => 'sort_due_date_desc',
2755                    'due asc' => 'sort_due_date_asc',
2756                    'title asc' => 'sort_title',
2757                ],
2758                'default_sort' => 'due asc',
2759            ];
2760        }
2761        if ($function == 'patronLogin') {
2762            return [
2763                'loginMethod'
2764                    => $this->config['Catalog']['loginMethod'] ?? 'password',
2765            ];
2766        }
2767
2768        return [];
2769    }
2770
2771    /**
2772     * Get bib records for recently returned items.
2773     *
2774     * @param int   $limit  Maximum number of records to retrieve (default = 30)
2775     * @param int   $maxage The maximum number of days to consider "recently
2776     * returned."
2777     * @param array $patron Patron Data
2778     *
2779     * @return array
2780     *
2781     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2782     */
2783    public function getRecentlyReturnedBibs(
2784        $limit = 30,
2785        $maxage = 30,
2786        $patron = null
2787    ) {
2788        $this->checkIntermittentFailure();
2789
2790        $results = $this->config['Records']['recently_returned']
2791            ?? $this->getRandomBibIds($limit);
2792        $mapper = function ($id) {
2793            return ['id' => $id];
2794        };
2795        return array_map($mapper, $results);
2796    }
2797
2798    /**
2799     * Get bib records for "trending" items (recently returned with high usage).
2800     *
2801     * @param int   $limit  Maximum number of records to retrieve (default = 30)
2802     * @param int   $maxage The maximum number of days' worth of data to examine.
2803     * @param array $patron Patron Data
2804     *
2805     * @return array
2806     */
2807    public function getTrendingBibs($limit = 30, $maxage = 30, $patron = null)
2808    {
2809        // This is similar to getRecentlyReturnedBibs for demo purposes.
2810        return $this->getRecentlyReturnedBibs($limit, $maxage, $patron);
2811    }
2812
2813    /**
2814     * Get list of users for whom the provided patron is a proxy.
2815     *
2816     * @param array $patron The patron array with username and password
2817     *
2818     * @return array
2819     *
2820     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2821     */
2822    public function getProxiedUsers(array $patron): array
2823    {
2824        return $this->config['ProxiedUsers'] ?? [];
2825    }
2826
2827    /**
2828     * Get list of users who act as proxies for the provided patron.
2829     *
2830     * @param array $patron The patron array with username and password
2831     *
2832     * @return array
2833     *
2834     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2835     */
2836    public function getProxyingUsers(array $patron): array
2837    {
2838        return $this->config['ProxyingUsers'] ?? [];
2839    }
2840
2841    /**
2842     * Provide an array of URL data (in the same format returned by the record
2843     * driver's getURLs method) for the specified bibliographic record.
2844     *
2845     * @param string $id Bibliographic record ID
2846     *
2847     * @return array
2848     */
2849    public function getUrlsForRecord(string $id): array
2850    {
2851        $links = [];
2852        if ($this->config['RecordLinks']['fakeOpacLink'] ?? false) {
2853            $links[] = [
2854                'url' => 'http://localhost/my-fake-ils?id=' . urlencode($id),
2855                'desc' => 'View in OPAC (fake demo link)',
2856            ];
2857        }
2858        return $links;
2859    }
2860}