Bug 27992: Unit test
[koha.git] / t / db_dependent / Circulation.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 use utf8;
20
21 use Test::More tests => 67;
22 use Test::Exception;
23 use Test::MockModule;
24 use Test::Deep qw( cmp_deeply );
25 use Test::Warn;
26
27 use Data::Dumper;
28 use DateTime;
29 use Time::Fake;
30 use POSIX qw( floor );
31 use t::lib::Mocks;
32 use t::lib::TestBuilder;
33
34 use C4::Accounts;
35 use C4::Calendar qw( new insert_single_holiday insert_week_day_holiday delete_holiday );
36 use C4::Circulation qw( AddIssue AddReturn CanBookBeRenewed GetIssuingCharges AddRenewal GetSoonestRenewDate GetLatestAutoRenewDate LostItem GetUpcomingDueIssues CanBookBeIssued AddIssuingCharge MarkIssueReturned ProcessOfflinePayment transferbook updateWrongTransfer );
37 use C4::Biblio;
38 use C4::Items qw( ModItemTransfer );
39 use C4::Log;
40 use C4::Reserves qw( AddReserve ModReserve ModReserveCancelAll ModReserveAffect CheckReserves GetOtherReserves );
41 use C4::Overdues qw( CalcFine UpdateFine get_chargeable_units );
42 use C4::Members::Messaging qw( SetMessagingPreference );
43 use Koha::DateUtils qw( dt_from_string output_pref );
44 use Koha::Database;
45 use Koha::Items;
46 use Koha::Item::Transfers;
47 use Koha::Checkouts;
48 use Koha::Patrons;
49 use Koha::Patron::Debarments qw( AddDebarment DelUniqueDebarment );
50 use Koha::Holds;
51 use Koha::CirculationRules;
52 use Koha::Subscriptions;
53 use Koha::Account::Lines;
54 use Koha::Account::Offsets;
55 use Koha::ActionLogs;
56 use Koha::Notice::Messages;
57 use Koha::Cache::Memory::Lite;
58
59 my $builder = t::lib::TestBuilder->new;
60 sub set_userenv {
61     my ( $library ) = @_;
62     my $staff = $builder->build_object({ class => "Koha::Patrons" });
63     t::lib::Mocks::mock_userenv({ patron => $staff, branchcode => $library->{branchcode} });
64 }
65
66 sub str {
67     my ( $error, $question, $alert ) = @_;
68     my $s;
69     $s  = %$error    ? ' (error: '    . join( ' ', keys %$error    ) . ')' : '';
70     $s .= %$question ? ' (question: ' . join( ' ', keys %$question ) . ')' : '';
71     $s .= %$alert    ? ' (alert: '    . join( ' ', keys %$alert    ) . ')' : '';
72     return $s;
73 }
74
75 sub test_debarment_on_checkout {
76     my ($params) = @_;
77     my $item     = $params->{item};
78     my $library  = $params->{library};
79     my $patron   = $params->{patron};
80     my $due_date = $params->{due_date} || dt_from_string;
81     my $return_date = $params->{return_date} || dt_from_string;
82     my $expected_expiration_date = $params->{expiration_date};
83
84     $expected_expiration_date = output_pref(
85         {
86             dt         => $expected_expiration_date,
87             dateformat => 'sql',
88             dateonly   => 1,
89         }
90     );
91     my @caller      = caller;
92     my $line_number = $caller[2];
93     AddIssue( $patron, $item->barcode, $due_date );
94
95     my ( undef, $message ) = AddReturn( $item->barcode, $library->{branchcode}, undef, $return_date );
96     is( $message->{WasReturned} && exists $message->{Debarred}, 1, 'AddReturn must have debarred the patron' )
97         or diag('AddReturn returned message ' . Dumper $message );
98     my $suspensions = $patron->restrictions->search({ type => 'SUSPENSION' } );
99     is( $suspensions->count, 1, 'Test at line ' . $line_number );
100
101     my $THE_suspension = $suspensions->next;
102     is( $THE_suspension->expiration,
103         $expected_expiration_date, 'Test at line ' . $line_number );
104     Koha::Patron::Debarments::DelUniqueDebarment(
105         { borrowernumber => $patron->borrowernumber, type => 'SUSPENSION' } );
106 };
107
108 my $schema = Koha::Database->schema;
109 $schema->storage->txn_begin;
110 my $dbh = C4::Context->dbh;
111
112 # Prevent random failures by mocking ->now
113 my $now_value       = dt_from_string;
114 my $mocked_datetime = Test::MockModule->new('DateTime');
115 $mocked_datetime->mock( 'now', sub { return $now_value->clone; } );
116
117 my $cache = Koha::Caches->get_instance();
118 $dbh->do(q|DELETE FROM special_holidays|);
119 $dbh->do(q|DELETE FROM repeatable_holidays|);
120 my $branches = Koha::Libraries->search();
121 for my $branch ( $branches->next ) {
122     my $key = $branch->branchcode . "_holidays";
123     $cache->clear_from_cache($key);
124 }
125
126 # Start with a clean slate
127 $dbh->do('DELETE FROM issues');
128 $dbh->do('DELETE FROM borrowers');
129
130 # Disable recording of the staff who checked out an item until we're ready for it
131 t::lib::Mocks::mock_preference('RecordStaffUserOnCheckout', 0);
132
133 my $module = Test::MockModule->new('C4::Context');
134
135 my $library = $builder->build({
136     source => 'Branch',
137 });
138 my $library2 = $builder->build({
139     source => 'Branch',
140 });
141 my $itemtype = $builder->build(
142     {
143         source => 'Itemtype',
144         value  => {
145             notforloan          => undef,
146             rentalcharge        => 0,
147             rentalcharge_daily => 0,
148             defaultreplacecost  => undef,
149             processfee          => undef
150         }
151     }
152 )->{itemtype};
153 my $patron_category = $builder->build(
154     {
155         source => 'Category',
156         value  => {
157             category_type                 => 'P',
158             enrolmentfee                  => 0,
159             BlockExpiredPatronOpacActions => -1, # Pick the pref value
160         }
161     }
162 );
163
164 my $CircControl = C4::Context->preference('CircControl');
165 my $HomeOrHoldingBranch = C4::Context->preference('HomeOrHoldingBranch');
166
167 my $item = $builder->build_object({
168     class => 'Koha::Items',
169     value => {
170         homebranch => $library2->{branchcode},
171         holdingbranch => $library2->{branchcode}
172     }
173 });
174
175 my $borrower = $builder->build_object({
176     class => 'Koha::Patrons',
177     value => { branchcode => $library2->{branchcode} }
178 });
179
180 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
181
182 # No userenv, PickupLibrary
183 t::lib::Mocks::mock_preference('IndependentBranches', '0');
184 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
185 is(
186     C4::Context->preference('CircControl'),
187     'PickupLibrary',
188     'CircControl changed to PickupLibrary'
189 );
190 is(
191     C4::Circulation::_GetCircControlBranch($item, $borrower),
192     $item->get_column($HomeOrHoldingBranch),
193     '_GetCircControlBranch returned item branch (no userenv defined)'
194 );
195
196 # No userenv, PatronLibrary
197 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
198 is(
199     C4::Context->preference('CircControl'),
200     'PatronLibrary',
201     'CircControl changed to PatronLibrary'
202 );
203 is(
204     C4::Circulation::_GetCircControlBranch($item, $borrower),
205     $borrower->branchcode,
206     '_GetCircControlBranch returned borrower branch'
207 );
208
209 # No userenv, ItemHomeLibrary
210 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
211 is(
212     C4::Context->preference('CircControl'),
213     'ItemHomeLibrary',
214     'CircControl changed to ItemHomeLibrary'
215 );
216 is(
217     $item->get_column($HomeOrHoldingBranch),
218     C4::Circulation::_GetCircControlBranch($item, $borrower),
219     '_GetCircControlBranch returned item branch'
220 );
221
222 # Now, set a userenv
223 t::lib::Mocks::mock_userenv({ branchcode => $library2->{branchcode} });
224 is(C4::Context->userenv->{branch}, $library2->{branchcode}, 'userenv set');
225
226 # Userenv set, PickupLibrary
227 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
228 is(
229     C4::Context->preference('CircControl'),
230     'PickupLibrary',
231     'CircControl changed to PickupLibrary'
232 );
233 is(
234     C4::Circulation::_GetCircControlBranch($item, $borrower),
235     $library2->{branchcode},
236     '_GetCircControlBranch returned current branch'
237 );
238
239 # Userenv set, PatronLibrary
240 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
241 is(
242     C4::Context->preference('CircControl'),
243     'PatronLibrary',
244     'CircControl changed to PatronLibrary'
245 );
246 is(
247     C4::Circulation::_GetCircControlBranch($item, $borrower),
248     $borrower->branchcode,
249     '_GetCircControlBranch returned borrower branch'
250 );
251
252 # Userenv set, ItemHomeLibrary
253 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
254 is(
255     C4::Context->preference('CircControl'),
256     'ItemHomeLibrary',
257     'CircControl changed to ItemHomeLibrary'
258 );
259 is(
260     C4::Circulation::_GetCircControlBranch($item, $borrower),
261     $item->get_column($HomeOrHoldingBranch),
262     '_GetCircControlBranch returned item branch'
263 );
264
265 # Reset initial configuration
266 t::lib::Mocks::mock_preference('CircControl', $CircControl);
267 is(
268     C4::Context->preference('CircControl'),
269     $CircControl,
270     'CircControl reset to its initial value'
271 );
272
273 # Set a simple circ policy
274 $dbh->do('DELETE FROM circulation_rules');
275 Koha::CirculationRules->set_rules(
276     {
277         categorycode => undef,
278         branchcode   => undef,
279         itemtype     => undef,
280         rules        => {
281             reservesallowed     => 25,
282             issuelength         => 14,
283             lengthunit          => 'days',
284             renewalsallowed     => 1,
285             renewalperiod       => 7,
286             norenewalbefore     => undef,
287             noautorenewalbefore => undef,
288             auto_renew          => 0,
289             fine                => .10,
290             chargeperiod        => 1,
291         }
292     }
293 );
294
295 subtest "CanBookBeRenewed AllowRenewalIfOtherItemsAvailable multiple borrowers and items tests" => sub {
296     plan tests => 7;
297
298     #Can only reserve from home branch
299     Koha::CirculationRules->set_rule(
300         {
301             branchcode   => undef,
302             itemtype     => undef,
303             rule_name    => 'holdallowed',
304             rule_value   => 1
305         }
306     );
307     Koha::CirculationRules->set_rule(
308         {
309             branchcode   => undef,
310             categorycode   => undef,
311             itemtype     => undef,
312             rule_name    => 'onshelfholds',
313             rule_value   => 1
314         }
315     );
316
317     # Patrons from three different branches
318     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
319     my $patron_hold_1   = $builder->build_object({ class => 'Koha::Patrons' });
320     my $patron_hold_2   = $builder->build_object({ class => 'Koha::Patrons' });
321     my $biblio = $builder->build_sample_biblio();
322
323     # Item at each patron branch
324     my $item_1 = $builder->build_sample_item({
325         biblionumber => $biblio->biblionumber,
326         homebranch   => $patron->branchcode
327     });
328     my $item_2 = $builder->build_sample_item({
329         biblionumber => $biblio->biblionumber,
330         homebranch   => $patron_hold_2->branchcode
331     });
332     my $item_3 = $builder->build_sample_item({
333         biblionumber => $biblio->biblionumber,
334         homebranch   => $patron_hold_1->branchcode
335     });
336
337     my $issue = AddIssue( $patron, $item_1->barcode);
338     my $datedue = dt_from_string( $issue->date_due() );
339     is (defined $issue->date_due(), 1, "Item 1 checked out, due date: " . $issue->date_due() );
340
341     # Biblio-level holds
342     my $reserve_1 = AddReserve(
343         {
344             branchcode       => $patron_hold_1->branchcode,
345             borrowernumber   => $patron_hold_1->borrowernumber,
346             biblionumber     => $biblio->biblionumber,
347             priority         => 1,
348             reservation_date => dt_from_string(),
349             expiration_date  => undef,
350             itemnumber       => undef,
351             found            => undef,
352         }
353     );
354     AddReserve(
355         {
356             branchcode       => $patron_hold_2->branchcode,
357             borrowernumber   => $patron_hold_2->borrowernumber,
358             biblionumber     => $biblio->biblionumber,
359             priority         => 2,
360             reservation_date => dt_from_string(),
361             expiration_date  => undef,
362             itemnumber       => undef,
363             found            => undef,
364         }
365     );
366     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 0 );
367
368     my ( $renewokay, $error ) = CanBookBeRenewed($patron, $issue);
369     is( $renewokay, 0, 'Cannot renew, reserved');
370     is( $error, 'on_reserve', 'Cannot renew, reserved (returned error is on_reserve)');
371
372     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 1 );
373
374     ( $renewokay, $error ) = CanBookBeRenewed($patron, $issue);
375     is( $renewokay, 1, 'Can renew, two items available for two holds');
376     is( $error, undef, 'Can renew, each reserve has an item');
377
378     # Item level hold
379     my $hold = Koha::Holds->find( $reserve_1 );
380     $hold->itemnumber( $item_1->itemnumber )->store;
381
382     ( $renewokay, $error ) = CanBookBeRenewed($patron, $issue);
383     is( $renewokay, 0, 'Cannot renew when there is an item specific hold');
384     is( $error, 'on_reserve', 'Cannot renew, only this item can fill the reserve');
385 };
386
387 subtest "GetIssuingCharges tests" => sub {
388     plan tests => 4;
389     my $branch_discount = $builder->build_object({ class => 'Koha::Libraries' });
390     my $branch_no_discount = $builder->build_object({ class => 'Koha::Libraries' });
391     Koha::CirculationRules->set_rule(
392         {
393             categorycode => undef,
394             branchcode   => $branch_discount->branchcode,
395             itemtype     => undef,
396             rule_name    => 'rentaldiscount',
397             rule_value   => 15
398         }
399     );
400     my $itype_charge = $builder->build_object({
401         class => 'Koha::ItemTypes',
402         value => {
403             rentalcharge => 10
404         }
405     });
406     my $itype_no_charge = $builder->build_object({
407         class => 'Koha::ItemTypes',
408         value => {
409             rentalcharge => 0
410         }
411     });
412     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
413     my $item_1 = $builder->build_sample_item({ itype => $itype_charge->itemtype });
414     my $item_2 = $builder->build_sample_item({ itype => $itype_no_charge->itemtype });
415
416     t::lib::Mocks::mock_userenv({ branchcode => $branch_no_discount->branchcode });
417     # For now the sub always uses the env branch, this should follow CircControl instead
418     my ($charge, $itemtype) = GetIssuingCharges( $item_1->itemnumber, $patron->borrowernumber);
419     is( $charge + 0, 10.00, "Charge fetched correctly when no discount exists");
420     ($charge, $itemtype) = GetIssuingCharges( $item_2->itemnumber, $patron->borrowernumber);
421     is( $charge + 0, 0.00, "Charge fetched correctly when no discount exists and no charge");
422
423     t::lib::Mocks::mock_userenv({ branchcode => $branch_discount->branchcode });
424     # For now the sub always uses the env branch, this should follow CircControl instead
425     ($charge, $itemtype) = GetIssuingCharges( $item_1->itemnumber, $patron->borrowernumber);
426     is( $charge + 0, 8.50, "Charge fetched correctly when discount exists");
427     ($charge, $itemtype) = GetIssuingCharges( $item_2->itemnumber, $patron->borrowernumber);
428     is( $charge + 0, 0.00, "Charge fetched correctly when discount exists and no charge");
429
430 };
431
432 my ( $reused_itemnumber_1, $reused_itemnumber_2 );
433 subtest "CanBookBeRenewed tests" => sub {
434     plan tests => 113;
435
436     C4::Context->set_preference('ItemsDeniedRenewal','');
437     # Generate test biblio
438     my $biblio = $builder->build_sample_biblio();
439
440     my $branch = $library2->{branchcode};
441
442     my $item_1 = $builder->build_sample_item(
443         {
444             biblionumber     => $biblio->biblionumber,
445             library          => $branch,
446             replacementprice => 12.00,
447             itype            => $itemtype
448         }
449     );
450     $reused_itemnumber_1 = $item_1->itemnumber;
451
452     my $item_2 = $builder->build_sample_item(
453         {
454             biblionumber     => $biblio->biblionumber,
455             library          => $branch,
456             replacementprice => 23.00,
457             itype            => $itemtype
458         }
459     );
460     $reused_itemnumber_2 = $item_2->itemnumber;
461
462     my $item_3 = $builder->build_sample_item(
463         {
464             biblionumber     => $biblio->biblionumber,
465             library          => $branch,
466             replacementprice => 23.00,
467             itype            => $itemtype
468         }
469     );
470
471     # Create borrowers
472     my %renewing_borrower_data = (
473         firstname =>  'John',
474         surname => 'Renewal',
475         categorycode => $patron_category->{categorycode},
476         branchcode => $branch,
477         autorenew_checkouts => 1,
478     );
479
480     my %reserving_borrower_data = (
481         firstname =>  'Katrin',
482         surname => 'Reservation',
483         categorycode => $patron_category->{categorycode},
484         branchcode => $branch,
485     );
486
487     my %hold_waiting_borrower_data = (
488         firstname =>  'Kyle',
489         surname => 'Reservation',
490         categorycode => $patron_category->{categorycode},
491         branchcode => $branch,
492     );
493
494     my %restricted_borrower_data = (
495         firstname =>  'Alice',
496         surname => 'Reservation',
497         categorycode => $patron_category->{categorycode},
498         debarred => '3228-01-01',
499         branchcode => $branch,
500     );
501
502     my %expired_borrower_data = (
503         firstname =>  'Ça',
504         surname => 'Glisse',
505         categorycode => $patron_category->{categorycode},
506         branchcode => $branch,
507         dateexpiry => dt_from_string->subtract( months => 1 ),
508         autorenew_checkouts => 1,
509     );
510
511     my $renewing_borrower_obj = Koha::Patron->new(\%renewing_borrower_data)->store;
512     my $renewing_borrowernumber = $renewing_borrower_obj->borrowernumber;
513     my $reserving_borrowernumber = Koha::Patron->new(\%reserving_borrower_data)->store->borrowernumber;
514     my $hold_waiting_borrowernumber = Koha::Patron->new(\%hold_waiting_borrower_data)->store->borrowernumber;
515     my $restricted_borrower_obj = Koha::Patron->new(\%restricted_borrower_data)->store;
516
517     my $expired_borrower_obj = Koha::Patron->new(\%expired_borrower_data)->store;
518
519     my $bibitems       = '';
520     my $priority       = '1';
521     my $resdate        = undef;
522     my $expdate        = undef;
523     my $notes          = '';
524     my $checkitem      = undef;
525     my $found          = undef;
526
527     my $issue_1 = AddIssue( $renewing_borrower_obj, $item_1->barcode);
528     my $datedue = dt_from_string( $issue_1->date_due() );
529     is (defined $issue_1->date_due(), 1, "Item 1 checked out, due date: " . $issue_1->date_due() );
530
531     my $issue_2 = AddIssue( $renewing_borrower_obj, $item_2->barcode);
532     is (defined $issue_2, 1, "Item 2 checked out, due date: " . $issue_2->date_due());
533
534     my $borrowing_borrowernumber = Koha::Checkouts->find( { itemnumber => $item_1->itemnumber } )->borrowernumber;
535     is ($borrowing_borrowernumber, $renewing_borrowernumber, "Item checked out to ".$renewing_borrower_obj->firstname." ".$renewing_borrower_obj->surname);
536
537     my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1, 1);
538     is( $renewokay, 1, 'Can renew, no holds for this title or item');
539
540
541     # Biblio-level hold, renewal test
542     AddReserve(
543         {
544             branchcode       => $branch,
545             borrowernumber   => $reserving_borrowernumber,
546             biblionumber     => $biblio->biblionumber,
547             priority         => $priority,
548             reservation_date => $resdate,
549             expiration_date  => $expdate,
550             notes            => $notes,
551             itemnumber       => $checkitem,
552             found            => $found,
553         }
554     );
555
556     # Testing of feature to allow the renewal of reserved items if other items on the record can fill all needed holds
557     Koha::CirculationRules->set_rule(
558         {
559             categorycode => undef,
560             branchcode   => undef,
561             itemtype     => undef,
562             rule_name    => 'onshelfholds',
563             rule_value   => '1',
564         }
565     );
566     Koha::CirculationRules->set_rule(
567         {
568             categorycode => undef,
569             branchcode   => undef,
570             itemtype     => undef,
571             rule_name    => 'renewalsallowed',
572             rule_value   => '5',
573         }
574     );
575     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 1 );
576     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
577     is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
578     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_2);
579     is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
580
581
582     # Second biblio-level hold
583     my $reserve_id = AddReserve(
584         {
585             branchcode       => $branch,
586             borrowernumber   => $reserving_borrowernumber,
587             biblionumber     => $biblio->biblionumber,
588             priority         => $priority,
589             reservation_date => $resdate,
590             expiration_date  => $expdate,
591             notes            => $notes,
592             itemnumber       => $checkitem,
593             found            => $found,
594         }
595     );
596     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
597     is( $renewokay, 0, 'Renewal not possible when single patron\'s holds exceed the number of available items');
598     Koha::Holds->find($reserve_id)->delete;
599
600     # Now let's add an item level hold, we should no longer be able to renew the item
601     my $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
602         {
603             borrowernumber => $hold_waiting_borrowernumber,
604             biblionumber   => $biblio->biblionumber,
605             itemnumber     => $item_1->itemnumber,
606             branchcode     => $branch,
607             priority       => 3,
608             reservedate    => '1999-01-01',
609         }
610     );
611     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
612     is( $renewokay, 0, 'Bug 13919 - Renewal possible with item level hold on item');
613     $hold->delete();
614
615     # Now let's add a waiting hold on the 3rd item, it's no longer available tp check out by just anyone, so we should no longer
616     # be able to renew these items
617     $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
618         {
619             borrowernumber => $hold_waiting_borrowernumber,
620             biblionumber   => $biblio->biblionumber,
621             itemnumber     => $item_3->itemnumber,
622             branchcode     => $branch,
623             priority       => 0,
624             found          => 'W'
625         }
626     );
627     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
628     is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
629     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_2);
630     is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
631     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 0 );
632
633     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
634     is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
635     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
636
637     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_2);
638     is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
639     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
640
641     my $reserveid = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next->reserve_id;
642     my $reserving_borrower = Koha::Patrons->find( $reserving_borrowernumber );
643     AddIssue($reserving_borrower, $item_3->barcode);
644     my $reserve = $dbh->selectrow_hashref(
645         'SELECT * FROM old_reserves WHERE reserve_id = ?',
646         { Slice => {} },
647         $reserveid
648     );
649     is($reserve->{found}, 'F', 'hold marked completed when checking out item that fills it');
650
651     # Item-level hold, renewal test
652     AddReserve(
653         {
654             branchcode       => $branch,
655             borrowernumber   => $reserving_borrowernumber,
656             biblionumber     => $biblio->biblionumber,
657             priority         => $priority,
658             reservation_date => $resdate,
659             expiration_date  => $expdate,
660             notes            => $notes,
661             itemnumber       => $item_1->itemnumber,
662             found            => $found,
663         }
664     );
665
666     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1, 1);
667     is( $renewokay, 0, '(Bug 10663) Cannot renew, item reserved');
668     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, item reserved (returned error is on_reserve)');
669
670     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_2, 1);
671     is( $renewokay, 1, 'Can renew item 2, item-level hold is on item 1');
672
673     # Items can't fill hold for reasons
674     $issue_1->item->notforloan(1)->store;
675     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1, 1);
676     is( $renewokay, 0, 'Cannot renew, item is marked not for loan, but an item specific hold always blocks');
677     $item_1->set({notforloan => 0, itype => $itemtype })->store;
678
679     # FIXME: Add more for itemtype not for loan etc.
680
681     # Restricted users cannot renew when RestrictionBlockRenewing is enabled
682     my $item_5 = $builder->build_sample_item(
683         {
684             biblionumber     => $biblio->biblionumber,
685             library          => $branch,
686             replacementprice => 23.00,
687             itype            => $itemtype,
688         }
689     );
690     my $issue_5 = AddIssue($restricted_borrower_obj, $item_5->barcode);
691     is (defined $issue_5, 1, "Item with date due checked out, due date: ". $issue_5->date_due);
692
693     t::lib::Mocks::mock_preference('RestrictionBlockRenewing','1');
694     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_2);
695     is( $renewokay, 1, '(Bug 8236), Can renew, user is not restricted');
696     ( $renewokay, $error ) = CanBookBeRenewed($restricted_borrower_obj, $issue_5);
697     is( $renewokay, 0, '(Bug 8236), Cannot renew, user is restricted');
698     is( $error, 'restriction', "Correct error returned");
699
700     # Users cannot renew an overdue item
701     my $item_6 = $builder->build_sample_item(
702         {
703             biblionumber     => $biblio->biblionumber,
704             library          => $branch,
705             replacementprice => 23.00,
706             itype            => $itemtype,
707         }
708     );
709
710     my $item_7 = $builder->build_sample_item(
711         {
712             biblionumber     => $biblio->biblionumber,
713             library          => $branch,
714             replacementprice => 23.00,
715             itype            => $itemtype,
716         }
717     );
718
719     my $issue_6 = AddIssue( $renewing_borrower_obj, $item_6->barcode);
720     is (defined $issue_6, 1, "Item 2 checked out, due date: ".$issue_6->date_due);
721
722     my $now = dt_from_string();
723     my $five_weeks = DateTime::Duration->new(weeks => 5);
724     my $five_weeks_ago = $now - $five_weeks;
725     t::lib::Mocks::mock_preference('finesMode', 'production');
726
727     my $issue_7 = AddIssue($renewing_borrower_obj, $item_7->barcode, $five_weeks_ago);
728     is (defined $issue_7, 1, "Item with passed date due checked out, due date: " . $issue_7->date_due);
729
730     t::lib::Mocks::mock_preference('OverduesBlockRenewing','allow');
731     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_6);
732     is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
733     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_7);
734     is( $renewokay, 1, '(Bug 8236), Can renew, this item is overdue but not pref does not block');
735
736     t::lib::Mocks::mock_preference('OverduesBlockRenewing','block');
737     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_6);
738     is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is not overdue but patron has overdues');
739     is( $error, 'overdue', "Correct error returned");
740     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_7);
741     is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue so patron has overdues');
742     is( $error, 'overdue', "Correct error returned");
743
744     t::lib::Mocks::mock_preference('OverduesBlockRenewing','blockitem');
745     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_6);
746     is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
747     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_7);
748     is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue');
749     is( $error, 'overdue', "Correct error returned");
750
751     my ( $fine ) = CalcFine( $item_7->unblessed, $renewing_borrower_obj->categorycode, $branch, $five_weeks_ago, $now );
752     C4::Overdues::UpdateFine(
753         {
754             issue_id       => $issue_7->id(),
755             itemnumber     => $item_7->itemnumber,
756             borrowernumber => $renewing_borrower_obj->borrowernumber,
757             amount         => $fine,
758             due            => Koha::DateUtils::output_pref($five_weeks_ago)
759         }
760     );
761
762     # Make sure fine calculation isn't skipped when adding renewal
763     t::lib::Mocks::mock_preference('CalculateFinesOnReturn', 1);
764
765     # Calculate new due-date based on the present date not to incur
766     # multiple fees
767     t::lib::Mocks::mock_preference('RenewalPeriodBase', 'now');
768
769     my $staff = $builder->build_object({ class => "Koha::Patrons" });
770     t::lib::Mocks::mock_userenv({ patron => $staff });
771
772     t::lib::Mocks::mock_preference('RenewalLog', 0);
773     my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
774     my %params_renewal = (
775         timestamp => { -like => $date . "%" },
776         module => "CIRCULATION",
777         action => "RENEWAL",
778     );
779     my %params_issue = (
780         timestamp => { -like => $date . "%" },
781         module => "CIRCULATION",
782         action => "ISSUE"
783     );
784     my $old_log_size = Koha::ActionLogs->count( \%params_renewal );
785     my $dt = dt_from_string();
786     Time::Fake->offset( $dt->epoch );
787     my $datedue1 = AddRenewal(
788         {
789             borrowernumber => $renewing_borrower_obj->borrowernumber,
790             itemnumber     => $item_7->itemnumber,
791             branch         => $branch
792         }
793     );
794     my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
795     is ($new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog');
796     isnt (DateTime->compare($datedue1, $dt), 0, "AddRenewal returned a good duedate");
797     Time::Fake->reset;
798
799     t::lib::Mocks::mock_preference('RenewalLog', 1);
800     $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
801     $old_log_size = Koha::ActionLogs->count( \%params_renewal );
802     AddRenewal(
803         {
804             borrowernumber => $renewing_borrower_obj->borrowernumber,
805             itemnumber     => $item_7->itemnumber,
806             branch         => $branch
807         }
808     );
809     $new_log_size = Koha::ActionLogs->count( \%params_renewal );
810     is ($new_log_size, $old_log_size + 1, 'renew log successfully added');
811
812     my $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower_obj->borrowernumber, itemnumber => $item_7->itemnumber } );
813     is( $fines->count, 1, 'AddRenewal left fine' );
814     is( $fines->next->status, 'RENEWED', 'Fine on renewed item is closed out properly' );
815     $fines->delete();
816
817     my $old_issue_log_size = Koha::ActionLogs->count( \%params_issue );
818     my $old_renew_log_size = Koha::ActionLogs->count( \%params_renewal );
819     AddIssue(
820         $renewing_borrower_obj,
821         $item_7->barcode,
822         Koha::DateUtils::output_pref({str=>$issue_6->date_due, dateformat =>'iso'}),
823         0,
824         $date,
825         0,
826         undef
827     ); # TODO: Already issued???
828     $new_log_size = Koha::ActionLogs->count( \%params_renewal );
829     is ($new_log_size, $old_renew_log_size + 1, 'renew log successfully added when renewed via issuing');
830     $new_log_size = Koha::ActionLogs->count( \%params_issue );
831     is ($new_log_size, $old_issue_log_size, 'renew not logged as issue when renewed via issuing');
832
833     $hold = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next;
834     $hold->cancel;
835
836     # Bug 14101
837     # Test automatic renewal before value for "norenewalbefore" in policy is set
838     # In this case automatic renewal should still be permitted
839     my $item_4 = $builder->build_sample_item(
840         {
841             biblionumber     => $biblio->biblionumber,
842             library          => $branch,
843             replacementprice => 16.00,
844             itype            => $itemtype,
845         }
846     );
847
848     my $auto_renew_issue =
849         AddIssue( $renewing_borrower_obj, $item_4->barcode, undef, undef, undef, undef, { auto_renew => 1 } );
850     my $info;
851     ( $renewokay, $error, $info ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
852     is( $info->{soonest_renew_date}, undef, "soonest_renew_date is not returned because this issue can be renewed" );
853     is( $renewokay, 1, 'Bug 25393: Can do a manual renew, even if renewal is automatic and premature' );
854     is(
855         $error, 'auto_renew',
856         'Bug 25393: Can do a manual renew, even if renewal is automatic and premature, norenewalbefore = undef -> manual renewals are allowed at any time'
857     );
858
859     my ( $renewokay_cron, $error_cron, $info_cron ) =
860         CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, undef, 1 );
861     is(
862         $info_cron->{soonest_renew_date}, dt_from_string( $auto_renew_issue->date_due ),
863         "Due date is returned as earliest renewal date when the CanBookBeRenewed check is done by cron and error is 'auto_too_soon'"
864     );
865     AddReserve(
866         {
867             branchcode       => $branch,
868             borrowernumber   => $reserving_borrowernumber,
869             biblionumber     => $biblio->biblionumber,
870             itemnumber       => $bibitems,
871             priority         => $priority,
872             reservation_date => $resdate,
873             expiration_date  => $expdate,
874             notes            => $notes,
875             title            => 'a title',
876             itemnumber       => $item_4->itemnumber,
877             found            => $found
878         }
879     );
880     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
881     is( $renewokay, 0, 'Still should not be able to renew' );
882     is( $error, 'on_reserve', 'returned code is on_reserve, reserve checked when not checking for cron' );
883     ( $renewokay, $error, $info ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, undef, 1 );
884     is( $renewokay, 0, 'Still should not be able to renew' );
885     is( $error, 'auto_too_soon', 'returned code is auto_too_soon, reserve not checked when checking for cron' );
886     is( $info->{soonest_renew_date}, dt_from_string($auto_renew_issue->date_due), "Due date is returned as earliest renewal date when error is 'auto_too_soon'" );
887     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, 1 );
888     is( $renewokay, 0, 'Still should not be able to renew' );
889     is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
890     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, 1, 1 );
891     is( $renewokay, 0, 'Still should not be able to renew' );
892     is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
893     $dbh->do('UPDATE circulation_rules SET rule_value = 0 where rule_name = "norenewalbefore"');
894     Koha::Cache::Memory::Lite->flush();
895     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, 1 );
896     is( $renewokay, 0, 'Still should not be able to renew' );
897     is( $error, 'on_reserve', 'returned code is on_reserve, auto_renew only happens if not on reserve' );
898     ModReserveCancelAll($item_4->itemnumber, $reserving_borrowernumber);
899
900     $renewing_borrower_obj = Koha::Patrons->find($renewing_borrower_obj->borrowernumber);
901     $renewing_borrower_obj->autorenew_checkouts(0)->store;
902     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
903     is( $renewokay, 1, 'No renewal before is undef, but patron opted out of auto_renewal' );
904     $renewing_borrower_obj->autorenew_checkouts(1)->store;
905
906
907     # Bug 7413
908     # Test premature manual renewal
909     Koha::CirculationRules->set_rule(
910         {
911             categorycode => undef,
912             branchcode   => undef,
913             itemtype     => undef,
914             rule_name    => 'norenewalbefore',
915             rule_value   => '7',
916         }
917     );
918
919     ( $renewokay, $error, $info ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
920     is( $renewokay, 0, 'Bug 7413: Cannot renew, renewal is premature');
921     is( $error, 'too_soon', 'Bug 7413: Cannot renew, renewal is premature (returned code is too_soon)');
922     is( $info->{soonest_renew_date}, dt_from_string($issue_1->date_due)->subtract( days => 7 ), "Soonest renew date returned when error is 'too_soon'");
923
924     # Bug 25393
925     # Test premature automatic renewal
926     Koha::CirculationRules->set_rule(
927         {
928             categorycode => undef,
929             branchcode   => undef,
930             itemtype     => undef,
931             rule_name    => 'noautorenewalbefore',
932             rule_value   => '7',
933         }
934     );
935
936     ( $renewokay, $error, $info ) =
937     CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
938     is( $renewokay, 0, 'Bug 25393: Cannot renew, renewal is premature' );
939     is( $error, 'too_soon',
940         'Bug 25393: Cannot renew, renewal is premature (returned code is too_soon)'
941     );
942     is(
943         $info->{soonest_renew_date}, dt_from_string( $auto_renew_issue->date_due )->subtract( days => 7 ),
944         "Soonest renew date returned when error is 'auto_too_soon'"
945     );
946
947     ( $renewokay_cron, $error_cron, $info_cron ) =
948         CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, undef, 1 );
949     is(
950         $error_cron, 'auto_too_soon',
951         'Bug 25393: Cannot renew, renewal is automatic and premature (returned code is auto_too_soon)'
952     );
953     $renewing_borrower_obj->autorenew_checkouts(0)->store;
954     ( $renewokay, $error, $info ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
955     is( $renewokay, 0, 'No renewal before is 7, patron opted out of auto_renewal still cannot renew early' );
956     is( $error, 'too_soon', 'Error is too_soon, no auto' );
957     is( $info->{soonest_renew_date}, dt_from_string($auto_renew_issue->date_due)->subtract( days => 7 ), "Soonest renew date returned when error is 'too_soon'");
958     $renewing_borrower_obj->autorenew_checkouts(1)->store;
959
960     # Change policy so that loans can only be renewed exactly on due date (0 days prior to due date)
961     # and test automatic renewal again
962     $dbh->do(q{UPDATE circulation_rules SET rule_value = '0' WHERE rule_name = 'norenewalbefore'});
963     Koha::Cache::Memory::Lite->flush();
964     ( $renewokay, $error, $info ) =
965       CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
966     is( $renewokay, 0, 'Bug 25393: Cannot manual renew, renewal is too soon' );
967     is( $error, 'too_soon',
968         'Bug 25393: Cannot manual renew, renewal is too soon, even though it\'s also scheduled for auto renewal'
969     );
970     is( $info->{soonest_renew_date}, dt_from_string($auto_renew_issue->date_due), "Soonest renew date returned when error is 'auto_too_soon'");
971
972     # Now we test for noautorenewalbefore if cron
973     $dbh->do(q{UPDATE circulation_rules SET rule_value = '0' WHERE rule_name = 'noautorenewalbefore'});
974     Koha::Cache::Memory::Lite->flush();
975     ( $renewokay, $error, $info ) =
976       CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, undef, 1 );
977     is( $renewokay, 0, 'Bug 25393: Cannot auto renew, renewal is too soon' );
978     is( $error, 'auto_too_soon',
979         'Bug 25393: Cannot auto renew, auto renewal is too soon'
980     );
981     is( $info->{soonest_renew_date}, dt_from_string($auto_renew_issue->date_due), "Soonest renew date returned when error is 'auto_too_soon'");
982
983     $renewing_borrower_obj->autorenew_checkouts(0)->store;
984     ( $renewokay, $error, $info ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
985     is( $renewokay, 0, 'No renewal before is 0, patron opted out of auto_renewal still cannot renew early' );
986     is( $error, 'too_soon', 'Error is too_soon, no auto' );
987     is( $info->{soonest_renew_date}, dt_from_string($auto_renew_issue->date_due), "Soonest renew date returned when error is 'auto_too_soon'");
988     $renewing_borrower_obj->autorenew_checkouts(1)->store;
989
990     # Change policy so that loans can be renewed 99 days prior to the due date
991     # and test automatic renewal again
992     $dbh->do(q{UPDATE circulation_rules SET rule_value = '99' WHERE rule_name = 'norenewalbefore'});
993     Koha::Cache::Memory::Lite->flush();
994     ( $renewokay, $error ) =
995       CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
996     is( $renewokay, 1, 'Bug 25393: Can do a manual renew, even if renewal is automatic and premature' );
997     is( $error, 'auto_renew',
998         'Bug 14101: Cannot renew, renewal is automatic (returned code is auto_renew)'
999     );
1000
1001     # Now we test for noautorenewalbefore if cron
1002     $dbh->do(q{UPDATE circulation_rules SET rule_value = '99' WHERE rule_name = 'noautorenewalbefore'});
1003     Koha::Cache::Memory::Lite->flush();
1004     ( $renewokay, $error, $info ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, undef, 1 );
1005     is( $renewokay, 1, 'Bug 25393: Can auto renew' );
1006     is( $error, 'auto_renew', 'Bug 25393: Can auto renew' );
1007     is( $info->{soonest_renew_date}, undef, "soonest_renew_date is not returned because this issue can be renewed" );
1008
1009     $renewing_borrower_obj->autorenew_checkouts(0)->store;
1010     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
1011     is( $renewokay, 1, 'No renewal before is 99, patron opted out of auto_renewal so can renew' );
1012     $renewing_borrower_obj->autorenew_checkouts(1)->store;
1013
1014     subtest "too_late_renewal / no_auto_renewal_after" => sub {
1015         plan tests => 16;
1016         my $item_to_auto_renew = $builder->build_sample_item(
1017             {
1018                 biblionumber => $biblio->biblionumber,
1019                 library      => $branch,
1020             }
1021         );
1022
1023         my $ten_days_before = dt_from_string->add( days => -10 );
1024         my $ten_days_ahead  = dt_from_string->add( days => 10 );
1025         my $issue = AddIssue( $renewing_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1026
1027         Koha::CirculationRules->set_rules(
1028             {
1029                 categorycode => undef,
1030                 branchcode   => undef,
1031                 itemtype     => undef,
1032                 rules        => {
1033                     norenewalbefore       => '7',
1034                     no_auto_renewal_after => '9',
1035                 }
1036             }
1037         );
1038         ( $renewokay, $error ) =
1039           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1040         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1041         is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
1042
1043         Koha::CirculationRules->set_rules(
1044             {
1045                 categorycode => undef,
1046                 branchcode   => undef,
1047                 itemtype     => undef,
1048                 rules        => {
1049                     norenewalbefore       => '7',
1050                     no_auto_renewal_after => '10',
1051                 }
1052             }
1053         );
1054         ( $renewokay, $error ) =
1055           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1056         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1057         is( $error, 'auto_too_late', 'Cannot auto renew, too late - no_auto_renewal_after is inclusive(returned code is auto_too_late)' );
1058
1059         Koha::CirculationRules->set_rules(
1060             {
1061                 categorycode => undef,
1062                 branchcode   => undef,
1063                 itemtype     => undef,
1064                 rules        => {
1065                     norenewalbefore       => '7',
1066                     no_auto_renewal_after => '11',
1067                 }
1068             }
1069         );
1070         ( $renewokay, $error ) =
1071           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1072         is( $renewokay, 0, 'Do not renew, too_soon' );
1073         is( $error, 'too_soon', 'Cannot renew, too_soon - no_auto_renewal_after is defined(returned code is too_soon)' );
1074
1075         # Now check for cron
1076         Koha::CirculationRules->set_rules(
1077             {
1078                 categorycode => undef,
1079                 branchcode   => undef,
1080                 itemtype     => undef,
1081                 rules        => {
1082                     noautorenewalbefore   => '9',
1083                     no_auto_renewal_after => '11',
1084                 }
1085             }
1086         );
1087         ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $issue, undef, 1 );
1088         is( $renewokay, 0, 'Do not renew, too_soon' );
1089         is(
1090             $error, 'auto_too_soon',
1091             'Cannot renew, too_soon - no_auto_renewal_after is defined(returned code is too_soon)'
1092         );
1093
1094         Koha::CirculationRules->set_rules(
1095             {
1096                 categorycode => undef,
1097                 branchcode   => undef,
1098                 itemtype     => undef,
1099                 rules        => {
1100                     norenewalbefore       => '10',
1101                     no_auto_renewal_after => '11',
1102                 }
1103             }
1104         );
1105         ( $renewokay, $error ) =
1106           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1107         is( $renewokay, 1,            'Renew, even if renewal is automatic' );
1108         is( $error,     'auto_renew', 'Cannot renew, renew is automatic' );
1109
1110         Koha::CirculationRules->set_rules(
1111             {
1112                 categorycode => undef,
1113                 branchcode   => undef,
1114                 itemtype     => undef,
1115                 rules        => {
1116                     norenewalbefore       => '10',
1117                     no_auto_renewal_after => undef,
1118                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
1119                 }
1120             }
1121         );
1122         ( $renewokay, $error ) =
1123           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1124         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1125         is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
1126
1127         Koha::CirculationRules->set_rules(
1128             {
1129                 categorycode => undef,
1130                 branchcode   => undef,
1131                 itemtype     => undef,
1132                 rules        => {
1133                     norenewalbefore       => '7',
1134                     no_auto_renewal_after => '15',
1135                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
1136                 }
1137             }
1138         );
1139         ( $renewokay, $error ) =
1140           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1141         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1142         is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
1143
1144         Koha::CirculationRules->set_rules(
1145             {
1146                 categorycode => undef,
1147                 branchcode   => undef,
1148                 itemtype     => undef,
1149                 rules        => {
1150                     norenewalbefore       => '10',
1151                     no_auto_renewal_after => undef,
1152                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => 1 ),
1153                 }
1154             }
1155         );
1156         ( $renewokay, $error ) =
1157           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1158         is( $renewokay, 1, 'Renew, even if renewal is automatic' );
1159         is( $error, 'auto_renew', 'Cannot renew, renew is automatic' );
1160     };
1161
1162     subtest "auto_too_much_oweing | OPACFineNoRenewalsBlockAutoRenew & OPACFineNoRenewalsIncludeCredit" => sub {
1163         plan tests => 10;
1164         my $item_to_auto_renew = $builder->build_sample_item(
1165             {
1166                 biblionumber => $biblio->biblionumber,
1167                 library      => $branch,
1168             }
1169         );
1170
1171         my $ten_days_before = dt_from_string->add( days => -10 );
1172         my $ten_days_ahead = dt_from_string->add( days => 10 );
1173         my $issue = AddIssue( $renewing_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1174
1175         Koha::CirculationRules->set_rules(
1176             {
1177                 categorycode => undef,
1178                 branchcode   => undef,
1179                 itemtype     => undef,
1180                 rules        => {
1181                     norenewalbefore       => '10',
1182                     no_auto_renewal_after => '11',
1183                 }
1184             }
1185         );
1186         C4::Context->set_preference('OPACFineNoRenewalsBlockAutoRenew','1');
1187         C4::Context->set_preference('OPACFineNoRenewals','10');
1188         C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
1189         my $fines_amount = 5;
1190         my $account = Koha::Account->new({patron_id => $renewing_borrowernumber});
1191         $account->add_debit(
1192             {
1193                 amount      => $fines_amount,
1194                 interface   => 'test',
1195                 type        => 'OVERDUE',
1196                 item_id     => $item_to_auto_renew->itemnumber,
1197                 description => "Some fines"
1198             }
1199         )->status('RETURNED')->store;
1200         ( $renewokay, $error ) =
1201           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1202         is( $renewokay, 1, 'Renew, even if renewal is automatic' );
1203         is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 5' );
1204
1205         $account->add_debit(
1206             {
1207                 amount      => $fines_amount,
1208                 interface   => 'test',
1209                 type        => 'OVERDUE',
1210                 item_id     => $item_to_auto_renew->itemnumber,
1211                 description => "Some fines"
1212             }
1213         )->status('RETURNED')->store;
1214         ( $renewokay, $error ) =
1215           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1216         is( $renewokay, 1, 'Renew, even if renewal is automatic' );
1217         is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 10' );
1218
1219         $account->add_debit(
1220             {
1221                 amount      => $fines_amount,
1222                 interface   => 'test',
1223                 type        => 'OVERDUE',
1224                 item_id     => $item_to_auto_renew->itemnumber,
1225                 description => "Some fines"
1226             }
1227         )->status('RETURNED')->store;
1228         ( $renewokay, $error ) =
1229           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1230         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1231         is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, patron has 15' );
1232
1233         $account->add_credit(
1234             {
1235                 amount      => $fines_amount,
1236                 interface   => 'test',
1237                 type        => 'PAYMENT',
1238                 description => "Some payment"
1239             }
1240         )->store;
1241         ( $renewokay, $error ) =
1242           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1243         is( $renewokay, 1, 'Renew, even if renewal is automatic' );
1244         is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit'  );
1245
1246         C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','0');
1247         ( $renewokay, $error ) =
1248           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1249         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1250         is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit'  );
1251
1252         $dbh->do('DELETE FROM accountlines WHERE borrowernumber=?', undef, $renewing_borrowernumber);
1253         C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
1254     };
1255
1256     subtest "auto_account_expired | BlockExpiredPatronOpacActions" => sub {
1257         plan tests => 6;
1258         my $item_to_auto_renew = $builder->build_sample_item(
1259             {
1260                 biblionumber => $biblio->biblionumber,
1261                 library      => $branch,
1262             }
1263         );
1264
1265         Koha::CirculationRules->set_rules(
1266             {
1267                 categorycode => undef,
1268                 branchcode   => undef,
1269                 itemtype     => undef,
1270                 rules        => {
1271                     norenewalbefore       => 10,
1272                     no_auto_renewal_after => 11,
1273                 }
1274             }
1275         );
1276
1277         my $ten_days_before = dt_from_string->add( days => -10 );
1278         my $ten_days_ahead = dt_from_string->add( days => 10 );
1279
1280         # Patron is expired and BlockExpiredPatronOpacActions=0
1281         # => auto renew is allowed
1282         t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 0);
1283         my $issue = AddIssue( $expired_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1284         ( $renewokay, $error ) =
1285           CanBookBeRenewed( $expired_borrower_obj, $issue );
1286         is( $renewokay, 1, 'Renew, even if renewal is automatic' );
1287         is( $error, 'auto_renew', 'Can auto renew, patron is expired but BlockExpiredPatronOpacActions=0' );
1288         Koha::Checkouts->find( $issue->issue_id )->delete;
1289
1290
1291         # Patron is expired and BlockExpiredPatronOpacActions=1
1292         # => auto renew is not allowed
1293         t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1294         $issue = AddIssue( $expired_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1295         ( $renewokay, $error ) =
1296           CanBookBeRenewed( $expired_borrower_obj, $issue );
1297         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1298         is( $error, 'auto_account_expired', 'Can not auto renew, lockExpiredPatronOpacActions=1 and patron is expired' );
1299         $issue->delete;
1300
1301         # Patron is not expired and BlockExpiredPatronOpacActions=1
1302         # => auto renew is allowed
1303         t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1304         $issue = AddIssue( $renewing_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1305         ( $renewokay, $error ) =
1306           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1307         is( $renewokay, 1, 'Renew, even if renewal is automatic' );
1308         is( $error, 'auto_renew', 'Can auto renew, BlockExpiredPatronOpacActions=1 but patron is not expired' );
1309         $issue->delete;
1310     };
1311
1312     subtest "GetLatestAutoRenewDate" => sub {
1313         plan tests => 5;
1314         my $item_to_auto_renew = $builder->build_sample_item(
1315             {
1316                 biblionumber => $biblio->biblionumber,
1317                 library      => $branch,
1318             }
1319         );
1320
1321         my $ten_days_before = dt_from_string->add( days => -10 );
1322         my $ten_days_ahead  = dt_from_string->add( days => 10 );
1323         my $issue = AddIssue( $renewing_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1324         Koha::CirculationRules->set_rules(
1325             {
1326                 categorycode => undef,
1327                 branchcode   => undef,
1328                 itemtype     => undef,
1329                 rules        => {
1330                     norenewalbefore       => '7',
1331                     no_auto_renewal_after => '',
1332                     no_auto_renewal_after_hard_limit => undef,
1333                 }
1334             }
1335         );
1336         my $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrower_obj, $issue );
1337         is( $latest_auto_renew_date, undef, 'GetLatestAutoRenewDate should return undef if no_auto_renewal_after or no_auto_renewal_after_hard_limit are not defined' );
1338         my $five_days_before = dt_from_string->add( days => -5 );
1339         Koha::CirculationRules->set_rules(
1340             {
1341                 categorycode => undef,
1342                 branchcode   => undef,
1343                 itemtype     => undef,
1344                 rules        => {
1345                     norenewalbefore       => '10',
1346                     no_auto_renewal_after => '5',
1347                     no_auto_renewal_after_hard_limit => undef,
1348                 }
1349             }
1350         );
1351         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrower_obj,, $issue );
1352         is( $latest_auto_renew_date->truncate( to => 'minute' ),
1353             $five_days_before->truncate( to => 'minute' ),
1354             'GetLatestAutoRenewDate should return -5 days if no_auto_renewal_after = 5 and date_due is 10 days before'
1355         );
1356         my $five_days_ahead = dt_from_string->add( days => 5 );
1357         $dbh->do(q{UPDATE circulation_rules SET rule_value = '10' WHERE rule_name = 'norenewalbefore'});
1358         $dbh->do(q{UPDATE circulation_rules SET rule_value = '15' WHERE rule_name = 'no_auto_renewal_after'});
1359         $dbh->do(q{UPDATE circulation_rules SET rule_value = NULL WHERE rule_name = 'no_auto_renewal_after_hard_limit'});
1360         Koha::Cache::Memory::Lite->flush();
1361         Koha::CirculationRules->set_rules(
1362             {
1363                 categorycode => undef,
1364                 branchcode   => undef,
1365                 itemtype     => undef,
1366                 rules        => {
1367                     norenewalbefore       => '10',
1368                     no_auto_renewal_after => '15',
1369                     no_auto_renewal_after_hard_limit => undef,
1370                 }
1371             }
1372         );
1373         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrower_obj, $issue );
1374         is( $latest_auto_renew_date->truncate( to => 'minute' ),
1375             $five_days_ahead->truncate( to => 'minute' ),
1376             'GetLatestAutoRenewDate should return +5 days if no_auto_renewal_after = 15 and date_due is 10 days before'
1377         );
1378         my $two_days_ahead = dt_from_string->add( days => 2 );
1379         Koha::CirculationRules->set_rules(
1380             {
1381                 categorycode => undef,
1382                 branchcode   => undef,
1383                 itemtype     => undef,
1384                 rules        => {
1385                     norenewalbefore       => '10',
1386                     no_auto_renewal_after => '',
1387                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1388                 }
1389             }
1390         );
1391         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrower_obj, $issue );
1392         is( $latest_auto_renew_date->truncate( to => 'day' ),
1393             $two_days_ahead->truncate( to => 'day' ),
1394             'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is defined and not no_auto_renewal_after'
1395         );
1396         Koha::CirculationRules->set_rules(
1397             {
1398                 categorycode => undef,
1399                 branchcode   => undef,
1400                 itemtype     => undef,
1401                 rules        => {
1402                     norenewalbefore       => '10',
1403                     no_auto_renewal_after => '15',
1404                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1405                 }
1406             }
1407         );
1408         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrower_obj, $issue );
1409         is( $latest_auto_renew_date->truncate( to => 'day' ),
1410             $two_days_ahead->truncate( to => 'day' ),
1411             'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is < no_auto_renewal_after'
1412         );
1413
1414     };
1415
1416     subtest "auto_renew_final" => sub {
1417         plan tests => 6;
1418         my $item_to_auto_renew = $builder->build_sample_item();
1419         Koha::CirculationRules->set_rules(
1420             {
1421                 categorycode => undef,
1422                 branchcode   => undef,
1423                 itemtype     => $item_to_auto_renew->itype,
1424                 rules        => {
1425                     norenewalbefore => undef,
1426                     renewalsallowed => 1,
1427                 }
1428             }
1429         );
1430
1431         my $ten_days_before = dt_from_string->add( days => -10 );
1432         my $ten_days_ahead  = dt_from_string->add( days => 10 );
1433         my $issue           = AddIssue(
1434             $renewing_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before,
1435             undef, { auto_renew => 1 }
1436         );
1437         $issue->renewals_count(0)->store;
1438
1439         ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $issue );
1440         is( $renewokay, 1,                  'Success for auto_renewal' );
1441         is( $error,     'auto_renew_final', 'Auto renewal allowed, but it is the last one' );
1442
1443         # Final unseen renewal
1444         Koha::CirculationRules->set_rules(
1445             {
1446                 categorycode => undef,
1447                 branchcode   => undef,
1448                 itemtype     => $item_to_auto_renew->itype,
1449                 rules        => {
1450                     unseen_renewals_allowed => 2,
1451                     renewalsallowed         => 10,
1452                 }
1453             }
1454         );
1455         t::lib::Mocks::mock_preference( 'UnseenRenewals', 1 );
1456         $issue->unseen_renewals(1)->renewals_count(1)->store;
1457
1458         ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $issue );
1459         is( $renewokay, 1,                   'Success for auto_renewal' );
1460         is( $error,     'auto_unseen_final', 'Final unseen renewal reported, not final overall' );
1461
1462         # Final unseen renewal
1463         Koha::CirculationRules->set_rules(
1464             {
1465                 categorycode => undef,
1466                 branchcode   => undef,
1467                 itemtype     => $item_to_auto_renew->itype,
1468                 rules        => {
1469                     unseen_renewals_allowed => 2,
1470                     renewalsallowed         => 2,
1471                 }
1472             }
1473         );
1474         ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $issue );
1475         is( $renewokay, 1,                  'Success for auto_renewal' );
1476         is( $error,     'auto_renew_final', 'Final auto renewal reported' );
1477
1478     };
1479
1480     # Too many renewals
1481     # set policy to forbid renewals
1482     Koha::CirculationRules->set_rules(
1483         {
1484             categorycode => undef,
1485             branchcode   => undef,
1486             itemtype     => undef,
1487             rules        => {
1488                 norenewalbefore => undef,
1489                 renewalsallowed => 0,
1490             }
1491         }
1492     );
1493
1494     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
1495     is( $renewokay, 0, 'Cannot renew, 0 renewals allowed');
1496     is( $error, 'too_many', 'Cannot renew, 0 renewals allowed (returned code is too_many)');
1497
1498     # Too many unseen renewals
1499     Koha::CirculationRules->set_rules(
1500         {
1501             categorycode => undef,
1502             branchcode   => undef,
1503             itemtype     => undef,
1504             rules        => {
1505                 unseen_renewals_allowed => 2,
1506                 renewalsallowed => 10,
1507             }
1508         }
1509     );
1510     t::lib::Mocks::mock_preference('UnseenRenewals', 1);
1511
1512     $issue_1->unseen_renewals(2)->store;
1513
1514     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
1515     is( $renewokay, 0, 'Cannot renew, 0 unseen renewals allowed');
1516     is( $error, 'too_unseen', 'Cannot renew, returned code is too_unseen');
1517     Koha::CirculationRules->set_rules(
1518         {
1519             categorycode => undef,
1520             branchcode   => undef,
1521             itemtype     => undef,
1522             rules        => {
1523                 norenewalbefore => undef,
1524                 renewalsallowed => 0,
1525             }
1526         }
1527     );
1528     t::lib::Mocks::mock_preference('UnseenRenewals', 0);
1529
1530     # Test WhenLostForgiveFine and WhenLostChargeReplacementFee
1531     t::lib::Mocks::mock_preference('WhenLostForgiveFine','1');
1532     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1533
1534     C4::Overdues::UpdateFine(
1535         {
1536             issue_id       => $issue_1->id(),
1537             itemnumber     => $item_1->itemnumber,
1538             borrowernumber => $renewing_borrower_obj->borrowernumber,
1539             amount         => 15.00,
1540             type           => q{},
1541             due            => Koha::DateUtils::output_pref($datedue)
1542         }
1543     );
1544
1545     my $line = Koha::Account::Lines->search({ borrowernumber => $renewing_borrower_obj->borrowernumber })->next();
1546     is( $line->debit_type_code, 'OVERDUE', 'Account line type is OVERDUE' );
1547     is( $line->status, 'UNRETURNED', 'Account line status is UNRETURNED' );
1548     is( $line->amountoutstanding+0, 15, 'Account line amount outstanding is 15.00' );
1549     is( $line->amount+0, 15, 'Account line amount is 15.00' );
1550     is( $line->issue_id, $issue_1->id, 'Account line issue id matches' );
1551
1552     my $offset = Koha::Account::Offsets->search({ debit_id => $line->id })->next();
1553     is( $offset->type, 'CREATE', 'Account offset type is CREATE' );
1554     is( $offset->amount+0, 15, 'Account offset amount is 15.00' );
1555
1556     t::lib::Mocks::mock_preference('WhenLostForgiveFine','0');
1557     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','0');
1558
1559     LostItem( $item_1->itemnumber, 'test', 1 );
1560
1561     $line = Koha::Account::Lines->find($line->id);
1562     is( $line->debit_type_code, 'OVERDUE', 'Account type remains as OVERDUE' );
1563     isnt( $line->status, 'UNRETURNED', 'Account status correctly changed from UNRETURNED to RETURNED' );
1564
1565     my $item = Koha::Items->find($item_1->itemnumber);
1566     ok( !$item->onloan(), "Lost item marked as returned has false onloan value" );
1567     my $checkout = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber });
1568     is( $checkout, undef, 'LostItem called with forced return has checked in the item' );
1569
1570     my $total_due = $dbh->selectrow_array(
1571         'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1572         undef, $renewing_borrower_obj->borrowernumber
1573     );
1574
1575     is( $total_due+0, 15, 'Borrower only charged replacement fee with both WhenLostForgiveFine and WhenLostChargeReplacementFee enabled' );
1576
1577     C4::Context->dbh->do("DELETE FROM accountlines");
1578
1579     C4::Overdues::UpdateFine(
1580         {
1581             issue_id       => $issue_2->id(),
1582             itemnumber     => $item_2->itemnumber,
1583             borrowernumber => $renewing_borrower_obj->borrowernumber,
1584             amount         => 15.00,
1585             type           => q{},
1586             due            => Koha::DateUtils::output_pref($datedue)
1587         }
1588     );
1589
1590     LostItem( $item_2->itemnumber, 'test', 0 );
1591
1592     my $item2 = Koha::Items->find($item_2->itemnumber);
1593     ok( $item2->onloan(), "Lost item *not* marked as returned has true onloan value" );
1594     ok( Koha::Checkouts->find({ itemnumber => $item_2->itemnumber }), 'LostItem called without forced return has checked in the item' );
1595
1596     $total_due = $dbh->selectrow_array(
1597         'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1598         undef, $renewing_borrower_obj->borrowernumber
1599     );
1600
1601     ok( $total_due == 15, 'Borrower only charged fine with both WhenLostForgiveFine and WhenLostChargeReplacementFee disabled' );
1602
1603     my $future = dt_from_string();
1604     $future->add( days => 7 );
1605     my $units = C4::Overdues::get_chargeable_units('days', $future, $now, $library2->{branchcode});
1606     ok( $units == 0, '_get_chargeable_units returns 0 for items not past due date (Bug 12596)' );
1607
1608     my $manager = $builder->build_object({ class => "Koha::Patrons" });
1609     t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
1610     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1611     $checkout = Koha::Checkouts->find( { itemnumber => $item_3->itemnumber } );
1612     LostItem( $item_3->itemnumber, 'test', 0 );
1613     my $accountline = Koha::Account::Lines->find( { itemnumber => $item_3->itemnumber } );
1614     is( $accountline->issue_id, $checkout->id, "Issue id added for lost replacement fee charge" );
1615     is(
1616         $accountline->description,
1617         sprintf( "%s %s %s",
1618             $item_3->biblio->title  || '',
1619             $item_3->barcode        || '',
1620             $item_3->itemcallnumber || '' ),
1621         "Account line description must not contain 'Lost Items ', but be title, barcode, itemcallnumber"
1622     );
1623
1624     # Recalls
1625     t::lib::Mocks::mock_preference('UseRecalls', 1);
1626     Koha::CirculationRules->set_rules({
1627         categorycode => undef,
1628         branchcode => undef,
1629         itemtype => undef,
1630         rules => {
1631             recalls_allowed => 10,
1632             renewalsallowed => 5,
1633         },
1634     });
1635     my $recall_borrower = $builder->build_object({ class => 'Koha::Patrons' });
1636     my $recall_biblio = $builder->build_sample_biblio();
1637     my $recall_item1 = $builder->build_sample_item({ biblionumber => $recall_biblio->biblionumber });
1638     my $recall_item2 = $builder->build_sample_item({ biblionumber => $recall_biblio->biblionumber });
1639
1640     my $recall_issue = AddIssue( $renewing_borrower_obj, $recall_item1->barcode );
1641
1642     # item-level and this item: renewal not allowed
1643     my $recall = Koha::Recall->new({
1644         biblio_id => $recall_item1->biblionumber,
1645         item_id => $recall_item1->itemnumber,
1646         patron_id => $recall_borrower->borrowernumber,
1647         pickup_library_id => $recall_borrower->branchcode,
1648         item_level => 1,
1649     })->store;
1650     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $recall_issue );
1651     is( $error, 'recalled', 'Cannot renew item that has been recalled' );
1652     $recall->set_cancelled;
1653
1654     # biblio-level requested recall: renewal not allowed
1655     $recall = Koha::Recall->new({
1656         biblio_id => $recall_item1->biblionumber,
1657         item_id => undef,
1658         patron_id => $recall_borrower->borrowernumber,
1659         pickup_library_id => $recall_borrower->branchcode,
1660         item_level => 0,
1661     })->store;
1662     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $recall_issue );
1663     is( $error, 'recalled', 'Cannot renew item if biblio is recalled and has no item allocated' );
1664     $recall->set_cancelled;
1665
1666     # item-level and not this item: renewal allowed
1667     $recall = Koha::Recall->new({
1668         biblio_id => $recall_item2->biblionumber,
1669         item_id => $recall_item2->itemnumber,
1670         patron_id => $recall_borrower->borrowernumber,
1671         pickup_library_id => $recall_borrower->branchcode,
1672         item_level => 1,
1673     })->store;
1674     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $recall_issue );
1675     is( $renewokay, 1, 'Can renew item if item-level recall on biblio is not on this item' );
1676     $recall->set_cancelled;
1677
1678     # biblio-level waiting recall: renewal allowed
1679     $recall = Koha::Recall->new({
1680         biblio_id => $recall_item1->biblionumber,
1681         item_id => undef,
1682         patron_id => $recall_borrower->borrowernumber,
1683         pickup_library_id => $recall_borrower->branchcode,
1684         item_level => 0,
1685     })->store;
1686     $recall->set_waiting({ item => $recall_item1 });
1687     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $recall_issue );
1688     is( $renewokay, 1, 'Can renew item if biblio-level recall has already been allocated an item' );
1689     $recall->set_cancelled;
1690 };
1691
1692 subtest "GetUpcomingDueIssues" => sub {
1693     plan tests => 12;
1694
1695     my $branch   = $library2->{branchcode};
1696
1697     #Create another record
1698     my $biblio2 = $builder->build_sample_biblio();
1699
1700     #Create third item
1701     my $item_1 = Koha::Items->find($reused_itemnumber_1);
1702     my $item_2 = Koha::Items->find($reused_itemnumber_2);
1703     my $item_3 = $builder->build_sample_item(
1704         {
1705             biblionumber     => $biblio2->biblionumber,
1706             library          => $branch,
1707             itype            => $itemtype,
1708         }
1709     );
1710
1711
1712     # Create a borrower
1713     my %a_borrower_data = (
1714         firstname =>  'Fridolyn',
1715         surname => 'SOMERS',
1716         categorycode => $patron_category->{categorycode},
1717         branchcode => $branch,
1718     );
1719
1720     my $a_borrower_borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1721     my $a_borrower = Koha::Patrons->find( $a_borrower_borrowernumber );
1722
1723     my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 );
1724     my $two_days_ahead = DateTime->today(time_zone => C4::Context->tz())->add( days => 2 );
1725     my $today = DateTime->today(time_zone => C4::Context->tz());
1726
1727     my $issue = AddIssue( $a_borrower, $item_1->barcode, $yesterday );
1728     my $datedue = dt_from_string( $issue->date_due() );
1729     my $issue_2 = AddIssue( $a_borrower, $item_2->barcode, $two_days_ahead );
1730     my $datedue2 = dt_from_string( $issue->date_due() );
1731
1732     my $upcoming_dues;
1733
1734     # GetUpcomingDueIssues tests
1735     for my $i(0..1) {
1736         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1737         is ( scalar( @$upcoming_dues ), 0, "No items due in less than one day ($i days in advance)" );
1738     }
1739
1740     #days_in_advance needs to be inclusive, so 1 matches items due tomorrow, 0 items due today etc.
1741     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
1742     is ( scalar ( @$upcoming_dues), 1, "Only one item due in 2 days or less" );
1743
1744     for my $i(3..5) {
1745         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1746         is ( scalar( @$upcoming_dues ), 1,
1747             "Bug 9362: Only one item due in more than 2 days ($i days in advance)" );
1748     }
1749
1750     # Bug 11218 - Due notices not generated - GetUpcomingDueIssues needs to select due today items as well
1751
1752     my $issue3 = AddIssue( $a_borrower, $item_3->barcode, $today );
1753
1754     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => -1 } );
1755     is ( scalar ( @$upcoming_dues), 0, "Overdues can not be selected" );
1756
1757     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 0 } );
1758     is ( scalar ( @$upcoming_dues), 1, "1 item is due today" );
1759
1760     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 1 } );
1761     is ( scalar ( @$upcoming_dues), 1, "1 item is due today, none tomorrow" );
1762
1763     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 }  );
1764     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1765
1766     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 3 } );
1767     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1768
1769     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues();
1770     is ( scalar ( @$upcoming_dues), 2, "days_in_advance is 7 in GetUpcomingDueIssues if not provided" );
1771
1772 };
1773
1774 subtest "Bug 13841 - Do not create new 0 amount fines" => sub {
1775     my $branch   = $library2->{branchcode};
1776
1777     my $biblio = $builder->build_sample_biblio();
1778
1779     #Create third item
1780     my $item = $builder->build_sample_item(
1781         {
1782             biblionumber     => $biblio->biblionumber,
1783             library          => $branch,
1784             itype            => $itemtype,
1785         }
1786     );
1787
1788     # Create a borrower
1789     my %a_borrower_data = (
1790         firstname =>  'Kyle',
1791         surname => 'Hall',
1792         categorycode => $patron_category->{categorycode},
1793         branchcode => $branch,
1794     );
1795
1796     my $borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1797
1798     my $borrower = Koha::Patrons->find( $borrowernumber );
1799     my $issue = AddIssue( $borrower, $item->barcode );
1800     UpdateFine(
1801         {
1802             issue_id       => $issue->id(),
1803             itemnumber     => $item->itemnumber,
1804             borrowernumber => $borrowernumber,
1805             amount         => 0,
1806             type           => q{}
1807         }
1808     );
1809
1810     my $hr = $dbh->selectrow_hashref(q{SELECT COUNT(*) AS count FROM accountlines WHERE borrowernumber = ? AND itemnumber = ?}, undef, $borrowernumber, $item->itemnumber );
1811     my $count = $hr->{count};
1812
1813     is ( $count, 0, "Calling UpdateFine on non-existant fine with an amount of 0 does not result in an empty fine" );
1814 };
1815
1816 subtest "AllowRenewalIfOtherItemsAvailable tests" => sub {
1817     plan tests => 13;
1818     my $biblio = $builder->build_sample_biblio();
1819     my $item_1 = $builder->build_sample_item(
1820         {
1821             biblionumber     => $biblio->biblionumber,
1822             library          => $library2->{branchcode},
1823         }
1824     );
1825     my $item_2= $builder->build_sample_item(
1826         {
1827             biblionumber     => $biblio->biblionumber,
1828             library          => $library2->{branchcode},
1829             itype            => $item_1->effective_itemtype,
1830         }
1831     );
1832
1833     Koha::CirculationRules->set_rules(
1834         {
1835             categorycode => undef,
1836             itemtype     => $item_1->effective_itemtype,
1837             branchcode   => undef,
1838             rules        => {
1839                 reservesallowed => 25,
1840                 holds_per_record => 25,
1841                 issuelength     => 14,
1842                 lengthunit      => 'days',
1843                 renewalsallowed => 1,
1844                 renewalperiod   => 7,
1845                 norenewalbefore => undef,
1846                 auto_renew      => 0,
1847                 fine            => .10,
1848                 chargeperiod    => 1,
1849                 maxissueqty     => 20
1850             }
1851         }
1852     );
1853
1854
1855     my $borrower1 = Koha::Patron->new({
1856         firstname    => 'Kyle',
1857         surname      => 'Hall',
1858         categorycode => $patron_category->{categorycode},
1859         branchcode   => $library2->{branchcode},
1860     })->store;
1861     my $borrowernumber2 = Koha::Patron->new({
1862         firstname    => 'Chelsea',
1863         surname      => 'Hall',
1864         categorycode => $patron_category->{categorycode},
1865         branchcode   => $library2->{branchcode},
1866     })->store->borrowernumber;
1867     my $patron_category_2 = $builder->build(
1868         {
1869             source => 'Category',
1870             value  => {
1871                 category_type                 => 'P',
1872                 enrolmentfee                  => 0,
1873                 BlockExpiredPatronOpacActions => -1, # Pick the pref value
1874             }
1875         }
1876     );
1877     my $borrowernumber3 = Koha::Patron->new({
1878         firstname    => 'Carnegie',
1879         surname      => 'Hall',
1880         categorycode => $patron_category_2->{categorycode},
1881         branchcode   => $library2->{branchcode},
1882     })->store->borrowernumber;
1883
1884     my $issue = AddIssue( $borrower1, $item_1->barcode );
1885
1886     my ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
1887     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with no hold on the record' );
1888
1889     AddReserve(
1890         {
1891             branchcode     => $library2->{branchcode},
1892             borrowernumber => $borrowernumber2,
1893             biblionumber   => $biblio->biblionumber,
1894             priority       => 1,
1895         }
1896     );
1897
1898     Koha::CirculationRules->set_rules(
1899         {
1900             categorycode => undef,
1901             itemtype     => $item_1->effective_itemtype,
1902             branchcode   => undef,
1903             rules        => {
1904                 onshelfholds => 0,
1905             }
1906         }
1907     );
1908     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1909     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
1910     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfholds are disabled' );
1911
1912     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1913     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
1914     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled and onshelfholds is disabled' );
1915
1916     Koha::CirculationRules->set_rules(
1917         {
1918             categorycode => undef,
1919             itemtype     => $item_1->effective_itemtype,
1920             branchcode   => undef,
1921             rules        => {
1922                 onshelfholds => 1,
1923             }
1924         }
1925     );
1926     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1927     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
1928     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is disabled and onshelfhold is enabled' );
1929
1930     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1931     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
1932     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled' );
1933
1934     AddReserve(
1935         {
1936             branchcode     => $library2->{branchcode},
1937             borrowernumber => $borrowernumber3,
1938             biblionumber   => $biblio->biblionumber,
1939             priority       => 1,
1940         }
1941     );
1942
1943     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
1944     is( $renewokay, 0, 'Verify the borrower cannot renew with 2 holds on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled and one other item on record' );
1945
1946     my $item_3= $builder->build_sample_item(
1947         {
1948             biblionumber     => $biblio->biblionumber,
1949             library          => $library2->{branchcode},
1950             itype            => $item_1->effective_itemtype,
1951         }
1952     );
1953
1954     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
1955     is( $renewokay, 1, 'Verify the borrower cannot renew with 2 holds on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled and two other items on record' );
1956
1957     Koha::CirculationRules->set_rules(
1958         {
1959             categorycode => $patron_category_2->{categorycode},
1960             itemtype     => $item_1->effective_itemtype,
1961             branchcode   => undef,
1962             rules        => {
1963                 reservesallowed => 0,
1964             }
1965         }
1966     );
1967
1968     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
1969     is( $renewokay, 0, 'Verify the borrower cannot renew with 2 holds on the record, but only one of those holds can be filled when AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled and two other items on record' );
1970
1971     Koha::CirculationRules->set_rules(
1972         {
1973             categorycode => $patron_category_2->{categorycode},
1974             itemtype     => $item_1->effective_itemtype,
1975             branchcode   => undef,
1976             rules        => {
1977                 reservesallowed => 25,
1978             }
1979         }
1980     );
1981
1982     # Setting item not checked out to be not for loan but holdable
1983     $item_2->notforloan(-1)->store;
1984
1985     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
1986     is( $renewokay, 0, 'Bug 14337 - Verify the borrower can not renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled but the only available item is notforloan' );
1987
1988     my $mock_circ = Test::MockModule->new("C4::Circulation");
1989     $mock_circ->mock( CanItemBeReserved => sub {
1990         warn "Checked";
1991         return { status => 'no' }
1992     } );
1993
1994     $item_2->notforloan(0)->store;
1995     $item_3->delete();
1996     # Two items total, one item available, one issued, two holds on record
1997
1998     warnings_are{
1999        ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2000     } [], "CanItemBeReserved not called when there are more possible holds than available items";
2001     is( $renewokay, 0, 'Borrower cannot renew when there are more holds than available items' );
2002
2003     $item_3 = $builder->build_sample_item(
2004         {
2005             biblionumber     => $biblio->biblionumber,
2006             library          => $library2->{branchcode},
2007             itype            => $item_1->effective_itemtype,
2008         }
2009     );
2010
2011     Koha::CirculationRules->set_rules(
2012         {
2013             categorycode => undef,
2014             itemtype     => $item_1->effective_itemtype,
2015             branchcode   => undef,
2016             rules        => {
2017                 reservesallowed => 0,
2018             }
2019         }
2020     );
2021
2022     warnings_are{
2023        ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2024     } ["Checked","Checked"], "CanItemBeReserved only called once per available item if it returns a negative result for all items for a borrower";
2025     is( $renewokay, 0, 'Borrower cannot renew when there are more holds than available items' );
2026
2027 };
2028
2029 {
2030     # Don't allow renewing onsite checkout
2031     my $branch   = $library->{branchcode};
2032
2033     #Create another record
2034     my $biblio = $builder->build_sample_biblio();
2035
2036     my $item = $builder->build_sample_item(
2037         {
2038             biblionumber     => $biblio->biblionumber,
2039             library          => $branch,
2040             itype            => $itemtype,
2041         }
2042     );
2043
2044     my $borrower = Koha::Patron->new({
2045         firstname =>  'fn',
2046         surname => 'dn',
2047         categorycode => $patron_category->{categorycode},
2048         branchcode => $branch,
2049     })->store;
2050
2051     my $issue = AddIssue( $borrower, $item->barcode, undef, undef, undef, undef, { onsite_checkout => 1 } );
2052     my ( $renewed, $error ) = CanBookBeRenewed( $borrower, $issue );
2053     is( $renewed, 0, 'CanBookBeRenewed should not allow to renew on-site checkout' );
2054     is( $error, 'onsite_checkout', 'A correct error code should be returned by CanBookBeRenewed for on-site checkout' );
2055 }
2056
2057 {
2058     my $library = $builder->build({ source => 'Branch' });
2059
2060     my $biblio = $builder->build_sample_biblio();
2061
2062     my $item = $builder->build_sample_item(
2063         {
2064             biblionumber     => $biblio->biblionumber,
2065             library          => $library->{branchcode},
2066             itype            => $itemtype,
2067         }
2068     );
2069     my $patron = $builder->build_object( { class => 'Koha::Patrons',  value => { branchcode => $library->{branchcode}, categorycode => $patron_category->{categorycode} } } );
2070
2071     my $issue = AddIssue( $patron, $item->barcode );
2072     UpdateFine(
2073         {
2074             issue_id       => $issue->id,
2075             itemnumber     => $item->itemnumber,
2076             borrowernumber => $patron->borrowernumber,
2077             amount         => 1,
2078             type           => q{}
2079         }
2080     );
2081     UpdateFine(
2082         {
2083             issue_id       => $issue->id,
2084             itemnumber     => $item->itemnumber,
2085             borrowernumber => $patron->borrowernumber,
2086             amount         => 2,
2087             type           => q{}
2088         }
2089     );
2090     is( Koha::Account::Lines->search({ issue_id => $issue->id })->count, 1, 'UpdateFine should not create a new accountline when updating an existing fine');
2091 }
2092
2093 subtest 'CanBookBeIssued & AllowReturnToBranch' => sub {
2094     plan tests => 24;
2095
2096     my $homebranch    = $builder->build( { source => 'Branch' } );
2097     my $holdingbranch = $builder->build( { source => 'Branch' } );
2098     my $otherbranch   = $builder->build( { source => 'Branch' } );
2099     my $patron_1      = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2100     my $patron_2      = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2101
2102     my $item = $builder->build_sample_item(
2103         {
2104             homebranch    => $homebranch->{branchcode},
2105             holdingbranch => $holdingbranch->{branchcode},
2106         }
2107     );
2108     Koha::CirculationRules->set_rules(
2109         {
2110             categorycode => undef,
2111             itemtype     => $item->effective_itemtype,
2112             branchcode   => undef,
2113             rules        => {
2114                 reservesallowed => 25,
2115                 issuelength     => 14,
2116                 lengthunit      => 'days',
2117                 renewalsallowed => 1,
2118                 renewalperiod   => 7,
2119                 norenewalbefore => undef,
2120                 auto_renew      => 0,
2121                 fine            => .10,
2122                 chargeperiod    => 1,
2123                 maxissueqty     => 20
2124             }
2125         }
2126     );
2127
2128     set_userenv($holdingbranch);
2129
2130     my $issue = AddIssue( $patron_1, $item->barcode );
2131     is( ref($issue), 'Koha::Checkout', 'AddIssue should return a Koha::Checkout object' );
2132
2133     my ( $error, $question, $alerts );
2134
2135     # AllowReturnToBranch == anywhere
2136     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
2137     ## Test that unknown barcodes don't generate internal server errors
2138     set_userenv($homebranch);
2139     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, 'KohaIsAwesome' );
2140     ok( $error->{UNKNOWN_BARCODE}, '"KohaIsAwesome" is not a valid barcode as expected.' );
2141     ## Can be issued from homebranch
2142     set_userenv($homebranch);
2143     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2144     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2145     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2146     ## Can be issued from holdingbranch
2147     set_userenv($holdingbranch);
2148     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2149     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2150     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2151     ## Can be issued from another branch
2152     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2153     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2154     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2155
2156     # AllowReturnToBranch == holdingbranch
2157     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
2158     ## Cannot be issued from homebranch
2159     set_userenv($homebranch);
2160     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2161     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2162     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
2163     is( $error->{branch_to_return},         $holdingbranch->{branchcode}, 'branch_to_return matched holdingbranch' );
2164     ## Can be issued from holdinbranch
2165     set_userenv($holdingbranch);
2166     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2167     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2168     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2169     ## Cannot be issued from another branch
2170     set_userenv($otherbranch);
2171     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2172     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2173     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
2174     is( $error->{branch_to_return},         $holdingbranch->{branchcode}, 'branch_to_return matches holdingbranch' );
2175
2176     # AllowReturnToBranch == homebranch
2177     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
2178     ## Can be issued from holdinbranch
2179     set_userenv($homebranch);
2180     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2181     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2182     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2183     ## Cannot be issued from holdinbranch
2184     set_userenv($holdingbranch);
2185     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2186     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2187     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
2188     is( $error->{branch_to_return},         $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
2189     ## Cannot be issued from holdinbranch
2190     set_userenv($otherbranch);
2191     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2192     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2193     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
2194     is( $error->{branch_to_return},         $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
2195
2196     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
2197 };
2198
2199 subtest 'AddIssue & AllowReturnToBranch' => sub {
2200     plan tests => 9;
2201
2202     my $homebranch    = $builder->build( { source => 'Branch' } );
2203     my $holdingbranch = $builder->build( { source => 'Branch' } );
2204     my $otherbranch   = $builder->build( { source => 'Branch' } );
2205
2206     my $patron_1  = $builder->build_object({
2207         class => 'Koha::Patrons',
2208         value => { categorycode => $patron_category->{categorycode} }
2209     });
2210
2211     my $patron_2  = $builder->build_object({
2212         class => 'Koha::Patrons',
2213         value => { categorycode => $patron_category->{categorycode} }
2214     });
2215
2216
2217     my $item = $builder->build_sample_item(
2218         {
2219             homebranch    => $homebranch->{branchcode},
2220             holdingbranch => $holdingbranch->{branchcode},
2221         }
2222     );
2223
2224     set_userenv($holdingbranch);
2225
2226     my $ref_issue = 'Koha::Checkout';
2227     my $issue = AddIssue( $patron_1, $item->barcode );
2228
2229     my ( $error, $question, $alerts );
2230
2231     # AllowReturnToBranch == homebranch
2232     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
2233     ## Can be issued from homebranch
2234     set_userenv($homebranch);
2235     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from homebranch');
2236     set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
2237     ## Can be issued from holdinbranch
2238     set_userenv($holdingbranch);
2239     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from holdingbranch');
2240     set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
2241     ## Can be issued from another branch
2242     set_userenv($otherbranch);
2243     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from otherbranch');
2244     set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
2245
2246     # AllowReturnToBranch == holdinbranch
2247     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
2248     ## Cannot be issued from homebranch
2249     set_userenv($homebranch);
2250     is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from homebranch');
2251     ## Can be issued from holdingbranch
2252     set_userenv($holdingbranch);
2253     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - holdingbranch | Can be issued from holdingbranch');
2254     set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
2255     ## Cannot be issued from another branch
2256     set_userenv($otherbranch);
2257     is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from otherbranch');
2258
2259     # AllowReturnToBranch == homebranch
2260     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
2261     ## Can be issued from homebranch
2262     set_userenv($homebranch);
2263     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - homebranch | Can be issued from homebranch' );
2264     AddIssue( $patron_1, $item->barcode ); # Re-issue to patron 1
2265     ## Cannot be issued from holdinbranch
2266     set_userenv($holdingbranch);
2267     is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from holdingbranch' );
2268     ## Cannot be issued from another branch
2269     set_userenv($otherbranch);
2270     is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from otherbranch' );
2271     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
2272 };
2273
2274 subtest 'AddIssue | recalls' => sub {
2275     plan tests => 3;
2276
2277     t::lib::Mocks::mock_preference("UseRecalls", 1);
2278     t::lib::Mocks::mock_preference("item-level_itypes", 1);
2279     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
2280     my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
2281     my $item = $builder->build_sample_item;
2282     Koha::CirculationRules->set_rules({
2283         branchcode => undef,
2284         itemtype => undef,
2285         categorycode => undef,
2286         rules => {
2287             recalls_allowed => 10,
2288         },
2289     });
2290
2291     # checking out item that they have recalled
2292     my $recall1 = Koha::Recall->new(
2293         {   patron_id         => $patron1->borrowernumber,
2294             biblio_id         => $item->biblionumber,
2295             item_id           => $item->itemnumber,
2296             item_level        => 1,
2297             pickup_library_id => $patron1->branchcode,
2298         }
2299     )->store;
2300     AddIssue( $patron1, $item->barcode, undef, undef, undef, undef, { recall_id => $recall1->id } );
2301     $recall1 = Koha::Recalls->find( $recall1->id );
2302     is( $recall1->fulfilled, 1, 'Recall was fulfilled when patron checked out item' );
2303     AddReturn( $item->barcode, $item->homebranch );
2304
2305     # this item is has a recall request. cancel recall
2306     my $recall2 = Koha::Recall->new(
2307         {   patron_id         => $patron2->borrowernumber,
2308             biblio_id         => $item->biblionumber,
2309             item_id           => $item->itemnumber,
2310             item_level        => 1,
2311             pickup_library_id => $patron2->branchcode,
2312         }
2313     )->store;
2314     AddIssue( $patron1, $item->barcode, undef, undef, undef, undef, { recall_id => $recall2->id, cancel_recall => 'cancel' } );
2315     $recall2 = Koha::Recalls->find( $recall2->id );
2316     is( $recall2->cancelled, 1, 'Recall was cancelled when patron checked out item' );
2317     AddReturn( $item->barcode, $item->homebranch );
2318
2319     # this item is waiting to fulfill a recall. revert recall
2320     my $recall3 = Koha::Recall->new(
2321         {   patron_id         => $patron2->borrowernumber,
2322             biblio_id         => $item->biblionumber,
2323             item_id           => $item->itemnumber,
2324             item_level        => 1,
2325             pickup_library_id => $patron2->branchcode,
2326         }
2327     )->store;
2328     $recall3->set_waiting;
2329     AddIssue( $patron1, $item->barcode, undef, undef, undef, undef, { recall_id => $recall3->id, cancel_recall => 'revert' } );
2330     $recall3 = Koha::Recalls->find( $recall3->id );
2331     is( $recall3->requested, 1, 'Recall was reverted from waiting when patron checked out item' );
2332     AddReturn( $item->barcode, $item->homebranch );
2333 };
2334
2335 subtest 'AddIssue & illrequests.due_date' => sub {
2336     plan tests => 2;
2337
2338     t::lib::Mocks::mock_preference( 'ILLModule', 1 );
2339     my $library = $builder->build( { source => 'Branch' } );
2340     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2341     my $item = $builder->build_sample_item();
2342
2343     set_userenv($library);
2344
2345     my $custom_date_due = '9999-12-18 12:34:56';
2346     my $expected_date_due = '9999-12-18 23:59:00';
2347     my $illrequest = Koha::Illrequest->new({
2348         borrowernumber => $patron->borrowernumber,
2349         biblio_id => $item->biblionumber,
2350         branchcode => $library->{'branchcode'},
2351         due_date => $custom_date_due,
2352     })->store;
2353
2354     my $issue = AddIssue( $patron, $item->barcode );
2355     is( $issue->date_due, $expected_date_due, 'Custom illrequest date due has been set for this issue');
2356
2357     $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2358     $item = $builder->build_sample_item();
2359     $custom_date_due = '9999-12-19';
2360     $expected_date_due = '9999-12-19 23:59:00';
2361     $illrequest = Koha::Illrequest->new({
2362         borrowernumber => $patron->borrowernumber,
2363         biblio_id => $item->biblionumber,
2364         branchcode => $library->{'branchcode'},
2365         due_date => $custom_date_due,
2366     })->store;
2367
2368     $issue = AddIssue( $patron, $item->barcode );
2369     is( $issue->date_due, $expected_date_due, 'Custom illrequest date due has been set for this issue');
2370 };
2371
2372 subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
2373     plan tests => 8;
2374
2375     my $library = $builder->build( { source => 'Branch' } );
2376     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2377     my $item_1 = $builder->build_sample_item(
2378         {
2379             library => $library->{branchcode},
2380         }
2381     );
2382     my $item_2 = $builder->build_sample_item(
2383         {
2384             library => $library->{branchcode},
2385         }
2386     );
2387     Koha::CirculationRules->set_rules(
2388         {
2389             categorycode => undef,
2390             itemtype     => undef,
2391             branchcode   => $library->{branchcode},
2392             rules        => {
2393                 reservesallowed => 25,
2394                 issuelength     => 14,
2395                 lengthunit      => 'days',
2396                 renewalsallowed => 1,
2397                 renewalperiod   => 7,
2398                 norenewalbefore => undef,
2399                 auto_renew      => 0,
2400                 fine            => .10,
2401                 chargeperiod    => 1,
2402                 maxissueqty     => 20
2403             }
2404         }
2405     );
2406
2407     my ( $error, $question, $alerts );
2408
2409     # Patron cannot issue item_1, they have overdues
2410     my $yesterday = DateTime->today( time_zone => C4::Context->tz() )->add( days => -1 );
2411     my $issue = AddIssue( $patron, $item_1->barcode, $yesterday );    # Add an overdue
2412
2413     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'confirmation' );
2414     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2415     is( keys(%$error) + keys(%$alerts),  0, 'No key for error and alert' . str($error, $question, $alerts) );
2416     is( $question->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=confirmation, USERBLOCKEDOVERDUE should be set for question' );
2417
2418     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'block' );
2419     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2420     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
2421     is( $error->{USERBLOCKEDOVERDUE},      1, 'OverduesBlockCirc=block, USERBLOCKEDOVERDUE should be set for error' );
2422
2423     # Patron cannot issue item_1, they are debarred
2424     my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 );
2425     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber, expiration => $tomorrow } );
2426     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2427     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
2428     is( $error->{USERBLOCKEDWITHENDDATE}, output_pref( { dt => $tomorrow, dateformat => 'sql', dateonly => 1 } ), 'USERBLOCKEDWITHENDDATE should be tomorrow' );
2429
2430     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber } );
2431     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2432     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
2433     is( $error->{USERBLOCKEDNOENDDATE},    '9999-12-31', 'USERBLOCKEDNOENDDATE should be 9999-12-31 for unlimited debarments' );
2434 };
2435
2436 subtest 'CanBookBeIssued + Statistic patrons "X"' => sub {
2437     plan tests => 11;
2438
2439     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
2440     my $patron_category_x = $builder->build_object(
2441         {
2442             class => 'Koha::Patron::Categories',
2443             value => { category_type => 'X' }
2444         }
2445     );
2446     my $patron = $builder->build_object(
2447         {
2448             class => 'Koha::Patrons',
2449             value => {
2450                 categorycode  => $patron_category_x->categorycode,
2451                 gonenoaddress => undef,
2452                 lost          => undef,
2453                 debarred      => undef,
2454                 borrowernotes => ""
2455             }
2456         }
2457     );
2458     my $item_1 = $builder->build_sample_item(
2459         {
2460             library => $library->{branchcode},
2461         }
2462     );
2463
2464     my ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_1->barcode );
2465     is( $error->{STATS}, 1, '"Error" flag "STATS" must be set if CanBookBeIssued is called with a statistic patron (category_type=X)' );
2466
2467     my $stat = Koha::Statistics->search( { itemnumber => $item_1->itemnumber } )->next;
2468     is( $stat->branch,         C4::Context->userenv->{'branch'}, 'Recorded a branch' );
2469     is( $stat->type,           'localuse',                       'Recorded type as localuse' );
2470     is( $stat->itemnumber,     $item_1->itemnumber,              'Recorded an itemnumber' );
2471     is( $stat->itemtype,       $item_1->effective_itemtype,      'Recorded an itemtype' );
2472     is( $stat->borrowernumber, $patron->borrowernumber,          'Recorded a borrower number' );
2473     is( $stat->ccode,          $item_1->ccode,                   'Recorded a collection code' );
2474     is( $stat->categorycode,   $patron->categorycode,            'Recorded a categorycode' );
2475     is( $stat->location,       $item_1->location,                'Recorded a location' );
2476
2477
2478     t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
2479     my $patron_2  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2480     my $item_2 = $builder->build_sample_item( { library => $library->branchcode } );
2481     my $issue = AddIssue( $patron_2, $item_2->barcode );
2482     $item_2->discard_changes;
2483     ok($item_2->onloan, "Item is checked out");
2484
2485     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2486     $item_2->discard_changes;
2487     ok(!$item_2->onloan, "Checked out item is returned");
2488
2489     # TODO There are other tests to provide here
2490 };
2491
2492 subtest 'MultipleReserves' => sub {
2493     plan tests => 3;
2494
2495     my $biblio = $builder->build_sample_biblio();
2496
2497     my $branch = $library2->{branchcode};
2498
2499     my $item_1 = $builder->build_sample_item(
2500         {
2501             biblionumber     => $biblio->biblionumber,
2502             library          => $branch,
2503             replacementprice => 12.00,
2504             itype            => $itemtype,
2505         }
2506     );
2507
2508     my $item_2 = $builder->build_sample_item(
2509         {
2510             biblionumber     => $biblio->biblionumber,
2511             library          => $branch,
2512             replacementprice => 12.00,
2513             itype            => $itemtype,
2514         }
2515     );
2516
2517     my $bibitems       = '';
2518     my $priority       = '1';
2519     my $resdate        = undef;
2520     my $expdate        = undef;
2521     my $notes          = '';
2522     my $checkitem      = undef;
2523     my $found          = undef;
2524
2525     my %renewing_borrower_data = (
2526         firstname =>  'John',
2527         surname => 'Renewal',
2528         categorycode => $patron_category->{categorycode},
2529         branchcode => $branch,
2530     );
2531     my $patron = Koha::Patron->new(\%renewing_borrower_data)->store;
2532     my $issue = AddIssue( $patron, $item_1->barcode);
2533     my $datedue = dt_from_string( $issue->date_due() );
2534     is (defined $issue->date_due(), 1, "item 1 checked out");
2535     my $borrowing_borrowernumber = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber })->borrowernumber;
2536
2537     my %reserving_borrower_data1 = (
2538         firstname =>  'Katrin',
2539         surname => 'Reservation',
2540         categorycode => $patron_category->{categorycode},
2541         branchcode => $branch,
2542     );
2543     my $reserving_borrowernumber1 = Koha::Patron->new(\%reserving_borrower_data1)->store->borrowernumber;
2544     AddReserve(
2545         {
2546             branchcode       => $branch,
2547             borrowernumber   => $reserving_borrowernumber1,
2548             biblionumber     => $biblio->biblionumber,
2549             priority         => $priority,
2550             reservation_date => $resdate,
2551             expiration_date  => $expdate,
2552             notes            => $notes,
2553             itemnumber       => $checkitem,
2554             found            => $found,
2555         }
2556     );
2557
2558     my %reserving_borrower_data2 = (
2559         firstname =>  'Kirk',
2560         surname => 'Reservation',
2561         categorycode => $patron_category->{categorycode},
2562         branchcode => $branch,
2563     );
2564     my $reserving_borrowernumber2 = Koha::Patron->new(\%reserving_borrower_data2)->store->borrowernumber;
2565     AddReserve(
2566         {
2567             branchcode       => $branch,
2568             borrowernumber   => $reserving_borrowernumber2,
2569             biblionumber     => $biblio->biblionumber,
2570             priority         => $priority,
2571             reservation_date => $resdate,
2572             expiration_date  => $expdate,
2573             notes            => $notes,
2574             itemnumber       => $checkitem,
2575             found            => $found,
2576         }
2577     );
2578
2579     {
2580         my ( $renewokay, $error ) = CanBookBeRenewed($patron, $issue, 1);
2581         is($renewokay, 0, 'Bug 17941 - should cover the case where 2 books are both reserved, so failing');
2582     }
2583
2584     my $item_3 = $builder->build_sample_item(
2585         {
2586             biblionumber     => $biblio->biblionumber,
2587             library          => $branch,
2588             replacementprice => 12.00,
2589             itype            => $itemtype,
2590         }
2591     );
2592
2593     {
2594         my ( $renewokay, $error ) = CanBookBeRenewed($patron, $issue, 1);
2595         is($renewokay, 1, 'Bug 17941 - should cover the case where 2 books are reserved, but a third one is available');
2596     }
2597 };
2598
2599 subtest 'CanBookBeIssued + AllowMultipleIssuesOnABiblio' => sub {
2600     plan tests => 5;
2601
2602     my $library = $builder->build( { source => 'Branch' } );
2603     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2604
2605     my $biblionumber = $builder->build_sample_biblio(
2606         {
2607             branchcode => $library->{branchcode},
2608         }
2609     )->biblionumber;
2610     my $item_1 = $builder->build_sample_item(
2611         {
2612             biblionumber => $biblionumber,
2613             library      => $library->{branchcode},
2614         }
2615     );
2616
2617     my $item_2 = $builder->build_sample_item(
2618         {
2619             biblionumber => $biblionumber,
2620             library      => $library->{branchcode},
2621         }
2622     );
2623
2624     Koha::CirculationRules->set_rules(
2625         {
2626             categorycode => undef,
2627             itemtype     => undef,
2628             branchcode   => $library->{branchcode},
2629             rules        => {
2630                 reservesallowed => 25,
2631                 issuelength     => 14,
2632                 lengthunit      => 'days',
2633                 renewalsallowed => 1,
2634                 renewalperiod   => 7,
2635                 norenewalbefore => undef,
2636                 auto_renew      => 0,
2637                 fine            => .10,
2638                 chargeperiod    => 1,
2639                 maxissueqty     => 20
2640             }
2641         }
2642     );
2643
2644     my ( $error, $question, $alerts );
2645     my $issue = AddIssue( $patron, $item_1->barcode, dt_from_string->add( days => 1 ) );
2646
2647     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
2648     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2649     cmp_deeply(
2650         { error => $error, alerts => $alerts },
2651         { error => {}, alerts => {} },
2652         'No error or alert should be raised'
2653     );
2654     is( $question->{BIBLIO_ALREADY_ISSUED}, 1, 'BIBLIO_ALREADY_ISSUED question flag should be set if AllowMultipleIssuesOnABiblio=0 and issue already exists' );
2655
2656     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
2657     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2658     cmp_deeply(
2659         { error => $error, question => $question, alerts => $alerts },
2660         { error => {}, question => {}, alerts => {} },
2661         'No BIBLIO_ALREADY_ISSUED flag should be set if AllowMultipleIssuesOnABiblio=1'
2662     );
2663
2664     # Add a subscription
2665     Koha::Subscription->new({ biblionumber => $biblionumber })->store;
2666
2667     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
2668     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2669     cmp_deeply(
2670         { error => $error, question => $question, alerts => $alerts },
2671         { error => {}, question => {}, alerts => {} },
2672         'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
2673     );
2674
2675     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
2676     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2677     cmp_deeply(
2678         { error => $error, question => $question, alerts => $alerts },
2679         { error => {}, question => {}, alerts => {} },
2680         'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
2681     );
2682 };
2683
2684 subtest 'AddReturn + CumulativeRestrictionPeriods' => sub {
2685     plan tests => 8;
2686
2687     my $library = $builder->build( { source => 'Branch' } );
2688     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2689
2690     # Add 2 items
2691     my $biblionumber = $builder->build_sample_biblio(
2692         {
2693             branchcode => $library->{branchcode},
2694         }
2695     )->biblionumber;
2696     my $item_1 = $builder->build_sample_item(
2697         {
2698             biblionumber => $biblionumber,
2699             library      => $library->{branchcode},
2700         }
2701     );
2702     my $item_2 = $builder->build_sample_item(
2703         {
2704             biblionumber => $biblionumber,
2705             library      => $library->{branchcode},
2706         }
2707     );
2708
2709     # And the circulation rule
2710     Koha::CirculationRules->search->delete;
2711     Koha::CirculationRules->set_rules(
2712         {
2713             categorycode => undef,
2714             itemtype     => undef,
2715             branchcode   => undef,
2716             rules        => {
2717                 issuelength => 1,
2718                 firstremind => 1,        # 1 day of grace
2719                 finedays    => 2,        # 2 days of fine per day of overdue
2720                 lengthunit  => 'days',
2721             }
2722         }
2723     );
2724
2725     # Patron cannot issue item_1, they have overdues
2726     my $now = dt_from_string;
2727     my $five_days_ago = $now->clone->subtract( days => 5 );
2728     my $ten_days_ago  = $now->clone->subtract( days => 10 );
2729     AddIssue( $patron, $item_1->barcode, $five_days_ago );    # Add an overdue
2730     AddIssue( $patron, $item_2->barcode, $ten_days_ago )
2731       ;    # Add another overdue
2732
2733     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '0' );
2734     AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
2735     my $suspensions = $patron->restrictions->search( { type => 'SUSPENSION' } );
2736     is( $suspensions->count, 1, "Suspension added" );
2737     my $THE_suspension = $suspensions->next;
2738
2739     # FIXME Is it right? I'd have expected 5 * 2 - 1 instead
2740     # Same for the others
2741     my $expected_expiration = output_pref(
2742         {
2743             dt         => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2744             dateformat => 'sql',
2745             dateonly   => 1
2746         }
2747     );
2748     is( $THE_suspension->expiration, $expected_expiration, "Suspesion expiration set" );
2749
2750     AddReturn( $item_2->barcode, $library->{branchcode}, undef, $now );
2751     $suspensions = $patron->restrictions->search( { type => 'SUSPENSION' } );
2752     is( $suspensions->count, 1, "Only one suspension" );
2753     $THE_suspension = $suspensions->next;
2754
2755     $expected_expiration = output_pref(
2756         {
2757             dt         => $now->clone->add( days => ( 10 - 1 ) * 2 ),
2758             dateformat => 'sql',
2759             dateonly   => 1
2760         }
2761     );
2762     is( $THE_suspension->expiration, $expected_expiration, "Suspension expiration date updated" );
2763
2764     Koha::Patron::Debarments::DelUniqueDebarment(
2765         { borrowernumber => $patron->borrowernumber, type => 'SUSPENSION' } );
2766
2767     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '1' );
2768     AddIssue( $patron, $item_1->barcode, $five_days_ago );    # Add an overdue
2769     AddIssue( $patron, $item_2->barcode, $ten_days_ago )
2770       ;    # Add another overdue
2771     AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
2772     $suspensions = $patron->restrictions->search( { type => 'SUSPENSION' } );
2773     is( $suspensions->count, 1, "Only one suspension" );
2774     $THE_suspension = $suspensions->next;
2775
2776     $expected_expiration = output_pref(
2777         {
2778             dt         => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2779             dateformat => 'sql',
2780             dateonly   => 1
2781         }
2782     );
2783     is( $THE_suspension->expiration, $expected_expiration, "Suspension expiration date updated" );
2784
2785     AddReturn( $item_2->barcode, $library->{branchcode}, undef, $now );
2786     $suspensions = $patron->restrictions->search( { type => 'SUSPENSION' } );
2787     is( $suspensions->count, 1, "Only one suspension" );
2788     $THE_suspension = $suspensions->next;
2789
2790     $expected_expiration = output_pref(
2791         {
2792             dt => $now->clone->add( days => ( 5 - 1 ) * 2 + ( 10 - 1 ) * 2 ),
2793             dateformat => 'sql',
2794             dateonly   => 1
2795         }
2796     );
2797     is( $THE_suspension->expiration, $expected_expiration, "Suspension expiration date updated" );
2798 };
2799
2800 subtest 'AddReturn + suspension_chargeperiod' => sub {
2801     plan tests => 29;
2802
2803     my $library = $builder->build( { source => 'Branch' } );
2804     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2805
2806     my $biblionumber = $builder->build_sample_biblio(
2807         {
2808             branchcode => $library->{branchcode},
2809         }
2810     )->biblionumber;
2811     my $item_1 = $builder->build_sample_item(
2812         {
2813             biblionumber => $biblionumber,
2814             library      => $library->{branchcode},
2815         }
2816     );
2817
2818     # And the issuing rule
2819     Koha::CirculationRules->search->delete;
2820     Koha::CirculationRules->set_rules(
2821         {
2822             categorycode => '*',
2823             itemtype     => '*',
2824             branchcode   => '*',
2825             rules        => {
2826                 issuelength => 1,
2827                 firstremind => 0,    # 0 day of grace
2828                 finedays    => 2,    # 2 days of fine per day of overdue
2829                 suspension_chargeperiod => 1,
2830                 lengthunit              => 'days',
2831             }
2832         }
2833     );
2834
2835     my $now = dt_from_string;
2836     my $five_days_ago = $now->clone->subtract( days => 5 );
2837     # We want to charge 2 days every day, without grace
2838     # With 5 days of overdue: 5 * Z
2839     my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
2840     test_debarment_on_checkout(
2841         {
2842             item            => $item_1,
2843             library         => $library,
2844             patron          => $patron,
2845             due_date        => $five_days_ago,
2846             expiration_date => $expected_expiration,
2847         }
2848     );
2849
2850     # Same with undef firstremind
2851     Koha::CirculationRules->search->delete;
2852     Koha::CirculationRules->set_rules(
2853         {
2854             categorycode => '*',
2855             itemtype     => '*',
2856             branchcode   => '*',
2857             rules        => {
2858                 issuelength => 1,
2859                 firstremind => undef,    # 0 day of grace
2860                 finedays    => 2,    # 2 days of fine per day of overdue
2861                 suspension_chargeperiod => 1,
2862                 lengthunit              => 'days',
2863             }
2864         }
2865     );
2866     {
2867     my $now = dt_from_string;
2868     my $five_days_ago = $now->clone->subtract( days => 5 );
2869     # We want to charge 2 days every day, without grace
2870     # With 5 days of overdue: 5 * Z
2871     my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
2872     test_debarment_on_checkout(
2873         {
2874             item            => $item_1,
2875             library         => $library,
2876             patron          => $patron,
2877             due_date        => $five_days_ago,
2878             expiration_date => $expected_expiration,
2879         }
2880     );
2881     }
2882     # We want to charge 2 days every 2 days, without grace
2883     # With 5 days of overdue: (5 * 2) / 2
2884     Koha::CirculationRules->set_rule(
2885         {
2886             categorycode => undef,
2887             branchcode   => undef,
2888             itemtype     => undef,
2889             rule_name    => 'suspension_chargeperiod',
2890             rule_value   => '2',
2891         }
2892     );
2893
2894     $expected_expiration = $now->clone->add( days => floor( 5 * 2 ) / 2 );
2895     test_debarment_on_checkout(
2896         {
2897             item            => $item_1,
2898             library         => $library,
2899             patron          => $patron,
2900             due_date        => $five_days_ago,
2901             expiration_date => $expected_expiration,
2902         }
2903     );
2904
2905     # We want to charge 2 days every 3 days, with 1 day of grace
2906     # With 5 days of overdue: ((5-1) / 3 ) * 2
2907     Koha::CirculationRules->set_rules(
2908         {
2909             categorycode => undef,
2910             branchcode   => undef,
2911             itemtype     => undef,
2912             rules        => {
2913                 suspension_chargeperiod => 3,
2914                 firstremind             => 1,
2915             }
2916         }
2917     );
2918     $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 1 ) / 3 ) * 2 ) );
2919     test_debarment_on_checkout(
2920         {
2921             item            => $item_1,
2922             library         => $library,
2923             patron          => $patron,
2924             due_date        => $five_days_ago,
2925             expiration_date => $expected_expiration,
2926         }
2927     );
2928
2929     # Use finesCalendar to know if holiday must be skipped to calculate the due date
2930     # We want to charge 2 days every days, with 0 day of grace (to not burn brains)
2931     Koha::CirculationRules->set_rules(
2932         {
2933             categorycode => undef,
2934             branchcode   => undef,
2935             itemtype     => undef,
2936             rules        => {
2937                 finedays                => 2,
2938                 suspension_chargeperiod => 1,
2939                 firstremind             => 0,
2940             }
2941         }
2942     );
2943     t::lib::Mocks::mock_preference('finesCalendar', 'noFinesWhenClosed');
2944     t::lib::Mocks::mock_preference('SuspensionsCalendar', 'noSuspensionsWhenClosed');
2945
2946     # Adding a holiday 2 days ago
2947     my $calendar = C4::Calendar->new(branchcode => $library->{branchcode});
2948     my $two_days_ago = $now->clone->subtract( days => 2 );
2949     $calendar->insert_single_holiday(
2950         day             => $two_days_ago->day,
2951         month           => $two_days_ago->month,
2952         year            => $two_days_ago->year,
2953         title           => 'holidayTest-2d',
2954         description     => 'holidayDesc 2 days ago'
2955     );
2956     # With 5 days of overdue, only 4 (x finedays=2) days must charged (one was an holiday)
2957     $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) );
2958     test_debarment_on_checkout(
2959         {
2960             item            => $item_1,
2961             library         => $library,
2962             patron          => $patron,
2963             due_date        => $five_days_ago,
2964             expiration_date => $expected_expiration,
2965         }
2966     );
2967
2968     # Adding a holiday 2 days ahead, with finesCalendar=noFinesWhenClosed it should be skipped
2969     my $two_days_ahead = $now->clone->add( days => 2 );
2970     $calendar->insert_single_holiday(
2971         day             => $two_days_ahead->day,
2972         month           => $two_days_ahead->month,
2973         year            => $two_days_ahead->year,
2974         title           => 'holidayTest+2d',
2975         description     => 'holidayDesc 2 days ahead'
2976     );
2977
2978     # Same as above, but we should skip D+2
2979     $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) + 1 );
2980     test_debarment_on_checkout(
2981         {
2982             item            => $item_1,
2983             library         => $library,
2984             patron          => $patron,
2985             due_date        => $five_days_ago,
2986             expiration_date => $expected_expiration,
2987         }
2988     );
2989
2990     # Adding another holiday, day of expiration date
2991     my $expected_expiration_dt = dt_from_string($expected_expiration);
2992     $calendar->insert_single_holiday(
2993         day             => $expected_expiration_dt->day,
2994         month           => $expected_expiration_dt->month,
2995         year            => $expected_expiration_dt->year,
2996         title           => 'holidayTest_exp',
2997         description     => 'holidayDesc on expiration date'
2998     );
2999     # Expiration date will be the day after
3000     test_debarment_on_checkout(
3001         {
3002             item            => $item_1,
3003             library         => $library,
3004             patron          => $patron,
3005             due_date        => $five_days_ago,
3006             expiration_date => $expected_expiration_dt->clone->add( days => 1 ),
3007         }
3008     );
3009
3010     test_debarment_on_checkout(
3011         {
3012             item            => $item_1,
3013             library         => $library,
3014             patron          => $patron,
3015             return_date     => $now->clone->add(days => 5),
3016             expiration_date => $now->clone->add(days => 5 + (5 * 2 - 1) ),
3017         }
3018     );
3019
3020     test_debarment_on_checkout(
3021         {
3022             item            => $item_1,
3023             library         => $library,
3024             patron          => $patron,
3025             due_date        => $now->clone->add(days => 1),
3026             return_date     => $now->clone->add(days => 5),
3027             expiration_date => $now->clone->add(days => 5 + (4 * 2 - 1) ),
3028         }
3029     );
3030
3031     Koha::CirculationRules->search->delete;
3032     Koha::CirculationRules->set_rules(
3033         {
3034             categorycode => undef,
3035             itemtype     => undef,
3036             branchcode   => undef,
3037             rules => {
3038                 finedays   => 0,
3039                 lengthunit => 'days',
3040               }
3041         }
3042     );
3043
3044     Koha::Patron::Debarments::AddDebarment(
3045         {
3046             borrowernumber => $patron->borrowernumber,
3047             expiration     => '9999-12-31',
3048             type           => 'MANUAL',
3049         }
3050     );
3051
3052     AddIssue( $patron, $item_1->barcode, $now->clone->subtract( days => 1 ) );
3053     my ( undef, $message ) = AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
3054     is( $message->{WasReturned} && exists $message->{ForeverDebarred}, 1, 'Forever debarred message for Addreturn when overdue');
3055
3056     Koha::Patron::Debarments::DelUniqueDebarment(
3057         {
3058             borrowernumber => $patron->borrowernumber,
3059             type           => 'MANUAL',
3060         }
3061     );
3062     Koha::Patron::Debarments::AddDebarment(
3063         {
3064             borrowernumber => $patron->borrowernumber,
3065             expiration     => $now->clone->add( days => 10 ),
3066             type           => 'MANUAL',
3067         }
3068     );
3069
3070     AddIssue( $patron, $item_1->barcode, $now->clone->subtract( days => 1 ) );
3071     (undef, $message) = AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
3072     is( $message->{WasReturned} && exists $message->{PrevDebarred}, 1, 'Previously debarred message for Addreturn when overdue');
3073 };
3074
3075 subtest 'CanBookBeIssued + AutoReturnCheckedOutItems' => sub {
3076     plan tests => 2;
3077
3078     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
3079     my $patron1 = $builder->build_object(
3080         {
3081             class => 'Koha::Patrons',
3082             value => {
3083                 branchcode   => $library->branchcode,
3084                 categorycode => $patron_category->{categorycode}
3085             }
3086         }
3087     );
3088     my $patron2 = $builder->build_object(
3089         {
3090             class => 'Koha::Patrons',
3091             value => {
3092                 branchcode   => $library->branchcode,
3093                 categorycode => $patron_category->{categorycode}
3094             }
3095         }
3096     );
3097
3098     t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
3099
3100     my $item = $builder->build_sample_item(
3101         {
3102             library      => $library->branchcode,
3103         }
3104     );
3105
3106     my ( $error, $question, $alerts );
3107     my $issue = AddIssue( $patron1, $item->barcode );
3108
3109     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
3110     ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->barcode );
3111     is( $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER question flag should be set if AutoReturnCheckedOutItems is disabled and item is checked out to another' );
3112
3113     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 1);
3114     ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->barcode );
3115     is( $alerts->{RETURNED_FROM_ANOTHER}->{patron}->borrowernumber, $patron1->borrowernumber, 'RETURNED_FROM_ANOTHER alert flag should be set if AutoReturnCheckedOutItems is enabled and item is checked out to another' );
3116
3117     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
3118 };
3119
3120
3121 subtest 'AddReturn | is_overdue' => sub {
3122     plan tests => 9;
3123
3124     t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'batchmod|moredetail|cronjob|additem|pendingreserves|onpayment');
3125     t::lib::Mocks::mock_preference('CalculateFinesOnReturn', 1);
3126     t::lib::Mocks::mock_preference('finesMode', 'production');
3127     t::lib::Mocks::mock_preference('MaxFine', '100');
3128
3129     my $library = $builder->build( { source => 'Branch' } );
3130     my $patron  = $builder->build_object(
3131         {
3132             class => 'Koha::Patrons',
3133             value => { categorycode => $patron_category->{categorycode} }
3134         }
3135     );
3136     my $manager = $builder->build_object( { class => "Koha::Patrons" } );
3137     t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
3138
3139     my $item = $builder->build_sample_item(
3140         {
3141             library      => $library->{branchcode},
3142             replacementprice => 7
3143         }
3144     );
3145
3146     Koha::CirculationRules->search->delete;
3147     Koha::CirculationRules->set_rules(
3148         {
3149             categorycode => undef,
3150             itemtype     => undef,
3151             branchcode   => undef,
3152             rules        => {
3153                 issuelength  => 6,
3154                 lengthunit   => 'days',
3155                 fine         => 1,        # Charge 1 every day of overdue
3156                 chargeperiod => 1,
3157             }
3158         }
3159     );
3160
3161     my $now   = dt_from_string;
3162     my $one_day_ago   = $now->clone->subtract( days => 1 );
3163     my $two_days_ago  = $now->clone->subtract( days => 2 );
3164     my $five_days_ago = $now->clone->subtract( days => 5 );
3165     my $ten_days_ago  = $now->clone->subtract( days => 10 );
3166
3167     # No return date specified, today will be used => 10 days overdue charged
3168     AddIssue( $patron, $item->barcode, $ten_days_ago ); # date due was 10d ago
3169     AddReturn( $item->barcode, $library->{branchcode} );
3170     is( int($patron->account->balance()), 10, 'Patron should have a charge of 10 (10 days x 1)' );
3171     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3172
3173     # specify return date 5 days before => no overdue charged
3174     AddIssue( $patron, $item->barcode, $five_days_ago ); # date due was 5d ago
3175     AddReturn( $item->barcode, $library->{branchcode}, undef, $ten_days_ago );
3176     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
3177     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3178
3179     # specify return date 5 days later => 5 days overdue charged
3180     AddIssue( $patron, $item->barcode, $ten_days_ago ); # date due was 10d ago
3181     AddReturn( $item->barcode, $library->{branchcode}, undef, $five_days_ago );
3182     is( int($patron->account->balance()), 5, 'AddReturn: pass return_date => overdue' );
3183     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3184
3185     # specify return date 5 days later, specify exemptfine => no overdue charge
3186     AddIssue( $patron, $item->barcode, $ten_days_ago ); # date due was 10d ago
3187     AddReturn( $item->barcode, $library->{branchcode}, 1, $five_days_ago );
3188     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
3189     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3190
3191     subtest 'bug 22877 | Lost item return' => sub {
3192
3193         plan tests => 3;
3194
3195         my $issue = AddIssue( $patron, $item->barcode, $ten_days_ago );    # date due was 10d ago
3196
3197         # Fake fines cronjob on this checkout
3198         my ($fine) =
3199           CalcFine( $item, $patron->categorycode, $library->{branchcode},
3200             $ten_days_ago, $now );
3201         UpdateFine(
3202             {
3203                 issue_id       => $issue->issue_id,
3204                 itemnumber     => $item->itemnumber,
3205                 borrowernumber => $patron->borrowernumber,
3206                 amount         => $fine,
3207                 due            => output_pref($ten_days_ago)
3208             }
3209         );
3210         is( int( $patron->account->balance() ),
3211             10, "Overdue fine of 10 days overdue" );
3212
3213         # Fake longoverdue with charge and not marking returned
3214         LostItem( $item->itemnumber, 'cronjob', 0 );
3215         is( int( $patron->account->balance() ),
3216             17, "Lost fine of 7 plus 10 days overdue" );
3217
3218         # Now we return it today
3219         AddReturn( $item->barcode, $library->{branchcode} );
3220         is( int( $patron->account->balance() ),
3221             17, "Should have a single 10 days overdue fine and lost charge" );
3222
3223         # Cleanup
3224         Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3225     };
3226
3227     subtest 'bug 8338 | backdated return resulting in zero amount fine' => sub {
3228
3229         plan tests => 17;
3230
3231         t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
3232
3233         my $issue = AddIssue( $patron, $item->barcode, $one_day_ago );    # date due was 1d ago
3234
3235         # Fake fines cronjob on this checkout
3236         my ($fine) =
3237           CalcFine( $item, $patron->categorycode, $library->{branchcode},
3238             $one_day_ago, $now );
3239         UpdateFine(
3240             {
3241                 issue_id       => $issue->issue_id,
3242                 itemnumber     => $item->itemnumber,
3243                 borrowernumber => $patron->borrowernumber,
3244                 amount         => $fine,
3245                 due            => output_pref($one_day_ago)
3246             }
3247         );
3248         is( int( $patron->account->balance() ),
3249             1, "Overdue fine of 1 day overdue" );
3250
3251         # Backdated return (dropbox mode example - charge should be removed)
3252         AddReturn( $item->barcode, $library->{branchcode}, 1, $one_day_ago );
3253         is( int( $patron->account->balance() ),
3254             0, "Overdue fine should be annulled" );
3255         my $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
3256         is( $lines->count, 0, "Overdue fine accountline has been removed");
3257
3258         $issue = AddIssue( $patron, $item->barcode, $two_days_ago );    # date due was 2d ago
3259
3260         # Fake fines cronjob on this checkout
3261         ($fine) =
3262           CalcFine( $item, $patron->categorycode, $library->{branchcode},
3263             $two_days_ago, $now );
3264         UpdateFine(
3265             {
3266                 issue_id       => $issue->issue_id,
3267                 itemnumber     => $item->itemnumber,
3268                 borrowernumber => $patron->borrowernumber,
3269                 amount         => $fine,
3270                 due            => output_pref($one_day_ago)
3271             }
3272         );
3273         is( int( $patron->account->balance() ),
3274             2, "Overdue fine of 2 days overdue" );
3275
3276         # Payment made against fine
3277         $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
3278         my $debit = $lines->next;
3279         my $credit = $patron->account->add_credit(
3280             {
3281                 amount    => 2,
3282                 type      => 'PAYMENT',
3283                 interface => 'test',
3284             }
3285         );
3286         $credit->apply( { debits => [$debit] } );
3287
3288         is( int( $patron->account->balance() ),
3289             0, "Overdue fine should be paid off" );
3290         $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
3291         is ( $lines->count, 2, "Overdue (debit) and Payment (credit) present");
3292         my $line = $lines->next;
3293         is( $line->amount+0, 2, "Overdue fine amount remains as 2 days");
3294         is( $line->amountoutstanding+0, 0, "Overdue fine amountoutstanding reduced to 0");
3295
3296         # Backdated return (dropbox mode example - charge should be removed)
3297         AddReturn( $item->barcode, $library->{branchcode}, undef, $one_day_ago );
3298         is( int( $patron->account->balance() ),
3299             -1, "Refund credit has been applied" );
3300         $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber }, { order_by => { '-asc' => 'accountlines_id' }});
3301         is( $lines->count, 3, "Overdue (debit), Payment (credit) and Refund (credit) are all present");
3302
3303         $line = $lines->next;
3304         is($line->amount+0,1, "Overdue fine amount has been reduced to 1");
3305         is($line->amountoutstanding+0,0, "Overdue fine amount outstanding remains at 0");
3306         is($line->status,'RETURNED', "Overdue fine is fixed");
3307         $line = $lines->next;
3308         is($line->amount+0,-2, "Original payment amount remains as 2");
3309         is($line->amountoutstanding+0,0, "Original payment remains applied");
3310         $line = $lines->next;
3311         is($line->amount+0,-1, "Refund amount correctly set to 1");
3312         is($line->amountoutstanding+0,-1, "Refund amount outstanding unspent");
3313
3314         # Cleanup
3315         Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3316     };
3317
3318     subtest 'bug 25417 | backdated return + exemptfine' => sub {
3319
3320         plan tests => 2;
3321
3322         t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
3323
3324         my $issue = AddIssue( $patron, $item->barcode, $one_day_ago );    # date due was 1d ago
3325
3326         # Fake fines cronjob on this checkout
3327         my ($fine) =
3328           CalcFine( $item, $patron->categorycode, $library->{branchcode},
3329             $one_day_ago, $now );
3330         UpdateFine(
3331             {
3332                 issue_id       => $issue->issue_id,
3333                 itemnumber     => $item->itemnumber,
3334                 borrowernumber => $patron->borrowernumber,
3335                 amount         => $fine,
3336                 due            => output_pref($one_day_ago)
3337             }
3338         );
3339         is( int( $patron->account->balance() ),
3340             1, "Overdue fine of 1 day overdue" );
3341
3342         # Backdated return (dropbox mode example - charge should no longer exist)
3343         AddReturn( $item->barcode, $library->{branchcode}, 1, $one_day_ago );
3344         is( int( $patron->account->balance() ),
3345             0, "Overdue fine should be annulled" );
3346
3347         # Cleanup
3348         Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3349     };
3350
3351     subtest 'bug 24075 | backdated return with return datetime matching due datetime' => sub {
3352         plan tests => 7;
3353
3354         t::lib::Mocks::mock_preference( 'CalculateFinesOnBackdate', 1 );
3355
3356         my $due_date = dt_from_string;
3357         my $issue = AddIssue( $patron, $item->barcode, $due_date );
3358
3359         # Add fine
3360         UpdateFine(
3361             {
3362                 issue_id       => $issue->issue_id,
3363                 itemnumber     => $item->itemnumber,
3364                 borrowernumber => $patron->borrowernumber,
3365                 amount         => 0.25,
3366                 due            => output_pref($due_date)
3367             }
3368         );
3369         is( $patron->account->balance(),
3370             0.25, 'Overdue fine of $0.25 recorded' );
3371
3372         # Backdate return to exact due date and time
3373         my ( undef, $message ) =
3374           AddReturn( $item->barcode, $library->{branchcode},
3375             undef, $due_date );
3376
3377         my $accountline =
3378           Koha::Account::Lines->find( { issue_id => $issue->id } );
3379         ok( !$accountline, 'accountline removed as expected' );
3380
3381         # Re-issue
3382         $issue = AddIssue( $patron, $item->barcode, $due_date );
3383
3384         # Add fine
3385         UpdateFine(
3386             {
3387                 issue_id       => $issue->issue_id,
3388                 itemnumber     => $item->itemnumber,
3389                 borrowernumber => $patron->borrowernumber,
3390                 amount         => .25,
3391                 due            => output_pref($due_date)
3392             }
3393         );
3394         is( $patron->account->balance(),
3395             0.25, 'Overdue fine of $0.25 recorded' );
3396
3397         # Partial pay accruing fine
3398         my $lines = Koha::Account::Lines->search(
3399             {
3400                 borrowernumber => $patron->borrowernumber,
3401                 issue_id       => $issue->id
3402             }
3403         );
3404         my $debit  = $lines->next;
3405         my $credit = $patron->account->add_credit(
3406             {
3407                 amount    => .20,
3408                 type      => 'PAYMENT',
3409                 interface => 'test',
3410             }
3411         );
3412         $credit->apply( { debits => [$debit] } );
3413
3414         is( $patron->account->balance(), .05, 'Overdue fine reduced to $0.05' );
3415
3416         # Backdate return to exact due date and time
3417         ( undef, $message ) =
3418           AddReturn( $item->barcode, $library->{branchcode},
3419             undef, $due_date );
3420
3421         $lines = Koha::Account::Lines->search(
3422             {
3423                 borrowernumber => $patron->borrowernumber,
3424                 issue_id       => $issue->id
3425             }
3426         );
3427         $accountline = $lines->next;
3428         is( $accountline->amountoutstanding + 0,
3429             0, 'Partially paid fee amount outstanding was reduced to 0' );
3430         is( $accountline->amount + 0,
3431             0, 'Partially paid fee amount was reduced to 0' );
3432         is( $patron->account->balance(), -0.20, 'Patron refund recorded' );
3433
3434         # Cleanup
3435         Koha::Account::Lines->search(
3436             { borrowernumber => $patron->borrowernumber } )->delete;
3437     };
3438
3439     subtest 'enh 23091 | Lost item return policies' => sub {
3440         plan tests => 5;
3441
3442         my $manager = $builder->build_object({ class => "Koha::Patrons" });
3443
3444         my $branchcode_false =
3445           $builder->build( { source => 'Branch' } )->{branchcode};
3446         my $specific_rule_false = $builder->build(
3447             {
3448                 source => 'CirculationRule',
3449                 value  => {
3450                     branchcode   => $branchcode_false,
3451                     categorycode => undef,
3452                     itemtype     => undef,
3453                     rule_name    => 'lostreturn',
3454                     rule_value   => 0
3455                 }
3456             }
3457         );
3458         my $branchcode_refund =
3459           $builder->build( { source => 'Branch' } )->{branchcode};
3460         my $specific_rule_refund = $builder->build(
3461             {
3462                 source => 'CirculationRule',
3463                 value  => {
3464                     branchcode   => $branchcode_refund,
3465                     categorycode => undef,
3466                     itemtype     => undef,
3467                     rule_name    => 'lostreturn',
3468                     rule_value   => 'refund'
3469                 }
3470             }
3471         );
3472         my $branchcode_restore =
3473           $builder->build( { source => 'Branch' } )->{branchcode};
3474         my $specific_rule_restore = $builder->build(
3475             {
3476                 source => 'CirculationRule',
3477                 value  => {
3478                     branchcode   => $branchcode_restore,
3479                     categorycode => undef,
3480                     itemtype     => undef,
3481                     rule_name    => 'lostreturn',
3482                     rule_value   => 'restore'
3483                 }
3484             }
3485         );
3486         my $branchcode_charge =
3487           $builder->build( { source => 'Branch' } )->{branchcode};
3488         my $specific_rule_charge = $builder->build(
3489             {
3490                 source => 'CirculationRule',
3491                 value  => {
3492                     branchcode   => $branchcode_charge,
3493                     categorycode => undef,
3494                     itemtype     => undef,
3495                     rule_name    => 'lostreturn',
3496                     rule_value   => 'charge'
3497                 }
3498             }
3499         );
3500
3501         my $branchcode_refund_unpaid =
3502         $builder->build( { source => 'Branch' } )->{branchcode};
3503         my $specific_rule_refund_unpaid = $builder->build(
3504             {
3505                 source => 'CirculationRule',
3506                 value  => {
3507                     branchcode   => $branchcode_refund_unpaid,
3508                     categorycode => undef,
3509                     itemtype     => undef,
3510                     rule_name    => 'lostreturn',
3511                     rule_value   => 'refund_unpaid'
3512                 }
3513             }
3514         );
3515
3516         my $replacement_amount = 99.00;
3517         t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
3518         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
3519         t::lib::Mocks::mock_preference( 'WhenLostForgiveFine',          0 );
3520         t::lib::Mocks::mock_preference( 'BlockReturnOfLostItems',       0 );
3521         t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl',
3522             'CheckinLibrary' );
3523         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge',
3524             undef );
3525
3526         subtest 'lostreturn | refund_unpaid' => sub {
3527             plan tests => 21;
3528
3529             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_refund_unpaid });
3530
3531             my $item = $builder->build_sample_item(
3532                 {
3533                     replacementprice => $replacement_amount
3534                 }
3535             );
3536
3537             # Issue the item
3538             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode );
3539
3540             # Mark item as lost
3541             $item->itemlost(3)->store;
3542             C4::Circulation::LostItem( $item->itemnumber, 1 );
3543
3544             my $lost_fee_lines = Koha::Account::Lines->search(
3545                 {
3546                     borrowernumber  => $patron->id,
3547                     itemnumber      => $item->itemnumber,
3548                     debit_type_code => 'LOST'
3549                 }
3550             );
3551             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3552             my $lost_fee_line = $lost_fee_lines->next;
3553             is( int($lost_fee_line->amount),
3554                 $replacement_amount, 'The right LOST amount is generated' );
3555             is( int($lost_fee_line->amountoutstanding),
3556                 $replacement_amount,
3557                 'The right LOST amountoutstanding is generated' );
3558             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3559
3560             is(
3561                 int($patron->account->balance),
3562                 $replacement_amount ,
3563                 "Account balance equals the replacement amount after being charged lost fee when no payments has been made"
3564             );
3565
3566             # Return lost item without any payments having been made
3567             my ( $returned, $message ) = AddReturn( $item->barcode, $branchcode_refund_unpaid );
3568
3569             $lost_fee_line->discard_changes;
3570
3571             is( int($lost_fee_line->amount), $replacement_amount, 'The LOST amount is left intact' );
3572             is( int($lost_fee_line->amountoutstanding) , 0, 'The LOST amountoutstanding is zero' );
3573             is( $lost_fee_line->status, 'FOUND', 'The FOUND status was set' );
3574             is(
3575                 int($patron->account->balance),
3576                 0,
3577                 'Account balance should be zero after returning item with lost fee when no payments has been made'
3578             );
3579
3580             # Create a second item
3581             $item = $builder->build_sample_item(
3582                 {
3583                     replacementprice => $replacement_amount
3584                 }
3585             );
3586
3587             # Issue the item
3588             $issue = C4::Circulation::AddIssue( $patron, $item->barcode );
3589
3590             # Mark item as lost
3591             $item->itemlost(3)->store;
3592             C4::Circulation::LostItem( $item->itemnumber, 1 );
3593
3594             $lost_fee_lines = Koha::Account::Lines->search(
3595                 {
3596                     borrowernumber  => $patron->id,
3597                     itemnumber      => $item->itemnumber,
3598                     debit_type_code => 'LOST'
3599                 }
3600             );
3601             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3602             $lost_fee_line = $lost_fee_lines->next;
3603
3604             # Make partial payment
3605             $patron->account->payin_amount({
3606                 type => 'PAYMENT',
3607                 interface => 'intranet',
3608                 payment_type => 'CASH',
3609                 user_id => $patron->borrowernumber,
3610                 amount => 39.00,
3611                 debits => [$lost_fee_line]
3612             });
3613
3614             $lost_fee_line->discard_changes;
3615
3616             is( int($lost_fee_line->amountoutstanding),
3617                 60,
3618                 'The LOST amountoutstanding is the expected amount after partial payment of lost fee'
3619             );
3620
3621             is(
3622                 int($patron->account->balance),
3623                 60,
3624                 'Account balance is the expected amount after partial payment of lost fee'
3625             );
3626
3627              # Return lost item with partial payment having been made
3628             ( $returned, $message ) = AddReturn( $item->barcode, $branchcode_refund_unpaid );
3629
3630             $lost_fee_line->discard_changes;
3631
3632             is( int($lost_fee_line->amountoutstanding) , 0, 'The LOST amountoutstanding is zero after returning lost item with partial payment' );
3633             is( $lost_fee_line->status, 'FOUND', 'The FOUND status was set for lost item with partial payment' );
3634             is(
3635                 int($patron->account->balance),
3636                 0,
3637                 'Account balance should be zero after returning item with lost fee when partial payment has been made'
3638             );
3639
3640             # Create a third item
3641             $item = $builder->build_sample_item(
3642                 {
3643                     replacementprice => $replacement_amount
3644                 }
3645             );
3646
3647             # Issue the item
3648             $issue = C4::Circulation::AddIssue( $patron, $item->barcode );
3649
3650             # Mark item as lost
3651             $item->itemlost(3)->store;
3652             C4::Circulation::LostItem( $item->itemnumber, 1 );
3653
3654             $lost_fee_lines = Koha::Account::Lines->search(
3655                 {
3656                     borrowernumber  => $patron->id,
3657                     itemnumber      => $item->itemnumber,
3658                     debit_type_code => 'LOST'
3659                 }
3660             );
3661             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3662             $lost_fee_line = $lost_fee_lines->next;
3663
3664             # Make full payment
3665             $patron->account->payin_amount({
3666                 type => 'PAYMENT',
3667                 interface => 'intranet',
3668                 payment_type => 'CASH',
3669                 user_id => $patron->borrowernumber,
3670                 amount => $replacement_amount,
3671                 debits => [$lost_fee_line]
3672             });
3673
3674             $lost_fee_line->discard_changes;
3675
3676             is( int($lost_fee_line->amountoutstanding),
3677                 0,
3678                 'The LOST amountoutstanding is the expected amount after partial payment of lost fee'
3679             );
3680
3681             is(
3682                 int($patron->account->balance),
3683                 0,
3684                 'Account balance is the expected amount after partial payment of lost fee'
3685             );
3686
3687              # Return lost item with partial payment having been made
3688             ( $returned, $message ) = AddReturn( $item->barcode, $branchcode_refund_unpaid );
3689
3690             $lost_fee_line->discard_changes;
3691
3692             is( int($lost_fee_line->amountoutstanding) , 0, 'The LOST amountoutstanding is zero after returning lost item with full payment' );
3693             is( $lost_fee_line->status, 'FOUND', 'The FOUND status was set for lost item with partial payment' );
3694             is(
3695                 int($patron->account->balance),
3696                 0,
3697                 'Account balance should be zero after returning item with lost fee when full payment has been made'
3698             );
3699         };
3700
3701         subtest 'lostreturn | false' => sub {
3702             plan tests => 12;
3703
3704             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_false });
3705
3706             my $item = $builder->build_sample_item(
3707                 {
3708                     replacementprice => $replacement_amount
3709                 }
3710             );
3711
3712             # Issue the item
3713             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode, $ten_days_ago );
3714
3715             # Fake fines cronjob on this checkout
3716             my ($fine) =
3717               CalcFine( $item, $patron->categorycode, $library->{branchcode},
3718                 $ten_days_ago, $now );
3719             UpdateFine(
3720                 {
3721                     issue_id       => $issue->issue_id,
3722                     itemnumber     => $item->itemnumber,
3723                     borrowernumber => $patron->borrowernumber,
3724                     amount         => $fine,
3725                     due            => output_pref($ten_days_ago)
3726                 }
3727             );
3728             my $overdue_fees = Koha::Account::Lines->search(
3729                 {
3730                     borrowernumber  => $patron->id,
3731                     itemnumber      => $item->itemnumber,
3732                     debit_type_code => 'OVERDUE'
3733                 }
3734             );
3735             is( $overdue_fees->count, 1, 'Overdue item fee produced' );
3736             my $overdue_fee = $overdue_fees->next;
3737             is( $overdue_fee->amount + 0,
3738                 10, 'The right OVERDUE amount is generated' );
3739             is( $overdue_fee->amountoutstanding + 0,
3740                 10,
3741                 'The right OVERDUE amountoutstanding is generated' );
3742
3743             # Simulate item marked as lost
3744             $item->itemlost(3)->store;
3745             C4::Circulation::LostItem( $item->itemnumber, 1 );
3746
3747             my $lost_fee_lines = Koha::Account::Lines->search(
3748                 {
3749                     borrowernumber  => $patron->id,
3750                     itemnumber      => $item->itemnumber,
3751                     debit_type_code => 'LOST'
3752                 }
3753             );
3754             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3755             my $lost_fee_line = $lost_fee_lines->next;
3756             is( $lost_fee_line->amount + 0,
3757                 $replacement_amount, 'The right LOST amount is generated' );
3758             is( $lost_fee_line->amountoutstanding + 0,
3759                 $replacement_amount,
3760                 'The right LOST amountoutstanding is generated' );
3761             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3762
3763             # Return lost item
3764             my ( $returned, $message ) =
3765               AddReturn( $item->barcode, $branchcode_false, undef, $five_days_ago );
3766
3767             $overdue_fee->discard_changes;
3768             is( $overdue_fee->amount + 0,
3769                 10, 'The OVERDUE amount is left intact' );
3770             is( $overdue_fee->amountoutstanding + 0,
3771                 10,
3772                 'The OVERDUE amountoutstanding is left intact' );
3773
3774             $lost_fee_line->discard_changes;
3775             is( $lost_fee_line->amount + 0,
3776                 $replacement_amount, 'The LOST amount is left intact' );
3777             is( $lost_fee_line->amountoutstanding + 0,
3778                 $replacement_amount,
3779                 'The LOST amountoutstanding is left intact' );
3780             # FIXME: Should we set the LOST fee status to 'FOUND' regardless of whether we're refunding or not?
3781             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3782         };
3783
3784         subtest 'lostreturn | refund' => sub {
3785             plan tests => 12;
3786
3787             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_refund });
3788
3789             my $item = $builder->build_sample_item(
3790                 {
3791                     replacementprice => $replacement_amount
3792                 }
3793             );
3794
3795             # Issue the item
3796             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode, $ten_days_ago );
3797
3798             # Fake fines cronjob on this checkout
3799             my ($fine) =
3800               CalcFine( $item, $patron->categorycode, $library->{branchcode},
3801                 $ten_days_ago, $now );
3802             UpdateFine(
3803                 {
3804                     issue_id       => $issue->issue_id,
3805                     itemnumber     => $item->itemnumber,
3806                     borrowernumber => $patron->borrowernumber,
3807                     amount         => $fine,
3808                     due            => output_pref($ten_days_ago)
3809                 }
3810             );
3811             my $overdue_fees = Koha::Account::Lines->search(
3812                 {
3813                     borrowernumber  => $patron->id,
3814                     itemnumber      => $item->itemnumber,
3815                     debit_type_code => 'OVERDUE'
3816                 }
3817             );
3818             is( $overdue_fees->count, 1, 'Overdue item fee produced' );
3819             my $overdue_fee = $overdue_fees->next;
3820             is( $overdue_fee->amount + 0,
3821                 10, 'The right OVERDUE amount is generated' );
3822             is( $overdue_fee->amountoutstanding + 0,
3823                 10,
3824                 'The right OVERDUE amountoutstanding is generated' );
3825
3826             # Simulate item marked as lost
3827             $item->itemlost(3)->store;
3828             C4::Circulation::LostItem( $item->itemnumber, 1 );
3829
3830             my $lost_fee_lines = Koha::Account::Lines->search(
3831                 {
3832                     borrowernumber  => $patron->id,
3833                     itemnumber      => $item->itemnumber,
3834                     debit_type_code => 'LOST'
3835                 }
3836             );
3837             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3838             my $lost_fee_line = $lost_fee_lines->next;
3839             is( $lost_fee_line->amount + 0,
3840                 $replacement_amount, 'The right LOST amount is generated' );
3841             is( $lost_fee_line->amountoutstanding + 0,
3842                 $replacement_amount,
3843                 'The right LOST amountoutstanding is generated' );
3844             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3845
3846             # Return the lost item
3847             my ( undef, $message ) =
3848               AddReturn( $item->barcode, $branchcode_refund, undef, $five_days_ago );
3849
3850             $overdue_fee->discard_changes;
3851             is( $overdue_fee->amount + 0,
3852                 10, 'The OVERDUE amount is left intact' );
3853             is( $overdue_fee->amountoutstanding + 0,
3854                 10,
3855                 'The OVERDUE amountoutstanding is left intact' );
3856
3857             $lost_fee_line->discard_changes;
3858             is( $lost_fee_line->amount + 0,
3859                 $replacement_amount, 'The LOST amount is left intact' );
3860             is( $lost_fee_line->amountoutstanding + 0,
3861                 0,
3862                 'The LOST amountoutstanding is refunded' );
3863             is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
3864         };
3865
3866         subtest 'lostreturn | restore' => sub {
3867             plan tests => 13;
3868
3869             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_restore });
3870
3871             my $item = $builder->build_sample_item(
3872                 {
3873                     replacementprice => $replacement_amount
3874                 }
3875             );
3876
3877             # Issue the item
3878             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode , $ten_days_ago);
3879
3880             # Fake fines cronjob on this checkout
3881             my ($fine) =
3882               CalcFine( $item, $patron->categorycode, $library->{branchcode},
3883                 $ten_days_ago, $now );
3884             UpdateFine(
3885                 {
3886                     issue_id       => $issue->issue_id,
3887                     itemnumber     => $item->itemnumber,
3888                     borrowernumber => $patron->borrowernumber,
3889                     amount         => $fine,
3890                     due            => output_pref($ten_days_ago)
3891                 }
3892             );
3893             my $overdue_fees = Koha::Account::Lines->search(
3894                 {
3895                     borrowernumber  => $patron->id,
3896                     itemnumber      => $item->itemnumber,
3897                     debit_type_code => 'OVERDUE'
3898                 }
3899             );
3900             is( $overdue_fees->count, 1, 'Overdue item fee produced' );
3901             my $overdue_fee = $overdue_fees->next;
3902             is( $overdue_fee->amount + 0,
3903                 10, 'The right OVERDUE amount is generated' );
3904             is( $overdue_fee->amountoutstanding + 0,
3905                 10,
3906                 'The right OVERDUE amountoutstanding is generated' );
3907
3908             # Simulate item marked as lost
3909             $item->itemlost(3)->store;
3910             C4::Circulation::LostItem( $item->itemnumber, 1 );
3911
3912             my $lost_fee_lines = Koha::Account::Lines->search(
3913                 {
3914                     borrowernumber  => $patron->id,
3915                     itemnumber      => $item->itemnumber,
3916                     debit_type_code => 'LOST'
3917                 }
3918             );
3919             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3920             my $lost_fee_line = $lost_fee_lines->next;
3921             is( $lost_fee_line->amount + 0,
3922                 $replacement_amount, 'The right LOST amount is generated' );
3923             is( $lost_fee_line->amountoutstanding + 0,
3924                 $replacement_amount,
3925                 'The right LOST amountoutstanding is generated' );
3926             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3927
3928             # Simulate refunding overdue fees upon marking item as lost
3929             my $overdue_forgive = $patron->account->add_credit(
3930                 {
3931                     amount     => 10.00,
3932                     user_id    => $manager->borrowernumber,
3933                     library_id => $branchcode_restore,
3934                     interface  => 'test',
3935                     type       => 'FORGIVEN',
3936                     item_id    => $item->itemnumber
3937                 }
3938             );
3939             $overdue_forgive->apply( { debits => [$overdue_fee] } );
3940             $overdue_fee->discard_changes;
3941             is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
3942
3943             # Do nothing
3944             my ( undef, $message ) =
3945               AddReturn( $item->barcode, $branchcode_restore, undef, $five_days_ago );
3946
3947             $overdue_fee->discard_changes;
3948             is( $overdue_fee->amount + 0,
3949                 10, 'The OVERDUE amount is left intact' );
3950             is( $overdue_fee->amountoutstanding + 0,
3951                 10,
3952                 'The OVERDUE amountoutstanding is restored' );
3953
3954             $lost_fee_line->discard_changes;
3955             is( $lost_fee_line->amount + 0,
3956                 $replacement_amount, 'The LOST amount is left intact' );
3957             is( $lost_fee_line->amountoutstanding + 0,
3958                 0,
3959                 'The LOST amountoutstanding is refunded' );
3960             is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
3961         };
3962
3963         subtest 'lostreturn | charge' => sub {
3964             plan tests => 16;
3965
3966             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_charge });
3967
3968             my $item = $builder->build_sample_item(
3969                 {
3970                     replacementprice => $replacement_amount
3971                 }
3972             );
3973
3974             # Issue the item
3975             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode, $ten_days_ago );
3976
3977             # Fake fines cronjob on this checkout
3978             my ($fine) =
3979               CalcFine( $item, $patron->categorycode, $library->{branchcode},
3980                 $ten_days_ago, $now );
3981             UpdateFine(
3982                 {
3983                     issue_id       => $issue->issue_id,
3984                     itemnumber     => $item->itemnumber,
3985                     borrowernumber => $patron->borrowernumber,
3986                     amount         => $fine,
3987                     due            => output_pref($ten_days_ago)
3988                 }
3989             );
3990             my $overdue_fees = Koha::Account::Lines->search(
3991                 {
3992                     borrowernumber  => $patron->id,
3993                     itemnumber      => $item->itemnumber,
3994                     debit_type_code => 'OVERDUE'
3995                 }
3996             );
3997             is( $overdue_fees->count, 1, 'Overdue item fee produced' );
3998             my $overdue_fee = $overdue_fees->next;
3999             is( $overdue_fee->amount + 0,
4000                 10, 'The right OVERDUE amount is generated' );
4001             is( $overdue_fee->amountoutstanding + 0,
4002                 10,
4003                 'The right OVERDUE amountoutstanding is generated' );
4004
4005             # Simulate item marked as lost
4006             $item->itemlost(3)->store;
4007             C4::Circulation::LostItem( $item->itemnumber, 1 );
4008
4009             my $lost_fee_lines = Koha::Account::Lines->search(
4010                 {
4011                     borrowernumber  => $patron->id,
4012                     itemnumber      => $item->itemnumber,
4013                     debit_type_code => 'LOST'
4014                 }
4015             );
4016             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
4017             my $lost_fee_line = $lost_fee_lines->next;
4018             is( $lost_fee_line->amount + 0,
4019                 $replacement_amount, 'The right LOST amount is generated' );
4020             is( $lost_fee_line->amountoutstanding + 0,
4021                 $replacement_amount,
4022                 'The right LOST amountoutstanding is generated' );
4023             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
4024
4025             # Simulate refunding overdue fees upon marking item as lost
4026             my $overdue_forgive = $patron->account->add_credit(
4027                 {
4028                     amount     => 10.00,
4029                     user_id    => $manager->borrowernumber,
4030                     library_id => $branchcode_charge,
4031                     interface  => 'test',
4032                     type       => 'FORGIVEN',
4033                     item_id    => $item->itemnumber
4034                 }
4035             );
4036             $overdue_forgive->apply( { debits => [$overdue_fee] } );
4037             $overdue_fee->discard_changes;
4038             is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
4039
4040             # Do nothing
4041             my ( undef, $message ) =
4042               AddReturn( $item->barcode, $branchcode_charge, undef, $five_days_ago );
4043
4044             $lost_fee_line->discard_changes;
4045             is( $lost_fee_line->amount + 0,
4046                 $replacement_amount, 'The LOST amount is left intact' );
4047             is( $lost_fee_line->amountoutstanding + 0,
4048                 0,
4049                 'The LOST amountoutstanding is refunded' );
4050             is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
4051
4052             $overdue_fees = Koha::Account::Lines->search(
4053                 {
4054                     borrowernumber  => $patron->id,
4055                     itemnumber      => $item->itemnumber,
4056                     debit_type_code => 'OVERDUE'
4057                 },
4058                 {
4059                     order_by => { '-asc' => 'accountlines_id'}
4060                 }
4061             );
4062             is( $overdue_fees->count, 2, 'A second OVERDUE fee has been added' );
4063             $overdue_fee = $overdue_fees->next;
4064             is( $overdue_fee->amount + 0,
4065                 10, 'The original OVERDUE amount is left intact' );
4066             is( $overdue_fee->amountoutstanding + 0,
4067                 0,
4068                 'The original OVERDUE amountoutstanding is left as forgiven' );
4069             $overdue_fee = $overdue_fees->next;
4070             is( $overdue_fee->amount + 0,
4071                 5, 'The new OVERDUE amount is correct for the backdated return' );
4072             is( $overdue_fee->amountoutstanding + 0,
4073                 5,
4074                 'The new OVERDUE amountoutstanding is correct for the backdated return' );
4075         };
4076     };
4077 };
4078
4079 subtest '_FixOverduesOnReturn' => sub {
4080     plan tests => 14;
4081
4082     my $manager = $builder->build_object({ class => "Koha::Patrons" });
4083     t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
4084
4085     my $biblio = $builder->build_sample_biblio({ author => 'Hall, Kylie' });
4086
4087     my $branchcode  = $library2->{branchcode};
4088
4089     my $item = $builder->build_sample_item(
4090         {
4091             biblionumber     => $biblio->biblionumber,
4092             library          => $branchcode,
4093             replacementprice => 99.00,
4094             itype            => $itemtype,
4095         }
4096     );
4097
4098     my $patron = $builder->build( { source => 'Borrower' } );
4099
4100     ## Start with basic call, should just close out the open fine
4101     my $accountline = Koha::Account::Line->new(
4102         {
4103             borrowernumber => $patron->{borrowernumber},
4104             debit_type_code    => 'OVERDUE',
4105             status         => 'UNRETURNED',
4106             itemnumber     => $item->itemnumber,
4107             amount => 99.00,
4108             amountoutstanding => 99.00,
4109             interface => 'test',
4110         }
4111     )->store();
4112
4113     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, undef, 'RETURNED' );
4114
4115     $accountline->_result()->discard_changes();
4116
4117     is( $accountline->amountoutstanding+0, 99, 'Fine has the same amount outstanding as previously' );
4118     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
4119     is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
4120
4121     ## Run again, with exemptfine enabled
4122     $accountline->set(
4123         {
4124             debit_type_code    => 'OVERDUE',
4125             status         => 'UNRETURNED',
4126             amountoutstanding => 99.00,
4127         }
4128     )->store();
4129
4130     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
4131
4132     $accountline->_result()->discard_changes();
4133     my $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'APPLY' })->next();
4134
4135     is( $accountline->amountoutstanding + 0, 0, 'Fine amountoutstanding has been reduced to 0' );
4136     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
4137     is( $accountline->status, 'RETURNED', 'Open fine ( account type OVERDUE ) has been set to returned ( status RETURNED )');
4138     is( ref $offset, "Koha::Account::Offset", "Found matching offset for fine reduction via forgiveness" );
4139     is( $offset->amount + 0, -99, "Amount of offset is correct" );
4140     my $credit = $offset->credit;
4141     is( ref $credit, "Koha::Account::Line", "Found matching credit for fine forgiveness" );
4142     is( $credit->amount + 0, -99, "Credit amount is set correctly" );
4143     is( $credit->amountoutstanding + 0, 0, "Credit amountoutstanding is correctly set to 0" );
4144
4145     # Bug 25417 - Only forgive fines where there is an amount outstanding to forgive
4146     $accountline->set(
4147         {
4148             debit_type_code    => 'OVERDUE',
4149             status         => 'UNRETURNED',
4150             amountoutstanding => 0.00,
4151         }
4152     )->store();
4153     $offset->delete;
4154
4155     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
4156
4157     $accountline->_result()->discard_changes();
4158     $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'CREATE' })->next();
4159     is( $offset, undef, "No offset created when trying to forgive fine with no outstanding balance" );
4160     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
4161     is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
4162 };
4163
4164 subtest 'Set waiting flag' => sub {
4165     plan tests => 11;
4166
4167     my $library_1 = $builder->build( { source => 'Branch' } );
4168     my $patron_1  = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
4169     my $library_2 = $builder->build( { source => 'Branch' } );
4170     my $patron_2  = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
4171
4172     my $item = $builder->build_sample_item(
4173         {
4174             library      => $library_1->{branchcode},
4175         }
4176     );
4177
4178     set_userenv( $library_2 );
4179     my $reserve_id = AddReserve(
4180         {
4181             branchcode     => $library_2->{branchcode},
4182             borrowernumber => $patron_2->{borrowernumber},
4183             biblionumber   => $item->biblionumber,
4184             priority       => 1,
4185             itemnumber     => $item->itemnumber,
4186         }
4187     );
4188
4189     set_userenv( $library_1 );
4190     my $do_transfer = 1;
4191     my ( $res, $rr ) = AddReturn( $item->barcode, $library_1->{branchcode} );
4192     ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
4193     my $hold = Koha::Holds->find( $reserve_id );
4194     is( $hold->found, 'T', 'Hold is in transit' );
4195
4196     my ( $status ) = CheckReserves($item);
4197     is( $status, 'Transferred', 'Hold is not waiting yet');
4198
4199     set_userenv( $library_2 );
4200     $do_transfer = 0;
4201     AddReturn( $item->barcode, $library_2->{branchcode} );
4202     ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
4203     $hold = Koha::Holds->find( $reserve_id );
4204     is( $hold->found, 'W', 'Hold is waiting' );
4205     ( $status ) = CheckReserves($item);
4206     is( $status, 'Waiting', 'Now the hold is waiting');
4207
4208     #Bug 21944 - Waiting transfer checked in at branch other than pickup location
4209     set_userenv( $library_1 );
4210     (undef, my $messages, undef, undef ) = AddReturn ( $item->barcode, $library_1->{branchcode} );
4211     $hold = Koha::Holds->find( $reserve_id );
4212     is( $hold->found, undef, 'Hold is no longer marked waiting' );
4213     is( $hold->priority, 1,  "Hold is now priority one again");
4214     is( $hold->waitingdate, undef, "Hold no longer has a waiting date");
4215     is( $hold->itemnumber, $item->itemnumber, "Hold has retained its' itemnumber");
4216     is( $messages->{ResFound}->{ResFound}, "Reserved", "Hold is still returned");
4217     is( $messages->{ResFound}->{found}, undef, "Hold is no longer marked found in return message");
4218     is( $messages->{ResFound}->{priority}, 1, "Hold is priority 1 in return message");
4219 };
4220
4221 subtest 'Cancel transfers on lost items' => sub {
4222     plan tests => 6;
4223
4224     my $library_to = $builder->build_object( { class => 'Koha::Libraries' } );
4225     my $item   = $builder->build_sample_item();
4226     my $holdingbranch = $item->holdingbranch;
4227     # Historic transfer (datearrived is defined)
4228     my $old_transfer = $builder->build_object(
4229         {
4230             class => 'Koha::Item::Transfers',
4231             value => {
4232                 itemnumber    => $item->itemnumber,
4233                 frombranch    => $holdingbranch,
4234                 tobranch      => $library_to->branchcode,
4235                 reason        => 'Manual',
4236                 datesent      => \'NOW()',
4237                 datearrived   => \'NOW()',
4238                 datecancelled => undef,
4239                 daterequested => \'NOW()'
4240             }
4241         }
4242     );
4243     # Queued transfer (datesent is undefined)
4244     my $transfer_1 = $builder->build_object(
4245         {
4246             class => 'Koha::Item::Transfers',
4247             value => {
4248                 itemnumber    => $item->itemnumber,
4249                 frombranch    => $holdingbranch,
4250                 tobranch      => $library_to->branchcode,
4251                 reason        => 'Manual',
4252                 datesent      => undef,
4253                 datearrived   => undef,
4254                 datecancelled => undef,
4255                 daterequested => \'NOW()'
4256             }
4257         }
4258     );
4259     # In transit transfer (datesent is defined, datearrived and datecancelled are both undefined)
4260     my $transfer_2 = $builder->build_object(
4261         {
4262             class => 'Koha::Item::Transfers',
4263             value => {
4264                 itemnumber    => $item->itemnumber,
4265                 frombranch    => $holdingbranch,
4266                 tobranch      => $library_to->branchcode,
4267                 reason        => 'Manual',
4268                 datesent      => \'NOW()',
4269                 datearrived   => undef,
4270                 datecancelled => undef,
4271                 daterequested => \'NOW()'
4272             }
4273         }
4274     );
4275
4276     # Simulate item being marked as lost
4277     $item->itemlost(1)->store;
4278     LostItem( $item->itemnumber, 'test', 1 );
4279
4280     $transfer_1->discard_changes;
4281     isnt($transfer_1->datecancelled, undef, "Queud transfer was cancelled upon item lost");
4282     is($transfer_1->cancellation_reason, 'ItemLost', "Cancellation reason was set to 'ItemLost'");
4283     $transfer_2->discard_changes;
4284     isnt($transfer_2->datecancelled, undef, "Active transfer was cancelled upon item lost");
4285     is($transfer_2->cancellation_reason, 'ItemLost', "Cancellation reason was set to 'ItemLost'");
4286     $old_transfer->discard_changes;
4287     is($old_transfer->datecancelled, undef, "Old transfers are unaffected");
4288     $item->discard_changes;
4289     is($item->holdingbranch, $holdingbranch, "Items holding branch remains unchanged");
4290 };
4291
4292 subtest 'CanBookBeIssued | is_overdue' => sub {
4293     plan tests => 3;
4294
4295     # Set a simple circ policy
4296     Koha::CirculationRules->set_rules(
4297         {
4298             categorycode => undef,
4299             branchcode   => undef,
4300             itemtype     => undef,
4301             rules        => {
4302                 maxissueqty     => 1,
4303                 reservesallowed => 25,
4304                 issuelength     => 14,
4305                 lengthunit      => 'days',
4306                 renewalsallowed => 1,
4307                 renewalperiod   => 7,
4308                 norenewalbefore => undef,
4309                 auto_renew      => 0,
4310                 fine            => .10,
4311                 chargeperiod    => 1,
4312             }
4313         }
4314     );
4315
4316     my $now   = dt_from_string()->truncate( to => 'day' );
4317     my $five_days_go = $now->clone->add( days => 5 );
4318     my $ten_days_go  = $now->clone->add( days => 10);
4319     my $library = $builder->build( { source => 'Branch' } );
4320     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
4321
4322     my $item = $builder->build_sample_item(
4323         {
4324             library      => $library->{branchcode},
4325         }
4326     );
4327
4328     my $issue = AddIssue( $patron, $item->barcode, $five_days_go ); # date due was 10d ago
4329     my $actualissue = Koha::Checkouts->find( { itemnumber => $item->itemnumber } );
4330     is( output_pref({ str => $actualissue->date_due, dateonly => 1}), output_pref({ str => $five_days_go, dateonly => 1}), "First issue works");
4331     my ($issuingimpossible, $needsconfirmation) = CanBookBeIssued($patron, $item->barcode, $ten_days_go, undef, undef, undef);
4332     is( $needsconfirmation->{RENEW_ISSUE}, 1, "This is a renewal");
4333     is( $needsconfirmation->{TOO_MANY}, undef, "Not too many, is a renewal");
4334 };
4335
4336 subtest 'ItemsDeniedRenewal rules are checked' => sub {
4337     plan tests => 4;
4338
4339     my $idr_lib = $builder->build_object({ class => 'Koha::Libraries'});
4340     Koha::CirculationRules->set_rules(
4341         {
4342             categorycode => '*',
4343             itemtype     => '*',
4344             branchcode   => $idr_lib->branchcode,
4345             rules        => {
4346                 reservesallowed => 25,
4347                 issuelength     => 14,
4348                 lengthunit      => 'days',
4349                 renewalsallowed => 10,
4350                 renewalperiod   => 7,
4351                 norenewalbefore => undef,
4352                 auto_renew      => 0,
4353                 fine            => .10,
4354                 chargeperiod    => 1,
4355             }
4356         }
4357     );
4358
4359     my $allow_book = $builder->build_object({ class => 'Koha::Items', value => {
4360         homebranch => $idr_lib->branchcode,
4361         withdrawn => 0,
4362         itype => 'NOHIDE',
4363         location => 'NOPROC'
4364         }
4365     });
4366
4367     my $idr_borrower = $builder->build_object({ class => 'Koha::Patrons', value=> {
4368         branchcode => $idr_lib->branchcode,
4369         }
4370     });
4371     my $future = dt_from_string->add( days => 1 );
4372     my $issue = $builder->build_object(
4373         {
4374             class => 'Koha::Checkouts',
4375             value => {
4376                 returndate      => undef,
4377                 renewals_count  => 0,
4378                 auto_renew      => 0,
4379                 borrowernumber  => $idr_borrower->borrowernumber,
4380                 itemnumber      => $allow_book->itemnumber,
4381                 onsite_checkout => 0,
4382                 date_due        => $future,
4383             }
4384         }
4385     );
4386
4387     my $mock_item_class = Test::MockModule->new("Koha::Item");
4388     $mock_item_class->mock( 'is_denied_renewal', sub { return 1; } );
4389
4390     my ( $mayrenew, $error ) = CanBookBeRenewed( $idr_borrower, $issue );
4391     is( $mayrenew, 0, 'Renewal blocked when $item->is_denied_renewal returns true' );
4392     is( $error, 'item_denied_renewal', 'Renewal blocked when $item->is_denied_renewal returns true' );
4393
4394     $mock_item_class->unmock( 'is_denied_renewal' );
4395     $mock_item_class->mock( 'is_denied_renewal', sub { return 0; } );
4396
4397     ( $mayrenew, $error ) = CanBookBeRenewed( $idr_borrower, $issue );
4398     is( $mayrenew, 1, 'Renewal allowed when $item->is_denied_renewal returns false' );
4399     is( $error, undef, 'Renewal allowed when $item->is_denied_renewal returns false' );
4400
4401     $mock_item_class->unmock( 'is_denied_renewal' );
4402 };
4403
4404 subtest 'CanBookBeIssued | item-level_itypes=biblio' => sub {
4405     plan tests => 2;
4406
4407     t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
4408     my $library = $builder->build( { source => 'Branch' } );
4409     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
4410
4411     my $item = $builder->build_sample_item(
4412         {
4413             library      => $library->{branchcode},
4414         }
4415     );
4416
4417     my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4418     is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
4419     is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
4420 };
4421
4422 subtest 'CanBookBeIssued | notforloan' => sub {
4423     plan tests => 2;
4424
4425     t::lib::Mocks::mock_preference('AllowNotForLoanOverride', 0);
4426
4427     my $library = $builder->build( { source => 'Branch' } );
4428     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
4429
4430     my $itemtype = $builder->build(
4431         {
4432             source => 'Itemtype',
4433             value  => { notforloan => undef, }
4434         }
4435     );
4436     my $item = $builder->build_sample_item(
4437         {
4438             library  => $library->{branchcode},
4439             itype    => $itemtype->{itemtype},
4440         }
4441     );
4442     $item->biblioitem->itemtype($itemtype->{itemtype})->store;
4443
4444     my ( $issuingimpossible, $needsconfirmation );
4445
4446
4447     subtest 'item-level_itypes = 1' => sub {
4448         plan tests => 6;
4449
4450         t::lib::Mocks::mock_preference('item-level_itypes', 1); # item
4451         # Is for loan at item type and item level
4452         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4453         is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
4454         is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
4455
4456         # not for loan at item type level
4457         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
4458         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4459         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
4460         is_deeply(
4461             $issuingimpossible,
4462             { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
4463             'Item can not be issued, not for loan at item type level'
4464         );
4465
4466         # not for loan at item level
4467         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
4468         $item->notforloan( 1 )->store;
4469         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4470         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
4471         is_deeply(
4472             $issuingimpossible,
4473             { NOT_FOR_LOAN => 1, item_notforloan => 1 },
4474             'Item can not be issued, not for loan at item type level'
4475         );
4476     };
4477
4478     subtest 'item-level_itypes = 0' => sub {
4479         plan tests => 6;
4480
4481         t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
4482
4483         # We set another itemtype for biblioitem
4484         my $itemtype = $builder->build(
4485             {
4486                 source => 'Itemtype',
4487                 value  => { notforloan => undef, }
4488             }
4489         );
4490
4491         # for loan at item type and item level
4492         $item->notforloan(0)->store;
4493         $item->biblioitem->itemtype($itemtype->{itemtype})->store;
4494         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4495         is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
4496         is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
4497
4498         # not for loan at item type level
4499         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
4500         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4501         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
4502         is_deeply(
4503             $issuingimpossible,
4504             { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
4505             'Item can not be issued, not for loan at item type level'
4506         );
4507
4508         # not for loan at item level
4509         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
4510         $item->notforloan( 1 )->store;
4511         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4512         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
4513         is_deeply(
4514             $issuingimpossible,
4515             { NOT_FOR_LOAN => 1, item_notforloan => 1 },
4516             'Item can not be issued, not for loan at item type level'
4517         );
4518     };
4519
4520     # TODO test with AllowNotForLoanOverride = 1
4521 };
4522
4523 subtest 'CanBookBeIssued | recalls' => sub {
4524     plan tests => 3;
4525
4526     t::lib::Mocks::mock_preference("UseRecalls", 1);
4527     t::lib::Mocks::mock_preference("item-level_itypes", 1);
4528     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
4529     my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
4530     my $item = $builder->build_sample_item;
4531     Koha::CirculationRules->set_rules({
4532         branchcode => undef,
4533         itemtype => undef,
4534         categorycode => undef,
4535         rules => {
4536             recalls_allowed => 10,
4537         },
4538     });
4539
4540     # item-level recall
4541     my $recall = Koha::Recall->new(
4542         {   patron_id         => $patron1->borrowernumber,
4543             biblio_id         => $item->biblionumber,
4544             item_id           => $item->itemnumber,
4545             item_level        => 1,
4546             pickup_library_id => $patron1->branchcode,
4547         }
4548     )->store;
4549
4550     my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron2, $item->barcode, undef, undef, undef, undef );
4551     is( $needsconfirmation->{RECALLED}->id, $recall->id, "Another patron has placed an item-level recall on this item" );
4552
4553     $recall->set_cancelled;
4554
4555     # biblio-level recall
4556     $recall = Koha::Recall->new(
4557         {   patron_id         => $patron1->borrowernumber,
4558             biblio_id         => $item->biblionumber,
4559             item_id           => undef,
4560             item_level        => 0,
4561             pickup_library_id => $patron1->branchcode,
4562         }
4563     )->store;
4564
4565     ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron2, $item->barcode, undef, undef, undef, undef );
4566     is( $needsconfirmation->{RECALLED}->id, $recall->id, "Another patron has placed a biblio-level recall and this item is eligible to fill it" );
4567
4568     $recall->set_cancelled;
4569
4570     # biblio-level recall
4571     $recall = Koha::Recall->new(
4572         {   patron_id         => $patron1->borrowernumber,
4573             biblio_id         => $item->biblionumber,
4574             item_id           => undef,
4575             item_level        => 0,
4576             pickup_library_id => $patron1->branchcode,
4577         }
4578     )->store;
4579     $recall->set_waiting( { item => $item, expirationdate => dt_from_string() } );
4580
4581     my ( undef, undef, undef, $messages ) = CanBookBeIssued( $patron1, $item->barcode, undef, undef, undef, undef );
4582     is( $messages->{RECALLED}, $recall->id, "This book can be issued by this patron and they have placed a recall" );
4583
4584     $recall->set_cancelled;
4585 };
4586
4587 subtest 'AddReturn should clear items.onloan for unissued items' => sub {
4588     plan tests => 1;
4589
4590     t::lib::Mocks::mock_preference( "AllowReturnToBranch", 'anywhere' );
4591     my $item = $builder->build_sample_item(
4592         {
4593             onloan => '2018-01-01',
4594         }
4595     );
4596
4597     AddReturn( $item->barcode, $item->homebranch );
4598     $item->discard_changes; # refresh
4599     is( $item->onloan, undef, 'AddReturn did clear items.onloan' );
4600 };
4601
4602 subtest 'AddReturn | recalls' => sub {
4603     plan tests => 3;
4604
4605     t::lib::Mocks::mock_preference("UseRecalls", 1);
4606     t::lib::Mocks::mock_preference("item-level_itypes", 1);
4607     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
4608     my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
4609     my $item1 = $builder->build_sample_item;
4610     Koha::CirculationRules->set_rules({
4611         branchcode => undef,
4612         itemtype => undef,
4613         categorycode => undef,
4614         rules => {
4615             recalls_allowed => 10,
4616         },
4617     });
4618
4619     # this item can fill a recall with pickup at this branch
4620     AddIssue( $patron1, $item1->barcode );
4621     my $recall1 = Koha::Recall->new(
4622         {   patron_id         => $patron2->borrowernumber,
4623             biblio_id         => $item1->biblionumber,
4624             item_id           => $item1->itemnumber,
4625             item_level        => 1,
4626             pickup_library_id => $item1->homebranch,
4627         }
4628     )->store;
4629     my ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $item1->homebranch );
4630     is( $messages->{RecallFound}->id, $recall1->id, "Recall found" );
4631     $recall1->set_cancelled;
4632
4633     # this item can fill a recall but needs transfer
4634     AddIssue( $patron1, $item1->barcode );
4635     $recall1 = Koha::Recall->new(
4636         {   patron_id         => $patron2->borrowernumber,
4637             biblio_id         => $item1->biblionumber,
4638             item_id           => $item1->itemnumber,
4639             item_level        => 1,
4640             pickup_library_id => $patron2->branchcode,
4641         }
4642     )->store;
4643     ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $item1->homebranch );
4644     is( $messages->{RecallNeedsTransfer}, $item1->homebranch, "Recall requiring transfer found" );
4645     $recall1->set_cancelled;
4646
4647     # this item is already in transit, do not ask to transfer
4648     AddIssue( $patron1, $item1->barcode );
4649     $recall1 = Koha::Recall->new(
4650         {   patron_id         => $patron2->borrowernumber,
4651             biblio_id         => $item1->biblionumber,
4652             item_id           => $item1->itemnumber,
4653             item_level        => 1,
4654             pickup_library_id => $patron2->branchcode,
4655         }
4656     )->store;
4657     $recall1->start_transfer;
4658     ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $patron2->branchcode );
4659     is( $messages->{TransferredRecall}->id, $recall1->id, "In transit recall found" );
4660     $recall1->set_cancelled;
4661 };
4662
4663 subtest 'AddReturn | bundles' => sub {
4664     plan tests => 1;
4665
4666     my $schema = Koha::Database->schema;
4667     $schema->storage->txn_begin;
4668
4669     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
4670     my $host_item1 = $builder->build_sample_item;
4671     my $bundle_item1 = $builder->build_sample_item;
4672     $schema->resultset('ItemBundle')
4673       ->create(
4674         { host => $host_item1->itemnumber, item => $bundle_item1->itemnumber } );
4675
4676     my ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $bundle_item1->barcode, $bundle_item1->homebranch );
4677     is($messages->{InBundle}->id, $host_item1->id, 'AddReturn returns InBundle host item when item is part of a bundle');
4678
4679     $schema->storage->txn_rollback;
4680 };
4681
4682 subtest 'AddRenewal and AddIssuingCharge tests' => sub {
4683
4684     plan tests => 13;
4685
4686
4687     t::lib::Mocks::mock_preference('item-level_itypes', 1);
4688
4689     my $issuing_charges = 15;
4690     my $title   = 'A title';
4691     my $author  = 'Author, An';
4692     my $barcode = 'WHATARETHEODDS';
4693
4694     my $circ = Test::MockModule->new('C4::Circulation');
4695     $circ->mock(
4696         'GetIssuingCharges',
4697         sub {
4698             return $issuing_charges;
4699         }
4700     );
4701
4702     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
4703     my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes', value => { rentalcharge_daily => 0.00 }});
4704     my $patron   = $builder->build_object({
4705         class => 'Koha::Patrons',
4706         value => { branchcode => $library->id }
4707     });
4708
4709     my $biblio = $builder->build_sample_biblio({ title=> $title, author => $author });
4710     my $item_id = Koha::Item->new(
4711         {
4712             biblionumber     => $biblio->biblionumber,
4713             homebranch       => $library->id,
4714             holdingbranch    => $library->id,
4715             barcode          => $barcode,
4716             replacementprice => 23.00,
4717             itype            => $itemtype->id
4718         },
4719     )->store->itemnumber;
4720     my $item = Koha::Items->find( $item_id );
4721
4722     my $context = Test::MockModule->new('C4::Context');
4723     $context->mock( userenv => { branch => $library->id } );
4724
4725     # Check the item out
4726     AddIssue( $patron, $item->barcode );
4727
4728     throws_ok {
4729         AddRenewal(
4730             {
4731                 borrowernumber  => $patron->borrowernumber,
4732                 itemnumber      => $item->itemnumber,
4733                 branch          => $library->id,
4734                 lastreneweddate => { break => "the_renewal" }
4735             }
4736         );
4737     } 'Koha::Exceptions::Checkout::FailedRenewal', 'Exception is thrown when renewal update to issues fails';
4738
4739     t::lib::Mocks::mock_preference( 'RenewalLog', 0 );
4740     my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
4741     my %params_renewal = (
4742         timestamp => { -like => $date . "%" },
4743         module => "CIRCULATION",
4744         action => "RENEWAL",
4745     );
4746     my $old_log_size = Koha::ActionLogs->count( \%params_renewal );;
4747     AddRenewal(
4748         {
4749             borrowernumber => $patron->id,
4750             itemnumber     => $item->id,
4751             branch         => $library->id
4752         }
4753     );
4754     my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
4755     is( $new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog' );
4756
4757     my $checkouts = $patron->checkouts;
4758     # The following will fail if run on 00:00:00
4759     unlike ( $checkouts->next->lastreneweddate, qr/00:00:00/, 'AddRenewal should set the renewal date with the time part');
4760
4761     my $lines = Koha::Account::Lines->search({
4762         borrowernumber => $patron->id,
4763         itemnumber     => $item->id
4764     });
4765
4766     is( $lines->count, 2 );
4767
4768     my $line = $lines->next;
4769     is( $line->debit_type_code, 'RENT',       'The issue of item with issuing charge generates an accountline of the correct type' );
4770     is( $line->branchcode,  $library->id, 'AddIssuingCharge correctly sets branchcode' );
4771     is( $line->description, '',     'AddIssue does not set a hardcoded description for the accountline' );
4772
4773     $line = $lines->next;
4774     is( $line->debit_type_code, 'RENT_RENEW', 'The renewal of item with issuing charge generates an accountline of the correct type' );
4775     is( $line->branchcode,  $library->id, 'AddRenewal correctly sets branchcode' );
4776     is( $line->description, '', 'AddRenewal does not set a hardcoded description for the accountline' );
4777
4778     t::lib::Mocks::mock_preference( 'RenewalLog', 1 );
4779
4780     $context = Test::MockModule->new('C4::Context');
4781     $context->mock( userenv => { branch => undef, interface => 'CRON'} ); #Test statistical logging of renewal via cron (atuo_renew)
4782
4783     my $now = dt_from_string;
4784     $date = output_pref( { dt => $now, dateonly => 1, dateformat => 'iso' } );
4785     $old_log_size = Koha::ActionLogs->count( \%params_renewal );
4786     my $sth = $dbh->prepare("SELECT COUNT(*) FROM statistics WHERE itemnumber = ? AND branch = ?");
4787     $sth->execute($item->id, $library->id);
4788     my ($old_stats_size) = $sth->fetchrow_array;
4789     AddRenewal(
4790         {
4791             borrowernumber => $patron->id,
4792             itemnumber     => $item->id,
4793             branch         => $library->id
4794         }
4795     );
4796     $new_log_size = Koha::ActionLogs->count( \%params_renewal );
4797     $sth->execute($item->id, $library->id);
4798     my ($new_stats_size) = $sth->fetchrow_array;
4799     is( $new_log_size, $old_log_size + 1, 'renew log successfully added' );
4800     is( $new_stats_size, $old_stats_size + 1, 'renew statistic successfully added with passed branch' );
4801
4802     AddReturn( $item->id, $library->id, undef, $date );
4803     AddIssue( $patron, $item->barcode, $now );
4804     AddRenewal(
4805         {
4806             borrowernumber => $patron->id,
4807             itemnumber     => $item->id,
4808             branch         => $library->id,
4809             skipfinecalc   => 1
4810         }
4811     );
4812     my $lines_skipped = Koha::Account::Lines->search({
4813         borrowernumber => $patron->id,
4814         itemnumber     => $item->id
4815     });
4816     is( $lines_skipped->count, 5, 'Passing skipfinecalc causes fine calculation on renewal to be skipped' );
4817
4818 };
4819
4820 subtest 'AddRenewal() adds to renewals' => sub {
4821     plan tests => 5;
4822
4823     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
4824     my $patron   = $builder->build_object({
4825         class => 'Koha::Patrons',
4826         value => { branchcode => $library->id }
4827     });
4828
4829     my $item = $builder->build_sample_item();
4830
4831     set_userenv( $library->unblessed );
4832
4833     # Check the item out
4834     my $issue = AddIssue( $patron, $item->barcode );
4835     is(ref($issue), 'Koha::Checkout', 'Issue added');
4836
4837     # Renew item
4838     my $duedate = AddRenewal(
4839         {
4840             borrowernumber => $patron->id,
4841             itemnumber     => $item->id,
4842             branch         => $library->id,
4843             automatic      => 1
4844         }
4845     );
4846
4847     ok( $duedate, "Renewal added" );
4848
4849     my $renewals = Koha::Checkouts::Renewals->search({ checkout_id => $issue->issue_id });
4850     is($renewals->count, 1, 'One renewal added');
4851     my $THE_renewal = $renewals->next;
4852     is( $THE_renewal->renewer_id, C4::Context->userenv->{'number'}, 'Renewer recorded from context' );
4853     is( $THE_renewal->renewal_type, 'Automatic', 'AddRenewal "automatic" parameter sets renewal type to "Automatic"');
4854 };
4855
4856 subtest 'ProcessOfflinePayment() tests' => sub {
4857
4858     plan tests => 4;
4859
4860
4861     my $amount = 123;
4862
4863     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
4864     my $library = $builder->build_object({ class => 'Koha::Libraries' });
4865     my $result  = C4::Circulation::ProcessOfflinePayment({ cardnumber => $patron->cardnumber, amount => $amount, branchcode => $library->id });
4866
4867     is( $result, 'Success.', 'The right string is returned' );
4868
4869     my $lines = $patron->account->lines;
4870     is( $lines->count, 1, 'line created correctly');
4871
4872     my $line = $lines->next;
4873     is( $line->amount+0, $amount * -1, 'amount picked from params' );
4874     is( $line->branchcode, $library->id, 'branchcode set correctly' );
4875
4876 };
4877
4878 subtest 'Incremented fee tests' => sub {
4879     plan tests => 19;
4880
4881     my $dt = dt_from_string();
4882     Time::Fake->offset( $dt->epoch );
4883
4884     t::lib::Mocks::mock_preference( 'item-level_itypes', 1 );
4885
4886     my $library = $builder->build_object( { class => 'Koha::Libraries' } )->store;
4887
4888     $module->mock( 'userenv', sub { { branch => $library->id } } );
4889
4890     my $patron = $builder->build_object(
4891         {
4892             class => 'Koha::Patrons',
4893             value => { categorycode => $patron_category->{categorycode} }
4894         }
4895     )->store;
4896
4897     my $itemtype = $builder->build_object(
4898         {
4899             class => 'Koha::ItemTypes',
4900             value => {
4901                 notforloan                   => undef,
4902                 rentalcharge                 => 0,
4903                 rentalcharge_daily           => 1,
4904                 rentalcharge_daily_calendar  => 0
4905             }
4906         }
4907     )->store;
4908
4909     my $item = $builder->build_sample_item(
4910         {
4911             library  => $library->id,
4912             itype    => $itemtype->id,
4913         }
4914     );
4915
4916     is( $itemtype->rentalcharge_daily + 0,1, 'Daily rental charge stored and retreived correctly' );
4917     is( $item->effective_itemtype, $itemtype->id, "Itemtype set correctly for item" );
4918
4919     my $now         = dt_from_string;
4920     my $dt_from     = $now->clone;
4921     my $dt_to       = $now->clone->add( days => 7 );
4922     my $dt_to_renew = $now->clone->add( days => 13 );
4923
4924     # Daily Tests
4925     my $issue =
4926       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
4927     my $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
4928     is(
4929         $accountline->amount + 0,
4930         7,
4931         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0"
4932     );
4933     $accountline->delete();
4934     AddRenewal(
4935         {
4936             borrowernumber  => $patron->id,
4937             itemnumber      => $item->id,
4938             branch          => $library->id,
4939             datedue         => $dt_to_renew,
4940             lastreneweddate => $dt_to
4941         }
4942     );
4943     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
4944     is(
4945         $accountline->amount + 0,
4946         6,
4947         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0, for renewal"
4948     );
4949     $accountline->delete();
4950     $issue->delete();
4951
4952     t::lib::Mocks::mock_preference( 'finesCalendar', 'noFinesWhenClosed' );
4953     $itemtype->rentalcharge_daily_calendar(1)->store();
4954     $issue =
4955       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
4956     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
4957     is(
4958         $accountline->amount + 0,
4959         7,
4960         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1"
4961     );
4962     $accountline->delete();
4963     AddRenewal(
4964         {
4965             borrowernumber  => $patron->id,
4966             itemnumber      => $item->id,
4967             branch          => $library->id,
4968             datedue         => $dt_to_renew,
4969             lastreneweddate => $dt_to
4970         }
4971     );
4972     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
4973     is(
4974         $accountline->amount + 0,
4975         6,
4976         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1, for renewal"
4977     );
4978     $accountline->delete();
4979     $issue->delete();
4980
4981     my $calendar = C4::Calendar->new( branchcode => $library->id );
4982     # DateTime 1..7 (Mon..Sun), C4::Calender 0..6 (Sun..Sat)
4983     my $closed_day =
4984         ( $dt_from->day_of_week == 6 ) ? 0
4985       : ( $dt_from->day_of_week == 7 ) ? 1
4986       :                                  $dt_from->day_of_week + 1;
4987     my $closed_day_name = $dt_from->clone->add(days => 1)->day_name;
4988     $calendar->insert_week_day_holiday(
4989         weekday     => $closed_day,
4990         title       => 'Test holiday',
4991         description => 'Test holiday'
4992     );
4993     $issue =
4994       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
4995     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
4996     is(
4997         $accountline->amount + 0,
4998         6,
4999         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name"
5000     );
5001     $accountline->delete();
5002     AddRenewal(
5003         {
5004             borrowernumber  => $patron->id,
5005             itemnumber      => $item->id,
5006             branch          => $library->id,
5007             datedue         => $dt_to_renew,
5008             lastreneweddate => $dt_to
5009         }
5010     );
5011     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5012     is(
5013         $accountline->amount + 0,
5014         5,
5015         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name, for renewal"
5016     );
5017     $accountline->delete();
5018     $issue->delete();
5019
5020     $itemtype->rentalcharge(2)->store;
5021     is( $itemtype->rentalcharge + 0, 2, 'Rental charge updated and retreived correctly' );
5022     $issue =
5023       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5024     my $accountlines =
5025       Koha::Account::Lines->search( { itemnumber => $item->id } );
5026     is( $accountlines->count, '2', "Fixed charge and accrued charge recorded distinctly" );
5027     $accountlines->delete();
5028     AddRenewal(
5029         {
5030             borrowernumber  => $patron->id,
5031             itemnumber      => $item->id,
5032             branch          => $library->id,
5033             datedue         => $dt_to_renew,
5034             lastreneweddate => $dt_to
5035         }
5036     );
5037     $accountlines = Koha::Account::Lines->search( { itemnumber => $item->id } );
5038     is( $accountlines->count, '2', "Fixed charge and accrued charge recorded distinctly, for renewal" );
5039     $accountlines->delete();
5040     $issue->delete();
5041     $itemtype->rentalcharge(0)->store;
5042     is( $itemtype->rentalcharge + 0, 0, 'Rental charge reset and retreived correctly' );
5043
5044     # Hourly
5045     Koha::CirculationRules->set_rule(
5046         {
5047             categorycode => $patron->categorycode,
5048             itemtype     => $itemtype->id,
5049             branchcode   => $library->id,
5050             rule_name    => 'lengthunit',
5051             rule_value   => 'hours',
5052         }
5053     );
5054
5055     $itemtype->rentalcharge_hourly('0.25')->store();
5056     is( $itemtype->rentalcharge_hourly, '0.25', 'Hourly rental charge stored and retreived correctly' );
5057
5058     $dt_to       = $now->clone->add( hours => 168 );
5059     $dt_to_renew = $now->clone->add( hours => 312 );
5060
5061     $itemtype->rentalcharge_hourly_calendar(0)->store();
5062     $issue =
5063       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5064     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5065     is(
5066         $accountline->amount + 0,
5067         42,
5068         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0 (168h * 0.25u)"
5069     );
5070     $accountline->delete();
5071     AddRenewal(
5072         {
5073             borrowernumber  => $patron->id,
5074             itemnumber      => $item->id,
5075             branch          => $library->id,
5076             datedue         => $dt_to_renew,
5077             lastreneweddate => $dt_to
5078         }
5079     );
5080     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5081     is(
5082         $accountline->amount + 0,
5083         36,
5084         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0, for renewal (312h - 168h * 0.25u)"
5085     );
5086     $accountline->delete();
5087     $issue->delete();
5088
5089     $itemtype->rentalcharge_hourly_calendar(1)->store();
5090     $issue =
5091       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5092     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5093     is(
5094         $accountline->amount + 0,
5095         36,
5096         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name (168h - 24h * 0.25u)"
5097     );
5098     $accountline->delete();
5099     AddRenewal(
5100         {
5101             borrowernumber  => $patron->id,
5102             itemnumber      => $item->id,
5103             branch          => $library->id,
5104             datedue         => $dt_to_renew,
5105             lastreneweddate => $dt_to
5106         }
5107     );
5108     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5109     is(
5110         $accountline->amount + 0,
5111         30,
5112         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name, for renewal (312h - 168h - 24h * 0.25u"
5113     );
5114     $accountline->delete();
5115     $issue->delete();
5116
5117     $calendar->delete_holiday( weekday => $closed_day );
5118     $issue =
5119       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5120     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5121     is(
5122         $accountline->amount + 0,
5123         42,
5124         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 (168h - 0h * 0.25u"
5125     );
5126     $accountline->delete();
5127     AddRenewal(
5128         {
5129             borrowernumber  => $patron->id,
5130             itemnumber      => $item->id,
5131             branch          => $library->id,
5132             datedue         => $dt_to_renew,
5133             lastreneweddate => $dt_to
5134         }
5135     );
5136     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5137     is(
5138         $accountline->amount + 0,
5139         36,
5140         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1, for renewal (312h - 168h - 0h * 0.25u)"
5141     );
5142     $accountline->delete();
5143     $issue->delete();
5144     Time::Fake->reset;
5145 };
5146
5147 subtest 'CanBookBeIssued & RentalFeesCheckoutConfirmation' => sub {
5148     plan tests => 2;
5149
5150     t::lib::Mocks::mock_preference('RentalFeesCheckoutConfirmation', 1);
5151     t::lib::Mocks::mock_preference('item-level_itypes', 1);
5152
5153     my $library =
5154       $builder->build_object( { class => 'Koha::Libraries' } )->store;
5155     my $patron = $builder->build_object(
5156         {
5157             class => 'Koha::Patrons',
5158             value => { categorycode => $patron_category->{categorycode} }
5159         }
5160     )->store;
5161
5162     my $itemtype = $builder->build_object(
5163         {
5164             class => 'Koha::ItemTypes',
5165             value => {
5166                 notforloan             => 0,
5167                 rentalcharge           => 0,
5168                 rentalcharge_daily => 0
5169             }
5170         }
5171     );
5172
5173     my $item = $builder->build_sample_item(
5174         {
5175             library    => $library->id,
5176             notforloan => 0,
5177             itemlost   => 0,
5178             withdrawn  => 0,
5179             itype      => $itemtype->id,
5180         }
5181     )->store;
5182
5183     my ( $issuingimpossible, $needsconfirmation );
5184     my $dt_from = dt_from_string();
5185     my $dt_due = $dt_from->clone->add( days => 3 );
5186
5187     $itemtype->rentalcharge(1)->store;
5188     ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
5189     is_deeply( $needsconfirmation, { RENTALCHARGE => '1.00' }, 'Item needs rentalcharge confirmation to be issued' );
5190     $itemtype->rentalcharge('0')->store;
5191     $itemtype->rentalcharge_daily(1)->store;
5192     ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
5193     is_deeply( $needsconfirmation, { RENTALCHARGE => '3' }, 'Item needs rentalcharge confirmation to be issued, increment' );
5194     $itemtype->rentalcharge_daily('0')->store;
5195 };
5196
5197 subtest 'CanBookBeIssued & CircConfirmItemParts' => sub {
5198     plan tests => 1;
5199
5200     t::lib::Mocks::mock_preference('CircConfirmItemParts', 1);
5201
5202     my $patron = $builder->build_object(
5203         {
5204             class => 'Koha::Patrons',
5205             value => { categorycode => $patron_category->{categorycode} }
5206         }
5207     )->store;
5208
5209     my $item = $builder->build_sample_item(
5210         {
5211             materials => 'includes DVD',
5212         }
5213     )->store;
5214
5215     my $dt_due = dt_from_string->add( days => 3 );
5216
5217     my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
5218     is_deeply( $needsconfirmation, { ADDITIONAL_MATERIALS => 'includes DVD' }, 'Item needs confirmation of additional parts' );
5219 };
5220
5221 subtest 'Do not return on renewal (LOST charge)' => sub {
5222     plan tests => 1;
5223
5224     t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'onpayment');
5225     my $library = $builder->build_object( { class => "Koha::Libraries" } );
5226     my $manager = $builder->build_object( { class => "Koha::Patrons" } );
5227     t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
5228
5229     my $biblio = $builder->build_sample_biblio;
5230
5231     my $item = $builder->build_sample_item(
5232         {
5233             biblionumber     => $biblio->biblionumber,
5234             library          => $library->branchcode,
5235             replacementprice => 99.00,
5236             itype            => $itemtype,
5237         }
5238     );
5239
5240     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
5241     AddIssue( $patron, $item->barcode );
5242
5243     my $accountline = Koha::Account::Line->new(
5244         {
5245             borrowernumber    => $patron->borrowernumber,
5246             debit_type_code   => 'LOST',
5247             status            => undef,
5248             itemnumber        => $item->itemnumber,
5249             amount            => 12,
5250             amountoutstanding => 12,
5251             interface         => 'something',
5252         }
5253     )->store();
5254
5255     # AddRenewal doesn't call _FixAccountForLostAndFound
5256     AddIssue( $patron, $item->barcode );
5257
5258     is( $patron->checkouts->count, 1,
5259         'Renewal should not return the item even if a LOST payment has been made earlier'
5260     );
5261 };
5262
5263 subtest 'Filling a hold should cancel existing transfer' => sub {
5264     plan tests => 4;
5265
5266     t::lib::Mocks::mock_preference('AutomaticItemReturn', 1);
5267
5268     my $libraryA = $builder->build_object( { class => 'Koha::Libraries' } );
5269     my $libraryB = $builder->build_object( { class => 'Koha::Libraries' } );
5270     my $patron = $builder->build_object(
5271         {
5272             class => 'Koha::Patrons',
5273             value => {
5274                 categorycode => $patron_category->{categorycode},
5275                 branchcode => $libraryA->branchcode,
5276             }
5277         }
5278     )->store;
5279
5280     my $item = $builder->build_sample_item({
5281         homebranch => $libraryB->branchcode,
5282     });
5283
5284     my ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
5285     is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 1, "We generate a transfer on checkin");
5286     AddReserve({
5287         branchcode     => $libraryA->branchcode,
5288         borrowernumber => $patron->borrowernumber,
5289         biblionumber   => $item->biblionumber,
5290         itemnumber     => $item->itemnumber
5291     });
5292     my $reserves = Koha::Holds->search({ itemnumber => $item->itemnumber });
5293     is( $reserves->count, 1, "Reserve is placed");
5294     ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
5295     my $reserve = $reserves->next;
5296     ModReserveAffect( $item->itemnumber, $patron->borrowernumber, 0, $reserve->reserve_id );
5297     $reserve->discard_changes;
5298     ok( $reserve->found eq 'W', "Reserve is marked waiting" );
5299     is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 0, "No outstanding transfers when hold is waiting");
5300 };
5301
5302 subtest 'Tests for NoRefundOnLostReturnedItemsAge with AddReturn' => sub {
5303
5304     plan tests => 4;
5305
5306     t::lib::Mocks::mock_preference('BlockReturnOfLostItems', 0);
5307     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
5308     my $patron  = $builder->build_object(
5309         {
5310             class => 'Koha::Patrons',
5311             value => { categorycode => $patron_category->{categorycode} }
5312         }
5313     );
5314
5315     my $biblionumber = $builder->build_sample_biblio(
5316         {
5317             branchcode => $library->branchcode,
5318         }
5319     )->biblionumber;
5320
5321     # And the circulation rule
5322     Koha::CirculationRules->search->delete;
5323     Koha::CirculationRules->set_rules(
5324         {
5325             categorycode => undef,
5326             itemtype     => undef,
5327             branchcode   => undef,
5328             rules        => {
5329                 issuelength => 14,
5330                 lengthunit  => 'days',
5331             }
5332         }
5333     );
5334     $builder->build(
5335         {
5336             source => 'CirculationRule',
5337             value  => {
5338                 branchcode   => undef,
5339                 categorycode => undef,
5340                 itemtype     => undef,
5341                 rule_name    => 'lostreturn',
5342                 rule_value   => 'refund'
5343             }
5344         }
5345     );
5346
5347     subtest 'NoRefundOnLostReturnedItemsAge = undef' => sub {
5348         plan tests => 3;
5349
5350         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5351         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', undef );
5352
5353         my $lost_on = dt_from_string->subtract( days => 7 )->date;
5354
5355         my $item = $builder->build_sample_item(
5356             {
5357                 biblionumber     => $biblionumber,
5358                 library          => $library->branchcode,
5359                 replacementprice => '42',
5360             }
5361         );
5362         my $issue = AddIssue( $patron, $item->barcode );
5363         LostItem( $item->itemnumber, 'cli', 0 );
5364         $item->_result->itemlost(1);
5365         $item->_result->itemlost_on( $lost_on );
5366         $item->_result->update();
5367
5368         my $a = Koha::Account::Lines->search(
5369             {
5370                 itemnumber     => $item->id,
5371                 borrowernumber => $patron->borrowernumber
5372             }
5373         )->next;
5374         ok( $a, "Found accountline for lost fee" );
5375         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5376         my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
5377         $a = $a->get_from_storage;
5378         is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
5379         $a->delete;
5380     };
5381
5382     subtest 'NoRefundOnLostReturnedItemsAge > length of days item has been lost' => sub {
5383         plan tests => 3;
5384
5385         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5386         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5387
5388         my $lost_on = dt_from_string->subtract( days => 6 )->date;
5389
5390         my $item = $builder->build_sample_item(
5391             {
5392                 biblionumber     => $biblionumber,
5393                 library          => $library->branchcode,
5394                 replacementprice => '42',
5395             }
5396         );
5397         my $issue = AddIssue( $patron, $item->barcode );
5398         LostItem( $item->itemnumber, 'cli', 0 );
5399         $item->_result->itemlost(1);
5400         $item->_result->itemlost_on( $lost_on );
5401         $item->_result->update();
5402
5403         my $a = Koha::Account::Lines->search(
5404             {
5405                 itemnumber     => $item->id,
5406                 borrowernumber => $patron->borrowernumber
5407             }
5408         )->next;
5409         ok( $a, "Found accountline for lost fee" );
5410         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5411         my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
5412         $a = $a->get_from_storage;
5413         is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
5414         $a->delete;
5415     };
5416
5417     subtest 'NoRefundOnLostReturnedItemsAge = length of days item has been lost' => sub {
5418         plan tests => 3;
5419
5420         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5421         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5422
5423         my $lost_on = dt_from_string->subtract( days => 7 )->date;
5424
5425         my $item = $builder->build_sample_item(
5426             {
5427                 biblionumber     => $biblionumber,
5428                 library          => $library->branchcode,
5429                 replacementprice => '42',
5430             }
5431         );
5432         my $issue = AddIssue( $patron, $item->barcode );
5433         LostItem( $item->itemnumber, 'cli', 0 );
5434         $item->_result->itemlost(1);
5435         $item->_result->itemlost_on( $lost_on );
5436         $item->_result->update();
5437
5438         my $a = Koha::Account::Lines->search(
5439             {
5440                 itemnumber     => $item->id,
5441                 borrowernumber => $patron->borrowernumber
5442             }
5443         )->next;
5444         ok( $a, "Found accountline for lost fee" );
5445         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5446         my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
5447         $a = $a->get_from_storage;
5448         is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
5449         $a->delete;
5450     };
5451
5452     subtest 'NoRefundOnLostReturnedItemsAge < length of days item has been lost' => sub {
5453         plan tests => 3;
5454
5455         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5456         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5457
5458         my $lost_on = dt_from_string->subtract( days => 8 )->date;
5459
5460         my $item = $builder->build_sample_item(
5461             {
5462                 biblionumber     => $biblionumber,
5463                 library          => $library->branchcode,
5464                 replacementprice => '42',
5465             }
5466         );
5467         my $issue = AddIssue( $patron, $item->barcode );
5468         LostItem( $item->itemnumber, 'cli', 0 );
5469         $item->_result->itemlost(1);
5470         $item->_result->itemlost_on( $lost_on );
5471         $item->_result->update();
5472
5473         my $a = Koha::Account::Lines->search(
5474             {
5475                 itemnumber     => $item->id,
5476                 borrowernumber => $patron->borrowernumber
5477             }
5478         );
5479         $a = $a->next;
5480         ok( $a, "Found accountline for lost fee" );
5481         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5482         my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
5483         $a = $a->get_from_storage;
5484         is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
5485         $a->delete;
5486     };
5487 };
5488
5489 subtest 'Tests for NoRefundOnLostReturnedItemsAge with AddIssue' => sub {
5490
5491     plan tests => 4;
5492
5493     t::lib::Mocks::mock_preference('BlockReturnOfLostItems', 0);
5494     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
5495     my $patron  = $builder->build_object(
5496         {
5497             class => 'Koha::Patrons',
5498             value => { categorycode => $patron_category->{categorycode} }
5499         }
5500     );
5501     my $patron2  = $builder->build_object(
5502         {
5503             class => 'Koha::Patrons',
5504             value => { categorycode => $patron_category->{categorycode} }
5505         }
5506     );
5507
5508     my $biblionumber = $builder->build_sample_biblio(
5509         {
5510             branchcode => $library->branchcode,
5511         }
5512     )->biblionumber;
5513
5514     # And the circulation rule
5515     Koha::CirculationRules->search->delete;
5516     Koha::CirculationRules->set_rules(
5517         {
5518             categorycode => undef,
5519             itemtype     => undef,
5520             branchcode   => undef,
5521             rules        => {
5522                 issuelength => 14,
5523                 lengthunit  => 'days',
5524             }
5525         }
5526     );
5527     $builder->build(
5528         {
5529             source => 'CirculationRule',
5530             value  => {
5531                 branchcode   => undef,
5532                 categorycode => undef,
5533                 itemtype     => undef,
5534                 rule_name    => 'lostreturn',
5535                 rule_value   => 'refund'
5536             }
5537         }
5538     );
5539
5540     subtest 'NoRefundOnLostReturnedItemsAge = undef' => sub {
5541         plan tests => 3;
5542
5543         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5544         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', undef );
5545
5546         my $lost_on = dt_from_string->subtract( days => 7 )->date;
5547
5548         my $item = $builder->build_sample_item(
5549             {
5550                 biblionumber     => $biblionumber,
5551                 library          => $library->branchcode,
5552                 replacementprice => '42',
5553             }
5554         );
5555         my $issue = AddIssue( $patron, $item->barcode );
5556         LostItem( $item->itemnumber, 'cli', 0 );
5557         $item->_result->itemlost(1);
5558         $item->_result->itemlost_on( $lost_on );
5559         $item->_result->update();
5560
5561         my $a = Koha::Account::Lines->search(
5562             {
5563                 itemnumber     => $item->id,
5564                 borrowernumber => $patron->borrowernumber
5565             }
5566         )->next;
5567         ok( $a, "Found accountline for lost fee" );
5568         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5569         $issue = AddIssue( $patron2, $item->barcode );
5570         $a = $a->get_from_storage;
5571         is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
5572         $a->delete;
5573         $issue->delete;
5574     };
5575
5576     subtest 'NoRefundOnLostReturnedItemsAge > length of days item has been lost' => sub {
5577         plan tests => 3;
5578
5579         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5580         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5581
5582         my $lost_on = dt_from_string->subtract( days => 6 )->date;
5583
5584         my $item = $builder->build_sample_item(
5585             {
5586                 biblionumber     => $biblionumber,
5587                 library          => $library->branchcode,
5588                 replacementprice => '42',
5589             }
5590         );
5591         my $issue = AddIssue( $patron, $item->barcode );
5592         LostItem( $item->itemnumber, 'cli', 0 );
5593         $item->_result->itemlost(1);
5594         $item->_result->itemlost_on( $lost_on );
5595         $item->_result->update();
5596
5597         my $a = Koha::Account::Lines->search(
5598             {
5599                 itemnumber     => $item->id,
5600                 borrowernumber => $patron->borrowernumber
5601             }
5602         )->next;
5603         ok( $a, "Found accountline for lost fee" );
5604         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5605         $issue = AddIssue( $patron2, $item->barcode );
5606         $a = $a->get_from_storage;
5607         is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
5608         $a->delete;
5609     };
5610
5611     subtest 'NoRefundOnLostReturnedItemsAge = length of days item has been lost' => sub {
5612         plan tests => 3;
5613
5614         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5615         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5616
5617         my $lost_on = dt_from_string->subtract( days => 7 )->date;
5618
5619         my $item = $builder->build_sample_item(
5620             {
5621                 biblionumber     => $biblionumber,
5622                 library          => $library->branchcode,
5623                 replacementprice => '42',
5624             }
5625         );
5626         my $issue = AddIssue( $patron, $item->barcode );
5627         LostItem( $item->itemnumber, 'cli', 0 );
5628         $item->_result->itemlost(1);
5629         $item->_result->itemlost_on( $lost_on );
5630         $item->_result->update();
5631
5632         my $a = Koha::Account::Lines->search(
5633             {
5634                 itemnumber     => $item->id,
5635                 borrowernumber => $patron->borrowernumber
5636             }
5637         )->next;
5638         ok( $a, "Found accountline for lost fee" );
5639         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5640         $issue = AddIssue( $patron2, $item->barcode );
5641         $a = $a->get_from_storage;
5642         is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
5643         $a->delete;
5644     };
5645
5646     subtest 'NoRefundOnLostReturnedItemsAge < length of days item has been lost' => sub {
5647         plan tests => 3;
5648
5649         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5650         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5651
5652         my $lost_on = dt_from_string->subtract( days => 8 )->date;
5653
5654         my $item = $builder->build_sample_item(
5655             {
5656                 biblionumber     => $biblionumber,
5657                 library          => $library->branchcode,
5658                 replacementprice => '42',
5659             }
5660         );
5661         my $issue = AddIssue( $patron, $item->barcode );
5662         LostItem( $item->itemnumber, 'cli', 0 );
5663         $item->_result->itemlost(1);
5664         $item->_result->itemlost_on( $lost_on );
5665         $item->_result->update();
5666
5667         my $a = Koha::Account::Lines->search(
5668             {
5669                 itemnumber     => $item->id,
5670                 borrowernumber => $patron->borrowernumber
5671             }
5672         );
5673         $a = $a->next;
5674         ok( $a, "Found accountline for lost fee" );
5675         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5676         $issue = AddIssue( $patron2, $item->barcode );
5677         $a = $a->get_from_storage;
5678         is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
5679         $a->delete;
5680     };
5681 };
5682
5683 subtest 'transferbook tests' => sub {
5684     plan tests => 9;
5685
5686     throws_ok
5687     { C4::Circulation::transferbook({}); }
5688     'Koha::Exceptions::MissingParameter',
5689     'Koha::Patron->store raises an exception on missing params';
5690
5691     throws_ok
5692     { C4::Circulation::transferbook({to_branch=>'anything'}); }
5693     'Koha::Exceptions::MissingParameter',
5694     'Koha::Patron->store raises an exception on missing params';
5695
5696     throws_ok
5697     { C4::Circulation::transferbook({from_branch=>'anything'}); }
5698     'Koha::Exceptions::MissingParameter',
5699     'Koha::Patron->store raises an exception on missing params';
5700
5701     my ($doreturn,$messages) = C4::Circulation::transferbook({to_branch=>'there',from_branch=>'here'});
5702     is( $doreturn, 0, "No return without barcode");
5703     ok( exists $messages->{BadBarcode}, "We get a BadBarcode message if no barcode passed");
5704     is( $messages->{BadBarcode}, undef, "No barcode passed means undef BadBarcode" );
5705
5706     ($doreturn,$messages) = C4::Circulation::transferbook({to_branch=>'there',from_branch=>'here',barcode=>'BadBarcode'});
5707     is( $doreturn, 0, "No return without barcode");
5708     ok( exists $messages->{BadBarcode}, "We get a BadBarcode message if no barcode passed");
5709     is( $messages->{BadBarcode}, 'BadBarcode', "No barcode passed means undef BadBarcode" );
5710
5711 };
5712
5713 subtest 'Checkout should correctly terminate a transfer' => sub {
5714     plan tests => 7;
5715
5716     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
5717     my $patron_1 = $builder->build_object(
5718         {
5719             class => 'Koha::Patrons',
5720             value => { branchcode => $library_1->branchcode }
5721         }
5722     );
5723     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
5724     my $patron_2 = $builder->build_object(
5725         {
5726             class => 'Koha::Patrons',
5727             value => { branchcode => $library_2->branchcode }
5728         }
5729     );
5730
5731     my $item = $builder->build_sample_item(
5732         {
5733             library => $library_1->branchcode,
5734         }
5735     );
5736
5737     t::lib::Mocks::mock_userenv( { branchcode => $library_1->branchcode } );
5738     my $reserve_id = AddReserve(
5739         {
5740             branchcode     => $library_2->branchcode,
5741             borrowernumber => $patron_2->borrowernumber,
5742             biblionumber   => $item->biblionumber,
5743             itemnumber     => $item->itemnumber,
5744             priority       => 1,
5745         }
5746     );
5747
5748     my $do_transfer = 1;
5749     ModItemTransfer( $item->itemnumber, $library_1->branchcode,
5750         $library_2->branchcode, 'Manual' );
5751     ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
5752     GetOtherReserves( $item->itemnumber )
5753       ;    # To put the Reason, it's what does returns.pl...
5754     my $hold = Koha::Holds->find($reserve_id);
5755     is( $hold->found, 'T', 'Hold is in transit' );
5756     my $transfer = $item->get_transfer;
5757     is( $transfer->frombranch, $library_1->branchcode );
5758     is( $transfer->tobranch,   $library_2->branchcode );
5759     is( $transfer->reason,     'Reserve' );
5760
5761     t::lib::Mocks::mock_userenv( { branchcode => $library_2->branchcode } );
5762     AddIssue( $patron_1, $item->barcode );
5763     $transfer = $transfer->get_from_storage;
5764     isnt( $transfer->datearrived, undef );
5765     $hold = $hold->get_from_storage;
5766     is( $hold->found, undef, 'Hold is waiting' );
5767     is( $hold->priority, 1, );
5768 };
5769
5770 subtest 'AddIssue records staff who checked out item if appropriate' => sub  {
5771     plan tests => 2;
5772
5773     $module->mock( 'userenv', sub { { branch => $library->{id} } } );
5774
5775     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
5776     my $patron = $builder->build_object(
5777         {
5778             class => 'Koha::Patrons',
5779             value => { categorycode => $patron_category->{categorycode} }
5780         }
5781     );
5782     my $issuer = $builder->build_object(
5783         {
5784             class => 'Koha::Patrons',
5785             value => { categorycode => $patron_category->{categorycode} }
5786         }
5787     );
5788     my $item_1 = $builder->build_sample_item(
5789         {
5790             library  => $library->{branchcode}
5791         }
5792     );
5793
5794     my $item_2 = $builder->build_sample_item(
5795         {
5796             library  => $library->branchcode
5797         }
5798     );
5799
5800     $module->mock( 'userenv', sub { { branch => $library->id, number => $issuer->borrowernumber } } );
5801
5802     my $dt_from = dt_from_string();
5803     my $dt_to   = dt_from_string()->add( days => 7 );
5804
5805     my $issue_1 = AddIssue( $patron, $item_1->barcode, $dt_to, undef, $dt_from );
5806
5807     is( $issue_1->issuer, undef, "Staff who checked out the item not recorded when RecordStaffUserOnCheckout turned off" );
5808
5809     t::lib::Mocks::mock_preference('RecordStaffUserOnCheckout', 1);
5810
5811     my $issue_2 =
5812       AddIssue( $patron, $item_2->barcode, $dt_to, undef, $dt_from );
5813
5814     is( $issue_2->issuer->borrowernumber, $issuer->borrowernumber, "Staff who checked out the item recorded when RecordStaffUserOnCheckout turned on" );
5815 };
5816
5817 subtest "Item's onloan value should be set if checked out item is checked out to a different patron" => sub {
5818     plan tests => 2;
5819
5820     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
5821     my $patron_1 = $builder->build_object(
5822         {
5823             class => 'Koha::Patrons',
5824             value => { branchcode => $library_1->branchcode }
5825         }
5826     );
5827     my $patron_2 = $builder->build_object(
5828         {
5829             class => 'Koha::Patrons',
5830             value => { branchcode => $library_1->branchcode }
5831         }
5832     );
5833
5834     my $item = $builder->build_sample_item(
5835         {
5836             library => $library_1->branchcode,
5837         }
5838     );
5839
5840     AddIssue( $patron_1, $item->barcode );
5841     ok( $item->get_from_storage->onloan, "Item's onloan column is set after initial checkout" );
5842     AddIssue( $patron_2, $item->barcode );
5843     ok( $item->get_from_storage->onloan, "Item's onloan column is set after second checkout" );
5844 };
5845
5846 subtest "updateWrongTransfer tests" => sub {
5847     plan tests => 5;
5848
5849     my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
5850     my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
5851     my $library3 = $builder->build_object( { class => 'Koha::Libraries' } );
5852     my $item     = $builder->build_sample_item(
5853         {
5854             homebranch    => $library1->branchcode,
5855             holdingbranch => $library2->branchcode,
5856             datelastseen  => undef
5857         }
5858     );
5859
5860     my $transfer = $builder->build_object(
5861         {
5862             class => 'Koha::Item::Transfers',
5863             value => {
5864                 itemnumber    => $item->itemnumber,
5865                 frombranch    => $library2->branchcode,
5866                 tobranch      => $library1->branchcode,
5867                 daterequested => dt_from_string,
5868                 datesent      => dt_from_string,
5869                 datecancelled => undef,
5870                 datearrived   => undef,
5871                 reason        => 'Manual'
5872             }
5873         }
5874     );
5875     is( ref($transfer), 'Koha::Item::Transfer', 'Mock transfer added' );
5876
5877     my $new_transfer = C4::Circulation::updateWrongTransfer($item->itemnumber, $library1->branchcode);
5878     is(ref($new_transfer), 'Koha::Item::Transfer', "updateWrongTransfer returns a 'Koha::Item::Transfer' object");
5879     ok( !$new_transfer->in_transit, "New transfer is NOT created as in transit (or cancelled)");
5880
5881     my $original_transfer = $transfer->get_from_storage;
5882     ok( defined($original_transfer->datecancelled), "Original transfer was cancelled");
5883     is( $original_transfer->cancellation_reason, 'WrongTransfer', "Original transfer cancellation reason is 'WrongTransfer'");
5884 };
5885
5886 subtest "SendCirculationAlert" => sub {
5887     plan tests => 3;
5888
5889     # When you would unsuspectingly call this unit test (with perl, not prove), you will be bitten by LOCK.
5890     # LOCK will commit changes and ruin your data
5891     # In order to prevent that, we will add KOHA_TESTING to $ENV; see further Circulation.pm
5892     $ENV{KOHA_TESTING} = 1;
5893
5894     # Setup branch, borrowr, and notice
5895     my $library = $builder->build_object({ class => 'Koha::Libraries' });
5896     set_userenv( $library->unblessed);
5897     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
5898     C4::Members::Messaging::SetMessagingPreference({
5899         borrowernumber => $patron->id,
5900         message_transport_types => ['sms'],
5901         message_attribute_id => 5
5902     });
5903     my $item = $builder->build_sample_item();
5904     my $checkin_notice = $builder->build_object({
5905         class => 'Koha::Notice::Templates',
5906         value =>{
5907             module => 'circulation',
5908             code => 'CHECKIN',
5909             branchcode => $library->branchcode,
5910             name => 'Test Checkin',
5911             is_html => 0,
5912             content => "Checkins:\n----\n[% biblio.title %]-[% old_checkout.issue_id %]\n----Thank you.",
5913             message_transport_type => 'sms',
5914             lang => 'default'
5915         }
5916     })->store;
5917
5918     # Checkout an item, mark it returned, generate a notice
5919     my $issue_1 = AddIssue( $patron, $item->barcode);
5920     MarkIssueReturned( $patron->borrowernumber, $item->itemnumber, undef, 0, { skip_record_index => 1} );
5921     C4::Circulation::SendCirculationAlert({
5922         type => 'CHECKIN',
5923         item => $item->unblessed,
5924         borrower => $patron->unblessed,
5925         branch => $library->branchcode,
5926         issue => $issue_1
5927     });
5928     my $notice = Koha::Notice::Messages->find({ borrowernumber => $patron->id, letter_code => 'CHECKIN' });
5929     is($notice->content,"Checkins:\n".$item->biblio->title."-".$issue_1->id."\nThank you.", 'Letter generated with expected output on first checkin' );
5930     is($notice->to_address, $patron->smsalertnumber, "Letter has the correct to_address set to smsalertnumber for SMS type notices");
5931
5932     # Checkout an item, mark it returned, generate a notice
5933     my $issue_2 = AddIssue( $patron, $item->barcode);
5934     MarkIssueReturned( $patron->borrowernumber, $item->itemnumber, undef, 0, { skip_record_index => 1} );
5935     C4::Circulation::SendCirculationAlert({
5936         type => 'CHECKIN',
5937         item => $item->unblessed,
5938         borrower => $patron->unblessed,
5939         branch => $library->branchcode,
5940         issue => $issue_2
5941     });
5942     $notice->discard_changes();
5943     is($notice->content,"Checkins:\n".$item->biblio->title."-".$issue_1->id."\n".$item->biblio->title."-".$issue_2->id."\nThank you.", 'Letter appended with expected output on second checkin' );
5944
5945 };
5946
5947 subtest "GetSoonestRenewDate tests" => sub {
5948     plan tests => 6;
5949     Koha::CirculationRules->set_rule(
5950         {
5951             categorycode => undef,
5952             branchcode   => undef,
5953             itemtype     => undef,
5954             rule_name    => 'norenewalbefore',
5955             rule_value   => '7',
5956         }
5957     );
5958     my $patron = $builder->build_object(
5959         {
5960             class => 'Koha::Patrons',
5961             value => {
5962                 autorenew_checkouts => 1,
5963             }
5964         }
5965     );
5966     my $item = $builder->build_sample_item();
5967     my $issue = AddIssue( $patron, $item->barcode);
5968     my $datedue = dt_from_string( $issue->date_due() );
5969
5970     # Bug 14395
5971     # Test 'exact time' setting for syspref NoRenewalBeforePrecision
5972     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact_time' );
5973     is(
5974         GetSoonestRenewDate( $patron, $issue ),
5975         $datedue->clone->add( days => -7 ),
5976         'Bug 14395: Renewals permitted 7 days before due date, as expected'
5977     );
5978
5979     # Bug 14395
5980     # Test 'date' setting for syspref NoRenewalBeforePrecision
5981     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
5982     is(
5983         GetSoonestRenewDate( $patron, $issue ),
5984         $datedue->clone->add( days => -7 )->truncate( to => 'day' ),
5985         'Bug 14395: Renewals permitted 7 days before due date, as expected'
5986     );
5987
5988
5989     Koha::CirculationRules->set_rule(
5990         {
5991             categorycode => undef,
5992             branchcode   => undef,
5993             itemtype     => undef,
5994             rule_name    => 'norenewalbefore',
5995             rule_value   => undef,
5996         }
5997     );
5998
5999     is(
6000         GetSoonestRenewDate( $patron, $issue ),
6001         dt_from_string,
6002         'Checkouts without auto-renewal can be renewed immediately if no norenewalbefore'
6003     );
6004
6005     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
6006     $issue->auto_renew(1)->store;
6007     is(
6008         GetSoonestRenewDate( $patron, $issue, 1 ),
6009         $datedue->clone->truncate( to => 'day' ),
6010         'Checkouts with auto-renewal can be renewed earliest on due date if noautorenewalbefore'
6011     );
6012     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact' );
6013     is(
6014         GetSoonestRenewDate( $patron, $issue, 1 ),
6015         $datedue,
6016         'Checkouts with auto-renewal can be renewed earliest on due date if noautorenewalbefore'
6017     );
6018
6019     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
6020     Koha::CirculationRules->set_rule(
6021         {
6022             categorycode => undef,
6023             branchcode   => undef,
6024             itemtype     => undef,
6025             rule_name    => 'norenewalbefore',
6026             rule_value   => 1,
6027         }
6028     );
6029     $issue->date_due(dt_from_string)->store;
6030     is(
6031         GetSoonestRenewDate( $patron, $issue ),
6032         dt_from_string->subtract( days => 1 )->truncate( to => 'day' ),
6033         'Checkouts with auto-renewal can be renewed 1 day before due date if no renewalbefore = 1 and precision = "date"'
6034     );
6035
6036 };
6037
6038 subtest "CanBookBeIssued + needsconfirmation message" => sub {
6039     plan tests => 4;
6040
6041     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
6042     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
6043     my $biblio = $builder->build_object({ class => 'Koha::Biblios' });
6044     my $biblioitem = $builder->build_object({ class => 'Koha::Biblioitems', value => { biblionumber => $biblio->biblionumber }});
6045     my $item = $builder->build_object({ class => 'Koha::Items' , value => { itype => $itemtype, biblionumber => $biblio->biblionumber }});
6046
6047     my $hold = $builder->build_object({ class => 'Koha::Holds', value => {
6048         biblionumber => $item->biblionumber,
6049         branchcode => $library->branchcode,
6050         itemnumber => undef,
6051         itemtype => undef,
6052         priority => 1,
6053         found => undef,
6054         suspend => 0,
6055         item_group_id => undef
6056     }});
6057
6058     my ( $error, $needsconfirmation, $alerts, $messages );
6059
6060     ( $error, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( $patron, $item->barcode );
6061     is($needsconfirmation->{resbranchcode}, $hold->branchcode, "Branchcodes match when hold exists.");
6062
6063     $hold->priority(0)->store();
6064
6065     $hold->found("W")->store();
6066     ( $error, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( $patron, $item->barcode );
6067     is($needsconfirmation->{resbranchcode}, $hold->branchcode, "Branchcodes match when hold is waiting.");
6068
6069     $hold->found("T")->store();
6070     ( $error, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( $patron, $item->barcode );
6071     is($needsconfirmation->{resbranchcode}, $hold->branchcode, "Branchcodes match when hold is being transferred.");
6072
6073     $hold->found("P")->store();
6074     ( $error, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( $patron, $item->barcode );
6075     is($needsconfirmation->{resbranchcode}, $hold->branchcode, "Branchcodes match when hold is being processed.");
6076 };
6077
6078 subtest 'Tests for BlockReturnOfWithdrawnItems' => sub {
6079
6080     plan tests => 1;
6081
6082     t::lib::Mocks::mock_preference('BlockReturnOfWithdrawnItems', 1);
6083     t::lib::Mocks::mock_preference('RecordLocalUseOnReturn', 0);
6084     my $item = $builder->build_sample_item();
6085     $item->withdrawn(1)->itemlost(1)->store;
6086     my @return = AddReturn( $item->barcode, $item->homebranch, 0, undef );
6087     is_deeply(
6088         \@return,
6089         [ 0, { NotIssued => $item->barcode, withdrawn => 1 }, undef, {} ], "Item returned as withdrawn, no other messages");
6090 };
6091
6092 subtest 'Tests for transfer not in transit' => sub {
6093
6094     plan tests => 2;
6095
6096
6097     # These tests are to ensure a 'pending' transfer, generated by
6098     # stock rotation, will be advanced when checked in
6099
6100     my $item = $builder->build_sample_item();
6101     my $transfer = $builder->build_object({ class => 'Koha::Item::Transfers', value => {
6102         itemnumber => $item->id,
6103         reason => 'StockrotationRepatriation',
6104         datesent => undef,
6105         frombranch => $item->homebranch,
6106     }});
6107     my @return = AddReturn( $item->barcode, $item->homebranch, 0, undef );
6108     is_deeply(
6109         \@return,
6110         [ 0, { WasTransfered => $transfer->tobranch, TransferTrigger => 'StockrotationRepatriation', NotIssued => $item->barcode }, undef, {} ], "Item is reported to have been transferred");
6111
6112     $transfer->discard_changes;
6113     ok( $transfer->datesent, 'The datesent field is populated, i.e. transfer is initiated');
6114
6115 };
6116
6117 subtest 'Tests for RecordLocalUseOnReturn' => sub {
6118
6119     plan tests => 2;
6120
6121     t::lib::Mocks::mock_preference('RecordLocalUseOnReturn', 0);
6122     my $item = $builder->build_sample_item();
6123     $item->withdrawn(1)->itemlost(1)->store;
6124     my @return = AddReturn( $item->barcode, $item->homebranch, 0, undef );
6125     is_deeply(
6126         \@return,
6127         [ 0, { NotIssued => $item->barcode, withdrawn => 1  }, undef, {} ], "RecordLocalUSeOnReturn is off, no local use recorded");
6128
6129     t::lib::Mocks::mock_preference('RecordLocalUseOnReturn', 1);
6130     my @return2 = AddReturn( $item->barcode, $item->homebranch, 0, undef );
6131     is_deeply(
6132         \@return2,
6133         [ 0, { NotIssued => $item->barcode, withdrawn => 1, LocalUse => 1  }, undef, {} ], "Local use is recorded");
6134 };
6135
6136 $schema->storage->txn_rollback;
6137 C4::Context->clear_syspref_cache();
6138 $branches = Koha::Libraries->search();
6139 for my $branch ( $branches->next ) {
6140     my $key = $branch->branchcode . "_holidays";
6141     $cache->clear_from_cache($key);
6142 }