@@ -, +, @@ xnciptoolkit drivers --- 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 | 146 +++++ 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 | 51 ++ 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 | 648 ++++++++++++++++++++++ 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, 3763 insertions(+), 16 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/Checkout.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/checkout.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 --- a/Koha/Accountline.pm +++ a/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; --- a/Koha/Accountlines.pm +++ a/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; --- a/Koha/Item.pm +++ a/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 --- a/Koha/Item/Availability.pm +++ a/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; --- a/Koha/REST/V1/Accountline.pm +++ a/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; --- a/Koha/REST/V1/Availability.pm +++ a/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; --- a/Koha/REST/V1/Biblio.pm +++ a/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; --- a/Koha/REST/V1/Checkout.pm +++ a/Koha/REST/V1/Checkout.pm @@ -0,0 +1,146 @@ +package Koha::REST::V1::Checkout; + +# 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::Circulation; +use Koha::Issues; +use Koha::OldIssues; + +sub list { + 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 $checkouts = C4::Circulation::GetIssues({ + borrowernumber => $borrowernumber + }); + + $c->$cb($checkouts, 200); +} + +sub get { + 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::Issues->find($checkout_id); + + if (!$checkout) { + return $c->$cb({ + error => "Checkout doesn't exist" + }, 404); + } + + return $c->$cb($checkout->unblessed, 200); +} + +sub renew { + 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::Issues->find($checkout_id); + + if (!$checkout) { + return $c->$cb({ + error => "Checkout doesn't exist" + }, 404); + } + + $checkout = $checkout->unblessed; + + my $borrowernumber = $checkout->borrowernumber; + my $itemnumber = $checkout->itemnumber; + + my ($can_renew, $error) = C4::Circulation::CanBookBeRenewed( + $borrowernumber, $itemnumber); + + if (!$can_renew) { + return $c->$cb({error => "Renewal not authorized ($error)"}, 403); + } + + AddRenewal($borrowernumber, $itemnumber, $checkout->{branchcode}); + $checkout = Koha::Issues->find($checkout_id); + + 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; --- a/Koha/REST/V1/Item.pm +++ a/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; --- a/Koha/REST/V1/Library.pm +++ a/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; --- a/Koha/REST/V1/Patron.pm +++ a/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; --- a/api/v1/definitions/accountline.json +++ a/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" + } + } +} --- a/api/v1/definitions/amountpaid.json +++ a/api/v1/definitions/amountpaid.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "amount": { + "type": "number", + "description": "Amount paid" + } + } +} --- a/api/v1/definitions/availabilities.json +++ a/api/v1/definitions/availabilities.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "availability.json" } +} --- a/api/v1/definitions/availability.json +++ a/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" + } + } +} --- a/api/v1/definitions/availabilitystatus.json +++ a/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" + } + } + } +} --- a/api/v1/definitions/biblio.json +++ a/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" + } + } + } + } + } +} --- a/api/v1/definitions/checkout.json +++ a/api/v1/definitions/checkout.json @@ -0,0 +1,51 @@ +{ + "type": "object", + "properties": { + "issue_id": { + "type": "string", + "description": "internally assigned checkout identifier" + }, + "borrowernumber": { + "type": ["string", "null"], + "description": "internally assigned user identifier" + }, + "itemnumber": { + "type": ["string", "null"], + "description": "internally assigned item identifier" + }, + "date_due": { + "description": "Due date" + }, + "branchcode": { + "type": ["string", "null"], + "description": "code of patron's home branch" + }, + "issuingbranch": { + "description": "Code of the branch where issue was made" + }, + "returndate": { + "description": "Date the item was returned" + }, + "lastreneweddate": { + "description": "Date the item was last renewed" + }, + "return": { + "description": "?" + }, + "renewals": { + "description": "Number of renewals" + }, + "auto_renew": { + "description": "Auto renewal" + }, + "timestamp": { + "description": "Last update time" + }, + "issuedate": { + "description": "Date the item was issued" + }, + "onsite_checkout": { + "description": "On site checkout" + } + } +} --- a/api/v1/definitions/editAccountlineBody.json +++ a/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" + } + } +} --- a/api/v1/definitions/index.json +++ a/api/v1/definitions/index.json @@ -1,6 +1,16 @@ { + "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" }, "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" } } --- a/api/v1/definitions/item.json +++ a/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" + } + } +} --- a/api/v1/definitions/libraries.json +++ a/api/v1/definitions/libraries.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "library.json" } +} --- a/api/v1/definitions/library.json +++ a/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" + } + } +} --- a/api/v1/definitions/partialpayAccountlineBody.json +++ a/api/v1/definitions/partialpayAccountlineBody.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "amount": { + "description": "Amount to pay" + }, + "note": { + "description": "Payment note" + } + } +} --- a/api/v1/swagger.json +++ a/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": { @@ -332,12 +497,457 @@ } } } + }, + "/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", + "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", + "schema": { + "$ref": "#/definitions/checkouts" + } + }, + "403": { + "description": "Access forbidden", + "schema": { "$ref": "#/definitions/error" } + }, + "404": { + "description": "Borrower not found", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, + "/checkouts/{checkout_id}": { + "get": { + "operationId": "getCheckout", + "tags": ["borrowers", "checkouts"], + "parameters": [ + { + "name": "checkout_id", + "in": "path", + "description": "Internal checkout identifier", + "required": true, + "type": "integer" + } + ], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Updated borrower's checkout", + "schema": { "$ref": "#/definitions/checkout" } + }, + "403": { + "description": "Access forbidden", + "schema": { "$ref": "#/definitions/error" } + }, + "404": { + "description": "Checkout not found", + "schema": { "$ref": "#/definitions/error" } + } + } + }, + "put": { + "operationId": "renewCheckout", + "tags": ["borrowers", "checkouts"], + "parameters": [ + { + "name": "checkout_id", + "in": "path", + "description": "Internal checkout identifier", + "required": true, + "type": "integer" + } + ], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Updated borrower's checkout", + "schema": { "$ref": "#/definitions/checkout" } + }, + "403": { + "description": "Cannot renew checkout", + "schema": { "$ref": "#/definitions/error" } + }, + "404": { + "description": "Checkout not found", + "schema": { "$ref": "#/definitions/error" } + } + } + } + }, + "/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", @@ -351,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" } } } --- a/t/Koha/Item/Availability.t +++ a/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"); --- a/t/db_dependent/Items.t +++ a/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; }; --- a/t/db_dependent/api/v1/accountlines.t +++ a/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; --- a/t/db_dependent/api/v1/availability.t +++ a/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, + } + }); +} --- a/t/db_dependent/api/v1/biblios.t +++ a/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; +} --- a/t/db_dependent/api/v1/checkoutshistory.t +++ a/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; +} --- a/t/db_dependent/api/v1/items.t +++ a/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; +} --- a/t/db_dependent/api/v1/libraries.t +++ a/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; --- a/t/db_dependent/api/v1/patrons.t +++ a/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; --