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