From 93d7ba8ebca183704965e68393ed5c9aea62c0e0 Mon Sep 17 00:00:00 2001
From: Robert Lange <robert.lange@uni-leipzig.de>
Date: Tue, 23 Jun 2020 09:37:38 +0200
Subject: [PATCH] refs #17508 [master] handle ill pickup branch

* introduce PAIA options containing pickupbranch
* needed to differentiate between storageId and pickuplocation of ILL items:
** read paiaConditions from FincLibero.ini
** add method 'mapOptions' to read certain or all options of PAIA conditions

for details see issue/17508
---
 module/finc/src/finc/ILS/Driver/PAIA.php      |  74 ++
 .../fincWithAnotherPickupBranchOption.json    |  50 ++
 .../fincWithMultiplePickupBranchOptions.json  |  53 ++
 .../fincWithOnePickupBranchOption.json        |  66 ++
 .../paia/response/fincWithUnknownOption.json  |  65 ++
 .../paia/response/fincWithoutOptions.json     |  55 ++
 .../src/fincTest/ILS/Driver/PAIATest.php      | 769 ++++++++++++++++++
 7 files changed, 1132 insertions(+)
 create mode 100644 module/finc/tests/fixtures/paia/response/fincWithAnotherPickupBranchOption.json
 create mode 100644 module/finc/tests/fixtures/paia/response/fincWithMultiplePickupBranchOptions.json
 create mode 100644 module/finc/tests/fixtures/paia/response/fincWithOnePickupBranchOption.json
 create mode 100644 module/finc/tests/fixtures/paia/response/fincWithUnknownOption.json
 create mode 100644 module/finc/tests/fixtures/paia/response/fincWithoutOptions.json
 create mode 100644 module/finc/tests/unit-tests/src/fincTest/ILS/Driver/PAIATest.php

diff --git a/module/finc/src/finc/ILS/Driver/PAIA.php b/module/finc/src/finc/ILS/Driver/PAIA.php
index 579991694b2..6eb349d54e3 100644
--- a/module/finc/src/finc/ILS/Driver/PAIA.php
+++ b/module/finc/src/finc/ILS/Driver/PAIA.php
@@ -71,6 +71,11 @@ class PAIA extends \VuFind\ILS\Driver\PAIA
 
     protected $notificationsPrefix;
 
+    /**
+     * @var array
+     */
+    protected $conditions;
+
     /**
      * Constructor
      *
@@ -97,6 +102,10 @@ class PAIA extends \VuFind\ILS\Driver\PAIA
         if (isset($this->config['PAIA']['paiaNotificationsPrefix'])) {
             $this->notificationsPrefix = $this->config['PAIA']['paiaNotificationsPrefix'];
         }
+
+        if (isset($this->config['PAIA']['paiaConditions'])) {
+            $this->conditions = $this->config['PAIA']['paiaConditions'];
+        }
     }
 
     /**
@@ -972,6 +981,9 @@ class PAIA extends \VuFind\ILS\Driver\PAIA
             $result['upc'] = null;
             */
 
+            /* #17508 read optional PAIA information like pickupbranch */
+            $this->mapOptions($doc, $result);
+
             $results[] = $result;
         }
         return $results;
@@ -1511,4 +1523,66 @@ class PAIA extends \VuFind\ILS\Driver\PAIA
         // return TRUE on success
         return true;
     }
+
+    /***
+     * finds conditions in PAIA items array and adds it as options in finc converted items array
+     *
+     * Conditions according to PAIA default are only intended for request, renew or cancel
+     * BUT here used for items too, therefore should be only one option per condition
+     * see: http://gbv.github.io/paia/paia.html#conditions-and-confirmations
+     *
+     * @param array         $doc  Array of PAIA input to be mapped
+     * @param array         $result Array of PAIA output - called by reference
+     * @param array|null    $configConditions conditions to be considered (optional, if null use config)
+     *
+     * @return bool True if any option was found, otherwise false.
+     */
+    protected function mapOptions(array $doc, array &$result, array $configConditions = null): bool
+    {
+        $configConditions = $configConditions ?? $this->conditions ?? [];
+
+        foreach ($configConditions as $configCondition) {
+            if ($optionsValues = $doc["condition"][$configCondition]["option"] ?? []) {
+                $found = $result["options"][$configCondition] = $optionsValues;
+            }
+        }
+
+        return !empty($found);
+    }
+
+    /**
+     * Get (first) option for given condition and valid options from finc converted PAIA result
+     *
+     * @param string    $condition
+     * @param array     $result
+     * @param boolean   $onlyFirst
+     * @param array     $validOptions
+     *
+     * @return array of option values: each either valid id or label or null if none set
+     */
+    public function getOptions(string $condition, array $result, bool $onlyFirst, array $validOptions = null): array
+    {
+        $retval = [];
+        if (in_array($condition, $this->conditions)) {
+            $options = $result['options'][$condition] ?? [];
+            foreach ($options as $option) {
+                if (!$validOptions) {
+                    $retval[] = $option['about'] ?? $option['id'] ?? null;
+                    if ($onlyFirst) {
+                        break;
+                    }
+                } else {
+                    foreach ($validOptions as $validOption) {
+                        if (array_intersect($validOption, $option)) {
+                            $retval[] = $option['id'] ?? $option['about'] ?? null;
+                            if ($onlyFirst) {
+                                break 2;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return $retval;
+    }
 }
diff --git a/module/finc/tests/fixtures/paia/response/fincWithAnotherPickupBranchOption.json b/module/finc/tests/fixtures/paia/response/fincWithAnotherPickupBranchOption.json
new file mode 100644
index 00000000000..75a8a368417
--- /dev/null
+++ b/module/finc/tests/fixtures/paia/response/fincWithAnotherPickupBranchOption.json
@@ -0,0 +1,50 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2020 10:38:21 GMT
+Cache-Control: no-cache
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: change_password read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: read_patron
+Content-Type: application/json; charset=UTF-8
+
+{
+  "doc": [
+    {
+      "status": 3,
+      "item": "DE-Zi4:barcode:03201978",
+      "about": "Urbanität als Habitus",
+      "renewals": 3,
+      "starttime": "2020-03-13T00:00:00+01:00",
+      "endtime": "2020-07-10T00:00:00+02:00",
+      "canrenew": true
+    },
+    {
+      "status": 4,
+      "item": "DE-Zi4:barcode:80086919",
+      "about": "Fernleihe mit Abholstation",
+      "condition": {
+        "http://purl.org/ontology/paia#PickupCondition": {
+          "option": [
+            {
+              "id": "http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwZI",
+              "about": "Zweigbibliothek Zittau",
+              "amount": "2,50 EUR"
+            }
+        ]
+        }
+      },
+      "storage": "Hochschulbibliothek Görlitz",
+      "storageid": "http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwGR"
+    },
+    {
+      "status": 0,
+      "item": "ILL:13940",
+      "about": "Aushandlungen städtischer Größe - Eckert, Anna",
+      "label": "Medium im Vormerkregal",
+      "starttime": "2020-03-12T00:00:00+01:00"
+    }
+  ]
+}
diff --git a/module/finc/tests/fixtures/paia/response/fincWithMultiplePickupBranchOptions.json b/module/finc/tests/fixtures/paia/response/fincWithMultiplePickupBranchOptions.json
new file mode 100644
index 00000000000..19225a142cb
--- /dev/null
+++ b/module/finc/tests/fixtures/paia/response/fincWithMultiplePickupBranchOptions.json
@@ -0,0 +1,53 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2020 10:38:21 GMT
+Cache-Control: no-cache
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: change_password read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: read_patron
+Content-Type: application/json; charset=UTF-8
+
+{
+  "doc": [
+    {
+      "status": 3,
+      "item": "DE-Zi4:barcode:03201978",
+      "about": "Urbanität als Habitus",
+      "renewals": 3,
+      "starttime": "2020-03-13T00:00:00+01:00",
+      "endtime": "2020-07-10T00:00:00+02:00",
+      "canrenew": true
+    },
+    {
+      "status": 4,
+      "item": "DE-Zi4:barcode:80086919",
+      "about": "Fernleihe mit Abholstation",
+      "condition": {
+        "http://purl.org/ontology/paia#PickupCondition": {
+          "option": [
+            {
+              "id": "http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwZI",
+              "about": "Zweigbibliothek Zittau"
+            },
+            {
+              "id": "http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwGR",
+              "about": "Zweigbibliothek Görlitz"
+            }
+        ]
+        }
+      },
+      "storage": "Hochschulbibliothek Görlitz",
+      "storageid": "http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwGR"
+    },
+    {
+      "status": 0,
+      "item": "ILL:13940",
+      "about": "Aushandlungen städtischer Größe - Eckert, Anna",
+      "label": "Medium im Vormerkregal",
+      "starttime": "2020-03-12T00:00:00+01:00"
+    }
+  ]
+}
diff --git a/module/finc/tests/fixtures/paia/response/fincWithOnePickupBranchOption.json b/module/finc/tests/fixtures/paia/response/fincWithOnePickupBranchOption.json
new file mode 100644
index 00000000000..c00ed7217e7
--- /dev/null
+++ b/module/finc/tests/fixtures/paia/response/fincWithOnePickupBranchOption.json
@@ -0,0 +1,66 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2020 10:38:21 GMT
+Cache-Control: no-cache
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: change_password read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: read_patron
+Content-Type: application/json; charset=UTF-8
+
+{
+  "doc": [
+    {
+      "status": 4,
+      "item": "DE-Zi4:barcode:80086919",
+      "about": "Fernleihe mit Abholstation",
+      "condition": {
+        "http://purl.org/ontology/paia#PickupCondition": {
+          "option": [
+            {
+              "id": "http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwGR",
+              "about": "Zweigbibliothek Görlitz",
+              "amount": "2.50 EUR"
+            }
+        ]
+        }
+      },
+      "storage": "Hochschulbibliothek Zittau",
+      "storageid": "http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwZI"
+    },
+    {
+      "status": 3,
+      "item": "DE-Zi4:barcode:02415857",
+      "about": "Die Eigenlogik der Städte",
+      "renewals": 3,
+      "starttime": "2020-03-13T00:00:00+01:00",
+      "endtime": "2020-07-10T00:00:00+02:00",
+      "canrenew": true
+    },
+    {
+      "status": 4,
+      "item": "DE-Zi4:barcode:03039724",
+      "about": "Stadtsoziologie",
+      "storage": "Hochschulbibliothek Zittau",
+      "storageid": "http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwZI"
+    },
+    {
+      "status": 3,
+      "item": "DE-Zi4:barcode:03201978",
+      "about": "Urbanität als Habitus",
+      "renewals": 3,
+      "starttime": "2020-03-13T00:00:00+01:00",
+      "endtime": "2020-07-10T00:00:00+02:00",
+      "canrenew": true
+    },
+    {
+      "status": 0,
+      "item": "ILL:13940",
+      "about": "Aushandlungen städtischer Größe - Eckert, Anna",
+      "label": "Medium im Vormerkregal",
+      "starttime": "2020-03-12T00:00:00+01:00"
+    }
+  ]
+}
diff --git a/module/finc/tests/fixtures/paia/response/fincWithUnknownOption.json b/module/finc/tests/fixtures/paia/response/fincWithUnknownOption.json
new file mode 100644
index 00000000000..e3120951b9c
--- /dev/null
+++ b/module/finc/tests/fixtures/paia/response/fincWithUnknownOption.json
@@ -0,0 +1,65 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2020 10:38:21 GMT
+Cache-Control: no-cache
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: change_password read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: read_patron
+Content-Type: application/json; charset=UTF-8
+
+{
+  "doc": [
+    {
+      "status": 4,
+      "item": "DE-Zi4:barcode:80086919",
+      "about": "Fernleihe mit Abholstation",
+      "condition": {
+        "http://purl.org/ontology/paia#PickupCondition": {
+          "option": [
+            {
+              "id": "http://data.ub.uni-leipzig.de/resource/DE-Zi4/unknownLocation",
+              "about": "Zweigbibliothek Neu aber noch nicht bekannt / konfiguriert => soll nicht angezeigt werden"
+            }
+        ]
+        }
+      },
+      "storage": "Hochschulbibliothek Zittau",
+      "storageid": "http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwZI"
+    },
+    {
+      "status": 3,
+      "item": "DE-Zi4:barcode:02415857",
+      "about": "Die Eigenlogik der Städte",
+      "renewals": 3,
+      "starttime": "2020-03-13T00:00:00+01:00",
+      "endtime": "2020-07-10T00:00:00+02:00",
+      "canrenew": true
+    },
+    {
+      "status": 4,
+      "item": "DE-Zi4:barcode:03039724",
+      "about": "Stadtsoziologie",
+      "storage": "Hochschulbibliothek Zittau",
+      "storageid": "http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwZI"
+    },
+    {
+      "status": 3,
+      "item": "DE-Zi4:barcode:03201978",
+      "about": "Urbanität als Habitus",
+      "renewals": 3,
+      "starttime": "2020-03-13T00:00:00+01:00",
+      "endtime": "2020-07-10T00:00:00+02:00",
+      "canrenew": true
+    },
+    {
+      "status": 0,
+      "item": "ILL:13940",
+      "about": "Aushandlungen städtischer Größe - Eckert, Anna",
+      "label": "Medium im Vormerkregal",
+      "starttime": "2020-03-12T00:00:00+01:00"
+    }
+  ]
+}
diff --git a/module/finc/tests/fixtures/paia/response/fincWithoutOptions.json b/module/finc/tests/fixtures/paia/response/fincWithoutOptions.json
new file mode 100644
index 00000000000..f75b1602ac8
--- /dev/null
+++ b/module/finc/tests/fixtures/paia/response/fincWithoutOptions.json
@@ -0,0 +1,55 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2020 10:38:21 GMT
+Cache-Control: no-cache
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: change_password read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: read_patron
+Content-Type: application/json; charset=UTF-8
+
+{
+  "doc": [
+    {
+      "status": 4,
+      "item": "DE-Zi4:barcode:80086919",
+      "about": "Fernleihe mit Abholstation",
+      "storage": "Hochschulbibliothek Zittau",
+      "storageid": "http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwZI"
+    },
+    {
+      "status": 3,
+      "item": "DE-Zi4:barcode:02415857",
+      "about": "Die Eigenlogik der Städte",
+      "renewals": 3,
+      "starttime": "2020-03-13T00:00:00+01:00",
+      "endtime": "2020-07-10T00:00:00+02:00",
+      "canrenew": true
+    },
+    {
+      "status": 4,
+      "item": "DE-Zi4:barcode:03039724",
+      "about": "Stadtsoziologie",
+      "storage": "Hochschulbibliothek Zittau",
+      "storageid": "http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwZI"
+    },
+    {
+      "status": 3,
+      "item": "DE-Zi4:barcode:03201978",
+      "about": "Urbanität als Habitus",
+      "renewals": 3,
+      "starttime": "2020-03-13T00:00:00+01:00",
+      "endtime": "2020-07-10T00:00:00+02:00",
+      "canrenew": true
+    },
+    {
+      "status": 0,
+      "item": "ILL:13940",
+      "about": "Aushandlungen städtischer Größe - Eckert, Anna",
+      "label": "Medium im Vormerkregal",
+      "starttime": "2020-03-12T00:00:00+01:00"
+    }
+  ]
+}
diff --git a/module/finc/tests/unit-tests/src/fincTest/ILS/Driver/PAIATest.php b/module/finc/tests/unit-tests/src/fincTest/ILS/Driver/PAIATest.php
new file mode 100644
index 00000000000..392a0b27049
--- /dev/null
+++ b/module/finc/tests/unit-tests/src/fincTest/ILS/Driver/PAIATest.php
@@ -0,0 +1,769 @@
+<?php
+/**
+ * ILS driver test
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Leipzig University Library 2020.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Robert Lange <lange@ub.uni-leipzig.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Page
+ */
+namespace VuFindTest\ILS\Driver;
+
+use finc\ILS\Driver\PAIA as fincPAIA;
+use InvalidArgumentException;
+use Zend\Http\Client\Adapter\Test as TestAdapter;
+use Zend\Http\Response as HttpResponse;
+
+/**
+ * ILS driver test
+ *
+ * @category VuFind
+ * @package  FincTest
+ * @author   Robert Lange <lange@ub.uni-leipzig.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Page
+ */
+class PAIAFincTest extends \VuFindTest\ILS\Driver\PAIATest
+{
+    public static $pickupCondition = 'http://purl.org/ontology/paia#PickupCondition';
+
+    public static $validConfigWithPickUpOption = [
+        'DAIA' =>
+            [
+                'baseUrl'           => 'http://daia.gbv.de/',
+            ],
+        'PAIA' =>
+            [
+                'baseUrl'           => 'http://paia.gbv.de/',
+                'paiaConditions'    =>
+                    [
+                        'http://purl.org/ontology/paia#PickupCondition'
+                    ]
+            ]
+    ];
+
+    public static $validConfigWithOptionForOtherInstance = [
+        'DAIA' =>
+            [
+                'baseUrl'           => 'http://daia.gbv.de/',
+            ],
+        'PAIA' =>
+            [
+                'baseUrl'           => 'http://paia.gbv.de/',
+                'paiaConditions'    =>
+                    [
+                        'http://purl.org/ontology/paia#PickupCondition' =>  'http://data.ub.uni-leipzig.de/resource/DE-15/department/wrongPickup'
+                    ]
+            ]
+    ];
+
+
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testMapOptionsPickupBranchTrue()
+    {
+        list($method, $paia) = $this->prepareByReflection();
+
+        $pickUpId       = 'http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwGR';
+        $pickUpAbout    = 'Zweigbibliothek Görlitz';
+        $input  = [
+            'condition' => [
+                self::$pickupCondition => [
+                    'option' => [
+                        0 => [
+                            'id'    => $pickUpId,
+                            'about' => $pickUpAbout
+                        ]
+                    ]
+                ]
+            ]
+        ];
+
+
+        $conditions = [
+            self::$pickupCondition
+        ];
+
+        $output = [];
+        $method->invokeArgs($paia, [$input, &$output, $conditions]);
+
+        $this->assertEquals($pickUpId, $output['options'][self::$pickupCondition][0]['id'],
+            "Wrong pickup branch id!"
+        );
+
+        $this->assertEquals($pickUpAbout, $output['options'][self::$pickupCondition][0]['about'],
+            "Wrong pickup branch label!"
+        );
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testMapOptionsNewTrue()
+    {
+        list($method, $paia) = $this->prepareByReflection();
+
+        $pickUpId       = 'http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwGR';
+        $pickUpAbout    = 'Zweigbibliothek Görlitz';
+        $newOptionId    = 'http://data.ub.uni-leipzig.de/resource/DE-Zi4/newOption';
+        $newOption      = 'a new option within new condition';
+        $input  = [
+            'condition' => [
+                self::$pickupCondition => [
+                    'option' => [
+                        0 => [
+                            'id'    => $pickUpId,
+                            'about' => $pickUpAbout
+                        ]
+                    ]
+                ],
+                'http://purl.org/ontology/paia#newCondition' => [
+                    'option' => [
+                        0 => [
+                            'id' => $newOptionId,
+                            'about' => $newOption
+                        ]
+                    ]
+                ]
+            ]
+        ];
+
+        $conditions = [
+            self::$pickupCondition,
+            'http://purl.org/ontology/paia#newCondition'
+        ];
+
+        $output = [];
+        $method->invokeArgs($paia, [$input, &$output, $conditions]);
+        $this->assertEquals($pickUpId, $output['options'][self::$pickupCondition][0]['id'],
+            "Wrong pickup branch id!"
+        );
+        $this->assertEquals($pickUpAbout, $output['options'][self::$pickupCondition][0]['about'],
+            "Wrong pickup branch label!"
+        );
+        $this->assertEquals($newOptionId, $output['options']['http://purl.org/ontology/paia#newCondition'][0]['id'],
+            "New option id missing!"
+        );
+        $this->assertEquals($newOption, $output['options']['http://purl.org/ontology/paia#newCondition'][0]['about'],
+            "New option label missing!"
+        );
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testMapOptionsPickupBranchFalseNoConditions()
+    {
+        list($method, $paia) = $this->prepareByReflection();
+
+        $pickUpBranch = 'Zweigbibliothek Görlitz';
+        $input  = [
+            'condition' => [
+                self::$pickupCondition => [
+                    'option' => [
+                        0 => [
+                            'id' => 'http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwGR',
+                            'about' => $pickUpBranch
+                        ]
+                    ]
+                ]
+            ]
+        ];
+
+        $output = [];
+        $method->invokeArgs($paia, [$input, &$output, []]);
+        $this->assertFalse(isset($output["options"]),
+            "Unknown option was mapped"
+        );
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testMapOptionsPickupBranchTrueUnknownOptionId()
+    {
+        /* validate new options otherwise, e.g. by checking $presentationPickups later */
+        list($method, $paia) = $this->prepareByReflection();
+
+        $pickUpBranch = 'Zweigbibliothek Görlitz';
+        $input  = [
+            'condition' => [
+                self::$pickupCondition => [
+                    'option' => [
+                        0 => [
+                            'id' => 'http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/fake',
+                            'about' => $pickUpBranch
+                        ]
+                    ]
+                ]
+            ]
+        ];
+
+        $conditions = [
+            self::$pickupCondition
+        ];
+
+        $output = [];
+        $method->invokeArgs($paia, [$input, &$output, $conditions]);
+        $this->assertTrue(isset($output["options"]),
+            "Unknown option was not mapped"
+        );
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testGetMyHoldsWithoutOptionConfigurationDefined()
+    {
+        $conn = $this->createConnector('fincWithOnePickupBranchOption.json');
+        /* default config */
+        $conn->setConfig($this->validConfig);
+        $conn->init();
+        $result = $conn->getMyHolds($this->patron);
+
+        $filtered = array_filter(
+            $result,
+            function ($resultItem) {
+                if (isset($resultItem['options'])) {
+                    return true;
+                }
+            }
+        );
+
+        /* no option or pickup branch */
+        $this->assertTrue(count($filtered) == 0,
+            "Two many items with options!"
+        );
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testGetMyHoldsWithoutPaiaResponseOptions()
+    {
+        $conn = $this->createConnector('fincWithoutOptions.json');
+        $conn->setConfig(self::$validConfigWithPickUpOption);
+        $conn->init();
+        $result = $conn->getMyHolds($this->patron);
+
+        $filtered = array_filter(
+            $result,
+            function ($resultItem) {
+                if (isset($resultItem['options'])) {
+                    return true;
+                }
+            }
+        );
+
+        /* no option nor pickup branch */
+        $this->assertTrue(count($filtered) == 0,
+            "Two many items with options!"
+        );
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testGetMyHoldsAllowUnknownOptions()
+    {
+        $conn = $this->createConnector('fincWithUnknownOption.json');
+        /* default config */
+        $conn->setConfig(self::$validConfigWithPickUpOption);
+        $conn->init();
+        $result = $conn->getMyHolds($this->patron);
+
+        $filtered = array_filter(
+            $result,
+            function ($resultItem) {
+                if (isset($resultItem['options'])) {
+                    return true;
+                }
+            }
+        );
+
+        /* no option */
+        $this->assertTrue(count($filtered) > 0,
+            "Two many items with options!"
+        );
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testGetMyHoldsWrongInstance()
+    {
+        $conn = $this->createConnector('fincWithOnePickupBranchOption.json');
+        /* default config */
+        $conn->setConfig(self::$validConfigWithOptionForOtherInstance);
+        $conn->init();
+        $result = $conn->getMyHolds($this->patron);
+
+        $filtered = array_filter(
+            $result,
+            function ($resultItem) {
+                if (isset($resultItem['options'])) {
+                    return true;
+                }
+            }
+        );
+
+        /* no option */
+        $this->assertTrue(count($filtered) == 0,
+            "Two many items with options!"
+        );
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testGetMyHoldsWithPickUpBranch()
+    {
+        $conn = $this->createConnector('fincWithOnePickupBranchOption.json');
+        $conn->setConfig(self::$validConfigWithPickUpOption);
+        $conn->init();
+        $result = $conn->getMyHolds($this->patron);
+
+        $filtered = array_filter(
+            $result,
+            function ($resultItem) {
+                if (isset($resultItem['options'])) {
+                    return true;
+                }
+            }
+        );
+
+        /* check pickupbranch */
+        $this->assertTrue(count($filtered[0]["options"][self::$pickupCondition]) > 0,
+            "No pickup branch mapped!");
+
+        $this->assertEquals("Zweigbibliothek Görlitz", $filtered[0]["options"][self::$pickupCondition][0]["about"],
+            "Wrong pickup branch!"
+        );
+
+        /* but keep storage id (location) */
+        $this->assertEquals("Hochschulbibliothek Zittau", $filtered[0]["location"],
+            "Wrong pickup branch!"
+        );
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testGetMyHoldsWithAnotherPickUpBranch()
+    {
+        $conn = $this->createConnector('fincWithAnotherPickupBranchOption.json');
+        $conn->setConfig(self::$validConfigWithPickUpOption);
+        $conn->init();
+        $result = $conn->getMyHolds($this->patron);
+
+        $filtered = array_filter(
+            $result,
+            function ($resultItem) {
+                if (isset($resultItem['options'])) {
+                    return true;
+                }
+            }
+        );
+
+        /* check pickupbranch */
+        $this->assertTrue(count($filtered[0]["options"][self::$pickupCondition]) > 0,
+            "No pickup branch mapped!");
+
+        $this->assertEquals("http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwZI", $filtered[0]["options"][self::$pickupCondition][0]["id"],
+            "Wrong pickup branch id!"
+        );
+
+        $this->assertEquals("Zweigbibliothek Zittau", $filtered[0]["options"][self::$pickupCondition][0]["about"],
+            "Wrong pickup branch label!"
+        );
+
+        $this->assertEquals("2,50 EUR", $filtered[0]["options"][self::$pickupCondition][0]["amount"],
+            "Wrong pickup branch amount!"
+        );
+
+        /* but keep storage id (location) */
+        $this->assertEquals("Hochschulbibliothek Görlitz", $filtered[0]["location"],
+            "Wrong pickup branch!"
+        );
+    }
+
+    /**
+     * Test Multiple Options => use first option when displaying items
+     *
+     * @return void
+     */
+    public function testGetMyHoldsWithMultipleOptionsPickUpBranch()
+    {
+        $conn = $this->createConnector('fincWithMultiplePickupBranchOptions.json');
+        $conn->setConfig(self::$validConfigWithPickUpOption);
+        $conn->init();
+        $result = $conn->getMyHolds($this->patron);
+
+        $filtered = array_filter(
+            $result,
+            function ($resultItem) {
+                if (isset($resultItem['options'])) {
+                    return true;
+                }
+            }
+        );
+
+        /* check pickupbranch */
+        $this->assertTrue(count($filtered) == 1,
+            "No pickup branch mapped!");
+
+        $this->assertEquals("http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwZI", $filtered[0]["options"][self::$pickupCondition][0]["id"],
+            "Wrong pickup branch id!"
+        );
+
+        $this->assertEquals("Zweigbibliothek Zittau", $filtered[0]["options"][self::$pickupCondition][0]["about"],
+            "Wrong pickup branch label!"
+        );
+
+        $this->assertEquals("http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwGR", $filtered[0]["options"][self::$pickupCondition][1]["id"],
+            "Wrong pickup branch id!"
+        );
+
+        $this->assertEquals("Zweigbibliothek Görlitz", $filtered[0]["options"][self::$pickupCondition][1]["about"],
+            "Wrong pickup branch label!"
+        );
+
+        /* but keep storage id (location) */
+        $this->assertEquals("Hochschulbibliothek Görlitz", $filtered[0]["location"],
+            "Wrong pickup branch!"
+        );
+    }
+
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testgetOptionsReturnsIDTrue()
+    {
+        $conn = $this->createConnector('fincWithMultiplePickupBranchOptions.json');
+        $conn->setConfig(self::$validConfigWithPickUpOption);
+        $conn->init();
+        $options['options'][self::$pickupCondition][0]['id'] = 'TESTID';
+
+        $this->assertEquals(
+            ['TESTID'],
+            $conn->getOptions(self::$pickupCondition, $options, true),
+            "Wrong option value"
+        );
+
+        $validOptions[0][0] = 'TESTID';
+        $this->assertEquals(
+            ['TESTID'],
+            $conn->getOptions(self::$pickupCondition, $options, true, $validOptions),
+            "Wrong option value"
+        );
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testgetOptionsNotOnlyFirstButAll()
+    {
+        $conn = $this->createConnector('fincWithMultiplePickupBranchOptions.json');
+        $conn->setConfig(self::$validConfigWithPickUpOption);
+        $conn->init();
+        $options['options'][self::$pickupCondition][0]['id'] = 'TESTID1';
+        $options['options'][self::$pickupCondition][1]['id'] = 'TESTID2';
+
+        $this->assertEquals(
+            ['TESTID1', 'TESTID2'],
+            $conn->getOptions(self::$pickupCondition, $options, false),
+            "Wrong option value"
+        );
+
+        $this->assertEquals(
+            ['TESTID1', 'TESTID2'],
+            $conn->getOptions(self::$pickupCondition, $options, false, $validOptions),
+            "Wrong option value"
+        );
+
+        /* first option only label set */
+        $options['options'][self::$pickupCondition][0]['id']    = null;
+        $options['options'][self::$pickupCondition][0]['about'] = 'TESTLABEL1';
+        $options['options'][self::$pickupCondition][1]['id']    = 'TESTID2';
+
+        $this->assertEquals(
+            ['TESTLABEL1', 'TESTID2'],
+            $conn->getOptions(self::$pickupCondition, $options, false, $validOptions),
+            "Wrong option value"
+        );
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testgetOptionsReturnsLabelTrue()
+    {
+        $conn = $this->createConnector('fincWithMultiplePickupBranchOptions.json');
+        $conn->setConfig(self::$validConfigWithPickUpOption);
+        $conn->init();
+        $options['options'][self::$pickupCondition][0]['id'] = null;
+        $options['options'][self::$pickupCondition][0]['amount'] = '2,50 Euro';
+        $options['options'][self::$pickupCondition][0]['about'] = 'use label when id missing';
+
+        $this->assertEquals(
+            ['use label when id missing'],
+            $conn->getOptions(self::$pickupCondition, $options, true),
+            "Wrong option value!"
+        );
+
+        /* also return label instead of id when no valid options are given */
+        $options['options'][self::$pickupCondition][0]['id'] = 'http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwGR';
+        $options['options'][self::$pickupCondition][0]['about'] = 'Take label, if parameter valid Options missing';
+        $this->assertEquals(
+            ['Take label, if parameter valid Options missing'],
+            $conn->getOptions(self::$pickupCondition, $options, true),
+            "Wrong option value!"
+        );
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testgetOptionsReturnsEmptyArray()
+    {
+        $conn = $this->createConnector('fincWithMultiplePickupBranchOptions.json');
+        $conn->setConfig(self::$validConfigWithPickUpOption);
+        $conn->init();
+
+        $options['options'][self::$pickupCondition][0]['id'] = 'http://data.ub.uni-leipzig.de/resource/DE-Zi4/department/zwGR';
+        $options['options'][self::$pickupCondition][0]['about'] = 'Zweigbibliothek Görlitz';
+
+        /* invalid condition */
+        $this->assertEquals(
+            [],
+            $conn->getOptions('fakeCondition', $options, true),
+            "Wrong option value!"
+        );
+
+        /* no options in result array */
+        $this->assertEquals(
+            [],
+            $conn->getOptions('fakeCondition', [], true),
+            "Wrong option value!"
+        );
+
+        /* invalid option */
+        $validOptions[0][0] = 'xxx';
+        $this->assertEquals(
+            [],
+            $conn->getOptions(self::$pickupCondition, $options, true, $validOptions),
+            "Wrong option value!"
+        );
+
+        /* no option values */
+        $options['options'][self::$pickupCondition][0]['id'] = null;
+        $options['options'][self::$pickupCondition][0]['about'] = null;
+        $this->assertEquals(
+            [""],
+            $conn->getOptions(self::$pickupCondition, $options, true),
+            "Wrong option value!"
+        );
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testCallMapOptionsTrue() : void
+    {
+        $paia = $this->getMockBuilder(PAIAMock::class)
+            ->enableProxyingToOriginalMethods()
+            ->setMethods(['mapOptions'])
+            ->setConstructorArgs([new \VuFind\Date\Converter(), new \Zend\Session\SessionManager()])
+            ->getMock();
+
+        $paia->prepare("fincWithOnePickupBranchOption.json");
+
+        $paia->expects($this->atLeastOnce())
+            ->method('mapOptions');
+
+        /* only used by holdings so far */
+        $paia->getMyHolds($this->patron);
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testCallMapOptionsFalse() : void
+    {
+        $paia = $this->getMockBuilder(PAIAMock::class)
+            ->enableProxyingToOriginalMethods()
+            ->setMethods(['mapOptions'])
+            ->setConstructorArgs([new \VuFind\Date\Converter(), new \Zend\Session\SessionManager()])
+            ->getMock();
+
+        $paia->prepare("fincWithOnePickupBranchOption.json");
+
+        $paia->expects($this->never())
+            ->method('mapOptions');
+
+        $paia->getMyILLRequests($this->patron);
+        $paia->getMyFines($this->patron);
+        $paia->getMyStorageRetrievalRequests($this->patron);
+        $paia->getMyTransactions($this->patron);
+    }
+
+    /**
+     * Create connector with fixture file.
+     *
+     * @param string $fixture Fixture file
+     *
+     * @return Connector
+     *
+     * @throws InvalidArgumentException Fixture file does not exist
+     */
+    protected function createConnector($fixture = null)
+    {
+        if (empty($fixture) || strpos($fixture, 'finc') === false) {
+            return parent::createConnector($fixture);
+        }
+
+        $adapter = new TestAdapter();
+        if ($fixture) {
+            $file = realpath(
+                __DIR__ .
+                '/../../../../../fixtures/paia/response/' . $fixture
+            );
+            if (!is_string($file) || !file_exists($file) || !is_readable($file)) {
+                throw new InvalidArgumentException(
+                    sprintf('Unable to load fixture file: %s ', $file)
+                );
+            }
+            $response = file_get_contents($file);
+            $responseObj = HttpResponse::fromString($response);
+            $adapter->setResponse($responseObj);
+        }
+        $service = new \VuFindHttp\HttpService();
+        $service->setDefaultAdapter($adapter);
+        $conn = new PAIAMock(
+            new \VuFind\Date\Converter(),
+            new \Zend\Session\SessionManager()
+        );
+        $conn->setHttpService($service);
+        return $conn;
+    }
+
+    /**
+     * @return array
+     * @throws \ReflectionException
+     */
+    protected function prepareByReflection()
+    {
+        /* use reflection due to protected access level */
+        $class = new \ReflectionClass('finc\ILS\Driver\PAIA');
+        $method = $class->getMethod("mapOptions");
+        $method->setAccessible(true);
+
+        $paia = new fincPAIA(
+            new \VuFind\Date\Converter(),
+            new \Zend\Session\SessionManager()
+        );
+
+        return [$method, $paia];
+    }
+}
+
+/* helper class to make protected finc methods public and reuse Vufind mocking approach */
+class PAIAMock extends fincPAIA
+{
+    public function prepare($fixture)
+    {
+        $adapter = new TestAdapter();
+        if ($fixture) {
+            $file = realpath(
+                __DIR__ .
+                '/../../../../../fixtures/paia/response/' . $fixture
+            );
+            if (!is_string($file) || !file_exists($file) || !is_readable($file)) {
+                throw new InvalidArgumentException(
+                    sprintf('Unable to load fixture file: %s ', $file)
+                );
+            }
+            $response = file_get_contents($file);
+            $responseObj = HttpResponse::fromString($response);
+            $adapter->setResponse($responseObj);
+        }
+        $service = new \VuFindHttp\HttpService();
+        $service->setDefaultAdapter($adapter);
+        $this->setHttpService($service);
+        $this->setConfig(PAIAFincTest::$validConfigWithPickUpOption);
+        parent::init();
+    }
+
+    public function parsePAIAJson($file)
+    {
+        return $this->paiaParseJsonAsArray($file);
+    }
+
+    public function myHoldsMapping($items)
+    {
+        return parent::myHoldsMapping($items);
+    }
+
+    public function paiaGetItems($patron, $filter = [])
+    {
+        return parent::paiaGetItems($patron, $filter);
+    }
+
+    public function paiaCheckScope($scope)
+    {
+        return true;
+    }
+}
-- 
GitLab