Bug 33607: Handle default framework
[koha.git] / t / db_dependent / Reserves.t
1 #!/usr/bin/perl
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18 use Modern::Perl;
19
20 use Test::More tests => 77;
21 use Test::MockModule;
22 use Test::Warn;
23
24 use t::lib::Mocks;
25 use t::lib::TestBuilder;
26
27 use MARC::Record;
28 use DateTime::Duration;
29
30 use C4::Circulation qw( AddReturn AddIssue );
31 use C4::Items;
32 use C4::Biblio qw( GetMarcFromKohaField ModBiblio );
33 use C4::Members;
34 use C4::Reserves qw( AddReserve AlterPriority CheckReserves GetReservesControlBranch ModReserve ModReserveAffect ReserveSlip CalculatePriority CanReserveBeCanceledFromOpac CanBookBeReserved IsAvailableForItemLevelRequest MoveReserve ChargeReserveFee RevertWaitingStatus CanItemBeReserved MergeHolds );
35 use Koha::ActionLogs;
36 use Koha::Biblios;
37 use Koha::Caches;
38 use Koha::DateUtils qw( dt_from_string output_pref );
39 use Koha::Holds;
40 use Koha::Items;
41 use Koha::Libraries;
42 use Koha::Notice::Templates;
43 use Koha::Patrons;
44 use Koha::Patron::Categories;
45 use Koha::CirculationRules;
46
47 BEGIN {
48     require_ok('C4::Reserves');
49 }
50
51 # Start transaction
52 my $database = Koha::Database->new();
53 my $schema = $database->schema();
54 $schema->storage->txn_begin();
55 my $dbh = C4::Context->dbh;
56 $dbh->do('DELETE FROM circulation_rules');
57
58 my $builder = t::lib::TestBuilder->new;
59
60 my $frameworkcode = q//;
61
62
63 t::lib::Mocks::mock_preference('ReservesNeedReturns', 1);
64
65 # Somewhat arbitrary field chosen for age restriction unit tests. Must be added to db before the framework is cached
66 $dbh->do("update marc_subfield_structure set kohafield='biblioitems.agerestriction' where tagfield='521' and tagsubfield='a' and frameworkcode=?", undef, $frameworkcode);
67 my $cache = Koha::Caches->get_instance;
68 $cache->clear_from_cache("MarcStructure-0-$frameworkcode");
69 $cache->clear_from_cache("MarcStructure-1-$frameworkcode");
70 $cache->clear_from_cache("MarcSubfieldStructure-$frameworkcode");
71
72 ## Setup Test
73 # Add branches
74 my $branch_1 = $builder->build({ source => 'Branch' })->{ branchcode };
75 my $branch_2 = $builder->build({ source => 'Branch' })->{ branchcode };
76 my $branch_3 = $builder->build({ source => 'Branch' })->{ branchcode };
77 # Add categories
78 my $category_1 = $builder->build({ source => 'Category' })->{ categorycode };
79 my $category_2 = $builder->build({ source => 'Category' })->{ categorycode };
80 # Add an item type
81 my $itemtype = $builder->build(
82     { source => 'Itemtype', value => { notforloan => undef } } )->{itemtype};
83
84 t::lib::Mocks::mock_userenv({ branchcode => $branch_1 });
85
86 my $bibnum = $builder->build_sample_biblio({frameworkcode => $frameworkcode})->biblionumber;
87
88 # Create a helper item instance for testing
89 my $item = $builder->build_sample_item({ biblionumber => $bibnum, library => $branch_1, itype => $itemtype });
90
91 my $biblio_with_no_item = $builder->build_sample_biblio;
92
93 # Modify item; setting barcode.
94 my $testbarcode = '97531';
95 $item->barcode($testbarcode)->store; # FIXME We should not hardcode a barcode! Also, what's the purpose of this?
96
97
98 # Create a borrower
99 my %data = (
100     firstname =>  'my firstname',
101     surname => 'my surname',
102     categorycode => $category_1,
103     branchcode => $branch_1,
104 );
105 Koha::Patron::Categories->find($category_1)->set({ enrolmentfee => 0})->store;
106 my $borrowernumber = Koha::Patron->new(\%data)->store->borrowernumber;
107 my $patron = Koha::Patrons->find( $borrowernumber );
108 my $borrower = $patron->unblessed;
109 my $biblionumber   = $bibnum;
110
111 my $branchcode = Koha::Libraries->search->next->branchcode;
112
113 AddReserve(
114     {
115         branchcode     => $branchcode,
116         borrowernumber => $borrowernumber,
117         biblionumber   => $biblionumber,
118         priority       => 1,
119     }
120 );
121
122 my ($status, $reserve, $all_reserves) = CheckReserves( $item );
123
124 is($status, "Reserved", "CheckReserves Test 1");
125
126 ok(exists($reserve->{reserve_id}), 'CheckReserves() include reserve_id in its response');
127
128 ($status, $reserve, $all_reserves) = CheckReserves( $item );
129 is($status, "Reserved", "CheckReserves Test 2");
130
131 my $ReservesControlBranch = C4::Context->preference('ReservesControlBranch');
132 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
133 ok(
134     'ItemHomeLib' eq GetReservesControlBranch(
135         { homebranch => 'ItemHomeLib' },
136         { branchcode => 'PatronHomeLib' }
137     ), "GetReservesControlBranch returns item home branch when set to ItemHomeLibrary"
138 );
139 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
140 ok(
141     'PatronHomeLib' eq GetReservesControlBranch(
142         { homebranch => 'ItemHomeLib' },
143         { branchcode => 'PatronHomeLib' }
144     ), "GetReservesControlBranch returns patron home branch when set to PatronLibrary"
145 );
146 t::lib::Mocks::mock_preference( 'ReservesControlBranch', $ReservesControlBranch );
147
148 ###
149 ### Regression test for bug 10272
150 ###
151 my %requesters = ();
152 $requesters{$branch_1} = Koha::Patron->new({
153     branchcode   => $branch_1,
154     categorycode => $category_2,
155     surname      => "borrower from $branch_1",
156 })->store->borrowernumber;
157 for my $i ( 2 .. 5 ) {
158     $requesters{"CPL$i"} = Koha::Patron->new({
159         branchcode   => $branch_1,
160         categorycode => $category_2,
161         surname      => "borrower $i from $branch_1",
162     })->store->borrowernumber;
163 }
164 $requesters{$branch_2} = Koha::Patron->new({
165     branchcode   => $branch_2,
166     categorycode => $category_2,
167     surname      => "borrower from $branch_2",
168 })->store->borrowernumber;
169 $requesters{$branch_3} = Koha::Patron->new({
170     branchcode   => $branch_3,
171     categorycode => $category_2,
172     surname      => "borrower from $branch_3",
173 })->store->borrowernumber;
174
175 # Configure rules so that $branch_1 allows only $branch_1 patrons
176 # to request its items, while $branch_2 will allow its items
177 # to fill holds from anywhere.
178
179 $dbh->do('DELETE FROM circulation_rules');
180 Koha::CirculationRules->set_rules(
181     {
182         branchcode   => undef,
183         categorycode => undef,
184         itemtype     => undef,
185         rules        => {
186             reservesallowed => 25,
187             holds_per_record => 1,
188         }
189     }
190 );
191
192 # CPL allows only its own patrons to request its items
193 Koha::CirculationRules->set_rules(
194     {
195         branchcode   => $branch_1,
196         itemtype     => undef,
197         rules        => {
198             holdallowed  => 'from_home_library',
199             returnbranch => 'homebranch',
200         }
201     }
202 );
203
204 # ... while FPL allows anybody to request its items
205 Koha::CirculationRules->set_rules(
206     {
207         branchcode   => $branch_2,
208         itemtype     => undef,
209         rules        => {
210             holdallowed  => 'from_any_library',
211             returnbranch => 'homebranch',
212         }
213     }
214 );
215
216 my $bibnum2 = $builder->build_sample_biblio({frameworkcode => $frameworkcode})->biblionumber;
217
218 my ($itemnum_cpl, $itemnum_fpl);
219 $itemnum_cpl = $builder->build_sample_item(
220     {
221         biblionumber => $bibnum2,
222         library      => $branch_1,
223         barcode      => 'bug10272_CPL',
224         itype        => $itemtype
225     }
226 )->itemnumber;
227 $itemnum_fpl = $builder->build_sample_item(
228     {
229         biblionumber => $bibnum2,
230         library      => $branch_2,
231         barcode      => 'bug10272_FPL',
232         itype        => $itemtype
233     }
234 )->itemnumber;
235
236 # Ensure that priorities are numbered correcly when a hold is moved to waiting
237 # (bug 11947)
238 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum2));
239 AddReserve(
240     {
241         branchcode     => $branch_3,
242         borrowernumber => $requesters{$branch_3},
243         biblionumber   => $bibnum2,
244         priority       => 1,
245     }
246 );
247 AddReserve(
248     {
249         branchcode     => $branch_2,
250         borrowernumber => $requesters{$branch_2},
251         biblionumber   => $bibnum2,
252         priority       => 2,
253     }
254 );
255 AddReserve(
256     {
257         branchcode     => $branch_1,
258         borrowernumber => $requesters{$branch_1},
259         biblionumber   => $bibnum2,
260         priority       => 3,
261     }
262 );
263 ModReserveAffect($itemnum_cpl, $requesters{$branch_3}, 0);
264
265 # Now it should have different priorities.
266 my $biblio = Koha::Biblios->find( $bibnum2 );
267 my $holds = $biblio->holds({}, { order_by => 'reserve_id' });;
268 is($holds->next->priority, 0, 'Item is correctly waiting');
269 is($holds->next->priority, 1, 'Item is correctly priority 1');
270 is($holds->next->priority, 2, 'Item is correctly priority 2');
271
272 my @reserves = Koha::Holds->search({ borrowernumber => $requesters{$branch_3} })->waiting->as_list;
273 is( @reserves, 1, 'GetWaiting got only the waiting reserve' );
274 is( $reserves[0]->borrowernumber(), $requesters{$branch_3}, 'GetWaiting got the reserve for the correct borrower' );
275
276
277 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum2));
278 AddReserve(
279     {
280         branchcode     => $branch_3,
281         borrowernumber => $requesters{$branch_3},
282         biblionumber   => $bibnum2,
283         priority       => 1,
284     }
285 );
286 AddReserve(
287     {
288         branchcode     => $branch_2,
289         borrowernumber => $requesters{$branch_2},
290         biblionumber   => $bibnum2,
291         priority       => 2,
292     }
293 );
294
295 AddReserve(
296     {
297         branchcode     => $branch_1,
298         borrowernumber => $requesters{$branch_1},
299         biblionumber   => $bibnum2,
300         priority       => 3,
301     }
302 );
303
304 # Ensure that the item's home library controls hold policy lookup
305 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
306
307 my $messages;
308 # Return the CPL item at FPL.  The hold that should be triggered is
309 # the one placed by the CPL patron, as the other two patron's hold
310 # requests cannot be filled by that item per policy.
311 (undef, $messages, undef, undef) = AddReturn('bug10272_CPL', $branch_2);
312 is( $messages->{ResFound}->{borrowernumber},
313     $requesters{$branch_1},
314     'restrictive library\'s items only fill requests by own patrons (bug 10272)');
315
316 # Return the FPL item at FPL.  The hold that should be triggered is
317 # the one placed by the RPL patron, as that patron is first in line
318 # and RPL imposes no restrictions on whose holds its items can fill.
319
320 # Ensure that the preference 'LocalHoldsPriority' is not set (Bug 15244):
321 t::lib::Mocks::mock_preference( 'LocalHoldsPriority', '' );
322
323 (undef, $messages, undef, undef) = AddReturn('bug10272_FPL', $branch_2);
324 is( $messages->{ResFound}->{borrowernumber},
325     $requesters{$branch_3},
326     'for generous library, its items fill first hold request in line (bug 10272)');
327
328 $biblio = Koha::Biblios->find( $biblionumber );
329 $holds = $biblio->holds;
330 is($holds->count, 1, "Only one reserves for this biblio");
331 $holds->next->reserve_id;
332
333 # Tests for bug 9761 (ConfirmFutureHolds): new CheckReserves lookahead parameter, and corresponding change in AddReturn
334 # Note that CheckReserve uses its lookahead parameter and does not check ConfirmFutureHolds pref (it should be passed if needed like AddReturn does)
335 # Test 9761a: Add a reserve without date, CheckReserve should return it
336 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum));
337 AddReserve(
338     {
339         branchcode     => $branch_1,
340         borrowernumber => $requesters{$branch_1},
341         biblionumber   => $bibnum,
342         priority       => 1,
343     }
344 );
345 ($status)=CheckReserves( $item );
346 is( $status, 'Reserved', 'CheckReserves returns reserve without lookahead');
347 ($status)=CheckReserves( $item, 7 );
348 is( $status, 'Reserved', 'CheckReserves also returns reserve with lookahead');
349
350 # Test 9761b: Add a reserve with future date, CheckReserve should not return it
351 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum));
352 t::lib::Mocks::mock_preference('AllowHoldDateInFuture', 1);
353 my $resdate= dt_from_string();
354 $resdate->add_duration(DateTime::Duration->new(days => 4));
355 my $reserve_id = AddReserve(
356     {
357         branchcode       => $branch_1,
358         borrowernumber   => $requesters{$branch_1},
359         biblionumber     => $bibnum,
360         priority         => 1,
361         reservation_date => $resdate,
362     }
363 );
364 ($status)=CheckReserves( $item );
365 is( $status, '', 'CheckReserves returns no future reserve without lookahead');
366
367 # Test 9761c: Add a reserve with future date, CheckReserve should return it if lookahead is high enough
368 ($status)=CheckReserves( $item, 3 );
369 is( $status, '', 'CheckReserves returns no future reserve with insufficient lookahead');
370 ($status)=CheckReserves( $item, 4 );
371 is( $status, 'Reserved', 'CheckReserves returns future reserve with sufficient lookahead');
372
373 # Test 9761d: Check ResFound message of AddReturn for future hold
374 # Note that AddReturn is in Circulation.pm, but this test really pertains to reserves; AddReturn uses the ConfirmFutureHolds pref when calling CheckReserves
375 # In this test we do not need an issued item; it is just a 'checkin'
376 t::lib::Mocks::mock_preference('ConfirmFutureHolds', 0);
377 (my $doreturn, $messages)= AddReturn($testbarcode,$branch_1);
378 is($messages->{ResFound}//'', '', 'AddReturn does not care about future reserve when ConfirmFutureHolds is off');
379 t::lib::Mocks::mock_preference('ConfirmFutureHolds', 3);
380 ($doreturn, $messages)= AddReturn($testbarcode,$branch_1);
381 is(exists $messages->{ResFound}?1:0, 0, 'AddReturn ignores future reserve beyond ConfirmFutureHolds days');
382 t::lib::Mocks::mock_preference('ConfirmFutureHolds', 7);
383 ($doreturn, $messages)= AddReturn($testbarcode,$branch_1);
384 is(exists $messages->{ResFound}?1:0, 1, 'AddReturn considers future reserve within ConfirmFutureHolds days');
385
386 my $now_holder = $builder->build_object({ class => 'Koha::Patrons', value => {
387     branchcode       => $branch_1,
388 }});
389 my $now_reserve_id = AddReserve(
390     {
391         branchcode       => $branch_1,
392         borrowernumber   => $requesters{$branch_1},
393         biblionumber     => $bibnum,
394         priority         => 2,
395         reservation_date => dt_from_string(),
396     }
397 );
398 my $which_highest;
399 ($status,$which_highest)=CheckReserves( $item, 3 );
400 is( $which_highest->{reserve_id}, $now_reserve_id, 'CheckReserves returns lower priority current reserve with insufficient lookahead');
401 ($status, $which_highest)=CheckReserves( $item, 4 );
402 is( $which_highest->{reserve_id}, $reserve_id, 'CheckReserves returns higher priority future reserve with sufficient lookahead');
403 ModReserve({ reserve_id => $now_reserve_id, rank => 'del', cancellation_reason => 'test reserve' });
404
405
406 # End of tests for bug 9761 (ConfirmFutureHolds)
407
408
409 # test marking a hold as captured
410 my $hold_notice_count = count_hold_print_messages();
411 ModReserveAffect($item->itemnumber, $requesters{$branch_1}, 0);
412 my $new_count = count_hold_print_messages();
413 is($new_count, $hold_notice_count + 1, 'patron notified when item set to waiting');
414
415 # test that duplicate notices aren't generated
416 ModReserveAffect($item->itemnumber, $requesters{$branch_1}, 0);
417 $new_count = count_hold_print_messages();
418 is($new_count, $hold_notice_count + 1, 'patron not notified a second time (bug 11445)');
419
420 # avoiding the not_same_branch error
421 t::lib::Mocks::mock_preference('IndependentBranches', 0);
422 $item = Koha::Items->find($item->itemnumber);
423 is(
424     @{$item->safe_delete->messages}[0]->message,
425     'book_reserved',
426     'item that is captured to fill a hold cannot be deleted',
427 );
428
429 my $letter = ReserveSlip( { branchcode => $branch_1, reserve_id => $reserve_id } );
430 ok(defined($letter), 'can successfully generate hold slip (bug 10949)');
431
432 # Tests for bug 9788: Does Koha::Item->current_holds return a future wait?
433 # 9788a: current_holds does not return future next available hold
434 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum));
435 t::lib::Mocks::mock_preference('ConfirmFutureHolds', 2);
436 t::lib::Mocks::mock_preference('AllowHoldDateInFuture', 1);
437 $resdate= dt_from_string();
438 $resdate->add_duration(DateTime::Duration->new(days => 2));
439 AddReserve(
440     {
441         branchcode       => $branch_1,
442         borrowernumber   => $requesters{$branch_1},
443         biblionumber     => $bibnum,
444         priority         => 1,
445         reservation_date => $resdate,
446     }
447 );
448
449 $holds = $item->current_holds;
450 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
451 my $future_holds = $holds->search({ reservedate => { '>' => $dtf->format_date( dt_from_string ) } } );
452 is( $future_holds->count, 0, 'current_holds does not return a future next available hold');
453 # 9788b: current_holds does not return future item level hold
454 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum));
455 AddReserve(
456     {
457         branchcode       => $branch_1,
458         borrowernumber   => $requesters{$branch_1},
459         biblionumber     => $bibnum,
460         priority         => 1,
461         reservation_date => $resdate,
462         itemnumber       => $item->itemnumber,
463     }
464 ); #item level hold
465 $future_holds = $holds->search({ reservedate => { '>' => $dtf->format_date( dt_from_string ) } } );
466 is( $future_holds->count, 0, 'current_holds does not return a future item level hold' );
467 # 9788c: current_holds returns future wait (confirmed future hold)
468 ModReserveAffect( $item->itemnumber,  $requesters{$branch_1} , 0); #confirm hold
469 $future_holds = $holds->search({ reservedate => { '>' => $dtf->format_date( dt_from_string ) } } );
470 is( $future_holds->count, 1, 'current_holds returns a future wait (confirmed future hold)' );
471 # End of tests for bug 9788
472
473 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum));
474 # Tests for CalculatePriority (bug 8918)
475 my $p = C4::Reserves::CalculatePriority($bibnum2);
476 is($p, 4, 'CalculatePriority should now return priority 4');
477 AddReserve(
478     {
479         branchcode     => $branch_1,
480         borrowernumber => $requesters{'CPL2'},
481         biblionumber   => $bibnum2,
482         priority       => $p,
483     }
484 );
485 $p = C4::Reserves::CalculatePriority($bibnum2);
486 is($p, 5, 'CalculatePriority should now return priority 5');
487 #some tests on bibnum
488 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum));
489 $p = C4::Reserves::CalculatePriority($bibnum);
490 is($p, 1, 'CalculatePriority should now return priority 1');
491 #add a new reserve and confirm it to waiting
492 AddReserve(
493     {
494         branchcode     => $branch_1,
495         borrowernumber => $requesters{$branch_1},
496         biblionumber   => $bibnum,
497         priority       => $p,
498         itemnumber     => $item->itemnumber,
499     }
500 );
501 $p = C4::Reserves::CalculatePriority($bibnum);
502 is($p, 2, 'CalculatePriority should now return priority 2');
503 ModReserveAffect( $item->itemnumber,  $requesters{$branch_1} , 0);
504 $p = C4::Reserves::CalculatePriority($bibnum);
505 is($p, 1, 'CalculatePriority should now return priority 1');
506 #add another biblio hold, no resdate
507 AddReserve(
508     {
509         branchcode     => $branch_1,
510         borrowernumber => $requesters{'CPL2'},
511         biblionumber   => $bibnum,
512         priority       => $p,
513     }
514 );
515 $p = C4::Reserves::CalculatePriority($bibnum);
516 is($p, 2, 'CalculatePriority should now return priority 2');
517 #add another future hold
518 t::lib::Mocks::mock_preference('AllowHoldDateInFuture', 1);
519 $resdate= dt_from_string();
520 $resdate->add_duration(DateTime::Duration->new(days => 1));
521 AddReserve(
522     {
523         branchcode     => $branch_1,
524         borrowernumber => $requesters{'CPL2'},
525         biblionumber   => $bibnum,
526         priority       => $p,
527         reservation_date => $resdate,
528     }
529 );
530 $p = C4::Reserves::CalculatePriority($bibnum);
531 is($p, 2, 'CalculatePriority should now still return priority 2');
532 #calc priority with future resdate
533 $p = C4::Reserves::CalculatePriority($bibnum, $resdate);
534 is($p, 3, 'CalculatePriority should now return priority 3');
535 # End of tests for bug 8918
536
537 # regression test for bug 12630
538 # Now there are 2 reserves on $bibnum
539 t::lib::Mocks::mock_preference('AllowHoldDateInFuture', 1);
540 my $bor_tmp_1 = $builder->build_object({ class => 'Koha::Patrons',value =>{
541     firstname =>  'my firstname tmp 1',
542     surname => 'my surname tmp 1',
543     categorycode => 'S',
544     branchcode => 'CPL',
545 }});
546 my $bor_tmp_2 = $builder->build_object({ class => 'Koha::Patrons',value =>{
547     firstname =>  'my firstname tmp 2',
548     surname => 'my surname tmp 2',
549     categorycode => 'S',
550     branchcode => 'CPL',
551 }});
552 my $borrowernumber_tmp_1 = $bor_tmp_1->borrowernumber;
553 my $borrowernumber_tmp_2 = $bor_tmp_2->borrowernumber;
554 my $date_in_future = dt_from_string();
555 $date_in_future = $date_in_future->add_duration(DateTime::Duration->new(days => 1));
556 AddReserve({
557     branchcode => 'CPL',
558     borrowernumber => $borrowernumber_tmp_1,
559     biblionumber => $bibnum,
560     priority => 3,
561     reservation_date => $date_in_future
562 });
563 AddReserve({
564     branchcode => 'CPL',
565     borrowernumber => $borrowernumber_tmp_2,
566     biblionumber => $bibnum,
567     priority => 4,
568     reservation_date => $date_in_future
569 });
570 my @r1 = Koha::Holds->search({ borrowernumber => $borrowernumber_tmp_1 })->as_list;
571 my @r2 = Koha::Holds->search({ borrowernumber => $borrowernumber_tmp_2 })->as_list;
572 is( $r1[0]->priority, 3, 'priority for hold in future should be correct');
573 is( $r2[0]->priority, 4, 'priority for hold not in future should be correct');
574 # end of tests for bug 12630
575
576 # Tests for cancel reserves by users from OPAC.
577 $dbh->do('DELETE FROM reserves', undef, ($bibnum));
578 AddReserve(
579     {
580         branchcode     => $branch_1,
581         borrowernumber => $requesters{$branch_1},
582         biblionumber   => $bibnum,
583         priority       => 1,
584     }
585 );
586 my (undef, $canres, undef) = CheckReserves( $item );
587
588 is( CanReserveBeCanceledFromOpac(), undef,
589     'CanReserveBeCanceledFromOpac should return undef if called without any parameter'
590 );
591 is(
592     CanReserveBeCanceledFromOpac( $canres->{resserve_id} ),
593     undef,
594     'CanReserveBeCanceledFromOpac should return undef if called without the reserve_id'
595 );
596 is(
597     CanReserveBeCanceledFromOpac( undef, $requesters{CPL} ),
598     undef,
599     'CanReserveBeCanceledFromOpac should return undef if called without borrowernumber'
600 );
601
602 my $cancancel = CanReserveBeCanceledFromOpac($canres->{reserve_id}, $requesters{$branch_1});
603 is($cancancel, 1, 'Can user cancel its own reserve');
604
605 $cancancel = CanReserveBeCanceledFromOpac($canres->{reserve_id}, $requesters{$branch_2});
606 is($cancancel, 0, 'Other user cant cancel reserve');
607
608 ModReserveAffect($item->itemnumber, $requesters{$branch_1}, 1);
609 $cancancel = CanReserveBeCanceledFromOpac($canres->{reserve_id}, $requesters{$branch_1});
610 is($cancancel, 0, 'Reserve in transfer status cant be canceled');
611
612 $dbh->do('DELETE FROM reserves', undef, ($bibnum));
613 is( CanReserveBeCanceledFromOpac($canres->{resserve_id}, $requesters{$branch_1}), undef,
614     'Cannot cancel a deleted hold' );
615
616 AddReserve(
617     {
618         branchcode     => $branch_1,
619         borrowernumber => $requesters{$branch_1},
620         biblionumber   => $bibnum,
621         priority       => 1,
622     }
623 );
624 (undef, $canres, undef) = CheckReserves( $item );
625
626 ModReserveAffect($item->itemnumber, $requesters{$branch_1}, 0);
627 $cancancel = CanReserveBeCanceledFromOpac($canres->{reserve_id}, $requesters{$branch_1});
628 is($cancancel, 0, 'Reserve in waiting status cant be canceled');
629
630 # End of tests for bug 12876
631
632        ####
633 ####### Testing Bug 13113 - Prevent juvenile/children from reserving ageRestricted material >>>
634        ####
635
636 t::lib::Mocks::mock_preference( 'AgeRestrictionMarker', 'FSK|PEGI|Age|K' );
637
638 #Reserving an not-agerestricted Biblio by a Borrower with no dateofbirth is tested previously.
639
640 #Set the ageRestriction for the Biblio
641 $biblio = Koha::Biblios->find($bibnum);
642 my $record = $biblio->metadata->record;
643 my ( $ageres_tagid, $ageres_subfieldid ) = GetMarcFromKohaField( "biblioitems.agerestriction" );
644 $record->append_fields(  MARC::Field->new($ageres_tagid, '', '', $ageres_subfieldid => 'PEGI 16')  );
645 C4::Biblio::ModBiblio( $record, $bibnum, $frameworkcode );
646
647 is( C4::Reserves::CanBookBeReserved($borrowernumber, $biblionumber)->{status} , 'OK', "Reserving an ageRestricted Biblio without a borrower dateofbirth succeeds" );
648
649 #Set the dateofbirth for the Borrower making them "too young".
650 $borrower->{dateofbirth} = DateTime->now->add( years => -15 );
651 Koha::Patrons->find( $borrowernumber )->set({ dateofbirth => $borrower->{dateofbirth} })->store;
652
653 is( C4::Reserves::CanBookBeReserved($borrowernumber, $biblionumber)->{status} , 'ageRestricted', "Reserving a 'PEGI 16' Biblio by a 15 year old borrower fails");
654
655 #Set the dateofbirth for the Borrower making them "too old".
656 $borrower->{dateofbirth} = DateTime->now->add( years => -30 );
657 Koha::Patrons->find( $borrowernumber )->set({ dateofbirth => $borrower->{dateofbirth} })->store;
658
659 is( C4::Reserves::CanBookBeReserved($borrowernumber, $biblionumber)->{status} , 'OK', "Reserving a 'PEGI 16' Biblio by a 30 year old borrower succeeds");
660
661 is( C4::Reserves::CanBookBeReserved($borrowernumber, $biblio_with_no_item->biblionumber)->{status} , '', "Biblio with no item. Status is empty");
662        ####
663 ####### EO Bug 13113 <<<
664        ####
665
666 ok( C4::Reserves::IsAvailableForItemLevelRequest($item, $patron), "Reserving a book on item level" );
667
668 my $pickup_branch = $builder->build({ source => 'Branch' })->{ branchcode };
669 t::lib::Mocks::mock_preference( 'UseBranchTransferLimits',  '1' );
670 t::lib::Mocks::mock_preference( 'BranchTransferLimitsType', 'itemtype' );
671 my $limit = Koha::Item::Transfer::Limit->new(
672     {
673         toBranch   => $pickup_branch,
674         fromBranch => $item->holdingbranch,
675         itemtype   => $item->effective_itemtype,
676     }
677 )->store();
678 is( C4::Reserves::IsAvailableForItemLevelRequest($item, $patron, $pickup_branch), 0, "Item level request not available due to transfer limit" );
679 t::lib::Mocks::mock_preference( 'UseBranchTransferLimits',  '0' );
680
681 my $categorycode = $borrower->{categorycode};
682 my $holdingbranch = $item->{holdingbranch};
683 Koha::CirculationRules->set_rules(
684     {
685         categorycode => $categorycode,
686         itemtype     => $item->effective_itemtype,
687         branchcode   => $holdingbranch,
688         rules => {
689             onshelfholds => 1,
690         }
691     }
692 );
693
694 # tests for MoveReserve in relation to ConfirmFutureHolds (BZ 14526)
695 #   hold from A pos 1, today, no fut holds: MoveReserve should fill it
696 $dbh->do('DELETE FROM reserves', undef, ($bibnum));
697 t::lib::Mocks::mock_preference('ConfirmFutureHolds', 0);
698 t::lib::Mocks::mock_preference('AllowHoldDateInFuture', 1);
699 AddReserve(
700     {
701         branchcode     => $branch_1,
702         borrowernumber => $borrowernumber,
703         biblionumber   => $bibnum,
704         priority       => 1,
705     }
706 );
707 MoveReserve( $item->itemnumber, $borrowernumber );
708 ($status)=CheckReserves( $item );
709 is( $status, '', 'MoveReserve filled hold');
710 #   hold from A waiting, today, no fut holds: MoveReserve should fill it
711 AddReserve(
712     {
713         branchcode     => $branch_1,
714         borrowernumber => $borrowernumber,
715         biblionumber   => $bibnum,
716         priority       => 1,
717         found          => 'W',
718     }
719 );
720 MoveReserve( $item->itemnumber, $borrowernumber );
721 ($status)=CheckReserves( $item );
722 is( $status, '', 'MoveReserve filled waiting hold');
723 #   hold from A pos 1, tomorrow, no fut holds: not filled
724 $resdate= dt_from_string();
725 $resdate->add_duration(DateTime::Duration->new(days => 1));
726 AddReserve(
727     {
728         branchcode     => $branch_1,
729         borrowernumber => $borrowernumber,
730         biblionumber   => $bibnum,
731         priority       => 1,
732         reservation_date => $resdate,
733     }
734 );
735 MoveReserve( $item->itemnumber, $borrowernumber );
736 ($status)=CheckReserves( $item, 1 );
737 is( $status, 'Reserved', 'MoveReserve did not fill future hold');
738 $dbh->do('DELETE FROM reserves', undef, ($bibnum));
739 #   hold from A pos 1, tomorrow, fut holds=2: MoveReserve should fill it
740 t::lib::Mocks::mock_preference('ConfirmFutureHolds', 2);
741 AddReserve(
742     {
743         branchcode     => $branch_1,
744         borrowernumber => $borrowernumber,
745         biblionumber   => $bibnum,
746         priority       => 1,
747         reservation_date => $resdate,
748     }
749 );
750 MoveReserve( $item->itemnumber, $borrowernumber );
751 ($status)=CheckReserves( $item, undef, 2 );
752 is( $status, '', 'MoveReserve filled future hold now');
753 #   hold from A waiting, tomorrow, fut holds=2: MoveReserve should fill it
754 AddReserve(
755     {
756         branchcode     => $branch_1,
757         borrowernumber => $borrowernumber,
758         biblionumber   => $bibnum,
759         priority       => 1,
760         reservation_date => $resdate,
761     }
762 );
763 MoveReserve( $item->itemnumber, $borrowernumber );
764 ($status)=CheckReserves( $item, undef, 2 );
765 is( $status, '', 'MoveReserve filled future waiting hold now');
766 #   hold from A pos 1, today+3, fut holds=2: MoveReserve should not fill it
767 $resdate= dt_from_string();
768 $resdate->add_duration(DateTime::Duration->new(days => 3));
769 AddReserve(
770     {
771         branchcode     => $branch_1,
772         borrowernumber => $borrowernumber,
773         biblionumber   => $bibnum,
774         priority       => 1,
775         reservation_date => $resdate,
776     }
777 );
778 MoveReserve( $item->itemnumber, $borrowernumber );
779 ($status)=CheckReserves( $item, 3 );
780 is( $status, 'Reserved', 'MoveReserve did not fill future hold of 3 days');
781 $dbh->do('DELETE FROM reserves', undef, ($bibnum));
782
783 $cache->clear_from_cache("MarcStructure-0-$frameworkcode");
784 $cache->clear_from_cache("MarcStructure-1-$frameworkcode");
785 $cache->clear_from_cache("MarcSubfieldStructure-$frameworkcode");
786
787 subtest '_koha_notify_reserve() tests' => sub {
788
789     plan tests => 3;
790
791     my $branch = $builder->build_object({
792         class => 'Koha::Libraries',
793         value => {
794             branchemail => 'branch@e.mail',
795             branchreplyto => 'branch@reply.to',
796             pickup_location => 1
797         }
798     });
799     my $item = $builder->build_sample_item({
800         homebranch => $branch->branchcode,
801         holdingbranch => $branch->branchcode
802     });
803
804     my $wants_hold_and_email = {
805         wants_digest => '0',
806         transports => {
807             sms => 'HOLD',
808             email => 'HOLD',
809             },
810         letter_code => 'HOLD'
811     };
812
813     my $mp = Test::MockModule->new( 'C4::Members::Messaging' );
814
815     $mp->mock("GetMessagingPreferences",$wants_hold_and_email);
816
817     $dbh->do('DELETE FROM letter');
818
819     my $email_hold_notice = $builder->build({
820             source => 'Letter',
821             value => {
822                 message_transport_type => 'email',
823                 branchcode => '',
824                 code => 'HOLD',
825                 module => 'reserves',
826                 lang => 'default',
827             }
828         });
829
830     my $sms_hold_notice = $builder->build({
831             source => 'Letter',
832             value => {
833                 message_transport_type => 'sms',
834                 branchcode => '',
835                 code => 'HOLD',
836                 module => 'reserves',
837                 lang=>'default',
838             }
839         });
840
841     my $hold_borrower = $builder->build({
842             source => 'Borrower',
843             value => {
844                 smsalertnumber=>'5555555555',
845                 email=>'a@b.com',
846             }
847         })->{borrowernumber};
848
849     C4::Reserves::AddReserve(
850         {
851             branchcode     => $item->homebranch,
852             borrowernumber => $hold_borrower,
853             biblionumber   => $item->biblionumber,
854         }
855     );
856
857     ModReserveAffect($item->itemnumber, $hold_borrower, 0);
858     my $sms_message_address = $schema->resultset('MessageQueue')->search({
859             letter_code     => 'HOLD',
860             message_transport_type => 'sms',
861             borrowernumber => $hold_borrower,
862         })->next()->to_address();
863     is($sms_message_address, undef ,"We should not populate the sms message with the sms number, sending will do so");
864
865     my $email = $schema->resultset('MessageQueue')->search({
866             letter_code     => 'HOLD',
867             message_transport_type => 'email',
868             borrowernumber => $hold_borrower,
869         })->next();
870     my $email_to_address = $email->to_address();
871     is($email_to_address, undef ,"We should not populate the hold message with the email address, sending will do so");
872     my $email_from_address = $email->from_address();
873     is($email_from_address,'branch@e.mail',"Library's from address is used for sending");
874
875 };
876
877 subtest 'ReservesNeedReturns' => sub {
878     plan tests => 18;
879
880     my $library    = $builder->build_object( { class => 'Koha::Libraries' } );
881     my $item_info  = {
882         homebranch       => $library->branchcode,
883         holdingbranch    => $library->branchcode,
884     };
885     my $item = $builder->build_sample_item($item_info);
886     my $patron   = $builder->build_object(
887         {
888             class => 'Koha::Patrons',
889             value => { branchcode => $library->branchcode, }
890         }
891     );
892     my $patron_2   = $builder->build_object(
893         {
894             class => 'Koha::Patrons',
895             value => { branchcode => $library->branchcode, }
896         }
897     );
898
899     my $priority = 1;
900
901     t::lib::Mocks::mock_preference('ReservesNeedReturns', 1); # Test with feature disabled
902     my $hold = place_item_hold( $patron, $item, $library, $priority );
903     is( $hold->priority, $priority, 'If ReservesNeedReturns is 1, priority must not have been set to changed' );
904     is( $hold->found, undef, 'If ReservesNeedReturns is 1, found must not have been set waiting' );
905     $hold->delete;
906
907     t::lib::Mocks::mock_preference('ReservesNeedReturns', 0); # '0' means 'Automatically mark a hold as found and waiting'
908     $hold = place_item_hold( $patron, $item, $library, $priority );
909     is( $hold->priority, 0, 'If ReservesNeedReturns is 0 and no other status, priority must have been set to 0' );
910     is( $hold->found, 'W', 'If ReservesNeedReturns is 0 and no other status, found must have been set waiting' );
911     $hold->delete;
912
913     $item->onloan('2010-01-01')->store;
914     $hold = place_item_hold( $patron, $item, $library, $priority );
915     is( $hold->priority, 1, 'If ReservesNeedReturns is 0 but item onloan priority must be set to 1' );
916     $hold->delete;
917
918     t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0); # '0' means damaged holds not allowed
919     $item->onloan(undef)->damaged(1)->store;
920     $hold = place_item_hold( $patron, $item, $library, $priority );
921     is( $hold->priority, 1, 'If ReservesNeedReturns is 0 but item damaged and not allowed holds on damaged items priority must be set to 1' );
922     $hold->delete;
923     t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 1); # '0' means damaged holds not allowed
924     $hold = place_item_hold( $patron, $item, $library, $priority );
925     is( $hold->priority, 0, 'If ReservesNeedReturns is 0 and damaged holds allowed, priority must have been set to 0' );
926     is( $hold->found,  'W', 'If ReservesNeedReturns is 0 and damaged holds allowed, found must have been set waiting' );
927     $hold->delete;
928
929     my $hold_1 = place_item_hold( $patron, $item, $library, $priority );
930     is( $hold_1->found,  'W', 'First hold on item is set to waiting with ReservesNeedReturns set to 0' );
931     is( $hold_1->priority, 0, 'First hold on item is set to waiting with ReservesNeedReturns set to 0' );
932     $hold = place_item_hold( $patron_2, $item, $library, $priority );
933     is( $hold->priority, 1, 'If ReservesNeedReturns is 0 but item already on hold priority must be set to 1' );
934     $hold->delete;
935     $hold_1->delete;
936
937     my $transfer = $builder->build_object({
938         class => "Koha::Item::Transfers",
939         value => {
940           itemnumber  => $item->itemnumber,
941           datearrived => undef,
942           datecancelled => undef
943         }
944     });
945     $item->damaged(0)->store;
946     $hold = place_item_hold( $patron, $item, $library, $priority );
947     is( $hold->found, undef, 'If ReservesNeedReturns is 0 but item in transit the hold must not be set to waiting' );
948     is( $hold->priority, 1,  'If ReservesNeedReturns is 0 but item in transit the hold must not be set to waiting' );
949     $hold->delete;
950     $transfer->delete;
951
952     $hold = place_item_hold( $patron, $item, $library, $priority );
953     is( $hold->priority, 0, 'If ReservesNeedReturns is 0 and no other status, priority must have been set to 0' );
954     is( $hold->found, 'W', 'If ReservesNeedReturns is 0 and no other status, found must have been set waiting' );
955     $hold_1 = place_item_hold( $patron, $item, $library, $priority );
956     is( $hold_1->priority, 1, 'If ReservesNeedReturns is 0 but item has a hold priority is 1' );
957     $hold_1->suspend(1)->store; # We suspend the hold
958     $hold->delete; # Delete the waiting hold
959     $hold = place_item_hold( $patron, $item, $library, $priority );
960     is( $hold->priority, 0, 'If ReservesNeedReturns is 0 and other hold(s) suspended, priority must have been set to 0' );
961     is( $hold->found, 'W', 'If ReservesNeedReturns is 0 and other  hold(s) suspended, found must have been set waiting' );
962
963
964
965
966     t::lib::Mocks::mock_preference('ReservesNeedReturns', 1); # Don't affect other tests
967 };
968
969 subtest 'ChargeReserveFee tests' => sub {
970
971     plan tests => 8;
972
973     my $library = $builder->build_object({ class => 'Koha::Libraries' });
974     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
975
976     my $fee   = 20;
977     my $title = 'A title';
978
979     my $context = Test::MockModule->new('C4::Context');
980     $context->mock( userenv => { branch => $library->id } );
981
982     my $line = C4::Reserves::ChargeReserveFee( $patron->id, $fee, $title );
983
984     is( ref($line), 'Koha::Account::Line' , 'Returns a Koha::Account::Line object');
985     ok( $line->is_debit, 'Generates a debit line' );
986     is( $line->debit_type_code, 'RESERVE' , 'generates RESERVE debit_type');
987     is( $line->borrowernumber, $patron->id , 'generated line belongs to the passed patron');
988     is( $line->amount, $fee , 'amount set correctly');
989     is( $line->amountoutstanding, $fee , 'amountoutstanding set correctly');
990     is( $line->description, "$title" , 'description is title of reserved item');
991     is( $line->branchcode, $library->id , "Library id is picked from userenv and stored correctly" );
992 };
993
994 subtest 'reserves.item_level_hold' => sub {
995     plan tests => 2;
996
997     my $item   = $builder->build_sample_item;
998     my $patron = $builder->build_object(
999         {
1000             class => 'Koha::Patrons',
1001             value => { branchcode => $item->homebranch }
1002         }
1003     );
1004
1005     subtest 'item level hold' => sub {
1006         plan tests => 3;
1007         my $reserve_id = AddReserve(
1008             {
1009                 branchcode     => $item->homebranch,
1010                 borrowernumber => $patron->borrowernumber,
1011                 biblionumber   => $item->biblionumber,
1012                 priority       => 1,
1013                 itemnumber     => $item->itemnumber,
1014             }
1015         );
1016
1017         my $hold = Koha::Holds->find($reserve_id);
1018         is( $hold->item_level_hold, 1, 'item_level_hold should be set when AddReserve is called with a specific item' );
1019
1020         # Mark it waiting
1021         ModReserveAffect( $item->itemnumber, $patron->borrowernumber, 1 );
1022
1023         my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
1024         $mock->mock( 'enqueue', sub {
1025             my ( $self, $args ) = @_;
1026             is_deeply(
1027                 $args->{biblio_ids},
1028                 [ $hold->biblionumber ],
1029                 "AlterPriority triggers a holds queue update for the related biblio"
1030             );
1031         } );
1032
1033         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
1034
1035         # Revert the waiting status
1036         C4::Reserves::RevertWaitingStatus(
1037             { itemnumber => $item->itemnumber } );
1038
1039         $hold = Koha::Holds->find($reserve_id);
1040
1041         is( $hold->itemnumber, $item->itemnumber, 'Itemnumber should not be removed when the waiting status is revert' );
1042
1043         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
1044
1045         $hold->set_waiting;
1046
1047         # Revert the waiting status, RealTimeHoldsQueue => shouldn't add a test
1048         C4::Reserves::RevertWaitingStatus(
1049             { itemnumber => $item->itemnumber } );
1050
1051         $hold->delete;    # cleanup
1052     };
1053
1054     subtest 'biblio level hold' => sub {
1055         plan tests => 3;
1056         my $reserve_id = AddReserve(
1057             {
1058                 branchcode     => $item->homebranch,
1059                 borrowernumber => $patron->borrowernumber,
1060                 biblionumber   => $item->biblionumber,
1061                 priority       => 1,
1062             }
1063         );
1064
1065         my $hold = Koha::Holds->find($reserve_id);
1066         is( $hold->item_level_hold, 0, 'item_level_hold should not be set when AddReserve is called without a specific item' );
1067
1068         # Mark it waiting
1069         ModReserveAffect( $item->itemnumber, $patron->borrowernumber, 1 );
1070
1071         $hold = Koha::Holds->find($reserve_id);
1072         is( $hold->itemnumber, $item->itemnumber, 'Itemnumber should be set on hold confirmation' );
1073
1074         # Revert the waiting status
1075         C4::Reserves::RevertWaitingStatus( { itemnumber => $item->itemnumber } );
1076
1077         $hold = Koha::Holds->find($reserve_id);
1078         is( $hold->itemnumber, undef, 'Itemnumber should be removed when the waiting status is revert' );
1079
1080         $hold->delete;
1081     };
1082
1083 };
1084
1085 subtest 'MoveReserve additional test' => sub {
1086
1087     plan tests => 4;
1088
1089     # Create the items and patrons we need
1090     my $biblio = $builder->build_sample_biblio();
1091     my $itype = $builder->build_object({ class => "Koha::ItemTypes", value => { notforloan => 0 } });
1092     my $item_1 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber,notforloan => 0, itype => $itype->itemtype });
1093     my $item_2 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber, notforloan => 0, itype => $itype->itemtype });
1094     my $patron_1 = $builder->build_object({ class => "Koha::Patrons" });
1095     my $patron_2 = $builder->build_object({ class => "Koha::Patrons" });
1096
1097     # Place a hold on the title for both patrons
1098     my $reserve_1 = AddReserve(
1099         {
1100             branchcode     => $item_1->homebranch,
1101             borrowernumber => $patron_1->borrowernumber,
1102             biblionumber   => $biblio->biblionumber,
1103             priority       => 1,
1104             itemnumber     => $item_1->itemnumber,
1105         }
1106     );
1107     my $reserve_2 = AddReserve(
1108         {
1109             branchcode     => $item_2->homebranch,
1110             borrowernumber => $patron_2->borrowernumber,
1111             biblionumber   => $biblio->biblionumber,
1112             priority       => 1,
1113             itemnumber     => $item_1->itemnumber,
1114         }
1115     );
1116     is($patron_1->holds->next()->reserve_id, $reserve_1, "The 1st patron has a hold");
1117     is($patron_2->holds->next()->reserve_id, $reserve_2, "The 2nd patron has a hold");
1118
1119     # Fake the holds queue
1120     $dbh->do(q{INSERT INTO hold_fill_targets VALUES (?, ?, ?, ?, ?,?)},undef,($patron_1->borrowernumber,$biblio->biblionumber,$item_1->itemnumber,$item_1->homebranch,0,$reserve_1));
1121
1122     # The 2nd hold should be filed even if the item is preselected for the first hold
1123     MoveReserve($item_1->itemnumber,$patron_2->borrowernumber);
1124     is($patron_2->holds->count, 0, "The 2nd patrons no longer has a hold");
1125     is($patron_2->old_holds->next()->reserve_id, $reserve_2, "The 2nd patrons hold was filled and moved to old holds");
1126
1127 };
1128
1129 subtest 'RevertWaitingStatus' => sub {
1130
1131     plan tests => 2;
1132
1133     # Create the items and patrons we need
1134     my $biblio  = $builder->build_sample_biblio();
1135     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1136     my $itype   = $builder->build_object(
1137         { class => "Koha::ItemTypes", value => { notforloan => 0 } } );
1138     my $item_1 = $builder->build_sample_item(
1139         {
1140             biblionumber => $biblio->biblionumber,
1141             itype        => $itype->itemtype,
1142             library      => $library->branchcode
1143         }
1144     );
1145     my $patron_1 = $builder->build_object( { class => "Koha::Patrons" } );
1146     my $patron_2 = $builder->build_object( { class => "Koha::Patrons" } );
1147     my $patron_3 = $builder->build_object( { class => "Koha::Patrons" } );
1148     my $patron_4 = $builder->build_object( { class => "Koha::Patrons" } );
1149
1150     # Place a hold on the title for both patrons
1151     my $priority = 1;
1152     my $hold_1 = place_item_hold( $patron_1, $item_1, $library, $priority );
1153     my $hold_2 = place_item_hold( $patron_2, $item_1, $library, $priority );
1154     my $hold_3 = place_item_hold( $patron_3, $item_1, $library, $priority );
1155     my $hold_4 = place_item_hold( $patron_4, $item_1, $library, $priority );
1156
1157     $hold_1->set_waiting;
1158     AddIssue( $patron_3->unblessed, $item_1->barcode, undef, 'revert' );
1159
1160     my $holds = $biblio->holds;
1161     is( $holds->count, 3, 'One hold has been deleted' );
1162     is_deeply(
1163         [
1164             $holds->next->priority, $holds->next->priority,
1165             $holds->next->priority
1166         ],
1167         [ 1, 2, 3 ],
1168         'priorities have been reordered'
1169     );
1170 };
1171
1172 subtest 'CheckReserves additional tests' => sub {
1173
1174     plan tests => 8;
1175
1176     my $item = $builder->build_sample_item;
1177     my $reserve1 = $builder->build_object(
1178         {
1179             class => "Koha::Holds",
1180             value => {
1181                 found            => undef,
1182                 priority         => 1,
1183                 itemnumber       => undef,
1184                 biblionumber     => $item->biblionumber,
1185                 waitingdate      => undef,
1186                 cancellationdate => undef,
1187                 item_level_hold  => 0,
1188                 lowestPriority   => 0,
1189                 expirationdate   => undef,
1190                 suspend_until    => undef,
1191                 suspend          => 0,
1192                 itemtype         => undef,
1193             }
1194         }
1195     );
1196     my $reserve2 = $builder->build_object(
1197         {
1198             class => "Koha::Holds",
1199             value => {
1200                 found            => undef,
1201                 priority         => 2,
1202                 biblionumber     => $item->biblionumber,
1203                 borrowernumber   => $reserve1->borrowernumber,
1204                 itemnumber       => undef,
1205                 waitingdate      => undef,
1206                 cancellationdate => undef,
1207                 item_level_hold  => 0,
1208                 lowestPriority   => 0,
1209                 expirationdate   => undef,
1210                 suspend_until    => undef,
1211                 suspend          => 0,
1212                 itemtype         => undef,
1213             }
1214         }
1215     );
1216
1217     my $tmp_holdsqueue = $builder->build(
1218         {
1219             source => 'TmpHoldsqueue',
1220             value  => {
1221                 borrowernumber => $reserve1->borrowernumber,
1222                 biblionumber   => $reserve1->biblionumber,
1223             }
1224         }
1225     );
1226     my $fill_target = $builder->build(
1227         {
1228             source => 'HoldFillTarget',
1229             value  => {
1230                 borrowernumber     => $reserve1->borrowernumber,
1231                 biblionumber       => $reserve1->biblionumber,
1232                 itemnumber         => $item->itemnumber,
1233                 item_level_request => 0,
1234             }
1235         }
1236     );
1237
1238     ModReserveAffect( $item->itemnumber, $reserve1->borrowernumber, 1,
1239         $reserve1->reserve_id );
1240     my ( $status, $matched_reserve, $possible_reserves ) =
1241       CheckReserves( $item );
1242
1243     is( $status, 'Transferred', "We found a reserve" );
1244     is( $matched_reserve->{reserve_id},
1245         $reserve1->reserve_id, "We got the Transit reserve" );
1246     is( scalar @$possible_reserves, 2, 'We do get both reserves' );
1247
1248     my $patron_B = $builder->build_object({ class => "Koha::Patrons" });
1249     my $item_A = $builder->build_sample_item;
1250     my $item_B = $builder->build_sample_item({
1251         homebranch => $patron_B->branchcode,
1252         biblionumber => $item_A->biblionumber,
1253         itype => $item_A->itype
1254     });
1255     Koha::CirculationRules->set_rules(
1256         {
1257             branchcode   => undef,
1258             categorycode => undef,
1259             itemtype     => $item_A->itype,
1260             rules        => {
1261                 reservesallowed => 25,
1262                 holds_per_record => 1,
1263             }
1264         }
1265     );
1266     Koha::CirculationRules->set_rule({
1267         branchcode => undef,
1268         itemtype   => $item_A->itype,
1269         rule_name  => 'holdallowed',
1270         rule_value => 'from_home_library'
1271     });
1272     my $reserve_id = AddReserve(
1273         {
1274             branchcode     => $patron_B->branchcode,
1275             borrowernumber => $patron_B->borrowernumber,
1276             biblionumber   => $item_A->biblionumber,
1277             priority       => 1,
1278             itemnumber     => undef,
1279         }
1280     );
1281
1282     ok( $reserve_id, "We can place a record level hold because one item is owned by patron's home library");
1283     t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary');
1284     ( $status, $matched_reserve, $possible_reserves ) = CheckReserves( $item_A );
1285     is( $status, "", "We do not fill the hold with item A because it is not from the patron's homebranch");
1286     Koha::CirculationRules->set_rule({
1287         branchcode => $item_A->homebranch,
1288         itemtype   => $item_A->itype,
1289         rule_name  => 'holdallowed',
1290         rule_value => 'from_any_library'
1291     });
1292     ( $status, $matched_reserve, $possible_reserves ) = CheckReserves( $item_A );
1293     is( $status, "Reserved", "We fill the hold with item A because item's branch rule says allow any");
1294
1295
1296     # Changing the control branch should change only the rule we get
1297     t::lib::Mocks::mock_preference('ReservesControlBranch', 'PatronLibrary');
1298     ( $status, $matched_reserve, $possible_reserves ) = CheckReserves( $item_A );
1299     is( $status, "", "We do not fill the hold with item A because it is not from the patron's homebranch");
1300     Koha::CirculationRules->set_rule({
1301         branchcode   => $patron_B->branchcode,
1302         itemtype   => $item_A->itype,
1303         rule_name  => 'holdallowed',
1304         rule_value => 'from_any_library'
1305     });
1306     ( $status, $matched_reserve, $possible_reserves ) = CheckReserves( $item_A );
1307     is( $status, "Reserved", "We fill the hold with item A because patron's branch rule says allow any");
1308
1309 };
1310
1311 subtest 'AllowHoldOnPatronPossession test' => sub {
1312
1313     plan tests => 4;
1314
1315     # Create the items and patrons we need
1316     my $biblio = $builder->build_sample_biblio();
1317     my $itype = $builder->build_object({ class => "Koha::ItemTypes", value => { notforloan => 0 } });
1318     my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber,notforloan => 0, itype => $itype->itemtype });
1319     my $patron = $builder->build_object({ class => "Koha::Patrons",
1320                                           value => { branchcode => $item->homebranch }});
1321
1322     C4::Circulation::AddIssue($patron->unblessed,
1323                               $item->barcode);
1324     t::lib::Mocks::mock_preference('AllowHoldsOnPatronsPossessions', 0);
1325
1326     is(C4::Reserves::CanBookBeReserved($patron->borrowernumber,
1327                                        $item->biblionumber)->{status},
1328        'alreadypossession',
1329        'Patron cannot place hold on a book loaned to itself');
1330
1331     is(C4::Reserves::CanItemBeReserved( $patron, $item )->{status},
1332        'alreadypossession',
1333        'Patron cannot place hold on an item loaned to itself');
1334
1335     t::lib::Mocks::mock_preference('AllowHoldsOnPatronsPossessions', 1);
1336
1337     is(C4::Reserves::CanBookBeReserved($patron->borrowernumber,
1338                                        $item->biblionumber)->{status},
1339        'OK',
1340        'Patron can place hold on a book loaned to itself');
1341
1342     is(C4::Reserves::CanItemBeReserved( $patron, $item )->{status},
1343        'OK',
1344        'Patron can place hold on an item loaned to itself');
1345 };
1346
1347 subtest 'MergeHolds' => sub {
1348
1349     plan tests => 1;
1350
1351     my $biblio_1  = $builder->build_sample_biblio();
1352     my $biblio_2  = $builder->build_sample_biblio();
1353     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1354     my $itype   = $builder->build_object(
1355         { class => "Koha::ItemTypes", value => { notforloan => 0 } } );
1356     my $item_1 = $builder->build_sample_item(
1357         {
1358             biblionumber => $biblio_1->biblionumber,
1359             itype        => $itype->itemtype,
1360             library      => $library->branchcode
1361         }
1362     );
1363     my $patron_1 = $builder->build_object( { class => "Koha::Patrons" } );
1364
1365     # Place a hold on $biblio_1
1366     my $priority = 1;
1367     place_item_hold( $patron_1, $item_1, $library, $priority );
1368
1369     # Move and make sure hold is now on $biblio_2
1370     C4::Reserves::MergeHolds($dbh, $biblio_2->biblionumber, $biblio_1->biblionumber);
1371     is( $biblio_2->holds->count, 1, 'Hold has been transferred' );
1372 };
1373
1374 subtest 'ModReserveAffect logging' => sub {
1375
1376     plan tests => 4;
1377
1378     my $item = $builder->build_sample_item;
1379     my $patron = $builder->build_object(
1380         {
1381             class => "Koha::Patrons",
1382             value => { branchcode => $item->homebranch }
1383         }
1384     );
1385
1386     t::lib::Mocks::mock_userenv({ patron => $patron });
1387     t::lib::Mocks::mock_preference('HoldsLog', 1);
1388
1389     my $reserve_id = AddReserve(
1390         {
1391             branchcode     => $item->homebranch,
1392             borrowernumber => $patron->borrowernumber,
1393             biblionumber   => $item->biblionumber,
1394             priority       => 1,
1395             itemnumber     => $item->itemnumber,
1396         }
1397     );
1398
1399     my $hold = Koha::Holds->find($reserve_id);
1400     my $previous_timestamp = '1970-01-01 12:34:56';
1401     $hold->timestamp($previous_timestamp)->store;
1402
1403     $hold = Koha::Holds->find($reserve_id);
1404     is( $hold->timestamp, $previous_timestamp, 'Make sure the previous timestamp has been used' );
1405
1406     # Avoid warnings
1407     my $reserve_mock = Test::MockModule->new('C4::Reserves');
1408     $reserve_mock->mock( '_koha_notify_reserve', undef );
1409
1410     # Mark it waiting
1411     ModReserveAffect( $item->itemnumber, $patron->borrowernumber );
1412
1413     $hold->discard_changes;
1414     ok( $hold->is_waiting, 'Hold has been set waiting' );
1415     isnt( $hold->timestamp, $previous_timestamp, 'The timestamp has been modified' );
1416
1417     my $log = Koha::ActionLogs->search({ module => 'HOLDS', action => 'MODIFY', object => $hold->reserve_id })->next;
1418     my $expected = sprintf q{'timestamp' => '%s'}, $hold->timestamp;
1419     like( $log->info, qr{$expected}, 'Timestamp logged is the current one' );
1420 };
1421
1422 sub count_hold_print_messages {
1423     my $message_count = $dbh->selectall_arrayref(q{
1424         SELECT COUNT(*)
1425         FROM message_queue
1426         WHERE letter_code = 'HOLD' 
1427         AND   message_transport_type = 'print'
1428     });
1429     return $message_count->[0]->[0];
1430 }
1431
1432 sub place_item_hold {
1433     my ($patron,$item,$library,$priority) = @_;
1434
1435     my $hold_id = C4::Reserves::AddReserve(
1436         {
1437             branchcode     => $library->branchcode,
1438             borrowernumber => $patron->borrowernumber,
1439             biblionumber   => $item->biblionumber,
1440             priority       => $priority,
1441             title          => "title for fee",
1442             itemnumber     => $item->itemnumber,
1443         }
1444     );
1445
1446     my $hold = Koha::Holds->find($hold_id);
1447     return $hold;
1448 }
1449
1450 # we reached the finish
1451 $schema->storage->txn_rollback();
1452
1453 subtest 'IsAvailableForItemLevelRequest() tests' => sub {
1454
1455     plan tests => 2;
1456
1457     $schema->storage->txn_begin;
1458
1459     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1460
1461     my $item_type = undef;
1462
1463     my $item_mock = Test::MockModule->new('Koha::Item');
1464     $item_mock->mock( 'effective_itemtype', sub { return $item_type; } );
1465
1466     my $item = $builder->build_sample_item;
1467
1468     ok(
1469         !C4::Reserves::IsAvailableForItemLevelRequest( $item, $patron ),
1470         "Item not available for item-level hold because no effective item type"
1471     );
1472
1473     # Weird use case to highlight issue
1474     $item_type = '0';
1475     Koha::ItemTypes->search( { itemtype => $item_type } )->delete;
1476     my $itemtype = $builder->build_object(
1477         {
1478             class => 'Koha::ItemTypes',
1479             value => { itemtype => $item_type }
1480         }
1481     );
1482     ok(
1483         C4::Reserves::IsAvailableForItemLevelRequest( $item, $patron ),
1484         "Item not available for item-level hold because no effective item type"
1485     );
1486
1487     $schema->storage->txn_rollback;
1488 };
1489
1490 subtest 'AddReserve() tests' => sub {
1491
1492     plan tests => 1;
1493
1494     $schema->storage->txn_begin;
1495
1496     my $library = $builder->build_object({ class => 'Koha::Libraries' });
1497     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
1498     my $biblio  = $builder->build_sample_biblio;
1499
1500     my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
1501     $mock->mock( 'enqueue', sub {
1502         my ( $self, $args ) = @_;
1503         is_deeply(
1504             $args->{biblio_ids},
1505             [ $biblio->id ],
1506             "AddReserve triggers a holds queue update for the related biblio"
1507         );
1508     } );
1509
1510     t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
1511
1512     AddReserve(
1513         {
1514             branchcode     => $library->branchcode,
1515             borrowernumber => $patron->id,
1516             biblionumber   => $biblio->id,
1517         }
1518     );
1519
1520     t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
1521
1522     AddReserve(
1523         {
1524             branchcode     => $library->branchcode,
1525             borrowernumber => $patron->id,
1526             biblionumber   => $biblio->id,
1527         }
1528     );
1529
1530     $schema->storage->txn_rollback;
1531 };
1532
1533 subtest 'AlterPriorty() tests' => sub {
1534
1535     plan tests => 2;
1536
1537     $schema->storage->txn_begin;
1538
1539     my $library = $builder->build_object({ class => 'Koha::Libraries' });
1540     my $patron_1  = $builder->build_object({ class => 'Koha::Patrons' });
1541     my $patron_2  = $builder->build_object({ class => 'Koha::Patrons' });
1542     my $patron_3  = $builder->build_object({ class => 'Koha::Patrons' });
1543     my $biblio  = $builder->build_sample_biblio;
1544
1545     my $reserve_id = AddReserve(
1546         {
1547             branchcode     => $library->branchcode,
1548             borrowernumber => $patron_1->id,
1549             biblionumber   => $biblio->id,
1550         }
1551     );
1552     AddReserve(
1553         {
1554             branchcode     => $library->branchcode,
1555             borrowernumber => $patron_2->id,
1556             biblionumber   => $biblio->id,
1557         }
1558     );
1559     AddReserve(
1560         {
1561             branchcode     => $library->branchcode,
1562             borrowernumber => $patron_3->id,
1563             biblionumber   => $biblio->id,
1564         }
1565     );
1566
1567     my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
1568     $mock->mock( 'enqueue', sub {
1569         my ( $self, $args ) = @_;
1570         is_deeply(
1571             $args->{biblio_ids},
1572             [ $biblio->id ],
1573             "AlterPriority triggers a holds queue update for the related biblio"
1574         );
1575     } );
1576
1577     t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
1578
1579     AlterPriority( "bottom", $reserve_id, 1, 2, 1, 3 );
1580
1581     my $hold = Koha::Holds->find($reserve_id);
1582
1583     is($hold->priority,3,'Successfully altered priority to bottom');
1584
1585     t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
1586
1587     AlterPriority( "bottom", $reserve_id, 1, 2, 1, 3 );
1588
1589     $schema->storage->txn_rollback;
1590 };
1591
1592 subtest 'CanBookBeReserved() tests' => sub {
1593
1594     plan tests => 2;
1595
1596     $schema->storage->txn_begin;
1597
1598     my $library = $builder->build_object(
1599         { class => 'Koha::Libraries', value => { pickup_location => 1 } } );
1600     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1601     my $itype  = $builder->build_object( { class => 'Koha::ItemTypes' } );
1602
1603     my $biblio = $builder->build_sample_biblio();
1604     my $item_1 = $builder->build_sample_item(
1605         { biblionumber => $biblio->id, itype => $itype->id } );
1606     my $item_2 = $builder->build_sample_item(
1607         { biblionumber => $biblio->id, itype => $itype->id } );
1608
1609     Koha::CirculationRules->delete;
1610     Koha::CirculationRules->set_rules(
1611         {
1612             branchcode   => undef,
1613             categorycode => undef,
1614             itemtype     => undef,
1615             rules        => {
1616                 holds_per_record => 100,
1617             }
1618         }
1619     );
1620     Koha::CirculationRules->set_rules(
1621         {
1622             branchcode   => undef,
1623             categorycode => undef,
1624             itemtype     => $itype->id,
1625             rules        => {
1626                 reservesallowed => 2,
1627             }
1628         }
1629     );
1630
1631     C4::Reserves::AddReserve(
1632         {
1633             branchcode     => $library->id,
1634             borrowernumber => $patron->id,
1635             biblionumber   => $biblio->id,
1636             title          => $biblio->title,
1637             itemnumber     => $item_1->id
1638         }
1639     );
1640
1641     ## Limit on item type is 2, only one hold, success tests
1642
1643     my $res = CanBookBeReserved( $patron->id, $biblio->id, $library->id,
1644         { itemtype => $itype->id } );
1645     is_deeply( $res, { status => 'OK' },
1646         'Holds on itemtype limit not reached' );
1647
1648     # Add a second hold, biblio-level and item type-constrained
1649     C4::Reserves::AddReserve(
1650         {
1651             branchcode     => $library->id,
1652             borrowernumber => $patron->id,
1653             biblionumber   => $biblio->id,
1654             title          => $biblio->title,
1655             itemtype       => $itype->id,
1656         }
1657     );
1658
1659     ## Limit on item type is 2, two holds, one of them biblio-level/item type-constrained
1660
1661     $res = CanBookBeReserved( $patron->id, $biblio->id, $library->id,
1662         { itemtype => $itype->id } );
1663     is_deeply( $res, { status => '' }, 'Holds on itemtype limit reached' );
1664
1665     $schema->storage->txn_rollback;
1666 };
1667
1668 subtest 'CanItemBeReserved() tests' => sub {
1669
1670     plan tests => 2;
1671
1672     $schema->storage->txn_begin;
1673
1674     my $library = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1 } } );
1675     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
1676     my $itype   = $builder->build_object( { class => 'Koha::ItemTypes' } );
1677
1678     my $biblio = $builder->build_sample_biblio();
1679     my $item_1 = $builder->build_sample_item({ biblionumber => $biblio->id, itype => $itype->id });
1680     my $item_2 = $builder->build_sample_item({ biblionumber => $biblio->id, itype => $itype->id });
1681
1682     Koha::CirculationRules->delete;
1683     Koha::CirculationRules->set_rules(
1684         {   branchcode   => undef,
1685             categorycode => undef,
1686             itemtype     => undef,
1687             rules        => {
1688                 holds_per_record => 100,
1689             }
1690         }
1691     );
1692     Koha::CirculationRules->set_rules(
1693         {   branchcode   => undef,
1694             categorycode => undef,
1695             itemtype     => $itype->id,
1696             rules        => {
1697                 reservesallowed => 2,
1698             }
1699         }
1700     );
1701
1702     C4::Reserves::AddReserve(
1703         {
1704             branchcode     => $library->id,
1705             borrowernumber => $patron->id,
1706             biblionumber   => $biblio->id,
1707             title          => $biblio->title,
1708             itemnumber     => $item_1->id
1709         }
1710     );
1711
1712     ## Limit on item type is 2, only one hold, success tests
1713
1714     my $res = CanItemBeReserved( $patron, $item_2, $library->id );
1715     is_deeply( $res, { status => 'OK' }, 'Holds on itemtype limit not reached' );
1716
1717     # Add a second hold, biblio-level and item type-constrained
1718     C4::Reserves::AddReserve(
1719         {
1720             branchcode     => $library->id,
1721             borrowernumber => $patron->id,
1722             biblionumber   => $biblio->id,
1723             title          => $biblio->title,
1724             itemtype       => $itype->id,
1725         }
1726     );
1727
1728     ## Limit on item type is 2, two holds, one of them biblio-level/item type-constrained
1729
1730     $res = CanItemBeReserved( $patron, $item_2, $library->id );
1731     is_deeply( $res, { status => 'tooManyReserves', limit => 2 }, 'Holds on itemtype limit reached' );
1732
1733     $schema->storage->txn_rollback;
1734 };
1735
1736 subtest 'DefaultHoldExpiration tests' => sub {
1737     plan tests => 2;
1738     $schema->storage->txn_begin;
1739
1740     t::lib::Mocks::mock_preference( 'DefaultHoldExpirationdate', 1 );
1741     t::lib::Mocks::mock_preference( 'DefaultHoldExpirationdatePeriod', 365 );
1742     t::lib::Mocks::mock_preference( 'DefaultHoldExpirationUnitOfTime', 'days;' );
1743
1744     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
1745     my $item    = $builder->build_sample_item();
1746
1747     my $reserve_id = AddReserve({
1748         branchcode     => $item->homebranch,
1749         borrowernumber => $patron->id,
1750         biblionumber   => $item->biblionumber,
1751     });
1752
1753     my $today = dt_from_string();
1754     my $hold = Koha::Holds->find( $reserve_id );
1755
1756     is( $hold->reservedate, $today->ymd, "Hold created today" );
1757     is( $hold->expirationdate, $today->add( days => 365)->ymd, "Reserve date set 1 year from today" );
1758
1759     $schema->txn_rollback;
1760 };