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