From e8a4b0245354446a008affdf9cd645b8272e193e Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Fri, 18 Nov 2016 13:10:01 +0200 Subject: [PATCH] Bug 17712: Item availability for hold Adds Koha::Item::Availability::Hold class for item hold availability. This patch adds item holdability queries in two contexts: 1. in_opac (perspective of patron themselves) 2. in_intranet (perspective of a librarian to patron) Returns a Koha::Item::Availability::Hold object, which contains all information on availability for hold in that context. This may include additional notes, reasons to ask for confirmation or reasons for unavailability. Example: To find out if patron can hold an item in OPAC, we will write: my $availability = Koha::Availability::Hold->item({ patron => $patron, item => $item, to_branch => $branchcode, # transfer allowed from holdingbranch to to_branch? })->in_opac; if ($availability->available) { # yes! } else { foreach my $reason (keys %{$availability->unavailabilities}) { # each reason for unavailability } foreach my $reason (keys %{$availability->confirmations}) { # each reason that requires confirmation } foreach my $reason (keys %{$availability->notes}) { # each additional note } } Signed-off-by: Benjamin Rokseth --- Koha/Availability/Hold.pm | 38 +++ Koha/Item/Availability/Hold.pm | 364 ++++++++++++++++++++ .../Hold/Intranet/HoldPolicyOverride.t | 90 +++++ .../Koha/Item/Availability/Hold/Opac/HoldRules.t | 290 ++++++++++++++++ .../Koha/Item/Availability/Hold/Opac/ItemStatus.t | 371 +++++++++++++++++++++ .../Item/Availability/Hold/Opac/LibraryItemRules.t | 260 +++++++++++++++ 6 files changed, 1413 insertions(+) create mode 100644 Koha/Availability/Hold.pm create mode 100644 Koha/Item/Availability/Hold.pm create mode 100644 t/db_dependent/Koha/Item/Availability/Hold/Intranet/HoldPolicyOverride.t create mode 100644 t/db_dependent/Koha/Item/Availability/Hold/Opac/HoldRules.t create mode 100644 t/db_dependent/Koha/Item/Availability/Hold/Opac/ItemStatus.t create mode 100644 t/db_dependent/Koha/Item/Availability/Hold/Opac/LibraryItemRules.t diff --git a/Koha/Availability/Hold.pm b/Koha/Availability/Hold.pm new file mode 100644 index 0000000..f2d7cb9 --- /dev/null +++ b/Koha/Availability/Hold.pm @@ -0,0 +1,38 @@ +package Koha::Availability::Hold; + +# Copyright Koha-Suomi Oy 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 Koha::Item::Availability::Hold; + +use Koha::Exceptions; + +sub new { + my ($class, $params) = @_; + + bless $params, $class; +} + +sub item { + my ($self, $params) = @_; + + return Koha::Item::Availability::Hold->new($params); +} + +1; diff --git a/Koha/Item/Availability/Hold.pm b/Koha/Item/Availability/Hold.pm new file mode 100644 index 0000000..0f423b7 --- /dev/null +++ b/Koha/Item/Availability/Hold.pm @@ -0,0 +1,364 @@ +package Koha::Item::Availability::Hold; + +# Copyright Koha-Suomi Oy 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 base qw(Koha::Item::Availability); + +use Koha::Items; +use Koha::Patrons; + +use Koha::Availability::Checks::Biblio; +use Koha::Availability::Checks::Biblioitem; +use Koha::Availability::Checks::IssuingRule; +use Koha::Availability::Checks::Item; +use Koha::Availability::Checks::LibraryItemRule; +use Koha::Availability::Checks::Patron; + +=head1 NAME + +Koha::Item::Availability::Hold - Koha Item Availability Hold object class + +=head1 SYNOPSIS + +my $holdability = Koha::Item::Availability::Hold->new({ + item => $item, # which item this availability is for + patron => $patron, # check item availability for this patron +}) + +=head1 DESCRIPTION + +Class for checking item hold availability. + +This class contains different levels of "recipes" that determine whether or not +an item should be considered available. + +=head2 Class Methods + +=cut + +=head3 new + +Constructs an item hold availability object. Item is always required. Patron is +required if patron related checks are needed. + +MANDATORY PARAMETERS + + item (or itemnumber) + +Item is a Koha::Item -object. + +OPTIONAL PARAMETERS + + patron (or borrowernumber) + to_branch + +Patron is a Koha::Patron -object. To_branch is a branchcode of pickup library. + +Returns a Koha::Item::Availability::Hold -object. + +=cut + +sub new { + my ($class, $params) = @_; + + my $self = $class->SUPER::new($params); + + # Additionally, consider any transfer limits to pickup library by + # providing to_branch parameter with branchcode of pickup library + $self->{'to_branch'} = $params->{'to_branch'}; + + return $self; +} + +sub in_intranet { + my ($self) = @_; + my $reason; + + $self->reset; + + my $item = $self->item; + my $patron; + unless ($patron = $self->patron) { + Koha::Exceptions::MissingParameter->throw( + error => 'Missing parameter patron. This level of availability query ' + .'requires Koha::Item::Availability::Hold to have a patron parameter.' + ); + } + + $self->common_biblio_checks; + $self->common_biblioitem_checks; + $self->common_issuing_rule_checks; + $self->common_item_checks; + $self->common_library_item_rule_checks; + $self->common_patron_checks; + + # Additionally, a librarian can override any unavailabilities if system + # preference AllowHoldPolicyOverride is enabled + if (C4::Context->preference('AllowHoldPolicyOverride')) { + # Copy unavailabilities to reasons to ask for confirmation, and reset + # reasons of unavailabilities + $self->confirmations({ %{$self->unavailabilities}, %{$self->confirmations} }); + $self->unavailabilities({}); + $self->available(1); + } + + return $self; +} + +sub in_opac { + my ($self) = @_; + my $reason; + + $self->reset; + + my $item = $self->item; + my $patron; + unless ($patron = $self->patron) { + Koha::Exceptions::MissingParameter->throw( + error => 'Missing parameter patron. This level of availability query ' + .'requires Koha::Item::Availability::Hold to have a patron parameter.' + ); + } + + # Check if holds are allowed in OPAC + if (!C4::Context->preference('RequestOnOpac')) { + $self->unavailable(Koha::Exceptions::Hold::NotAllowedInOPAC->new); + return $self; + } + + $self->common_biblio_checks; + $self->common_biblioitem_checks; + $self->common_issuing_rule_checks; + $self->common_item_checks; + $self->common_library_item_rule_checks; + $self->common_patron_checks; + $self->opac_specific_issuing_rule_checks; + + return $self; +} + +=head3 common_biblio_checks + +Common checks for both OPAC and intranet. + +=cut + +sub common_biblio_checks { + my ($self, $biblio) = @_; + my $reason; + + unless ($biblio) { + $biblio = Koha::Biblios->find($self->item->biblionumber); + } + + my $bibcalc = Koha::Availability::Checks::Biblio->new($biblio); + + if ($reason = $bibcalc->forbid_holds_on_patrons_possessions($self->patron)) { + $self->unavailable($reason); + } + + return $self; +} + +=head3 common_biblioitem_checks + +Common checks for both OPAC and intranet. + +=cut + +sub common_biblioitem_checks { + my ($self, $bibitem) = @_; + my $reason; + + unless ($bibitem) { + $bibitem = Koha::Biblioitems->find($self->item->biblioitemnumber); + } + + my $bibitemcalc = Koha::Availability::Checks::Biblioitem->new($bibitem); + + if ($reason = $bibitemcalc->age_restricted($self->patron)) { + $self->unavailable($reason); + } + + return $self; +} + +=head3 common_issuing_rule_checks + +Common checks for both OPAC and intranet. + +=cut + +sub common_issuing_rule_checks { + my ($self, $params) = @_; + my $reason; + + my $item = $self->item; + my $patron = $self->patron; + my $branchcode = $params->{'branchcode'} ? $params->{'branchcode'} + : $self->_get_reservescontrol_branchcode($item, $patron); + my $holdrulecalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + branchcode => $branchcode, + use_cache => $params->{'use_cache'}, + }); + + if ($reason = $holdrulecalc->zero_holds_allowed) { + $self->unavailable($reason); + } else { + if ($reason = $holdrulecalc->maximum_holds_reached) { + $self->unavailable($reason); + } + if ($reason = $holdrulecalc->maximum_holds_for_record_reached($params)) { + $self->unavailable($reason); + } + } + + return $self; +} + +=head3 common_item_checks + +Common checks for both OPAC and intranet. + +=cut + +sub common_item_checks { + my ($self, $params) = @_; + my $reason; + + my $item = $self->item; + my $patron = $self->patron; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + + $self->unavailable($reason) if $reason = $itemcalc->damaged; + $self->unavailable($reason) if $reason = $itemcalc->lost; + $self->unavailable($reason) if $reason = $itemcalc->restricted; + $self->unavailable($reason) if $reason = $itemcalc->unknown_barcode; + $self->unavailable($reason) if $reason = $itemcalc->withdrawn; + if ($reason = $itemcalc->notforloan) { + unless ($reason->status < 0) { + $self->unavailable($reason); + } else { + $self->note($reason); + } + } + $self->unavailable($reason) if $reason = $itemcalc->held_by_patron($patron, $params); + $self->unavailable($reason) if $reason = $itemcalc->from_another_library; + if ($self->to_branch && ($reason = $itemcalc->transfer_limit($self->to_branch))) { + $self->unavailable($reason); + } + + return $self; +} + +=head3 common_library_item_rule_checks + +Common checks for both OPAC and intranet. + +=cut + +sub common_library_item_rule_checks { + my ($self) = @_; + my $reason; + + my $item = $self->item; + my $patron = $self->patron; + my $libitemrule = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron, + }); + + if ($reason = $libitemrule->hold_not_allowed_by_library) { + $self->unavailable($reason); + } elsif ($reason = $libitemrule->hold_not_allowed_by_other_library) { + $self->unavailable($reason); + } + + return $self; +} + +=head3 common_patron_checks + +Common checks for both OPAC and intranet. + +=cut + +sub common_patron_checks { + my ($self) = @_; + my $reason; + + my $patron = $self->patron; + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + + $self->unavailable($reason) if $reason = $patroncalc->debt_hold; + $self->unavailable($reason) if $reason = $patroncalc->debarred; + $self->unavailable($reason) if $reason = $patroncalc->exceeded_maxreserves; + $self->unavailable($reason) if $reason = $patroncalc->gonenoaddress; + $self->unavailable($reason) if $reason = $patroncalc->lost; + if (($reason = $patroncalc->expired) + && C4::Context->preference('BlockExpiredPatronOpacActions')) { + $self->unavailable($reason); + } + + return $self; +} + +=head3 opac_specific_checks + +OPAC-specific holdability checks. + +=cut + +sub opac_specific_issuing_rule_checks { + my ($self, $branchcode) = @_; + my $reason; + + my $item = $self->item; + my $patron = $self->patron; + $branchcode ||= $self->_get_reservescontrol_branchcode($item, $patron); + my $holdrulecalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + branchcode => $branchcode, + }); + if ($reason = $holdrulecalc->opac_item_level_hold_forbidden) { + $self->unavailable($reason); + } + + return $self; +} + + +sub _get_reservescontrol_branchcode { + my ($self, $item, $patron) = @_; + + my $branchcode; + my $controlbranch = C4::Context->preference('ReservesControlBranch'); + if ($patron && $controlbranch eq 'PatronLibrary') { + $branchcode = $patron->branchcode; + } elsif ($item && $controlbranch eq 'ItemHomeLibrary') { + $branchcode = $item->homebranch; + } + return $branchcode; +} + +1; diff --git a/t/db_dependent/Koha/Item/Availability/Hold/Intranet/HoldPolicyOverride.t b/t/db_dependent/Koha/Item/Availability/Hold/Intranet/HoldPolicyOverride.t new file mode 100644 index 0000000..901e2f2 --- /dev/null +++ b/t/db_dependent/Koha/Item/Availability/Hold/Intranet/HoldPolicyOverride.t @@ -0,0 +1,90 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 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 => 9; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use Koha::Database; +use Koha::IssuingRules; +use Koha::Items; +use Koha::ItemTypes; + +use Koha::Item::Availability::Hold; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +t::lib::Mocks::mock_preference('AllowHoldPolicyOverride', 1); +t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0); + +# Create test item and patron, and add some reasons that will be need confirmation +my $item = build_a_test_item(); +$item->set({ + barcode => '', + damaged => 1, + itemlost => 1, + notforloan => 1, + restricted => 1, + withdrawn => 1, +})->store; # 6 reasons +my $patron = build_a_test_patron(); +Koha::Account::Line->new({ + borrowernumber => $patron->borrowernumber, + amountoutstanding => 999999999, + accounttype => 'F', +})->store; # 1 reason + +Koha::IssuingRules->search->delete; +my $rule = Koha::IssuingRule->new({ + branchcode => $item->homebranch, + itemtype => $item->effective_itemtype, + categorycode => '*', + holds_per_record => 0, + reservesallowed => 0, +})->store; # 1 reason +my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_intranet; +ok($availability->can('in_intranet'), 'Attempt to check availability in intranet' + .' while considering AllowHoldPolicyOverride system preference.'); +is(C4::Context->preference('AllowHoldPolicyOverride'), 1, 'Given librarians are ' + .'allowed to override hold policy restrictions.'); +ok($availability->available, 'When librarian checks item availability for ' + .'patron, they see that the status is available.'); +ok(!$availability->unavailable, 'There are no reasons for unavailability.'); +is($availability->confirm, 8, 'There are 8 things to be confirmed.'); + +t::lib::Mocks::mock_preference('AllowHoldPolicyOverride', 0); +$availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_intranet; +is(C4::Context->preference('AllowHoldPolicyOverride'), 0, 'Changed setting - ' + .' librarians are no long allowed to override hold policy restrictions.'); +ok(!$availability->available, 'When librarian checks item availability for ' + .'patron, they see that the it is NOT available.'); +ok(!$availability->confirm, 'There are no to be confirmed.'); +is($availability->unavailable, 8, 'There are 8 reasons for unavailability.'); + +$schema->storage->txn_rollback; + +1; diff --git a/t/db_dependent/Koha/Item/Availability/Hold/Opac/HoldRules.t b/t/db_dependent/Koha/Item/Availability/Hold/Opac/HoldRules.t new file mode 100644 index 0000000..eeacd1d --- /dev/null +++ b/t/db_dependent/Koha/Item/Availability/Hold/Opac/HoldRules.t @@ -0,0 +1,290 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 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 => 5; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use Koha::Database; +use Koha::IssuingRules; +use Koha::Items; +use Koha::ItemTypes; + +use Koha::Item::Availability::Hold; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +subtest 'Given there are no hold rules blocking a hold from me' => \&t_hold_rules_nothing_blocking; +sub t_hold_rules_nothing_blocking { + plan tests => 7; + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => $patron->branchcode, + itemtype => $item->effective_itemtype, + categorycode => '*', + holds_per_record => 1, + reservesallowed => 1, + opacitemholds => 'Y', + })->store; + + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + + is($rule->reservesallowed, 1, 'As I look at hold rules, I match a rule says reservesallowed is 1.'); + is($rule->holds_per_record, 1, 'This rule also says holds_per_record is 1.'); + is($rule->opacitemholds, 'Y', 'This rule also says OPAC item holds are allowed.'); + ok($availability->available, 'When they request availability, then the item is available.'); + ok(!$availability->unavailable, 'Then there are no reasons for unavailability.'); + ok(!$availability->confirm, 'Then there is nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional availability notes.'); +}; + +subtest 'Given zero holds are allowed' => sub { + plan tests => 4; + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => '*', + categorycode => '*', + holds_per_record => 0, + reservesallowed => 0, + opacitemholds => 'Y', + })->store; + + sub t_zero_holds_allowed { + my ($item, $patron, $rule) = @_; + + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Hold::ZeroHoldsAllowed'; + + is($rule->reservesallowed, 0, 'As I study this rule, it says zero reserves are allowed.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + ok(!$availability->confirm, 'Then there are nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + is($availability->unavailable, 1, 'Then there is only one reason for unavailability.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, + 'Then there is an unavailability status indicating that holds are not allowed at all.'); + } + subtest '...on any item type or in any library' => sub { + plan tests => 6; + \&t_zero_holds_allowed($item, $patron, $rule); + }; + subtest '...in item home library' => sub { + plan tests => 2; + + subtest '...while ReservesControlBranch = ItemHomeLibrary' => sub { + plan tests => 7; + + t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary'); + $rule->branchcode($item->homebranch)->store; + is($rule->branchcode, $item->homebranch, 'There is a hold rule matching item homebranch.'); + t_zero_holds_allowed($item, $patron, $rule); + $rule->branchcode('*')->store; + set_default_system_preferences(); + }; + subtest '...while ReservesControlBranch = PatronLibrary' => sub { + plan tests => 7; + + t::lib::Mocks::mock_preference('ReservesControlBranch', 'PatronLibrary'); + $rule->branchcode($item->homebranch)->store; + + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + + is($rule->branchcode, $item->homebranch, 'There is a hold rule matching item homebranch.'); + is($rule->reservesallowed, 0, 'As I study this rule, it says zero reserves are allowed.'); + is(C4::Context->preference('ReservesControlBranch'), 'PatronLibrary', 'However, system preference ' + .'ReserveControlBranch says we should use PatronLibrary for matching hold rules.'); + ok($availability->available, 'When I availability, then the item is available.'); + ok(!$availability->confirm, 'Then there are nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + ok(!$availability->unavailable, 'Then there are no reasons for unavailability.'); + + $rule->branchcode('*')->store; + }; + }; + subtest '...in patron library' => sub { + plan tests => 2; + + subtest '...while ReservesControlBranch = ItemHomeLibrary' => sub { + plan tests => 7; + + t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary'); + $rule->branchcode($patron->branchcode)->store; + + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + + is($rule->branchcode, $patron->branchcode, 'There is a hold rule matching patron branchcode.'); + is($rule->reservesallowed, 0, 'As I study this rule, it says zero reserves are allowed.'); + is(C4::Context->preference('ReservesControlBranch'), 'ItemHomeLibrary', 'However, system preference ' + .'ReserveControlBranch says we should use ItemHomeLibrary for matching hold rules.'); + ok($availability->available, 'When I availability, then the item is available.'); + ok(!$availability->confirm, 'Then there are nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + ok(!$availability->unavailable, 'Then there are no reasons for unavailability.'); + + $rule->branchcode('*')->store; + }; + subtest '...while ReservesControlBranch = PatronLibrary' => sub { + plan tests => 6; + + t::lib::Mocks::mock_preference('ReservesControlBranch', 'PatronLibrary'); + $rule->branchcode($patron->branchcode)->store; + + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Hold::ZeroHoldsAllowed'; + + is($rule->branchcode, $patron->branchcode, 'There is a hold rule matching patron branchcode.'); + is($rule->reservesallowed, 0, 'As I study this rule, it says zero reserves are allowed.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + ok(!$availability->confirm, 'Then there are nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + is($availability->unavailable, 1, 'Then there is one reason for unavailability.'); + + $rule->branchcode('*')->store; + }; + }; + + subtest '...on effective item type' => sub { + plan tests => 7; + $rule->itemtype($item->effective_itemtype)->store; + is($rule->itemtype, $item->effective_itemtype, 'There is a hold rule matching effective itemtype.'); + t_zero_holds_allowed($item, $patron, $rule); + $rule->itemtype('*')->store; + }; +}; + +subtest 'Given OPAC item holds are not allowed' => \&t_opac_item_hold_not_allowed; +sub t_opac_item_hold_not_allowed { + plan tests => 6; + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => '*', + categorycode => '*', + holds_per_record => 1, + reservesallowed => 1, + opacitemholds => 'N', + })->store; + + is($rule->opacitemholds, 'N', 'As I look at issuing rules, I find out that OPAC item holds are not allowed.'); + + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Hold::ItemLevelHoldNotAllowed'; + + ok(!$availability->available, 'When I request availability, then the item is not available.'); + ok(!$availability->confirm, 'Then there are nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + is($availability->unavailable, 1, 'Then there is only one reason for unavailability.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, + 'Then there is an unavailability status indicating that item level holds are not allowed.'); +}; + +subtest 'Given I have too many holds in my library' => \&t_too_many_holds_patron_library; +sub t_too_many_holds_patron_library { + plan tests => 8; + + t::lib::Mocks::mock_preference('ReservesControlBranch', 'PatronLibrary'); + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $item2 = build_a_test_item(); + $item->homebranch($item2->homebranch)->store; + $item->itype($item2->itype)->store; + my $reserve_id = add_item_level_hold($item2, $patron, $item2->homebranch); + my $hold = Koha::Holds->find({ borrowernumber => $patron->borrowernumber }); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => $patron->branchcode, + itemtype => $item->effective_itemtype, + categorycode => $patron->categorycode, + holds_per_record => 3, + reservesallowed => 1, + opacitemholds => 'Y', + })->store; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Hold::MaximumHoldsReached'; + + is(C4::Context->preference('ReservesControlBranch'), 'PatronLibrary', 'We will be checking my library\'s rules for holdability.'); + is($rule->reservesallowed, 1, 'As I look at circulation rules, I can see that only one reserve is allowed.'); + is($hold->reserve_id, $reserve_id, 'I have placed one hold already.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is only one reason for unavailability.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, + 'Then there is an unavailability status indicating that maximum holds have been reached.'); + + my $ex = $availability->unavailabilities->{$expecting}; + is($ex->max_holds_allowed, 1, 'Then, from the status, I can see the maximum holds allowed.'); + is($ex->current_hold_count, 1, 'Then, from the status, I can see my current hold count.'); +}; + +subtest 'Given I have too many holds in item\'s library' => \&t_too_many_holds_item_home_library ; +sub t_too_many_holds_item_home_library { + plan tests => 8; + + t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary'); + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $item2 = build_a_test_item(); + $item->homebranch($item2->homebranch)->store; + $item->itype($item2->itype)->store; + my $reserve_id = add_item_level_hold($item2, $patron, $item2->homebranch); + my $hold = Koha::Holds->find({ borrowernumber => $patron->borrowernumber }); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => $item2->homebranch, + itemtype => $item2->effective_itemtype, + categorycode => $patron->categorycode, + holds_per_record => 3, + reservesallowed => 1, + opacitemholds => 'Y', + })->store; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Hold::MaximumHoldsReached'; + + is(C4::Context->preference('ReservesControlBranch'), 'ItemHomeLibrary', 'We will be checking item\'s home library rules for holdability.'); + is($rule->reservesallowed, 1, 'As I look at circulation rules, I can see that only one reserve is allowed.'); + is($hold->reserve_id, $reserve_id, 'I have placed one hold already.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is only one reason for unavailability.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, + 'Then there is an unavailability status indicating that maximum holds have been reached.'); + + my $ex = $availability->unavailabilities->{$expecting}; + is($ex->max_holds_allowed, 1, 'Then, from the status, I can see the maximum holds allowed.'); + is($ex->current_hold_count, 1, 'Then, from the status, I can see my current hold count.'); +}; + +$schema->storage->txn_rollback; + +1; diff --git a/t/db_dependent/Koha/Item/Availability/Hold/Opac/ItemStatus.t b/t/db_dependent/Koha/Item/Availability/Hold/Opac/ItemStatus.t new file mode 100644 index 0000000..88b9397 --- /dev/null +++ b/t/db_dependent/Koha/Item/Availability/Hold/Opac/ItemStatus.t @@ -0,0 +1,371 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 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 t1hat 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 => 14; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use Koha::Database; +use Koha::IssuingRules; +use Koha::Items; +use Koha::ItemTypes; + +use Koha::Item::Availability::Hold; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +subtest 'Given item is in a good state for availability' => \&t_ok_availability; +sub t_ok_availability { + plan tests => 3; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + + ok($availability->available, 'When I request availability, then the item is available.'); + ok(!$availability->confirm, 'Then nothing needs to be confirmed.'); + ok(!$availability->unavailable, 'Then there are no reasons to be unavailable.'); +} + +subtest 'Given item is damaged' => sub { + plan tests => 2; + + subtest 'Given AllowHoldsOnDamagedItems is disabled' => \&t_damaged_item_allow_disabled; + subtest 'Given AllowHoldsOnDamagedItems is enabled' => \&t_damaged_item_allow_enabled; + sub t_damaged_item_allow_disabled { + plan tests => 4; + + t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0); + + my $patron = build_a_test_patron(); + my $item = build_a_test_item()->set({damaged=>1})->store; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Item::Damaged'; + + is($item->damaged, 1, 'When I look at the item, I see that it is damaged.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is only one unavailability reason.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, + 'Then there is an unavailability status indicating damaged item.'); + }; + sub t_damaged_item_allow_enabled { + plan tests => 4; + + t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 1); + + my $patron = build_a_test_patron(); + my $item = build_a_test_item()->set({damaged=>1})->store; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + + is($item->damaged, 1, 'When I look at the item, I see that it is damaged.'); + ok($availability->available, 'When I request availability, then the item is available.'); + ok(!$availability->unavailable, 'Then there are no statuses for unavailability.'); + ok(!$availability->confirm, 'Then there is no reason to have availability confirmed.'); + }; +}; + +subtest 'Given item is lost' => \&t_lost; +sub t_lost { + plan tests => 4; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item()->set({itemlost=>1})->store; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Item::Lost'; + + is($item->itemlost, 1, 'When I try to look at the item, I find out that it is lost.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is only one unavailability reason.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, + 'Then there is an unavailability status indicating lost item.'); +}; + +subtest 'Given item is not for loan' => \&t_notforloan; +sub t_notforloan { + plan tests => 4; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item()->set({notforloan=>1})->store; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Item::NotForLoan'; + + is($item->notforloan, 1, 'When I look at the item, I see that it is not for loan.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is only one unavailability reason.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, + 'Then there is an unavailability status indicating the item is not for loan.'); +}; + +subtest 'Given item type is not for loan' => sub { + + subtest 'Given item-level_itypes is on (item-itemtype)' => \&t_itemlevel_itemtype_notforloan_item_level_itypes_on; + subtest 'Given item-level_itypes is off (biblioitem-itemtype)' => \&t_itemlevel_itemtype_notforloan_item_level_itypes_off; + sub t_itemlevel_itemtype_notforloan_item_level_itypes_on { + plan tests => 5; + + t::lib::Mocks::mock_preference('item-level_itypes', 1); + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + my $itemtype = Koha::ItemTypes->find($item->itype); + $itemtype->set({notforloan=>1})->store; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::ItemType::NotForLoan'; + + is(Koha::ItemTypes->find($biblioitem->itemtype)->notforloan, 0, 'Biblioitem itemtype is for loan.'); + is(Koha::ItemTypes->find($item->itype)->notforloan, 1, 'Item itemtype is not for loan.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is only one unavailability reason.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, + "Then there is an unavailability status indicating the itemtype is not forloan."); + }; + sub t_itemlevel_itemtype_notforloan_item_level_itypes_off { + plan tests => 5; + + t::lib::Mocks::mock_preference('item-level_itypes', 0); + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + my $itemtype = Koha::ItemTypes->find($biblioitem->itemtype); + $itemtype->set({notforloan=>1})->store; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::ItemType::NotForLoan'; + + is(Koha::ItemTypes->find($biblioitem->itemtype)->notforloan, 1, 'Biblioitem itemtype is not for loan.'); + is(Koha::ItemTypes->find($item->itype)->notforloan, 0, 'Item itemtype is for loan.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is only one unavailability reason.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, + "Then there is an unavailability status indicating the itemtype is not forloan."); + }; +}; + +subtest 'Given item is ordered' => \&t_ordered; +sub t_ordered { + plan tests => 4; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item()->set({notforloan=>-1})->store; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Item::NotForLoan'; + + ok($availability->available, 'When I request availability, then the item is available.'); + ok(!$availability->unavailable, 'Then there are no reasons for unavailability.'); + is(ref($availability->notes->{$expecting}), $expecting, + 'Then there is an additional note indicating not for loan status.'); + is($availability->notes->{$expecting}->code, 'Ordered', 'Not for loan code says the item is ordered.') +}; + +subtest 'Given item is restricted' => \&t_restricted; +sub t_restricted { + plan tests => 4; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item()->set({restricted=>1})->store; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Item::Restricted'; + + is($item->restricted, 1, 'When I look at the item, I see that it is restricted.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is only one unavailability reason.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, + 'Then there is an unavailability status indicating restricted item.'); +}; + +subtest 'Transfer is limited' => \&t_transfer_limit; +sub t_transfer_limit { + plan tests => 4; + + t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1); + t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype'); + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $branch2 = Koha::Libraries->find($builder->build({ source => 'Branch' })->{branchcode}); + is(C4::Circulation::CreateBranchTransferLimit( + $branch2->branchcode, + $item->holdingbranch, + $item->effective_itemtype + ), 1, 'There is a branch transfer limit for itemtype from ' + .$item->holdingbranch.' to '.$branch2->branchcode .'.'); + my $availability = Koha::Item::Availability::Hold->new({ + item => $item, + patron => $patron, + to_branch => $branch2->branchcode, + })->in_opac; + my $expecting = 'Koha::Exceptions::Item::CannotBeTransferred'; + ok(!$availability->available, 'When I check availability for hold, then item' + .' is not available'); + is($availability->unavailable, 1, 'Then there is one reason for unavailability'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, 'Then there' + .' is an unavailability status indicating unability to transfer the item.'); +}; + +subtest 'Given item has no barcode' => \&t_unknown_barcode; +sub t_unknown_barcode { + plan tests => 4; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item()->set({barcode=>undef})->store; + my $expecting = 'Koha::Exceptions::Item::UnknownBarcode'; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + + is($item->barcode, undef, 'When I look at the item, we see that it has undefined barcode.'); + ok($availability->unavailable, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is only one unavailability reason.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, + 'Then there is an unavailability status indicating unknown barcode.'); +}; + +subtest 'Given item is withdrawn' => \&t_withdrawn; +sub t_withdrawn { + plan tests => 4; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item()->set({restricted=>1})->store; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Item::Restricted'; + + is($item->restricted, 1, 'When I look at the item, I see that it is restricted.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is only one unavailability reason.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, + 'Then there is an unavailability status indicating restricted item.'); +}; + +subtest 'Already held' => \&t_already_held; +sub t_already_held { + plan tests => 8; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $reserve_id = add_item_level_hold($item, $patron, $item->homebranch); + my $hold = Koha::Holds->find({ borrowernumber => $patron->borrowernumber }); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => $item->homebranch, + itemtype => $item->effective_itemtype, + categorycode => $patron->categorycode, + holds_per_record => 9001, + reservesallowed => 9001, + opacitemholds => 'Y', + })->store; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Item::AlreadyHeldForThisPatron'; + + is($rule->reservesallowed, 9001, 'As I look at circulation rules, I can see that many reserves are allowed.'); + ok($reserve_id, 'I have placed a hold on an item.'); + is($hold->itemnumber, $item->itemnumber, 'The item I have hold for is the same item I will check availability for.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + ok(!$availability->confirm, 'Then there is nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + is($availability->unavailable, 1, 'Then there is only one reason for unavailability.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, + 'Then there is an unavailability status indicating that I have already held this.'); +}; + +subtest 'Less than maxreserves' => \&t_less_than_maxreserves; +sub t_less_than_maxreserves { + plan tests => 5; + + t::lib::Mocks::mock_preference('maxreserves', 50); + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $item2 = build_a_test_item(); + my $reserve_id = add_item_level_hold($item2, $patron, $item2->homebranch); + my $holdcount = Koha::Holds->search({ borrowernumber => $patron->borrowernumber })->count; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + + ok(C4::Context->preference('maxreserves') > $holdcount, 'When I check my holds, I can see that I have less than maximum allowed.'); + ok($availability->available, 'When I request availability, then the item is available.'); + ok(!$availability->confirm, 'Then there is nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + ok(!$availability->unavailable, 'Then there are no reasons for unavailability.'); +}; + +subtest 'Equal to maxreserves' => \&t_equal_to_maxreserves; +sub t_equal_to_maxreserves { + plan tests => 8; + + t::lib::Mocks::mock_preference('maxreserves', 1); + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $item2 = build_a_test_item(); + my $reserve_id = add_item_level_hold($item2, $patron, $item2->homebranch); + my $holdcount = Koha::Holds->search({ borrowernumber => $patron->borrowernumber })->count; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Hold::MaximumHoldsReached'; + + ok(C4::Context->preference('maxreserves') == $holdcount, 'When I check my holds, I can see that I maximum allowed holds.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + ok(!$availability->confirm, 'Then there is nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + is($availability->unavailable, 1, 'Then there are is one reason for unavailability.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, 'Then there is an unavailability' + .' status indicating maximum holds have been reached.'); + + my $ex = $availability->unavailabilities->{$expecting}; + is($ex->max_holds_allowed, 1, 'Then, from the status, I can see the maximum holds allowed.'); + is($ex->current_hold_count, 1, 'Then, from the status, I can see my current hold count.'); +}; + +subtest 'More than maxreserves' => \&t_more_than_maxreserves; +sub t_more_than_maxreserves { + plan tests => 8; + + t::lib::Mocks::mock_preference('maxreserves', 1); + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $item2 = build_a_test_item(); + my $item3 = build_a_test_item(); + my $reserve_id = add_item_level_hold($item2, $patron, $item2->homebranch); + my $reserve_id2 = add_item_level_hold($item3, $patron, $item3->homebranch); + my $holdcount = Koha::Holds->search({ borrowernumber => $patron->borrowernumber })->count; + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Hold::MaximumHoldsReached'; + + ok(C4::Context->preference('maxreserves') < $holdcount, 'When I check my holds, I can see that I have more holds than allowed. How?!'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + ok(!$availability->confirm, 'Then there is nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + is($availability->unavailable, 1, 'Then there are is one reason for unavailability.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, 'Then there is an unavailability' + .' status indicating maximum holds have been reached.'); + + my $ex = $availability->unavailabilities->{$expecting}; + is($ex->max_holds_allowed, 1, 'Then, from the status, I can see the maximum holds allowed.'); + is($ex->current_hold_count, 2, 'Then, from the status, I can see my current hold count.'); +}; + +$schema->storage->txn_rollback; + +1; diff --git a/t/db_dependent/Koha/Item/Availability/Hold/Opac/LibraryItemRules.t b/t/db_dependent/Koha/Item/Availability/Hold/Opac/LibraryItemRules.t new file mode 100644 index 0000000..aa24149 --- /dev/null +++ b/t/db_dependent/Koha/Item/Availability/Hold/Opac/LibraryItemRules.t @@ -0,0 +1,260 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 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 => 6; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use Koha::Database; +use Koha::IssuingRules; +use Koha::Items; +use Koha::ItemTypes; + +use Koha::Item::Availability::Hold; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +subtest 'Given my library does not allow holds in branch item rules' => \&t_holdnotallowed_patronlibrary; +sub t_holdnotallowed_patronlibrary { + plan tests => 7; + + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary'); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $patron->branchcode, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from my library.'); + + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Hold::NotAllowedByLibrary'; + + is(C4::Context->preference('CircControl'), 'PatronLibrary', 'Koha is configured to use patron\'s library for checkout rules.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is one reason for unavailability.'); + ok(!$availability->confirm, 'Then there is nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, 'Then there is an unavailability' + .' note indicating library does not allow item to be held.'); +}; + +subtest 'Given item\'s home library does not allow holds in branch item rules' => \&t_holdnotallowed_itemhomelibrary; +sub t_holdnotallowed_itemhomelibrary { + plan tests => 7; + + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary'); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $item->homebranch, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says item\'s library forbids holds.'); + + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Hold::NotAllowedByLibrary'; + + is(C4::Context->preference('CircControl'), 'ItemHomeLibrary', 'Koha is configured to use item\'s library for checkout rules.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is one reason for unavailability.'); + ok(!$availability->confirm, 'Then there is nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, 'Then there is an unavailability' + .' note indicating library does not allow item to be held.'); +}; + +subtest 'Given my library allows holds only from my library' => \&t_holdallowed_only_from_patronlibrary; +sub t_holdallowed_only_from_patronlibrary { + plan tests => 8; + + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary'); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $patron->branchcode, $item->effective_itemtype, 1, 'homebranch'), 'There is a branch item' + .' rule that says holds are allowed only from my library.'); + + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Hold::NotAllowedFromOtherLibraries'; + + is(C4::Context->preference('CircControl'), 'PatronLibrary', 'Koha is configured to use patron\'s library for checkout rules.'); + ok($item->homebranch ne $patron->branchcode, 'I am from different library than the item.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is one reason for unavailability.'); + ok(!$availability->confirm, 'Then there is nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, 'Then there is an unavailability' + .' status indicating library does not allow item to be held from other libraries.'); +}; + +subtest 'Given item\'s library allows holds only its library' => \&t_holdallowed_only_from_itemhomelibrary; +sub t_holdallowed_only_from_itemhomelibrary { + plan tests => 8; + + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary'); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $item->homebranch, $item->effective_itemtype, 1, 'homebranch'), 'There is a branch item' + .' rule that says holds are allowed only in item home branch.'); + + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Hold::NotAllowedFromOtherLibraries'; + + is(C4::Context->preference('CircControl'), 'ItemHomeLibrary', 'Koha is configured to use item\'s library for checkout rules.'); + ok($item->homebranch ne $patron->branchcode, 'I am from different library than the item.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there is one reason for unavailability.'); + ok(!$availability->confirm, 'Then there is nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, 'Then there is an unavailability' + .' status indicating library does not allow item to be held from other libraries.'); +}; + +subtest 'Given my library allows holds from any other libraries' => \&t_holdallowed_from_any_library_patronlibrary; +sub t_holdallowed_from_any_library_patronlibrary { + plan tests => 3; + + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary'); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $patron->branchcode, $item->effective_itemtype, 2, 'homebranch'), 'There is a branch item' + .' rule that says holds are allowed from any library.'); + + subtest 'Given IndependentBranches is on and canreservefromotherbranches is off' => sub { + plan tests => 9; + + t::lib::Mocks::mock_preference('canreservefromotherbranches', 0); + t::lib::Mocks::mock_preference('IndependentBranches', 1); + + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Hold::NotAllowedFromOtherLibraries'; + + is(C4::Context->preference('CircControl'), 'PatronLibrary', 'Koha is configured to use patron\'s library for checkout rules.'); + is(C4::Context->preference('canreservefromotherbranches'), 0, 'People cannot reserve from other libraries.'); + is(C4::Context->preference('IndependentBranches'), 1, 'Libraries are independent.'); + ok($item->homebranch ne $patron->branchcode, 'I am from different library than the item.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there are no reasons for unavailability.'); + ok(!$availability->confirm, 'Then there is nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, 'Then there is an unavailability' + .' status indicating library does not allow item to be held from other libraries.'); + }; + + subtest 'Given IndependentBranches is off and canreservefromotherbranches is on' => sub { + plan tests => 8; + + t::lib::Mocks::mock_preference('canreservefromotherbranches', 1); + t::lib::Mocks::mock_preference('IndependentBranches', 0); + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + + is(C4::Context->preference('CircControl'), 'PatronLibrary', 'Koha is configured to use patron\'s library for checkout rules.'); + is(C4::Context->preference('canreservefromotherbranches'), 1, 'People can reserve from other libraries.'); + is(C4::Context->preference('IndependentBranches'), 0, 'Libraries are not independent.'); + ok($item->homebranch ne $patron->branchcode, 'I am from different library than the item.'); + ok($availability->available, 'When I request availability, then the item is available.'); + ok(!$availability->unavailable, 'Then there are no reasons for unavailability.'); + ok(!$availability->confirm, 'Then there is nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + }; +}; + +subtest 'Given item\'s library allows holds from any other libraries' => \&t_holdallowed_from_any_library_itemhomelibrary; +sub t_holdallowed_from_any_library_itemhomelibrary { + plan tests => 3; + + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary'); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $item->homebranch, $item->effective_itemtype, 2, 'homebranch'), 'There is a branch item' + .' rule in item\'s homebranch that says holds are allowed from any library.'); + + subtest 'Given IndependentBranches is on and canreservefromotherbranches is off' => sub { + plan tests => 9; + + t::lib::Mocks::mock_preference('canreservefromotherbranches', 0); + t::lib::Mocks::mock_preference('IndependentBranches', 1); + + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + my $expecting = 'Koha::Exceptions::Hold::NotAllowedFromOtherLibraries'; + + is(C4::Context->preference('CircControl'), 'ItemHomeLibrary', 'Koha is configured to use item\'s library for checkout rules.'); + is(C4::Context->preference('canreservefromotherbranches'), 0, 'People cannot reserve from other libraries.'); + is(C4::Context->preference('IndependentBranches'), 1, 'Libraries are independent.'); + ok($item->homebranch ne $patron->branchcode, 'I am from different library than the item.'); + ok(!$availability->available, 'When I request availability, then the item is not available.'); + is($availability->unavailable, 1, 'Then there are no reasons for unavailability.'); + ok(!$availability->confirm, 'Then there is nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + is(ref($availability->unavailabilities->{$expecting}), $expecting, 'Then there is an unavailability' + .' status indicating library does not allow item to be held from other libraries.'); + }; + + subtest 'Given IndependentBranches is off and canreservefromotherbranches is on' => sub { + plan tests => 8; + + t::lib::Mocks::mock_preference('canreservefromotherbranches', 1); + t::lib::Mocks::mock_preference('IndependentBranches', 0); + my $availability = Koha::Item::Availability::Hold->new({item => $item, patron => $patron})->in_opac; + + is(C4::Context->preference('CircControl'), 'ItemHomeLibrary', 'Koha is configured to use item\'s library for checkout rules.'); + is(C4::Context->preference('canreservefromotherbranches'), 1, 'People can reserve from other libraries.'); + is(C4::Context->preference('IndependentBranches'), 0, 'Libraries are not independent.'); + ok($item->homebranch ne $patron->branchcode, 'I am from different library than the item.'); + ok($availability->available, 'When I request availability, then the item is available.'); + ok(!$availability->unavailable, 'Then there are no reasons for unavailability.'); + ok(!$availability->confirm, 'Then there is nothing to be confirmed.'); + ok(!$availability->note, 'Then there are no additional notes.'); + }; +}; -- 2.1.4