From 27808d8a348f9e52216f3e294d8b52fb83c84842 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