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