From d01b870164793430cb612e72d803c18468f7fc8f Mon Sep 17 00:00:00 2001
From: Aleisha Amohia <aleishaamohia@hotmail.com>
Date: Thu, 1 Feb 2024 00:43:26 +0000
Subject: [PATCH] Bug 35967: Add REST API endpoint to list patron recalls

This enhancement adds a REST API endpoint to list a patron's recalls:

/api/v1/patrons/{patron_id}/recalls

This depends on the logged in patron having the manage_recalls subpermission.

To test:

1. Log in to the staff interface as your superlibrarian self (Patron A)
2. Go to Koha Administration -> Global system preferences. Enable the UseRecalls system preference
3. Set the relevant recalls circulation and fines rules
4. Search for an item (Item A)
5. Check out Item A to yourself (Patron A)
6. Log in to the OPAC as Patron B, a patron who does not have the manage_recalls permission
7. Search for Item A and request a recall
8. While still logged in to the OPAC as Patron B, hit this URL: https://your-opac-url/api/v1/patrons/patron-b-borrowernumber/recalls (swap out your URL and Patron B's borrowernumber)
9. Confirm you are given an error: "Authorization failure. Missing required permission(s)."
10. Log out of the OPAC and log back in, this time as Patron A
11. Hit the URL again https://your-opac-url/api/v1/patrons/patron-b-borrowernumber/recalls
12. Confirm you are able to view a list of Patron B's recalls
13. Confirm tests pass: t/db_dependent/api/v1/patrons_recalls.t

Sponsored-by: Auckland University of Technology
---
 Koha/REST/V1/Patrons/Recalls.pm           | 66 ++++++++++++++++++
 api/v1/swagger/definitions/recall.yaml    | 81 +++++++++++++++++++++++
 api/v1/swagger/definitions/recalls.yaml   |  5 ++
 api/v1/swagger/paths/patrons_recalls.yaml | 52 +++++++++++++++
 api/v1/swagger/swagger.yaml               |  9 +++
 t/db_dependent/api/v1/patrons_recalls.t   | 70 ++++++++++++++++++++
 6 files changed, 283 insertions(+)
 create mode 100644 Koha/REST/V1/Patrons/Recalls.pm
 create mode 100644 api/v1/swagger/definitions/recall.yaml
 create mode 100644 api/v1/swagger/definitions/recalls.yaml
 create mode 100644 api/v1/swagger/paths/patrons_recalls.yaml
 create mode 100755 t/db_dependent/api/v1/patrons_recalls.t

diff --git a/Koha/REST/V1/Patrons/Recalls.pm b/Koha/REST/V1/Patrons/Recalls.pm
new file mode 100644
index 00000000000..38c42648c14
--- /dev/null
+++ b/Koha/REST/V1/Patrons/Recalls.pm
@@ -0,0 +1,66 @@
+package Koha::REST::V1::Patrons::Recalls;
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha 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 Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use Mojo::Base 'Mojolicious::Controller';
+
+use Koha::Patrons;
+
+=head1 NAME
+
+Koha::REST::V1::Patrons::Recalls
+
+=head1 API
+
+=head2 Methods
+
+=head3 list
+
+Controller function that handles listing Koha::Recall objects for the requested patron
+
+=cut
+
+sub list {
+    my $c = shift->openapi->valid_input or return;
+
+    my $patron = Koha::Patrons->find( $c->param('patron_id') );
+
+    unless ( $patron ) {
+        return $c->render(
+            status  => 404,
+            openapi => {
+                error => 'Patron not found'
+            }
+        );
+    }
+
+    return try {
+
+        my $recalls = $c->objects->search( $patron->recalls );
+
+        return $c->render(
+            status  => 200,
+            openapi => $recalls
+        );
+    }
+    catch {
+        $c->unhandled_exception($_);
+    };
+}
+
+1;
diff --git a/api/v1/swagger/definitions/recall.yaml b/api/v1/swagger/definitions/recall.yaml
new file mode 100644
index 00000000000..5c310179aa3
--- /dev/null
+++ b/api/v1/swagger/definitions/recall.yaml
@@ -0,0 +1,81 @@
+---
+type: object
+properties:
+  recall_id:
+    type: integer
+    description: Internal recall identifier
+  patron_id:
+    type: integer
+    description: Internal patron identifier
+  created_date:
+    type:
+      - string
+      - "null"
+    format: date-time
+    description: The date the recall was requested
+  biblio_id:
+    type: integer
+    description: Internal biblio identifier
+  pickup_library_id:
+    type:
+      - string
+      - "null"
+    description: Internal library identifier for the pickup library
+  completed_date:
+    type:
+      - string
+      - "null"
+    format: date-time
+    description: The date the recall was fulfilled
+  notes:
+    type:
+      - string
+      - "null"
+    description: Notes related to this recall
+  priority:
+    type:
+      - integer
+      - "null"
+    description: Where in the queue the patron sits (not yet implemented)
+  status:
+    type:
+      - string
+      - "null"
+    description: Status of the recall
+    enum:
+      - requested
+      - overdue
+      - waiting
+      - in_transit
+      - cancelled
+      - expired
+      - fulfilled
+  timestamp:
+    type: string
+    format: date-time
+    description: Timestamp for the latest recall update
+  item_id:
+    type:
+      - integer
+      - "null"
+    description: Internal item identifier
+  waiting_date:
+    type:
+      - string
+      - "null"
+    format: date-time
+    description: The date the item was marked as waiting for the patron at the library
+  expiration_date:
+    type:
+      - string
+      - "null"
+    format: date-time
+    description: The date the recall expires
+  completed:
+    type: boolean
+    description: Controls if the recall is fulfilled
+  item_level:
+    type: boolean
+    description: If the recall is requested at item level
+
+additionalProperties: false
diff --git a/api/v1/swagger/definitions/recalls.yaml b/api/v1/swagger/definitions/recalls.yaml
new file mode 100644
index 00000000000..afe4216a5ed
--- /dev/null
+++ b/api/v1/swagger/definitions/recalls.yaml
@@ -0,0 +1,5 @@
+---
+type: array
+items:
+  $ref: "recall.yaml"
+additionalProperties: false
diff --git a/api/v1/swagger/paths/patrons_recalls.yaml b/api/v1/swagger/paths/patrons_recalls.yaml
new file mode 100644
index 00000000000..c4b2d994da6
--- /dev/null
+++ b/api/v1/swagger/paths/patrons_recalls.yaml
@@ -0,0 +1,52 @@
+---
+"/patrons/{patron_id}/recalls":
+  get:
+    x-mojo-to: Patrons::Recalls#list
+    operationId: getPatronRecalls
+    tags:
+      - recalls
+    summary: List recalls for a patron
+    parameters:
+      - $ref: "../swagger.yaml#/parameters/patron_id_pp"
+      - $ref: "../swagger.yaml#/parameters/match"
+      - $ref: "../swagger.yaml#/parameters/order_by"
+      - $ref: "../swagger.yaml#/parameters/page"
+      - $ref: "../swagger.yaml#/parameters/per_page"
+      - $ref: "../swagger.yaml#/parameters/q_param"
+      - $ref: "../swagger.yaml#/parameters/q_body"
+      - $ref: "../swagger.yaml#/parameters/request_id_header"
+    produces:
+      - application/json
+    responses:
+      "200":
+        description: The patron's recalls
+        schema:
+          type: array
+          items:
+            $ref: "../swagger.yaml#/definitions/recall"
+      "401":
+        description: Authentication required
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      "403":
+        description: Access forbidden
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      "404":
+        description: Patron not found
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      "500":
+        description: |
+          Internal server error. Possible `error_code` attribute values:
+
+          * `internal_server_error`
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      "503":
+        description: Under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+    x-koha-authorization:
+      permissions:
+        recalls: manage_recalls
diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml
index 227921d7957..563575784a3 100644
--- a/api/v1/swagger/swagger.yaml
+++ b/api/v1/swagger/swagger.yaml
@@ -140,6 +140,10 @@ definitions:
     $ref: ./definitions/preservation_processing.yaml
   quote:
     $ref: ./definitions/quote.yaml
+  recall:
+    $ref: ./definitions/recall.yaml
+  recalls:
+    $ref: ./definitions/recalls.yaml
   renewal:
     $ref: ./definitions/renewal.yaml
   renewals:
@@ -427,6 +431,8 @@ paths:
     $ref: "./paths/patrons_password.yaml#/~1patrons~1{patron_id}~1password"
   "/patrons/{patron_id}/password/expiration_date":
     $ref: "./paths/patrons_password.yaml#/~1patrons~1{patron_id}~1password~1expiration_date"
+  "/patrons/{patron_id}/recalls":
+    $ref: "./paths/patrons_recalls.yaml#/~1patrons~1{patron_id}~1recalls"
   /preservation/config:
     $ref: ./paths/preservation_config.yaml#/~1preservation~1config
   /preservation/trains:
@@ -1146,6 +1152,9 @@ tags:
   - description: "Manage quotes\n"
     name: quotes
     x-displayName: Quotes
+  - description: "Manage recalls\n"
+    name: recalls
+    x-displayName: Recalls
   - description: "Manage return claims\n"
     name: return_claims
     x-displayName: Return claims
diff --git a/t/db_dependent/api/v1/patrons_recalls.t b/t/db_dependent/api/v1/patrons_recalls.t
new file mode 100755
index 00000000000..990d8423e9b
--- /dev/null
+++ b/t/db_dependent/api/v1/patrons_recalls.t
@@ -0,0 +1,70 @@
+#!/usr/bin/env perl
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha 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 Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use Test::More tests => 1;
+use Test::Mojo;
+
+use t::lib::TestBuilder;
+use t::lib::Mocks;
+
+use Koha::Database;
+
+my $schema  = Koha::Database->new->schema;
+my $builder = t::lib::TestBuilder->new();
+
+my $t = Test::Mojo->new('Koha::REST::V1');
+
+t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
+
+subtest 'list() tests' => sub {
+
+    plan tests => 9;
+
+    $schema->storage->txn_begin;
+
+    my $patron = $builder->build_object({
+        class => 'Koha::Patrons',
+        value => { flags => 2 ** 27 } # recalls flag == 27
+    });
+    my $password = 'thePassword123';
+    $patron->set_password({ password => $password, skip_validation => 1 });
+    my $userid = $patron->userid;
+
+    $t->get_ok("//$userid:$password@/api/v1/patrons/" . $patron->id . '/recalls')
+      ->status_is( 200, 'SWAGGER3.2.2' )
+      ->json_is( [] );
+
+    my $recall_1 = $builder->build_object({ class => 'Koha::Recalls', value => { patron_id => $patron->id } });
+    my $recall_2 = $builder->build_object({ class => 'Koha::Recalls', value => { patron_id => $patron->id } });
+
+    $t->get_ok("//$userid:$password@/api/v1/patrons/" . $patron->id . '/recalls?_order_by=+me.recall_id')
+      ->status_is( 200, 'SWAGGER3.2.2' )
+      ->json_is( '' => [ $recall_1->to_api, $recall_2->to_api ], 'Recalls retrieved' );
+
+    my $non_existent_patron = $builder->build_object({ class => 'Koha::Patrons' });
+    my $non_existent_patron_id = $non_existent_patron->id;
+    # get rid of the patron
+    $non_existent_patron->delete;
+
+    $t->get_ok("//$userid:$password@/api/v1/patrons/" . $non_existent_patron_id . '/recalls')
+      ->status_is( 404 )
+      ->json_is( '/error' => 'Patron not found' );
+
+    $schema->storage->txn_rollback;
+};
-- 
2.30.2