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