From dbb0ff31b5f61eb207ecc8a7ffa3d678f9b2302d Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Tue, 28 Jun 2016 17:05:56 +0300 Subject: [PATCH] Bug 16826: Add API route for getting item availability GET /availability/items?itemnumber=123 GET /availability/items?itemnumber=123+456+789 GET /availability/items?biblionumber=321 GET /availability/items?biblionumber=321+654+987 This patch adds above routes for checking item availability. The route does not require login and does not consider patron status, as its purpose is to be fast and efficient in checking general availability of item(s), much like in a typical search. Because of this, this route does not tell us whether item is holdable or available for checkout for a specific patron. Patron-specific availability will be introduced in another patch. For this route, item is NOT available if: - item on loan, - item reserved, - item withdrawn, - item lost, - item restricted, - item or itemtype not for loan Depending on system preference, item may or may not be available: - item damaged (depending on AllowHoldsOnDamagedItems), The returned information is some necessary item information combined with: - available (bool) - availability_description ((array of strings) reasons for possible restrictions to availability, such as ["notforloan"]) - hold_queue_length (int) - expected_available ((string) due date if onloan) Possible values in availability_description are an empty array and any of: "onloan", "reserved", "damaged", "withdrawn", "itemlost", "restricted", "notforloan", "ordered" Patch adds $item->get_availability() in Koha::Items and introduces a new Koha::Item::Availability class where we can store item-related availability information. Includes REST tests and unit tests for new subroutines in Koha::Item and Koha::Item::Availability. To test: 1. Play around with an item. Place a hold on it, checkout, set it damaged etc. 2. Make GET requests to /api/v1/availability/items?itemnumber=YYY, where YYY is an existing itemnumber. You can also try with biblionumber=XXX query parameter, where XXX is an existing biblionumber. 3. Check that the availability status is as described in the patch description above. Also make sure reasons for unavailability are in availability_description. 4. Repeat steps 1-3 until you are confident route works as expected. 5. Run the following tests: - t/Koha/Item/Availability.t - t/db_dependent/Items.t - t/db_dependent/api/v1/availability.t --- Koha/Item.pm | 97 ++++++++++++++++ Koha/Item/Availability.pm | 198 +++++++++++++++++++++++++++++++++ Koha/REST/V1/Availability.pm | 88 +++++++++++++++ api/v1/definitions/availabilities.json | 4 + api/v1/definitions/availability.json | 57 ++++++++++ api/v1/definitions/index.json | 2 + api/v1/swagger.json | 50 ++++++++- t/Koha/Item/Availability.t | 42 +++++++ t/db_dependent/Items.t | 83 +++++++++++++- t/db_dependent/api/v1/availability.t | 195 ++++++++++++++++++++++++++++++++ 10 files changed, 813 insertions(+), 3 deletions(-) create mode 100644 Koha/Item/Availability.pm create mode 100644 Koha/REST/V1/Availability.pm create mode 100644 api/v1/definitions/availabilities.json create mode 100644 api/v1/definitions/availability.json create mode 100644 t/Koha/Item/Availability.t create mode 100644 t/db_dependent/api/v1/availability.t diff --git a/Koha/Item.pm b/Koha/Item.pm index d721537..d94ecfd 100644 --- a/Koha/Item.pm +++ b/Koha/Item.pm @@ -23,6 +23,13 @@ use Carp; use Koha::Database; +use C4::Circulation; +use C4::Context; +use C4::Members; +use C4::Reserves; +use Koha::Holds; +use Koha::Issues; +use Koha::Item::Availability; use Koha::Patrons; use Koha::Libraries; @@ -50,6 +57,80 @@ sub effective_itemtype { return $self->_result()->effective_itemtype(); } +=head3 get_availability + +my $available = $item->get_availability(); + +Gets availability of the item. Note that this subroutine does not check patron +status. Since availability is a broad term, we cannot claim the item is +"available" and cover all the possible cases. Instead, this subroutine simply +checks if the item is not onloan, not reserved, not notforloan, not withdrawn, +not lost or not damaged. + +Returns Koha::Item::Availability object. + +Use $item->is_available() for a simple Boolean value. + +=cut + +sub get_availability { + my ( $self ) = @_; + + my $availability = Koha::Item::Availability->new->set_available; + + if ($self->onloan) { + my $issue = Koha::Issues->search({ itemnumber => $self->itemnumber })->next; + $availability->set_unavailable("onloan", $issue->date_due) if $issue; + } + + $availability->set_unavailable("withdrawn") if $self->withdrawn; + $availability->set_unavailable("itemlost") if $self->itemlost; + $availability->set_unavailable("restricted") if $self->restricted; + + if ($self->damaged) { + if (C4::Context->preference('AllowHoldsOnDamagedItems')) { + $availability->push_description("damaged"); + } else { + $availability->set_unavailable("damaged"); + } + } + + my $itemtype; + if (C4::Context->preference('item-level_itypes')) { + $itemtype = Koha::ItemTypes->find( $self->itype ); + } else { + my $biblioitem = Koha::Biblioitems->find( $self->biblioitemnumber ); + $itemtype = Koha::ItemTypes->find( $biblioitem->itemype ); + } + + if ($self->notforloan > 0 || $itemtype && $itemtype->notforloan) { + $availability->set_unavailable("notforloan"); + } elsif ($self->notforloan < 0) { + $availability->set_unavailable("ordered"); + } + + if (Koha::Holds->search( [ + { itemnumber => $self->itemnumber }, + { found => [ '=', 'W', 'T' ] } + ], { order_by => 'priority' } )->count()) { + $availability->set_unavailable("reserved"); + } + + return $availability; +} + +=head3 hold_queue_length + +=cut + +sub hold_queue_length { + my ( $self ) = @_; + + my $reserves = Koha::Holds->search({ itemnumber => $self->itemnumber }); + return $reserves->count() if $reserves; + return 0; +} + =head3 home_branch =cut @@ -74,6 +155,22 @@ sub holding_branch { return $self->{_holding_branch}; } +=head3 is_available + +my $available = $item->is_available(); + +See Koha::Item->get_availability() for documentation. + +=cut + +sub is_available { + my ( $self, $status ) = @_; + + my $availability = $self->get_availability(); + + return $availability->{available}; +} + =head3 last_returned_by Gets and sets the last borrower to return an item. diff --git a/Koha/Item/Availability.pm b/Koha/Item/Availability.pm new file mode 100644 index 0000000..f5abfda --- /dev/null +++ b/Koha/Item/Availability.pm @@ -0,0 +1,198 @@ +package Koha::Item::Availability; + +# Copyright KohaSuomi 2016 +# +# 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 Koha::Logger; + +=head1 NAME + +Koha::Item::Availability - Koha Item Availability object class + +=head1 SYNOPSIS + + my $item = Koha::Items->find(1337); + my $availability = $item->get_availability(); + # ref($availability) eq 'Koha::Item::Availability' + + print $availability->get_description() unless $availability->{available} + +=head1 DESCRIPTION + +This class stores item availability information aiming to having one consistent +item availability object instead of many different types of HASHES and Boolean +values. + +See Koha::Item for availability subroutines. + +=head2 Class Methods + +=cut + +=head3 new + +Returns a new Koha::Item::Availability object. + +=cut + +sub new { + my ( $class ) = @_; + + my $self = { + availability_description => [], + availability_needs_confirmation => undef, + available => undef, + expected_available => undef, + }; + + bless( $self, $class ); +} + +=head3 push_description + +$availability->push_description("notforloan"); +$availability->push_description("withdrawn); + +# $availability->{availability_description} = ["notforloan", "withdrawn"] + +Pushes a new description to $availability object. Does not duplicate existing +descriptions. + +Returns updated Koha::Item::Availability object. + +=cut + +sub push_description { + my ($self, $description) = @_; + + return $self unless $description; + + if (ref($description) eq 'ARRAY') { + foreach my $desc (@$description) { + if (grep(/^$desc$/, $self->{availability_description})){ + next; + } + push $self->{availability_description}, $desc; + } + } else { + if (!grep(/^$description$/, $self->{availability_description})){ + push $self->{availability_description}, $description; + } + } + + return $self; +} + +=head3 reset + +$availability->reset; + +Resets the object. + +=cut + +sub reset { + my ( $self ) = @_; + + $self->{available} = undef; + $self->{availability_needs_confirmation} = undef; + $self->{expected_available} = undef; + $self->{availability_description} = []; + return $self; +} + +=head3 set_available + +$availability->set_available; + +Sets the Koha::Item::Availability object status to available. + $availability->{available} == 1 + +Overrides old availability status, but does not override other stored data in +the object. Create a new Koha::Item::Availability object to get a fresh start. +Appends any previously defined availability descriptions with push_description(). + +Returns updated Koha::Item::Availability object. + +=cut + +sub set_available { + my ($self, $description) = @_; + + return $self->_update_availability_status(1, 0, $description); +} + +=head3 set_needs_confirmation + +$availability->set_needs_confirmation("unbelieveable_reason", "2016-07-07"); + +Sets the Koha::Item::Availability object status to unavailable, +but needs confirmation. + $availability->{available} == 0 + $availability->{availability_needs_confirmation} == 1 + +Overrides old availability statuses, but does not override other stored data in +the object. Create a new Koha::Item::Availability object to get a fresh start. +Appends any previously defined availability descriptions with push_description(). +Allows you to define expected availability date in C<$expected>. + +Returns updated Koha::Item::Availability object. + +=cut + +sub set_needs_confirmation { + my ($self, $description, $expected) = @_; + + return $self->_update_availability_status(0, 1, $description, $expected); +} + +=head3 set_unavailable + +$availability->set_unavailable("onloan", "2016-07-07"); + +Sets the Koha::Item::Availability object status to unavailable. + $availability->{available} == 0 + +Overrides old availability status, but does not override other stored data in +the object. Create a new Koha::Item::Availability object to get a fresh start. +Appends any previously defined availability descriptions with push_description(). +Allows you to define expected availability date in C<$expected>. + +Returns updated Koha::Item::Availability object. + +=cut + +sub set_unavailable { + my ($self, $description, $expected) = @_; + + return $self->_update_availability_status(0, 0, $description, $expected); +} + +sub _update_availability_status { + my ( $self, $available, $needs, $desc, $expected ) = @_; + + $self->{available} = $available; + $self->{availability_needs_confirmation} = $needs; + $self->{expected_available} = $expected; + $self->push_description($desc); + + return $self; +} + +1; diff --git a/Koha/REST/V1/Availability.pm b/Koha/REST/V1/Availability.pm new file mode 100644 index 0000000..0bf1dfb --- /dev/null +++ b/Koha/REST/V1/Availability.pm @@ -0,0 +1,88 @@ +package Koha::REST::V1::Availability; + +# 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 Mojo::JSON; + +use Koha::Holds; +use Koha::Items; + +sub items { + my ($c, $args, $cb) = @_; + + my @items; + if ($args->{'itemnumber'}) { + push @items, _item_availability(@{$args->{'itemnumber'}}); + } + if ($args->{'biblionumber'}) { + my $found_items = Koha::Items->search({ biblionumber => { + '=', \@{$args->{'biblionumber'}} + } })->as_list(); + my @itemnumbers; + foreach my $item (@$found_items) { + push @itemnumbers, $item->itemnumber; + } + + push @items, _item_availability(@itemnumbers); + } + + return $c->$cb({ error => "Item(s) not found"}, 404) unless scalar @items; + return $c->$cb([ @items ], 200); +} + +sub _item_availability { + my (@itemnumbers) = @_; + + my @items; + + foreach my $itemnumber (@itemnumbers) { + my $item = Koha::Items->find($itemnumber); + + unless ($item) { + next; + } + + my $status = $item->get_availability(); + delete $status->{availability_needs_confirmation}; + $status->{available} = $status->{available} + ? Mojo::JSON->true + : Mojo::JSON->false; + + my $holds; + $holds->{'hold_queue_length'} = $item->hold_queue_length(); + + my $iteminfo = { + itemnumber => $item->itemnumber, + barcode => $item->barcode, + biblionumber => $item->biblionumber, + biblioitemnumber => $item->biblioitemnumber, + holdingbranch => $item->holdingbranch, + homebranch => $item->homebranch, + location => $item->location, + itemcallnumber => $item->itemcallnumber, + }; + + # merge availability, hold information and item information + push @items, { %{$status}, %{$holds}, %{$iteminfo} }; + } + + return @items; +} + +1; diff --git a/api/v1/definitions/availabilities.json b/api/v1/definitions/availabilities.json new file mode 100644 index 0000000..20fa958 --- /dev/null +++ b/api/v1/definitions/availabilities.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "availability.json" } +} diff --git a/api/v1/definitions/availability.json b/api/v1/definitions/availability.json new file mode 100644 index 0000000..8317789 --- /dev/null +++ b/api/v1/definitions/availability.json @@ -0,0 +1,57 @@ +{ + "type": "object", + "properties": { + "available": { + "type": "boolean", + "description": "availability status" + }, + "availability_description": { + "type": "array", + "items": { + "type": ["string"], + "description": "information on item's availability, reasons for unavailability" + }, + "description": "more information on item's availability" + }, + "barcode": { + "type": ["string", "null"], + "description": "item barcode" + }, + "biblioitemnumber": { + "type": "string", + "description": "internally assigned biblio item identifier" + }, + "biblionumber": { + "type": "string", + "description": "internally assigned biblio identifier" + }, + "expected_available": { + "type": ["string", "null"], + "description": "date this item is expected to be available" + }, + "holdQueueLength": { + "type": ["integer", "null"], + "description": "number of holdings placed on title/item" + }, + "holdingbranch": { + "type": ["string", "null"], + "description": "library that is currently in possession item" + }, + "homebranch": { + "type": ["string", "null"], + "description": "library that owns this item" + }, + "itemcallnumber": { + "type": ["string", "null"], + "description": "call number for this item" + }, + "itemnumber": { + "type": "string", + "description": "internally assigned item identifier" + }, + "location": { + "type": ["string", "null"], + "description": "authorized value for the shelving location for this item" + } + } +} diff --git a/api/v1/definitions/index.json b/api/v1/definitions/index.json index 8baf7e3..3378ccc 100644 --- a/api/v1/definitions/index.json +++ b/api/v1/definitions/index.json @@ -1,4 +1,6 @@ { + "availability": { "$ref": "availability.json" }, + "availabilities": { "$ref": "availabilities.json" }, "patron": { "$ref": "patron.json" }, "holds": { "$ref": "holds.json" }, "hold": { "$ref": "hold.json" }, diff --git a/api/v1/swagger.json b/api/v1/swagger.json index e126b24..d1de762 100644 --- a/api/v1/swagger.json +++ b/api/v1/swagger.json @@ -14,6 +14,34 @@ }, "basePath": "/api/v1", "paths": { + "/availability/items": { + "get": { + "operationId": "itemsAvailability", + "tags": ["items", "availability"], + "parameters": [ + { "$ref": "#/parameters/itemnumbersQueryParam" }, + { "$ref": "#/parameters/biblionumbersQueryParam" } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Availability information on item(s)", + "schema": { + "$ref": "#/definitions/availabilities" + } + }, + "400": { + "description": "Missing or wrong parameters", + "schema": { "$ref": "#/definitions/error" } + }, + "404": { + "description": "No item(s) found", + "schema": { "$ref": "#/definitions/error" } + } + } + } + }, "/patrons": { "get": { "operationId": "listPatrons", @@ -360,9 +388,19 @@ } }, "definitions": { - "$ref": "./definitions/index.json" + "$ref": "definitions/index.json" }, "parameters": { + "biblionumbersQueryParam": { + "name": "biblionumber", + "in": "query", + "description": "Internal biblios identifier", + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "ssv" + }, "borrowernumberPathParam": { "name": "borrowernumber", "in": "path", @@ -383,6 +421,16 @@ "description": "Internal item identifier", "required": true, "type": "integer" + }, + "itemnumbersQueryParam": { + "name": "itemnumber", + "in": "query", + "description": "Internal items identifier", + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "ssv" } } } diff --git a/t/Koha/Item/Availability.t b/t/Koha/Item/Availability.t new file mode 100644 index 0000000..047180d --- /dev/null +++ b/t/Koha/Item/Availability.t @@ -0,0 +1,42 @@ +#!/usr/bin/perl + +# Copyright KohaSuomi 2016 +# +# 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 . + +use Modern::Perl; +use Test::More tests => 7; + +use C4::Context; + +use_ok('Koha::Item::Availability'); + +my $availability = Koha::Item::Availability->new->set_available; + +is($availability->{available}, 1, "Available"); +$availability->set_needs_confirmation; +is($availability->{availability_needs_confirmation}, 1, "Needs confirmation"); +$availability->set_unavailable; +is($availability->{available}, 0, "Not available"); + +$availability->push_description("such available"); +$availability->push_description("wow"); + +is($availability->{availability_description}[0], "such available", "Found correct description 1/2"); +is($availability->{availability_description}[1], "wow", "Found correct description 2/2"); + +$availability->reset; +is($availability->{available}, undef, "Availability reset"); diff --git a/t/db_dependent/Items.t b/t/db_dependent/Items.t index b494a70..bca383f 100755 --- a/t/db_dependent/Items.t +++ b/t/db_dependent/Items.t @@ -20,7 +20,12 @@ use Modern::Perl; use MARC::Record; use C4::Biblio; +use C4::Circulation; +use C4::Reserves; use Koha::Database; +use Koha::Hold; +use Koha::Issue; +use Koha::Item::Availability; use Koha::Library; use t::lib::Mocks; @@ -30,6 +35,11 @@ use Test::More tests => 10; use Test::Warn; +my @USERENV = (1,'test','MASTERTEST','Test','Test','t','Test',0,); +my $BRANCH_IDX = 5; +C4::Context->_new_userenv ('DUMMY_SESSION_ID'); +C4::Context->set_userenv ( @USERENV ); + BEGIN { use_ok('C4::Items'); use_ok('Koha::Items'); @@ -432,7 +442,7 @@ subtest 'SearchItems test' => sub { subtest 'Koha::Item(s) tests' => sub { - plan tests => 5; + plan tests => 25; $schema->storage->txn_begin(); @@ -443,6 +453,15 @@ subtest 'Koha::Item(s) tests' => sub { my $library2 = $builder->build({ source => 'Branch', }); + my $borrower = $builder->build({ + source => 'Borrower', + }); + my $itemtype = $builder->build({ + source => 'Itemtype', + value => { + notforloan => 1 + } + }); # Create a biblio and item for testing t::lib::Mocks::mock_preference('marcflavour', 'MARC21'); @@ -461,6 +480,67 @@ subtest 'Koha::Item(s) tests' => sub { is( ref($holdingbranch), 'Koha::Library', "Got Koha::Library from holding_branch method" ); is( $holdingbranch->branchcode(), $library2->{branchcode}, "Home branch code matches holdingbranch" ); + # Availability tests + my $availability = $item->get_availability(); + is (ref($availability), 'Koha::Item::Availability', 'Got Koha::Item::Availability'); + is( $availability->{available}, 1, "Item is available" ); + + $item->set({ onloan => "", damaged => 1 })->store(); + $availability = $item->get_availability(); + is( $availability->{available}, C4::Context->preference('AllowHoldsOnDamagedItems'), "Good availability for damaged item" ); + is( $availability->{availability_description}[0], "damaged", "Availability description is 'damaged'" ); + + $item->set({ damaged => 0, withdrawn => 1 })->store(); + $availability = $item->get_availability(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{availability_description}[0], "withdrawn", "Availability description is 'withdrawn'" ); + + $item->set({ withdrawn => 0, itemlost => 1 })->store(); + $availability = $item->get_availability(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{availability_description}[0], "itemlost", "Availability description is 'itemlost'" ); + + $item->set({ itemlost => 0, restricted => 1 })->store(); + $availability = $item->get_availability(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{availability_description}[0], "restricted", "Availability description is 'restricted'" ); + + $item->set({ restricted => 0, notforloan => 1 })->store(); + $availability = $item->get_availability(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{availability_description}[0], "notforloan", "Availability description is 'notforloan'" ); + + $item->set({ notforloan => 0, itype => $itemtype->{itemtype} })->store(); + $availability = $item->get_availability(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{availability_description}[0], "notforloan", "Availability description is 'notforloan' (itemtype)" ); + + $item->set({ itype => undef, barcode => "test" })->store(); + my $reserve = Koha::Hold->new( + { + biblionumber => $item->biblionumber, + itemnumber => $item->itemnumber, + waitingdate => '2000-01-01', + borrowernumber => $borrower->{borrowernumber}, + branchcode => $item->homebranch, + suspend => 0, + } + )->store(); + $availability = $item->get_availability(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{availability_description}[0], "reserved", "Availability description is 'reserved'" ); + CancelReserve({ reserve_id => $reserve->reserve_id }); + + $availability = $item->get_availability(); + is( $availability->{available}, 1, "Item is available" ); + + my $issue = AddIssue($borrower, $item->barcode, undef, 1); + $item = Koha::Items->find($item->itemnumber); # refresh item + $availability = $item->get_availability(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{availability_description}[0], "onloan", "Availability description is 'onloan'" ); + is( $availability->{expected_available}, $issue->date_due, "Expected to be available '".$issue->date_due."'"); + $schema->storage->txn_rollback; }; @@ -468,7 +548,6 @@ subtest 'C4::Biblio::EmbedItemsInMarcBiblio' => sub { plan tests => 7; $schema->storage->txn_begin(); - my $builder = t::lib::TestBuilder->new; my $library1 = $builder->build({ source => 'Branch', diff --git a/t/db_dependent/api/v1/availability.t b/t/db_dependent/api/v1/availability.t new file mode 100644 index 0000000..4144279 --- /dev/null +++ b/t/db_dependent/api/v1/availability.t @@ -0,0 +1,195 @@ +#!/usr/bin/env perl + +# Copyright KohaSuomi 2016 +# +# 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 => 76; +use Test::Mojo; +use t::lib::TestBuilder; + +use Mojo::JSON; + +use C4::Auth; +use C4::Circulation; +use C4::Context; + +use Koha::Database; +use Koha::Items; +use Koha::Patron; + +my $builder = t::lib::TestBuilder->new(); + +my $dbh = C4::Context->dbh; +$dbh->{AutoCommit} = 0; +$dbh->{RaiseError} = 1; + +$ENV{REMOTE_ADDR} = '127.0.0.1'; +my $t = Test::Mojo->new('Koha::REST::V1'); + +my @USERENV = (1,'test','MASTERTEST','Test','Test','t','Test',0,); +my $BRANCH_IDX = 5; +C4::Context->_new_userenv ('DUMMY_SESSION_ID'); +C4::Context->set_userenv ( @USERENV ); + +my $categorycode = $builder->build({ source => 'Category' })->{ categorycode }; +my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode }; + +my $borrower = $builder->build({ source => 'Borrower' }); +my $biblio = $builder->build({ source => 'Biblio' }); +my $biblio2 = $builder->build({ source => 'Biblio' }); +my $biblionumber = $biblio->{biblionumber}; +my $biblionumber2 = $biblio2->{biblionumber}; + +# $item = available, $item2 = unavailable +my $items; +$items->{available} = build_item($biblionumber); +$items->{notforloan} = build_item($biblionumber2, { notforloan => 1 }); +$items->{damaged} = build_item($biblionumber2, { damaged => 1 }); +$items->{withdrawn} = build_item($biblionumber2, { withdrawn => 1 }); +$items->{onloan} = build_item($biblionumber2, { onloan => undef }); +$items->{itemlost} = build_item($biblionumber2, { itemlost => 1 }); +$items->{reserved} = build_item($biblionumber2); +my $reserve = Koha::Hold->new( + { + biblionumber => $items->{reserved}->{biblionumber}, + itemnumber => $items->{reserved}->{itemnumber}, + waitingdate => '2000-01-01', + borrowernumber => $borrower->{borrowernumber}, + branchcode => $items->{reserved}->{homebranch}, + suspend => 0, + } + )->store(); + +my $itemnumber = $items->{available}->{itemnumber}; + +$t->get_ok("/api/v1/availability/items?itemnumber=-500382") + ->status_is(404); + +$t->get_ok("/api/v1/availability/items?itemnumber=-500382+-500383") + ->status_is(404); + +$t->get_ok("/api/v1/availability/items?biblionumber=-500382") + ->status_is(404); + +$t->get_ok("/api/v1/availability/items?biblionumber=-500382+-500383") + ->status_is(404); + +# available item +$t->get_ok("/api/v1/availability/items?itemnumber=$itemnumber") + ->status_is(200) + ->json_is('/0/itemnumber', $itemnumber) + ->json_is('/0/biblionumber', $biblionumber) + ->json_is('/0/available', Mojo::JSON->true) + ->json_is('/0/availability_description', []) + ->json_is('/0/hold_queue_length', 0); +$t->get_ok("/api/v1/availability/items?biblionumber=$biblionumber") + ->status_is(200) + ->json_is('/0/itemnumber', $itemnumber) + ->json_is('/0/biblionumber', $biblionumber) + ->json_is('/0/available', Mojo::JSON->true) + ->json_is('/0/availability_description', []) + ->json_is('/0/hold_queue_length', 0); + +# notforloan item +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{notforloan}->{itemnumber}) + ->status_is(200) + ->json_is('/0/itemnumber', $items->{notforloan}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/available', Mojo::JSON->false) + ->json_is('/0/availability_description/0', "notforloan") + ->json_is('/0/hold_queue_length', 0); + +# damaged item +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{damaged}->{itemnumber}) + ->status_is(200) + ->json_is('/0/itemnumber', $items->{damaged}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/available', C4::Context->preference('AllowHoldsOnDamagedItems') ? Mojo::JSON->true : Mojo::JSON->false) + ->json_is('/0/availability_description/0', "damaged") + ->json_is('/0/hold_queue_length', 0); + +# withdrawn item +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{withdrawn}->{itemnumber}) + ->status_is(200) + ->json_is('/0/itemnumber', $items->{withdrawn}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/available', Mojo::JSON->false) + ->json_is('/0/availability_description/0', "withdrawn") + ->json_is('/0/hold_queue_length', 0); + +# lost item +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{itemlost}->{itemnumber}) + ->status_is(200) + ->json_is('/0/itemnumber', $items->{itemlost}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/available', Mojo::JSON->false) + ->json_is('/0/availability_description/0', "itemlost") + ->json_is('/0/hold_queue_length', 0); + +my $issue = AddIssue($borrower, $items->{onloan}->{barcode}, undef, 1); + +# issued item +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{onloan}->{itemnumber}) + ->status_is(200) + ->json_is('/0/itemnumber', $items->{onloan}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/available', Mojo::JSON->false) + ->json_is('/0/availability_description/0', "onloan") + ->json_is('/0/expected_available', $issue->date_due) + ->json_is('/0/hold_queue_length', 0); + +# reserved item +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{reserved}->{itemnumber}) + ->status_is(200) + ->json_is('/0/itemnumber', $items->{reserved}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/available', Mojo::JSON->false) + ->json_is('/0/availability_description/0', "reserved") + ->json_is('/0/hold_queue_length', 1); + +# multiple in one request +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{notforloan}->{itemnumber}."+$itemnumber+-500382") + ->status_is(200) + ->json_is('/0/itemnumber', $items->{notforloan}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/available', Mojo::JSON->false) + ->json_is('/0/availability_description/0', "notforloan") + ->json_is('/1/itemnumber', $itemnumber) + ->json_is('/1/biblionumber', $biblionumber) + ->json_is('/1/available', Mojo::JSON->true) + ->json_is('/1/availability_description', []) + ->json_is('/1/hold_queue_length', 0); + +sub build_item { + my ($biblionumber, $field) = @_; + + return $builder->build({ + source => 'Item', + value => { + biblionumber => $biblionumber, + notforloan => $field->{notforloan} || 0, + damaged => $field->{damaged} || 0, + withdrawn => $field->{withdrawn} || 0, + itemlost => $field->{itemlost} || 0, + restricted => $field->{restricted} || undef, + onloan => $field->{onloan} || undef, + itype => $field->{itype} || undef, + } + }); +} -- 1.9.1