Bug 26043: Fix random failure from Holds.t
[srvgit] / t / db_dependent / Holds.t
1 #!/usr/bin/perl
2
3 use Modern::Perl;
4
5 use t::lib::Mocks;
6 use t::lib::TestBuilder;
7
8 use C4::Context;
9
10 use Test::More tests => 66;
11 use MARC::Record;
12
13 use C4::Biblio;
14 use C4::Calendar;
15 use C4::Items;
16 use C4::Reserves;
17
18 use Koha::Biblios;
19 use Koha::CirculationRules;
20 use Koha::Database;
21 use Koha::DateUtils qw( dt_from_string output_pref );
22 use Koha::Holds;
23 use Koha::Item::Transfer::Limits;
24 use Koha::Items;
25 use Koha::Libraries;
26 use Koha::Library::Groups;
27 use Koha::Patrons;
28
29 BEGIN {
30     use FindBin;
31     use lib $FindBin::Bin;
32 }
33
34 my $schema  = Koha::Database->new->schema;
35 $schema->storage->txn_begin;
36
37 my $builder = t::lib::TestBuilder->new();
38 my $dbh     = C4::Context->dbh;
39
40 # Create two random branches
41 my $branch_1 = $builder->build({ source => 'Branch' })->{ branchcode };
42 my $branch_2 = $builder->build({ source => 'Branch' })->{ branchcode };
43
44 my $category = $builder->build({ source => 'Category' });
45
46 my $borrowers_count = 5;
47
48 $dbh->do('DELETE FROM itemtypes');
49 $dbh->do('DELETE FROM reserves');
50 $dbh->do('DELETE FROM circulation_rules');
51 my $insert_sth = $dbh->prepare('INSERT INTO itemtypes (itemtype) VALUES (?)');
52 $insert_sth->execute('CAN');
53 $insert_sth->execute('CANNOT');
54 $insert_sth->execute('DUMMY');
55 $insert_sth->execute('ONLY1');
56
57 # Setup Test------------------------
58 my $biblio = $builder->build_sample_biblio({ itemtype => 'DUMMY' });
59
60 # Create item instance for testing.
61 my $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber })->itemnumber;
62
63 # Create some borrowers
64 my @borrowernumbers;
65 foreach (1..$borrowers_count) {
66     my $borrowernumber = Koha::Patron->new({
67         firstname =>  'my firstname',
68         surname => 'my surname ' . $_,
69         categorycode => $category->{categorycode},
70         branchcode => $branch_1,
71     })->store->borrowernumber;
72     push @borrowernumbers, $borrowernumber;
73 }
74
75 # Create five item level holds
76 foreach my $borrowernumber ( @borrowernumbers ) {
77     AddReserve(
78         {
79             branchcode     => $branch_1,
80             borrowernumber => $borrowernumber,
81             biblionumber   => $biblio->biblionumber,
82             priority       => C4::Reserves::CalculatePriority( $biblio->biblionumber ),
83             itemnumber     => $itemnumber,
84         }
85     );
86 }
87
88 my $holds = $biblio->holds;
89 is( $holds->count, $borrowers_count, 'Test GetReserves()' );
90 is( $holds->next->priority, 1, "Reserve 1 has a priority of 1" );
91 is( $holds->next->priority, 2, "Reserve 2 has a priority of 2" );
92 is( $holds->next->priority, 3, "Reserve 3 has a priority of 3" );
93 is( $holds->next->priority, 4, "Reserve 4 has a priority of 4" );
94 is( $holds->next->priority, 5, "Reserve 5 has a priority of 5" );
95
96 my $item = Koha::Items->find( $itemnumber );
97 $holds = $item->current_holds;
98 my $first_hold = $holds->next;
99 my $reservedate = $first_hold->reservedate;
100 my $borrowernumber = $first_hold->borrowernumber;
101 my $branch_1code = $first_hold->branchcode;
102 my $reserve_id = $first_hold->reserve_id;
103 is( $reservedate, output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 }), "holds_placed_today should return a valid reserve date");
104 is( $borrowernumber, $borrowernumbers[0], "holds_placed_today should return a valid borrowernumber");
105 is( $branch_1code, $branch_1, "holds_placed_today should return a valid branchcode");
106 ok($reserve_id, "Test holds_placed_today()");
107
108 my $hold = Koha::Holds->find( $reserve_id );
109 ok( $hold, "Koha::Holds found the hold" );
110 my $hold_biblio = $hold->biblio();
111 ok( $hold_biblio, "Got biblio using biblio() method" );
112 ok( $hold_biblio == $hold->biblio(), "biblio method returns stashed biblio" );
113 my $hold_item = $hold->item();
114 ok( $hold_item, "Got item using item() method" );
115 ok( $hold_item == $hold->item(), "item method returns stashed item" );
116 my $hold_branch = $hold->branch();
117 ok( $hold_branch, "Got branch using branch() method" );
118 ok( $hold_branch == $hold->branch(), "branch method returns stashed branch" );
119 my $hold_found = $hold->found();
120 $hold->set({ found => 'W'})->store();
121 is( Koha::Holds->waiting()->count(), 1, "Koha::Holds->waiting returns waiting holds" );
122 is( Koha::Holds->unfilled()->count(), 4, "Koha::Holds->unfilled returns unfilled holds" );
123
124 my $patron = Koha::Patrons->find( $borrowernumbers[0] );
125 $holds = $patron->holds;
126 is( $holds->next->borrowernumber, $borrowernumbers[0], "Test Koha::Patron->holds");
127
128
129 $holds = $item->current_holds;
130 $first_hold = $holds->next;
131 $borrowernumber = $first_hold->borrowernumber;
132 $branch_1code = $first_hold->branchcode;
133 $reserve_id = $first_hold->reserve_id;
134
135 ModReserve({
136     reserve_id    => $reserve_id,
137     rank          => '4',
138     branchcode    => $branch_1,
139     itemnumber    => $itemnumber,
140     suspend_until => output_pref( { dt => dt_from_string( "2013-01-01", "iso" ), dateonly => 1 } ),
141 });
142
143 $hold = Koha::Holds->find( $reserve_id );
144 ok( $hold->priority eq '4', "Test ModReserve, priority changed correctly" );
145 ok( $hold->suspend, "Test ModReserve, suspend hold" );
146 is( $hold->suspend_until, '2013-01-01 00:00:00', "Test ModReserve, suspend until date" );
147
148 ModReserve({ # call without reserve_id
149     rank          => '3',
150     biblionumber  => $biblio->biblionumber,
151     itemnumber    => $itemnumber,
152     borrowernumber => $borrowernumber,
153 });
154 $hold = Koha::Holds->find( $reserve_id );
155 ok( $hold->priority eq '3', "Test ModReserve, priority changed correctly" );
156
157 ToggleSuspend( $reserve_id );
158 $hold = Koha::Holds->find( $reserve_id );
159 ok( ! $hold->suspend, "Test ToggleSuspend(), no date" );
160
161 ToggleSuspend( $reserve_id, '2012-01-01' );
162 $hold = Koha::Holds->find( $reserve_id );
163 is( $hold->suspend_until, '2012-01-01 00:00:00', "Test ToggleSuspend(), with date" );
164
165 AutoUnsuspendReserves();
166 $hold = Koha::Holds->find( $reserve_id );
167 ok( ! $hold->suspend, "Test AutoUnsuspendReserves()" );
168
169 SuspendAll(
170     borrowernumber => $borrowernumber,
171     biblionumber   => $biblio->biblionumber,
172     suspend => 1,
173     suspend_until => '2012-01-01',
174 );
175 $hold = Koha::Holds->find( $reserve_id );
176 is( $hold->suspend, 1, "Test SuspendAll()" );
177 is( $hold->suspend_until, '2012-01-01 00:00:00', "Test SuspendAll(), with date" );
178
179 SuspendAll(
180     borrowernumber => $borrowernumber,
181     biblionumber   => $biblio->biblionumber,
182     suspend => 0,
183 );
184 $hold = Koha::Holds->find( $reserve_id );
185 is( $hold->suspend, 0, "Test resuming with SuspendAll()" );
186 is( $hold->suspend_until, undef, "Test resuming with SuspendAll(), should have no suspend until date" );
187
188 # Add a new hold for the borrower whose hold we canceled earlier, this time at the bib level
189     AddReserve(
190         {
191             branchcode     => $branch_1,
192             borrowernumber => $borrowernumbers[0],
193             biblionumber   => $biblio->biblionumber,
194         }
195     );
196
197 $patron = Koha::Patrons->find( $borrowernumber );
198 $holds = $patron->holds;
199 my $reserveid = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $borrowernumbers[0] })->next->reserve_id;
200 ModReserveMinusPriority( $itemnumber, $reserveid );
201 $holds = $patron->holds;
202 is( $holds->search({ itemnumber => $itemnumber })->count, 1, "Test ModReserveMinusPriority()" );
203
204 $holds = $biblio->holds;
205 $hold = $holds->next;
206 AlterPriority( 'top', $hold->reserve_id, undef, 2, 1, 6 );
207 $hold = Koha::Holds->find( $reserveid );
208 is( $hold->priority, '1', "Test AlterPriority(), move to top" );
209
210 AlterPriority( 'down', $hold->reserve_id, undef, 2, 1, 6 );
211 $hold = Koha::Holds->find( $reserveid );
212 is( $hold->priority, '2', "Test AlterPriority(), move down" );
213
214 AlterPriority( 'up', $hold->reserve_id, 1, 3, 1, 6 );
215 $hold = Koha::Holds->find( $reserveid );
216 is( $hold->priority, '1', "Test AlterPriority(), move up" );
217
218 AlterPriority( 'bottom', $hold->reserve_id, undef, 2, 1, 6 );
219 $hold = Koha::Holds->find( $reserveid );
220 is( $hold->priority, '6', "Test AlterPriority(), move to bottom" );
221
222 # Regression test for bug 2394
223 #
224 # If IndependentBranches is ON and canreservefromotherbranches is OFF,
225 # a patron is not permittedo to request an item whose homebranch (i.e.,
226 # owner of the item) is different from the patron's own library.
227 # However, if canreservefromotherbranches is turned ON, the patron can
228 # create such hold requests.
229 #
230 # Note that canreservefromotherbranches has no effect if
231 # IndependentBranches is OFF.
232
233 my $foreign_biblio = $builder->build_sample_biblio({ itemtype => 'DUMMY' });
234 my $foreign_itemnumber = $builder->build_sample_item({ library => $branch_2, biblionumber => $foreign_biblio->biblionumber })->itemnumber;
235 Koha::CirculationRules->set_rules(
236     {
237         categorycode => undef,
238         branchcode   => undef,
239         itemtype     => undef,
240         rules        => {
241             reservesallowed  => 25,
242             holds_per_record => 99,
243         }
244     }
245 );
246 Koha::CirculationRules->set_rules(
247     {
248         categorycode => undef,
249         branchcode   => undef,
250         itemtype     => 'CANNOT',
251         rules        => {
252             reservesallowed  => 0,
253             holds_per_record => 99,
254         }
255     }
256 );
257
258 # make sure some basic sysprefs are set
259 t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary');
260 t::lib::Mocks::mock_preference('item-level_itypes', 1);
261
262 # if IndependentBranches is OFF, a $branch_1 patron can reserve an $branch_2 item
263 t::lib::Mocks::mock_preference('IndependentBranches', 0);
264
265 is(
266     CanItemBeReserved($borrowernumbers[0], $foreign_itemnumber)->{status}, 'OK',
267     '$branch_1 patron allowed to reserve $branch_2 item with IndependentBranches OFF (bug 2394)'
268 );
269
270 # if IndependentBranches is OFF, a $branch_1 patron cannot reserve an $branch_2 item
271 t::lib::Mocks::mock_preference('IndependentBranches', 1);
272 t::lib::Mocks::mock_preference('canreservefromotherbranches', 0);
273 ok(
274     CanItemBeReserved($borrowernumbers[0], $foreign_itemnumber)->{status} eq 'cannotReserveFromOtherBranches',
275     '$branch_1 patron NOT allowed to reserve $branch_2 item with IndependentBranches ON ... (bug 2394)'
276 );
277
278 # ... unless canreservefromotherbranches is ON
279 t::lib::Mocks::mock_preference('canreservefromotherbranches', 1);
280 ok(
281     CanItemBeReserved($borrowernumbers[0], $foreign_itemnumber)->{status} eq 'OK',
282     '... unless canreservefromotherbranches is ON (bug 2394)'
283 );
284
285 {
286     # Regression test for bug 11336 # Test if ModReserve correctly recalculate the priorities
287     $biblio = $builder->build_sample_biblio({ itemtype => 'DUMMY' });
288     $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber })->itemnumber;
289     my $reserveid1 = AddReserve(
290         {
291             branchcode     => $branch_1,
292             borrowernumber => $borrowernumbers[0],
293             biblionumber   => $biblio->biblionumber,
294             priority       => 1
295         }
296     );
297
298     $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber })->itemnumber;
299     my $reserveid2 = AddReserve(
300         {
301             branchcode     => $branch_1,
302             borrowernumber => $borrowernumbers[1],
303             biblionumber   => $biblio->biblionumber,
304             priority       => 2
305         }
306     );
307
308     $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber })->itemnumber;
309     my $reserveid3 = AddReserve(
310         {
311             branchcode     => $branch_1,
312             borrowernumber => $borrowernumbers[2],
313             biblionumber   => $biblio->biblionumber,
314             priority       => 3
315         }
316     );
317
318     my $hhh = Koha::Holds->search({ biblionumber => $biblio->biblionumber });
319     my $hold3 = Koha::Holds->find( $reserveid3 );
320     is( $hold3->priority, 3, "The 3rd hold should have a priority set to 3" );
321     ModReserve({ reserve_id => $reserveid1, rank => 'del' });
322     ModReserve({ reserve_id => $reserveid2, rank => 'del' });
323     is( $hold3->discard_changes->priority, 1, "After ModReserve, the 3rd reserve becomes the first on the waiting list" );
324 }
325
326 Koha::Items->find($itemnumber)->damaged(1)->store; # FIXME The $itemnumber is a bit confusing here
327 t::lib::Mocks::mock_preference( 'AllowHoldsOnDamagedItems', 1 );
328 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber)->{status}, 'OK', "Patron can reserve damaged item with AllowHoldsOnDamagedItems enabled" );
329 ok( defined( ( CheckReserves($itemnumber) )[1] ), "Hold can be trapped for damaged item with AllowHoldsOnDamagedItems enabled" );
330
331 $hold = Koha::Hold->new(
332     {
333         borrowernumber => $borrowernumbers[0],
334         itemnumber     => $itemnumber,
335         biblionumber   => $biblio->biblionumber,
336     }
337 )->store();
338 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber )->{status},
339     'itemAlreadyOnHold',
340     "Patron cannot place a second item level hold for a given item" );
341 $hold->delete();
342
343 t::lib::Mocks::mock_preference( 'AllowHoldsOnDamagedItems', 0 );
344 ok( CanItemBeReserved( $borrowernumbers[0], $itemnumber)->{status} eq 'damaged', "Patron cannot reserve damaged item with AllowHoldsOnDamagedItems disabled" );
345 ok( !defined( ( CheckReserves($itemnumber) )[1] ), "Hold cannot be trapped for damaged item with AllowHoldsOnDamagedItems disabled" );
346
347 # Items that are not for loan, but holdable should not be trapped until they are available for loan
348 t::lib::Mocks::mock_preference( 'TrapHoldsOnOrder', 0 );
349 Koha::Items->find($itemnumber)->damaged(0)->notforloan(-1)->store;
350 Koha::Holds->search({ biblionumber => $biblio->id })->delete();
351 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber)->{status}, 'OK', "Patron can place hold on item that is not for loan but holdable ( notforloan < 0 )" );
352 $hold = Koha::Hold->new(
353     {
354         borrowernumber => $borrowernumbers[0],
355         itemnumber     => $itemnumber,
356         biblionumber   => $biblio->biblionumber,
357         found          => undef,
358         priority       => 1,
359         reservedate    => dt_from_string,
360         branchcode     => $branch_1,
361     }
362 )->store();
363 ok( !defined( ( CheckReserves($itemnumber) )[1] ), "Hold cannot be trapped for item that is not for loan but holdable ( notforloan < 0 )" );
364 t::lib::Mocks::mock_preference( 'TrapHoldsOnOrder', 1 );
365 ok( defined( ( CheckReserves($itemnumber) )[1] ), "Hold is trapped for item that is not for loan but holdable ( notforloan < 0 )" );
366 t::lib::Mocks::mock_preference( 'SkipHoldTrapOnNotForLoanValue', '-1' );
367 ok( !defined( ( CheckReserves($itemnumber) )[1] ), "Hold cannot be trapped for item with notforloan value matching SkipHoldTrapOnNotForLoanValue" );
368 t::lib::Mocks::mock_preference( 'SkipHoldTrapOnNotForLoanValue', '-1|1' );
369 ok( !defined( ( CheckReserves($itemnumber) )[1] ), "Hold cannot be trapped for item with notforloan value matching SkipHoldTrapOnNotForLoanValue" );
370 $hold->delete();
371
372 # Regression test for bug 9532
373 $biblio = $builder->build_sample_biblio({ itemtype => 'CANNOT' });
374 $item = $builder->build_sample_item({ library => $branch_1, itype => 'CANNOT', biblionumber => $biblio->biblionumber});
375 AddReserve(
376     {
377         branchcode     => $branch_1,
378         borrowernumber => $borrowernumbers[0],
379         biblionumber   => $biblio->biblionumber,
380         priority       => 1,
381     }
382 );
383 is(
384     CanItemBeReserved( $borrowernumbers[0], $item->itemnumber)->{status}, 'tooManyReserves',
385     "cannot request item if policy that matches on item-level item type forbids it"
386 );
387
388 $item->itype('CAN')->store;
389 ok(
390     CanItemBeReserved( $borrowernumbers[0], $item->itemnumber)->{status} eq 'OK',
391     "can request item if policy that matches on item type allows it"
392 );
393
394 t::lib::Mocks::mock_preference('item-level_itypes', 0);
395 $item->itype(undef)->store;
396 ok(
397     CanItemBeReserved( $borrowernumbers[0], $item->itemnumber)->{status} eq 'tooManyReserves',
398     "cannot request item if policy that matches on bib-level item type forbids it (bug 9532)"
399 );
400
401
402 # Test branch item rules
403
404 $dbh->do('DELETE FROM circulation_rules');
405 Koha::CirculationRules->set_rules(
406     {
407         categorycode => undef,
408         branchcode   => undef,
409         itemtype     => undef,
410         rules        => {
411             reservesallowed  => 25,
412             holds_per_record => 99,
413         }
414     }
415 );
416 Koha::CirculationRules->set_rules(
417     {
418         branchcode => $branch_1,
419         itemtype   => 'CANNOT',
420         rules => {
421             holdallowed => 0,
422             returnbranch => 'homebranch',
423         }
424     }
425 );
426 Koha::CirculationRules->set_rules(
427     {
428         branchcode => $branch_1,
429         itemtype   => 'CAN',
430         rules => {
431             holdallowed => 1,
432             returnbranch => 'homebranch',
433         }
434     }
435 );
436 $biblio = $builder->build_sample_biblio({ itemtype => 'CANNOT' });
437 $itemnumber = $builder->build_sample_item({ library => $branch_1, itype => 'CANNOT', biblionumber => $biblio->biblionumber})->itemnumber;
438 is(CanItemBeReserved($borrowernumbers[0], $itemnumber)->{status}, 'notReservable',
439     "CanItemBeReserved should return 'notReservable'");
440
441 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
442 $itemnumber = $builder->build_sample_item({ library => $branch_2, itype => 'CAN', biblionumber => $biblio->biblionumber})->itemnumber;
443 is(CanItemBeReserved($borrowernumbers[0], $itemnumber)->{status},
444     'cannotReserveFromOtherBranches',
445     "CanItemBeReserved should use PatronLibrary rule when ReservesControlBranch set to 'PatronLibrary'");
446 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
447 is(CanItemBeReserved($borrowernumbers[0], $itemnumber)->{status},
448     'OK',
449     "CanItemBeReserved should use item home library rule when ReservesControlBranch set to 'ItemsHomeLibrary'");
450
451 $itemnumber = $builder->build_sample_item({ library => $branch_1, itype => 'CAN', biblionumber => $biblio->biblionumber})->itemnumber;
452 is(CanItemBeReserved($borrowernumbers[0], $itemnumber)->{status}, 'OK',
453     "CanItemBeReserved should return 'OK'");
454
455 # Bug 12632
456 t::lib::Mocks::mock_preference( 'item-level_itypes',     1 );
457 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
458
459 $dbh->do('DELETE FROM reserves');
460 $dbh->do('DELETE FROM issues');
461 $dbh->do('DELETE FROM items');
462 $dbh->do('DELETE FROM biblio');
463
464 $biblio = $builder->build_sample_biblio({ itemtype => 'ONLY1' });
465 $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber})->itemnumber;
466
467 Koha::CirculationRules->set_rules(
468     {
469         categorycode => undef,
470         branchcode   => undef,
471         itemtype     => 'ONLY1',
472         rules        => {
473             reservesallowed  => 1,
474             holds_per_record => 99,
475         }
476     }
477 );
478 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber )->{status},
479     'OK', 'Patron can reserve item with hold limit of 1, no holds placed' );
480
481 my $res_id = AddReserve(
482     {
483         branchcode     => $branch_1,
484         borrowernumber => $borrowernumbers[0],
485         biblionumber   => $biblio->biblionumber,
486         priority       => 1,
487     }
488 );
489
490 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber )->{status},
491     'tooManyReserves', 'Patron cannot reserve item with hold limit of 1, 1 bib level hold placed' );
492
493     #results should be the same for both ReservesControlBranch settings
494 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
495 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber )->{status},
496     'tooManyReserves', 'Patron cannot reserve item with hold limit of 1, 1 bib level hold placed' );
497 #reset for further tests
498 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
499
500 subtest 'Test max_holds per library/patron category' => sub {
501     plan tests => 6;
502
503     $dbh->do('DELETE FROM reserves');
504
505     $biblio = $builder->build_sample_biblio;
506     $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber})->itemnumber;
507     Koha::CirculationRules->set_rules(
508         {
509             categorycode => undef,
510             branchcode   => undef,
511             itemtype     => $biblio->itemtype,
512             rules        => {
513                 reservesallowed  => 99,
514                 holds_per_record => 99,
515             }
516         }
517     );
518
519     for ( 1 .. 3 ) {
520         AddReserve(
521             {
522                 branchcode     => $branch_1,
523                 borrowernumber => $borrowernumbers[0],
524                 biblionumber   => $biblio->biblionumber,
525                 priority       => 1,
526             }
527         );
528     }
529
530     my $count =
531       Koha::Holds->search( { borrowernumber => $borrowernumbers[0] } )->count();
532     is( $count, 3, 'Patron now has 3 holds' );
533
534     my $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
535     is( $ret->{status}, 'OK', 'Patron can place hold with no borrower circ rules' );
536
537     my $rule_all = Koha::CirculationRules->set_rule(
538         {
539             categorycode => $category->{categorycode},
540             branchcode   => undef,
541             rule_name    => 'max_holds',
542             rule_value   => 3,
543         }
544     );
545
546     my $rule_branch = Koha::CirculationRules->set_rule(
547         {
548             branchcode   => $branch_1,
549             categorycode => $category->{categorycode},
550             rule_name    => 'max_holds',
551             rule_value   => 5,
552         }
553     );
554
555     $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
556     is( $ret->{status}, 'OK', 'Patron can place hold with branch/category rule of 5, category rule of 3' );
557
558     $rule_branch->delete();
559
560     $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
561     is( $ret->{status}, 'tooManyReserves', 'Patron cannot place hold with only a category rule of 3' );
562
563     $rule_all->delete();
564     $rule_branch->rule_value(3);
565     $rule_branch->store();
566
567     $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
568     is( $ret->{status}, 'tooManyReserves', 'Patron cannot place hold with only a branch/category rule of 3' );
569
570     $rule_branch->rule_value(5);
571     $rule_branch->update();
572     $rule_branch->rule_value(5);
573     $rule_branch->store();
574
575     $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
576     is( $ret->{status}, 'OK', 'Patron can place hold with branch/category rule of 5, category rule of 5' );
577 };
578
579 subtest 'Pickup location availability tests' => sub {
580     plan tests => 4;
581
582     $biblio = $builder->build_sample_biblio({ itemtype => 'ONLY1' });
583     $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber})->itemnumber;
584     #Add a default rule to allow some holds
585
586     Koha::CirculationRules->set_rules(
587         {
588             branchcode   => undef,
589             categorycode => undef,
590             itemtype     => undef,
591             rules        => {
592                 reservesallowed  => 25,
593                 holds_per_record => 99,
594             }
595         }
596     );
597     my $item = Koha::Items->find($itemnumber);
598     my $branch_to = $builder->build({ source => 'Branch' })->{ branchcode };
599     my $library = Koha::Libraries->find($branch_to);
600     $library->pickup_location('1')->store;
601     my $patron = $builder->build({ source => 'Borrower' })->{ borrowernumber };
602
603     t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
604     t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
605
606     $library->pickup_location('1')->store;
607     is(CanItemBeReserved($patron, $item->itemnumber, $branch_to)->{status},
608        'OK', 'Library is a pickup location');
609
610     my $limit = Koha::Item::Transfer::Limit->new({
611         fromBranch => $item->holdingbranch,
612         toBranch => $branch_to,
613         itemtype => $item->effective_itemtype,
614     })->store;
615     is(CanItemBeReserved($patron, $item->itemnumber, $branch_to)->{status},
616        'cannotBeTransferred', 'Item cannot be transferred');
617     $limit->delete;
618
619     $library->pickup_location('0')->store;
620     is(CanItemBeReserved($patron, $item->itemnumber, $branch_to)->{status},
621        'libraryNotPickupLocation', 'Library is not a pickup location');
622     is(CanItemBeReserved($patron, $item->itemnumber, 'nonexistent')->{status},
623        'libraryNotFound', 'Cannot set unknown library as pickup location');
624 };
625
626 $schema->storage->txn_rollback;
627
628 subtest 'CanItemBeReserved / holds_per_day tests' => sub {
629
630     plan tests => 10;
631
632     $schema->storage->txn_begin;
633
634     Koha::Holds->search->delete;
635     $dbh->do('DELETE FROM issues');
636     Koha::Items->search->delete;
637     Koha::Biblios->search->delete;
638
639     my $itemtype = $builder->build_object( { class => 'Koha::ItemTypes' } );
640     my $library  = $builder->build_object( { class => 'Koha::Libraries' } );
641     my $patron   = $builder->build_object( { class => 'Koha::Patrons' } );
642
643     # Create 3 biblios with items
644     my $biblio_1 = $builder->build_sample_biblio({ itemtype => $itemtype->itemtype });
645     my $itemnumber_1 = $builder->build_sample_item({ library => $library->branchcode, biblionumber => $biblio_1->biblionumber})->itemnumber;
646     my $biblio_2 = $builder->build_sample_biblio({ itemtype => $itemtype->itemtype });
647     my $itemnumber_2 = $builder->build_sample_item({ library => $library->branchcode, biblionumber => $biblio_2->biblionumber})->itemnumber;
648     my $biblio_3 = $builder->build_sample_biblio({ itemtype => $itemtype->itemtype });
649     my $itemnumber_3 = $builder->build_sample_item({ library => $library->branchcode, biblionumber => $biblio_3->biblionumber})->itemnumber;
650
651     Koha::CirculationRules->search->delete;
652     Koha::CirculationRules->set_rules(
653         {
654             categorycode => '*',
655             branchcode   => '*',
656             itemtype     => $itemtype->itemtype,
657             rules        => {
658                 reservesallowed  => 1,
659                 holds_per_record => 99,
660                 holds_per_day    => 2
661             }
662         }
663     );
664
665     is_deeply(
666         CanItemBeReserved( $patron->borrowernumber, $itemnumber_1 ),
667         { status => 'OK' },
668         'Patron can reserve item with hold limit of 1, no holds placed'
669     );
670
671     AddReserve(
672         {
673             branchcode     => $library->branchcode,
674             borrowernumber => $patron->borrowernumber,
675             biblionumber   => $biblio_1->biblionumber,
676             priority       => 1,
677         }
678     );
679
680     is_deeply(
681         CanItemBeReserved( $patron->borrowernumber, $itemnumber_1 ),
682         { status => 'tooManyReserves', limit => 1 },
683         'Patron cannot reserve item with hold limit of 1, 1 bib level hold placed'
684     );
685
686     # Raise reservesallowed to avoid tooManyReserves from it
687     Koha::CirculationRules->set_rule(
688         {
689
690             categorycode => '*',
691             branchcode   => '*',
692             itemtype     => $itemtype->itemtype,
693             rule_name  => 'reservesallowed',
694             rule_value => 3,
695         }
696     );
697
698     is_deeply(
699         CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
700         { status => 'OK' },
701         'Patron can reserve item with 2 reserves daily cap'
702     );
703
704     # Add a second reserve
705     my $res_id = AddReserve(
706         {
707             branchcode     => $library->branchcode,
708             borrowernumber => $patron->borrowernumber,
709             biblionumber   => $biblio_2->biblionumber,
710             priority       => 1,
711         }
712     );
713     is_deeply(
714         CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
715         { status => 'tooManyReservesToday', limit => 2 },
716         'Patron cannot a third item with 2 reserves daily cap'
717     );
718
719     # Update last hold so reservedate is in the past, so 2 holds, but different day
720     $hold = Koha::Holds->find($res_id);
721     my $yesterday = dt_from_string() - DateTime::Duration->new( days => 1 );
722     $hold->reservedate($yesterday)->store;
723
724     is_deeply(
725         CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
726         { status => 'OK' },
727         'Patron can reserve item with 2 bib level hold placed on different days, 2 reserves daily cap'
728     );
729
730     # Set holds_per_day to 0
731     Koha::CirculationRules->set_rule(
732         {
733
734             categorycode => '*',
735             branchcode   => '*',
736             itemtype     => $itemtype->itemtype,
737             rule_name  => 'holds_per_day',
738             rule_value => 0,
739         }
740     );
741
742
743     # Delete existing holds
744     Koha::Holds->search->delete;
745     is_deeply(
746         CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
747         { status => 'tooManyReservesToday', limit => 0 },
748         'Patron cannot reserve if holds_per_day is 0 (i.e. 0 is 0)'
749     );
750
751     Koha::CirculationRules->set_rule(
752         {
753
754             categorycode => '*',
755             branchcode   => '*',
756             itemtype     => $itemtype->itemtype,
757             rule_name  => 'holds_per_day',
758             rule_value => undef,
759         }
760     );
761
762     Koha::Holds->search->delete;
763     is_deeply(
764         CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
765         { status => 'OK' },
766         'Patron can reserve if holds_per_day is undef (i.e. undef is unlimited daily cap)'
767     );
768     AddReserve(
769         {
770             branchcode     => $library->branchcode,
771             borrowernumber => $patron->borrowernumber,
772             biblionumber   => $biblio_1->biblionumber,
773             priority       => 1,
774         }
775     );
776     AddReserve(
777         {
778             branchcode     => $library->branchcode,
779             borrowernumber => $patron->borrowernumber,
780             biblionumber   => $biblio_2->biblionumber,
781             priority       => 1,
782         }
783     );
784
785     is_deeply(
786         CanItemBeReserved( $patron->borrowernumber, $itemnumber_3 ),
787         { status => 'OK' },
788         'Patron can reserve if holds_per_day is undef (i.e. undef is unlimited daily cap)'
789     );
790     AddReserve(
791         {
792             branchcode     => $library->branchcode,
793             borrowernumber => $patron->borrowernumber,
794             biblionumber   => $biblio_3->biblionumber,
795             priority       => 1,
796         }
797     );
798     is_deeply(
799         CanItemBeReserved( $patron->borrowernumber, $itemnumber_3 ),
800         { status => 'tooManyReserves', limit => 3 },
801         'Unlimited daily holds, but reached reservesallowed'
802     );
803     #results should be the same for both ReservesControlBranch settings
804     t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary');
805     is_deeply(
806         CanItemBeReserved( $patron->borrowernumber, $itemnumber_3 ),
807         { status => 'tooManyReserves', limit => 3 },
808         'Unlimited daily holds, but reached reservesallowed'
809     );
810
811     $schema->storage->txn_rollback;
812 };
813
814 subtest 'CanItemBeReserved / branch_not_in_hold_group' => sub {
815     plan tests => 9;
816
817     $schema->storage->txn_begin;
818
819     # Cleanup database
820     Koha::Holds->search->delete;
821     $dbh->do('DELETE FROM issues');
822     Koha::CirculationRules->set_rule(
823         {
824             branchcode   => undef,
825             categorycode => undef,
826             itemtype     => undef,
827             rule_name    => 'reservesallowed',
828             rule_value   => 25,
829         }
830     );
831
832     Koha::Items->search->delete;
833     Koha::Biblios->search->delete;
834
835     # Create item types
836     my $itemtype1 = $builder->build_object( { class => 'Koha::ItemTypes' } );
837     my $itemtype2 = $builder->build_object( { class => 'Koha::ItemTypes' } );
838
839     # Create libraries
840     my $library1  = $builder->build_object( { class => 'Koha::Libraries' } );
841     my $library2  = $builder->build_object( { class => 'Koha::Libraries' } );
842     my $library3  = $builder->build_object( { class => 'Koha::Libraries' } );
843
844     # Create library groups hierarchy
845     my $rootgroup  = $builder->build_object( { class => 'Koha::Library::Groups', value => {ft_local_hold_group => 1} } );
846     my $group1  = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library1->branchcode}} );
847     my $group2  = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library2->branchcode} } );
848
849     # Create 2 patrons
850     my $patron1   = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library1->branchcode} } );
851     my $patron3   = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library3->branchcode} } );
852
853     # Create 3 biblios with items
854     my $biblio_1 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
855     my $item_1   = $builder->build_sample_item(
856         {
857             biblionumber => $biblio_1->biblionumber,
858             library      => $library1->branchcode
859         }
860     );
861     my $biblio_2 = $builder->build_sample_biblio({ itemtype => $itemtype2->itemtype });
862     my $item_2   = $builder->build_sample_item(
863         {
864             biblionumber => $biblio_2->biblionumber,
865             library      => $library2->branchcode
866         }
867     );
868     my $itemnumber_2 = $item_2->itemnumber;
869     my $biblio_3 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
870     my $item_3   = $builder->build_sample_item(
871         {
872             biblionumber => $biblio_3->biblionumber,
873             library      => $library1->branchcode
874         }
875     );
876
877     # Test 1: Patron 3 can place hold
878     is_deeply(
879         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
880         { status => 'OK' },
881         'Patron can place hold if no circ_rules where defined'
882     );
883
884     # Insert default circ rule of holds allowed only from local hold group for all libraries
885     Koha::CirculationRules->set_rules(
886         {
887             branchcode => undef,
888             itemtype   => undef,
889             rules => {
890                 holdallowed => 3,
891                 hold_fulfillment_policy => 'any',
892                 returnbranch => 'any'
893             }
894         }
895     );
896
897     # Test 2: Patron 1 can place hold
898     is_deeply(
899         CanItemBeReserved( $patron1->borrowernumber, $itemnumber_2 ),
900         { status => 'OK' },
901         'Patron can place hold because patron\'s home library is part of hold group'
902     );
903
904     # Test 3: Patron 3 cannot place hold
905     is_deeply(
906         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
907         { status => 'branchNotInHoldGroup' },
908         'Patron cannot place hold because patron\'s home library is not part of hold group'
909     );
910
911     # Insert default circ rule to "any" for library 2
912     Koha::CirculationRules->set_rules(
913         {
914             branchcode => $library2->branchcode,
915             itemtype   => undef,
916             rules => {
917                 holdallowed => 2,
918                 hold_fulfillment_policy => 'any',
919                 returnbranch => 'any'
920             }
921         }
922     );
923
924     # Test 4: Patron 3 can place hold
925     is_deeply(
926         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
927         { status => 'OK' },
928         'Patron can place hold if holdallowed is set to "any" for library 2'
929     );
930
931     # Update default circ rule to "hold group" for library 2
932     Koha::CirculationRules->set_rules(
933         {
934             branchcode => $library2->branchcode,
935             itemtype   => undef,
936             rules => {
937                 holdallowed => 3,
938                 hold_fulfillment_policy => 'any',
939                 returnbranch => 'any'
940             }
941         }
942     );
943
944     # Test 5: Patron 3 cannot place hold
945     is_deeply(
946         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
947         { status => 'branchNotInHoldGroup' },
948         'Patron cannot place hold if holdallowed is set to "hold group" for library 2'
949     );
950
951     # Insert default item rule to "any" for itemtype 2
952     Koha::CirculationRules->set_rules(
953         {
954             branchcode => $library2->branchcode,
955             itemtype   => $itemtype2->itemtype,
956             rules => {
957                 holdallowed => 2,
958                 hold_fulfillment_policy => 'any',
959                 returnbranch => 'any'
960             }
961         }
962     );
963
964     # Test 6: Patron 3 can place hold
965     is_deeply(
966         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
967         { status => 'OK' },
968         'Patron can place hold if holdallowed is set to "any" for itemtype 2'
969     );
970
971     # Update default item rule to "hold group" for itemtype 2
972     Koha::CirculationRules->set_rules(
973         {
974             branchcode => $library2->branchcode,
975             itemtype   => $itemtype2->itemtype,
976             rules => {
977                 holdallowed => 3,
978                 hold_fulfillment_policy => 'any',
979                 returnbranch => 'any'
980             }
981         }
982     );
983
984     # Test 7: Patron 3 cannot place hold
985     is_deeply(
986         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
987         { status => 'branchNotInHoldGroup' },
988         'Patron cannot place hold if holdallowed is set to "hold group" for itemtype 2'
989     );
990
991     # Insert branch item rule to "any" for itemtype 2 and library 2
992     Koha::CirculationRules->set_rules(
993         {
994             branchcode => $library2->branchcode,
995             itemtype   => $itemtype2->itemtype,
996             rules => {
997                 holdallowed => 2,
998                 hold_fulfillment_policy => 'any',
999                 returnbranch => 'any'
1000             }
1001         }
1002     );
1003
1004     # Test 8: Patron 3 can place hold
1005     is_deeply(
1006         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
1007         { status => 'OK' },
1008         'Patron can place hold if holdallowed is set to "any" for itemtype 2 and library 2'
1009     );
1010
1011     # Update branch item rule to "hold group" for itemtype 2 and library 2
1012     Koha::CirculationRules->set_rules(
1013         {
1014             branchcode => $library2->branchcode,
1015             itemtype   => $itemtype2->itemtype,
1016             rules => {
1017                 holdallowed => 3,
1018                 hold_fulfillment_policy => 'any',
1019                 returnbranch => 'any'
1020             }
1021         }
1022     );
1023
1024     # Test 9: Patron 3 cannot place hold
1025     is_deeply(
1026         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
1027         { status => 'branchNotInHoldGroup' },
1028         'Patron cannot place hold if holdallowed is set to "hold group" for itemtype 2 and library 2'
1029     );
1030
1031     $schema->storage->txn_rollback;
1032
1033 };
1034
1035 subtest 'CanItemBeReserved / pickup_not_in_hold_group' => sub {
1036     plan tests => 9;
1037
1038     $schema->storage->txn_begin;
1039
1040     # Cleanup database
1041     Koha::Holds->search->delete;
1042     $dbh->do('DELETE FROM issues');
1043     Koha::CirculationRules->set_rule(
1044         {
1045             branchcode   => undef,
1046             categorycode => undef,
1047             itemtype     => undef,
1048             rule_name    => 'reservesallowed',
1049             rule_value   => 25,
1050         }
1051     );
1052
1053     Koha::Items->search->delete;
1054     Koha::Biblios->search->delete;
1055
1056     # Create item types
1057     my $itemtype1 = $builder->build_object( { class => 'Koha::ItemTypes' } );
1058     my $itemtype2 = $builder->build_object( { class => 'Koha::ItemTypes' } );
1059
1060     # Create libraries
1061     my $library1  = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } );
1062     my $library2  = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } );
1063     my $library3  = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } );
1064
1065     # Create library groups hierarchy
1066     my $rootgroup  = $builder->build_object( { class => 'Koha::Library::Groups', value => {ft_local_hold_group => 1} } );
1067     my $group1  = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library1->branchcode}} );
1068     my $group2  = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library2->branchcode} } );
1069
1070     # Create 2 patrons
1071     my $patron1   = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library1->branchcode} } );
1072     my $patron3   = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library3->branchcode} } );
1073
1074     # Create 3 biblios with items
1075     my $biblio_1 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
1076     my $item_1   = $builder->build_sample_item(
1077         {
1078             biblionumber => $biblio_1->biblionumber,
1079             library      => $library1->branchcode
1080         }
1081     );
1082     my $biblio_2 = $builder->build_sample_biblio({ itemtype => $itemtype2->itemtype });
1083     my $item_2   = $builder->build_sample_item(
1084         {
1085             biblionumber => $biblio_2->biblionumber,
1086             library      => $library2->branchcode
1087         }
1088     );
1089     my $itemnumber_2 = $item_2->itemnumber;
1090     my $biblio_3 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
1091     my $item_3   = $builder->build_sample_item(
1092         {
1093             biblionumber => $biblio_3->biblionumber,
1094             library      => $library1->branchcode
1095         }
1096     );
1097
1098     # Test 1: Patron 3 can place hold
1099     is_deeply(
1100         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1101         { status => 'OK' },
1102         'Patron can place hold if no circ_rules where defined'
1103     );
1104
1105     # Insert default circ rule of holds allowed only from local hold group for all libraries
1106     Koha::CirculationRules->set_rules(
1107         {
1108             branchcode => undef,
1109             itemtype   => undef,
1110             rules => {
1111                 holdallowed => 2,
1112                 hold_fulfillment_policy => 'holdgroup',
1113                 returnbranch => 'any'
1114             }
1115         }
1116     );
1117
1118     # Test 2: Patron 1 can place hold
1119     is_deeply(
1120         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library1->branchcode ),
1121         { status => 'OK' },
1122         'Patron can place hold because pickup location is part of hold group'
1123     );
1124
1125     # Test 3: Patron 3 cannot place hold
1126     is_deeply(
1127         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1128         { status => 'pickupNotInHoldGroup' },
1129         'Patron cannot place hold because pickup location is not part of hold group'
1130     );
1131
1132     # Insert default circ rule to "any" for library 2
1133     Koha::CirculationRules->set_rules(
1134         {
1135             branchcode => $library2->branchcode,
1136             itemtype   => undef,
1137             rules => {
1138                 holdallowed => 2,
1139                 hold_fulfillment_policy => 'any',
1140                 returnbranch => 'any'
1141             }
1142         }
1143     );
1144
1145     # Test 4: Patron 3 can place hold
1146     is_deeply(
1147         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1148         { status => 'OK' },
1149         'Patron can place hold if default_branch_circ_rules is set to "any" for library 2'
1150     );
1151
1152     # Update default circ rule to "hold group" for library 2
1153     Koha::CirculationRules->set_rules(
1154         {
1155             branchcode => $library2->branchcode,
1156             itemtype   => undef,
1157             rules => {
1158                 holdallowed => 2,
1159                 hold_fulfillment_policy => 'holdgroup',
1160                 returnbranch => 'any'
1161             }
1162         }
1163     );
1164
1165     # Test 5: Patron 3 cannot place hold
1166     is_deeply(
1167         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1168         { status => 'pickupNotInHoldGroup' },
1169         'Patron cannot place hold if hold_fulfillment_policy is set to "hold group" for library 2'
1170     );
1171
1172     # Insert default item rule to "any" for itemtype 2
1173     Koha::CirculationRules->set_rules(
1174         {
1175             branchcode => $library2->branchcode,
1176             itemtype   => $itemtype2->itemtype,
1177             rules => {
1178                 holdallowed => 2,
1179                 hold_fulfillment_policy => 'any',
1180                 returnbranch => 'any'
1181             }
1182         }
1183     );
1184
1185     # Test 6: Patron 3 can place hold
1186     is_deeply(
1187         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1188         { status => 'OK' },
1189         'Patron can place hold if hold_fulfillment_policy is set to "any" for itemtype 2'
1190     );
1191
1192     # Update default item rule to "hold group" for itemtype 2
1193     Koha::CirculationRules->set_rules(
1194         {
1195             branchcode => $library2->branchcode,
1196             itemtype   => $itemtype2->itemtype,
1197             rules => {
1198                 holdallowed => 2,
1199                 hold_fulfillment_policy => 'holdgroup',
1200                 returnbranch => 'any'
1201             }
1202         }
1203     );
1204
1205     # Test 7: Patron 3 cannot place hold
1206     is_deeply(
1207         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1208         { status => 'pickupNotInHoldGroup' },
1209         'Patron cannot place hold if hold_fulfillment_policy is set to "hold group" for itemtype 2'
1210     );
1211
1212     # Insert branch item rule to "any" for itemtype 2 and library 2
1213     Koha::CirculationRules->set_rules(
1214         {
1215             branchcode => $library2->branchcode,
1216             itemtype   => $itemtype2->itemtype,
1217             rules => {
1218                 holdallowed => 2,
1219                 hold_fulfillment_policy => 'any',
1220                 returnbranch => 'any'
1221             }
1222         }
1223     );
1224
1225     # Test 8: Patron 3 can place hold
1226     is_deeply(
1227         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1228         { status => 'OK' },
1229         'Patron can place hold if hold_fulfillment_policy is set to "any" for itemtype 2 and library 2'
1230     );
1231
1232     # Update branch item rule to "hold group" for itemtype 2 and library 2
1233     Koha::CirculationRules->set_rules(
1234         {
1235             branchcode => $library2->branchcode,
1236             itemtype   => $itemtype2->itemtype,
1237             rules => {
1238                 holdallowed => 2,
1239                 hold_fulfillment_policy => 'holdgroup',
1240                 returnbranch => 'any'
1241             }
1242         }
1243     );
1244
1245     # Test 9: Patron 3 cannot place hold
1246     is_deeply(
1247         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1248         { status => 'pickupNotInHoldGroup' },
1249         'Patron cannot place hold if hold_fulfillment_policy is set to "hold group" for itemtype 2 and library 2'
1250     );
1251
1252     $schema->storage->txn_rollback;
1253 };