Bug 28503: Unit tests
[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 => 68;
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;
31 use C4::Items;
32 use C4::Biblio;
33 use C4::Members;
34 use C4::Reserves;
35 use Koha::ActionLogs;
36 use Koha::Caches;
37 use Koha::DateUtils;
38 use Koha::Holds;
39 use Koha::Items;
40 use Koha::Libraries;
41 use Koha::Notice::Templates;
42 use Koha::Patrons;
43 use Koha::Patron::Categories;
44 use Koha::CirculationRules;
45
46 BEGIN {
47     require_ok('C4::Reserves');
48 }
49
50 # Start transaction
51 my $database = Koha::Database->new();
52 my $schema = $database->schema();
53 $schema->storage->txn_begin();
54 my $dbh = C4::Context->dbh;
55 $dbh->do('DELETE FROM circulation_rules');
56
57 my $builder = t::lib::TestBuilder->new;
58
59 my $frameworkcode = q//;
60
61
62 t::lib::Mocks::mock_preference('ReservesNeedReturns', 1);
63
64 # Somewhat arbitrary field chosen for age restriction unit tests. Must be added to db before the framework is cached
65 $dbh->do("update marc_subfield_structure set kohafield='biblioitems.agerestriction' where tagfield='521' and tagsubfield='a' and frameworkcode=?", undef, $frameworkcode);
66 my $cache = Koha::Caches->get_instance;
67 $cache->clear_from_cache("MarcStructure-0-$frameworkcode");
68 $cache->clear_from_cache("MarcStructure-1-$frameworkcode");
69 $cache->clear_from_cache("default_value_for_mod_marc-");
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 # Create a borrower
98 my %data = (
99     firstname =>  'my firstname',
100     surname => 'my surname',
101     categorycode => $category_1,
102     branchcode => $branch_1,
103 );
104 Koha::Patron::Categories->find($category_1)->set({ enrolmentfee => 0})->store;
105 my $borrowernumber = Koha::Patron->new(\%data)->store->borrowernumber;
106 my $patron = Koha::Patrons->find( $borrowernumber );
107 my $borrower = $patron->unblessed;
108 my $biblionumber   = $bibnum;
109 my $barcode        = $testbarcode;
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->itemnumber, $barcode);
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->itemnumber);
129 is($status, "Reserved", "CheckReserves Test 2");
130
131 ($status, $reserve, $all_reserves) = CheckReserves(undef, $barcode);
132 is($status, "Reserved", "CheckReserves Test 3");
133
134 my $ReservesControlBranch = C4::Context->preference('ReservesControlBranch');
135 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
136 ok(
137     'ItemHomeLib' eq GetReservesControlBranch(
138         { homebranch => 'ItemHomeLib' },
139         { branchcode => 'PatronHomeLib' }
140     ), "GetReservesControlBranch returns item home branch when set to ItemHomeLibrary"
141 );
142 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
143 ok(
144     'PatronHomeLib' eq GetReservesControlBranch(
145         { homebranch => 'ItemHomeLib' },
146         { branchcode => 'PatronHomeLib' }
147     ), "GetReservesControlBranch returns patron home branch when set to PatronLibrary"
148 );
149 t::lib::Mocks::mock_preference( 'ReservesControlBranch', $ReservesControlBranch );
150
151 ###
152 ### Regression test for bug 10272
153 ###
154 my %requesters = ();
155 $requesters{$branch_1} = Koha::Patron->new({
156     branchcode   => $branch_1,
157     categorycode => $category_2,
158     surname      => "borrower from $branch_1",
159 })->store->borrowernumber;
160 for my $i ( 2 .. 5 ) {
161     $requesters{"CPL$i"} = Koha::Patron->new({
162         branchcode   => $branch_1,
163         categorycode => $category_2,
164         surname      => "borrower $i from $branch_1",
165     })->store->borrowernumber;
166 }
167 $requesters{$branch_2} = Koha::Patron->new({
168     branchcode   => $branch_2,
169     categorycode => $category_2,
170     surname      => "borrower from $branch_2",
171 })->store->borrowernumber;
172 $requesters{$branch_3} = Koha::Patron->new({
173     branchcode   => $branch_3,
174     categorycode => $category_2,
175     surname      => "borrower from $branch_3",
176 })->store->borrowernumber;
177
178 # Configure rules so that $branch_1 allows only $branch_1 patrons
179 # to request its items, while $branch_2 will allow its items
180 # to fill holds from anywhere.
181
182 $dbh->do('DELETE FROM circulation_rules');
183 Koha::CirculationRules->set_rules(
184     {
185         branchcode   => undef,
186         categorycode => undef,
187         itemtype     => undef,
188         rules        => {
189             reservesallowed => 25,
190             holds_per_record => 1,
191             onshelfholds => 1,
192         }
193     }
194 );
195
196 # CPL allows only its own patrons to request its items
197 Koha::CirculationRules->set_rules(
198     {
199         branchcode   => $branch_1,
200         itemtype     => undef,
201         rules        => {
202             holdallowed  => 'from_home_library',
203             returnbranch => 'homebranch',
204         }
205     }
206 );
207
208 # ... while FPL allows anybody to request its items
209 Koha::CirculationRules->set_rules(
210     {
211         branchcode   => $branch_2,
212         itemtype     => undef,
213         rules        => {
214             holdallowed  => 'from_any_library',
215             returnbranch => 'homebranch',
216         }
217     }
218 );
219
220 my $bibnum2 = $builder->build_sample_biblio({frameworkcode => $frameworkcode})->biblionumber;
221
222 my ($itemnum_cpl, $itemnum_fpl);
223 $itemnum_cpl = $builder->build_sample_item(
224     {
225         biblionumber => $bibnum2,
226         library      => $branch_1,
227         barcode      => 'bug10272_CPL',
228         itype        => $itemtype
229     }
230 )->itemnumber;
231 $itemnum_fpl = $builder->build_sample_item(
232     {
233         biblionumber => $bibnum2,
234         library      => $branch_2,
235         barcode      => 'bug10272_FPL',
236         itype        => $itemtype
237     }
238 )->itemnumber;
239
240 # Ensure that priorities are numbered correcly when a hold is moved to waiting
241 # (bug 11947)
242 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum2));
243 AddReserve(
244     {
245         branchcode     => $branch_3,
246         borrowernumber => $requesters{$branch_3},
247         biblionumber   => $bibnum2,
248         priority       => 1,
249     }
250 );
251 AddReserve(
252     {
253         branchcode     => $branch_2,
254         borrowernumber => $requesters{$branch_2},
255         biblionumber   => $bibnum2,
256         priority       => 2,
257     }
258 );
259 AddReserve(
260     {
261         branchcode     => $branch_1,
262         borrowernumber => $requesters{$branch_1},
263         biblionumber   => $bibnum2,
264         priority       => 3,
265     }
266 );
267 ModReserveAffect($itemnum_cpl, $requesters{$branch_3}, 0);
268
269 # Now it should have different priorities.
270 my $biblio = Koha::Biblios->find( $bibnum2 );
271 my $holds = $biblio->holds({}, { order_by => 'reserve_id' });;
272 is($holds->next->priority, 0, 'Item is correctly waiting');
273 is($holds->next->priority, 1, 'Item is correctly priority 1');
274 is($holds->next->priority, 2, 'Item is correctly priority 2');
275
276 my @reserves = Koha::Holds->search({ borrowernumber => $requesters{$branch_3} })->waiting();
277 is( @reserves, 1, 'GetWaiting got only the waiting reserve' );
278 is( $reserves[0]->borrowernumber(), $requesters{$branch_3}, 'GetWaiting got the reserve for the correct borrower' );
279
280
281 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum2));
282 AddReserve(
283     {
284         branchcode     => $branch_3,
285         borrowernumber => $requesters{$branch_3},
286         biblionumber   => $bibnum2,
287         priority       => 1,
288     }
289 );
290 AddReserve(
291     {
292         branchcode     => $branch_2,
293         borrowernumber => $requesters{$branch_2},
294         biblionumber   => $bibnum2,
295         priority       => 2,
296     }
297 );
298
299 AddReserve(
300     {
301         branchcode     => $branch_1,
302         borrowernumber => $requesters{$branch_1},
303         biblionumber   => $bibnum2,
304         priority       => 3,
305     }
306 );
307
308 # Ensure that the item's home library controls hold policy lookup
309 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
310
311 my $messages;
312 # Return the CPL item at FPL.  The hold that should be triggered is
313 # the one placed by the CPL patron, as the other two patron's hold
314 # requests cannot be filled by that item per policy.
315 (undef, $messages, undef, undef) = AddReturn('bug10272_CPL', $branch_2);
316 is( $messages->{ResFound}->{borrowernumber},
317     $requesters{$branch_1},
318     'restrictive library\'s items only fill requests by own patrons (bug 10272)');
319
320 # Return the FPL item at FPL.  The hold that should be triggered is
321 # the one placed by the RPL patron, as that patron is first in line
322 # and RPL imposes no restrictions on whose holds its items can fill.
323
324 # Ensure that the preference 'LocalHoldsPriority' is not set (Bug 15244):
325 t::lib::Mocks::mock_preference( 'LocalHoldsPriority', '' );
326
327 (undef, $messages, undef, undef) = AddReturn('bug10272_FPL', $branch_2);
328 is( $messages->{ResFound}->{borrowernumber},
329     $requesters{$branch_3},
330     'for generous library, its items fill first hold request in line (bug 10272)');
331
332 $biblio = Koha::Biblios->find( $biblionumber );
333 $holds = $biblio->holds;
334 is($holds->count, 1, "Only one reserves for this biblio");
335 $holds->next->reserve_id;
336
337 # Tests for bug 9761 (ConfirmFutureHolds): new CheckReserves lookahead parameter, and corresponding change in AddReturn
338 # Note that CheckReserve uses its lookahead parameter and does not check ConfirmFutureHolds pref (it should be passed if needed like AddReturn does)
339 # Test 9761a: Add a reserve without date, CheckReserve should return it
340 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum));
341 AddReserve(
342     {
343         branchcode     => $branch_1,
344         borrowernumber => $requesters{$branch_1},
345         biblionumber   => $bibnum,
346         priority       => 1,
347     }
348 );
349 ($status)=CheckReserves($item->itemnumber,undef,undef);
350 is( $status, 'Reserved', 'CheckReserves returns reserve without lookahead');
351 ($status)=CheckReserves($item->itemnumber,undef,7);
352 is( $status, 'Reserved', 'CheckReserves also returns reserve with lookahead');
353
354 # Test 9761b: Add a reserve with future date, CheckReserve should not return it
355 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum));
356 t::lib::Mocks::mock_preference('AllowHoldDateInFuture', 1);
357 my $resdate= dt_from_string();
358 $resdate->add_duration(DateTime::Duration->new(days => 4));
359 $resdate=output_pref($resdate);
360 my $reserve_id = AddReserve(
361     {
362         branchcode       => $branch_1,
363         borrowernumber   => $requesters{$branch_1},
364         biblionumber     => $bibnum,
365         priority         => 1,
366         reservation_date => $resdate,
367     }
368 );
369 ($status)=CheckReserves($item->itemnumber,undef,undef);
370 is( $status, '', 'CheckReserves returns no future reserve without lookahead');
371
372 # Test 9761c: Add a reserve with future date, CheckReserve should return it if lookahead is high enough
373 ($status)=CheckReserves($item->itemnumber,undef,3);
374 is( $status, '', 'CheckReserves returns no future reserve with insufficient lookahead');
375 ($status)=CheckReserves($item->itemnumber,undef,4);
376 is( $status, 'Reserved', 'CheckReserves returns future reserve with sufficient lookahead');
377
378 # Test 9761d: Check ResFound message of AddReturn for future hold
379 # Note that AddReturn is in Circulation.pm, but this test really pertains to reserves; AddReturn uses the ConfirmFutureHolds pref when calling CheckReserves
380 # In this test we do not need an issued item; it is just a 'checkin'
381 t::lib::Mocks::mock_preference('ConfirmFutureHolds', 0);
382 (my $doreturn, $messages)= AddReturn('97531',$branch_1);
383 is($messages->{ResFound}//'', '', 'AddReturn does not care about future reserve when ConfirmFutureHolds is off');
384 t::lib::Mocks::mock_preference('ConfirmFutureHolds', 3);
385 ($doreturn, $messages)= AddReturn('97531',$branch_1);
386 is(exists $messages->{ResFound}?1:0, 0, 'AddReturn ignores future reserve beyond ConfirmFutureHolds days');
387 t::lib::Mocks::mock_preference('ConfirmFutureHolds', 7);
388 ($doreturn, $messages)= AddReturn('97531',$branch_1);
389 is(exists $messages->{ResFound}?1:0, 1, 'AddReturn considers future reserve within ConfirmFutureHolds days');
390
391 # End of tests for bug 9761 (ConfirmFutureHolds)
392
393 # test marking a hold as captured
394 my $hold_notice_count = count_hold_print_messages();
395 ModReserveAffect($item->itemnumber, $requesters{$branch_1}, 0);
396 my $new_count = count_hold_print_messages();
397 is($new_count, $hold_notice_count + 1, 'patron notified when item set to waiting');
398
399 # test that duplicate notices aren't generated
400 ModReserveAffect($item->itemnumber, $requesters{$branch_1}, 0);
401 $new_count = count_hold_print_messages();
402 is($new_count, $hold_notice_count + 1, 'patron not notified a second time (bug 11445)');
403
404 # avoiding the not_same_branch error
405 t::lib::Mocks::mock_preference('IndependentBranches', 0);
406 $item = Koha::Items->find($item->itemnumber);
407 is(
408     $item->safe_delete,
409     'book_reserved',
410     'item that is captured to fill a hold cannot be deleted',
411 );
412
413 my $letter = ReserveSlip( { branchcode => $branch_1, reserve_id => $reserve_id } );
414 ok(defined($letter), 'can successfully generate hold slip (bug 10949)');
415
416 # Tests for bug 9788: Does Koha::Item->current_holds return a future wait?
417 # 9788a: current_holds does not return future next available hold
418 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum));
419 t::lib::Mocks::mock_preference('ConfirmFutureHolds', 2);
420 t::lib::Mocks::mock_preference('AllowHoldDateInFuture', 1);
421 $resdate= dt_from_string();
422 $resdate->add_duration(DateTime::Duration->new(days => 2));
423 $resdate=output_pref($resdate);
424 AddReserve(
425     {
426         branchcode       => $branch_1,
427         borrowernumber   => $requesters{$branch_1},
428         biblionumber     => $bibnum,
429         priority         => 1,
430         reservation_date => $resdate,
431     }
432 );
433
434 $holds = $item->current_holds;
435 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
436 my $future_holds = $holds->search({ reservedate => { '>' => $dtf->format_date( dt_from_string ) } } );
437 is( $future_holds->count, 0, 'current_holds does not return a future next available hold');
438 # 9788b: current_holds does not return future item level hold
439 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum));
440 AddReserve(
441     {
442         branchcode       => $branch_1,
443         borrowernumber   => $requesters{$branch_1},
444         biblionumber     => $bibnum,
445         priority         => 1,
446         reservation_date => $resdate,
447         itemnumber       => $item->itemnumber,
448     }
449 ); #item level hold
450 $future_holds = $holds->search({ reservedate => { '>' => $dtf->format_date( dt_from_string ) } } );
451 is( $future_holds->count, 0, 'current_holds does not return a future item level hold' );
452 # 9788c: current_holds returns future wait (confirmed future hold)
453 ModReserveAffect( $item->itemnumber,  $requesters{$branch_1} , 0); #confirm hold
454 $future_holds = $holds->search({ reservedate => { '>' => $dtf->format_date( dt_from_string ) } } );
455 is( $future_holds->count, 1, 'current_holds returns a future wait (confirmed future hold)' );
456 # End of tests for bug 9788
457
458 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum));
459 # Tests for CalculatePriority (bug 8918)
460 my $p = C4::Reserves::CalculatePriority($bibnum2);
461 is($p, 4, 'CalculatePriority should now return priority 4');
462 AddReserve(
463     {
464         branchcode     => $branch_1,
465         borrowernumber => $requesters{'CPL2'},
466         biblionumber   => $bibnum2,
467         priority       => $p,
468     }
469 );
470 $p = C4::Reserves::CalculatePriority($bibnum2);
471 is($p, 5, 'CalculatePriority should now return priority 5');
472 #some tests on bibnum
473 $dbh->do("DELETE FROM reserves WHERE biblionumber=?",undef,($bibnum));
474 $p = C4::Reserves::CalculatePriority($bibnum);
475 is($p, 1, 'CalculatePriority should now return priority 1');
476 #add a new reserve and confirm it to waiting
477 AddReserve(
478     {
479         branchcode     => $branch_1,
480         borrowernumber => $requesters{$branch_1},
481         biblionumber   => $bibnum,
482         priority       => $p,
483         itemnumber     => $item->itemnumber,
484     }
485 );
486 $p = C4::Reserves::CalculatePriority($bibnum);
487 is($p, 2, 'CalculatePriority should now return priority 2');
488 ModReserveAffect( $item->itemnumber,  $requesters{$branch_1} , 0);
489 $p = C4::Reserves::CalculatePriority($bibnum);
490 is($p, 1, 'CalculatePriority should now return priority 1');
491 #add another biblio hold, no resdate
492 AddReserve(
493     {
494         branchcode     => $branch_1,
495         borrowernumber => $requesters{'CPL2'},
496         biblionumber   => $bibnum,
497         priority       => $p,
498     }
499 );
500 $p = C4::Reserves::CalculatePriority($bibnum);
501 is($p, 2, 'CalculatePriority should now return priority 2');
502 #add another future hold
503 t::lib::Mocks::mock_preference('AllowHoldDateInFuture', 1);
504 $resdate= dt_from_string();
505 $resdate->add_duration(DateTime::Duration->new(days => 1));
506 AddReserve(
507     {
508         branchcode     => $branch_1,
509         borrowernumber => $requesters{'CPL2'},
510         biblionumber   => $bibnum,
511         priority       => $p,
512         reservation_date => output_pref($resdate),
513     }
514 );
515 $p = C4::Reserves::CalculatePriority($bibnum);
516 is($p, 2, 'CalculatePriority should now still return priority 2');
517 #calc priority with future resdate
518 $p = C4::Reserves::CalculatePriority($bibnum, $resdate);
519 is($p, 3, 'CalculatePriority should now return priority 3');
520 # End of tests for bug 8918
521
522 # Tests for cancel reserves by users from OPAC.
523 $dbh->do('DELETE FROM reserves', undef, ($bibnum));
524 AddReserve(
525     {
526         branchcode     => $branch_1,
527         borrowernumber => $requesters{$branch_1},
528         biblionumber   => $bibnum,
529         priority       => 1,
530     }
531 );
532 my (undef, $canres, undef) = CheckReserves($item->itemnumber);
533
534 is( CanReserveBeCanceledFromOpac(), undef,
535     'CanReserveBeCanceledFromOpac should return undef if called without any parameter'
536 );
537 is(
538     CanReserveBeCanceledFromOpac( $canres->{resserve_id} ),
539     undef,
540     'CanReserveBeCanceledFromOpac should return undef if called without the reserve_id'
541 );
542 is(
543     CanReserveBeCanceledFromOpac( undef, $requesters{CPL} ),
544     undef,
545     'CanReserveBeCanceledFromOpac should return undef if called without borrowernumber'
546 );
547
548 my $cancancel = CanReserveBeCanceledFromOpac($canres->{reserve_id}, $requesters{$branch_1});
549 is($cancancel, 1, 'Can user cancel its own reserve');
550
551 $cancancel = CanReserveBeCanceledFromOpac($canres->{reserve_id}, $requesters{$branch_2});
552 is($cancancel, 0, 'Other user cant cancel reserve');
553
554 ModReserveAffect($item->itemnumber, $requesters{$branch_1}, 1);
555 $cancancel = CanReserveBeCanceledFromOpac($canres->{reserve_id}, $requesters{$branch_1});
556 is($cancancel, 0, 'Reserve in transfer status cant be canceled');
557
558 $dbh->do('DELETE FROM reserves', undef, ($bibnum));
559 AddReserve(
560     {
561         branchcode     => $branch_1,
562         borrowernumber => $requesters{$branch_1},
563         biblionumber   => $bibnum,
564         priority       => 1,
565     }
566 );
567 (undef, $canres, undef) = CheckReserves($item->itemnumber);
568
569 ModReserveAffect($item->itemnumber, $requesters{$branch_1}, 0);
570 $cancancel = CanReserveBeCanceledFromOpac($canres->{reserve_id}, $requesters{$branch_1});
571 is($cancancel, 0, 'Reserve in waiting status cant be canceled');
572
573 # End of tests for bug 12876
574
575        ####
576 ####### Testing Bug 13113 - Prevent juvenile/children from reserving ageRestricted material >>>
577        ####
578
579 t::lib::Mocks::mock_preference( 'AgeRestrictionMarker', 'FSK|PEGI|Age|K' );
580
581 #Reserving an not-agerestricted Biblio by a Borrower with no dateofbirth is tested previously.
582
583 #Set the ageRestriction for the Biblio
584 my $record = GetMarcBiblio({ biblionumber =>  $bibnum });
585 my ( $ageres_tagid, $ageres_subfieldid ) = GetMarcFromKohaField( "biblioitems.agerestriction" );
586 $record->append_fields(  MARC::Field->new($ageres_tagid, '', '', $ageres_subfieldid => 'PEGI 16')  );
587 C4::Biblio::ModBiblio( $record, $bibnum, $frameworkcode );
588
589 is( C4::Reserves::CanBookBeReserved($borrowernumber, $biblionumber)->{status} , 'OK', "Reserving an ageRestricted Biblio without a borrower dateofbirth succeeds" );
590
591 #Set the dateofbirth for the Borrower making them "too young".
592 $borrower->{dateofbirth} = DateTime->now->add( years => -15 );
593 Koha::Patrons->find( $borrowernumber )->set({ dateofbirth => $borrower->{dateofbirth} })->store;
594
595 is( C4::Reserves::CanBookBeReserved($borrowernumber, $biblionumber)->{status} , 'ageRestricted', "Reserving a 'PEGI 16' Biblio by a 15 year old borrower fails");
596
597 #Set the dateofbirth for the Borrower making them "too old".
598 $borrower->{dateofbirth} = DateTime->now->add( years => -30 );
599 Koha::Patrons->find( $borrowernumber )->set({ dateofbirth => $borrower->{dateofbirth} })->store;
600
601 is( C4::Reserves::CanBookBeReserved($borrowernumber, $biblionumber)->{status} , 'OK', "Reserving a 'PEGI 16' Biblio by a 30 year old borrower succeeds");
602
603 is( C4::Reserves::CanBookBeReserved($borrowernumber, $biblio_with_no_item->biblionumber)->{status} , '', "Biblio with no item. Status is empty");
604        ####
605 ####### EO Bug 13113 <<<
606        ####
607
608 ok( C4::Reserves::IsAvailableForItemLevelRequest($item, $patron), "Reserving a book on item level" );
609
610 my $pickup_branch = $builder->build({ source => 'Branch' })->{ branchcode };
611 t::lib::Mocks::mock_preference( 'UseBranchTransferLimits',  '1' );
612 t::lib::Mocks::mock_preference( 'BranchTransferLimitsType', 'itemtype' );
613 my $limit = Koha::Item::Transfer::Limit->new(
614     {
615         toBranch   => $pickup_branch,
616         fromBranch => $item->holdingbranch,
617         itemtype   => $item->effective_itemtype,
618     }
619 )->store();
620 is( C4::Reserves::IsAvailableForItemLevelRequest($item, $patron, $pickup_branch), 0, "Item level request not available due to transfer limit" );
621 t::lib::Mocks::mock_preference( 'UseBranchTransferLimits',  '0' );
622
623 my $categorycode = $borrower->{categorycode};
624 my $holdingbranch = $item->{holdingbranch};
625 Koha::CirculationRules->set_rules(
626     {
627         categorycode => $categorycode,
628         itemtype     => $item->effective_itemtype,
629         branchcode   => $holdingbranch,
630         rules => {
631             onshelfholds => 1,
632         }
633     }
634 );
635
636 # tests for MoveReserve in relation to ConfirmFutureHolds (BZ 14526)
637 #   hold from A pos 1, today, no fut holds: MoveReserve should fill it
638 $dbh->do('DELETE FROM reserves', undef, ($bibnum));
639 t::lib::Mocks::mock_preference('ConfirmFutureHolds', 0);
640 t::lib::Mocks::mock_preference('AllowHoldDateInFuture', 1);
641 AddReserve(
642     {
643         branchcode     => $branch_1,
644         borrowernumber => $borrowernumber,
645         biblionumber   => $bibnum,
646         priority       => 1,
647     }
648 );
649 MoveReserve( $item->itemnumber, $borrowernumber );
650 ($status)=CheckReserves( $item->itemnumber );
651 is( $status, '', 'MoveReserve filled hold');
652 #   hold from A waiting, today, no fut holds: MoveReserve should fill it
653 AddReserve(
654     {
655         branchcode     => $branch_1,
656         borrowernumber => $borrowernumber,
657         biblionumber   => $bibnum,
658         priority       => 1,
659         found          => 'W',
660     }
661 );
662 MoveReserve( $item->itemnumber, $borrowernumber );
663 ($status)=CheckReserves( $item->itemnumber );
664 is( $status, '', 'MoveReserve filled waiting hold');
665 #   hold from A pos 1, tomorrow, no fut holds: not filled
666 $resdate= dt_from_string();
667 $resdate->add_duration(DateTime::Duration->new(days => 1));
668 $resdate=output_pref($resdate);
669 AddReserve(
670     {
671         branchcode     => $branch_1,
672         borrowernumber => $borrowernumber,
673         biblionumber   => $bibnum,
674         priority       => 1,
675         reservation_date => $resdate,
676     }
677 );
678 MoveReserve( $item->itemnumber, $borrowernumber );
679 ($status)=CheckReserves( $item->itemnumber, undef, 1 );
680 is( $status, 'Reserved', 'MoveReserve did not fill future hold');
681 $dbh->do('DELETE FROM reserves', undef, ($bibnum));
682 #   hold from A pos 1, tomorrow, fut holds=2: MoveReserve should fill it
683 t::lib::Mocks::mock_preference('ConfirmFutureHolds', 2);
684 AddReserve(
685     {
686         branchcode     => $branch_1,
687         borrowernumber => $borrowernumber,
688         biblionumber   => $bibnum,
689         priority       => 1,
690         reservation_date => $resdate,
691     }
692 );
693 MoveReserve( $item->itemnumber, $borrowernumber );
694 ($status)=CheckReserves( $item->itemnumber, undef, 2 );
695 is( $status, '', 'MoveReserve filled future hold now');
696 #   hold from A waiting, tomorrow, fut holds=2: MoveReserve should fill it
697 AddReserve(
698     {
699         branchcode     => $branch_1,
700         borrowernumber => $borrowernumber,
701         biblionumber   => $bibnum,
702         priority       => 1,
703         reservation_date => $resdate,
704     }
705 );
706 MoveReserve( $item->itemnumber, $borrowernumber );
707 ($status)=CheckReserves( $item->itemnumber, undef, 2 );
708 is( $status, '', 'MoveReserve filled future waiting hold now');
709 #   hold from A pos 1, today+3, fut holds=2: MoveReserve should not fill it
710 $resdate= dt_from_string();
711 $resdate->add_duration(DateTime::Duration->new(days => 3));
712 $resdate=output_pref($resdate);
713 AddReserve(
714     {
715         branchcode     => $branch_1,
716         borrowernumber => $borrowernumber,
717         biblionumber   => $bibnum,
718         priority       => 1,
719         reservation_date => $resdate,
720     }
721 );
722 MoveReserve( $item->itemnumber, $borrowernumber );
723 ($status)=CheckReserves( $item->itemnumber, undef, 3 );
724 is( $status, 'Reserved', 'MoveReserve did not fill future hold of 3 days');
725 $dbh->do('DELETE FROM reserves', undef, ($bibnum));
726
727 $cache->clear_from_cache("MarcStructure-0-$frameworkcode");
728 $cache->clear_from_cache("MarcStructure-1-$frameworkcode");
729 $cache->clear_from_cache("default_value_for_mod_marc-");
730 $cache->clear_from_cache("MarcSubfieldStructure-$frameworkcode");
731
732 subtest '_koha_notify_reserve() tests' => sub {
733
734     plan tests => 2;
735
736     my $wants_hold_and_email = {
737         wants_digest => '0',
738         transports => {
739             sms => 'HOLD',
740             email => 'HOLD',
741             },
742         letter_code => 'HOLD'
743     };
744
745     my $mp = Test::MockModule->new( 'C4::Members::Messaging' );
746
747     $mp->mock("GetMessagingPreferences",$wants_hold_and_email);
748
749     $dbh->do('DELETE FROM letter');
750
751     my $email_hold_notice = $builder->build({
752             source => 'Letter',
753             value => {
754                 message_transport_type => 'email',
755                 branchcode => '',
756                 code => 'HOLD',
757                 module => 'reserves',
758                 lang => 'default',
759             }
760         });
761
762     my $sms_hold_notice = $builder->build({
763             source => 'Letter',
764             value => {
765                 message_transport_type => 'sms',
766                 branchcode => '',
767                 code => 'HOLD',
768                 module => 'reserves',
769                 lang=>'default',
770             }
771         });
772
773     my $hold_borrower = $builder->build({
774             source => 'Borrower',
775             value => {
776                 smsalertnumber=>'5555555555',
777                 email=>'a@b.com',
778             }
779         })->{borrowernumber};
780
781     C4::Reserves::AddReserve(
782         {
783             branchcode     => $item->homebranch,
784             borrowernumber => $hold_borrower,
785             biblionumber   => $item->biblionumber,
786         }
787     );
788
789     ModReserveAffect($item->itemnumber, $hold_borrower, 0);
790     my $sms_message_address = $schema->resultset('MessageQueue')->search({
791             letter_code     => 'HOLD',
792             message_transport_type => 'sms',
793             borrowernumber => $hold_borrower,
794         })->next()->to_address();
795     is($sms_message_address, undef ,"We should not populate the sms message with the sms number, sending will do so");
796
797     my $email_message_address = $schema->resultset('MessageQueue')->search({
798             letter_code     => 'HOLD',
799             message_transport_type => 'email',
800             borrowernumber => $hold_borrower,
801         })->next()->to_address();
802     is($email_message_address, undef ,"We should not populate the hold message with the email address, sending will do so");
803
804 };
805
806 subtest 'ReservesNeedReturns' => sub {
807     plan tests => 18;
808
809     my $library    = $builder->build_object( { class => 'Koha::Libraries' } );
810     my $item_info  = {
811         homebranch       => $library->branchcode,
812         holdingbranch    => $library->branchcode,
813     };
814     my $item = $builder->build_sample_item($item_info);
815     my $patron   = $builder->build_object(
816         {
817             class => 'Koha::Patrons',
818             value => { branchcode => $library->branchcode, }
819         }
820     );
821     my $patron_2   = $builder->build_object(
822         {
823             class => 'Koha::Patrons',
824             value => { branchcode => $library->branchcode, }
825         }
826     );
827
828     my $priority = 1;
829
830     t::lib::Mocks::mock_preference('ReservesNeedReturns', 1); # Test with feature disabled
831     my $hold = place_item_hold( $patron, $item, $library, $priority );
832     is( $hold->priority, $priority, 'If ReservesNeedReturns is 1, priority must not have been set to changed' );
833     is( $hold->found, undef, 'If ReservesNeedReturns is 1, found must not have been set waiting' );
834     $hold->delete;
835
836     t::lib::Mocks::mock_preference('ReservesNeedReturns', 0); # '0' means 'Automatically mark a hold as found and waiting'
837     $hold = place_item_hold( $patron, $item, $library, $priority );
838     is( $hold->priority, 0, 'If ReservesNeedReturns is 0 and no other status, priority must have been set to 0' );
839     is( $hold->found, 'W', 'If ReservesNeedReturns is 0 and no other status, found must have been set waiting' );
840     $hold->delete;
841
842     $item->onloan('2010-01-01')->store;
843     $hold = place_item_hold( $patron, $item, $library, $priority );
844     is( $hold->priority, 1, 'If ReservesNeedReturns is 0 but item onloan priority must be set to 1' );
845     $hold->delete;
846
847     t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0); # '0' means damaged holds not allowed
848     $item->onloan(undef)->damaged(1)->store;
849     $hold = place_item_hold( $patron, $item, $library, $priority );
850     is( $hold->priority, 1, 'If ReservesNeedReturns is 0 but item damaged and not allowed holds on damaged items priority must be set to 1' );
851     $hold->delete;
852     t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 1); # '0' means damaged holds not allowed
853     $hold = place_item_hold( $patron, $item, $library, $priority );
854     is( $hold->priority, 0, 'If ReservesNeedReturns is 0 and damaged holds allowed, priority must have been set to 0' );
855     is( $hold->found,  'W', 'If ReservesNeedReturns is 0 and damaged holds allowed, found must have been set waiting' );
856     $hold->delete;
857
858     my $hold_1 = place_item_hold( $patron, $item, $library, $priority );
859     is( $hold_1->found,  'W', 'First hold on item is set to waiting with ReservesNeedReturns set to 0' );
860     is( $hold_1->priority, 0, 'First hold on item is set to waiting with ReservesNeedReturns set to 0' );
861     $hold = place_item_hold( $patron_2, $item, $library, $priority );
862     is( $hold->priority, 1, 'If ReservesNeedReturns is 0 but item already on hold priority must be set to 1' );
863     $hold->delete;
864     $hold_1->delete;
865
866     my $transfer = $builder->build_object({
867         class => "Koha::Item::Transfers",
868         value => {
869           itemnumber  => $item->itemnumber,
870           datearrived => undef,
871           datecancelled => undef
872         }
873     });
874     $item->damaged(0)->store;
875     $hold = place_item_hold( $patron, $item, $library, $priority );
876     is( $hold->found, undef, 'If ReservesNeedReturns is 0 but item in transit the hold must not be set to waiting' );
877     is( $hold->priority, 1,  'If ReservesNeedReturns is 0 but item in transit the hold must not be set to waiting' );
878     $hold->delete;
879     $transfer->delete;
880
881     $hold = place_item_hold( $patron, $item, $library, $priority );
882     is( $hold->priority, 0, 'If ReservesNeedReturns is 0 and no other status, priority must have been set to 0' );
883     is( $hold->found, 'W', 'If ReservesNeedReturns is 0 and no other status, found must have been set waiting' );
884     $hold_1 = place_item_hold( $patron, $item, $library, $priority );
885     is( $hold_1->priority, 1, 'If ReservesNeedReturns is 0 but item has a hold priority is 1' );
886     $hold_1->suspend(1)->store; # We suspend the hold
887     $hold->delete; # Delete the waiting hold
888     $hold = place_item_hold( $patron, $item, $library, $priority );
889     is( $hold->priority, 0, 'If ReservesNeedReturns is 0 and other hold(s) suspended, priority must have been set to 0' );
890     is( $hold->found, 'W', 'If ReservesNeedReturns is 0 and other  hold(s) suspended, found must have been set waiting' );
891
892
893
894
895     t::lib::Mocks::mock_preference('ReservesNeedReturns', 1); # Don't affect other tests
896 };
897
898 subtest 'ChargeReserveFee tests' => sub {
899
900     plan tests => 8;
901
902     my $library = $builder->build_object({ class => 'Koha::Libraries' });
903     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
904
905     my $fee   = 20;
906     my $title = 'A title';
907
908     my $context = Test::MockModule->new('C4::Context');
909     $context->mock( userenv => { branch => $library->id } );
910
911     my $line = C4::Reserves::ChargeReserveFee( $patron->id, $fee, $title );
912
913     is( ref($line), 'Koha::Account::Line' , 'Returns a Koha::Account::Line object');
914     ok( $line->is_debit, 'Generates a debit line' );
915     is( $line->debit_type_code, 'RESERVE' , 'generates RESERVE debit_type');
916     is( $line->borrowernumber, $patron->id , 'generated line belongs to the passed patron');
917     is( $line->amount, $fee , 'amount set correctly');
918     is( $line->amountoutstanding, $fee , 'amountoutstanding set correctly');
919     is( $line->description, "$title" , 'description is title of reserved item');
920     is( $line->branchcode, $library->id , "Library id is picked from userenv and stored correctly" );
921 };
922
923 subtest 'reserves.item_level_hold' => sub {
924     plan tests => 2;
925
926     my $item   = $builder->build_sample_item;
927     my $patron = $builder->build_object(
928         {
929             class => 'Koha::Patrons',
930             value => { branchcode => $item->homebranch }
931         }
932     );
933
934     subtest 'item level hold' => sub {
935         plan tests => 2;
936         my $reserve_id = AddReserve(
937             {
938                 branchcode     => $item->homebranch,
939                 borrowernumber => $patron->borrowernumber,
940                 biblionumber   => $item->biblionumber,
941                 priority       => 1,
942                 itemnumber     => $item->itemnumber,
943             }
944         );
945
946         my $hold = Koha::Holds->find($reserve_id);
947         is( $hold->item_level_hold, 1, 'item_level_hold should be set when AddReserve is called with a specific item' );
948
949         # Mark it waiting
950         ModReserveAffect( $item->itemnumber, $patron->borrowernumber, 1 );
951
952         # Revert the waiting status
953         C4::Reserves::RevertWaitingStatus(
954             { itemnumber => $item->itemnumber } );
955
956         $hold = Koha::Holds->find($reserve_id);
957
958         is( $hold->itemnumber, $item->itemnumber, 'Itemnumber should not be removed when the waiting status is revert' );
959
960         $hold->delete;    # cleanup
961     };
962
963     subtest 'biblio level hold' => sub {
964         plan tests => 3;
965         my $reserve_id = AddReserve(
966             {
967                 branchcode     => $item->homebranch,
968                 borrowernumber => $patron->borrowernumber,
969                 biblionumber   => $item->biblionumber,
970                 priority       => 1,
971             }
972         );
973
974         my $hold = Koha::Holds->find($reserve_id);
975         is( $hold->item_level_hold, 0, 'item_level_hold should not be set when AddReserve is called without a specific item' );
976
977         # Mark it waiting
978         ModReserveAffect( $item->itemnumber, $patron->borrowernumber, 1 );
979
980         $hold = Koha::Holds->find($reserve_id);
981         is( $hold->itemnumber, $item->itemnumber, 'Itemnumber should be set on hold confirmation' );
982
983         # Revert the waiting status
984         C4::Reserves::RevertWaitingStatus( { itemnumber => $item->itemnumber } );
985
986         $hold = Koha::Holds->find($reserve_id);
987         is( $hold->itemnumber, undef, 'Itemnumber should be removed when the waiting status is revert' );
988
989         $hold->delete;
990     };
991 };
992
993 subtest 'OnShelfHoldAllowed test' => sub {
994     plan tests => 3;
995     $dbh->do('DELETE FROM circulation_rules');
996     my $biblio = $builder->build_sample_biblio({frameworkcode => $frameworkcode})->biblionumber;
997
998     # Create a helper item instance for testing
999     my $item = $builder->build_sample_item({ biblionumber => $biblio, library => $branch_1, itype => $itemtype });
1000
1001     Koha::CirculationRules->set_rules(
1002         {
1003             branchcode   => undef,
1004             categorycode => undef,
1005             itemtype     => undef,
1006             rules        => {
1007                 reservesallowed => 25,
1008                 opacitemholds => 'Y',
1009                 onshelfholds => 1,
1010             }
1011         }
1012         );
1013
1014     my $canreserve = C4::Reserves::CanItemBeReserved(
1015         $patron->borrowernumber,
1016         $item->itemnumber,
1017         );
1018
1019     is( $canreserve->{status}, 'OK',
1020         'item-level holds should be possible with onshelfholdallowed set to "Yes"' );
1021
1022     Koha::CirculationRules->set_rules(
1023         {
1024             branchcode   => undef,
1025             categorycode => undef,
1026             itemtype     => undef,
1027             rules        => {
1028                 reservesallowed => 25,
1029                 opacitemholds => 'Y',
1030                 onshelfholds => '0',
1031             }
1032         });
1033
1034     $canreserve = C4::Reserves::CanItemBeReserved(
1035         $patron->borrowernumber,
1036         $item->itemnumber,
1037         );
1038
1039     is( $canreserve->{status}, 'onShelfHoldsNotAllowed',
1040         'item-level holds should not be possible with onshelfholdallowed set to "If any unavailable"' );
1041
1042     Koha::CirculationRules->set_rules(
1043         {
1044             branchcode   => undef,
1045             categorycode => undef,
1046             itemtype     => undef,
1047             rules        => {
1048                 reservesallowed => 25,
1049                 opacitemholds => 'Y',
1050                 onshelfholds => '2',
1051             }
1052         });
1053
1054     $canreserve = C4::Reserves::CanItemBeReserved(
1055         $patron->borrowernumber,
1056         $item->itemnumber,
1057         );
1058
1059     is( $canreserve->{status}, 'onShelfHoldsNotAllowed',
1060         'item-level holds should not be possible with onshelfholdallowed set to "If all unavailable"' );
1061 };
1062
1063 subtest 'MoveReserve additional test' => sub {
1064
1065     plan tests => 4;
1066
1067     # Create the items and patrons we need
1068     my $biblio = $builder->build_sample_biblio();
1069     my $itype = $builder->build_object({ class => "Koha::ItemTypes", value => { notforloan => 0 } });
1070     my $item_1 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber,notforloan => 0, itype => $itype->itemtype });
1071     my $item_2 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber, notforloan => 0, itype => $itype->itemtype });
1072     my $patron_1 = $builder->build_object({ class => "Koha::Patrons" });
1073     my $patron_2 = $builder->build_object({ class => "Koha::Patrons" });
1074
1075     # Place a hold on the title for both patrons
1076     my $reserve_1 = AddReserve(
1077         {
1078             branchcode     => $item_1->homebranch,
1079             borrowernumber => $patron_1->borrowernumber,
1080             biblionumber   => $biblio->biblionumber,
1081             priority       => 1,
1082             itemnumber     => $item_1->itemnumber,
1083         }
1084     );
1085     my $reserve_2 = AddReserve(
1086         {
1087             branchcode     => $item_2->homebranch,
1088             borrowernumber => $patron_2->borrowernumber,
1089             biblionumber   => $biblio->biblionumber,
1090             priority       => 1,
1091             itemnumber     => $item_1->itemnumber,
1092         }
1093     );
1094     is($patron_1->holds->next()->reserve_id, $reserve_1, "The 1st patron has a hold");
1095     is($patron_2->holds->next()->reserve_id, $reserve_2, "The 2nd patron has a hold");
1096
1097     # Fake the holds queue
1098     $dbh->do(q{INSERT INTO hold_fill_targets VALUES (?, ?, ?, ?, ?,?)},undef,($patron_1->borrowernumber,$biblio->biblionumber,$item_1->itemnumber,$item_1->homebranch,0,$reserve_1));
1099
1100     # The 2nd hold should be filed even if the item is preselected for the first hold
1101     MoveReserve($item_1->itemnumber,$patron_2->borrowernumber);
1102     is($patron_2->holds->count, 0, "The 2nd patrons no longer has a hold");
1103     is($patron_2->old_holds->next()->reserve_id, $reserve_2, "The 2nd patrons hold was filled and moved to old holds");
1104
1105 };
1106
1107 subtest 'RevertWaitingStatus' => sub {
1108
1109     plan tests => 2;
1110
1111     # Create the items and patrons we need
1112     my $biblio  = $builder->build_sample_biblio();
1113     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1114     my $itype   = $builder->build_object(
1115         { class => "Koha::ItemTypes", value => { notforloan => 0 } } );
1116     my $item_1 = $builder->build_sample_item(
1117         {
1118             biblionumber => $biblio->biblionumber,
1119             itype        => $itype->itemtype,
1120             library      => $library->branchcode
1121         }
1122     );
1123     my $patron_1 = $builder->build_object( { class => "Koha::Patrons" } );
1124     my $patron_2 = $builder->build_object( { class => "Koha::Patrons" } );
1125     my $patron_3 = $builder->build_object( { class => "Koha::Patrons" } );
1126     my $patron_4 = $builder->build_object( { class => "Koha::Patrons" } );
1127
1128     # Place a hold on the title for both patrons
1129     my $priority = 1;
1130     my $hold_1 = place_item_hold( $patron_1, $item_1, $library, $priority );
1131     my $hold_2 = place_item_hold( $patron_2, $item_1, $library, $priority );
1132     my $hold_3 = place_item_hold( $patron_3, $item_1, $library, $priority );
1133     my $hold_4 = place_item_hold( $patron_4, $item_1, $library, $priority );
1134
1135     $hold_1->set_waiting;
1136     AddIssue( $patron_3->unblessed, $item_1->barcode, undef, 'revert' );
1137
1138     my $holds = $biblio->holds;
1139     is( $holds->count, 3, 'One hold has been deleted' );
1140     is_deeply(
1141         [
1142             $holds->next->priority, $holds->next->priority,
1143             $holds->next->priority
1144         ],
1145         [ 1, 2, 3 ],
1146         'priorities have been reordered'
1147     );
1148 };
1149
1150 subtest 'CheckReserves additional tests' => sub {
1151
1152     plan tests => 8;
1153
1154     my $item = $builder->build_sample_item;
1155     my $reserve1 = $builder->build_object(
1156         {
1157             class => "Koha::Holds",
1158             value => {
1159                 found            => undef,
1160                 priority         => 1,
1161                 itemnumber       => undef,
1162                 biblionumber     => $item->biblionumber,
1163                 waitingdate      => undef,
1164                 cancellationdate => undef,
1165                 item_level_hold  => 0,
1166                 lowestPriority   => 0,
1167                 expirationdate   => undef,
1168                 suspend_until    => undef,
1169                 suspend          => 0,
1170                 itemtype         => undef,
1171             }
1172         }
1173     );
1174     my $reserve2 = $builder->build_object(
1175         {
1176             class => "Koha::Holds",
1177             value => {
1178                 found            => undef,
1179                 priority         => 2,
1180                 biblionumber     => $item->biblionumber,
1181                 borrowernumber   => $reserve1->borrowernumber,
1182                 itemnumber       => undef,
1183                 waitingdate      => undef,
1184                 cancellationdate => undef,
1185                 item_level_hold  => 0,
1186                 lowestPriority   => 0,
1187                 expirationdate   => undef,
1188                 suspend_until    => undef,
1189                 suspend          => 0,
1190                 itemtype         => undef,
1191             }
1192         }
1193     );
1194
1195     my $tmp_holdsqueue = $builder->build(
1196         {
1197             source => 'TmpHoldsqueue',
1198             value  => {
1199                 borrowernumber => $reserve1->borrowernumber,
1200                 biblionumber   => $reserve1->biblionumber,
1201             }
1202         }
1203     );
1204     my $fill_target = $builder->build(
1205         {
1206             source => 'HoldFillTarget',
1207             value  => {
1208                 borrowernumber     => $reserve1->borrowernumber,
1209                 biblionumber       => $reserve1->biblionumber,
1210                 itemnumber         => $item->itemnumber,
1211                 item_level_request => 0,
1212             }
1213         }
1214     );
1215
1216     ModReserveAffect( $item->itemnumber, $reserve1->borrowernumber, 1,
1217         $reserve1->reserve_id );
1218     my ( $status, $matched_reserve, $possible_reserves ) =
1219       CheckReserves( $item->itemnumber );
1220
1221     is( $status, 'Transferred', "We found a reserve" );
1222     is( $matched_reserve->{reserve_id},
1223         $reserve1->reserve_id, "We got the Transit reserve" );
1224     is( scalar @$possible_reserves, 2, 'We do get both reserves' );
1225
1226     my $patron_B = $builder->build_object({ class => "Koha::Patrons" });
1227     my $item_A = $builder->build_sample_item;
1228     my $item_B = $builder->build_sample_item({
1229         homebranch => $patron_B->branchcode,
1230         biblionumber => $item_A->biblionumber,
1231         itype => $item_A->itype
1232     });
1233     Koha::CirculationRules->set_rules(
1234         {
1235             branchcode   => undef,
1236             categorycode => undef,
1237             itemtype     => $item_A->itype,
1238             rules        => {
1239                 reservesallowed => 25,
1240                 holds_per_record => 1,
1241             }
1242         }
1243     );
1244     Koha::CirculationRules->set_rule({
1245         branchcode => undef,
1246         itemtype   => $item_A->itype,
1247         rule_name  => 'holdallowed',
1248         rule_value => 'from_home_library'
1249     });
1250     my $reserve_id = AddReserve(
1251         {
1252             branchcode     => $patron_B->branchcode,
1253             borrowernumber => $patron_B->borrowernumber,
1254             biblionumber   => $item_A->biblionumber,
1255             priority       => 1,
1256             itemnumber     => undef,
1257         }
1258     );
1259
1260     ok( $reserve_id, "We can place a record level hold because one item is owned by patron's home library");
1261     t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary');
1262     ( $status, $matched_reserve, $possible_reserves ) = CheckReserves( $item_A->itemnumber );
1263     is( $status, "", "We do not fill the hold with item A because it is not from the patron's homebranch");
1264     Koha::CirculationRules->set_rule({
1265         branchcode => $item_A->homebranch,
1266         itemtype   => $item_A->itype,
1267         rule_name  => 'holdallowed',
1268         rule_value => 'from_any_library'
1269     });
1270     ( $status, $matched_reserve, $possible_reserves ) = CheckReserves( $item_A->itemnumber );
1271     is( $status, "Reserved", "We fill the hold with item A because item's branch rule says allow any");
1272
1273
1274     # Changing the control branch should change only the rule we get
1275     t::lib::Mocks::mock_preference('ReservesControlBranch', 'PatronLibrary');
1276     ( $status, $matched_reserve, $possible_reserves ) = CheckReserves( $item_A->itemnumber );
1277     is( $status, "", "We do not fill the hold with item A because it is not from the patron's homebranch");
1278     Koha::CirculationRules->set_rule({
1279         branchcode   => $patron_B->branchcode,
1280         itemtype   => $item_A->itype,
1281         rule_name  => 'holdallowed',
1282         rule_value => 'from_any_library'
1283     });
1284     ( $status, $matched_reserve, $possible_reserves ) = CheckReserves( $item_A->itemnumber );
1285     is( $status, "Reserved", "We fill the hold with item A because patron's branch rule says allow any");
1286
1287 };
1288
1289 subtest 'AllowHoldOnPatronPossession test' => sub {
1290
1291     plan tests => 4;
1292
1293     # Create the items and patrons we need
1294     my $biblio = $builder->build_sample_biblio();
1295     my $itype = $builder->build_object({ class => "Koha::ItemTypes", value => { notforloan => 0 } });
1296     my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber,notforloan => 0, itype => $itype->itemtype });
1297     my $patron = $builder->build_object({ class => "Koha::Patrons",
1298                                           value => { branchcode => $item->homebranch }});
1299
1300     Koha::CirculationRules->set_rules(
1301         {
1302             branchcode   => undef,
1303             categorycode => undef,
1304             itemtype     => undef,
1305             rules        => {
1306                 onshelfholds => 1,
1307             }
1308         }
1309     );
1310
1311     C4::Circulation::AddIssue($patron->unblessed,
1312                               $item->barcode);
1313     t::lib::Mocks::mock_preference('AllowHoldsOnPatronsPossessions', 0);
1314
1315     is(C4::Reserves::CanBookBeReserved($patron->borrowernumber,
1316                                        $item->biblionumber)->{status},
1317        'alreadypossession',
1318        'Patron cannot place hold on a book loaned to itself');
1319
1320     is(C4::Reserves::CanItemBeReserved($patron->borrowernumber,
1321                                        $item->itemnumber)->{status},
1322        'alreadypossession',
1323        'Patron cannot place hold on an item loaned to itself');
1324
1325     t::lib::Mocks::mock_preference('AllowHoldsOnPatronsPossessions', 1);
1326
1327     is(C4::Reserves::CanBookBeReserved($patron->borrowernumber,
1328                                        $item->biblionumber)->{status},
1329        'OK',
1330        'Patron can place hold on a book loaned to itself');
1331
1332     is(C4::Reserves::CanItemBeReserved($patron->borrowernumber,
1333                                        $item->itemnumber)->{status},
1334        'OK',
1335        'Patron can place hold on an item loaned to itself');
1336 };
1337
1338 subtest 'MergeHolds' => sub {
1339
1340     plan tests => 1;
1341
1342     my $biblio_1  = $builder->build_sample_biblio();
1343     my $biblio_2  = $builder->build_sample_biblio();
1344     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1345     my $itype   = $builder->build_object(
1346         { class => "Koha::ItemTypes", value => { notforloan => 0 } } );
1347     my $item_1 = $builder->build_sample_item(
1348         {
1349             biblionumber => $biblio_1->biblionumber,
1350             itype        => $itype->itemtype,
1351             library      => $library->branchcode
1352         }
1353     );
1354     my $patron_1 = $builder->build_object( { class => "Koha::Patrons" } );
1355
1356     # Place a hold on $biblio_1
1357     my $priority = 1;
1358     place_item_hold( $patron_1, $item_1, $library, $priority );
1359
1360     # Move and make sure hold is now on $biblio_2
1361     C4::Reserves::MergeHolds($dbh, $biblio_2->biblionumber, $biblio_1->biblionumber);
1362     is( $biblio_2->holds->count, 1, 'Hold has been transferred' );
1363 };
1364
1365 subtest 'ModReserveAffect logging' => sub {
1366
1367     plan tests => 4;
1368
1369     my $item = $builder->build_sample_item;
1370     my $patron = $builder->build_object(
1371         {
1372             class => "Koha::Patrons",
1373             value => { branchcode => $item->homebranch }
1374         }
1375     );
1376
1377     t::lib::Mocks::mock_userenv({ patron => $patron });
1378     t::lib::Mocks::mock_preference('HoldsLog', 1);
1379
1380     my $reserve_id = AddReserve(
1381         {
1382             branchcode     => $item->homebranch,
1383             borrowernumber => $patron->borrowernumber,
1384             biblionumber   => $item->biblionumber,
1385             priority       => 1,
1386             itemnumber     => $item->itemnumber,
1387         }
1388     );
1389
1390     my $hold = Koha::Holds->find($reserve_id);
1391     my $previous_timestamp = '1970-01-01 12:34:56';
1392     $hold->timestamp($previous_timestamp)->store;
1393
1394     $hold = Koha::Holds->find($reserve_id);
1395     is( $hold->timestamp, $previous_timestamp, 'Make sure the previous timestamp has been used' );
1396
1397     # Mark it waiting
1398     ModReserveAffect( $item->itemnumber, $patron->borrowernumber );
1399
1400     $hold = Koha::Holds->find($reserve_id);
1401     is( $hold->found, 'W', 'Hold has been set waiting' );
1402     isnt( $hold->timestamp, $previous_timestamp, 'The timestamp has been modified' );
1403
1404     my $log = Koha::ActionLogs->search({ module => 'HOLDS', action => 'MODIFY', object => $hold->reserve_id })->next;
1405     my $expected = sprintf q{'timestamp' => '%s'}, $hold->timestamp;
1406     like( $log->info, qr{$expected}, 'Timestamp logged is the current one' );
1407 };
1408
1409 sub count_hold_print_messages {
1410     my $message_count = $dbh->selectall_arrayref(q{
1411         SELECT COUNT(*)
1412         FROM message_queue
1413         WHERE letter_code = 'HOLD' 
1414         AND   message_transport_type = 'print'
1415     });
1416     return $message_count->[0]->[0];
1417 }
1418
1419 sub place_item_hold {
1420     my ($patron,$item,$library,$priority) = @_;
1421
1422     my $hold_id = C4::Reserves::AddReserve(
1423         {
1424             branchcode     => $library->branchcode,
1425             borrowernumber => $patron->borrowernumber,
1426             biblionumber   => $item->biblionumber,
1427             priority       => $priority,
1428             title          => "title for fee",
1429             itemnumber     => $item->itemnumber,
1430         }
1431     );
1432
1433     my $hold = Koha::Holds->find($hold_id);
1434     return $hold;
1435 }
1436
1437 # we reached the finish
1438 $schema->storage->txn_rollback();