Bug 19532: (follow-up) aria-hidden attr on OPAC, and more
[koha.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 => 73;
11 use Test::Exception;
12
13 use MARC::Record;
14
15 use C4::Biblio;
16 use C4::Calendar;
17 use C4::Items;
18 use C4::Reserves qw( AddReserve CalculatePriority ModReserve ToggleSuspend AutoUnsuspendReserves SuspendAll ModReserveMinusPriority AlterPriority CanItemBeReserved CheckReserves );
19 use C4::Circulation qw( CanBookBeRenewed );
20
21 use Koha::Biblios;
22 use Koha::CirculationRules;
23 use Koha::Database;
24 use Koha::DateUtils qw( dt_from_string output_pref );
25 use Koha::Holds;
26 use Koha::Checkout;
27 use Koha::Item::Transfer::Limits;
28 use Koha::Items;
29 use Koha::Libraries;
30 use Koha::Library::Groups;
31 use Koha::Patrons;
32
33 BEGIN {
34     use FindBin;
35     use lib $FindBin::Bin;
36 }
37
38 my $schema  = Koha::Database->new->schema;
39 $schema->storage->txn_begin;
40
41 my $builder = t::lib::TestBuilder->new();
42 my $dbh     = C4::Context->dbh;
43
44 # Create two random branches
45 my $branch_1 = $builder->build({ source => 'Branch' })->{ branchcode };
46 my $branch_2 = $builder->build({ source => 'Branch' })->{ branchcode };
47
48 my $category = $builder->build({ source => 'Category' });
49
50 my $borrowers_count = 5;
51
52 $dbh->do('DELETE FROM itemtypes');
53 $dbh->do('DELETE FROM reserves');
54 $dbh->do('DELETE FROM circulation_rules');
55 my $insert_sth = $dbh->prepare('INSERT INTO itemtypes (itemtype) VALUES (?)');
56 $insert_sth->execute('CAN');
57 $insert_sth->execute('CANNOT');
58 $insert_sth->execute('DUMMY');
59 $insert_sth->execute('ONLY1');
60
61 # Setup Test------------------------
62 my $biblio = $builder->build_sample_biblio({ itemtype => 'DUMMY' });
63
64 # Create item instance for testing.
65 my $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber })->itemnumber;
66
67 # Create some borrowers
68 my @borrowernumbers;
69 my @patrons;
70 foreach (1..$borrowers_count) {
71     my $patron = Koha::Patron->new({
72         firstname =>  'my firstname',
73         surname => 'my surname ' . $_,
74         categorycode => $category->{categorycode},
75         branchcode => $branch_1,
76     })->store;
77     push @patrons, $patron;
78     push @borrowernumbers, $patron->borrowernumber;
79 }
80
81 # Create five item level holds
82 foreach my $borrowernumber ( @borrowernumbers ) {
83     AddReserve(
84         {
85             branchcode     => $branch_1,
86             borrowernumber => $borrowernumber,
87             biblionumber   => $biblio->biblionumber,
88             priority       => C4::Reserves::CalculatePriority( $biblio->biblionumber ),
89             itemnumber     => $itemnumber,
90         }
91     );
92 }
93
94 my $holds = $biblio->holds;
95 is( $holds->count, $borrowers_count, 'Test GetReserves()' );
96 is( $holds->next->priority, 1, "Reserve 1 has a priority of 1" );
97 is( $holds->next->priority, 2, "Reserve 2 has a priority of 2" );
98 is( $holds->next->priority, 3, "Reserve 3 has a priority of 3" );
99 is( $holds->next->priority, 4, "Reserve 4 has a priority of 4" );
100 is( $holds->next->priority, 5, "Reserve 5 has a priority of 5" );
101
102 my $item = Koha::Items->find( $itemnumber );
103 $holds = $item->current_holds;
104 my $first_hold = $holds->next;
105 my $reservedate = $first_hold->reservedate;
106 my $borrowernumber = $first_hold->borrowernumber;
107 my $branch_1code = $first_hold->branchcode;
108 my $reserve_id = $first_hold->reserve_id;
109 is( $reservedate, output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 }), "holds_placed_today should return a valid reserve date");
110 is( $borrowernumber, $borrowernumbers[0], "holds_placed_today should return a valid borrowernumber");
111 is( $branch_1code, $branch_1, "holds_placed_today should return a valid branchcode");
112 ok($reserve_id, "Test holds_placed_today()");
113
114 my $hold = Koha::Holds->find( $reserve_id );
115 ok( $hold, "Koha::Holds found the hold" );
116 my $hold_biblio = $hold->biblio();
117 ok( $hold_biblio, "Got biblio using biblio() method" );
118 ok( $hold_biblio == $hold->biblio(), "biblio method returns stashed biblio" );
119 my $hold_item = $hold->item();
120 ok( $hold_item, "Got item using item() method" );
121 ok( $hold_item == $hold->item(), "item method returns stashed item" );
122 my $hold_branch = $hold->branch();
123 ok( $hold_branch, "Got branch using branch() method" );
124 ok( $hold_branch == $hold->branch(), "branch method returns stashed branch" );
125 my $hold_found = $hold->found();
126 $hold->set({ found => 'W'})->store();
127 is( Koha::Holds->waiting()->count(), 1, "Koha::Holds->waiting returns waiting holds" );
128 is( Koha::Holds->unfilled()->count(), 4, "Koha::Holds->unfilled returns unfilled holds" );
129
130 my $patron = Koha::Patrons->find( $borrowernumbers[0] );
131 $holds = $patron->holds;
132 is( $holds->next->borrowernumber, $borrowernumbers[0], "Test Koha::Patron->holds");
133
134
135 $holds = $item->current_holds;
136 $first_hold = $holds->next;
137 $borrowernumber = $first_hold->borrowernumber;
138 $branch_1code = $first_hold->branchcode;
139 $reserve_id = $first_hold->reserve_id;
140
141 ModReserve({
142     reserve_id    => $reserve_id,
143     rank          => '4',
144     branchcode    => $branch_1,
145     itemnumber    => $itemnumber,
146     suspend_until => output_pref( { dt => dt_from_string( "2013-01-01", "iso" ), dateonly => 1 } ),
147 });
148
149 $hold = Koha::Holds->find( $reserve_id );
150 ok( $hold->priority eq '4', "Test ModReserve, priority changed correctly" );
151 ok( $hold->suspend, "Test ModReserve, suspend hold" );
152 is( $hold->suspend_until, '2013-01-01 00:00:00', "Test ModReserve, suspend until date" );
153
154 ModReserve({ # call without reserve_id
155     rank          => '3',
156     biblionumber  => $biblio->biblionumber,
157     itemnumber    => $itemnumber,
158     borrowernumber => $borrowernumber,
159 });
160 $hold = Koha::Holds->find( $reserve_id );
161 ok( $hold->priority eq '3', "Test ModReserve, priority changed correctly" );
162
163 ToggleSuspend( $reserve_id );
164 $hold = Koha::Holds->find( $reserve_id );
165 ok( ! $hold->suspend, "Test ToggleSuspend(), no date" );
166
167 ToggleSuspend( $reserve_id, '2012-01-01' );
168 $hold = Koha::Holds->find( $reserve_id );
169 is( $hold->suspend_until, '2012-01-01 00:00:00', "Test ToggleSuspend(), with date" );
170
171 AutoUnsuspendReserves();
172 $hold = Koha::Holds->find( $reserve_id );
173 ok( ! $hold->suspend, "Test AutoUnsuspendReserves()" );
174
175 SuspendAll(
176     borrowernumber => $borrowernumber,
177     biblionumber   => $biblio->biblionumber,
178     suspend => 1,
179     suspend_until => '2012-01-01',
180 );
181 $hold = Koha::Holds->find( $reserve_id );
182 is( $hold->suspend, 1, "Test SuspendAll()" );
183 is( $hold->suspend_until, '2012-01-01 00:00:00', "Test SuspendAll(), with date" );
184
185 SuspendAll(
186     borrowernumber => $borrowernumber,
187     biblionumber   => $biblio->biblionumber,
188     suspend => 0,
189 );
190 $hold = Koha::Holds->find( $reserve_id );
191 is( $hold->suspend, 0, "Test resuming with SuspendAll()" );
192 is( $hold->suspend_until, undef, "Test resuming with SuspendAll(), should have no suspend until date" );
193
194 # Add a new hold for the borrower whose hold we canceled earlier, this time at the bib level
195     AddReserve(
196         {
197             branchcode     => $branch_1,
198             borrowernumber => $borrowernumbers[0],
199             biblionumber   => $biblio->biblionumber,
200         }
201     );
202
203 $patron = Koha::Patrons->find( $borrowernumber );
204 $holds = $patron->holds;
205 my $reserveid = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $borrowernumbers[0] })->next->reserve_id;
206 ModReserveMinusPriority( $itemnumber, $reserveid );
207 $holds = $patron->holds;
208 is( $holds->search({ itemnumber => $itemnumber })->count, 1, "Test ModReserveMinusPriority()" );
209
210 $holds = $biblio->holds;
211 $hold = $holds->next;
212 AlterPriority( 'top', $hold->reserve_id, undef, 2, 1, 6 );
213 $hold = Koha::Holds->find( $reserveid );
214 is( $hold->priority, '1', "Test AlterPriority(), move to top" );
215
216 AlterPriority( 'down', $hold->reserve_id, undef, 2, 1, 6 );
217 $hold = Koha::Holds->find( $reserveid );
218 is( $hold->priority, '2', "Test AlterPriority(), move down" );
219
220 AlterPriority( 'up', $hold->reserve_id, 1, 3, 1, 6 );
221 $hold = Koha::Holds->find( $reserveid );
222 is( $hold->priority, '1', "Test AlterPriority(), move up" );
223
224 AlterPriority( 'bottom', $hold->reserve_id, undef, 2, 1, 6 );
225 $hold = Koha::Holds->find( $reserveid );
226 is( $hold->priority, '6', "Test AlterPriority(), move to bottom" );
227
228
229 $hold->delete;
230 throws_ok
231     { C4::Reserves::ModReserve({ reserve_id => $hold->reserve_id }) }
232     'Koha::Exceptions::ObjectNotFound',
233     'No hold with id ' . $hold->reserve_id;
234
235 # Regression test for bug 2394
236 #
237 # If IndependentBranches is ON and canreservefromotherbranches is OFF,
238 # a patron is not permittedo to request an item whose homebranch (i.e.,
239 # owner of the item) is different from the patron's own library.
240 # However, if canreservefromotherbranches is turned ON, the patron can
241 # create such hold requests.
242 #
243 # Note that canreservefromotherbranches has no effect if
244 # IndependentBranches is OFF.
245
246 my $foreign_biblio = $builder->build_sample_biblio({ itemtype => 'DUMMY' });
247 my $foreign_item = $builder->build_sample_item({ library => $branch_2, biblionumber => $foreign_biblio->biblionumber });
248 Koha::CirculationRules->set_rules(
249     {
250         categorycode => undef,
251         branchcode   => undef,
252         itemtype     => undef,
253         rules        => {
254             reservesallowed  => 25,
255             holds_per_record => 99,
256         }
257     }
258 );
259 Koha::CirculationRules->set_rules(
260     {
261         categorycode => undef,
262         branchcode   => undef,
263         itemtype     => 'CANNOT',
264         rules        => {
265             reservesallowed  => 0,
266             holds_per_record => 99,
267         }
268     }
269 );
270
271 # make sure some basic sysprefs are set
272 t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary');
273 t::lib::Mocks::mock_preference('item-level_itypes', 1);
274
275 # if IndependentBranches is OFF, a $branch_1 patron can reserve an $branch_2 item
276 t::lib::Mocks::mock_preference('IndependentBranches', 0);
277
278 is(
279     CanItemBeReserved($patrons[0], $foreign_item)->{status}, 'OK',
280     '$branch_1 patron allowed to reserve $branch_2 item with IndependentBranches OFF (bug 2394)'
281 );
282
283 # if IndependentBranches is OFF, a $branch_1 patron cannot reserve an $branch_2 item
284 t::lib::Mocks::mock_preference('IndependentBranches', 1);
285 t::lib::Mocks::mock_preference('canreservefromotherbranches', 0);
286 ok(
287     CanItemBeReserved($patrons[0], $foreign_item)->{status} eq 'cannotReserveFromOtherBranches',
288     '$branch_1 patron NOT allowed to reserve $branch_2 item with IndependentBranches ON ... (bug 2394)'
289 );
290
291 # ... unless canreservefromotherbranches is ON
292 t::lib::Mocks::mock_preference('canreservefromotherbranches', 1);
293 ok(
294     CanItemBeReserved($patrons[0], $foreign_item)->{status} eq 'OK',
295     '... unless canreservefromotherbranches is ON (bug 2394)'
296 );
297
298 {
299     # Regression test for bug 11336 # Test if ModReserve correctly recalculate the priorities
300     $biblio = $builder->build_sample_biblio({ itemtype => 'DUMMY' });
301     $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber })->itemnumber;
302     my $reserveid1 = AddReserve(
303         {
304             branchcode     => $branch_1,
305             borrowernumber => $borrowernumbers[0],
306             biblionumber   => $biblio->biblionumber,
307             priority       => 1
308         }
309     );
310
311     $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber })->itemnumber;
312     my $reserveid2 = AddReserve(
313         {
314             branchcode     => $branch_1,
315             borrowernumber => $borrowernumbers[1],
316             biblionumber   => $biblio->biblionumber,
317             priority       => 2
318         }
319     );
320
321     $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber })->itemnumber;
322     my $reserveid3 = AddReserve(
323         {
324             branchcode     => $branch_1,
325             borrowernumber => $borrowernumbers[2],
326             biblionumber   => $biblio->biblionumber,
327             priority       => 3
328         }
329     );
330
331     my $hhh = Koha::Holds->search({ biblionumber => $biblio->biblionumber });
332     my $hold3 = Koha::Holds->find( $reserveid3 );
333     is( $hold3->priority, 3, "The 3rd hold should have a priority set to 3" );
334     ModReserve({ reserve_id => $reserveid1, rank => 'del' });
335     ModReserve({ reserve_id => $reserveid2, rank => 'del' });
336     is( $hold3->discard_changes->priority, 1, "After ModReserve, the 3rd reserve becomes the first on the waiting list" );
337 }
338
339 my $damaged_item = Koha::Items->find($itemnumber)->damaged(1)->store; # FIXME The $itemnumber is a bit confusing here
340 t::lib::Mocks::mock_preference( 'AllowHoldsOnDamagedItems', 1 );
341 is( CanItemBeReserved( $patrons[0], $damaged_item)->{status}, 'OK', "Patron can reserve damaged item with AllowHoldsOnDamagedItems enabled" );
342 ok( defined( ( CheckReserves($itemnumber) )[1] ), "Hold can be trapped for damaged item with AllowHoldsOnDamagedItems enabled" );
343
344 $hold = Koha::Hold->new(
345     {
346         borrowernumber => $borrowernumbers[0],
347         itemnumber     => $itemnumber,
348         biblionumber   => $biblio->biblionumber,
349     }
350 )->store();
351 is( CanItemBeReserved( $patrons[0], $damaged_item )->{status},
352     'itemAlreadyOnHold',
353     "Patron cannot place a second item level hold for a given item" );
354 $hold->delete();
355
356 t::lib::Mocks::mock_preference( 'AllowHoldsOnDamagedItems', 0 );
357 ok( CanItemBeReserved( $patrons[0], $damaged_item)->{status} eq 'damaged', "Patron cannot reserve damaged item with AllowHoldsOnDamagedItems disabled" );
358 ok( !defined( ( CheckReserves($itemnumber) )[1] ), "Hold cannot be trapped for damaged item with AllowHoldsOnDamagedItems disabled" );
359
360 # Items that are not for loan, but holdable should not be trapped until they are available for loan
361 t::lib::Mocks::mock_preference( 'TrapHoldsOnOrder', 0 );
362 my $nfl_item = Koha::Items->find($itemnumber)->damaged(0)->notforloan(-1)->store;
363 Koha::Holds->search({ biblionumber => $biblio->id })->delete();
364 is( CanItemBeReserved( $patrons[0], $nfl_item)->{status}, 'OK', "Patron can place hold on item that is not for loan but holdable ( notforloan < 0 )" );
365 $hold = Koha::Hold->new(
366     {
367         borrowernumber => $borrowernumbers[0],
368         itemnumber     => $itemnumber,
369         biblionumber   => $biblio->biblionumber,
370         found          => undef,
371         priority       => 1,
372         reservedate    => dt_from_string,
373         branchcode     => $branch_1,
374     }
375 )->store();
376 ok( !defined( ( CheckReserves($itemnumber) )[1] ), "Hold cannot be trapped for item that is not for loan but holdable ( notforloan < 0 )" );
377 t::lib::Mocks::mock_preference( 'TrapHoldsOnOrder', 1 );
378 ok( defined( ( CheckReserves($itemnumber) )[1] ), "Hold is trapped for item that is not for loan but holdable ( notforloan < 0 )" );
379 t::lib::Mocks::mock_preference( 'SkipHoldTrapOnNotForLoanValue', '-1' );
380 ok( !defined( ( CheckReserves($itemnumber) )[1] ), "Hold cannot be trapped for item with notforloan value matching SkipHoldTrapOnNotForLoanValue" );
381 t::lib::Mocks::mock_preference( 'SkipHoldTrapOnNotForLoanValue', '-1|1' );
382 ok( !defined( ( CheckReserves($itemnumber) )[1] ), "Hold cannot be trapped for item with notforloan value matching SkipHoldTrapOnNotForLoanValue" );
383 is(
384     CanItemBeReserved( $patrons[0], $nfl_item)->{status}, 'itemAlreadyOnHold',
385     "cannot request item that you have already reservedd"
386 );
387 is(
388     CanItemBeReserved( $patrons[0], $item, undef, { ignore_hold_counts => 1 })->{status}, 'OK',
389     "can request item if we are not checking holds counts, but only if policy allows or forbids it"
390 );
391 $hold->delete();
392
393 # Regression test for bug 9532
394 $biblio = $builder->build_sample_biblio({ itemtype => 'CANNOT' });
395 $item = $builder->build_sample_item({ library => $branch_1, itype => 'CANNOT', biblionumber => $biblio->biblionumber});
396 AddReserve(
397     {
398         branchcode     => $branch_1,
399         borrowernumber => $borrowernumbers[0],
400         biblionumber   => $biblio->biblionumber,
401         priority       => 1,
402     }
403 );
404 is(
405     CanItemBeReserved( $patrons[0], $item)->{status}, 'noReservesAllowed',
406     "cannot request item if policy that matches on item-level item type forbids it"
407 );
408 is(
409     CanItemBeReserved( $patrons[0], $item, undef, { ignore_hold_counts => 1 })->{status}, 'noReservesAllowed',
410     "cannot request item if policy that matches on item-level item type forbids it even if ignoring counts"
411 );
412
413 subtest 'CanItemBeReserved' => sub {
414     plan tests => 2;
415
416     my $itemtype_can         = $builder->build({source => "Itemtype"})->{itemtype};
417     my $itemtype_cant        = $builder->build({source => "Itemtype"})->{itemtype};
418     my $itemtype_cant_record = $builder->build({source => "Itemtype"})->{itemtype};
419
420     Koha::CirculationRules->set_rules(
421         {
422             categorycode => undef,
423             branchcode   => undef,
424             itemtype     => $itemtype_cant,
425             rules        => {
426                 reservesallowed  => 0,
427                 holds_per_record => 99,
428             }
429         }
430     );
431     Koha::CirculationRules->set_rules(
432         {
433             categorycode => undef,
434             branchcode   => undef,
435             itemtype     => $itemtype_can,
436             rules        => {
437                 reservesallowed  => 2,
438                 holds_per_record => 2,
439             }
440         }
441     );
442     Koha::CirculationRules->set_rules(
443         {
444             categorycode => undef,
445             branchcode   => undef,
446             itemtype     => $itemtype_cant_record,
447             rules        => {
448                 reservesallowed  => 0,
449                 holds_per_record => 0,
450             }
451         }
452     );
453
454     Koha::CirculationRules->set_rules(
455         {
456             branchcode => $branch_1,
457             itemtype   => $itemtype_cant,
458             rules => {
459                 holdallowed => 0,
460                 returnbranch => 'homebranch',
461             }
462         }
463     );
464     Koha::CirculationRules->set_rules(
465         {
466             branchcode => $branch_1,
467             itemtype   => $itemtype_can,
468             rules => {
469                 holdallowed => 1,
470                 returnbranch => 'homebranch',
471             }
472         }
473     );
474
475     subtest 'noReservesAllowed' => sub {
476         plan tests => 5;
477
478         my $biblionumber_cannot = $builder->build_sample_biblio({ itemtype => $itemtype_cant })->biblionumber;
479         my $biblionumber_can = $builder->build_sample_biblio({ itemtype => $itemtype_can })->biblionumber;
480         my $biblionumber_record_cannot = $builder->build_sample_biblio({ itemtype => $itemtype_cant_record })->biblionumber;
481
482         my $item_1_can = $builder->build_sample_item({ homebranch => $branch_1, holdingbranch => $branch_1, itype => $itemtype_can, biblionumber => $biblionumber_cannot });
483         my $item_1_cannot = $builder->build_sample_item({ homebranch => $branch_1, holdingbranch => $branch_1, itype => $itemtype_cant, biblionumber => $biblionumber_cannot });
484         my $item_2_can = $builder->build_sample_item({ homebranch => $branch_1, holdingbranch => $branch_1, itype => $itemtype_can, biblionumber => $biblionumber_can });
485         my $item_2_cannot = $builder->build_sample_item({ homebranch => $branch_1, holdingbranch => $branch_1, itype => $itemtype_cant, biblionumber => $biblionumber_can });
486         my $item_3_cannot = $builder->build_sample_item({ homebranch => $branch_1, holdingbranch => $branch_1, itype => $itemtype_cant_record, biblionumber => $biblionumber_record_cannot });
487
488         Koha::Holds->search({borrowernumber => $borrowernumbers[0]})->delete;
489
490         t::lib::Mocks::mock_preference('item-level_itypes', 1);
491         is(
492             CanItemBeReserved( $patrons[0], $item_2_cannot)->{status}, 'noReservesAllowed',
493             "With item level set, rule from item must be picked (CANNOT)"
494         );
495         is(
496             CanItemBeReserved( $patrons[0], $item_1_can)->{status}, 'OK',
497             "With item level set, rule from item must be picked (CAN)"
498         );
499         t::lib::Mocks::mock_preference('item-level_itypes', 0);
500         is(
501             CanItemBeReserved( $patrons[0], $item_1_can)->{status}, 'noReservesAllowed',
502             "With biblio level set, rule from biblio must be picked (CANNOT)"
503         );
504         is(
505             CanItemBeReserved( $patrons[0], $item_2_cannot)->{status}, 'OK',
506             "With biblio level set, rule from biblio must be picked (CAN)"
507         );
508         is(
509             CanItemBeReserved( $patrons[0], $item_3_cannot)->{status}, 'noReservesAllowed',
510             "When no holds allowed and no holds per record allowed should return noReservesAllowed"
511         );
512     };
513
514     subtest 'tooManyHoldsForThisRecord + tooManyReserves + itemAlreadyOnHold' => sub {
515         plan tests => 7;
516
517         my $biblionumber_1 = $builder->build_sample_biblio({ itemtype => $itemtype_can })->biblionumber;
518         my $item_11 = $builder->build_sample_item({ homebranch => $branch_1, holdingbranch => $branch_1, itype => $itemtype_can, biblionumber => $biblionumber_1 });
519         my $item_12 = $builder->build_sample_item({ homebranch => $branch_1, holdingbranch => $branch_1, itype => $itemtype_can, biblionumber => $biblionumber_1 });
520         my $biblionumber_2 = $builder->build_sample_biblio({ itemtype => $itemtype_can })->biblionumber;
521         my $item_21 = $builder->build_sample_item({ homebranch => $branch_1, holdingbranch => $branch_1, itype => $itemtype_can, biblionumber => $biblionumber_2 });
522         my $item_22 = $builder->build_sample_item({ homebranch => $branch_1, holdingbranch => $branch_1, itype => $itemtype_can, biblionumber => $biblionumber_2 });
523
524         Koha::Holds->search({borrowernumber => $borrowernumbers[0]})->delete;
525
526         # Biblio-level hold
527         AddReserve({
528             branch => $branch_1,
529             borrowernumber => $borrowernumbers[0],
530             biblionumber => $biblionumber_1,
531         });
532         for my $item_level ( 0..1 ) {
533             t::lib::Mocks::mock_preference('item-level_itypes', $item_level);
534             is(
535                 # FIXME This is not really correct, but CanItemBeReserved does not check if biblio-level holds already exist
536                 CanItemBeReserved( $patrons[0], $item_11)->{status}, 'OK',
537                 "A biblio-level hold already exists - another hold can be placed on a specific item item"
538             );
539         }
540
541         Koha::Holds->search({borrowernumber => $borrowernumbers[0]})->delete;
542         # Item-level hold
543         AddReserve({
544             branch => $branch_1,
545             borrowernumber => $borrowernumbers[0],
546             biblionumber => $biblionumber_1,
547             itemnumber => $item_11->itemnumber,
548         });
549
550         $dbh->do('DELETE FROM circulation_rules');
551         Koha::CirculationRules->set_rules(
552             {
553                 categorycode => undef,
554                 branchcode   => undef,
555                 itemtype     => undef,
556                 rules        => {
557                     reservesallowed  => 5,
558                     holds_per_record => 1,
559                 }
560             }
561         );
562         is(
563             CanItemBeReserved( $patrons[0], $item_12)->{status}, 'tooManyHoldsForThisRecord',
564             "A item-level hold already exists and holds_per_record=1, another hold cannot be placed on this record"
565         );
566         Koha::CirculationRules->set_rules(
567             {
568                 categorycode => undef,
569                 branchcode   => undef,
570                 itemtype     => undef,
571                 rules        => {
572                     reservesallowed  => 1,
573                     holds_per_record => 1,
574                 }
575             }
576         );
577         is(
578             CanItemBeReserved( $patrons[0], $item_12)->{status}, 'tooManyHoldsForThisRecord',
579             "A item-level hold already exists and holds_per_record=1 - tooManyHoldsForThisRecord has priority over tooManyReserves"
580         );
581         Koha::CirculationRules->set_rules(
582             {
583                 categorycode => undef,
584                 branchcode   => undef,
585                 itemtype     => undef,
586                 rules        => {
587                     reservesallowed  => 5,
588                     holds_per_record => 2,
589                 }
590             }
591         );
592         is(
593             CanItemBeReserved( $patrons[0], $item_12)->{status}, 'OK',
594             "A item-level hold already exists but holds_per_record=2- another item-level hold can be placed on this record"
595         );
596
597         AddReserve({
598             branch => $branch_1,
599             borrowernumber => $borrowernumbers[0],
600             biblionumber => $biblionumber_2,
601             itemnumber => $item_21->itemnumber
602         });
603         Koha::CirculationRules->set_rules(
604             {
605                 categorycode => undef,
606                 branchcode   => undef,
607                 itemtype     => undef,
608                 rules        => {
609                     reservesallowed  => 2,
610                     holds_per_record => 2,
611                 }
612             }
613         );
614         is(
615             CanItemBeReserved( $patrons[0], $item_21)->{status}, 'itemAlreadyOnHold',
616             "A item-level holds already exists on this item, itemAlreadyOnHold should be raised"
617         );
618         is(
619             CanItemBeReserved( $patrons[0], $item_22)->{status}, 'tooManyReserves',
620             "This patron has already placed reservesallowed holds, tooManyReserves should be raised"
621         );
622     };
623 };
624
625
626 # Test branch item rules
627
628 $dbh->do('DELETE FROM circulation_rules');
629 Koha::CirculationRules->set_rules(
630     {
631         categorycode => undef,
632         branchcode   => undef,
633         itemtype     => undef,
634         rules        => {
635             reservesallowed  => 25,
636             holds_per_record => 99,
637         }
638     }
639 );
640 Koha::CirculationRules->set_rules(
641     {
642         branchcode => $branch_1,
643         itemtype   => 'CANNOT',
644         rules => {
645             holdallowed => 'not_allowed',
646             returnbranch => 'homebranch',
647         }
648     }
649 );
650 Koha::CirculationRules->set_rules(
651     {
652         branchcode => $branch_1,
653         itemtype   => 'CAN',
654         rules => {
655             holdallowed => 'from_home_library',
656             returnbranch => 'homebranch',
657         }
658     }
659 );
660 $biblio = $builder->build_sample_biblio({ itemtype => 'CANNOT' });
661 my $branch_rule_item = $builder->build_sample_item({ library => $branch_1, itype => 'CANNOT', biblionumber => $biblio->biblionumber});
662 is(CanItemBeReserved($patrons[0], $branch_rule_item)->{status}, 'notReservable',
663     "CanItemBeReserved should return 'notReservable'");
664
665 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
666 $branch_rule_item = $builder->build_sample_item({ library => $branch_2, itype => 'CAN', biblionumber => $biblio->biblionumber});
667 is(CanItemBeReserved($patrons[0], $branch_rule_item)->{status},
668     'cannotReserveFromOtherBranches',
669     "CanItemBeReserved should use PatronLibrary rule when ReservesControlBranch set to 'PatronLibrary'");
670 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
671 is(CanItemBeReserved($patrons[0], $branch_rule_item)->{status},
672     'OK',
673     "CanItemBeReserved should use item home library rule when ReservesControlBranch set to 'ItemsHomeLibrary'");
674
675 $branch_rule_item = $builder->build_sample_item({ library => $branch_1, itype => 'CAN', biblionumber => $biblio->biblionumber});
676 is(CanItemBeReserved($patrons[0], $branch_rule_item)->{status}, 'OK',
677     "CanItemBeReserved should return 'OK'");
678
679 # Bug 12632
680 t::lib::Mocks::mock_preference( 'item-level_itypes',     1 );
681 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
682
683 $dbh->do('DELETE FROM reserves');
684 $dbh->do('DELETE FROM issues');
685 $dbh->do('DELETE FROM items');
686 $dbh->do('DELETE FROM biblio');
687
688 $biblio = $builder->build_sample_biblio({ itemtype => 'ONLY1' });
689 my $limit_count_item = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber});
690
691 Koha::CirculationRules->set_rules(
692     {
693         categorycode => undef,
694         branchcode   => undef,
695         itemtype     => 'ONLY1',
696         rules        => {
697             reservesallowed  => 1,
698             holds_per_record => 99,
699         }
700     }
701 );
702 is( CanItemBeReserved( $patrons[0], $limit_count_item )->{status},
703     'OK', 'Patron can reserve item with hold limit of 1, no holds placed' );
704
705 my $res_id = AddReserve(
706     {
707         branchcode     => $branch_1,
708         borrowernumber => $borrowernumbers[0],
709         biblionumber   => $biblio->biblionumber,
710         priority       => 1,
711     }
712 );
713
714 is( CanItemBeReserved( $patrons[0], $limit_count_item )->{status},
715     'tooManyReserves', 'Patron cannot reserve item with hold limit of 1, 1 bib level hold placed' );
716 is( CanItemBeReserved( $patrons[0], $limit_count_item, undef, { ignore_hold_counts => 1 } )->{status},
717     'OK', 'Patron can reserve item if checking policy but not counts' );
718
719     #results should be the same for both ReservesControlBranch settings
720 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
721 is( CanItemBeReserved( $patrons[0], $limit_count_item )->{status},
722     'tooManyReserves', 'Patron cannot reserve item with hold limit of 1, 1 bib level hold placed' );
723 #reset for further tests
724 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
725
726 subtest 'Test max_holds per library/patron category' => sub {
727     plan tests => 6;
728
729     $dbh->do('DELETE FROM reserves');
730
731     $biblio = $builder->build_sample_biblio;
732     my $max_holds_item = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber});
733     Koha::CirculationRules->set_rules(
734         {
735             categorycode => undef,
736             branchcode   => undef,
737             itemtype     => $biblio->itemtype,
738             rules        => {
739                 reservesallowed  => 99,
740                 holds_per_record => 99,
741             }
742         }
743     );
744
745     for ( 1 .. 3 ) {
746         AddReserve(
747             {
748                 branchcode     => $branch_1,
749                 borrowernumber => $borrowernumbers[0],
750                 biblionumber   => $biblio->biblionumber,
751                 priority       => 1,
752             }
753         );
754     }
755
756     my $count =
757       Koha::Holds->search( { borrowernumber => $borrowernumbers[0] } )->count();
758     is( $count, 3, 'Patron now has 3 holds' );
759
760     my $ret = CanItemBeReserved( $patrons[0], $max_holds_item );
761     is( $ret->{status}, 'OK', 'Patron can place hold with no borrower circ rules' );
762
763     my $rule_all = Koha::CirculationRules->set_rule(
764         {
765             categorycode => $category->{categorycode},
766             branchcode   => undef,
767             rule_name    => 'max_holds',
768             rule_value   => 3,
769         }
770     );
771
772     my $rule_branch = Koha::CirculationRules->set_rule(
773         {
774             branchcode   => $branch_1,
775             categorycode => $category->{categorycode},
776             rule_name    => 'max_holds',
777             rule_value   => 5,
778         }
779     );
780
781     $ret = CanItemBeReserved( $patrons[0], $max_holds_item );
782     is( $ret->{status}, 'OK', 'Patron can place hold with branch/category rule of 5, category rule of 3' );
783
784     $rule_branch->delete();
785
786     $ret = CanItemBeReserved( $patrons[0], $max_holds_item );
787     is( $ret->{status}, 'tooManyReserves', 'Patron cannot place hold with only a category rule of 3' );
788
789     $rule_all->delete();
790     $rule_branch->rule_value(3);
791     $rule_branch->store();
792
793     $ret = CanItemBeReserved( $patrons[0], $max_holds_item );
794     is( $ret->{status}, 'tooManyReserves', 'Patron cannot place hold with only a branch/category rule of 3' );
795
796     $rule_branch->rule_value(5);
797     $rule_branch->update();
798     $rule_branch->rule_value(5);
799     $rule_branch->store();
800
801     $ret = CanItemBeReserved( $patrons[0], $max_holds_item );
802     is( $ret->{status}, 'OK', 'Patron can place hold with branch/category rule of 5, category rule of 5' );
803 };
804
805 subtest 'Pickup location availability tests' => sub {
806     plan tests => 4;
807
808     $biblio = $builder->build_sample_biblio({ itemtype => 'ONLY1' });
809     my $pickup_item = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber});
810     #Add a default rule to allow some holds
811
812     Koha::CirculationRules->set_rules(
813         {
814             branchcode   => undef,
815             categorycode => undef,
816             itemtype     => undef,
817             rules        => {
818                 reservesallowed  => 25,
819                 holds_per_record => 99,
820             }
821         }
822     );
823     my $branch_to = $builder->build({ source => 'Branch' })->{ branchcode };
824     my $library = Koha::Libraries->find($branch_to);
825     $library->pickup_location('1')->store;
826     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
827
828     t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
829     t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
830
831     $library->pickup_location('1')->store;
832     is(CanItemBeReserved($patron, $pickup_item, $branch_to)->{status},
833        'OK', 'Library is a pickup location');
834
835     my $limit = Koha::Item::Transfer::Limit->new({
836         fromBranch => $pickup_item->holdingbranch,
837         toBranch => $branch_to,
838         itemtype => $pickup_item->effective_itemtype,
839     })->store;
840     is(CanItemBeReserved($patron, $pickup_item, $branch_to)->{status},
841        'cannotBeTransferred', 'Item cannot be transferred');
842     $limit->delete;
843
844     $library->pickup_location('0')->store;
845     is(CanItemBeReserved($patron, $pickup_item, $branch_to)->{status},
846        'libraryNotPickupLocation', 'Library is not a pickup location');
847     is(CanItemBeReserved($patron, $pickup_item, 'nonexistent')->{status},
848        'libraryNotFound', 'Cannot set unknown library as pickup location');
849 };
850
851 $schema->storage->txn_rollback;
852
853 subtest 'CanItemBeReserved / holds_per_day tests' => sub {
854
855     plan tests => 10;
856
857     $schema->storage->txn_begin;
858
859     my $itemtype = $builder->build_object( { class => 'Koha::ItemTypes' } );
860     my $library  = $builder->build_object( { class => 'Koha::Libraries' } );
861     my $patron   = $builder->build_object( { class => 'Koha::Patrons' } );
862
863     # Create 3 biblios with items
864     my $biblio_1 = $builder->build_sample_biblio({ itemtype => $itemtype->itemtype });
865     my $item_1 = $builder->build_sample_item({ library => $library->branchcode, biblionumber => $biblio_1->biblionumber});
866     my $biblio_2 = $builder->build_sample_biblio({ itemtype => $itemtype->itemtype });
867     my $item_2 = $builder->build_sample_item({ library => $library->branchcode, biblionumber => $biblio_2->biblionumber});
868     my $biblio_3 = $builder->build_sample_biblio({ itemtype => $itemtype->itemtype });
869     my $item_3 = $builder->build_sample_item({ library => $library->branchcode, biblionumber => $biblio_3->biblionumber});
870
871     Koha::CirculationRules->set_rules(
872         {
873             categorycode => '*',
874             branchcode   => '*',
875             itemtype     => $itemtype->itemtype,
876             rules        => {
877                 reservesallowed  => 1,
878                 holds_per_record => 99,
879                 holds_per_day    => 2
880             }
881         }
882     );
883
884     is_deeply(
885         CanItemBeReserved( $patron, $item_1 ),
886         { status => 'OK' },
887         'Patron can reserve item with hold limit of 1, no holds placed'
888     );
889
890     AddReserve(
891         {
892             branchcode     => $library->branchcode,
893             borrowernumber => $patron->borrowernumber,
894             biblionumber   => $biblio_1->biblionumber,
895             priority       => 1,
896         }
897     );
898
899     is_deeply(
900         CanItemBeReserved( $patron, $item_1 ),
901         { status => 'tooManyReserves', limit => 1 },
902         'Patron cannot reserve item with hold limit of 1, 1 bib level hold placed'
903     );
904
905     # Raise reservesallowed to avoid tooManyReserves from it
906     Koha::CirculationRules->set_rule(
907         {
908
909             categorycode => '*',
910             branchcode   => '*',
911             itemtype     => $itemtype->itemtype,
912             rule_name  => 'reservesallowed',
913             rule_value => 3,
914         }
915     );
916
917     is_deeply(
918         CanItemBeReserved( $patron, $item_2 ),
919         { status => 'OK' },
920         'Patron can reserve item with 2 reserves daily cap'
921     );
922
923     # Add a second reserve
924     my $res_id = AddReserve(
925         {
926             branchcode     => $library->branchcode,
927             borrowernumber => $patron->borrowernumber,
928             biblionumber   => $biblio_2->biblionumber,
929             priority       => 1,
930         }
931     );
932     is_deeply(
933         CanItemBeReserved( $patron, $item_2 ),
934         { status => 'tooManyReservesToday', limit => 2 },
935         'Patron cannot a third item with 2 reserves daily cap'
936     );
937
938     # Update last hold so reservedate is in the past, so 2 holds, but different day
939     $hold = Koha::Holds->find($res_id);
940     my $yesterday = dt_from_string() - DateTime::Duration->new( days => 1 );
941     $hold->reservedate($yesterday)->store;
942
943     is_deeply(
944         CanItemBeReserved( $patron, $item_2 ),
945         { status => 'OK' },
946         'Patron can reserve item with 2 bib level hold placed on different days, 2 reserves daily cap'
947     );
948
949     # Set holds_per_day to 0
950     Koha::CirculationRules->set_rule(
951         {
952
953             categorycode => '*',
954             branchcode   => '*',
955             itemtype     => $itemtype->itemtype,
956             rule_name  => 'holds_per_day',
957             rule_value => 0,
958         }
959     );
960
961
962     # Delete existing holds
963     Koha::Holds->search->delete;
964     is_deeply(
965         CanItemBeReserved( $patron, $item_2 ),
966         { status => 'tooManyReservesToday', limit => 0 },
967         'Patron cannot reserve if holds_per_day is 0 (i.e. 0 is 0)'
968     );
969
970     Koha::CirculationRules->set_rule(
971         {
972
973             categorycode => '*',
974             branchcode   => '*',
975             itemtype     => $itemtype->itemtype,
976             rule_name  => 'holds_per_day',
977             rule_value => undef,
978         }
979     );
980
981     Koha::Holds->search->delete;
982     is_deeply(
983         CanItemBeReserved( $patron, $item_2 ),
984         { status => 'OK' },
985         'Patron can reserve if holds_per_day is undef (i.e. undef is unlimited daily cap)'
986     );
987     AddReserve(
988         {
989             branchcode     => $library->branchcode,
990             borrowernumber => $patron->borrowernumber,
991             biblionumber   => $biblio_1->biblionumber,
992             priority       => 1,
993         }
994     );
995     AddReserve(
996         {
997             branchcode     => $library->branchcode,
998             borrowernumber => $patron->borrowernumber,
999             biblionumber   => $biblio_2->biblionumber,
1000             priority       => 1,
1001         }
1002     );
1003
1004     is_deeply(
1005         CanItemBeReserved( $patron, $item_3 ),
1006         { status => 'OK' },
1007         'Patron can reserve if holds_per_day is undef (i.e. undef is unlimited daily cap)'
1008     );
1009     AddReserve(
1010         {
1011             branchcode     => $library->branchcode,
1012             borrowernumber => $patron->borrowernumber,
1013             biblionumber   => $biblio_3->biblionumber,
1014             priority       => 1,
1015         }
1016     );
1017     is_deeply(
1018         CanItemBeReserved( $patron, $item_3 ),
1019         { status => 'tooManyReserves', limit => 3 },
1020         'Unlimited daily holds, but reached reservesallowed'
1021     );
1022     #results should be the same for both ReservesControlBranch settings
1023     t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary');
1024     is_deeply(
1025         CanItemBeReserved( $patron, $item_3 ),
1026         { status => 'tooManyReserves', limit => 3 },
1027         'Unlimited daily holds, but reached reservesallowed'
1028     );
1029
1030     $schema->storage->txn_rollback;
1031 };
1032
1033 subtest 'CanItemBeReserved / branch_not_in_hold_group' => sub {
1034     plan tests => 9;
1035
1036     $schema->storage->txn_begin;
1037
1038     Koha::CirculationRules->set_rule(
1039         {
1040             branchcode   => undef,
1041             categorycode => undef,
1042             itemtype     => undef,
1043             rule_name    => 'reservesallowed',
1044             rule_value   => 25,
1045         }
1046     );
1047
1048     # Create item types
1049     my $itemtype1 = $builder->build_object( { class => 'Koha::ItemTypes' } );
1050     my $itemtype2 = $builder->build_object( { class => 'Koha::ItemTypes' } );
1051
1052     # Create libraries
1053     my $library1  = $builder->build_object( { class => 'Koha::Libraries' } );
1054     my $library2  = $builder->build_object( { class => 'Koha::Libraries' } );
1055     my $library3  = $builder->build_object( { class => 'Koha::Libraries' } );
1056
1057     # Create library groups hierarchy
1058     my $rootgroup  = $builder->build_object( { class => 'Koha::Library::Groups', value => {ft_local_hold_group => 1} } );
1059     my $group1  = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library1->branchcode}} );
1060     my $group2  = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library2->branchcode} } );
1061
1062     # Create 2 patrons
1063     my $patron1   = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library1->branchcode} } );
1064     my $patron3   = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library3->branchcode} } );
1065
1066     # Create 3 biblios with items
1067     my $biblio_1 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
1068     my $item_1   = $builder->build_sample_item(
1069         {
1070             biblionumber => $biblio_1->biblionumber,
1071             library      => $library1->branchcode
1072         }
1073     );
1074     my $biblio_2 = $builder->build_sample_biblio({ itemtype => $itemtype2->itemtype });
1075     my $item_2   = $builder->build_sample_item(
1076         {
1077             biblionumber => $biblio_2->biblionumber,
1078             library      => $library2->branchcode
1079         }
1080     );
1081     my $itemnumber_2 = $item_2->itemnumber;
1082     my $biblio_3 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
1083     my $item_3   = $builder->build_sample_item(
1084         {
1085             biblionumber => $biblio_3->biblionumber,
1086             library      => $library1->branchcode
1087         }
1088     );
1089
1090     # Test 1: Patron 3 can place hold
1091     is_deeply(
1092         CanItemBeReserved( $patron3, $item_2 ),
1093         { status => 'OK' },
1094         'Patron can place hold if no circ_rules where defined'
1095     );
1096
1097     # Insert default circ rule of holds allowed only from local hold group for all libraries
1098     Koha::CirculationRules->set_rules(
1099         {
1100             branchcode => undef,
1101             itemtype   => undef,
1102             rules => {
1103                 holdallowed => 'from_local_hold_group',
1104                 hold_fulfillment_policy => 'any',
1105                 returnbranch => 'any'
1106             }
1107         }
1108     );
1109
1110     # Test 2: Patron 1 can place hold
1111     is_deeply(
1112         CanItemBeReserved( $patron1, $item_2 ),
1113         { status => 'OK' },
1114         'Patron can place hold because patron\'s home library is part of hold group'
1115     );
1116
1117     # Test 3: Patron 3 cannot place hold
1118     is_deeply(
1119         CanItemBeReserved( $patron3, $item_2 ),
1120         { status => 'branchNotInHoldGroup' },
1121         'Patron cannot place hold because patron\'s home library is not part of hold group'
1122     );
1123
1124     # Insert default circ rule to "any" for library 2
1125     Koha::CirculationRules->set_rules(
1126         {
1127             branchcode => $library2->branchcode,
1128             itemtype   => undef,
1129             rules => {
1130                 holdallowed => 'from_any_library',
1131                 hold_fulfillment_policy => 'any',
1132                 returnbranch => 'any'
1133             }
1134         }
1135     );
1136
1137     # Test 4: Patron 3 can place hold
1138     is_deeply(
1139         CanItemBeReserved( $patron3, $item_2 ),
1140         { status => 'OK' },
1141         'Patron can place hold if holdallowed is set to "any" for library 2'
1142     );
1143
1144     # Update default circ rule to "hold group" for library 2
1145     Koha::CirculationRules->set_rules(
1146         {
1147             branchcode => $library2->branchcode,
1148             itemtype   => undef,
1149             rules => {
1150                 holdallowed => 'from_local_hold_group',
1151                 hold_fulfillment_policy => 'any',
1152                 returnbranch => 'any'
1153             }
1154         }
1155     );
1156
1157     # Test 5: Patron 3 cannot place hold
1158     is_deeply(
1159         CanItemBeReserved( $patron3, $item_2 ),
1160         { status => 'branchNotInHoldGroup' },
1161         'Patron cannot place hold if holdallowed is set to "hold group" for library 2'
1162     );
1163
1164     # Insert default item rule to "any" for itemtype 2
1165     Koha::CirculationRules->set_rules(
1166         {
1167             branchcode => $library2->branchcode,
1168             itemtype   => $itemtype2->itemtype,
1169             rules => {
1170                 holdallowed => 'from_any_library',
1171                 hold_fulfillment_policy => 'any',
1172                 returnbranch => 'any'
1173             }
1174         }
1175     );
1176
1177     # Test 6: Patron 3 can place hold
1178     is_deeply(
1179         CanItemBeReserved( $patron3, $item_2 ),
1180         { status => 'OK' },
1181         'Patron can place hold if holdallowed is set to "any" for itemtype 2'
1182     );
1183
1184     # Update default item rule to "hold group" for itemtype 2
1185     Koha::CirculationRules->set_rules(
1186         {
1187             branchcode => $library2->branchcode,
1188             itemtype   => $itemtype2->itemtype,
1189             rules => {
1190                 holdallowed => 'from_local_hold_group',
1191                 hold_fulfillment_policy => 'any',
1192                 returnbranch => 'any'
1193             }
1194         }
1195     );
1196
1197     # Test 7: Patron 3 cannot place hold
1198     is_deeply(
1199         CanItemBeReserved( $patron3, $item_2 ),
1200         { status => 'branchNotInHoldGroup' },
1201         'Patron cannot place hold if holdallowed is set to "hold group" for itemtype 2'
1202     );
1203
1204     # Insert branch item rule to "any" for itemtype 2 and library 2
1205     Koha::CirculationRules->set_rules(
1206         {
1207             branchcode => $library2->branchcode,
1208             itemtype   => $itemtype2->itemtype,
1209             rules => {
1210                 holdallowed => 'from_any_library',
1211                 hold_fulfillment_policy => 'any',
1212                 returnbranch => 'any'
1213             }
1214         }
1215     );
1216
1217     # Test 8: Patron 3 can place hold
1218     is_deeply(
1219         CanItemBeReserved( $patron3, $item_2 ),
1220         { status => 'OK' },
1221         'Patron can place hold if holdallowed is set to "any" for itemtype 2 and library 2'
1222     );
1223
1224     # Update branch item rule to "hold group" for itemtype 2 and library 2
1225     Koha::CirculationRules->set_rules(
1226         {
1227             branchcode => $library2->branchcode,
1228             itemtype   => $itemtype2->itemtype,
1229             rules => {
1230                 holdallowed => 'from_local_hold_group',
1231                 hold_fulfillment_policy => 'any',
1232                 returnbranch => 'any'
1233             }
1234         }
1235     );
1236
1237     # Test 9: Patron 3 cannot place hold
1238     is_deeply(
1239         CanItemBeReserved( $patron3, $item_2 ),
1240         { status => 'branchNotInHoldGroup' },
1241         'Patron cannot place hold if holdallowed is set to "hold group" for itemtype 2 and library 2'
1242     );
1243
1244     $schema->storage->txn_rollback;
1245
1246 };
1247
1248 subtest 'CanItemBeReserved / pickup_not_in_hold_group' => sub {
1249     plan tests => 9;
1250
1251     $schema->storage->txn_begin;
1252     Koha::CirculationRules->set_rule(
1253         {
1254             branchcode   => undef,
1255             categorycode => undef,
1256             itemtype     => undef,
1257             rule_name    => 'reservesallowed',
1258             rule_value   => 25,
1259         }
1260     );
1261
1262     # Create item types
1263     my $itemtype1 = $builder->build_object( { class => 'Koha::ItemTypes' } );
1264     my $itemtype2 = $builder->build_object( { class => 'Koha::ItemTypes' } );
1265
1266     # Create libraries
1267     my $library1  = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } );
1268     my $library2  = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } );
1269     my $library3  = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } );
1270
1271     # Create library groups hierarchy
1272     my $rootgroup  = $builder->build_object( { class => 'Koha::Library::Groups', value => {ft_local_hold_group => 1} } );
1273     my $group1  = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library1->branchcode}} );
1274     my $group2  = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library2->branchcode} } );
1275
1276     # Create 2 patrons
1277     my $patron1   = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library1->branchcode} } );
1278     my $patron3   = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library3->branchcode} } );
1279
1280     # Create 3 biblios with items
1281     my $biblio_1 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
1282     my $item_1   = $builder->build_sample_item(
1283         {
1284             biblionumber => $biblio_1->biblionumber,
1285             library      => $library1->branchcode
1286         }
1287     );
1288     my $biblio_2 = $builder->build_sample_biblio({ itemtype => $itemtype2->itemtype });
1289     my $item_2   = $builder->build_sample_item(
1290         {
1291             biblionumber => $biblio_2->biblionumber,
1292             library      => $library2->branchcode
1293         }
1294     );
1295     my $itemnumber_2 = $item_2->itemnumber;
1296     my $biblio_3 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
1297     my $item_3   = $builder->build_sample_item(
1298         {
1299             biblionumber => $biblio_3->biblionumber,
1300             library      => $library1->branchcode
1301         }
1302     );
1303
1304     # Test 1: Patron 3 can place hold
1305     is_deeply(
1306         CanItemBeReserved( $patron3, $item_2, $library3->branchcode ),
1307         { status => 'OK' },
1308         'Patron can place hold if no circ_rules where defined'
1309     );
1310
1311     # Insert default circ rule of holds allowed only from local hold group for all libraries
1312     Koha::CirculationRules->set_rules(
1313         {
1314             branchcode => undef,
1315             itemtype   => undef,
1316             rules => {
1317                 holdallowed => 'from_any_library',
1318                 hold_fulfillment_policy => 'holdgroup',
1319                 returnbranch => 'any'
1320             }
1321         }
1322     );
1323
1324     # Test 2: Patron 1 can place hold
1325     is_deeply(
1326         CanItemBeReserved( $patron3, $item_2, $library1->branchcode ),
1327         { status => 'OK' },
1328         'Patron can place hold because pickup location is part of hold group'
1329     );
1330
1331     # Test 3: Patron 3 cannot place hold
1332     is_deeply(
1333         CanItemBeReserved( $patron3, $item_2, $library3->branchcode ),
1334         { status => 'pickupNotInHoldGroup' },
1335         'Patron cannot place hold because pickup location is not part of hold group'
1336     );
1337
1338     # Insert default circ rule to "any" for library 2
1339     Koha::CirculationRules->set_rules(
1340         {
1341             branchcode => $library2->branchcode,
1342             itemtype   => undef,
1343             rules => {
1344                 holdallowed => 'from_any_library',
1345                 hold_fulfillment_policy => 'any',
1346                 returnbranch => 'any'
1347             }
1348         }
1349     );
1350
1351     # Test 4: Patron 3 can place hold
1352     is_deeply(
1353         CanItemBeReserved( $patron3, $item_2, $library3->branchcode ),
1354         { status => 'OK' },
1355         'Patron can place hold if default_branch_circ_rules is set to "any" for library 2'
1356     );
1357
1358     # Update default circ rule to "hold group" for library 2
1359     Koha::CirculationRules->set_rules(
1360         {
1361             branchcode => $library2->branchcode,
1362             itemtype   => undef,
1363             rules => {
1364                 holdallowed => 'from_any_library',
1365                 hold_fulfillment_policy => 'holdgroup',
1366                 returnbranch => 'any'
1367             }
1368         }
1369     );
1370
1371     # Test 5: Patron 3 cannot place hold
1372     is_deeply(
1373         CanItemBeReserved( $patron3, $item_2, $library3->branchcode ),
1374         { status => 'pickupNotInHoldGroup' },
1375         'Patron cannot place hold if hold_fulfillment_policy is set to "hold group" for library 2'
1376     );
1377
1378     # Insert default item rule to "any" for itemtype 2
1379     Koha::CirculationRules->set_rules(
1380         {
1381             branchcode => $library2->branchcode,
1382             itemtype   => $itemtype2->itemtype,
1383             rules => {
1384                 holdallowed => 'from_any_library',
1385                 hold_fulfillment_policy => 'any',
1386                 returnbranch => 'any'
1387             }
1388         }
1389     );
1390
1391     # Test 6: Patron 3 can place hold
1392     is_deeply(
1393         CanItemBeReserved( $patron3, $item_2, $library3->branchcode ),
1394         { status => 'OK' },
1395         'Patron can place hold if hold_fulfillment_policy is set to "any" for itemtype 2'
1396     );
1397
1398     # Update default item rule to "hold group" for itemtype 2
1399     Koha::CirculationRules->set_rules(
1400         {
1401             branchcode => $library2->branchcode,
1402             itemtype   => $itemtype2->itemtype,
1403             rules => {
1404                 holdallowed => 'from_any_library',
1405                 hold_fulfillment_policy => 'holdgroup',
1406                 returnbranch => 'any'
1407             }
1408         }
1409     );
1410
1411     # Test 7: Patron 3 cannot place hold
1412     is_deeply(
1413         CanItemBeReserved( $patron3, $item_2, $library3->branchcode ),
1414         { status => 'pickupNotInHoldGroup' },
1415         'Patron cannot place hold if hold_fulfillment_policy is set to "hold group" for itemtype 2'
1416     );
1417
1418     # Insert branch item rule to "any" for itemtype 2 and library 2
1419     Koha::CirculationRules->set_rules(
1420         {
1421             branchcode => $library2->branchcode,
1422             itemtype   => $itemtype2->itemtype,
1423             rules => {
1424                 holdallowed => 'from_any_library',
1425                 hold_fulfillment_policy => 'any',
1426                 returnbranch => 'any'
1427             }
1428         }
1429     );
1430
1431     # Test 8: Patron 3 can place hold
1432     is_deeply(
1433         CanItemBeReserved( $patron3, $item_2, $library3->branchcode ),
1434         { status => 'OK' },
1435         'Patron can place hold if hold_fulfillment_policy is set to "any" for itemtype 2 and library 2'
1436     );
1437
1438     # Update branch item rule to "hold group" for itemtype 2 and library 2
1439     Koha::CirculationRules->set_rules(
1440         {
1441             branchcode => $library2->branchcode,
1442             itemtype   => $itemtype2->itemtype,
1443             rules => {
1444                 holdallowed => 'from_any_library',
1445                 hold_fulfillment_policy => 'holdgroup',
1446                 returnbranch => 'any'
1447             }
1448         }
1449     );
1450
1451     # Test 9: Patron 3 cannot place hold
1452     is_deeply(
1453         CanItemBeReserved( $patron3, $item_2, $library3->branchcode ),
1454         { status => 'pickupNotInHoldGroup' },
1455         'Patron cannot place hold if hold_fulfillment_policy is set to "hold group" for itemtype 2 and library 2'
1456     );
1457
1458     $schema->storage->txn_rollback;
1459 };
1460
1461 subtest 'non priority holds' => sub {
1462
1463     plan tests => 6;
1464
1465     $schema->storage->txn_begin;
1466
1467     Koha::CirculationRules->set_rules(
1468         {
1469             branchcode   => undef,
1470             categorycode => undef,
1471             itemtype     => undef,
1472             rules        => {
1473                 renewalsallowed => 5,
1474                 reservesallowed => 5,
1475             }
1476         }
1477     );
1478
1479     my $item = $builder->build_sample_item;
1480
1481     my $patron1 = $builder->build_object(
1482         {
1483             class => 'Koha::Patrons',
1484             value => { branchcode => $item->homebranch }
1485         }
1486     );
1487     my $patron2 = $builder->build_object(
1488         {
1489             class => 'Koha::Patrons',
1490             value => { branchcode => $item->homebranch }
1491         }
1492     );
1493
1494     Koha::Checkout->new(
1495         {
1496             borrowernumber => $patron1->borrowernumber,
1497             itemnumber     => $item->itemnumber,
1498             branchcode     => $item->homebranch
1499         }
1500     )->store;
1501
1502     my $hid = AddReserve(
1503         {
1504             branchcode     => $item->homebranch,
1505             borrowernumber => $patron2->borrowernumber,
1506             biblionumber   => $item->biblionumber,
1507             priority       => 1,
1508             itemnumber     => $item->itemnumber,
1509         }
1510     );
1511
1512     my ( $ok, $err ) =
1513       CanBookBeRenewed( $patron1->borrowernumber, $item->itemnumber );
1514
1515     ok( !$ok, 'Cannot renew' );
1516     is( $err, 'on_reserve', 'Item is on hold' );
1517
1518     my $hold = Koha::Holds->find($hid);
1519     $hold->non_priority(1)->store;
1520
1521     ( $ok, $err ) =
1522       CanBookBeRenewed( $patron1->borrowernumber, $item->itemnumber );
1523
1524     ok( $ok, 'Can renew' );
1525     is( $err, undef, 'Item is on non priority hold' );
1526
1527     my $patron3 = $builder->build_object(
1528         {
1529             class => 'Koha::Patrons',
1530             value => { branchcode => $item->homebranch }
1531         }
1532     );
1533
1534     # Add second hold with non_priority = 0
1535     AddReserve(
1536         {
1537             branchcode     => $item->homebranch,
1538             borrowernumber => $patron3->borrowernumber,
1539             biblionumber   => $item->biblionumber,
1540             priority       => 2,
1541             itemnumber     => $item->itemnumber,
1542         }
1543     );
1544
1545     ( $ok, $err ) =
1546       CanBookBeRenewed( $patron1->borrowernumber, $item->itemnumber );
1547
1548     ok( !$ok, 'Cannot renew' );
1549     is( $err, 'on_reserve', 'Item is on hold' );
1550
1551     $schema->storage->txn_rollback;
1552 };
1553
1554 subtest 'CanItemBeReserved / recall' => sub {
1555     plan tests => 1;
1556
1557     $schema->storage->txn_begin;
1558
1559     my $itemtype1 = $builder->build_object( { class => 'Koha::ItemTypes' } );
1560     my $library1  = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } );
1561     my $patron1   = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library1->branchcode} } );
1562     my $biblio1 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
1563     my $item1   = $builder->build_sample_item(
1564         {
1565             biblionumber => $biblio1->biblionumber,
1566             library      => $library1->branchcode
1567         }
1568     );
1569     Koha::Recall->new({
1570         borrowernumber => $patron1->borrowernumber,
1571         biblionumber => $biblio1->biblionumber,
1572         branchcode => $library1->branchcode,
1573         itemnumber => $item1->itemnumber,
1574         recalldate => '2020-05-04 10:10:10',
1575         item_level_recall => 1,
1576     })->store;
1577     is( CanItemBeReserved( $patron1->borrowernumber, $item1->itemnumber, $library1->branchcode )->{status}, 'recall', "Can't reserve an item that they have already recalled" );
1578
1579     $schema->storage->txn_rollback;
1580 };
1581
1582 subtest 'CanItemBeReserved rule precedence tests' => sub {
1583
1584     plan tests => 3;
1585
1586     t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary');
1587     $schema->storage->txn_begin;
1588     my $library  = $builder->build_object( { class => 'Koha::Libraries', value => {
1589         pickup_location => 1,
1590     }});
1591     my $item = $builder->build_sample_item({
1592         homebranch    => $library->branchcode,
1593         holdingbranch => $library->branchcode
1594     });
1595     my $item2 = $builder->build_sample_item({
1596         homebranch    => $library->branchcode,
1597         holdingbranch => $library->branchcode,
1598         itype         => $item->itype
1599     });
1600     my $patron   = $builder->build_object({ class => 'Koha::Patrons', value => {
1601         branchcode => $library->branchcode
1602     }});
1603     Koha::CirculationRules->set_rules(
1604         {
1605             branchcode   => undef,
1606             categorycode => $patron->categorycode,
1607             itemtype     => $item->itype,
1608             rules        => {
1609                 reservesallowed  => 1,
1610             }
1611         }
1612     );
1613     is_deeply(
1614         CanItemBeReserved( $patron, $item, $library->branchcode ),
1615         { status => 'OK' },
1616         'Patron of specified category can place 1 hold on specified itemtype'
1617     );
1618     my $hold = $builder->build_object({ class => 'Koha::Holds', value => {
1619         biblionumber   => $item2->biblionumber,
1620         itemnumber     => $item2->itemnumber,
1621         found          => undef,
1622         priority       => 1,
1623         branchcode     => $library->branchcode,
1624         borrowernumber => $patron->borrowernumber,
1625     }});
1626     is_deeply(
1627         CanItemBeReserved( $patron, $item, $library->branchcode ),
1628         { status => 'tooManyReserves', limit => 1 },
1629         'Patron of specified category can place 1 hold on specified itemtype, cannot place a second'
1630     );
1631     Koha::CirculationRules->set_rules(
1632         {
1633             branchcode   => $library->branchcode,
1634             categorycode => undef,
1635             itemtype     => undef,
1636             rules        => {
1637                 reservesallowed  => 2,
1638             }
1639         }
1640     );
1641     is_deeply(
1642         CanItemBeReserved( $patron, $item, $library->branchcode ),
1643         { status => 'OK' },
1644         'Patron of specified category can place 1 hold on specified itemtype if library rule for all types and categories set to 2'
1645     );
1646
1647     $schema->storage->txn_rollback;
1648
1649 };
1650
1651 subtest 'ModReserve can only update expirationdate for found holds' => sub {
1652     plan tests => 2;
1653
1654     $schema->storage->txn_begin;
1655
1656     my $category = $builder->build({ source => 'Category' });
1657     my $branch = $builder->build({ source => 'Branch' })->{ branchcode };
1658     my $biblio = $builder->build_sample_biblio( { itemtype => 'DUMMY' } );
1659     my $itemnumber = $builder->build_sample_item(
1660         { library => $branch, biblionumber => $biblio->biblionumber } )
1661       ->itemnumber;
1662
1663     my $borrowernumber = Koha::Patron->new(
1664         {
1665             firstname    => 'my firstname',
1666             surname      => 'whatever surname',
1667             categorycode => $category->{categorycode},
1668             branchcode   => $branch,
1669         }
1670     )->store->borrowernumber;
1671
1672     my $reserve_id = AddReserve(
1673         {
1674             branchcode     => $branch,
1675             borrowernumber => $borrowernumber,
1676             biblionumber   => $biblio->biblionumber,
1677             priority       =>
1678               C4::Reserves::CalculatePriority( $biblio->biblionumber ),
1679             itemnumber => $itemnumber,
1680         }
1681     );
1682
1683     my $hold = Koha::Holds->find($reserve_id);
1684
1685     $hold->set( { priority => 0, found => 'W' } )->store();
1686
1687     ModReserve(
1688         {
1689             reserve_id     => $hold->id,
1690             expirationdate => '1981-06-10',
1691             priority       => 99,
1692             rank           => 0,
1693         }
1694     );
1695
1696     $hold = Koha::Holds->find($reserve_id);
1697
1698     is( $hold->expirationdate, '1981-06-10',
1699         'Found hold expiration date updated correctly' );
1700     is( $hold->priority, '0', 'Found hold priority was not updated' );
1701
1702     $schema->storage->txn_rollback;
1703
1704 };