From 4dfac36d7d2861de87c3e73e9e725431c209a131 Mon Sep 17 00:00:00 2001
From: Lari Taskula <lari.taskula@jns.fi>
Date: Tue, 13 Dec 2016 16:06:26 +0200
Subject: [PATCH] Bug 17712: Item and biblio availability in search context

Usage:
my $availability = Koha::Availability::Search->item({
  item => $item,
})->in_opac;
if ($availability->available) {
  # yes
}
---
 Koha/Availability/Search.pm                      |  47 ++++
 Koha/Biblio/Availability/Search.pm               | 177 +++++++++++++++
 Koha/Item/Availability/Search.pm                 |  76 +++++++
 t/db_dependent/Koha/Biblio/Availability/Search.t | 164 ++++++++++++++
 t/db_dependent/Koha/Item/Availability/Search.t   | 266 +++++++++++++++++++++++
 5 files changed, 730 insertions(+)
 create mode 100644 Koha/Availability/Search.pm
 create mode 100644 Koha/Biblio/Availability/Search.pm
 create mode 100644 Koha/Item/Availability/Search.pm
 create mode 100644 t/db_dependent/Koha/Biblio/Availability/Search.t
 create mode 100644 t/db_dependent/Koha/Item/Availability/Search.t

diff --git a/Koha/Availability/Search.pm b/Koha/Availability/Search.pm
new file mode 100644
index 0000000..90b100e
--- /dev/null
+++ b/Koha/Availability/Search.pm
@@ -0,0 +1,47 @@
+package Koha::Availability::Search;
+
+# 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::Biblio::Availability::Search;
+use Koha::Item::Availability::Search;
+
+use Koha::Exceptions;
+
+sub new {
+    my ($class, $params) = @_;
+
+    my $self = {};
+
+    bless $self, $class;
+}
+
+sub biblio {
+    my ($self, $params) = @_;
+
+    return Koha::Biblio::Availability::Search->new($params);
+}
+
+sub item {
+    my ($self, $params) = @_;
+
+    return Koha::Item::Availability::Search->new($params);
+}
+
+1;
diff --git a/Koha/Biblio/Availability/Search.pm b/Koha/Biblio/Availability/Search.pm
new file mode 100644
index 0000000..9c07450
--- /dev/null
+++ b/Koha/Biblio/Availability/Search.pm
@@ -0,0 +1,177 @@
+package Koha::Biblio::Availability::Search;
+
+# 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::Biblio::Availability);
+
+use Koha::Item::Availability::Search;
+
+=head1 NAME
+
+Koha::Biblio::Availability::Search - Koha Biblio Availability Search object class
+
+=head1 SYNOPSIS
+
+my $searchability = Koha::Biblio::Availability::Search->new({
+    biblio => $biblio,      # which biblio this availability is for
+})
+
+=head1 DESCRIPTION
+
+Class for checking biblio search availability.
+
+This class contains subroutines to determine biblio's availability for search
+result in different contexts.
+
+=head2 Class Methods
+
+=cut
+
+=head3 new
+
+Constructs an biblio search availability object. Biblio is always required.
+
+MANDATORY PARAMETERS
+
+    biblio (or biblionumber)
+
+Biblio is a Koha::Biblio -object.
+
+OPTIONAL PARAMETERS
+
+Returns a Koha::Biblio::Availability::Search -object.
+
+=cut
+
+sub new {
+    my ($class, $params) = @_;
+
+    my $self = $class->SUPER::new($params);
+
+    return $self;
+}
+
+sub in_intranet {
+    my ($self, $params) = @_;
+
+    $self->reset;
+
+    # Item looper
+    $self->_item_looper($params);
+
+    return $self;
+}
+
+sub in_opac {
+    my ($self, $params) = @_;
+
+    $self->reset;
+
+    $params->{'opac'} = 1;
+
+    # Item looper
+    $self->_item_looper($params);
+
+    return $self;
+}
+
+sub _item_looper {
+    my ($self, $params) = @_;
+
+    my @items = $self->biblio->items;
+    my @hostitemnumbers = C4::Items::get_hostitemnumbers_of($self->biblio->biblionumber);
+    if (@hostitemnumbers) {
+        my @hostitems = Koha::Items->search({
+            itemnumber => { 'in' => @hostitemnumbers }
+        });
+        push @items, @hostitems;
+    }
+
+    if (scalar(@items) == 0) {
+        $self->unavailable(Koha::Exceptions::Biblio::NoAvailableItems->new);
+        return;
+    }
+
+    my $opac = $params->{'opac'} ? 1:0;
+    my $hidelostitems = 0;
+    if (!$opac) {
+        $hidelostitems = C4::Context->preference('hidelostitems');
+    }
+
+    # Stop calculating item availabilities after $limit available items are found.
+    # E.g. parameter 'limit' with value 1 will find only one available item and
+    # return biblio as available if no other unavailabilities are found. If you
+    # want to calculate availability of every item in this biblio, do not give this
+    # parameter.
+    my $limit = $params->{'limit'};
+    my $avoid_queries_after =
+    C4::Context->preference('MaxSearchResultsItemsPerRecordStatusCheck');
+    my $count = 0;
+    foreach my $item (@items) {
+        # Break out of loop after $limit items are found available
+        if (defined $limit && @{$self->{'item_availabilities'}} >= $limit) {
+            last;
+        }
+
+        my $item_availability = Koha::Item::Availability::Search->new({
+            item => $item,
+        });
+        if ($count >= $avoid_queries_after) {
+            # A couple heuristics to limit how many times
+            # we query the database for item transfer information, sacrificing
+            # accuracy in some cases for speed;
+            #
+            # 1. don't query if item has one of the other statuses (done inside
+            #    item availability calculation)
+            # 2. don't check holds status if ignore_holds parameter is given
+            # 3. don't check transfer status if ignore_transfer parameter is given
+            $params->{'ignore_holds'} = 1;
+            $params->{'ignore_transfer'} = 1;
+        }
+        if ($opac) {
+            $item_availability = $item_availability->in_opac($params);
+            my $unavails = $item_availability->unavailabilities;
+            # Hide item in OPAC context if system preference hidelostitems is
+            # enabled.
+            my $lost = exists $unavails->{'Koha::Exceptions::Item::Lost'};
+            if ($hidelostitems && $lost) {
+                next;
+            }
+        } else {
+            $item_availability = $item_availability->in_intranet($params);
+        }
+        if ($item_availability->available) {
+            push $self->{'item_availabilities'}, $item_availability;
+        } else {
+            push $self->{'item_unavailabilities'}, $item_availability;
+        }
+        $count++;
+    }
+
+    # After going through items, if none are found available, set the biblio
+    # unavailable
+    if (@{$self->{'item_availabilities'}} == 0) {
+        $self->unavailable(Koha::Exceptions::Biblio::NoAvailableItems->new);
+    }
+
+    return $self;
+}
+
+1;
diff --git a/Koha/Item/Availability/Search.pm b/Koha/Item/Availability/Search.pm
new file mode 100644
index 0000000..5fa6960
--- /dev/null
+++ b/Koha/Item/Availability/Search.pm
@@ -0,0 +1,76 @@
+package Koha::Item::Availability::Search;
+
+# 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::Availability::Checks::Item;
+
+sub new {
+    my ($class, $params) = @_;
+
+    my $self = $class->SUPER::new($params);
+
+    return $self;
+}
+
+sub in_opac {
+    my ($self, $params) = @_;
+    my $reason;
+
+    $self->reset;
+
+    my $item = $self->item;
+
+    my $itemcalc = Koha::Availability::Checks::Item->new($item);
+    $self->unavailable($reason) if $reason = $itemcalc->lost;
+    $self->unavailable($reason) if $reason = $itemcalc->damaged;
+    $self->unavailable($reason) if $reason = $itemcalc->from_another_library;
+    $self->unavailable($reason) if $reason = $itemcalc->notforloan;
+    $self->unavailable($reason) if $reason = $itemcalc->restricted;
+    $self->unavailable($reason) if $reason = $itemcalc->unknown_barcode;
+    $self->unavailable($reason) if $reason = $itemcalc->withdrawn;
+    if ($itemcalc->onloan) {
+        $self->unavailable($reason) if $reason = $itemcalc->checked_out;
+    }
+
+    if (!$params->{'ignore_holds'} && $self->available) {
+        $self->unavailable($reason) if $reason = $itemcalc->held;
+    }
+    if (!$params->{'ignore_transfer'} && $self->available) {
+        $self->unavailable($reason) if $reason = $itemcalc->transfer;
+    }
+
+    return $self;
+}
+
+sub in_intranet {
+    my ($self, $params) = @_;
+
+    # TODO:
+    # Do we want to show some additional information in intranet context?
+    # Perhaps make some additional checks, or override some unavailabilities
+    # found in OPAC.
+    #
+    # For now, just run the same checks as OPAC context.
+    return $self->in_opac($params);
+}
+
+1;
diff --git a/t/db_dependent/Koha/Biblio/Availability/Search.t b/t/db_dependent/Koha/Biblio/Availability/Search.t
new file mode 100644
index 0000000..9d876e1
--- /dev/null
+++ b/t/db_dependent/Koha/Biblio/Availability/Search.t
@@ -0,0 +1,164 @@
+#!/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 => 4;
+use t::lib::Mocks;
+use t::lib::TestBuilder;
+require t::db_dependent::Koha::Availability::Helpers;
+use Benchmark;
+
+use Koha::Database;
+use Koha::IssuingRules;
+use Koha::Items;
+use Koha::ItemTypes;
+
+use Koha::Availability::Search;
+use Koha::Biblio::Availability::Search;
+
+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 'Biblio with zero available items' => \&t_no_available_items;
+sub t_no_available_items {
+    plan tests => 6;
+
+    my $item1 = build_a_test_item();
+    my $biblio = Koha::Biblios->find($item1->biblionumber);
+    my $item2 = build_a_test_item();
+    $item2->biblionumber($biblio->biblionumber)->store;
+    $item2->biblioitemnumber($item1->biblioitemnumber)->store;
+    $item1->withdrawn('1')->store;
+    $item2->notforloan('1')->store;
+    my $patron = build_a_test_patron();
+
+    my $availability = Koha::Biblio::Availability::Search->new({
+        biblio => $biblio,
+    })->in_opac;
+    my $expecting = 'Koha::Exceptions::Biblio::NoAvailableItems';
+
+    ok(!Koha::Item::Availability::Search->new({
+            item => $item1,
+        })->in_opac->available,
+       'When I look at the first item of two in this biblio, it is not available.');
+    ok(!Koha::Item::Availability::Search->new({
+            item => $item2
+        })->in_opac->available,
+       'When I look at the second item of two in this biblio, it is not available.');
+    ok(!$availability->available, 'Then, the biblio is not available.');
+    is($availability->unavailable, 1, 'Then, there is exactly one reason for unavailability.');
+    is(ref($availability->unavailabilities->{$expecting}), $expecting, 'The reason says there are no'
+       .' available items in this biblio.');
+    is(@{$availability->item_unavailabilities}, 2, 'There seems to be two items that are unavailable.');
+};
+
+subtest 'Biblio with one available items out of two' => \&t_one_out_of_two_items_available;
+sub t_one_out_of_two_items_available {
+    plan tests => 5;
+
+    my $item1 = build_a_test_item();
+    my $biblio = Koha::Biblios->find($item1->biblionumber);
+    my $item2 = build_a_test_item();
+    $item2->biblionumber($biblio->biblionumber)->store;
+    $item2->biblioitemnumber($item1->biblioitemnumber)->store;
+
+    my $patron = build_a_test_patron();
+    $item1->withdrawn('1')->store;
+
+    my $availability = Koha::Biblio::Availability::Search->new({biblio => $biblio})->in_opac;
+    my $item_availabilities = $availability->item_availabilities;
+    ok(!Koha::Item::Availability::Search->new({ item => $item1,})->in_opac->available,
+       'When I look at the first item of two in this biblio, it is not available.');
+    ok(Koha::Item::Availability::Search->new({ item => $item2,})->in_opac->available,
+       'When I look at the second item of two in this biblio, it seems to be available.');
+    ok($availability->available, 'Then, the biblio is available.');
+    is(@{$item_availabilities}, 1, 'There seems to be one available item in this biblio.');
+    is($item_availabilities->[0]->item->itemnumber, $item2->itemnumber, 'Then the only available item'
+       .' is the second item of this biblio.');
+};
+
+subtest 'Biblio with two items out of two available' => \&t_all_items_available;
+sub t_all_items_available {
+    plan tests => 4;
+
+    my $item1 = build_a_test_item();
+    my $biblio = Koha::Biblios->find($item1->biblionumber);
+    my $item2 = build_a_test_item();
+    $item2->biblionumber($biblio->biblionumber)->store;
+    $item2->biblioitemnumber($item1->biblioitemnumber)->store;
+
+    my $patron = build_a_test_patron();
+
+    my $availability = Koha::Biblio::Availability::Search->new({biblio => $biblio})->in_opac;
+    my $item_availabilities = $availability->item_availabilities;
+    ok(Koha::Item::Availability::Search->new({ item => $item1,})->in_opac->available,
+       'When I look at the first item of two in this biblio, it seems to be available.');
+    ok(Koha::Item::Availability::Search->new({ item => $item2,})->in_opac->available,
+       'When I look at the second item of two in this biblio, it seems to be available.');
+    ok($availability->available, 'Then, the biblio is available.');
+    is(@{$item_availabilities}, 2, 'There seems to be two available items in this biblio.');
+};
+
+subtest 'Prove that lower MaxSearchResultsItemsPerRecordStatusCheck boosts performance' => \&t_performance_proof;
+sub t_performance_proof {
+    plan tests => 6;
+
+    my $item = build_a_test_item();
+    my $biblio = Koha::Biblios->find($item->biblionumber);
+    my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber);
+
+    # add some items to biblio
+    my $count1 = 20;
+    my $count2 = 200;
+    for my $i (1..($count2-1)) { # one already built earlier
+        build_a_test_item($biblio, $biblioitem);
+    }
+
+    my $availability_iterations = 10;
+    my @items = $biblio->items;
+    is(@items, $count2, "We will get availability for $count2 items.");
+    t::lib::Mocks::mock_preference('MaxSearchResultsItemsPerRecordStatusCheck',
+                                    $count1);
+    is(C4::Context->preference('MaxSearchResultsItemsPerRecordStatusCheck'),
+       $count1, "First test will be done with"
+       ." MaxSearchResultsItemsPerRecordStatusCheck = $count1");
+    my $res1 = timethis($availability_iterations, sub {
+        Koha::Availability::Search->new->biblio({ biblio => $biblio })->in_opac;
+    });
+    ok($res1, "Calculated search availability $availability_iterations times.");
+    t::lib::Mocks::mock_preference('MaxSearchResultsItemsPerRecordStatusCheck',
+                                    $count2);
+    is(C4::Context->preference('MaxSearchResultsItemsPerRecordStatusCheck'),
+       $count2, "Second test will be done with"
+       ." MaxSearchResultsItemsPerRecordStatusCheck = $count2");
+    my $res2 = timethis($availability_iterations, sub {
+        Koha::Availability::Search->new->biblio({ biblio => $biblio })->in_opac;
+    });
+    ok($res2, "Calculated search availability $availability_iterations times.");
+    ok($res1->cpu_a < $res2->cpu_a, 'First test was faster than second.');
+};
+
+$schema->storage->txn_rollback;
+
+1;
diff --git a/t/db_dependent/Koha/Item/Availability/Search.t b/t/db_dependent/Koha/Item/Availability/Search.t
new file mode 100644
index 0000000..0f63446
--- /dev/null
+++ b/t/db_dependent/Koha/Item/Availability/Search.t
@@ -0,0 +1,266 @@
+#!/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 => 10;
+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::Search;
+
+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::Search->new({item => $item})->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::Search->new({item => $item})->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::Search->new({item => $item})->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::Search->new({item => $item})->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::Search->new({item => $item})->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::Search->new({item => $item})->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::Search->new({item => $item})->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::Search->new({item => $item})->in_opac;
+    my $expecting = 'Koha::Exceptions::Item::NotForLoan';
+
+    ok(!$availability->available, 'When I request availability, then the item is not available.');
+    ok($availability->unavailable, 'Then there is one reason for unavailability.');
+    is(ref($availability->unavailabilities->{$expecting}), $expecting,
+        'Then the unavailability status is indicating not for loan status.');
+    is($availability->unavailabilities->{$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::Search->new({item => $item})->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 '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::Search->new({item => $item})->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::Search->new({item => $item})->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 'Held' => \&t_held;
+sub t_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::Search->new({item => $item})->in_opac;
+    my $expecting = 'Koha::Exceptions::Item::Held';
+
+    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.');
+};
+
+$schema->storage->txn_rollback;
+
+1;
-- 
1.9.1