From 1ea8d83348e90c3c936e012a391d9381507b2440 Mon Sep 17 00:00:00 2001
From: Lari Taskula <lari.taskula@jns.fi>
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
}
}
---
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 <http://www.gnu.org/licenses>.
+
+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 <http://www.gnu.org/licenses>.
+
+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 <http://www.gnu.org/licenses>.
+
+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 <http://www.gnu.org/licenses>.
+
+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 <http://www.gnu.org/licenses>.
+
+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 <http://www.gnu.org/licenses>.
+
+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.');
+ };
+};
--
1.9.1