Koha-clms/t/db_dependent/Koha/Holds.t

1388 lines
49 KiB
Perl
Executable File

#!/usr/bin/perl
# Copyright 2020 Koha Development team
#
# 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 <https://www.gnu.org/licenses>.
use Modern::Perl;
use Test::NoWarnings;
use Test::More tests => 15;
use Test::Exception;
use Test::Warn;
use C4::Circulation qw( AddIssue );
use C4::Reserves qw( AddReserve CheckReserves ModReserve ModReserveCancelAll );
use Koha::AuthorisedValueCategory;
use Koha::Biblio::ItemGroups;
use Koha::Database;
use Koha::DateUtils qw( dt_from_string );
use Koha::Holds;
use Koha::Old::Holds;
use t::lib::Mocks;
use t::lib::TestBuilder;
my $schema = Koha::Database->new->schema;
$schema->storage->txn_begin;
my $builder = t::lib::TestBuilder->new;
subtest 'DB constraints' => sub {
plan tests => 2;
my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
my $item = $builder->build_sample_item;
my $hold_info = {
branchcode => $patron->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
priority => 1,
title => "title for fee",
itemnumber => $item->itemnumber,
};
my $reserve_id = C4::Reserves::AddReserve($hold_info);
my $hold = Koha::Holds->find($reserve_id);
warning_like {
throws_ok { $hold->priority(undef)->store }
'Koha::Exceptions::Object::NotNull',
'Storing a hold with NULL priority should throw a NotNull exception';
}
qr/required column value/, 'Warning from store on NotNull exception';
};
subtest 'cancel' => sub {
plan tests => 12;
my $biblioitem = $builder->build_object( { class => 'Koha::Biblioitems' } );
my $library = $builder->build_object( { class => 'Koha::Libraries' } );
my $itemtype = $builder->build_object( { class => 'Koha::ItemTypes', value => { rentalcharge => 0 } } );
my $item_info = {
biblionumber => $biblioitem->biblionumber,
biblioitemnumber => $biblioitem->biblioitemnumber,
homebranch => $library->branchcode,
holdingbranch => $library->branchcode,
itype => $itemtype->itemtype,
};
my $item = $builder->build_object( { class => 'Koha::Items', value => $item_info } );
my $manager = $builder->build_object( { class => "Koha::Patrons" } );
t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
my ( @patrons, @holds );
for my $i ( 0 .. 2 ) {
my $priority = $i + 1;
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { branchcode => $library->branchcode, }
}
);
my $reserve_id = C4::Reserves::AddReserve(
{
branchcode => $library->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
priority => $priority,
title => "title for fee",
itemnumber => $item->itemnumber,
}
);
my $hold = Koha::Holds->find($reserve_id);
push @patrons, $patron;
push @holds, $hold;
}
# There are 3 holds on this records
my $nb_of_holds = Koha::Holds->search( { biblionumber => $item->biblionumber } )->count;
is(
$nb_of_holds, 3,
'There should have 3 holds placed on this biblio record'
);
my $first_hold = $holds[0];
my $second_hold = $holds[1];
my $third_hold = $holds[2];
is(
ref($second_hold), 'Koha::Hold',
'We should play with Koha::Hold objects'
);
is(
$second_hold->priority, 2,
'Second hold should have a priority set to 3'
);
# Remove the second hold, only 2 should still exist in DB and priorities must have been updated
my $is_cancelled = $second_hold->cancel;
is(
ref($is_cancelled), 'Koha::Hold',
'Koha::Hold->cancel should return the Koha::Hold (?)'
); # This is can reconsidered
is(
$second_hold->in_storage, 0,
'The hold has been cancelled and does not longer exist in DB'
);
$nb_of_holds = Koha::Holds->search( { biblionumber => $item->biblionumber } )->count;
is(
$nb_of_holds, 2,
'a hold has been cancelled, there should have only 2 holds placed on this biblio record'
);
# discard_changes to refetch
is( $first_hold->discard_changes->priority, 1, 'First hold should still be first' );
is( $third_hold->discard_changes->priority, 2, 'Third hold should now be second' );
subtest 'charge_cancel_fee parameter' => sub {
plan tests => 18;
my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
my $library3 = $builder->build_object( { class => 'Koha::Libraries' } );
my $bib_title = "Test Title";
my $borrower =
$builder->build_object( { class => "Koha::Patrons", value => { branchcode => $library1->branchcode } } );
my $itemtype1 = $builder->build_object( { class => 'Koha::ItemTypes', value => {} } );
my $itemtype2 = $builder->build_object( { class => 'Koha::ItemTypes', value => {} } );
my $itemtype3 = $builder->build_object( { class => 'Koha::ItemTypes', value => {} } );
my $itemtype4 = $builder->build_object( { class => 'Koha::ItemTypes', value => {} } );
my $borrowernumber = $borrower->borrowernumber;
my $library_A_code = $library1->branchcode;
my $biblio = $builder->build_sample_biblio( { itemtype => $itemtype1->itemtype } );
my $biblionumber = $biblio->biblionumber;
my $item1 = $builder->build_sample_item(
{
biblionumber => $biblionumber,
itype => $itemtype1->itemtype,
homebranch => $library_A_code,
holdingbranch => $library_A_code
}
);
my $item2 = $builder->build_sample_item(
{
biblionumber => $biblionumber,
itype => $itemtype2->itemtype,
homebranch => $library_A_code,
holdingbranch => $library_A_code
}
);
my $item3 = $builder->build_sample_item(
{
biblionumber => $biblionumber,
itype => $itemtype3->itemtype,
homebranch => $library_A_code,
holdingbranch => $library_A_code
}
);
my $library_B_code = $library2->branchcode;
my $biblio2 = $builder->build_sample_biblio( { itemtype => $itemtype4->itemtype } );
my $biblionumber2 = $biblio2->biblionumber;
my $item4 = $builder->build_sample_item(
{
biblionumber => $biblionumber2,
itype => $itemtype4->itemtype,
homebranch => $library_B_code,
holdingbranch => $library_B_code
}
);
my $library_C_code = $library3->branchcode;
my $biblio3 = $builder->build_sample_biblio( { itemtype => $itemtype4->itemtype } );
my $biblionumber3 = $biblio3->biblionumber;
my $item5 = $builder->build_sample_item(
{
biblionumber => $biblionumber3,
itype => $itemtype4->itemtype,
homebranch => $library_C_code,
holdingbranch => $library_C_code
}
);
Koha::CirculationRules->set_rules(
{
itemtype => undef,
categorycode => undef,
branchcode => undef,
rules => { expire_reserves_charge => undef }
}
);
Koha::CirculationRules->set_rules(
{
itemtype => $itemtype1->itemtype,
categorycode => undef,
branchcode => undef,
rules => { expire_reserves_charge => '111' }
}
);
Koha::CirculationRules->set_rules(
{
itemtype => $itemtype2->itemtype,
categorycode => undef,
branchcode => undef,
rules => { expire_reserves_charge => undef }
}
);
Koha::CirculationRules->set_rules(
{
itemtype => undef,
categorycode => undef,
branchcode => $library_B_code,
rules => { expire_reserves_charge => '444' }
}
);
Koha::CirculationRules->set_rules(
{
itemtype => undef,
categorycode => undef,
branchcode => $library_C_code,
rules => { expire_reserves_charge => '0' }
}
);
t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
my $reserve_id;
my $account;
my $status;
my $start_balance;
# TEST: Hold itemtype1 item
$reserve_id = AddReserve(
{
branchcode => $library_A_code,
borrowernumber => $borrowernumber,
biblionumber => $biblionumber,
priority => 1,
itemnumber => $item1->itemnumber,
}
);
$account = Koha::Account->new( { patron_id => $borrowernumber } );
($status) = CheckReserves($item1);
is( $status, 'Reserved', "Hold for the itemtype1 created" );
$start_balance = $account->balance();
Koha::Holds->find($reserve_id)->cancel( { charge_cancel_fee => 1 } );
($status) = CheckReserves($item1);
is( $status, '', "Hold for the itemtype1 cancelled" );
is( $account->balance() - $start_balance, 111, "Used circulation rule for itemtype1" );
# TEST: circulation rule for itemtype2 has 'expire_reserves_charge' set undef, so it should use ExpireReservesMaxPickUpDelayCharge preference
t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge', 222 );
$reserve_id = AddReserve(
{
branchcode => $library_A_code,
borrowernumber => $borrowernumber,
biblionumber => $biblionumber,
priority => 1,
itemnumber => $item2->itemnumber,
}
);
$account = Koha::Account->new( { patron_id => $borrowernumber } );
($status) = CheckReserves($item2);
is( $status, 'Reserved', "Hold for the itemtype2 created" );
$start_balance = $account->balance();
Koha::Holds->find($reserve_id)->cancel( { charge_cancel_fee => 1 } );
($status) = CheckReserves($item2);
is( $status, '', "Hold for the itemtype2 cancelled" );
is(
$account->balance() - $start_balance, 222,
"Used ExpireReservesMaxPickUpDelayCharge preference as expire_reserves_charge set to undef"
);
# TEST: no circulation rules for itemtype3, it should use ExpireReservesMaxPickUpDelayCharge preference
t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge', 333 );
$reserve_id = AddReserve(
{
branchcode => $library_A_code,
borrowernumber => $borrowernumber,
biblionumber => $biblionumber,
priority => 1,
itemnumber => $item3->itemnumber,
}
);
$account = Koha::Account->new( { patron_id => $borrowernumber } );
($status) = CheckReserves($item3);
is( $status, 'Reserved', "Hold for the itemtype3 created" );
$start_balance = $account->balance();
Koha::Holds->find($reserve_id)->cancel( { charge_cancel_fee => 1 } );
($status) = CheckReserves($item3);
is( $status, '', "Hold for the itemtype3 cancelled" );
is(
$account->balance() - $start_balance, 333,
"Used ExpireReservesMaxPickUpDelayCharge preference as there's no circulation rules for itemtype3"
);
# TEST: circulation rule for itemtype4 with library_B_code
t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge', 555 );
$reserve_id = AddReserve(
{
branchcode => $library_B_code,
borrowernumber => $borrowernumber,
biblionumber => $biblionumber2,
priority => 1,
itemnumber => $item4->itemnumber,
}
);
$account = Koha::Account->new( { patron_id => $borrowernumber } );
($status) = CheckReserves($item4);
is( $status, 'Reserved', "Hold for the itemtype4 created" );
$start_balance = $account->balance();
Koha::Holds->find($reserve_id)->cancel( { charge_cancel_fee => 1 } );
($status) = CheckReserves($item4);
is( $status, '', "Hold for the itemtype4 cancelled" );
is( $account->balance() - $start_balance, 444, "Used circulation rule for itemtype4 with library_B_code" );
# TEST: circulation rule for library_C_code that has expire_reserves_charge = 0
t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge', 777 );
$reserve_id = AddReserve(
{
branchcode => $library_C_code,
borrowernumber => $borrowernumber,
biblionumber => $biblionumber3,
priority => 1,
itemnumber => $item5->itemnumber,
}
);
$account = Koha::Account->new( { patron_id => $borrowernumber } );
($status) = CheckReserves($item5);
is( $status, 'Reserved', "Hold for the itemtype5 created" );
$start_balance = $account->balance();
Koha::Holds->find($reserve_id)->cancel( { charge_cancel_fee => 1 } );
($status) = CheckReserves($item5);
is( $status, '', "Hold for the itemtype5 cancelled" );
is(
$account->balance() - $start_balance, 0,
"Used circulation rule for itemtype4 with library_C_code even though it's 0"
);
# TEST: charge_cancel_fee is 0
$reserve_id = AddReserve(
{
branchcode => $library_B_code,
borrowernumber => $borrowernumber,
biblionumber => $biblionumber2,
priority => 1,
itemnumber => $item4->itemnumber,
}
);
$account = Koha::Account->new( { patron_id => $borrowernumber } );
($status) = CheckReserves($item4);
is( $status, 'Reserved', "Hold for the itemtype4 created" );
$start_balance = $account->balance();
Koha::Holds->find($reserve_id)->cancel( { charge_cancel_fee => 0 } );
($status) = CheckReserves($item4);
is( $status, '', "Hold for the itemtype4 cancelled" );
is( $account->balance() - $start_balance, 0, "Patron not charged when charge_cancel_fee is 0" );
};
subtest 'waiting hold' => sub {
plan tests => 1;
my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
my $reserve_id = C4::Reserves::AddReserve(
{
branchcode => $library->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
priority => 1,
title => "title for fee",
itemnumber => $item->itemnumber,
found => 'W',
}
);
Koha::Holds->find($reserve_id)->cancel;
my $hold_old = Koha::Old::Holds->find($reserve_id);
is( $hold_old->found, 'W', 'The found column should have been kept and a hold is cancelled' );
};
subtest 'HoldsLog' => sub {
plan tests => 2;
my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
my $hold_info = {
branchcode => $library->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
priority => 1,
title => "title for fee",
itemnumber => $item->itemnumber,
};
t::lib::Mocks::mock_preference( 'HoldsLog', 0 );
my $reserve_id = C4::Reserves::AddReserve($hold_info);
Koha::Holds->find($reserve_id)->cancel;
my $number_of_logs =
$schema->resultset('ActionLog')
->search( { module => 'HOLDS', action => 'CANCEL', object => $reserve_id } )
->count;
is( $number_of_logs, 0, 'Without HoldsLog, Koha::Hold->cancel should not have logged' );
t::lib::Mocks::mock_preference( 'HoldsLog', 1 );
$reserve_id = C4::Reserves::AddReserve($hold_info);
Koha::Holds->find($reserve_id)->cancel;
$number_of_logs =
$schema->resultset('ActionLog')
->search( { module => 'HOLDS', action => 'CANCEL', object => $reserve_id } )
->count;
is( $number_of_logs, 1, 'With HoldsLog, Koha::Hold->cancel should have logged' );
};
subtest 'rollback' => sub {
plan tests => 4;
my $patron_category = $builder->build_object(
{
class => 'Koha::Patron::Categories',
}
);
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { categorycode => $patron_category->categorycode }
}
);
my $hold_info = {
branchcode => $library->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
priority => 1,
title => "title for fee",
itemnumber => $item->itemnumber,
};
t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge', 42 );
my $reserve_id = C4::Reserves::AddReserve($hold_info);
my $hold = Koha::Holds->find($reserve_id);
# Add a row with the same id to make the cancel fails
Koha::Old::Hold->new( $hold->unblessed )->store;
warning_like {
throws_ok { $hold->cancel( { charge_cancel_fee => 1 } ) }
'Koha::Exceptions::Object::DuplicateID',
'Cancelling a hold whose id already exists in old_reserves should throw a DuplicateID exception';
}
qr/Duplicate ID/, 'Warning from store on DuplicateID exception';
$hold = Koha::Holds->find($reserve_id);
is( ref($hold), 'Koha::Hold', 'The hold should not have been deleted' );
is(
$patron->account->balance, 0,
'If the hold has not been cancelled, the patron should not have been charged'
);
};
};
subtest 'cancel with reason' => sub {
plan tests => 7;
my $library = $builder->build_object( { class => 'Koha::Libraries' } );
my $item = $builder->build_sample_item( { library => $library->branchcode } );
my $manager = $builder->build_object( { class => "Koha::Patrons" } );
t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { branchcode => $library->branchcode, }
}
);
my $reserve_id = C4::Reserves::AddReserve(
{
branchcode => $library->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
priority => 1,
itemnumber => $item->itemnumber,
}
);
my $hold = Koha::Holds->find($reserve_id);
ok( $reserve_id, "Hold created" );
ok( $hold, "Hold found" );
my $av =
Koha::AuthorisedValue->new( { category => 'HOLD_CANCELLATION', authorised_value => 'TEST_REASON' } )->store;
Koha::Notice::Templates->search( { code => 'HOLD_CANCELLATION' } )->delete();
my $notice = Koha::Notice::Template->new(
{
name => 'Hold cancellation',
module => 'reserves',
code => 'HOLD_CANCELLATION',
title => 'Hold cancelled',
content => 'Your hold was cancelled.',
message_transport_type => 'email',
branchcode => q{},
}
)->store();
$hold->cancel( { cancellation_reason => 'TEST_REASON' } );
$hold = Koha::Holds->find($reserve_id);
is( $hold, undef, 'Hold is not in the reserves table' );
$hold = Koha::Old::Holds->find($reserve_id);
ok( $hold, 'Hold was found in the old reserves table' );
my $message = Koha::Notice::Messages->find( { borrowernumber => $patron->id, letter_code => 'HOLD_CANCELLATION' } );
ok( $message, 'Found hold cancellation message' );
is( $message->subject, 'Hold cancelled', 'Message has correct title' );
is( $message->content, 'Your hold was cancelled.', 'Message has correct content' );
$notice->delete;
$av->delete;
$message->delete;
};
subtest 'cancel all with reason' => sub {
plan tests => 7;
my $library = $builder->build_object( { class => 'Koha::Libraries' } );
my $item = $builder->build_sample_item( { library => $library->branchcode } );
my $manager = $builder->build_object( { class => "Koha::Patrons" } );
t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { branchcode => $library->branchcode, }
}
);
my $reserve_id = C4::Reserves::AddReserve(
{
branchcode => $library->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
priority => 1,
itemnumber => $item->itemnumber,
}
);
my $hold = Koha::Holds->find($reserve_id);
ok( $reserve_id, "Hold created" );
ok( $hold, "Hold found" );
my $av =
Koha::AuthorisedValue->new( { category => 'HOLD_CANCELLATION', authorised_value => 'TEST_REASON' } )->store;
Koha::Notice::Templates->search( { code => 'HOLD_CANCELLATION' } )->delete();
my $notice = Koha::Notice::Template->new(
{
name => 'Hold cancellation',
module => 'reserves',
code => 'HOLD_CANCELLATION',
title => 'Hold cancelled',
content => 'Your hold was cancelled.',
message_transport_type => 'email',
branchcode => q{},
}
)->store();
ModReserveCancelAll( $item->id, $patron->id, 'TEST_REASON' );
$hold = Koha::Holds->find($reserve_id);
is( $hold, undef, 'Hold is not in the reserves table' );
$hold = Koha::Old::Holds->find($reserve_id);
ok( $hold, 'Hold was found in the old reserves table' );
my $message = Koha::Notice::Messages->find( { borrowernumber => $patron->id, letter_code => 'HOLD_CANCELLATION' } );
ok( $message, 'Found hold cancellation message' );
is( $message->subject, 'Hold cancelled', 'Message has correct title' );
is( $message->content, 'Your hold was cancelled.', 'Message has correct content' );
$av->delete;
$message->delete;
};
subtest 'cancel specific hold with ModReserveCancelAll' => sub {
plan tests => 9;
my $library = $builder->build_object( { class => 'Koha::Libraries' } );
my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
my $item = $builder->build_sample_item( { library => $library->branchcode } );
t::lib::Mocks::mock_userenv( { branchcode => $library->branchcode } );
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { branchcode => $library->branchcode, }
}
);
my $patron2 = $builder->build_object(
{
class => 'Koha::Patrons',
value => { branchcode => $library2->branchcode, }
}
);
my $reserve = C4::Reserves::AddReserve(
{
branchcode => $library->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
itemnumber => $item->itemnumber,
priority => 0,
found => 'W',
}
);
my $reserve2 = C4::Reserves::AddReserve(
{
branchcode => $library->branchcode,
borrowernumber => $patron2->borrowernumber,
biblionumber => $item->biblionumber,
}
);
my $hold = Koha::Holds->find($reserve);
my $hold2 = Koha::Holds->find($reserve2);
my $messages;
my $nextreservinfo;
# Test case where there is another hold for the same branch
( $messages, $nextreservinfo ) = ModReserveCancelAll( $item->itemnumber, $hold->borrowernumber );
my $old_hold = Koha::Old::Holds->find( $hold->reserve_id );
$hold = Koha::Holds->find( $hold->reserve_id );
is( $hold, undef, 'First hold should be removed from reserves table' );
isnt( $old_hold, undef, 'First hold should be moved to old_reserves table' );
is( $hold2->priority, 1, 'Next reserve in line should be priority 1' );
is(
$nextreservinfo, $hold2->borrowernumber,
'ModReserveCancelAll should return the borrowernumber for the next hold in line'
);
is(
$messages->{'waiting'}, 1,
'ModReserveCancelAll should return a waiting message if next hold is for current branch'
);
# Test case where there is another hold for a different branch
$hold2->cancel;
my $reserve3 = C4::Reserves::AddReserve(
{
branchcode => $library->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
itemnumber => $item->itemnumber,
priority => 0,
found => 'W',
}
);
my $reserve4 = C4::Reserves::AddReserve(
{
branchcode => $library2->branchcode,
borrowernumber => $patron2->borrowernumber,
biblionumber => $item->biblionumber,
}
);
my $hold3 = Koha::Holds->find($reserve3);
my $hold4 = Koha::Holds->find($reserve4);
( $messages, $nextreservinfo ) = ModReserveCancelAll( $item->itemnumber, $hold3->borrowernumber );
is(
$nextreservinfo, $hold4->borrowernumber,
'ModReserveCancelAll should return the borrowernumber for the next hold in line'
);
is(
$messages->{'transfert'}, $hold4->branchcode,
'Next hold is for a different branch - ModReserveCancelAll should return its pickup branch in transfer message'
);
# Test case where there are no other holds
$hold4->cancel;
my $reserve5 = C4::Reserves::AddReserve(
{
branchcode => $library->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
itemnumber => $item->itemnumber,
priority => 0,
found => 'W',
}
);
my $hold5 = Koha::Holds->find($reserve5);
( $messages, $nextreservinfo ) = ModReserveCancelAll( $item->itemnumber, $hold5->borrowernumber );
is( $nextreservinfo, undef, 'No more holds, nextreservinfo should not be defined' );
is( $messages, undef, 'No more holds, messages should not be defined' );
};
subtest 'Desks' => sub {
plan tests => 5;
my $library = $builder->build_object( { class => 'Koha::Libraries' } );
my $desk = Koha::Desk->new(
{
desk_name => 'my_desk_name_for_test',
branchcode => $library->branchcode,
}
)->store;
ok( $desk, "Desk created" );
my $item = $builder->build_sample_item( { library => $library->branchcode } );
my $manager = $builder->build_object( { class => "Koha::Patrons" } );
t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { branchcode => $library->branchcode, }
}
);
my $reserve_id = C4::Reserves::AddReserve(
{
branchcode => $library->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
priority => 1,
itemnumber => $item->itemnumber,
}
);
my $hold = Koha::Holds->find($reserve_id);
ok( $reserve_id, "Hold created" );
ok( $hold, "Hold found" );
$hold->set_waiting( $desk->desk_id );
is( $hold->found, 'W', 'Hold is waiting with correct status set' );
is( $hold->desk_id, $desk->desk_id, 'Hold is attach to its desk' );
};
subtest 'get_items_that_can_fill' => sub {
plan tests => 7;
Koha::CirculationRules->search(
{
rule_name => 'holdallowed',
rule_value => 'not_allowed',
}
)->delete;
my $biblio = $builder->build_sample_biblio;
my $itype_1 = $builder->build_object( { class => 'Koha::ItemTypes' } ); # For 1, 2, 3, 4
my $itype_2 = $builder->build_object( { class => 'Koha::ItemTypes' } );
my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_1->itemtype } );
# waiting
my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_1->itemtype } );
my $item_3 =
$builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_1->itemtype } ); # onloan
my $item_4 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_1->itemtype } )
; # in transfer
my $item_5 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_2->itemtype } );
my $lost = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itemlost => 1 } );
my $withdrawn = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, withdrawn => 1 } );
my $notforloan = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => -1 } );
my $patron_1 = $builder->build_object( { class => 'Koha::Patrons' } );
my $patron_2 = $builder->build_object( { class => 'Koha::Patrons' } );
my $patron_3 = $builder->build_object( { class => 'Koha::Patrons' } );
my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
t::lib::Mocks::mock_userenv( { patron => $patron_1 } );
my $reserve_id_1 = C4::Reserves::AddReserve(
{
branchcode => $library_1->branchcode,
borrowernumber => $patron_1->borrowernumber,
biblionumber => $biblio->biblionumber,
priority => 1,
itemnumber => $item_1->itemnumber,
}
);
my $holds = Koha::Holds->search( { reserve_id => $reserve_id_1 } );
my $items = $holds->get_items_that_can_fill;
is_deeply(
[ map { $_->itemnumber } $items->as_list ], [ $item_1->itemnumber ],
'Item level hold can only be filled by the specific item'
);
my $reserve_id_2 = C4::Reserves::AddReserve(
{
branchcode => $library_1->branchcode,
borrowernumber => $patron_2->borrowernumber,
biblionumber => $biblio->biblionumber,
priority => 2,
branchcode => $item_1->homebranch,
}
);
my $waiting_reserve_id = C4::Reserves::AddReserve(
{
branchcode => $library_1->branchcode,
borrowernumber => $patron_2->borrowernumber,
biblionumber => $biblio->biblionumber,
priority => 0,
found => 'W',
itemnumber => $item_1->itemnumber,
}
);
my $notforloan_reserve_id = C4::Reserves::AddReserve(
{
branchcode => $library_1->branchcode,
borrowernumber => $patron_2->borrowernumber,
biblionumber => $biblio->biblionumber,
priority => 0,
itemnumber => $notforloan->itemnumber,
}
);
# item 3 is on loan
AddIssue( $patron_3, $item_3->barcode );
# item 4 is in transfer
my $from = $builder->build_object( { class => 'Koha::Libraries' } );
my $to = $builder->build_object( { class => 'Koha::Libraries' } );
Koha::Item::Transfer->new(
{
itemnumber => $item_4->itemnumber,
datearrived => undef,
frombranch => $from->branchcode,
tobranch => $to->branchcode
}
)->store;
$holds = Koha::Holds->search(
{ reserve_id => [ $reserve_id_1, $reserve_id_2, $waiting_reserve_id, $notforloan_reserve_id, ] } );
$items = $holds->get_items_that_can_fill;
is_deeply(
[ map { $_->itemnumber } $items->as_list ],
[ $item_2->itemnumber, $item_5->itemnumber ], 'Only item 2 and 5 are available for filling the hold'
);
my $waiting_recall = $builder->build_object(
{ class => 'Koha::Recalls', value => { item_id => $item_5->itemnumber, status => 'waiting' } } );
$items = $holds->get_items_that_can_fill;
is_deeply(
[ map { $_->itemnumber } $items->as_list ],
[ $item_2->itemnumber ],
'Only item 2 is available for filling the hold as item 5 is allocated to a pending recall'
);
$waiting_recall->delete;
# Marking item_5 is no hold allowed
Koha::CirculationRule->new(
{
rule_name => 'holdallowed',
rule_value => 'not_allowed',
itemtype => $item_5->itype
}
)->store;
$items = $holds->get_items_that_can_fill;
is_deeply(
[ map { $_->itemnumber } $items->as_list ],
[ $item_2->itemnumber ], 'Only item 2 is available for filling the hold'
);
my $noloan_itype = $builder->build_object( { class => 'Koha::ItemTypes', value => { notforloan => 1 } } );
t::lib::Mocks::mock_preference( 'item-level_itypes', 0 );
Koha::Holds->find($waiting_reserve_id)->delete;
$holds = Koha::Holds->search( { reserve_id => [ $reserve_id_1, $reserve_id_2 ] } );
$items = $holds->get_items_that_can_fill;
is_deeply(
[ sort { $a <=> $b } map { $_->itemnumber } $items->as_list ],
[ $item_1->itemnumber, $item_2->itemnumber, $item_5->itemnumber ],
'Items 1, 2, and 5 are available for filling the holds'
);
my $no_holds = Koha::Holds->new->empty();
my $no_items = $no_holds->get_items_that_can_fill();
is( ref $no_items, "Koha::Items", "Routine returns a Koha::Items object" );
is( $no_items->count, 0, "Object is empty when called on no holds" );
};
subtest 'set_waiting+patron_expiration_date' => sub {
plan tests => 2;
my $library = $builder->build_object( { class => 'Koha::Libraries' } );
my $item = $builder->build_sample_item( { library => $library->branchcode } );
my $manager = $builder->build_object( { class => "Koha::Patrons" } );
t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { branchcode => $library->branchcode, }
}
);
subtest 'patron_expiration_date < expiration_date' => sub {
plan tests => 6;
t::lib::Mocks::mock_preference( 'ReservesMaxPickUpDelay', 5 );
my $patron_expiration_date = dt_from_string->add( days => 3 )->ymd;
my $reserve_id = C4::Reserves::AddReserve(
{
branchcode => $library->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
priority => 1,
itemnumber => $item->itemnumber,
expiration_date => $patron_expiration_date,
}
);
my $hold = Koha::Holds->find($reserve_id);
is(
$hold->expirationdate,
$patron_expiration_date,
'expiration date set to patron expiration date'
);
is(
$hold->patron_expiration_date, $patron_expiration_date,
'patron expiration date correctly set'
);
$hold->set_waiting;
$hold = $hold->get_from_storage;
is( $hold->expirationdate, $patron_expiration_date );
is( $hold->patron_expiration_date, $patron_expiration_date );
$hold->revert_found();
$hold = $hold->get_from_storage;
is( $hold->expirationdate, $patron_expiration_date );
is( $hold->patron_expiration_date, $patron_expiration_date );
};
subtest 'patron_expiration_date > expiration_date' => sub {
plan tests => 6;
t::lib::Mocks::mock_preference( 'ReservesMaxPickUpDelay', 5 );
my $new_expiration_date = dt_from_string->add( days => 5 )->ymd;
my $patron_expiration_date = dt_from_string->add( days => 6 )->ymd;
my $reserve_id = C4::Reserves::AddReserve(
{
branchcode => $library->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
priority => 1,
itemnumber => $item->itemnumber,
expiration_date => $patron_expiration_date,
}
);
my $hold = Koha::Holds->find($reserve_id);
is(
$hold->expirationdate,
$patron_expiration_date,
'expiration date set to patron expiration date'
);
is(
$hold->patron_expiration_date, $patron_expiration_date,
'patron expiration date correctly set'
);
$hold->set_waiting;
$hold = $hold->get_from_storage;
is( $hold->expirationdate, $new_expiration_date );
is( $hold->patron_expiration_date, $patron_expiration_date );
$hold->revert_found();
$hold = $hold->get_from_storage;
is( $hold->expirationdate, $patron_expiration_date );
is( $hold->patron_expiration_date, $patron_expiration_date );
};
};
subtest 'Test Koha::Hold::item_group' => sub {
plan tests => 1;
my $library = $builder->build_object( { class => 'Koha::Libraries' } );
my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
my $item = $builder->build_sample_item;
my $item_group = $builder->build_object(
{
class => 'Koha::Biblio::ItemGroups',
}
);
my $reserve_id = AddReserve(
{
branchcode => $library->branchcode,
borrowernumber => $patron->borrowernumber,
biblionumber => $item->biblionumber,
itemnumber => $item->itemnumber,
item_group_id => $item_group->id,
}
);
my $hold = Koha::Holds->find($reserve_id);
is(
$hold->item_group_id, $item_group->id,
'Koha::Hold::item_group returns the correct item_group'
);
};
subtest 'count_holds' => sub {
plan tests => 3;
$schema->storage->txn_begin;
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
}
);
my $patron_id = $patron->borrowernumber;
my $hold1 = $builder->build_object(
{
class => 'Koha::Holds',
value => {
borrowernumber => $patron_id,
hold_group_id => undef,
},
}
);
is( $patron->holds->count_holds, 1, 'Test patron has 1 hold.' );
my $hold_group = $builder->build_object(
{
class => 'Koha::HoldGroups',
}
);
my $hold2 = $builder->build_object(
{
class => 'Koha::Holds',
value => {
borrowernumber => $patron_id,
hold_group_id => $hold_group->hold_group_id,
}
}
);
my $hold3 = $builder->build_object(
{
class => 'Koha::Holds',
value => {
borrowernumber => $patron_id,
hold_group_id => $hold_group->hold_group_id,
}
}
);
is( $patron->holds->count_holds, 2, 'Test patron has 2 holds.' );
my $hold_group2 = $builder->build_object(
{
class => 'Koha::HoldGroups',
}
);
my $hold4 = $builder->build_object(
{
class => 'Koha::Holds',
value => {
borrowernumber => $patron_id,
hold_group_id => $hold_group2->hold_group_id,
}
}
);
my $hold5 = $builder->build_object(
{
class => 'Koha::Holds',
value => {
borrowernumber => $patron_id,
hold_group_id => $hold_group2->hold_group_id,
}
}
);
my $hold6 = $builder->build_object(
{
class => 'Koha::Holds',
value => {
borrowernumber => $patron_id,
hold_group_id => $hold_group2->hold_group_id,
}
}
);
is( $patron->holds->count_holds, 3, 'Test patron has 3 holds.' );
$schema->storage->txn_rollback;
};
$schema->storage->txn_rollback;
subtest 'filter_by_found() tests' => sub {
plan tests => 5;
$schema->storage->txn_begin;
my $unfilled = $builder->build_object( { class => 'Koha::Holds', value => { found => undef } } );
my $processing = $builder->build_object( { class => 'Koha::Holds', value => { found => 'P' } } );
my $in_transit = $builder->build_object( { class => 'Koha::Holds', value => { found => 'T' } } );
my $waiting = $builder->build_object( { class => 'Koha::Holds', value => { found => 'W' } } );
my $holds = Koha::Holds->search(
{ reserve_id => [ $unfilled->id, $processing->id, $in_transit->id, $waiting->id ] },
{ order_by => ['reserve_id'] }
);
is( $holds->count, 4, 'Resultset count is correct' );
my $found_holds = $holds->filter_by_found;
is( $found_holds->count, 3, 'Resultset count is correct' );
ok( $found_holds->next->is_in_processing, 'Status is correct (P)' );
ok( $found_holds->next->is_in_transit, 'Status is correct (T)' );
ok( $found_holds->next->is_waiting, 'Status is correct (W)' );
$schema->storage->txn_rollback;
};
subtest 'filter_by_has_cancellation_requests() and filter_out_has_cancellation_requests() tests' => sub {
plan tests => 7;
$schema->storage->txn_begin;
my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
my $item_1 = $builder->build_sample_item;
my $item_2 = $builder->build_sample_item;
my $item_3 = $builder->build_sample_item;
my $hold_1 = $builder->build_object(
{
class => 'Koha::Holds',
value => {
found => 'W',
itemnumber => $item_1->id,
biblionumber => $item_1->biblionumber,
borrowernumber => $patron->id
}
}
);
my $hold_2 = $builder->build_object(
{
class => 'Koha::Holds',
value => {
found => 'W',
itemnumber => $item_2->id,
biblionumber => $item_2->biblionumber,
borrowernumber => $patron->id
}
}
);
my $hold_3 = $builder->build_object(
{
class => 'Koha::Holds',
value => {
found => 'W',
itemnumber => $item_3->id,
biblionumber => $item_3->biblionumber,
borrowernumber => $patron->id
}
}
);
my $rs = Koha::Holds->search( { reserve_id => [ $hold_1->id, $hold_2->id, $hold_3->id ] } );
is( $rs->count, 3 );
my $filtered_rs = $rs->filter_by_has_cancellation_requests;
is( $filtered_rs->count, 0 );
my $filtered_out_rs = $rs->filter_out_has_cancellation_requests;
is( $filtered_out_rs->count, 3 );
$hold_2->add_cancellation_request;
$filtered_rs = $rs->filter_by_has_cancellation_requests;
is( $filtered_rs->count, 1 );
is( $filtered_rs->next->id, $hold_2->id );
$filtered_out_rs = $rs->filter_out_has_cancellation_requests;
is( $filtered_out_rs->count, 2 );
is( $filtered_out_rs->next->id, $hold_1->id );
$schema->storage->txn_rollback;
};
subtest 'processing() tests' => sub {
plan tests => 3;
$schema->storage->txn_begin;
my $hold_1 = $builder->build_object(
{
class => 'Koha::Holds',
value => { found => 'P' }
}
);
my $hold_2 = $builder->build_object(
{
class => 'Koha::Holds',
value => { found => undef }
}
);
my $hold_3 = $builder->build_object(
{
class => 'Koha::Holds',
value => { found => 'T' }
}
);
my $holds = Koha::Holds->search( { reserve_id => [ $hold_1->id, $hold_2->id, $hold_3->id ] } );
is( $holds->count, 3, 'Resultset contains 3 holds' );
my $processing = $holds->processing;
is( $processing->count, 1 );
is( $processing->next->id, $hold_1->id, "First hold is the only one in 'processing'" );
$schema->storage->txn_rollback;
};
subtest 'item_level_holds_count() tests' => sub {
plan tests => 6;
$schema->storage->txn_begin;
my $biblio = $builder->build_sample_biblio;
my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
my $other_patron = $builder->build_object( { class => 'Koha::Patrons' } );
# Create a biblio-level hold for the patron
my $biblio_hold = $builder->build_object(
{
class => 'Koha::Holds',
value => {
borrowernumber => $patron->borrowernumber,
biblionumber => $biblio->biblionumber,
itemnumber => undef,
item_level_hold => 0,
}
}
);
is( $biblio_hold->item_level_holds_count, 0, 'No item-level holds yet for this patron/biblio' );
# Create first item-level hold for the patron
my $item_hold_1 = $builder->build_object(
{
class => 'Koha::Holds',
value => {
borrowernumber => $patron->borrowernumber,
biblionumber => $biblio->biblionumber,
itemnumber => $item_1->itemnumber,
item_level_hold => 1,
}
}
);
is( $item_hold_1->item_level_holds_count, 1, 'One item-level hold for this patron/biblio' );
is( $biblio_hold->item_level_holds_count, 1, 'Biblio-level hold also sees the item-level hold count' );
# Create second item-level hold for the patron
my $item_hold_2 = $builder->build_object(
{
class => 'Koha::Holds',
value => {
borrowernumber => $patron->borrowernumber,
biblionumber => $biblio->biblionumber,
itemnumber => $item_2->itemnumber,
item_level_hold => 1,
}
}
);
is( $item_hold_2->item_level_holds_count, 2, 'Two item-level holds for this patron/biblio' );
# Create item-level hold for different patron - should not be counted
my $other_patron_hold = $builder->build_object(
{
class => 'Koha::Holds',
value => {
borrowernumber => $other_patron->borrowernumber,
biblionumber => $biblio->biblionumber,
itemnumber => $item_1->itemnumber,
item_level_hold => 1,
}
}
);
is( $item_hold_1->item_level_holds_count, 2, 'Still two item-level holds for original patron' );
is( $other_patron_hold->item_level_holds_count, 1, 'Only one item-level hold for other patron' );
$schema->storage->txn_rollback;
};