688b12f2dd4981990661543089f996963cb450bf
[koha-equinox.git] / 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 => 64;
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->next->itemnumber, $itemnumber, "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 $hold->delete();
367
368 # Regression test for bug 9532
369 $biblio = $builder->build_sample_biblio({ itemtype => 'CANNOT' });
370 $item = $builder->build_sample_item({ library => $branch_1, itype => 'CANNOT', biblionumber => $biblio->biblionumber});
371 AddReserve(
372     {
373         branchcode     => $branch_1,
374         borrowernumber => $borrowernumbers[0],
375         biblionumber   => $biblio->biblionumber,
376         priority       => 1,
377     }
378 );
379 is(
380     CanItemBeReserved( $borrowernumbers[0], $item->itemnumber)->{status}, 'tooManyReserves',
381     "cannot request item if policy that matches on item-level item type forbids it"
382 );
383
384 $item->itype('CAN')->store;
385 ok(
386     CanItemBeReserved( $borrowernumbers[0], $item->itemnumber)->{status} eq 'OK',
387     "can request item if policy that matches on item type allows it"
388 );
389
390 t::lib::Mocks::mock_preference('item-level_itypes', 0);
391 $item->itype(undef)->store;
392 ok(
393     CanItemBeReserved( $borrowernumbers[0], $item->itemnumber)->{status} eq 'tooManyReserves',
394     "cannot request item if policy that matches on bib-level item type forbids it (bug 9532)"
395 );
396
397
398 # Test branch item rules
399
400 $dbh->do('DELETE FROM circulation_rules');
401 Koha::CirculationRules->set_rules(
402     {
403         categorycode => undef,
404         branchcode   => undef,
405         itemtype     => undef,
406         rules        => {
407             reservesallowed  => 25,
408             holds_per_record => 99,
409         }
410     }
411 );
412 Koha::CirculationRules->set_rules(
413     {
414         branchcode => $branch_1,
415         itemtype   => 'CANNOT',
416         rules => {
417             holdallowed => 0,
418             returnbranch => 'homebranch',
419         }
420     }
421 );
422 Koha::CirculationRules->set_rules(
423     {
424         branchcode => $branch_1,
425         itemtype   => 'CAN',
426         rules => {
427             holdallowed => 1,
428             returnbranch => 'homebranch',
429         }
430     }
431 );
432 $biblio = $builder->build_sample_biblio({ itemtype => 'CANNOT' });
433 $itemnumber = $builder->build_sample_item({ library => $branch_1, itype => 'CANNOT', biblionumber => $biblio->biblionumber})->itemnumber;
434 is(CanItemBeReserved($borrowernumbers[0], $itemnumber)->{status}, 'notReservable',
435     "CanItemBeReserved should return 'notReservable'");
436
437 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
438 $itemnumber = $builder->build_sample_item({ library => $branch_2, itype => 'CAN', biblionumber => $biblio->biblionumber})->itemnumber;
439 is(CanItemBeReserved($borrowernumbers[0], $itemnumber)->{status},
440     'cannotReserveFromOtherBranches',
441     "CanItemBeReserved should use PatronLibrary rule when ReservesControlBranch set to 'PatronLibrary'");
442 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
443 is(CanItemBeReserved($borrowernumbers[0], $itemnumber)->{status},
444     'OK',
445     "CanItemBeReserved should use item home library rule when ReservesControlBranch set to 'ItemsHomeLibrary'");
446
447 $itemnumber = $builder->build_sample_item({ library => $branch_1, itype => 'CAN', biblionumber => $biblio->biblionumber})->itemnumber;
448 is(CanItemBeReserved($borrowernumbers[0], $itemnumber)->{status}, 'OK',
449     "CanItemBeReserved should return 'OK'");
450
451 # Bug 12632
452 t::lib::Mocks::mock_preference( 'item-level_itypes',     1 );
453 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
454
455 $dbh->do('DELETE FROM reserves');
456 $dbh->do('DELETE FROM issues');
457 $dbh->do('DELETE FROM items');
458 $dbh->do('DELETE FROM biblio');
459
460 $biblio = $builder->build_sample_biblio({ itemtype => 'ONLY1' });
461 $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber})->itemnumber;
462
463 Koha::CirculationRules->set_rules(
464     {
465         categorycode => undef,
466         branchcode   => undef,
467         itemtype     => 'ONLY1',
468         rules        => {
469             reservesallowed  => 1,
470             holds_per_record => 99,
471         }
472     }
473 );
474 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber )->{status},
475     'OK', 'Patron can reserve item with hold limit of 1, no holds placed' );
476
477 my $res_id = AddReserve(
478     {
479         branchcode     => $branch_1,
480         borrowernumber => $borrowernumbers[0],
481         biblionumber   => $biblio->biblionumber,
482         priority       => 1,
483     }
484 );
485
486 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber )->{status},
487     'tooManyReserves', 'Patron cannot reserve item with hold limit of 1, 1 bib level hold placed' );
488
489     #results should be the same for both ReservesControlBranch settings
490 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
491 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber )->{status},
492     'tooManyReserves', 'Patron cannot reserve item with hold limit of 1, 1 bib level hold placed' );
493 #reset for further tests
494 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
495
496 subtest 'Test max_holds per library/patron category' => sub {
497     plan tests => 6;
498
499     $dbh->do('DELETE FROM reserves');
500
501     $biblio = $builder->build_sample_biblio;
502     $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber})->itemnumber;
503     Koha::CirculationRules->set_rules(
504         {
505             categorycode => undef,
506             branchcode   => undef,
507             itemtype     => $biblio->itemtype,
508             rules        => {
509                 reservesallowed  => 99,
510                 holds_per_record => 99,
511             }
512         }
513     );
514
515     for ( 1 .. 3 ) {
516         AddReserve(
517             {
518                 branchcode     => $branch_1,
519                 borrowernumber => $borrowernumbers[0],
520                 biblionumber   => $biblio->biblionumber,
521                 priority       => 1,
522             }
523         );
524     }
525
526     my $count =
527       Koha::Holds->search( { borrowernumber => $borrowernumbers[0] } )->count();
528     is( $count, 3, 'Patron now has 3 holds' );
529
530     my $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
531     is( $ret->{status}, 'OK', 'Patron can place hold with no borrower circ rules' );
532
533     my $rule_all = Koha::CirculationRules->set_rule(
534         {
535             categorycode => $category->{categorycode},
536             branchcode   => undef,
537             rule_name    => 'max_holds',
538             rule_value   => 3,
539         }
540     );
541
542     my $rule_branch = Koha::CirculationRules->set_rule(
543         {
544             branchcode   => $branch_1,
545             categorycode => $category->{categorycode},
546             rule_name    => 'max_holds',
547             rule_value   => 5,
548         }
549     );
550
551     $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
552     is( $ret->{status}, 'OK', 'Patron can place hold with branch/category rule of 5, category rule of 3' );
553
554     $rule_branch->delete();
555
556     $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
557     is( $ret->{status}, 'tooManyReserves', 'Patron cannot place hold with only a category rule of 3' );
558
559     $rule_all->delete();
560     $rule_branch->rule_value(3);
561     $rule_branch->store();
562
563     $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
564     is( $ret->{status}, 'tooManyReserves', 'Patron cannot place hold with only a branch/category rule of 3' );
565
566     $rule_branch->rule_value(5);
567     $rule_branch->update();
568     $rule_branch->rule_value(5);
569     $rule_branch->store();
570
571     $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
572     is( $ret->{status}, 'OK', 'Patron can place hold with branch/category rule of 5, category rule of 5' );
573 };
574
575 subtest 'Pickup location availability tests' => sub {
576     plan tests => 4;
577
578     $biblio = $builder->build_sample_biblio({ itemtype => 'ONLY1' });
579     $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber})->itemnumber;
580     #Add a default rule to allow some holds
581
582     Koha::CirculationRules->set_rules(
583         {
584             branchcode   => undef,
585             categorycode => undef,
586             itemtype     => undef,
587             rules        => {
588                 reservesallowed  => 25,
589                 holds_per_record => 99,
590             }
591         }
592     );
593     my $item = Koha::Items->find($itemnumber);
594     my $branch_to = $builder->build({ source => 'Branch' })->{ branchcode };
595     my $library = Koha::Libraries->find($branch_to);
596     $library->pickup_location('1')->store;
597     my $patron = $builder->build({ source => 'Borrower' })->{ borrowernumber };
598
599     t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
600     t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
601
602     $library->pickup_location('1')->store;
603     is(CanItemBeReserved($patron, $item->itemnumber, $branch_to)->{status},
604        'OK', 'Library is a pickup location');
605
606     my $limit = Koha::Item::Transfer::Limit->new({
607         fromBranch => $item->holdingbranch,
608         toBranch => $branch_to,
609         itemtype => $item->effective_itemtype,
610     })->store;
611     is(CanItemBeReserved($patron, $item->itemnumber, $branch_to)->{status},
612        'cannotBeTransferred', 'Item cannot be transferred');
613     $limit->delete;
614
615     $library->pickup_location('0')->store;
616     is(CanItemBeReserved($patron, $item->itemnumber, $branch_to)->{status},
617        'libraryNotPickupLocation', 'Library is not a pickup location');
618     is(CanItemBeReserved($patron, $item->itemnumber, 'nonexistent')->{status},
619        'libraryNotFound', 'Cannot set unknown library as pickup location');
620 };
621
622 $schema->storage->txn_rollback;
623
624 subtest 'CanItemBeReserved / holds_per_day tests' => sub {
625
626     plan tests => 10;
627
628     $schema->storage->txn_begin;
629
630     Koha::Holds->search->delete;
631     $dbh->do('DELETE FROM issues');
632     Koha::Items->search->delete;
633     Koha::Biblios->search->delete;
634
635     my $itemtype = $builder->build_object( { class => 'Koha::ItemTypes' } );
636     my $library  = $builder->build_object( { class => 'Koha::Libraries' } );
637     my $patron   = $builder->build_object( { class => 'Koha::Patrons' } );
638
639     # Create 3 biblios with items
640     my $biblio_1 = $builder->build_sample_biblio({ itemtype => $itemtype->itemtype });
641     my $itemnumber_1 = $builder->build_sample_item({ library => $library->branchcode, biblionumber => $biblio_1->biblionumber})->itemnumber;
642     my $biblio_2 = $builder->build_sample_biblio({ itemtype => $itemtype->itemtype });
643     my $itemnumber_2 = $builder->build_sample_item({ library => $library->branchcode, biblionumber => $biblio_2->biblionumber})->itemnumber;
644     my $biblio_3 = $builder->build_sample_biblio({ itemtype => $itemtype->itemtype });
645     my $itemnumber_3 = $builder->build_sample_item({ library => $library->branchcode, biblionumber => $biblio_3->biblionumber})->itemnumber;
646
647     Koha::CirculationRules->search->delete;
648     Koha::CirculationRules->set_rules(
649         {
650             categorycode => '*',
651             branchcode   => '*',
652             itemtype     => $itemtype->itemtype,
653             rules        => {
654                 reservesallowed  => 1,
655                 holds_per_record => 99,
656                 holds_per_day    => 2
657             }
658         }
659     );
660
661     is_deeply(
662         CanItemBeReserved( $patron->borrowernumber, $itemnumber_1 ),
663         { status => 'OK' },
664         'Patron can reserve item with hold limit of 1, no holds placed'
665     );
666
667     AddReserve(
668         {
669             branchcode     => $library->branchcode,
670             borrowernumber => $patron->borrowernumber,
671             biblionumber   => $biblio_1->biblionumber,
672             priority       => 1,
673         }
674     );
675
676     is_deeply(
677         CanItemBeReserved( $patron->borrowernumber, $itemnumber_1 ),
678         { status => 'tooManyReserves', limit => 1 },
679         'Patron cannot reserve item with hold limit of 1, 1 bib level hold placed'
680     );
681
682     # Raise reservesallowed to avoid tooManyReserves from it
683     Koha::CirculationRules->set_rule(
684         {
685
686             categorycode => '*',
687             branchcode   => '*',
688             itemtype     => $itemtype->itemtype,
689             rule_name  => 'reservesallowed',
690             rule_value => 3,
691         }
692     );
693
694     is_deeply(
695         CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
696         { status => 'OK' },
697         'Patron can reserve item with 2 reserves daily cap'
698     );
699
700     # Add a second reserve
701     my $res_id = AddReserve(
702         {
703             branchcode     => $library->branchcode,
704             borrowernumber => $patron->borrowernumber,
705             biblionumber   => $biblio_2->biblionumber,
706             priority       => 1,
707         }
708     );
709     is_deeply(
710         CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
711         { status => 'tooManyReservesToday', limit => 2 },
712         'Patron cannot a third item with 2 reserves daily cap'
713     );
714
715     # Update last hold so reservedate is in the past, so 2 holds, but different day
716     $hold = Koha::Holds->find($res_id);
717     my $yesterday = dt_from_string() - DateTime::Duration->new( days => 1 );
718     $hold->reservedate($yesterday)->store;
719
720     is_deeply(
721         CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
722         { status => 'OK' },
723         'Patron can reserve item with 2 bib level hold placed on different days, 2 reserves daily cap'
724     );
725
726     # Set holds_per_day to 0
727     Koha::CirculationRules->set_rule(
728         {
729
730             categorycode => '*',
731             branchcode   => '*',
732             itemtype     => $itemtype->itemtype,
733             rule_name  => 'holds_per_day',
734             rule_value => 0,
735         }
736     );
737
738
739     # Delete existing holds
740     Koha::Holds->search->delete;
741     is_deeply(
742         CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
743         { status => 'tooManyReservesToday', limit => 0 },
744         'Patron cannot reserve if holds_per_day is 0 (i.e. 0 is 0)'
745     );
746
747     Koha::CirculationRules->set_rule(
748         {
749
750             categorycode => '*',
751             branchcode   => '*',
752             itemtype     => $itemtype->itemtype,
753             rule_name  => 'holds_per_day',
754             rule_value => undef,
755         }
756     );
757
758     Koha::Holds->search->delete;
759     is_deeply(
760         CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
761         { status => 'OK' },
762         'Patron can reserve if holds_per_day is undef (i.e. undef is unlimited daily cap)'
763     );
764     AddReserve(
765         {
766             branchcode     => $library->branchcode,
767             borrowernumber => $patron->borrowernumber,
768             biblionumber   => $biblio_1->biblionumber,
769             priority       => 1,
770         }
771     );
772     AddReserve(
773         {
774             branchcode     => $library->branchcode,
775             borrowernumber => $patron->borrowernumber,
776             biblionumber   => $biblio_2->biblionumber,
777             priority       => 1,
778         }
779     );
780
781     is_deeply(
782         CanItemBeReserved( $patron->borrowernumber, $itemnumber_3 ),
783         { status => 'OK' },
784         'Patron can reserve if holds_per_day is undef (i.e. undef is unlimited daily cap)'
785     );
786     AddReserve(
787         {
788             branchcode     => $library->branchcode,
789             borrowernumber => $patron->borrowernumber,
790             biblionumber   => $biblio_3->biblionumber,
791             priority       => 1,
792         }
793     );
794     is_deeply(
795         CanItemBeReserved( $patron->borrowernumber, $itemnumber_3 ),
796         { status => 'tooManyReserves', limit => 3 },
797         'Unlimited daily holds, but reached reservesallowed'
798     );
799     #results should be the same for both ReservesControlBranch settings
800     t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary');
801     is_deeply(
802         CanItemBeReserved( $patron->borrowernumber, $itemnumber_3 ),
803         { status => 'tooManyReserves', limit => 3 },
804         'Unlimited daily holds, but reached reservesallowed'
805     );
806
807     $schema->storage->txn_rollback;
808 };
809
810 subtest 'CanItemBeReserved / branch_not_in_hold_group' => sub {
811     plan tests => 9;
812
813     $schema->storage->txn_begin;
814
815     # Cleanup database
816     Koha::Holds->search->delete;
817     $dbh->do('DELETE FROM issues');
818     Koha::CirculationRules->set_rule(
819         {
820             branchcode   => undef,
821             categorycode => undef,
822             itemtype     => undef,
823             rule_name    => 'reservesallowed',
824             rule_value   => 25,
825         }
826     );
827
828     Koha::Items->search->delete;
829     Koha::Biblios->search->delete;
830
831     # Create item types
832     my $itemtype1 = $builder->build_object( { class => 'Koha::ItemTypes' } );
833     my $itemtype2 = $builder->build_object( { class => 'Koha::ItemTypes' } );
834
835     # Create libraries
836     my $library1  = $builder->build_object( { class => 'Koha::Libraries' } );
837     my $library2  = $builder->build_object( { class => 'Koha::Libraries' } );
838     my $library3  = $builder->build_object( { class => 'Koha::Libraries' } );
839
840     # Create library groups hierarchy
841     my $rootgroup  = $builder->build_object( { class => 'Koha::Library::Groups', value => {ft_local_hold_group => 1} } );
842     my $group1  = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library1->branchcode}} );
843     my $group2  = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library2->branchcode} } );
844
845     # Create 2 patrons
846     my $patron1   = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library1->branchcode} } );
847     my $patron3   = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library3->branchcode} } );
848
849     # Create 3 biblios with items
850     my $biblio_1 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
851     my $item_1   = $builder->build_sample_item(
852         {
853             biblionumber => $biblio_1->biblionumber,
854             library      => $library1->branchcode
855         }
856     );
857     my $biblio_2 = $builder->build_sample_biblio({ itemtype => $itemtype2->itemtype });
858     my $item_2   = $builder->build_sample_item(
859         {
860             biblionumber => $biblio_2->biblionumber,
861             library      => $library2->branchcode
862         }
863     );
864     my $itemnumber_2 = $item_2->itemnumber;
865     my $biblio_3 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
866     my $item_3   = $builder->build_sample_item(
867         {
868             biblionumber => $biblio_3->biblionumber,
869             library      => $library1->branchcode
870         }
871     );
872
873     # Test 1: Patron 3 can place hold
874     is_deeply(
875         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
876         { status => 'OK' },
877         'Patron can place hold if no circ_rules where defined'
878     );
879
880     # Insert default circ rule of holds allowed only from local hold group for all libraries
881     Koha::CirculationRules->set_rules(
882         {
883             branchcode => undef,
884             itemtype   => undef,
885             rules => {
886                 holdallowed => 3,
887                 hold_fulfillment_policy => 'any',
888                 returnbranch => 'any'
889             }
890         }
891     );
892
893     # Test 2: Patron 1 can place hold
894     is_deeply(
895         CanItemBeReserved( $patron1->borrowernumber, $itemnumber_2 ),
896         { status => 'OK' },
897         'Patron can place hold because patron\'s home library is part of hold group'
898     );
899
900     # Test 3: Patron 3 cannot place hold
901     is_deeply(
902         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
903         { status => 'branchNotInHoldGroup' },
904         'Patron cannot place hold because patron\'s home library is not part of hold group'
905     );
906
907     # Insert default circ rule to "any" for library 2
908     Koha::CirculationRules->set_rules(
909         {
910             branchcode => $library2->branchcode,
911             itemtype   => undef,
912             rules => {
913                 holdallowed => 2,
914                 hold_fulfillment_policy => 'any',
915                 returnbranch => 'any'
916             }
917         }
918     );
919
920     # Test 4: Patron 3 can place hold
921     is_deeply(
922         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
923         { status => 'OK' },
924         'Patron can place hold if holdallowed is set to "any" for library 2'
925     );
926
927     # Update default circ rule to "hold group" for library 2
928     Koha::CirculationRules->set_rules(
929         {
930             branchcode => $library2->branchcode,
931             itemtype   => undef,
932             rules => {
933                 holdallowed => 3,
934                 hold_fulfillment_policy => 'any',
935                 returnbranch => 'any'
936             }
937         }
938     );
939
940     # Test 5: Patron 3 cannot place hold
941     is_deeply(
942         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
943         { status => 'branchNotInHoldGroup' },
944         'Patron cannot place hold if holdallowed is set to "hold group" for library 2'
945     );
946
947     # Insert default item rule to "any" for itemtype 2
948     Koha::CirculationRules->set_rules(
949         {
950             branchcode => $library2->branchcode,
951             itemtype   => $itemtype2->itemtype,
952             rules => {
953                 holdallowed => 2,
954                 hold_fulfillment_policy => 'any',
955                 returnbranch => 'any'
956             }
957         }
958     );
959
960     # Test 6: Patron 3 can place hold
961     is_deeply(
962         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
963         { status => 'OK' },
964         'Patron can place hold if holdallowed is set to "any" for itemtype 2'
965     );
966
967     # Update default item rule to "hold group" for itemtype 2
968     Koha::CirculationRules->set_rules(
969         {
970             branchcode => $library2->branchcode,
971             itemtype   => $itemtype2->itemtype,
972             rules => {
973                 holdallowed => 3,
974                 hold_fulfillment_policy => 'any',
975                 returnbranch => 'any'
976             }
977         }
978     );
979
980     # Test 7: Patron 3 cannot place hold
981     is_deeply(
982         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
983         { status => 'branchNotInHoldGroup' },
984         'Patron cannot place hold if holdallowed is set to "hold group" for itemtype 2'
985     );
986
987     # Insert branch item rule to "any" for itemtype 2 and library 2
988     Koha::CirculationRules->set_rules(
989         {
990             branchcode => $library2->branchcode,
991             itemtype   => $itemtype2->itemtype,
992             rules => {
993                 holdallowed => 2,
994                 hold_fulfillment_policy => 'any',
995                 returnbranch => 'any'
996             }
997         }
998     );
999
1000     # Test 8: Patron 3 can place hold
1001     is_deeply(
1002         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
1003         { status => 'OK' },
1004         'Patron can place hold if holdallowed is set to "any" for itemtype 2 and library 2'
1005     );
1006
1007     # Update branch item rule to "hold group" for itemtype 2 and library 2
1008     Koha::CirculationRules->set_rules(
1009         {
1010             branchcode => $library2->branchcode,
1011             itemtype   => $itemtype2->itemtype,
1012             rules => {
1013                 holdallowed => 3,
1014                 hold_fulfillment_policy => 'any',
1015                 returnbranch => 'any'
1016             }
1017         }
1018     );
1019
1020     # Test 9: Patron 3 cannot place hold
1021     is_deeply(
1022         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
1023         { status => 'branchNotInHoldGroup' },
1024         'Patron cannot place hold if holdallowed is set to "hold group" for itemtype 2 and library 2'
1025     );
1026
1027     $schema->storage->txn_rollback;
1028
1029 };
1030
1031 subtest 'CanItemBeReserved / pickup_not_in_hold_group' => sub {
1032     plan tests => 9;
1033
1034     $schema->storage->txn_begin;
1035
1036     # Cleanup database
1037     Koha::Holds->search->delete;
1038     $dbh->do('DELETE FROM issues');
1039     Koha::CirculationRules->set_rule(
1040         {
1041             branchcode   => undef,
1042             categorycode => undef,
1043             itemtype     => undef,
1044             rule_name    => 'reservesallowed',
1045             rule_value   => 25,
1046         }
1047     );
1048
1049     Koha::Items->search->delete;
1050     Koha::Biblios->search->delete;
1051
1052     # Create item types
1053     my $itemtype1 = $builder->build_object( { class => 'Koha::ItemTypes' } );
1054     my $itemtype2 = $builder->build_object( { class => 'Koha::ItemTypes' } );
1055
1056     # Create libraries
1057     my $library1  = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } );
1058     my $library2  = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } );
1059     my $library3  = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } );
1060
1061     # Create library groups hierarchy
1062     my $rootgroup  = $builder->build_object( { class => 'Koha::Library::Groups', value => {ft_local_hold_group => 1} } );
1063     my $group1  = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library1->branchcode}} );
1064     my $group2  = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library2->branchcode} } );
1065
1066     # Create 2 patrons
1067     my $patron1   = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library1->branchcode} } );
1068     my $patron3   = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library3->branchcode} } );
1069
1070     # Create 3 biblios with items
1071     my $biblio_1 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
1072     my $item_1   = $builder->build_sample_item(
1073         {
1074             biblionumber => $biblio_1->biblionumber,
1075             library      => $library1->branchcode
1076         }
1077     );
1078     my $biblio_2 = $builder->build_sample_biblio({ itemtype => $itemtype2->itemtype });
1079     my $item_2   = $builder->build_sample_item(
1080         {
1081             biblionumber => $biblio_2->biblionumber,
1082             library      => $library2->branchcode
1083         }
1084     );
1085     my $itemnumber_2 = $item_2->itemnumber;
1086     my $biblio_3 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
1087     my $item_3   = $builder->build_sample_item(
1088         {
1089             biblionumber => $biblio_3->biblionumber,
1090             library      => $library1->branchcode
1091         }
1092     );
1093
1094     # Test 1: Patron 3 can place hold
1095     is_deeply(
1096         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1097         { status => 'OK' },
1098         'Patron can place hold if no circ_rules where defined'
1099     );
1100
1101     # Insert default circ rule of holds allowed only from local hold group for all libraries
1102     Koha::CirculationRules->set_rules(
1103         {
1104             branchcode => undef,
1105             itemtype   => undef,
1106             rules => {
1107                 holdallowed => 2,
1108                 hold_fulfillment_policy => 'holdgroup',
1109                 returnbranch => 'any'
1110             }
1111         }
1112     );
1113
1114     # Test 2: Patron 1 can place hold
1115     is_deeply(
1116         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library1->branchcode ),
1117         { status => 'OK' },
1118         'Patron can place hold because pickup location is part of hold group'
1119     );
1120
1121     # Test 3: Patron 3 cannot place hold
1122     is_deeply(
1123         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1124         { status => 'pickupNotInHoldGroup' },
1125         'Patron cannot place hold because pickup location is not part of hold group'
1126     );
1127
1128     # Insert default circ rule to "any" for library 2
1129     Koha::CirculationRules->set_rules(
1130         {
1131             branchcode => $library2->branchcode,
1132             itemtype   => undef,
1133             rules => {
1134                 holdallowed => 2,
1135                 hold_fulfillment_policy => 'any',
1136                 returnbranch => 'any'
1137             }
1138         }
1139     );
1140
1141     # Test 4: Patron 3 can place hold
1142     is_deeply(
1143         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1144         { status => 'OK' },
1145         'Patron can place hold if default_branch_circ_rules is set to "any" for library 2'
1146     );
1147
1148     # Update default circ rule to "hold group" for library 2
1149     Koha::CirculationRules->set_rules(
1150         {
1151             branchcode => $library2->branchcode,
1152             itemtype   => undef,
1153             rules => {
1154                 holdallowed => 2,
1155                 hold_fulfillment_policy => 'holdgroup',
1156                 returnbranch => 'any'
1157             }
1158         }
1159     );
1160
1161     # Test 5: Patron 3 cannot place hold
1162     is_deeply(
1163         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1164         { status => 'pickupNotInHoldGroup' },
1165         'Patron cannot place hold if hold_fulfillment_policy is set to "hold group" for library 2'
1166     );
1167
1168     # Insert default item rule to "any" for itemtype 2
1169     Koha::CirculationRules->set_rules(
1170         {
1171             branchcode => $library2->branchcode,
1172             itemtype   => $itemtype2->itemtype,
1173             rules => {
1174                 holdallowed => 2,
1175                 hold_fulfillment_policy => 'any',
1176                 returnbranch => 'any'
1177             }
1178         }
1179     );
1180
1181     # Test 6: Patron 3 can place hold
1182     is_deeply(
1183         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1184         { status => 'OK' },
1185         'Patron can place hold if hold_fulfillment_policy is set to "any" for itemtype 2'
1186     );
1187
1188     # Update default item rule to "hold group" for itemtype 2
1189     Koha::CirculationRules->set_rules(
1190         {
1191             branchcode => $library2->branchcode,
1192             itemtype   => $itemtype2->itemtype,
1193             rules => {
1194                 holdallowed => 2,
1195                 hold_fulfillment_policy => 'holdgroup',
1196                 returnbranch => 'any'
1197             }
1198         }
1199     );
1200
1201     # Test 7: Patron 3 cannot place hold
1202     is_deeply(
1203         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1204         { status => 'pickupNotInHoldGroup' },
1205         'Patron cannot place hold if hold_fulfillment_policy is set to "hold group" for itemtype 2'
1206     );
1207
1208     # Insert branch item rule to "any" for itemtype 2 and library 2
1209     Koha::CirculationRules->set_rules(
1210         {
1211             branchcode => $library2->branchcode,
1212             itemtype   => $itemtype2->itemtype,
1213             rules => {
1214                 holdallowed => 2,
1215                 hold_fulfillment_policy => 'any',
1216                 returnbranch => 'any'
1217             }
1218         }
1219     );
1220
1221     # Test 8: Patron 3 can place hold
1222     is_deeply(
1223         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1224         { status => 'OK' },
1225         'Patron can place hold if hold_fulfillment_policy is set to "any" for itemtype 2 and library 2'
1226     );
1227
1228     # Update branch item rule to "hold group" for itemtype 2 and library 2
1229     Koha::CirculationRules->set_rules(
1230         {
1231             branchcode => $library2->branchcode,
1232             itemtype   => $itemtype2->itemtype,
1233             rules => {
1234                 holdallowed => 2,
1235                 hold_fulfillment_policy => 'holdgroup',
1236                 returnbranch => 'any'
1237             }
1238         }
1239     );
1240
1241     # Test 9: Patron 3 cannot place hold
1242     is_deeply(
1243         CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1244         { status => 'pickupNotInHoldGroup' },
1245         'Patron cannot place hold if hold_fulfillment_policy is set to "hold group" for itemtype 2 and library 2'
1246     );
1247
1248     $schema->storage->txn_rollback;
1249 };