From 35fc8239f000d5ce763399eada606325a158ba20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Kozlovsk=C3=BD?= Date: Sun, 31 Jul 2016 11:19:09 +0200 Subject: [PATCH] Bug 16652: RestfulAPI supporting services for Vufind and for xnciptoolkit drivers What this patch does is it takes current state of the following bugs: bug 13903 bug 16330 bug 13895 bug 16497 bug 16825 bug 17007 bug 16826 bug 15165 bug 17005 And rebases them on master altogether on each other fixing the merge conflicts. Using this patch it can be easily tried out the currently implemented functionality of REST API (even though in development for now) by running 'git bz apply 16652' after installing Koha's git-bz as decribed here: https://wiki.koha-community.org/wiki/Git_bz_configuration Also, the project xcncip2toolkit from eXtensibleCatalog has currently it's Koha connector implemented with respect to current state of these included bugs. See https://github.com/eXtensibleCatalog/xcncip2toolkit/blob/master/connectors/koha If you wish to try the full end-to-end functionality of this, you can run docker to install the xcncip2toolkit for you: https://github.com/eXtensibleCatalog/xcncip2toolkit/tree/master/dockerfile --- Koha/Accountline.pm | 30 ++ Koha/Accountlines.pm | 35 ++ Koha/Item.pm | 171 +++++++ Koha/Item/Availability.pm | 283 +++++++++++ Koha/REST/V1/Accountline.pm | 144 ++++++ Koha/REST/V1/Availability.pm | 99 ++++ Koha/REST/V1/Biblio.pm | 76 +++ Koha/REST/V1/Checkout.pm | 56 ++- Koha/REST/V1/Item.pm | 54 +++ Koha/REST/V1/Library.pm | 42 ++ Koha/REST/V1/Patron.pm | 157 +++++- api/v1/definitions/accountline.json | 56 +++ api/v1/definitions/amountpaid.json | 9 + api/v1/definitions/availabilities.json | 4 + api/v1/definitions/availability.json | 57 +++ api/v1/definitions/availabilitystatus.json | 16 + api/v1/definitions/biblio.json | 65 +++ api/v1/definitions/checkout.json | 4 +- api/v1/definitions/editAccountlineBody.json | 17 + api/v1/definitions/index.json | 10 + api/v1/definitions/item.json | 177 +++++++ api/v1/definitions/libraries.json | 4 + api/v1/definitions/library.json | 77 +++ api/v1/definitions/partialpayAccountlineBody.json | 11 + api/v1/swagger.json | 554 ++++++++++++++++++++++ t/Koha/Item/Availability.t | 71 +++ t/db_dependent/Items.t | 111 ++++- t/db_dependent/api/v1/accountlines.t | 246 ++++++++++ t/db_dependent/api/v1/availability.t | 289 +++++++++++ t/db_dependent/api/v1/biblios.t | 116 +++++ t/db_dependent/api/v1/checkoutshistory.t | 160 +++++++ t/db_dependent/api/v1/items.t | 117 +++++ t/db_dependent/api/v1/libraries.t | 63 +++ t/db_dependent/api/v1/patrons.t | 167 ++++++- 34 files changed, 3528 insertions(+), 20 deletions(-) create mode 100644 Koha/Accountline.pm create mode 100644 Koha/Accountlines.pm create mode 100644 Koha/Item/Availability.pm create mode 100644 Koha/REST/V1/Accountline.pm create mode 100644 Koha/REST/V1/Availability.pm create mode 100644 Koha/REST/V1/Biblio.pm create mode 100644 Koha/REST/V1/Item.pm create mode 100644 Koha/REST/V1/Library.pm create mode 100644 api/v1/definitions/accountline.json create mode 100644 api/v1/definitions/amountpaid.json create mode 100644 api/v1/definitions/availabilities.json create mode 100644 api/v1/definitions/availability.json create mode 100644 api/v1/definitions/availabilitystatus.json create mode 100644 api/v1/definitions/biblio.json create mode 100644 api/v1/definitions/editAccountlineBody.json create mode 100644 api/v1/definitions/item.json create mode 100644 api/v1/definitions/libraries.json create mode 100644 api/v1/definitions/library.json create mode 100644 api/v1/definitions/partialpayAccountlineBody.json create mode 100644 t/Koha/Item/Availability.t create mode 100644 t/db_dependent/api/v1/accountlines.t create mode 100644 t/db_dependent/api/v1/availability.t create mode 100644 t/db_dependent/api/v1/biblios.t create mode 100644 t/db_dependent/api/v1/checkoutshistory.t create mode 100644 t/db_dependent/api/v1/items.t create mode 100644 t/db_dependent/api/v1/libraries.t diff --git a/Koha/Accountline.pm b/Koha/Accountline.pm new file mode 100644 index 0000000..04254d7 --- /dev/null +++ b/Koha/Accountline.pm @@ -0,0 +1,30 @@ +package Koha::Accountline; + +# Copyright 2015 BibLibre +# +# 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::Database; + +use base qw(Koha::Object); + +sub _type { + return 'Accountline'; +} + +1; diff --git a/Koha/Accountlines.pm b/Koha/Accountlines.pm new file mode 100644 index 0000000..6d84094 --- /dev/null +++ b/Koha/Accountlines.pm @@ -0,0 +1,35 @@ +package Koha::Accountlines; + +# Copyright 2015 BibLibre +# +# 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::Database; +use Koha::Accountline; + +use base qw(Koha::Objects); + +sub _type { + return 'Accountline'; +} + +sub object_class { + return 'Koha::Accountline'; +} + +1; diff --git a/Koha/Item.pm b/Koha/Item.pm index d721537..0634d7b 100644 --- a/Koha/Item.pm +++ b/Koha/Item.pm @@ -23,6 +23,11 @@ use Carp; use Koha::Database; +use C4::Context; +use Koha::Holds; +use Koha::Issues; +use Koha::Item::Availability; +use Koha::ItemTypes; use Koha::Patrons; use Koha::Libraries; @@ -38,6 +43,160 @@ Koha::Item - Koha Item object class =cut +=head3 availabilities + +my $available = $item->availabilities(); + +Gets different availability types, generally, without considering patron status. + +Returns HASH containing Koha::Item::Availability objects for each availability +type. Currently implemented availabilities are: + * hold + * checkout + * local_use + * onsite_checkout + +=cut + +sub availabilities { + my ( $self, $params ) = @_; + + my $availabilities; # HASH containing different types of availabilities + my $availability = Koha::Item::Availability->new->set_available; + + $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->add_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"); + } + + # Hold + $availabilities->{'hold'} = $availability->clone; + + # Checkout + if ($self->onloan) { + my $issue = Koha::Issues->search({ itemnumber => $self->itemnumber })->next; + $availability->set_unavailable("onloan", $issue->date_due) if $issue; + } + + if (Koha::Holds->search( [ + { itemnumber => $self->itemnumber }, + { found => [ '=', 'W', 'T' ] } + ])->count()) { + $availability->set_unavailable("reserved"); + } + + $availabilities->{'checkout'} = $availability->clone; + + # Local Use, + if (grep(/^notforloan$/, @{$availability->{description}}) + && @{$availability->{description}} == 1) { + $availabilities->{'local_use'} = $availability->clone->set_available + ->del_description("notforloan"); + } else { + $availabilities->{'local_use'} = $availability->clone + ->del_description("notforloan"); + } + + # On-site checkout + if (!C4::Context->preference('OnSiteCheckouts')) { + $availabilities->{'onsite_checkout'} + = Koha::Item::Availability->new + ->set_unavailable("onsite_checkouts_disabled"); + } else { + $availabilities->{'onsite_checkout'} + = $availabilities->{'local_use'}->clone; + } + + return $availabilities; +} + +=head3 availability_for_checkout + +my $available = $item->availability_for_checkout(); + +Gets checkout availability of the item. This subroutine does not check patron +status, instead the purpose is to check general availability for this item. + +Returns Koha::Item::Availability object. + +=cut + +sub availability_for_checkout { + my ( $self ) = @_; + + return $self->availabilities->{'checkout'}; +} + +=head3 availability_for_local_use + +my $available = $item->availability_for_local_use(); + +Gets local use availability of the item. + +Returns Koha::Item::Availability object. + +=cut + +sub availability_for_local_use { + my ( $self ) = @_; + + return $self->availabilities->{'local_use'}; +} + +=head3 availability_for_onsite_checkout + +my $available = $item->availability_for_onsite_checkout(); + +Gets on-site checkout availability of the item. + +Returns Koha::Item::Availability object. + +=cut + +sub availability_for_onsite_checkout { + my ( $self ) = @_; + + return $self->availabilities->{'onsite_checkout'}; +} + +=head3 availability_for_reserve + +my $available = $item->availability_for_reserve(); + +Gets reserve availability of the item. This subroutine does not check patron +status, instead the purpose is to check general availability for this item. + +Returns Koha::Item::Availability object. + +=cut + +sub availability_for_reserve { + my ( $self ) = @_; + + return $self->availabilities->{'hold'}; +} + =head3 effective_itemtype Returns the itemtype for the item based on whether item level itemtypes are set or not. @@ -50,6 +209,18 @@ sub effective_itemtype { return $self->_result()->effective_itemtype(); } +=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 diff --git a/Koha/Item/Availability.pm b/Koha/Item/Availability.pm new file mode 100644 index 0000000..3cacd29 --- /dev/null +++ b/Koha/Item/Availability.pm @@ -0,0 +1,283 @@ +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 Storable qw(dclone); + +=head1 NAME + +Koha::Item::Availability - Koha Item Availability object class + +=head1 SYNOPSIS + + my $item = Koha::Items->find(1337); + my $availabilities = $item->availabilities(); + # ref($availabilities) eq 'HASH' + # ref($availabilities->{'checkout'}) eq 'Koha::Item::Availability' + + print "Available for checkout!" if $availabilities->{'checkout'}->{available}; + +=head1 DESCRIPTION + +This class holds item availability information. + +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 = { + description => [], + availability_needs_confirmation => undef, + available => undef, + expected_available => undef, + }; + + bless( $self, $class ); +} + +=head3 add_description + +$availability->add_description("notforloan"); +$availability->add_description("withdrawn); + +# $availability->{description} = ["notforloan", "withdrawn"] + +Pushes a new description to $availability object. Does not duplicate existing +descriptions. + +Returns updated Koha::Item::Availability object. + +=cut + +sub add_description { + my ($self, $description) = @_; + + return $self unless $description; + + if (ref($description) eq 'ARRAY') { + foreach my $desc (@$description) { + if (grep(/^$desc$/, @{$self->{description}})){ + next; + } + push $self->{description}, $desc; + } + } else { + if (!grep(/^$description$/, @{$self->{description}})){ + push $self->{description}, $description; + } + } + + return $self; +} + +=head3 clone + +$availability_cloned = $availability->clone; +$availability->set_unavailable; + +# $availability_cloned->{available} != $availability->{available} + +Clones the Koha::Item::Availability object. + +Returns cloned object. + +=cut + +sub clone { + my ( $self ) = @_; + + return dclone($self); +} + +=head3 del_description + +$availability->add_description(["notforloan", "withdrawn", "itemlost", "restricted"]); +$availability->del_description("withdrawn"); + +# $availability->{description} == ["notforloan", "itemlost", "restricted"] +$availability->del_description(["withdrawn", "restricted"]); +# $availability->{description} == ["itemlost"] + +Deletes an availability description(s) if it exists. + +Returns (possibly updated) Koha::Item::Availability object. + +=cut + +sub del_description { + my ($self, $description) = @_; + + return $self unless $description; + + my @updated; + if (ref($description) eq 'ARRAY') { + foreach my $desc (@$description) { + @updated = grep(!/^$desc$/, @{$self->{description}}); + } + } else { + @updated = grep(!/^$description$/, @{$self->{description}}); + } + $self->{description} = \@updated; + + return $self; +} + +=head3 hash_description + +$availability->add_description(["notforloan", "withdrawn"]); +$availability->has_description("withdrawn"); # 1 +$availability->has_description(["notforloan", "withdrawn"]); # 1 +$availability->has_description("itemlost"); # 0 + +Finds description(s) in availability descriptions. + +Returns 1 if found, 0 otherwise. + +=cut + +sub has_description { + my ($self, $description) = @_; + + return 0 unless $description; + + my @found; + if (ref($description) eq 'ARRAY') { + foreach my $desc (@$description) { + if (!grep(/^$desc$/, @{$self->{description}})){ + return 0; + } + } + } else { + if (!grep(/^$description$/, @{$self->{description}})){ + return 0; + } + } + + return 1; +} + +=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->{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 add_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 add_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 add_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 if $expected; + $self->add_description($desc); + + return $self; +} + +1; diff --git a/Koha/REST/V1/Accountline.pm b/Koha/REST/V1/Accountline.pm new file mode 100644 index 0000000..6e802a2 --- /dev/null +++ b/Koha/REST/V1/Accountline.pm @@ -0,0 +1,144 @@ +package Koha::REST::V1::Accountline; + +# 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::Auth qw( haspermission ); +use C4::Accounts qw( makepayment makepartialpayment recordpayment ); +use C4::Members qw( GetMember ); +use Koha::Accountlines; +use Scalar::Util qw( looks_like_number ); + +sub list { + my ($c, $args, $cb) = @_; + + my $user = $c->stash('koha.user'); + unless ($user && haspermission($user->userid, {updatecharges => 1})) { + return $c->$cb({error => "You don't have the required permission"}, 403); + } + + my $params = $c->req->params->to_hash; + my $accountlines = Koha::Accountlines->search($params); + + return $c->$cb($accountlines->unblessed, 200); +} + + +sub edit { + my ($c, $args, $cb) = @_; + + my $user = $c->stash('koha.user'); + unless ($user && haspermission($user->userid, {updatecharges => 1})) { + return $c->$cb({error => "You don't have the required permission"}, 403); + } + + my $accountline = Koha::Accountlines->find($args->{accountlines_id}); + unless ($accountline) { + return $c->$cb({error => "Accountline not found"}, 404); + } + + my $body = $c->req->json; + + $accountline->set( $body ); + $accountline->store(); + + return $c->$cb($accountline->unblessed(), 200); +} + + +sub pay { + my ($c, $args, $cb) = @_; + + my $user = $c->stash('koha.user'); + unless ($user && haspermission($user->userid, {updatecharges => 1})) { + return $c->$cb({error => "You don't have the required permission"}, 403); + } + + my $accountline = Koha::Accountlines->find($args->{accountlines_id}); + unless ($accountline) { + return $c->$cb({error => "Accountline not found"}, 404); + } + + makepayment($accountline->accountlines_id, + $accountline->borrowernumber, + $accountline->accountno, + $accountline->amount); + + $accountline = Koha::Accountlines->find($args->{accountlines_id}); + return $c->$cb($accountline->unblessed(), 200); +} + +sub partialpay { + my ($c, $args, $cb) = @_; + + my $user = $c->stash('koha.user'); + unless ($user && haspermission($user->userid, {updatecharges => 1})) { + return $c->$cb({error => "You don't have the required permission"}, 403); + } + + my $accountline = Koha::Accountlines->find($args->{accountlines_id}); + unless ($accountline) { + return $c->$cb({error => "Accountline not found"}, 404); + } + + my $body = $c->req->json; + my $amount = $body->{amount}; + my $note = $body->{note} || ''; + + unless ($amount && looks_like_number($amount)) { + return $c->$cb({error => "Invalid amount"}, 400); + } + + makepartialpayment($accountline->accountlines_id, + $accountline->borrowernumber, + $accountline->accountno, + $amount, '', '', $note); + + $accountline = Koha::Accountlines->find($args->{accountlines_id}); + return $c->$cb($accountline->unblessed(), 200); +} + + +sub payamount { + my ($c, $args, $cb) = @_; + + my $user = $c->stash('koha.user'); + unless ($user && haspermission($user->userid, {updatecharges => 1})) { + return $c->$cb({error => "You don't have the required permission"}, 403); + } + + my $borrower = GetMember(borrowernumber => $args->{borrowernumber}); + unless ($borrower) { + return $c->$cb({error => "Borrower not found"}, 404); + } + + my $body = $c->req->json; + my $amount = $body->{amount}; + my $note = $body->{note} || ''; + + unless ($amount && looks_like_number($amount)) { + return $c->$cb({error => "Invalid amount"}, 400); + } + + recordpayment($borrower->{borrowernumber}, $amount, '', $note); + + return $c->$cb({amount => $amount}, 200); +} + +1; diff --git a/Koha/REST/V1/Availability.pm b/Koha/REST/V1/Availability.pm new file mode 100644 index 0000000..ee4cf10 --- /dev/null +++ b/Koha/REST/V1/Availability.pm @@ -0,0 +1,99 @@ +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 $availabilities = _swaggerize_availabilities($item->availabilities()); + + 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, { %{$availabilities}, %{$holds}, %{$iteminfo} }; + } + + return @items; +} + +sub _swaggerize_availabilities { + my ($availabilities) = @_; + + foreach my $availability (keys $availabilities) { + delete $availabilities->{$availability}->{availability_needs_confirmation}; + $availabilities->{$availability}->{available} = + $availabilities->{$availability}->{available} + ? Mojo::JSON->true + : Mojo::JSON->false; + $availabilities->{$availability} = { %{$availabilities->{$availability}} }; + } + + return $availabilities; +} + +1; diff --git a/Koha/REST/V1/Biblio.pm b/Koha/REST/V1/Biblio.pm new file mode 100644 index 0000000..a10a8fa --- /dev/null +++ b/Koha/REST/V1/Biblio.pm @@ -0,0 +1,76 @@ +package Koha::REST::V1::Biblio; + +# 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 strict; +use warnings; + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; +use Mojo::JSON; + +use C4::Auth qw( haspermission ); +use C4::Context; +use C4::Items qw( GetItem GetHiddenItemnumbers ); + +use Koha::Biblios; +use Koha::Items; + +sub get { + my ($c, $args, $cb) = @_; + + my $biblionumber = $c->param('biblionumber'); + my $biblio = Koha::Biblios->find($biblionumber); + + unless ($biblio) { + return $c->$cb({error => "Biblio not found"}, 404); + } + + my $items ||= Koha::Items->search( { biblionumber => $biblionumber }, { + columns => [qw/itemnumber/], + })->unblessed; + + my $user = $c->stash('koha.user'); + my $isStaff = haspermission($user->userid, {borrowers => 1}); + + # Hide the hidden items from all but staff + my $opachiddenitems = ! $isStaff + && ( C4::Context->preference('OpacHiddenItems') !~ /^\s*$/ ); + + if ($opachiddenitems) { + + my @hiddenitems = C4::Items::GetHiddenItemnumbers( @{$items} ); + + my @filteredItems = (); + + # Convert to a hash for quick searching + my %hiddenitems = map { $_ => 1 } @hiddenitems; + foreach my $itemnumber ( map { $_->{itemnumber} } @{$items} ) { + next if $hiddenitems{$itemnumber}; + push @filteredItems, { itemnumber => $itemnumber }; + } + + $items = \@filteredItems; + } + + $biblio = $biblio->unblessed; + $biblio->{items} = $items; + + return $c->$cb($biblio, 200); +} + +1; diff --git a/Koha/REST/V1/Checkout.pm b/Koha/REST/V1/Checkout.pm index 5dd8128..3d1db57 100644 --- a/Koha/REST/V1/Checkout.pm +++ b/Koha/REST/V1/Checkout.pm @@ -22,6 +22,7 @@ use Mojo::Base 'Mojolicious::Controller'; use C4::Auth qw( haspermission ); use C4::Circulation; use Koha::Issues; +use Koha::OldIssues; sub list { my ($c, $args, $cb) = @_; @@ -72,12 +73,15 @@ sub renew { if (!$checkout) { return $c->$cb({ - error => "Checkout doesn't exists" + error => "Checkout doesn't exist" }, 404); } $checkout = $checkout->unblessed; - my ($borrowernumber, $itemnumber) = @$checkout{qw(borrowernumber itemnumber)}; + + my $borrowernumber = $checkout->borrowernumber; + my $itemnumber = $checkout->itemnumber; + my ($can_renew, $error) = C4::Circulation::CanBookBeRenewed( $borrowernumber, $itemnumber); @@ -91,4 +95,52 @@ sub renew { return $c->$cb($checkout->unblessed, 200); } +sub listhistory { + my ($c, $args, $cb) = @_; + + my $user = $c->stash('koha.user'); + unless ($user && haspermission($user->userid, { circulate => "circulate_remaining_permissions" })) { + return $c->$cb({error => "You don't have the required permission"}, 403); + } + + my $borrowernumber = $c->param('borrowernumber'); + + my %attributes = ( itemnumber => { "!=", undef } ); + if ($borrowernumber) { + return $c->$cb({ + error => "Patron doesn't exist" + }, 404) unless Koha::Patrons->find($borrowernumber); + + $attributes{borrowernumber} = $borrowernumber; + } + + # Retrieve all the issues in the history, but only the issue_id due to possible perfomance issues + my $checkouts = Koha::OldIssues->search( + \%attributes, + { columns => [qw/issue_id/]} + ); + + $c->$cb($checkouts->unblessed, 200); +} + +sub gethistory { + my ($c, $args, $cb) = @_; + + my $user = $c->stash('koha.user'); + unless ($user && haspermission($user->userid, { circulate => "circulate_remaining_permissions" })) { + return $c->$cb({error => "You don't have the required permission"}, 403); + } + + my $checkout_id = $args->{checkout_id}; + my $checkout = Koha::OldIssues->find($checkout_id); + + if (!$checkout) { + return $c->$cb({ + error => "Checkout doesn't exist" + }, 404); + } + + return $c->$cb($checkout->unblessed, 200); +} + 1; diff --git a/Koha/REST/V1/Item.pm b/Koha/REST/V1/Item.pm new file mode 100644 index 0000000..b51a225 --- /dev/null +++ b/Koha/REST/V1/Item.pm @@ -0,0 +1,54 @@ +package Koha::REST::V1::Item; + +# 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 C4::Auth qw( haspermission ); +use C4::Items qw( GetHiddenItemnumbers ); + +use Koha::Items; + +sub get { + my ($c, $args, $cb) = @_; + + my $itemnumber = $c->param('itemnumber'); + my $item = Koha::Items->find($itemnumber); + unless ($item) { + return $c->$cb({error => "Item not found"}, 404); + } + + # Hide non-public itemnotes if user has no staff access + my $user = $c->stash('koha.user'); + unless ($user && haspermission($user->userid, {catalogue => 1})) { + + my @hiddenitems = C4::Items::GetHiddenItemnumbers( ({ itemnumber => $itemnumber}) ); + my %hiddenitems = map { $_ => 1 } @hiddenitems; + + # Pretend it was not found as it's hidden from OPAC to regular users + return $c->$cb({error => "Item not found"}, 404) + if $hiddenitems{$itemnumber}; + + $item->set({ itemnotes_nonpublic => undef }); + } + + return $c->$cb($item->unblessed, 200); +} + +1; diff --git a/Koha/REST/V1/Library.pm b/Koha/REST/V1/Library.pm new file mode 100644 index 0000000..30a8982 --- /dev/null +++ b/Koha/REST/V1/Library.pm @@ -0,0 +1,42 @@ +package Koha::REST::V1::Library; + +# 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 Koha::Libraries; + +sub list { + my ($c, $args, $cb) = @_; + + my $libraries = Koha::Libraries->search; + return $c->$cb($libraries->unblessed, 200); +} + +sub get { + my ($c, $args, $cb) = @_; + + my $brancocode = $c->param('branchcode'); + my $library = Koha::Libraries->find({branchcode => $brancocode}); + unless ($library) { + return $c->$cb({error => "Library with branchcode \"$brancocode\" not found"}, 404); + } + + return $c->$cb($library->unblessed, 200); +} + +1; diff --git a/Koha/REST/V1/Patron.pm b/Koha/REST/V1/Patron.pm index 2851308..0411719 100644 --- a/Koha/REST/V1/Patron.pm +++ b/Koha/REST/V1/Patron.pm @@ -20,7 +20,10 @@ use Modern::Perl; use Mojo::Base 'Mojolicious::Controller'; use C4::Auth qw( haspermission ); +use Koha::AuthUtils qw(hash_password); use Koha::Patrons; +use Koha::Patron::Categories; +use Koha::Libraries; sub list { my ($c, $args, $cb) = @_; @@ -30,7 +33,17 @@ sub list { return $c->$cb({error => "You don't have the required permission"}, 403); } - my $patrons = Koha::Patrons->search; + my $params = $c->req->query_params->to_hash; + my $patrons; + if (keys %$params) { + my @valid_params = Koha::Patrons->_resultset->result_source->columns; + foreach my $key (keys %$params) { + delete $params->{$key} unless grep { $key eq $_ } @valid_params; + } + $patrons = Koha::Patrons->search($params); + } else { + $patrons = Koha::Patrons->search; + } $c->$cb($patrons->unblessed, 200); } @@ -55,4 +68,146 @@ sub get { return $c->$cb($patron->unblessed, 200); } +sub add { + my ($c, $args, $cb) = @_; + + my $user = $c->stash('koha.user'); + + unless ( $user && haspermission($user->userid, {borrowers => 1}) ) { + return $c->$cb({error => "You don't have the required permission"}, 403); + } + + my $body = $c->req->json; + + # patron cardnumber and/or userid unique? + if ($body->{cardnumber} || $body->{userid}) { + my $patron = Koha::Patrons->find({cardnumber => $body->{cardnumber}, userid => $body->{userid} }); + if ($patron) { + return $c->$cb({ + error => "Patron cardnumber and userid must be unique", + conflict => { cardnumber => $patron->cardnumber, userid => $patron->userid } + }, 409); + } + } + + my $branch = Koha::Libraries->find({branchcode => $body->{branchcode} }); + unless ($branch) { + return $c->$cb({error => "Library with branchcode \"" . $body->{branchcode} . "\" does not exist"}, 404); + } + my $category = Koha::Patron::Categories->find({ categorycode => $body->{categorycode} }); + unless ($category) { + return $c->$cb({error => "Patron category \"" . $body->{categorycode} . "\" does not exist"}, 404); + } + # All OK - save new patron + + if ($body->{password}) { $body->{password} = hash_password($body->{password}) }; # bcrypt password if given + + my $patron = eval { + Koha::Patron->new($body)->store; + }; + + unless ($patron) { + return $c->$cb({error => "Something went wrong, check Koha logs for details"}, 500); + } + + return $c->$cb($patron->unblessed, 201); +} + +sub edit { + my ($c, $args, $cb) = @_; + + my $user = $c->stash('koha.user'); + + unless ( $user && haspermission($user->userid, {borrowers => 1}) ) { + return $c->$cb({error => "You don't have the required permission"}, 403); + } + + my $patron = Koha::Patrons->find($args->{borrowernumber}); + + unless ($patron) { + return $c->$cb({error => "Patron not found"}, 404); + } + + my $body = $c->req->json; + + # Can we change userid and/or cardnumber? in that case check that they are altered first + if ($body->{cardnumber} || $body->{userid}) { + if ( ($body->{cardnumber} && $body->{cardnumber} ne $patron->cardnumber) || ($body->{userid} && $body->{userid} ne $patron->userid) ) { + my $conflictingPatron = Koha::Patrons->find({cardnumber => $body->{cardnumber}, userid => $body->{userid} }); + if ($conflictingPatron) { + return $c->$cb({ + error => "Patron cardnumber and userid must be unique", + conflict => { cardnumber => $conflictingPatron->cardnumber, userid => $conflictingPatron->userid } + }, 409); + } + } + } + + if ($body->{branchcode}) { + my $branch = Koha::Libraries->find({branchcode => $body->{branchcode} }); + unless ($branch) { + return $c->$cb({error => "Library with branchcode \"" . $body->{branchcode} . "\" does not exist"}, 404); + } + } + + if ($body->{categorycode}) { + my $category = Koha::Patron::Categories->find({ categorycode => $body->{categorycode} }); + unless ($category) { + return $c->$cb({error => "Patron category \"" . $body->{categorycode} . "\" does not exist"}, 404); + } + } + # ALL OK - Update patron + # Perhaps limit/validate what should be updated here? flags, et.al. + if ($body->{password}) { $body->{password} = hash_password($body->{password}) }; # bcrypt password if given + + my $updatedpatron = eval { + $patron->set($body); + }; + + if ($updatedpatron) { + if ($updatedpatron->is_changed) { + + my $res = eval { + $updatedpatron->store; + }; + + unless ($res) { + return $c->$cb({error => "Something went wrong, check Koha logs for details"}, 500); + } + return $c->$cb($res->unblessed, 200); + + } else { + return $c->$cb({}, 204); # No Content = No changes made + } + } else { + return $c->$cb({error => "Something went wrong, check Koha logs for details"}, 500); + } +} + +sub delete { + my ($c, $args, $cb) = @_; + my $user = $c->stash('koha.user'); + + unless ( $user && haspermission($user->userid, {borrowers => 1}) ) { + return $c->$cb({error => "You don't have the required permission"}, 403); + } + + my $patron = Koha::Patrons->find($args->{borrowernumber}); + + unless ($patron) { + return $c->$cb({error => "Patron not found"}, 404); + } + + # check if loans, reservations, debarrment, etc. before deletion! + my $res = $patron->delete; + + if ($res eq '1') { + return $c->$cb({}, 200); + } elsif ($res eq '-1') { + return $c->$cb({}, 404); + } else { + return $c->$cb({}, 400); + } +} + 1; diff --git a/api/v1/definitions/accountline.json b/api/v1/definitions/accountline.json new file mode 100644 index 0000000..d4b612e --- /dev/null +++ b/api/v1/definitions/accountline.json @@ -0,0 +1,56 @@ +{ + "type": "object", + "properties": { + "accountlines_id": { + "description": "Internal account line identifier" + }, + "borrowernumber": { + "description": "Internal borrower identifier" + }, + "accountno": { + "description": "?" + }, + "itemnumber": { + "description": "Internal item identifier" + }, + "date": { + "description": "Date when the account line was created" + }, + "time": { + "description": "Time when the account line was created" + }, + "amount": { + "description": "Amount" + }, + "description": { + "description": "Description of account line" + }, + "accounttype": { + "description": "Type of accountline" + }, + "amountoutstanding": { + "description": "Amount outstanding" + }, + "lastincrement": { + "description": "?" + }, + "timestamp": { + "description": "When the account line was last updated" + }, + "notify_id": { + "description": "?" + }, + "notify_level": { + "description": "?" + }, + "note": { + "description": "Accountline note" + }, + "manager_id": { + "description": "Borrowernumber of user that created the account line" + }, + "meansofpayment": { + "description": "Means of payment" + } + } +} diff --git a/api/v1/definitions/amountpaid.json b/api/v1/definitions/amountpaid.json new file mode 100644 index 0000000..589183f --- /dev/null +++ b/api/v1/definitions/amountpaid.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "amount": { + "type": "number", + "description": "Amount paid" + } + } +} 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..ae70624 --- /dev/null +++ b/api/v1/definitions/availability.json @@ -0,0 +1,57 @@ +{ + "type": "object", + "properties": { + "barcode": { + "type": ["string", "null"], + "description": "item barcode" + }, + "biblioitemnumber": { + "type": "string", + "description": "internally assigned biblio item identifier" + }, + "biblionumber": { + "type": "string", + "description": "internally assigned biblio identifier" + }, + "checkout": { + "$ref": "availabilitystatus.json" + }, + "expected_available": { + "type": ["string", "null"], + "description": "date this item is expected to be available" + }, + "hold": { + "$ref": "availabilitystatus.json" + }, + "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" + }, + "local_use": { + "$ref": "availabilitystatus.json" + }, + "location": { + "type": ["string", "null"], + "description": "authorized value for the shelving location for this item" + }, + "onsite_checkout": { + "$ref": "availabilitystatus.json" + } + } +} diff --git a/api/v1/definitions/availabilitystatus.json b/api/v1/definitions/availabilitystatus.json new file mode 100644 index 0000000..2bf1ea6 --- /dev/null +++ b/api/v1/definitions/availabilitystatus.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "properties": { + "available": { + "type": "boolean", + "description": "availability status" + }, + "description": { + "type": "array", + "items": { + "type": ["string", "null"], + "description": "more information on availability" + } + } + } +} diff --git a/api/v1/definitions/biblio.json b/api/v1/definitions/biblio.json new file mode 100644 index 0000000..a3b35b7 --- /dev/null +++ b/api/v1/definitions/biblio.json @@ -0,0 +1,65 @@ +{ + "type": "object", + "properties": { + "biblionumber": { + "type": "string", + "description": "internal bibliographic record identifier" + }, + "frameworkcode": { + "type": "string", + "description": "foreign key from the biblio_framework table to identify which framework was used in cataloging this record" + }, + "author": { + "type": ["string", "null"], + "description": "statement of responsibility from MARC record (100$a in MARC21)" + }, + "title": { + "type": ["string", "null"], + "description": "title (without the subtitle) from the MARC record (245$a in MARC21)" + }, + "untitle": { + "type": ["string", "null"], + "description": "uniform title (without the subtitle) from the MARC record (240$a in MARC21)" + }, + "notes": { + "type": ["string", "null"], + "description": "values from the general notes field in the MARC record (500$a in MARC21) split by bar (|)" + }, + "serial": { + "type": ["string", "null"], + "description": "Boolean indicating whether biblio is for a serial" + }, + "seriestitle": { + "type": ["string", "null"], + "description": "Title for describing the series" + }, + "copyrightdate": { + "type": ["string", "null"], + "description": "publication or copyright date from the MARC record" + }, + "timestamp": { + "type": "string", + "description": "date and time this record was last touched" + }, + "datecreated": { + "type": "string", + "description": "the date this record was added to Koha" + }, + "abstract": { + "type": ["string", "null"], + "description": "summary from the MARC record (520$a in MARC21)" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "itemnumber": { + "type": "string", + "description": "internal item identifier" + } + } + } + } + } +} diff --git a/api/v1/definitions/checkout.json b/api/v1/definitions/checkout.json index c56c6df..24bfec3 100644 --- a/api/v1/definitions/checkout.json +++ b/api/v1/definitions/checkout.json @@ -6,11 +6,11 @@ "description": "internally assigned checkout identifier" }, "borrowernumber": { - "type": "string", + "type": ["string", "null"], "description": "internally assigned user identifier" }, "itemnumber": { - "type": "string", + "type": ["string", "null"], "description": "internally assigned item identifier" }, "date_due": { diff --git a/api/v1/definitions/editAccountlineBody.json b/api/v1/definitions/editAccountlineBody.json new file mode 100644 index 0000000..6cb2f2f --- /dev/null +++ b/api/v1/definitions/editAccountlineBody.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "properties": { + "amount": { + "description": "Amount" + }, + "amountoutstanding": { + "description": "Amount outstanding" + }, + "note": { + "description": "Accountline note" + }, + "meansofpayment": { + "description": "Means of payment" + } + } +} diff --git a/api/v1/definitions/index.json b/api/v1/definitions/index.json index d3dea2e..5918207 100644 --- a/api/v1/definitions/index.json +++ b/api/v1/definitions/index.json @@ -1,8 +1,18 @@ { + "availability": { "$ref": "availability.json" }, + "availabilities": { "$ref": "availabilities.json" }, + "amountpaid": { "$ref": "amountpaid.json" }, + "accountline": { "$ref": "accountline.json" }, + "editAccountlineBody": { "$ref": "editAccountlineBody.json" }, + "partialpayAccountlineBody": { "$ref": "partialpayAccountlineBody.json" }, "patron": { "$ref": "patron.json" }, "checkouts": { "$ref": "checkouts.json" }, "checkout": { "$ref": "checkout.json" }, "holds": { "$ref": "holds.json" }, "hold": { "$ref": "hold.json" }, + "libraries": { "$ref": "libraries.json" }, + "library": { "$ref": "library.json" }, + "item": { "$ref": "item.json" }, + "biblio": { "$ref": "biblio.json" }, "error": { "$ref": "error.json" } } diff --git a/api/v1/definitions/item.json b/api/v1/definitions/item.json new file mode 100644 index 0000000..9c43f85 --- /dev/null +++ b/api/v1/definitions/item.json @@ -0,0 +1,177 @@ +{ + "type": "object", + "properties": { + "itemnumber": { + "type": "string", + "description": "internally assigned item identifier" + }, + "biblionumber": { + "type": "string", + "description": "internally assigned biblio identifier" + }, + "biblioitemnumber": { + "type": "string", + "description": "internally assigned biblio item identifier" + }, + "barcode": { + "type": ["string", "null"], + "description": "item barcode" + }, + "dateaccessioned": { + "type": ["string", "null"], + "description": "date the item was acquired or added to Koha" + }, + "booksellerid": { + "type": ["string", "null"], + "description": "where the item was purchased" + }, + "homebranch": { + "type": ["string", "null"], + "description": "library that owns this item" + }, + "price": { + "type": ["string", "null"], + "description": "purchase price" + }, + "replacementprice": { + "type": ["string", "null"], + "description": "cost the library charges to replace the item if it has been marked lost" + }, + "replacementpricedate": { + "type": ["string", "null"], + "description": "the date the price is effective from" + }, + "datelastborrowed": { + "type": ["string", "null"], + "description": "the date the item was last checked out/issued" + }, + "datelastseen": { + "type": ["string", "null"], + "description": "the date the item was last see (usually the last time the barcode was scanned or inventory was done)" + }, + "stack": { + "type": ["string", "null"], + "description": "?" + }, + "notforloan": { + "type": "string", + "description": "authorized value defining why this item is not for loan" + }, + "damaged": { + "type": "string", + "description": "authorized value defining this item as damaged" + }, + "itemlost": { + "type": "string", + "description": "authorized value defining this item as lost" + }, + "itemlost_on": { + "type": ["string", "null"], + "description": "the date and time an item was last marked as lost, NULL if not lost" + }, + "withdrawn": { + "type": "string", + "description": "authorized value defining this item as withdrawn" + }, + "withdrawn_on": { + "type": ["string", "null"], + "description": "the date and time an item was last marked as withdrawn, NULL if not withdrawn" + }, + "itemcallnumber": { + "type": ["string", "null"], + "description": "call number for this item" + }, + "coded_location_qualifier": { + "type": ["string", "null"], + "description": "coded location qualifier" + }, + "issues": { + "type": ["string", "null"], + "description": "number of times this item has been checked out/issued" + }, + "renewals": { + "type": ["string", "null"], + "description": "number of times this item has been renewed" + }, + "reserves": { + "type": ["string", "null"], + "description": "number of times this item has been placed on hold/reserved" + }, + "restricted": { + "type": ["string", "null"], + "description": "authorized value defining use restrictions for this item" + }, + "itemnotes": { + "type": ["string", "null"], + "description": "public notes on this item" + }, + "itemnotes_nonpublic": { + "type": ["string", "null"], + "description": "non-public notes on this item" + }, + "holdingbranch": { + "type": ["string", "null"], + "description": "library that is currently in possession item" + }, + "paidfor": { + "type": ["string", "null"], + "description": "?" + }, + "timestamp": { + "type": "string", + "description": "date and time this item was last altered" + }, + "location": { + "type": ["string", "null"], + "description": "authorized value for the shelving location for this item" + }, + "permanent_location": { + "type": ["string", "null"], + "description": "linked to the CART and PROC temporary locations feature, stores the permanent shelving location" + }, + "onloan": { + "type": ["string", "null"], + "description": "defines if item is checked out (NULL for not checked out, and checkout date for checked out)" + }, + "cn_source": { + "type": ["string", "null"], + "description": "classification source used on this item" + }, + "cn_sort": { + "type": ["string", "null"], + "description": "?" + }, + "ccode": { + "type": ["string", "null"], + "description": "authorized value for the collection code associated with this item" + }, + "materials": { + "type": ["string", "null"], + "description": "materials specified" + }, + "uri": { + "type": ["string", "null"], + "description": "URL for the item" + }, + "itype": { + "type": ["string", "null"], + "description": "itemtype defining the type for this item" + }, + "more_subfields_xml": { + "type": ["string", "null"], + "description": "additional 952 subfields in XML format" + }, + "enumchron": { + "type": ["string", "null"], + "description": "serial enumeration/chronology for the item" + }, + "copynumber": { + "type": ["string", "null"], + "description": "copy number" + }, + "stocknumber": { + "type": ["string", "null"], + "description": "inventory number" + } + } +} diff --git a/api/v1/definitions/libraries.json b/api/v1/definitions/libraries.json new file mode 100644 index 0000000..a79fd34 --- /dev/null +++ b/api/v1/definitions/libraries.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "library.json" } +} diff --git a/api/v1/definitions/library.json b/api/v1/definitions/library.json new file mode 100644 index 0000000..f841899 --- /dev/null +++ b/api/v1/definitions/library.json @@ -0,0 +1,77 @@ +{ + "type": "object", + "properties": { + "branchcode": { + "type": "string", + "description": "Internal library identifier" + }, + "branchname": { + "type": "string", + "description": "Printable name of library" + }, + "branchaddress1": { + "type": ["string", "null"], + "description": "the first address line of the library" + }, + "branchaddress2": { + "type": ["string", "null"], + "description": "the second address line of the library" + }, + "branchaddress3": { + "type": ["string", "null"], + "description": "the third address line of the library" + }, + "branchzip": { + "type": ["string", "null"], + "description": "the zip or postal code of the library" + }, + "branchcity": { + "type": ["string", "null"], + "description": "the city or province of the library" + }, + "branchstate": { + "type": ["string", "null"], + "description": "the reqional state of the library" + }, + "branchcountry": { + "type": ["string", "null"], + "description": "the county of the library" + }, + "branchphone": { + "type": ["string", "null"], + "description": "the primary phone of the library" + }, + "branchfax": { + "type": ["string", "null"], + "description": "the fax number of the library" + }, + "branchemail": { + "type": ["string", "null"], + "description": "the primary email address of the library" + }, + "branchreplyto": { + "type": ["string", "null"], + "description": "the email to be used as a Reply-To" + }, + "branchreturnpath": { + "type": ["string", "null"], + "description": "the email to be used as Return-Path" + }, + "branchurl": { + "type": ["string", "null"], + "description": "the URL for your library or branch's website" + }, + "branchip": { + "type": ["string", "null"], + "description": "the IP address for your library or branch" + }, + "branchnotes": { + "type": ["string", "null"], + "description": "notes related to your library or branch" + }, + "opac_info": { + "type": ["string", "null"], + "description": "HTML that displays in OPAC" + } + } +} diff --git a/api/v1/definitions/partialpayAccountlineBody.json b/api/v1/definitions/partialpayAccountlineBody.json new file mode 100644 index 0000000..a49f229 --- /dev/null +++ b/api/v1/definitions/partialpayAccountlineBody.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "amount": { + "description": "Amount to pay" + }, + "note": { + "description": "Payment note" + } + } +} diff --git a/api/v1/swagger.json b/api/v1/swagger.json index 68eeea6..b8390b8 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", @@ -38,6 +66,55 @@ } } } + }, + "post": { + "operationId": "addPatron", + "tags": ["patrons"], + "parameters": [{ + "name": "body", + "in": "body", + "description": "A JSON object containing information about the new patron", + "required": true, + "schema": { + "$ref": "#/definitions/patron" + } + }], + "consumes": ["application/json"], + "produces": ["application/json"], + "responses": { + "201": { + "description": "A successfully created patron", + "schema": { + "items": { + "$ref": "#/definitions/patron" + } + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "#/definitions/error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/error" + } + }, + "409": { + "description": "Conflict in creating resource", + "schema": { + "$ref": "#/definitions/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/error" + } + } + } } }, "/patrons/{borrowernumber}": { @@ -72,6 +149,94 @@ } } } + }, + "put": { + "operationId": "editPatron", + "tags": ["patrons"], + "parameters": [ + { "$ref": "#/parameters/borrowernumberPathParam" }, + { + "name": "body", + "in": "body", + "description": "A JSON object containing new information about existing patron", + "required": true, + "schema": { + "$ref": "#/definitions/patron" + } + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "responses": { + "200": { + "description": "A successfully updated patron", + "schema": { + "items": { + "$ref": "#/definitions/patron" + } + } + }, + "204": { + "description": "No Content", + "schema": { + "type": "object" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "#/definitions/error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/error" + } + }, + "409": { + "description": "Conflict in updating resource", + "schema": { + "$ref": "#/definitions/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + }, + "delete": { + "operationId": "deletePatron", + "tags": ["patrons"], + "parameters": [ + { "$ref": "#/parameters/borrowernumberPathParam" } + ], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Patron deleted successfully", + "schema": { + "type": "object" + } + }, + "400": { + "description": "Patron deletion failed", + "schema": { "$ref": "#/definitions/error" } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "#/definitions/error" + } + }, + "404": { + "description": "Patron not found", + "schema": { "$ref": "#/definitions/error" } + } + } } }, "/holds": { @@ -333,6 +498,75 @@ } } }, + "/accountlines": { + "get": { + "operationId": "listAccountlines", + "tags": ["accountlines"], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A list of accountlines", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/accountline" + } + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, + "/libraries": { + "get": { + "operationId": "listLibrary", + "tags": ["libraries"], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A list of libraries", + "schema": { + "$ref": "#/definitions/libraries" + } + } + } + } + }, + "/libraries/{branchcode}": { + "get": { + "operationId": "getLibrary", + "tags": ["libraries"], + "parameters": [ + { "$ref": "#/parameters/branchcodePathParam" } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A library", + "schema": { + "$ref": "#/definitions/library" + } + }, + "404": { + "description": "Library not found", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, "/checkouts": { "get": { "operationId": "listCheckouts", @@ -426,12 +660,294 @@ } } } + }, + "/checkouts/history": { + "get": { + "operationId": "listhistoryCheckouts", + "tags": ["borrowers", "checkouts"], + "parameters": [ + { + "name": "borrowernumber", + "in": "query", + "description": "Internal patron identifier", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A list of checkouts history", + "schema": { + "$ref": "#/definitions/checkouts" + } + }, + "403": { + "description": "Access forbidden", + "schema": { "$ref": "#/definitions/error" } + }, + "404": { + "description": "Borrower not found", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, + "/checkouts/history/{checkout_id}": { + "get": { + "operationId": "gethistoryCheckout", + "tags": ["borrowers", "checkouts"], + "parameters": [ + { + "name": "checkout_id", + "in": "path", + "description": "Internal checkout identifier", + "required": true, + "type": "integer" + } + ], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Got borrower's checkout", + "schema": { "$ref": "#/definitions/checkout" } + }, + "403": { + "description": "Access forbidden", + "schema": { "$ref": "#/definitions/error" } + }, + "404": { + "description": "Checkout not found", + "schema": { "$ref": "#/definitions/error" } + } + } + } + }, + "/items/{itemnumber}": { + "get": { + "operationId": "getItem", + "tags": ["items"], + "parameters": [ + { "$ref": "#/parameters/itemnumberPathParam" } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "responses": { + "200": { + "description": "An item", + "schema": { "$ref": "#/definitions/item" } + }, + "400": { + "description": "Missing or wrong parameters", + "schema": { "$ref": "#/definitions/error" } + }, + "404": { + "description": "Item not found", + "schema": { "$ref": "#/definitions/error" } + } + } + } + }, + "/biblios/{biblionumber}": { + "get": { + "operationId": "getBiblio", + "tags": ["biblios"], + "parameters": [ + { "$ref": "#/parameters/biblionumberPathParam" } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "responses": { + "200": { + "description": "An biblio", + "schema": { "$ref": "#/definitions/biblio" } + }, + "400": { + "description": "Missing or wrong parameters", + "schema": { "$ref": "#/definitions/error" } + }, + "404": { + "description": "Biblio not found", + "schema": { "$ref": "#/definitions/error" } + } + } + } + }, + "/accountlines/{accountlines_id}": { + "put": { + "operationId": "editAccountlines", + "tags": ["accountlines"], + "produces": [ + "application/json" + ], + "parameters": [ + { "$ref": "#/parameters/accountlinesIdPathParam" }, + { + "name": "body", + "in": "body", + "description": "A JSON object containing fields to modify", + "required": true, + "schema": { "$ref": "#/definitions/editAccountlineBody" } + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Updated accountline", + "schema": { "$ref": "#/definitions/accountline" } + }, + "400": { + "description": "Missing or wrong parameters", + "schema": { "$ref": "#/definitions/error" } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "#/definitions/error" + } + }, + "404": { + "description": "Accountline not found", + "schema": { "$ref": "#/definitions/error" } + } + } + } + }, + "/accountlines/{accountlines_id}/payment": { + "put": { + "operationId": "payAccountlines", + "tags": ["accountlines"], + "produces": [ + "application/json" + ], + "parameters": [ + { "$ref": "#/parameters/accountlinesIdPathParam" } + ], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Paid accountline", + "schema": { "$ref": "#/definitions/accountline" } + }, + "400": { + "description": "Missing or wrong parameters", + "schema": { "$ref": "#/definitions/error" } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "#/definitions/error" + } + }, + "404": { + "description": "Accountline not found", + "schema": { "$ref": "#/definitions/error" } + } + } + } + }, + "/accountlines/{accountlines_id}/partialpayment": { + "put": { + "operationId": "partialpayAccountlines", + "tags": ["accountlines"], + "produces": [ + "application/json" + ], + "parameters": [ + { "$ref": "#/parameters/accountlinesIdPathParam" }, + { + "name": "body", + "in": "body", + "description": "A JSON object containing fields to modify", + "required": true, + "schema": { "$ref": "#/definitions/partialpayAccountlineBody" } + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Paid accountline", + "schema": { "$ref": "#/definitions/accountline" } + }, + "400": { + "description": "Missing or wrong parameters", + "schema": { "$ref": "#/definitions/error" } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "#/definitions/error" + } + }, + "404": { + "description": "Accountline not found", + "schema": { "$ref": "#/definitions/error" } + } + } + } + }, + "/accountlines/{borrowernumber}/amountpayment": { + "put": { + "operationId": "payamountAccountlines", + "tags": ["accountlines"], + "produces": [ + "application/json" + ], + "parameters": [ + { "$ref": "#/parameters/borrowernumberPathParam" }, + { + "name": "body", + "in": "body", + "description": "A JSON object containing fields to modify", + "required": true, + "schema": { "$ref": "#/definitions/partialpayAccountlineBody" } + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Amount paid", + "schema": { "$ref": "#/definitions/amountpaid" } + }, + "400": { + "description": "Missing or wrong parameters", + "schema": { "$ref": "#/definitions/error" } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "#/definitions/error" + } + }, + "404": { + "description": "Borrower not found", + "schema": { "$ref": "#/definitions/error" } + } + } + } } }, "definitions": { "$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", @@ -445,6 +961,44 @@ "description": "Internal hold identifier", "required": true, "type": "integer" + }, + "branchcodePathParam": { + "name": "branchcode", + "in": "path", + "description": "Internal library identifier", + "required": true, + "type": "string" + }, + "itemnumberPathParam": { + "name": "itemnumber", + "in": "path", + "description": "Internal item identifier", + "required": true, + "type": "integer" + }, + "biblionumberPathParam": { + "name": "biblionumber", + "in": "path", + "description": "Internal biblio identifier", + "required": true, + "type": "integer" + }, + "itemnumbersQueryParam": { + "name": "itemnumber", + "in": "query", + "description": "Internal items identifier", + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "ssv" + }, + "accountlinesIdPathParam": { + "name": "accountlines_id", + "in": "path", + "description": "Internal accountline identifier", + "required": true, + "type": "integer" } } } diff --git a/t/Koha/Item/Availability.t b/t/Koha/Item/Availability.t new file mode 100644 index 0000000..5322c19 --- /dev/null +++ b/t/Koha/Item/Availability.t @@ -0,0 +1,71 @@ +#!/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 => 16; + +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->add_description("such available"); +$availability->add_description("wow"); +$availability->add_description("wow"); + +ok($availability->has_description("wow"), "Found description 'wow'"); +ok($availability->has_description(["wow", "such available"]), + "Found description 'wow' and 'such available'"); +is($availability->has_description(["wow", "much not found"]), 0, + "Didn't find 'wow' and 'much not found'"); +is($availability->{description}[0], "such available", + "Found correct description in correct index 1/4"); +is($availability->{description}[1], "wow", + "Found correct description in correct index 2/2"); + +$availability->add_description(["much description", "very doge"]); +is($availability->{description}[2], "much description", + "Found correct description in correct index 3/4"); +is($availability->{description}[3], "very doge", + "Found correct description in correct index 4/4"); + +$availability->del_description("wow"); +is($availability->{description}[1], "much description", + "Found description from correct index after del"); +$availability->del_description(["very doge", "such available"]); +is($availability->{description}[0], "much description", + "Found description from correct index after del"); + + +my $availability_clone = $availability; +$availability->set_unavailable; +is($availability_clone->{available}, $availability->{available}, + "Availability_clone points to availability"); +$availability_clone = $availability->clone; +$availability->set_available; +isnt($availability_clone->{available}, $availability->{available}, + "Availability_clone was cloned and no longer has same availability status"); + +$availability->reset; +is($availability->{available}, undef, "Availability reset"); diff --git a/t/db_dependent/Items.t b/t/db_dependent/Items.t index b494a70..f612995 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; @@ -432,7 +437,7 @@ subtest 'SearchItems test' => sub { subtest 'Koha::Item(s) tests' => sub { - plan tests => 5; + plan tests => 40; $schema->storage->txn_begin(); @@ -443,6 +448,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 +475,101 @@ 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->availability_for_checkout(); + is (ref($availability), 'Koha::Item::Availability', 'Got Koha::Item::Availability'); + is( $availability->{available}, 1, "Item is available" ); + $availability = $item->availability_for_local_use(); + is( $availability->{available}, 1, "Item is available for local use" ); + t::lib::Mocks::mock_preference('OnSiteCheckouts', 0); + $availability = $item->availability_for_onsite_checkout(); + is( $availability->{available}, 0, "Not available for on-site checkouts" ); + is( $availability->{description}[0], "onsite_checkouts_disabled", "Availability description is 'onsite_checkouts_disabled'" ); + t::lib::Mocks::mock_preference('OnSiteCheckouts', 1); + $availability = $item->availability_for_onsite_checkout(); + is( $availability->{available}, 1, "Available for on-site checkouts" ); + + $item->set({ onloan => "", damaged => 1 })->store(); + t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Damaged item unavailable" ); + is( $availability->{description}[0], "damaged", "Availability description is 'damaged'" ); + $availability = $item->availability_for_local_use(); + is( $availability->{available}, 0, "Item is not available for local use" ); + $availability = $item->availability_for_onsite_checkout(); + is( $availability->{available}, 0, "Item is not available for on-site checkouts" ); + t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 1); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 1, "Damaged item available" ); + is( $availability->{description}[0], "damaged", "Availability description is 'damaged'" ); + $availability = $item->availability_for_local_use(); + is( $availability->{available}, 1, "Item is available for local use" ); + $availability = $item->availability_for_onsite_checkout(); + is( $availability->{available}, 1, "Item is available for on-site checkouts" ); + + $item->set({ damaged => 0, withdrawn => 1 })->store(); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{description}[0], "withdrawn", "Availability description is 'withdrawn'" ); + + $item->set({ withdrawn => 0, itemlost => 1 })->store(); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{description}[0], "itemlost", "Availability description is 'itemlost'" ); + + $item->set({ itemlost => 0, restricted => 1 })->store(); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{description}[0], "restricted", "Availability description is 'restricted'" ); + + $item->set({ restricted => 0, notforloan => 1 })->store(); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{description}[0], "notforloan", "Availability description is 'notforloan'" ); + $availability = $item->availability_for_local_use(); + is( $availability->{available}, 1, "Item is available for local use" ); + $availability = $item->availability_for_onsite_checkout(); + is( $availability->{available}, C4::Context->preference('OnSiteCheckouts'), "Good availability for on-site checkouts" ); + + $item->set({ notforloan => 0, itype => $itemtype->{itemtype} })->store(); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{description}[0], "notforloan", "Availability description is 'notforloan' (itemtype)" ); + $availability = $item->availability_for_local_use(); + is( $availability->{available}, 1, "Item is available for local use" ); + $availability = $item->availability_for_onsite_checkout(); + is( $availability->{available}, C4::Context->preference('OnSiteCheckouts'), "Good availability for on-site checkouts" ); + + $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->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{description}[0], "reserved", "Availability description is 'reserved'" ); + $availability = $item->availability_for_reserve(); + is( $availability->{available}, 1, "Item is available for reserve" ); + CancelReserve({ reserve_id => $reserve->reserve_id }); + + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 1, "Item is available" ); + + my $module = new Test::MockModule('C4::Context'); + $module->mock( 'userenv', sub { { branch => $borrower->{branchcode} } } ); + my $issue = AddIssue($borrower, $item->barcode, undef, 1); + $item = Koha::Items->find($item->itemnumber); # refresh item + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $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; }; diff --git a/t/db_dependent/api/v1/accountlines.t b/t/db_dependent/api/v1/accountlines.t new file mode 100644 index 0000000..d13a447 --- /dev/null +++ b/t/db_dependent/api/v1/accountlines.t @@ -0,0 +1,246 @@ +#!/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 => 46; +use Test::Mojo; +use t::lib::TestBuilder; + +use C4::Auth; +use C4::Context; + +use Koha::Database; + +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 $categorycode = $builder->build({ source => 'Category' })->{ categorycode }; +my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode }; + +$t->get_ok('/api/v1/accountlines') + ->status_is(403); + +$t->put_ok("/api/v1/accountlines/11224409" => json => {'amount' => -5}) + ->status_is(403); + +$t->put_ok("/api/v1/accountlines/11224408/payment") + ->status_is(403); + +$t->put_ok("/api/v1/accountlines/11224407/partialpayment" => json => {'amount' => 8}) + ->status_is(403); + +my $loggedinuser = $builder->build({ + source => 'Borrower', + value => { + branchcode => $branchcode, + categorycode => $categorycode, + flags => 1024 + } +}); + +my $borrower = $builder->build({ + source => 'Borrower', + value => { + branchcode => $branchcode, + categorycode => $categorycode, + } +}); + +my $borrower2 = $builder->build({ + source => 'Borrower', + value => { + branchcode => $branchcode, + categorycode => $categorycode, + } +}); +my $borrowernumber = $borrower->{borrowernumber}; +my $borrowernumber2 = $borrower2->{borrowernumber}; + +$dbh->do(q| DELETE FROM accountlines |); +$dbh->do(q| + INSERT INTO accountlines (borrowernumber, amount, accounttype, amountoutstanding) + VALUES (?, 20, 'A', 20), (?, 40, 'F', 40), (?, 80, 'F', 80), (?, 10, 'F', 10) + |, undef, $borrowernumber, $borrowernumber, $borrowernumber, $borrowernumber2); + +my $session = C4::Auth::get_session(''); +$session->param('number', $loggedinuser->{ borrowernumber }); +$session->param('id', $loggedinuser->{ userid }); +$session->param('ip', '127.0.0.1'); +$session->param('lasttime', time()); +$session->flush; + +my $tx = $t->ua->build_tx(GET => "/api/v1/accountlines?borrowernumber=$borrowernumber"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(200); + +my $json = $t->tx->res->json; +ok(ref $json eq 'ARRAY', 'response is a JSON array'); +ok(scalar @$json == 3, 'response array contains 3 elements'); + +$tx = $t->ua->build_tx(GET => "/api/v1/accountlines"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(200); + +$json = $t->tx->res->json; +ok(ref $json eq 'ARRAY', 'response is a JSON array'); +ok(scalar @$json == 4, 'response array contains 3 elements'); + +# Editing accountlines tests +my $put_data = { + 'amount' => -19, + 'amountoutstanding' => -19 +}; + + +$tx = $t->ua->build_tx( + PUT => "/api/v1/accountlines/11224409" + => json => $put_data); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(404); + +my $accountline_to_edit = Koha::Accountlines->search({'borrowernumber' => $borrowernumber2})->unblessed()->[0]; + +$tx = $t->ua->build_tx( + PUT => "/api/v1/accountlines/$accountline_to_edit->{accountlines_id}" + => json => $put_data); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(200); + +my $accountline_edited = Koha::Accountlines->search({'borrowernumber' => $borrowernumber2})->unblessed()->[0]; + +is($accountline_edited->{amount}, '-19.000000'); +is($accountline_edited->{amountoutstanding}, '-19.000000'); + + +# Payment tests +$tx = $t->ua->build_tx(PUT => "/api/v1/accountlines/4562765765/payment"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(404); + +my $accountline_to_pay = Koha::Accountlines->search({'borrowernumber' => $borrowernumber, 'amount' => 20})->unblessed()->[0]; +$tx = $t->ua->build_tx(PUT => "/api/v1/accountlines/$accountline_to_pay->{accountlines_id}/payment"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(200); + +my $accountline_paid = Koha::Accountlines->search({'borrowernumber' => $borrowernumber, 'amount' => -20})->unblessed()->[0]; +ok($accountline_paid); + +# Partial payment tests +$put_data = { + 'amount' => 17, + 'note' => 'Partial payment' +}; + +$tx = $t->ua->build_tx( + PUT => "/api/v1/accountlines/11224419/partialpayment" + => json => $put_data); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(404); + +my $accountline_to_partiallypay = Koha::Accountlines->search({'borrowernumber' => $borrowernumber, 'amount' => 80})->unblessed()->[0]; + +$tx = $t->ua->build_tx(PUT => "/api/v1/accountlines/$accountline_to_partiallypay->{accountlines_id}/partialpayment" => json => {amount => 'foo'}); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(400); + +$tx = $t->ua->build_tx(PUT => "/api/v1/accountlines/$accountline_to_partiallypay->{accountlines_id}/partialpayment" => json => $put_data); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(200); + +$accountline_to_partiallypay = Koha::Accountlines->search({'borrowernumber' => $borrowernumber, 'amount' => 80})->unblessed()->[0]; +is($accountline_to_partiallypay->{amountoutstanding}, '63.000000'); + +my $accountline_partiallypaid = Koha::Accountlines->search({'borrowernumber' => $borrowernumber, 'amount' => 17})->unblessed()->[0]; +ok($accountline_partiallypaid); + +# Pay amount tests +my $borrower3 = $builder->build({ + source => 'Borrower', + value => { + branchcode => $branchcode, + categorycode => $categorycode, + } +}); +my $borrowernumber3 = $borrower3->{borrowernumber}; + +$dbh->do(q| + INSERT INTO accountlines (borrowernumber, amount, accounttype, amountoutstanding) + VALUES (?, 26, 'A', 26) + |, undef, $borrowernumber3); + +$t->put_ok("/api/v1/accountlines/$borrowernumber3/amountpayment" => json => {'amount' => 8}) + ->status_is(403); + +my $put_data2 = { + 'amount' => 24, + 'note' => 'Partial payment' +}; + +$tx = $t->ua->build_tx(PUT => "/api/v1/accountlines/8789798797/amountpayment" => json => $put_data2); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(404); + +$tx = $t->ua->build_tx(PUT => "/api/v1/accountlines/$borrowernumber3/amountpayment" => json => {amount => 0}); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(400); + +$tx = $t->ua->build_tx(PUT => "/api/v1/accountlines/$borrowernumber3/amountpayment" => json => {amount => 'foo'}); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(400); + +$tx = $t->ua->build_tx(PUT => "/api/v1/accountlines/$borrowernumber3/amountpayment" => json => $put_data2); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(200); + +$accountline_partiallypaid = Koha::Accountlines->search({'borrowernumber' => $borrowernumber3, 'amount' => 26})->unblessed()->[0]; + +is($accountline_partiallypaid->{amountoutstanding}, '2.000000'); + +$dbh->rollback; diff --git a/t/db_dependent/api/v1/availability.t b/t/db_dependent/api/v1/availability.t new file mode 100644 index 0000000..b3d18bf --- /dev/null +++ b/t/db_dependent/api/v1/availability.t @@ -0,0 +1,289 @@ +#!/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 => 165; +use Test::Mojo; +use t::lib::Mocks; +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 $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}; + +my $module = new Test::MockModule('C4::Context'); +$module->mock( 'userenv', sub { { branch => $borrower->{branchcode} } } ); + +# $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); + +t::lib::Mocks::mock_preference('OnSiteCheckouts', 0); +t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0); +# 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/checkout/available', Mojo::JSON->true) + ->json_is('/0/checkout/description', []) + ->json_is('/0/hold/available', Mojo::JSON->true) + ->json_is('/0/hold/description', []) + ->json_is('/0/local_use/available', Mojo::JSON->true) + ->json_is('/0/local_use/description', []) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["onsite_checkouts_disabled"]) + ->json_is('/0/hold_queue_length', 0); +t::lib::Mocks::mock_preference('OnSiteCheckouts', 1); +$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/checkout/available', Mojo::JSON->true) + ->json_is('/0/checkout/description', []) + ->json_is('/0/hold/available', Mojo::JSON->true) + ->json_is('/0/hold/description', []) + ->json_is('/0/local_use/available', Mojo::JSON->true) + ->json_is('/0/local_use/description', []) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->true) + ->json_is('/0/onsite_checkout/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/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description/0', "notforloan") + ->json_is('/0/hold/available', Mojo::JSON->false) + ->json_is('/0/hold/description', ["notforloan"]) + ->json_is('/0/local_use/available', Mojo::JSON->true) + ->json_is('/0/local_use/description', []) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->true) + ->json_is('/0/onsite_checkout/description', []) + ->json_is('/0/hold_queue_length', 0); +t::lib::Mocks::mock_preference('OnSiteCheckouts', 0); +$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/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description', ["notforloan"]) + ->json_is('/0/hold/available', Mojo::JSON->false) + ->json_is('/0/hold/description', ["notforloan"]) + ->json_is('/0/local_use/available', Mojo::JSON->true) + ->json_is('/0/local_use/description', []) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["onsite_checkouts_disabled"]) + ->json_is('/0/hold_queue_length', 0); +t::lib::Mocks::mock_preference('OnSiteCheckouts', 1); + +# 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/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description', ["damaged"]) + ->json_is('/0/hold/available', Mojo::JSON->false) + ->json_is('/0/hold/description', ["damaged"]) + ->json_is('/0/local_use/available', Mojo::JSON->false) + ->json_is('/0/local_use/description', ["damaged"]) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["damaged"]) + ->json_is('/0/hold_queue_length', 0); +t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 1); +$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/checkout/available', Mojo::JSON->true) + ->json_is('/0/checkout/description', ["damaged"]) + ->json_is('/0/hold/available', Mojo::JSON->true) + ->json_is('/0/hold/description', ["damaged"]) + ->json_is('/0/local_use/available', Mojo::JSON->true) + ->json_is('/0/local_use/description', ["damaged"]) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->true) + ->json_is('/0/onsite_checkout/description', ["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/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description', ["withdrawn"]) + ->json_is('/0/hold/available', Mojo::JSON->false) + ->json_is('/0/hold/description', ["withdrawn"]) + ->json_is('/0/local_use/available', Mojo::JSON->false) + ->json_is('/0/local_use/description', ["withdrawn"]) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["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/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description', ["itemlost"]) + ->json_is('/0/hold/available', Mojo::JSON->false) + ->json_is('/0/hold/description', ["itemlost"]) + ->json_is('/0/local_use/available', Mojo::JSON->false) + ->json_is('/0/local_use/description', ["itemlost"]) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["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/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description', ["onloan"]) + ->json_is('/0/hold/available', Mojo::JSON->true) + ->json_is('/0/hold/description', []) + ->json_is('/0/local_use/available', Mojo::JSON->false) + ->json_is('/0/local_use/description', ["onloan"]) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["onloan"]) + ->json_is('/0/checkout/expected_available', $issue->date_due) + ->json_is('/0/local_use/expected_available', $issue->date_due) + ->json_is('/0/onsite_checkout/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/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description', ["reserved"]) + ->json_is('/0/hold/available', Mojo::JSON->true) + ->json_is('/0/hold/description', []) + ->json_is('/0/local_use/available', Mojo::JSON->false) + ->json_is('/0/local_use/description', ["reserved"]) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["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/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description/0', "notforloan") + ->json_is('/0/hold/available', Mojo::JSON->false) + ->json_is('/0/hold/description', ["notforloan"]) + ->json_is('/0/local_use/available', Mojo::JSON->true) + ->json_is('/0/local_use/description', []) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->true) + ->json_is('/0/onsite_checkout/description', []) + ->json_is('/0/hold_queue_length', 0) + ->json_is('/1/itemnumber', $itemnumber) + ->json_is('/1/biblionumber', $biblionumber) + ->json_is('/1/checkout/available', Mojo::JSON->true) + ->json_is('/1/checkout/description', []) + ->json_is('/1/hold/available', Mojo::JSON->true) + ->json_is('/1/hold/description', []) + ->json_is('/1/local_use/available', Mojo::JSON->true) + ->json_is('/1/local_use/description', []) + ->json_is('/1/onsite_checkout/available', Mojo::JSON->true) + ->json_is('/1/onsite_checkout/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, + } + }); +} diff --git a/t/db_dependent/api/v1/biblios.t b/t/db_dependent/api/v1/biblios.t new file mode 100644 index 0000000..848fb7a --- /dev/null +++ b/t/db_dependent/api/v1/biblios.t @@ -0,0 +1,116 @@ +#!/usr/bin/env perl + +# Copyright 2016 Koha-Suomi +# +# 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 => 7; +use Test::Mojo; +use t::lib::TestBuilder; + +use C4::Auth; +use C4::Biblio; +use C4::Context; +use C4::Items; + +use Koha::Database; +use Koha::Patron; +use Koha::Items; + +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 $categorycode = $builder->build({ source => 'Category' })->{ categorycode }; +my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode }; +my $borrower = $builder->build({ + source => 'Borrower', + value => { + branchcode => $branchcode, + categorycode => $categorycode, + flags => 16, + } +}); + +my $librarian = $builder->build({ + source => "Borrower", + value => { + categorycode => $categorycode, + branchcode => $branchcode, + flags => 4, + }, +}); + +my ($session) = create_session($borrower); + +my $biblio = $builder->build({ + source => 'Biblio' +}); +my $biblionumber = $biblio->{biblionumber}; +my $item1 = $builder->build({ + source => 'Item', + value => { + biblionumber => $biblionumber, + } +}); +my $item2 = $builder->build({ + source => 'Item', + value => { + biblionumber => $biblionumber, + } +}); +my $item1number = $item1->{itemnumber}; +my $item2number = $item2->{itemnumber}; + +my $nonExistentBiblionumber = -14362719; +my $tx = $t->ua->build_tx(GET => "/api/v1/biblios/$nonExistentBiblionumber"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(404); + +$tx = $t->ua->build_tx(GET => "/api/v1/biblios/$biblionumber"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(200) + ->json_is('/items/0/itemnumber' => $item1number) + ->json_is('/items/1/itemnumber' => $item2number) + ->json_is('/biblionumber' => $biblionumber); + +$dbh->rollback; + +sub create_session { + my (@borrowers) = @_; + + my @sessions; + foreach $borrower (@borrowers) { + my $session = C4::Auth::get_session(''); + $session->param('number', $borrower->{borrowernumber}); + $session->param('id', $borrower->{userid}); + $session->param('ip', '127.0.0.1'); + $session->param('lasttime', time()); + $session->flush; + push @sessions, $session; + } + + return @sessions; +} diff --git a/t/db_dependent/api/v1/checkoutshistory.t b/t/db_dependent/api/v1/checkoutshistory.t new file mode 100644 index 0000000..c9d988a --- /dev/null +++ b/t/db_dependent/api/v1/checkoutshistory.t @@ -0,0 +1,160 @@ +#!/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 => 27; +use Test::MockModule; +use Test::Mojo; +use t::lib::TestBuilder; + +use DateTime; +use MARC::Record; + +use C4::Context; +use C4::Biblio; +use C4::Circulation; +use C4::Items; + +use Koha::Database; +use Koha::Patron; +use Koha::OldIssue; +use Koha::OldIssues; + +my $schema = Koha::Database->schema; +$schema->storage->txn_begin; +my $dbh = C4::Context->dbh; +my $builder = t::lib::TestBuilder->new; +$dbh->{RaiseError} = 1; + +$ENV{REMOTE_ADDR} = '127.0.0.1'; +my $t = Test::Mojo->new('Koha::REST::V1'); + +$dbh->do('DELETE FROM issues'); +$dbh->do('DELETE FROM items'); +$dbh->do('DELETE FROM issuingrules'); +my $loggedinuser = $builder->build({ source => 'Borrower' }); + +$dbh->do(q{ + INSERT INTO user_permissions (borrowernumber, module_bit, code) + VALUES (?, 1, 'circulate_remaining_permissions') +}, undef, $loggedinuser->{borrowernumber}); + +my $session = C4::Auth::get_session(''); +$session->param('number', $loggedinuser->{ borrowernumber }); +$session->param('id', $loggedinuser->{ userid }); +$session->param('ip', '127.0.0.1'); +$session->param('lasttime', time()); +$session->flush; + +my $borrower = $builder->build({ source => 'Borrower' }); +my $borrowernumber = $borrower->{borrowernumber}; + +my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode }; +my $module = new Test::MockModule('C4::Context'); +$module->mock('userenv', sub { { branch => $branchcode } }); + +my $tx = $t->ua->build_tx(GET => "/api/v1/checkouts/history?borrowernumber=$borrowernumber"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(200) + ->json_is([]); + +my $notexisting_borrowernumber = $borrowernumber + 1; +$tx = $t->ua->build_tx(GET => "/api/v1/checkouts/history?borrowernumber=$notexisting_borrowernumber"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(404) + ->json_has('/error'); + +my $biblionumber = create_biblio('RESTful Web APIs'); +my $itemnumber1 = create_item($biblionumber, 'TEST000001'); +my $itemnumber2 = create_item($biblionumber, 'TEST000002'); + +my $date_due = DateTime->now->add(weeks => 2); + +my $issueId = Koha::OldIssues->count({}) + int rand(150000); +my $issue1; +$issue1 = Koha::OldIssue->new({ issue_id => $issueId, borrowernumber => $borrowernumber, itemnumber => $itemnumber1, date_due => $date_due}); +$issue1->store(); + +my $date_due1 = Koha::DateUtils::dt_from_string( $issue1->date_due ); + +$tx = $t->ua->build_tx(GET => "/api/v1/checkouts/history?borrowernumber=$borrowernumber"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(200) + ->json_is('/0/issue_id' => $issueId) + ->json_hasnt('/1') + ->json_hasnt('/error'); + +$tx = $t->ua->build_tx(GET => "/api/v1/checkouts/history/" . $issue1->issue_id); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(200) + ->json_is('/borrowernumber' => $borrowernumber) + ->json_is('/itemnumber' => $itemnumber1) + ->json_is('/date_due' => $date_due1->ymd . ' ' . $date_due1->hms) + ->json_hasnt('/error'); + +$issue1->delete(); + +$tx = $t->ua->build_tx(GET => "/api/v1/checkouts/history/" . $issue1->issue_id); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(404) + ->json_has('/error'); + +$tx = $t->ua->build_tx(GET => "/api/v1/checkouts/history?borrowernumber=$borrowernumber"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(200) + ->json_hasnt('/0') + ->json_hasnt('/error'); + +Koha::Patrons->find($borrowernumber)->delete(); + +$tx = $t->ua->build_tx(GET => "/api/v1/checkouts/history?borrowernumber=$borrowernumber"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(404) + ->json_has('/error'); + +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; +} diff --git a/t/db_dependent/api/v1/items.t b/t/db_dependent/api/v1/items.t new file mode 100644 index 0000000..0536e0f --- /dev/null +++ b/t/db_dependent/api/v1/items.t @@ -0,0 +1,117 @@ +#!/usr/bin/env perl + +# Copyright 2016 Koha-Suomi +# +# 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 => 12; +use Test::Mojo; +use t::lib::TestBuilder; + +use C4::Auth; +use C4::Biblio; +use C4::Context; +use C4::Items; + +use Koha::Database; +use Koha::Patron; +use Koha::Items; + +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 $categorycode = $builder->build({ source => 'Category' })->{ categorycode }; +my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode }; +my $borrower = $builder->build({ + source => 'Borrower', + value => { + branchcode => $branchcode, + categorycode => $categorycode, + flags => 16, + } +}); + +my $librarian = $builder->build({ + source => "Borrower", + value => { + categorycode => $categorycode, + branchcode => $branchcode, + flags => 4, + }, +}); + +my ($session, $session2) = create_session($borrower, $librarian); + +my $biblio = $builder->build({ + source => 'Biblio' +}); +my $biblionumber = $biblio->{biblionumber}; +my $item = $builder->build({ + source => 'Item', + value => { + biblionumber => $biblionumber, + } +}); +my $itemnumber = $item->{itemnumber}; + +my $nonExistentItemnumber = -14362719; +my $tx = $t->ua->build_tx(GET => "/api/v1/items/$nonExistentItemnumber"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(404); + +$tx = $t->ua->build_tx(GET => "/api/v1/items/$itemnumber"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(200) + ->json_is('/itemnumber' => $itemnumber) + ->json_is('/biblionumber' => $biblionumber) + ->json_is('/itemnotes_nonpublic' => undef); + +$tx = $t->ua->build_tx(GET => "/api/v1/items/$itemnumber"); +$tx->req->cookies({name => 'CGISESSID', value => $session2->id}); +$t->request_ok($tx) + ->status_is(200) + ->json_is('/itemnumber' => $itemnumber) + ->json_is('/biblionumber' => $biblionumber) + ->json_is('/itemnotes_nonpublic' => $item->{itemnotes_nonpublic}); + +$dbh->rollback; + +sub create_session { + my (@borrowers) = @_; + + my @sessions; + foreach $borrower (@borrowers) { + my $session = C4::Auth::get_session(''); + $session->param('number', $borrower->{borrowernumber}); + $session->param('id', $borrower->{userid}); + $session->param('ip', '127.0.0.1'); + $session->param('lasttime', time()); + $session->flush; + push @sessions, $session; + } + + return @sessions; +} diff --git a/t/db_dependent/api/v1/libraries.t b/t/db_dependent/api/v1/libraries.t new file mode 100644 index 0000000..1f8a936 --- /dev/null +++ b/t/db_dependent/api/v1/libraries.t @@ -0,0 +1,63 @@ +#!/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 t::lib::TestBuilder; + +use Test::More tests => 11; +use Test::Mojo; + +use C4::Auth; +use C4::Context; +use Koha::Database; + +BEGIN { + use_ok('Koha::Object'); + use_ok('Koha::Libraries'); +} + +my $schema = Koha::Database->schema; +my $dbh = C4::Context->dbh; +my $builder = t::lib::TestBuilder->new; + +$ENV{REMOTE_ADDR} = '127.0.0.1'; +my $t = Test::Mojo->new('Koha::REST::V1'); + +$schema->storage->txn_begin; + +my $branch = $builder->build({ source => 'Branch' }); + +my $tx = $t->ua->build_tx(GET => '/api/v1/libraries'); +#$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(200); + +$tx = $t->ua->build_tx(GET => "/api/v1/libraries/" . $branch->{ branchcode }); +#$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(200) + ->json_is('/branchcode' => $branch->{ branchcode }) + ->json_is('/branchname' => $branch->{ branchname }); + +$tx = $t->ua->build_tx(GET => "/api/v1/libraries/" . "nonexistent"); +$t->request_ok($tx) + ->status_is(404) + ->json_is('/error' => "Library with branchcode \"nonexistent\" not found"); + +$schema->storage->txn_rollback; diff --git a/t/db_dependent/api/v1/patrons.t b/t/db_dependent/api/v1/patrons.t index 6bd4e1a..612b191 100644 --- a/t/db_dependent/api/v1/patrons.t +++ b/t/db_dependent/api/v1/patrons.t @@ -17,28 +17,32 @@ use Modern::Perl; -use Test::More tests => 10; -use Test::Mojo; use t::lib::TestBuilder; +use Test::More tests => 64; +use Test::Mojo; + use C4::Auth; use C4::Context; - use Koha::Database; -use Koha::Patron; -my $builder = t::lib::TestBuilder->new(); +BEGIN { + use_ok('Koha::Object'); + use_ok('Koha::Patron'); +} -my $dbh = C4::Context->dbh; -$dbh->{AutoCommit} = 0; -$dbh->{RaiseError} = 1; +my $schema = Koha::Database->schema; +my $dbh = C4::Context->dbh; +my $builder = t::lib::TestBuilder->new; $ENV{REMOTE_ADDR} = '127.0.0.1'; my $t = Test::Mojo->new('Koha::REST::V1'); +$schema->storage->txn_begin; + my $categorycode = $builder->build({ source => 'Category' })->{ categorycode }; my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode }; -my $borrower = $builder->build({ +my $patron = $builder->build({ source => 'Borrower', value => { branchcode => $branchcode, @@ -46,10 +50,12 @@ my $borrower = $builder->build({ } }); +### GET /api/v1/patrons + $t->get_ok('/api/v1/patrons') ->status_is(403); -$t->get_ok("/api/v1/patrons/" . $borrower->{ borrowernumber }) +$t->get_ok("/api/v1/patrons/" . $patron->{ borrowernumber }) ->status_is(403); my $loggedinuser = $builder->build({ @@ -73,12 +79,145 @@ $tx->req->cookies({name => 'CGISESSID', value => $session->id}); $tx->req->env({REMOTE_ADDR => '127.0.0.1'}); $t->request_ok($tx) ->status_is(200); +ok(@{$tx->res->json} >= 1, 'Json response lists all when no params given'); + +$tx = $t->ua->build_tx(GET => "/api/v1/patrons/" . $patron->{ borrowernumber }); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(200) + ->json_is('/borrowernumber' => $patron->{ borrowernumber }) + ->json_is('/surname' => $patron->{ surname }); + +$tx = $t->ua->build_tx(GET => '/api/v1/patrons' => form => {surname => 'nonexistent'}); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(200); +ok(@{$tx->res->json} == 0, "Json response yields no results when params doesn't match"); -$tx = $t->ua->build_tx(GET => "/api/v1/patrons/" . $borrower->{ borrowernumber }); +$tx = $t->ua->build_tx(GET => '/api/v1/patrons' => form => {surname => $patron->{ surname }}); $tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); $t->request_ok($tx) ->status_is(200) - ->json_is('/borrowernumber' => $borrower->{ borrowernumber }) - ->json_is('/surname' => $borrower->{ surname }); + ->json_has($patron); +ok(@{$tx->res->json} == 1, 'Json response yields expected results when params match'); + +### POST /api/v1/patrons + +my $newpatron = { + branchcode => $branchcode, + categorycode => $categorycode, + surname => "TestUser", + cardnumber => "123456", + userid => "testuser" +}; + +$newpatron->{ branchcode } = "nonexistent"; # Test invalid branchcode +$tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(404) + ->json_is('/error' => "Library with branchcode \"nonexistent\" does not exist"); + +$newpatron->{ branchcode } = $branchcode; +$newpatron->{ categorycode } = "nonexistent"; # Test invalid patron category +$tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(404) + ->json_is('/error' => "Patron category \"nonexistent\" does not exist"); +$newpatron->{ categorycode } = $categorycode; + +$newpatron->{ falseproperty } = "Non existent property"; +$tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(500) + ->json_is('/error' => "Something went wrong, check Koha logs for details"); + +delete $newpatron->{ falseproperty }; +$tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(201, 'Patron created successfully') + ->json_has('/borrowernumber', 'got a borrowernumber') + ->json_is('/cardnumber', $newpatron->{ cardnumber }) + ->json_is('/surname' => $newpatron->{ surname }) + ->json_is('/firstname' => $newpatron->{ firstname }); +$newpatron->{borrowernumber} = $tx->res->json->{borrowernumber}; + +$tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(409) + ->json_has('/error', 'Fails when trying to POST duplicate cardnumber or userid') + ->json_has('/conflict', { userid => $newpatron->{ userid }, cardnumber => $newpatron->{ cardnumber } }); + +### PUT /api/v1/patrons +$tx = $t->ua->build_tx(PUT => "/api/v1/patrons/0" => json => {}); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(404) + ->json_has('/error', 'Fails when trying to PUT nonexistent patron'); + +$tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" . $newpatron->{ borrowernumber } => json => {categorycode => "nonexistent"}); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(404) + ->json_is('/error' => "Patron category \"nonexistent\" does not exist"); + +$tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" . $newpatron->{ borrowernumber } => json => {branchcode => "nonexistent"}); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(404) + ->json_is('/error' => "Library with branchcode \"nonexistent\" does not exist"); + +$newpatron->{ falseproperty } = "Non existent property"; +$tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" . $newpatron->{ borrowernumber } => json => $newpatron); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(500) + ->json_is('/error' => "Something went wrong, check Koha logs for details"); +delete $newpatron->{ falseproperty }; + +$newpatron->{ cardnumber } = $patron-> { cardnumber }; +$newpatron->{ userid } = $patron-> { userid }; +$tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" . $newpatron->{ borrowernumber } => json => $newpatron); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(409) + ->json_has('/error' => "Fails when trying to update to an existing cardnumber or userid") + ->json_has('/conflict', { cardnumber => $patron->{ cardnumber }, userid => $patron->{ userid } }); + +$newpatron->{ cardnumber } = "123456"; +$newpatron->{ userid } = "testuser"; +$tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" . $newpatron->{ borrowernumber } => json => $newpatron); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(204, 'No changes - patron NOT updated'); + +$newpatron->{ cardnumber } = "234567"; +$newpatron->{ userid } = "updatedtestuser"; +$newpatron->{ surname } = "UpdatedTestUser"; + +$tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" . $newpatron->{ borrowernumber } => json => $newpatron); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(200, 'Patron updated successfully') + ->json_has($newpatron); + +### DELETE /api/v1/patrons + +$tx = $t->ua->build_tx(DELETE => "/api/v1/patrons/0"); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(404, 'Patron not found'); + +$tx = $t->ua->build_tx(DELETE => "/api/v1/patrons/" . $newpatron->{ borrowernumber }); +$tx->req->cookies({name => 'CGISESSID', value => $session->id}); +$t->request_ok($tx) + ->status_is(200, 'Patron deleted successfully'); + +$schema->storage->txn_rollback; -$dbh->rollback; -- 2.1.4