--- /dev/null
+package Koha::REST::V1::Reserve;
+
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use Modern::Perl;
+
+use Mojo::Base 'Mojolicious::Controller';
+
+use C4::Biblio;
+use C4::Reserves;
+
+use Koha::Patrons;
+use Koha::DateUtils;
+
+sub list {
+ my ($c, $args, $cb) = @_;
+
+ my $borrowernumber = $c->param('borrowernumber');
+ my $borrower = Koha::Patrons->find($borrowernumber);
+ unless ($borrower) {
+ return $c->$cb({error => "Borrower not found"}, 404);
+ }
+
+ my @reserves = C4::Reserves::GetReservesFromBorrowernumber($borrowernumber);
+
+ return $c->$cb(\@reserves, 200);
+}
+
+sub add {
+ my ($c, $args, $cb) = @_;
+
+ my $body = $c->req->json;
+
+ my $borrowernumber = $body->{borrowernumber};
+ my $biblionumber = $body->{biblionumber};
+ my $itemnumber = $body->{itemnumber};
+ my $branchcode = $body->{branchcode};
+ my $expirationdate = $body->{expirationdate};
+ my $borrower = Koha::Patrons->find($borrowernumber);
+ unless ($borrower) {
+ return $c->$cb({error => "Borrower not found"}, 404);
+ }
+
+ unless ($biblionumber or $itemnumber) {
+ return $c->$cb({
+ error => "At least one of biblionumber, itemnumber should be given"
+ }, 400);
+ }
+ unless ($branchcode) {
+ return $c->$cb({
+ error => "Branchcode is required"
+ }, 400);
+ }
+
+ if ($itemnumber) {
+ my $item_biblionumber = C4::Biblio::GetBiblionumberFromItemnumber($itemnumber);
+ if ($biblionumber and $biblionumber != $item_biblionumber) {
+ return $c->$cb({
+ error => "Item $itemnumber doesn't belong to biblio $biblionumber"
+ }, 400);
+ }
+ $biblionumber ||= $item_biblionumber;
+ }
+
+ my $biblio = C4::Biblio::GetBiblio($biblionumber);
+
+ my $can_reserve =
+ $itemnumber
+ ? CanItemBeReserved( $borrowernumber, $itemnumber )
+ : CanBookBeReserved( $borrowernumber, $biblionumber );
+
+ unless ($can_reserve eq 'OK') {
+ return $c->$cb({
+ error => "Reserve cannot be placed. Reason: $can_reserve"
+ }, 403);
+ }
+
+ my $priority = C4::Reserves::CalculatePriority($biblionumber);
+ $itemnumber ||= undef;
+
+ # AddReserve expects date to be in syspref format
+ if ($expirationdate) {
+ $expirationdate = output_pref(dt_from_string($expirationdate, 'iso'));
+ }
+
+ my $reserve_id = C4::Reserves::AddReserve($branchcode, $borrowernumber,
+ $biblionumber, undef, $priority, undef, $expirationdate, undef,
+ $biblio->{title}, $itemnumber);
+
+ unless ($reserve_id) {
+ return $c->$cb({
+ error => "Error while placing reserve. See Koha logs for details."
+ }, 500);
+ }
+
+ my $reserve = C4::Reserves::GetReserve($reserve_id);
+
+ return $c->$cb($reserve, 201);
+}
+
+sub edit {
+ my ($c, $args, $cb) = @_;
+
+ my $reserve_id = $args->{reserve_id};
+ my $reserve = C4::Reserves::GetReserve($reserve_id);
+
+ unless ($reserve) {
+ return $c->$cb({error => "Reserve not found"}, 404);
+ }
+
+ my $body = $c->req->json;
+
+ my $branchcode = $body->{branchcode};
+ my $priority = $body->{priority};
+ my $suspend_until = $body->{suspend_until};
+
+ if ($suspend_until) {
+ $suspend_until = output_pref(dt_from_string($suspend_until, 'iso'));
+ }
+
+ my $params = {
+ reserve_id => $reserve_id,
+ branchcode => $branchcode,
+ rank => $priority,
+ suspend_until => $suspend_until,
+ };
+ C4::Reserves::ModReserve($params);
+ $reserve = C4::Reserves::GetReserve($reserve_id);
+
+ return $c->$cb($reserve, 200);
+}
+
+sub delete {
+ my ($c, $args, $cb) = @_;
+
+ my $reserve_id = $args->{reserve_id};
+ my $reserve = C4::Reserves::GetReserve($reserve_id);
+
+ unless ($reserve) {
+ return $c->$cb({error => "Reserve not found"}, 404);
+ }
+
+ C4::Reserves::CancelReserve({ reserve_id => $reserve_id });
+
+ return $c->$cb({}, 200);
+}
+
+1;
{
"patron": { "$ref": "patron.json" },
+ "reserves": { "$ref": "reserves.json" },
+ "reserve": { "$ref": "reserve.json" },
"error": { "$ref": "error.json" }
}
--- /dev/null
+{
+ "type": "object",
+ "properties": {
+ "reserve_id": {
+ "description": "Internal reserve identifier"
+ },
+ "borrowernumber": {
+ "type": "string",
+ "description": "internally assigned user identifier"
+ },
+ "reservedate": {
+ "description": "the date the reserve was placed"
+ },
+ "biblionumber": {
+ "type": "string",
+ "description": "internally assigned biblio identifier"
+ },
+ "branchcode": {
+ "type": ["string", "null"],
+ "description": "internally assigned branch identifier"
+ },
+ "notificationdate": {
+ "description": "currently unused"
+ },
+ "reminderdate": {
+ "description": "currently unused"
+ },
+ "cancellationdate": {
+ "description": "the date the reserve was cancelled"
+ },
+ "reservenotes": {
+ "description": "notes related to this reserve"
+ },
+ "priority": {
+ "description": "where in the queue the patron sits"
+ },
+ "found": {
+ "description": "a one letter code defining what the status of the reserve is after it has been confirmed"
+ },
+ "timestamp": {
+ "description": "date and time the reserve was last updated"
+ },
+ "itemnumber": {
+ "type": ["string", "null"],
+ "description": "internally assigned item identifier"
+ },
+ "waitingdate": {
+ "description": "the date the item was marked as waiting for the patron at the library"
+ },
+ "expirationdate": {
+ "description": "the date the reserve expires"
+ },
+ "lowestPriority": {
+ "description": ""
+ },
+ "suspend": {
+ "description": ""
+ },
+ "suspend_until": {
+ "description": ""
+ }
+ }
+}
--- /dev/null
+{
+ "type": "array",
+ "items": { "$ref": "reserve.json" }
+}
}
}
}
+ },
+ "/reserves": {
+ "get": {
+ "operationId": "listReserves",
+ "tags": ["borrowers", "reserves"],
+ "parameters": [
+ {
+ "name": "borrowernumber",
+ "in": "query",
+ "description": "Internal borrower identifier",
+ "required": true,
+ "type": "integer"
+ }
+ ],
+ "produces": ["application/json"],
+ "responses": {
+ "200": {
+ "description": "A list of reserves",
+ "schema": { "$ref": "#/definitions/reserves" }
+ },
+ "404": {
+ "description": "Borrower not found",
+ "schema": { "$ref": "#/definitions/error" }
+ }
+ }
+ },
+ "post": {
+ "operationId": "addReserve",
+ "tags": ["borrowers", "reserves"],
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "description": "A JSON object containing informations about the new reserve",
+ "required": true,
+ "schema": {
+ "type": "object",
+ "properties": {
+ "borrowernumber": {
+ "description": "Borrower internal identifier",
+ "type": "integer"
+ },
+ "biblionumber": {
+ "description": "Biblio internal identifier",
+ "type": "integer"
+ },
+ "itemnumber": {
+ "description": "Item internal identifier",
+ "type": "integer"
+ },
+ "branchcode": {
+ "description": "Pickup location",
+ "type": "string"
+ },
+ "expirationdate": {
+ "description": "Reserve end date",
+ "type": "string",
+ "format": "date"
+ }
+ }
+ }
+ }
+ ],
+ "consumes": ["application/json"],
+ "produces": ["application/json"],
+ "responses": {
+ "201": {
+ "description": "Created reserve",
+ "schema": { "$ref": "#/definitions/reserve" }
+ },
+ "400": {
+ "description": "Missing or wrong parameters",
+ "schema": { "$ref": "#/definitions/error" }
+ },
+ "403": {
+ "description": "Reserve not allowed",
+ "schema": { "$ref": "#/definitions/error" }
+ },
+ "404": {
+ "description": "Borrower not found",
+ "schema": { "$ref": "#/definitions/error" }
+ },
+ "500": {
+ "description": "Internal error",
+ "schema": { "$ref": "#/definitions/error" }
+ }
+ }
+ }
+ },
+ "/reserves/{reserve_id}": {
+ "put": {
+ "operationId": "editReserve",
+ "tags": ["reserves"],
+ "parameters": [
+ { "$ref": "#/parameters/reserveIdPathParam" },
+ {
+ "name": "body",
+ "in": "body",
+ "description": "A JSON object containing fields to modify",
+ "required": true,
+ "schema": {
+ "type": "object",
+ "properties": {
+ "priority": {
+ "description": "Position in waiting queue",
+ "type": "integer",
+ "minimum": 1
+ },
+ "branchcode": {
+ "description": "Pickup location",
+ "type": "string"
+ },
+ "suspend_until": {
+ "description": "Suspend until",
+ "type": "string",
+ "format": "date"
+ }
+ }
+ }
+ }
+ ],
+ "consumes": ["application/json"],
+ "produces": ["application/json"],
+ "responses": {
+ "200": {
+ "description": "Updated reserve",
+ "schema": { "$ref": "#/definitions/reserve" }
+ },
+ "400": {
+ "description": "Missing or wrong parameters",
+ "schema": { "$ref": "#/definitions/error" }
+ },
+ "404": {
+ "description": "Reserve not found",
+ "schema": { "$ref": "#/definitions/error" }
+ }
+ }
+ },
+ "delete": {
+ "operationId": "deleteReserve",
+ "tags": ["reserves"],
+ "parameters": [
+ { "$ref": "#/parameters/reserveIdPathParam" }
+ ],
+ "produces": ["application/json"],
+ "responses": {
+ "200": {
+ "description": "Successful deletion",
+ "schema": {
+ "type": "object"
+ }
+ },
+ "404": {
+ "description": "Reserve not found",
+ "schema": { "$ref": "#/definitions/error" }
+ }
+ }
+ }
}
},
"definitions": {
"description": "Internal patron identifier",
"required": true,
"type": "integer"
+ },
+ "reserveIdPathParam": {
+ "name": "reserve_id",
+ "in": "path",
+ "description": "Internal reserve identifier",
+ "required": true,
+ "type": "integer"
}
}
}
--- /dev/null
+#!/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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use Modern::Perl;
+
+use Test::More tests => 30;
+use Test::Mojo;
+
+use DateTime;
+
+use C4::Context;
+use C4::Biblio;
+use C4::Items;
+use C4::Reserves;
+
+use Koha::Database;
+use Koha::Patron;
+
+my $dbh = C4::Context->dbh;
+$dbh->{AutoCommit} = 0;
+$dbh->{RaiseError} = 1;
+
+my $t = Test::Mojo->new('Koha::REST::V1');
+
+my $categorycode = Koha::Database->new()->schema()->resultset('Category')->first()->categorycode();
+my $branchcode = Koha::Database->new()->schema()->resultset('Branch')->first()->branchcode();
+
+my $borrower = Koha::Patron->new;
+$borrower->categorycode( $categorycode );
+$borrower->branchcode( $branchcode );
+$borrower->surname("Test Surname");
+$borrower->store;
+my $borrowernumber = $borrower->borrowernumber;
+
+my $borrower2 = Koha::Patron->new;
+$borrower2->categorycode( $categorycode );
+$borrower2->branchcode( $branchcode );
+$borrower2->surname("Test Surname 2");
+$borrower2->store;
+my $borrowernumber2 = $borrower2->borrowernumber;
+
+my $biblionumber = create_biblio('RESTful Web APIs');
+my $itemnumber = create_item($biblionumber, 'TEST000001');
+
+my $reserve_id = C4::Reserves::AddReserve($branchcode, $borrowernumber,
+ $biblionumber, undef, 1, undef, undef, undef, '', $itemnumber);
+
+# Add another reserve to be able to change first reserve's rank
+C4::Reserves::AddReserve($branchcode, $borrowernumber2,
+ $biblionumber, undef, 2, undef, undef, undef, '', $itemnumber);
+
+my $suspend_until = DateTime->now->add(days => 10)->ymd;
+my $put_data = {
+ priority => 2,
+ suspend_until => $suspend_until,
+};
+$t->put_ok("/api/v1/reserves/$reserve_id" => json => $put_data)
+ ->status_is(200)
+ ->json_is('/reserve_id', $reserve_id)
+ ->json_is('/suspend_until', $suspend_until . ' 00:00:00')
+ ->json_is('/priority', 2);
+
+$t->delete_ok("/api/v1/reserves/$reserve_id")
+ ->status_is(200);
+
+$t->put_ok("/api/v1/reserves/$reserve_id" => json => $put_data)
+ ->status_is(404)
+ ->json_has('/error');
+
+$t->delete_ok("/api/v1/reserves/$reserve_id")
+ ->status_is(404)
+ ->json_has('/error');
+
+
+$t->get_ok("/api/v1/reserves?borrowernumber=$borrowernumber")
+ ->status_is(200)
+ ->json_is([]);
+
+my $inexisting_borrowernumber = $borrowernumber2 + 1;
+$t->get_ok("/api/v1/reserves?borrowernumber=$inexisting_borrowernumber")
+ ->status_is(404)
+ ->json_has('/error');
+
+$dbh->do('DELETE FROM issuingrules');
+$dbh->do(q{
+ INSERT INTO issuingrules (categorycode, branchcode, itemtype, reservesallowed)
+ VALUES (?, ?, ?, ?)
+}, {}, '*', '*', '*', 1);
+
+my $expirationdate = DateTime->now->add(days => 10)->ymd;
+my $post_data = {
+ borrowernumber => int($borrowernumber),
+ biblionumber => int($biblionumber),
+ itemnumber => int($itemnumber),
+ branchcode => $branchcode,
+ expirationdate => $expirationdate,
+};
+$t->post_ok("/api/v1/reserves" => json => $post_data)
+ ->status_is(201)
+ ->json_has('/reserve_id');
+
+$reserve_id = $t->tx->res->json->{reserve_id};
+
+$t->get_ok("/api/v1/reserves?borrowernumber=$borrowernumber")
+ ->status_is(200)
+ ->json_is('/0/reserve_id', $reserve_id)
+ ->json_is('/0/expirationdate', $expirationdate)
+ ->json_is('/0/branchcode', $branchcode);
+
+$t->post_ok("/api/v1/reserves" => json => $post_data)
+ ->status_is(403)
+ ->json_like('/error', qr/tooManyReserves/);
+
+
+$dbh->rollback;
+
+sub create_biblio {
+ my ($title) = @_;
+
+ my $record = new MARC::Record;
+ $record->append_fields(
+ new MARC::Field('200', ' ', ' ', a => $title),
+ );
+
+ my ($biblionumber) = C4::Biblio::AddBiblio($record, '');
+
+ return $biblionumber;
+}
+
+sub create_item {
+ my ($biblionumber, $barcode) = @_;
+
+ my $item = {
+ barcode => $barcode,
+ };
+
+ my $itemnumber = C4::Items::AddItem($item, $biblionumber);
+
+ return $itemnumber;
+}