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