Bug 28787: Send a notice with the TOTP token
[koha-ffzg.git] / t / db_dependent / Circulation.t
index 41b415c..3c69d68 100755 (executable)
@@ -18,7 +18,7 @@
 use Modern::Perl;
 use utf8;
 
-use Test::More tests => 51;
+use Test::More tests => 63;
 use Test::Exception;
 use Test::MockModule;
 use Test::Deep qw( cmp_deeply );
@@ -32,29 +32,35 @@ use t::lib::Mocks;
 use t::lib::TestBuilder;
 
 use C4::Accounts;
-use C4::Calendar;
-use C4::Circulation;
+use C4::Calendar qw( new insert_single_holiday insert_week_day_holiday delete_holiday );
+use C4::Circulation qw( AddIssue AddReturn CanBookBeRenewed GetIssuingCharges AddRenewal GetSoonestRenewDate GetLatestAutoRenewDate LostItem GetUpcomingDueIssues CanBookBeIssued AddIssuingCharge MarkIssueReturned ProcessOfflinePayment transferbook updateWrongTransfer );
 use C4::Biblio;
-use C4::Items;
+use C4::Items qw( ModItemTransfer );
 use C4::Log;
-use C4::Reserves;
-use C4::Overdues qw(UpdateFine CalcFine);
-use Koha::DateUtils;
+use C4::Reserves qw( AddReserve ModReserve ModReserveCancelAll ModReserveAffect CheckReserves GetOtherReserves );
+use C4::Overdues qw( CalcFine UpdateFine get_chargeable_units );
+use C4::Members::Messaging qw( SetMessagingPreference );
+use Koha::DateUtils qw( dt_from_string output_pref );
 use Koha::Database;
 use Koha::Items;
 use Koha::Item::Transfers;
 use Koha::Checkouts;
 use Koha::Patrons;
+use Koha::Patron::Debarments qw( GetDebarments AddDebarment DelUniqueDebarment );
 use Koha::Holds;
 use Koha::CirculationRules;
 use Koha::Subscriptions;
 use Koha::Account::Lines;
 use Koha::Account::Offsets;
 use Koha::ActionLogs;
+use Koha::Notice::Messages;
+use Koha::Cache::Memory::Lite;
 
+my $builder = t::lib::TestBuilder->new;
 sub set_userenv {
     my ( $library ) = @_;
-    t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
+    my $staff = $builder->build_object({ class => "Koha::Patrons" });
+    t::lib::Mocks::mock_userenv({ patron => $staff, branchcode => $library->{branchcode} });
 }
 
 sub str {
@@ -101,7 +107,6 @@ sub test_debarment_on_checkout {
 
 my $schema = Koha::Database->schema;
 $schema->storage->txn_begin;
-my $builder = t::lib::TestBuilder->new;
 my $dbh = C4::Context->dbh;
 
 # Prevent random failures by mocking ->now
@@ -282,9 +287,140 @@ Koha::CirculationRules->set_rules(
     }
 );
 
+subtest "CanBookBeRenewed AllowRenewalIfOtherItemsAvailable multiple borrowers and items tests" => sub {
+    plan tests => 5;
+
+    #Can only reserve from home branch
+    Koha::CirculationRules->set_rule(
+        {
+            branchcode   => undef,
+            itemtype     => undef,
+            rule_name    => 'holdallowed',
+            rule_value   => 1
+        }
+    );
+    Koha::CirculationRules->set_rule(
+        {
+            branchcode   => undef,
+            categorycode   => undef,
+            itemtype     => undef,
+            rule_name    => 'onshelfholds',
+            rule_value   => 1
+        }
+    );
+
+    # Patrons from three different branches
+    my $patron_borrower = $builder->build_object({ class => 'Koha::Patrons' });
+    my $patron_hold_1   = $builder->build_object({ class => 'Koha::Patrons' });
+    my $patron_hold_2   = $builder->build_object({ class => 'Koha::Patrons' });
+    my $biblio = $builder->build_sample_biblio();
+
+    # Item at each patron branch
+    my $item_1 = $builder->build_sample_item({
+        biblionumber => $biblio->biblionumber,
+        homebranch   => $patron_borrower->branchcode
+    });
+    my $item_2 = $builder->build_sample_item({
+        biblionumber => $biblio->biblionumber,
+        homebranch   => $patron_hold_2->branchcode
+    });
+    my $item_3 = $builder->build_sample_item({
+        biblionumber => $biblio->biblionumber,
+        homebranch   => $patron_hold_1->branchcode
+    });
+
+    my $issue = AddIssue( $patron_borrower->unblessed, $item_1->barcode);
+    my $datedue = dt_from_string( $issue->date_due() );
+    is (defined $issue->date_due(), 1, "Item 1 checked out, due date: " . $issue->date_due() );
+
+    # Biblio-level holds
+    AddReserve(
+        {
+            branchcode       => $patron_hold_1->branchcode,
+            borrowernumber   => $patron_hold_1->borrowernumber,
+            biblionumber     => $biblio->biblionumber,
+            priority         => 1,
+            reservation_date => dt_from_string(),
+            expiration_date  => undef,
+            itemnumber       => undef,
+            found            => undef,
+        }
+    );
+    AddReserve(
+        {
+            branchcode       => $patron_hold_2->branchcode,
+            borrowernumber   => $patron_hold_2->borrowernumber,
+            biblionumber     => $biblio->biblionumber,
+            priority         => 2,
+            reservation_date => dt_from_string(),
+            expiration_date  => undef,
+            itemnumber       => undef,
+            found            => undef,
+        }
+    );
+    t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 0 );
+
+    my ( $renewokay, $error ) = CanBookBeRenewed($patron_borrower->borrowernumber, $item_1->itemnumber);
+    is( $renewokay, 0, 'Cannot renew, reserved');
+    is( $error, 'on_reserve', 'Cannot renew, reserved (returned error is on_reserve)');
+
+    t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 1 );
+
+    ( $renewokay, $error ) = CanBookBeRenewed($patron_borrower->borrowernumber, $item_1->itemnumber);
+    is( $renewokay, 1, 'Can renew, two items available for two holds');
+    is( $error, undef, 'Can renew, each reserve has an item');
+
+
+};
+
+subtest "GetIssuingCharges tests" => sub {
+    plan tests => 4;
+    my $branch_discount = $builder->build_object({ class => 'Koha::Libraries' });
+    my $branch_no_discount = $builder->build_object({ class => 'Koha::Libraries' });
+    Koha::CirculationRules->set_rule(
+        {
+            categorycode => undef,
+            branchcode   => $branch_discount->branchcode,
+            itemtype     => undef,
+            rule_name    => 'rentaldiscount',
+            rule_value   => 15
+        }
+    );
+    my $itype_charge = $builder->build_object({
+        class => 'Koha::ItemTypes',
+        value => {
+            rentalcharge => 10
+        }
+    });
+    my $itype_no_charge = $builder->build_object({
+        class => 'Koha::ItemTypes',
+        value => {
+            rentalcharge => 0
+        }
+    });
+    my $patron = $builder->build_object({ class => 'Koha::Patrons' });
+    my $item_1 = $builder->build_sample_item({ itype => $itype_charge->itemtype });
+    my $item_2 = $builder->build_sample_item({ itype => $itype_no_charge->itemtype });
+
+    t::lib::Mocks::mock_userenv({ branchcode => $branch_no_discount->branchcode });
+    # For now the sub always uses the env branch, this should follow CircControl instead
+    my ($charge, $itemtype) = GetIssuingCharges( $item_1->itemnumber, $patron->borrowernumber);
+    is( $charge + 0, 10.00, "Charge fetched correctly when no discount exists");
+    ($charge, $itemtype) = GetIssuingCharges( $item_2->itemnumber, $patron->borrowernumber);
+    is( $charge + 0, 0.00, "Charge fetched correctly when no discount exists and no charge");
+
+    t::lib::Mocks::mock_userenv({ branchcode => $branch_discount->branchcode });
+    # For now the sub always uses the env branch, this should follow CircControl instead
+    ($charge, $itemtype) = GetIssuingCharges( $item_1->itemnumber, $patron->borrowernumber);
+    is( $charge + 0, 8.50, "Charge fetched correctly when discount exists");
+    ($charge, $itemtype) = GetIssuingCharges( $item_2->itemnumber, $patron->borrowernumber);
+    is( $charge + 0, 0.00, "Charge fetched correctly when discount exists and no charge");
+
+};
+
 my ( $reused_itemnumber_1, $reused_itemnumber_2 );
 subtest "CanBookBeRenewed tests" => sub {
-    plan tests => 87;
+    plan tests => 104;
 
     C4::Context->set_preference('ItemsDeniedRenewal','');
     # Generate test biblio
@@ -419,6 +555,15 @@ subtest "CanBookBeRenewed tests" => sub {
             rule_value   => '1',
         }
     );
+    Koha::CirculationRules->set_rule(
+        {
+            categorycode => undef,
+            branchcode   => undef,
+            itemtype     => undef,
+            rule_name    => 'renewalsallowed',
+            rule_value   => '5',
+        }
+    );
     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 1 );
     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
     is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
@@ -522,6 +667,7 @@ subtest "CanBookBeRenewed tests" => sub {
     is( $renewokay, 1, '(Bug 8236), Can renew, user is not restricted');
     ( $renewokay, $error ) = CanBookBeRenewed($restricted_borrowernumber, $item_5->itemnumber);
     is( $renewokay, 0, '(Bug 8236), Cannot renew, user is restricted');
+    is( $error, 'restriction', "Correct error returned");
 
     # Users cannot renew an overdue item
     my $item_6 = $builder->build_sample_item(
@@ -564,6 +710,12 @@ subtest "CanBookBeRenewed tests" => sub {
         }
     );
 
+    # Make sure fine calculation isn't skipped when adding renewal
+    t::lib::Mocks::mock_preference('CalculateFinesOnReturn', 1);
+
+    my $staff = $builder->build_object({ class => "Koha::Patrons" });
+    t::lib::Mocks::mock_userenv({ patron => $staff });
+
     t::lib::Mocks::mock_preference('RenewalLog', 0);
     my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
     my %params_renewal = (
@@ -598,6 +750,27 @@ subtest "CanBookBeRenewed tests" => sub {
     isnt( $fines->next->status, 'UNRETURNED', 'Fine on renewed item is closed out properly' );
     $fines->delete();
 
+    t::lib::Mocks::mock_preference('OverduesBlockRenewing','allow');
+    ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
+    is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
+    ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
+    is( $renewokay, 1, '(Bug 8236), Can renew, this item is overdue but not pref does not block');
+
+    t::lib::Mocks::mock_preference('OverduesBlockRenewing','block');
+    ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
+    is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is not overdue but patron has overdues');
+    is( $error, 'overdue', "Correct error returned");
+    ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
+    is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue so patron has overdues');
+    is( $error, 'overdue', "Correct error returned");
+
+    t::lib::Mocks::mock_preference('OverduesBlockRenewing','blockitem');
+    ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
+    is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
+    ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
+    is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue');
+    is( $error, 'overdue', "Correct error returned");
+
 
     my $old_issue_log_size = Koha::ActionLogs->count( \%params_issue );
     my $old_renew_log_size = Koha::ActionLogs->count( \%params_renewal );
@@ -610,13 +783,6 @@ subtest "CanBookBeRenewed tests" => sub {
     $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $item_7->itemnumber } );
     $fines->delete();
 
-    t::lib::Mocks::mock_preference('OverduesBlockRenewing','blockitem');
-    ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
-    is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
-    ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
-    is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue');
-
-
     $hold = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next;
     $hold->cancel;
 
@@ -633,11 +799,13 @@ subtest "CanBookBeRenewed tests" => sub {
     );
 
     $issue = AddIssue( $renewing_borrower, $item_4->barcode, undef, undef, undef, undef, { auto_renew => 1 } );
-    ( $renewokay, $error ) =
+    my $info;
+    ( $renewokay, $error, $info ) =
       CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
     is( $error, 'auto_too_soon',
         'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = undef (returned code is auto_too_soon)' );
+    is( $info->{soonest_renew_date} , dt_from_string($issue->date_due), "Due date is returned as earliest renewal date when error is 'auto_too_soon'" );
     AddReserve(
         {
             branchcode       => $branch,
@@ -656,9 +824,10 @@ subtest "CanBookBeRenewed tests" => sub {
     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
     is( $renewokay, 0, 'Still should not be able to renew' );
     is( $error, 'on_reserve', 'returned code is on_reserve, reserve checked when not checking for cron' );
-    ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, undef, 1 );
+    ( $renewokay, $error, $info ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, undef, 1 );
     is( $renewokay, 0, 'Still should not be able to renew' );
     is( $error, 'auto_too_soon', 'returned code is auto_too_soon, reserve not checked when checking for cron' );
+    is( $info->{soonest_renew_date}, dt_from_string($issue->date_due), "Due date is returned as earliest renewal date when error is 'auto_too_soon'" );
     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1 );
     is( $renewokay, 0, 'Still should not be able to renew' );
     is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
@@ -666,6 +835,7 @@ subtest "CanBookBeRenewed tests" => sub {
     is( $renewokay, 0, 'Still should not be able to renew' );
     is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
     $dbh->do('UPDATE circulation_rules SET rule_value = 0 where rule_name = "norenewalbefore"');
+    Koha::Cache::Memory::Lite->flush();
     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1 );
     is( $renewokay, 0, 'Still should not be able to renew' );
     is( $error, 'on_reserve', 'returned code is on_reserve, auto_renew only happens if not on reserve' );
@@ -691,62 +861,51 @@ subtest "CanBookBeRenewed tests" => sub {
         }
     );
 
-    ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
+    ( $renewokay, $error, $info ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
     is( $renewokay, 0, 'Bug 7413: Cannot renew, renewal is premature');
     is( $error, 'too_soon', 'Bug 7413: Cannot renew, renewal is premature (returned code is too_soon)');
-
-    # Bug 14395
-    # Test 'exact time' setting for syspref NoRenewalBeforePrecision
-    t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact_time' );
-    is(
-        GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
-        $datedue->clone->add( days => -7 ),
-        'Bug 14395: Renewals permitted 7 days before due date, as expected'
-    );
-
-    # Bug 14395
-    # Test 'date' setting for syspref NoRenewalBeforePrecision
-    t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
-    is(
-        GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
-        $datedue->clone->add( days => -7 )->truncate( to => 'day' ),
-        'Bug 14395: Renewals permitted 7 days before due date, as expected'
-    );
+    is( $info->{soonest_renew_date}, dt_from_string($issue->date_due)->subtract( days => 7 ), "Soonest renew date returned when error is 'too_soon'");
 
     # Bug 14101
     # Test premature automatic renewal
-    ( $renewokay, $error ) =
+    ( $renewokay, $error, $info ) =
       CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
     is( $error, 'auto_too_soon',
         'Bug 14101: Cannot renew, renewal is automatic and premature (returned code is auto_too_soon)'
     );
+    is( $info->{soonest_renew_date}, dt_from_string($issue->date_due)->subtract( days => 7 ), "Soonest renew date returned when error is 'auto_too_soon'");
 
     $renewing_borrower_obj->autorenew_checkouts(0)->store;
-    ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
+    ( $renewokay, $error, $info ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
     is( $renewokay, 0, 'No renewal before is 7, patron opted out of auto_renewal still cannot renew early' );
     is( $error, 'too_soon', 'Error is too_soon, no auto' );
+    is( $info->{soonest_renew_date}, dt_from_string($issue->date_due)->subtract( days => 7 ), "Soonest renew date returned when error is 'too_soon'");
     $renewing_borrower_obj->autorenew_checkouts(1)->store;
 
     # Change policy so that loans can only be renewed exactly on due date (0 days prior to due date)
     # and test automatic renewal again
     $dbh->do(q{UPDATE circulation_rules SET rule_value = '0' WHERE rule_name = 'norenewalbefore'});
-    ( $renewokay, $error ) =
+    Koha::Cache::Memory::Lite->flush();
+    ( $renewokay, $error, $info ) =
       CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
     is( $error, 'auto_too_soon',
         'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = 0 (returned code is auto_too_soon)'
     );
+    is( $info->{soonest_renew_date}, dt_from_string($issue->date_due), "Soonest renew date returned when error is 'auto_too_soon'");
 
     $renewing_borrower_obj->autorenew_checkouts(0)->store;
-    ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
+    ( $renewokay, $error, $info ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
     is( $renewokay, 0, 'No renewal before is 0, patron opted out of auto_renewal still cannot renew early' );
     is( $error, 'too_soon', 'Error is too_soon, no auto' );
+    is( $info->{soonest_renew_date}, dt_from_string($issue->date_due), "Soonest renew date returned when error is 'auto_too_soon'");
     $renewing_borrower_obj->autorenew_checkouts(1)->store;
 
     # Change policy so that loans can be renewed 99 days prior to the due date
     # and test automatic renewal again
     $dbh->do(q{UPDATE circulation_rules SET rule_value = '99' WHERE rule_name = 'norenewalbefore'});
+    Koha::Cache::Memory::Lite->flush();
     ( $renewokay, $error ) =
       CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic' );
@@ -1090,6 +1249,7 @@ subtest "CanBookBeRenewed tests" => sub {
         $dbh->do(q{UPDATE circulation_rules SET rule_value = '10' WHERE rule_name = 'norenewalbefore'});
         $dbh->do(q{UPDATE circulation_rules SET rule_value = '15' WHERE rule_name = 'no_auto_renewal_after'});
         $dbh->do(q{UPDATE circulation_rules SET rule_value = NULL WHERE rule_name = 'no_auto_renewal_after_hard_limit'});
+        Koha::Cache::Memory::Lite->flush();
         Koha::CirculationRules->set_rules(
             {
                 categorycode => undef,
@@ -1163,6 +1323,36 @@ subtest "CanBookBeRenewed tests" => sub {
     is( $renewokay, 0, 'Cannot renew, 0 renewals allowed');
     is( $error, 'too_many', 'Cannot renew, 0 renewals allowed (returned code is too_many)');
 
+    # Too many unseen renewals
+    Koha::CirculationRules->set_rules(
+        {
+            categorycode => undef,
+            branchcode   => undef,
+            itemtype     => undef,
+            rules        => {
+                unseen_renewals_allowed => 2,
+                renewalsallowed => 10,
+            }
+        }
+    );
+    t::lib::Mocks::mock_preference('UnseenRenewals', 1);
+    $dbh->do('UPDATE issues SET unseen_renewals = 2 where borrowernumber = ? AND itemnumber = ?', undef, ($renewing_borrowernumber, $item_1->itemnumber));
+    ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
+    is( $renewokay, 0, 'Cannot renew, 0 unseen renewals allowed');
+    is( $error, 'too_unseen', 'Cannot renew, returned code is too_unseen');
+    Koha::CirculationRules->set_rules(
+        {
+            categorycode => undef,
+            branchcode   => undef,
+            itemtype     => undef,
+            rules        => {
+                norenewalbefore => undef,
+                renewalsallowed => 0,
+            }
+        }
+    );
+    t::lib::Mocks::mock_preference('UnseenRenewals', 0);
+
     # Test WhenLostForgiveFine and WhenLostChargeReplacementFee
     t::lib::Mocks::mock_preference('WhenLostForgiveFine','1');
     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
@@ -1186,7 +1376,7 @@ subtest "CanBookBeRenewed tests" => sub {
     is( $line->issue_id, $issue->id, 'Account line issue id matches' );
 
     my $offset = Koha::Account::Offsets->search({ debit_id => $line->id })->next();
-    is( $offset->type, 'OVERDUE', 'Account offset type is Fine' );
+    is( $offset->type, 'CREATE', 'Account offset type is CREATE' );
     is( $offset->amount+0, 15, 'Account offset amount is 15.00' );
 
     t::lib::Mocks::mock_preference('WhenLostForgiveFine','0');
@@ -1241,13 +1431,6 @@ subtest "CanBookBeRenewed tests" => sub {
     my $units = C4::Overdues::get_chargeable_units('days', $future, $now, $library2->{branchcode});
     ok( $units == 0, '_get_chargeable_units returns 0 for items not past due date (Bug 12596)' );
 
-    # Users cannot renew any item if there is an overdue item
-    t::lib::Mocks::mock_preference('OverduesBlockRenewing','block');
-    ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
-    is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
-    ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
-    is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
-
     my $manager = $builder->build_object({ class => "Koha::Patrons" });
     t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
@@ -1263,6 +1446,73 @@ subtest "CanBookBeRenewed tests" => sub {
             $item_3->itemcallnumber || '' ),
         "Account line description must not contain 'Lost Items ', but be title, barcode, itemcallnumber"
     );
+
+    # Recalls
+    t::lib::Mocks::mock_preference('UseRecalls', 1);
+    Koha::CirculationRules->set_rules({
+        categorycode => undef,
+        branchcode => undef,
+        itemtype => undef,
+        rules => {
+            recalls_allowed => 10,
+            renewalsallowed => 5,
+        },
+    });
+    my $recall_borrower = $builder->build_object({ class => 'Koha::Patrons' });
+    my $recall_biblio = $builder->build_object({ class => 'Koha::Biblios' });
+    my $recall_item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $recall_biblio->biblionumber } });
+    my $recall_item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $recall_biblio->biblionumber } });
+
+    AddIssue( $renewing_borrower, $recall_item1->barcode );
+
+    # item-level and this item: renewal not allowed
+    my $recall = Koha::Recall->new({
+        biblio_id => $recall_item1->biblionumber,
+        item_id => $recall_item1->itemnumber,
+        patron_id => $recall_borrower->borrowernumber,
+        pickup_library_id => $recall_borrower->branchcode,
+        item_level => 1,
+    })->store;
+    ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $recall_item1->itemnumber );
+    is( $error, 'recalled', 'Cannot renew item that has been recalled' );
+    $recall->set_cancelled;
+
+    # biblio-level requested recall: renewal not allowed
+    $recall = Koha::Recall->new({
+        biblio_id => $recall_item1->biblionumber,
+        item_id => undef,
+        patron_id => $recall_borrower->borrowernumber,
+        pickup_library_id => $recall_borrower->branchcode,
+        item_level => 0,
+    })->store;
+    ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $recall_item1->itemnumber );
+    is( $error, 'recalled', 'Cannot renew item if biblio is recalled and has no item allocated' );
+    $recall->set_cancelled;
+
+    # item-level and not this item: renewal allowed
+    $recall = Koha::Recall->new({
+        biblio_id => $recall_item2->biblionumber,
+        item_id => $recall_item2->itemnumber,
+        patron_id => $recall_borrower->borrowernumber,
+        pickup_library_id => $recall_borrower->branchcode,
+        item_level => 1,
+    })->store;
+    ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $recall_item1->itemnumber );
+    is( $renewokay, 1, 'Can renew item if item-level recall on biblio is not on this item' );
+    $recall->set_cancelled;
+
+    # biblio-level waiting recall: renewal allowed
+    $recall = Koha::Recall->new({
+        biblio_id => $recall_item1->biblionumber,
+        item_id => undef,
+        patron_id => $recall_borrower->borrowernumber,
+        pickup_library_id => $recall_borrower->branchcode,
+        item_level => 0,
+    })->store;
+    $recall->set_waiting({ item => $recall_item1 });
+    ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $recall_item1->itemnumber );
+    is( $renewokay, 1, 'Can renew item if biblio-level recall has already been allocated an item' );
+    $recall->set_cancelled;
 };
 
 subtest "GetUpcomingDueIssues" => sub {
@@ -1390,16 +1640,30 @@ subtest "Bug 13841 - Do not create new 0 amount fines" => sub {
 };
 
 subtest "AllowRenewalIfOtherItemsAvailable tests" => sub {
-    $dbh->do('DELETE FROM issues');
-    $dbh->do('DELETE FROM items');
-    $dbh->do('DELETE FROM circulation_rules');
+    plan tests => 13;
+    my $biblio = $builder->build_sample_biblio();
+    my $item_1 = $builder->build_sample_item(
+        {
+            biblionumber     => $biblio->biblionumber,
+            library          => $library2->{branchcode},
+        }
+    );
+    my $item_2= $builder->build_sample_item(
+        {
+            biblionumber     => $biblio->biblionumber,
+            library          => $library2->{branchcode},
+            itype            => $item_1->effective_itemtype,
+        }
+    );
+
     Koha::CirculationRules->set_rules(
         {
             categorycode => undef,
-            itemtype     => undef,
+            itemtype     => $item_1->effective_itemtype,
             branchcode   => undef,
             rules        => {
                 reservesallowed => 25,
+                holds_per_record => 25,
                 issuelength     => 14,
                 lengthunit      => 'days',
                 renewalsallowed => 1,
@@ -1412,23 +1676,7 @@ subtest "AllowRenewalIfOtherItemsAvailable tests" => sub {
             }
         }
     );
-    my $biblio = $builder->build_sample_biblio();
-
-    my $item_1 = $builder->build_sample_item(
-        {
-            biblionumber     => $biblio->biblionumber,
-            library          => $library2->{branchcode},
-            itype            => $itemtype,
-        }
-    );
 
-    my $item_2= $builder->build_sample_item(
-        {
-            biblionumber     => $biblio->biblionumber,
-            library          => $library2->{branchcode},
-            itype            => $itemtype,
-        }
-    );
 
     my $borrowernumber1 = Koha::Patron->new({
         firstname    => 'Kyle',
@@ -1442,6 +1690,22 @@ subtest "AllowRenewalIfOtherItemsAvailable tests" => sub {
         categorycode => $patron_category->{categorycode},
         branchcode   => $library2->{branchcode},
     })->store->borrowernumber;
+    my $patron_category_2 = $builder->build(
+        {
+            source => 'Category',
+            value  => {
+                category_type                 => 'P',
+                enrolmentfee                  => 0,
+                BlockExpiredPatronOpacActions => -1, # Pick the pref value
+            }
+        }
+    );
+    my $borrowernumber3 = Koha::Patron->new({
+        firstname    => 'Carnegie',
+        surname      => 'Hall',
+        categorycode => $patron_category_2->{categorycode},
+        branchcode   => $library2->{branchcode},
+    })->store->borrowernumber;
 
     my $borrower1 = Koha::Patrons->find( $borrowernumber1 )->unblessed;
     my $borrower2 = Koha::Patrons->find( $borrowernumber2 )->unblessed;
@@ -1463,7 +1727,7 @@ subtest "AllowRenewalIfOtherItemsAvailable tests" => sub {
     Koha::CirculationRules->set_rules(
         {
             categorycode => undef,
-            itemtype     => undef,
+            itemtype     => $item_1->effective_itemtype,
             branchcode   => undef,
             rules        => {
                 onshelfholds => 0,
@@ -1474,53 +1738,121 @@ subtest "AllowRenewalIfOtherItemsAvailable tests" => sub {
     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfholds are disabled' );
 
+    t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
+    ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
+    is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled and onshelfholds is disabled' );
+
     Koha::CirculationRules->set_rules(
         {
             categorycode => undef,
-            itemtype     => undef,
+            itemtype     => $item_1->effective_itemtype,
             branchcode   => undef,
             rules        => {
-                onshelfholds => 0,
+                onshelfholds => 1,
             }
         }
     );
+    t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
+    ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
+    is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is disabled and onshelfhold is enabled' );
+
     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
-    is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled and onshelfholds is disabled' );
+    is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled' );
+
+    AddReserve(
+        {
+            branchcode     => $library2->{branchcode},
+            borrowernumber => $borrowernumber3,
+            biblionumber   => $biblio->biblionumber,
+            priority       => 1,
+        }
+    );
+
+    ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
+    is( $renewokay, 0, 'Verify the borrower cannot renew with 2 holds on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled and one other item on record' );
+
+    my $item_3= $builder->build_sample_item(
+        {
+            biblionumber     => $biblio->biblionumber,
+            library          => $library2->{branchcode},
+            itype            => $item_1->effective_itemtype,
+        }
+    );
+
+    ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
+    is( $renewokay, 1, 'Verify the borrower cannot renew with 2 holds on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled and two other items on record' );
 
     Koha::CirculationRules->set_rules(
         {
-            categorycode => undef,
-            itemtype     => undef,
+            categorycode => $patron_category_2->{categorycode},
+            itemtype     => $item_1->effective_itemtype,
             branchcode   => undef,
             rules        => {
-                onshelfholds => 1,
+                reservesallowed => 0,
             }
         }
     );
-    t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
+
     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
-    is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is disabled and onshelfhold is enabled' );
+    is( $renewokay, 0, 'Verify the borrower cannot renew with 2 holds on the record, but only one of those holds can be filled when AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled and two other items on record' );
 
     Koha::CirculationRules->set_rules(
         {
-            categorycode => undef,
-            itemtype     => undef,
+            categorycode => $patron_category_2->{categorycode},
+            itemtype     => $item_1->effective_itemtype,
             branchcode   => undef,
             rules        => {
-                onshelfholds => 1,
+                reservesallowed => 25,
             }
         }
     );
-    t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
-    ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
-    is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled' );
 
     # Setting item not checked out to be not for loan but holdable
     $item_2->notforloan(-1)->store;
 
     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
     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' );
+
+    my $mock_circ = Test::MockModule->new("C4::Circulation");
+    $mock_circ->mock( CanItemBeReserved => sub {
+        warn "Checked";
+        return { status => 'no' }
+    } );
+
+    $item_2->notforloan(0)->store;
+    $item_3->delete();
+    # Two items total, one item available, one issued, two holds on record
+
+    warnings_are{
+       ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
+    } [], "CanItemBeReserved not called when there are more possible holds than available items";
+    is( $renewokay, 0, 'Borrower cannot renew when there are more holds than available items' );
+
+    $item_3 = $builder->build_sample_item(
+        {
+            biblionumber     => $biblio->biblionumber,
+            library          => $library2->{branchcode},
+            itype            => $item_1->effective_itemtype,
+        }
+    );
+
+    Koha::CirculationRules->set_rules(
+        {
+            categorycode => undef,
+            itemtype     => $item_1->effective_itemtype,
+            branchcode   => undef,
+            rules        => {
+                reservesallowed => 0,
+            }
+        }
+    );
+
+    warnings_are{
+       ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
+    } ["Checked","Checked"], "CanItemBeReserved only called once per available item if it returns a negative result for all items for a borrower";
+    is( $renewokay, 0, 'Borrower cannot renew when there are more holds than available items' );
+
 };
 
 {
@@ -1605,6 +1937,25 @@ subtest 'CanBookBeIssued & AllowReturnToBranch' => sub {
             holdingbranch => $holdingbranch->{branchcode},
         }
     );
+    Koha::CirculationRules->set_rules(
+        {
+            categorycode => undef,
+            itemtype     => $item->effective_itemtype,
+            branchcode   => undef,
+            rules        => {
+                reservesallowed => 25,
+                issuelength     => 14,
+                lengthunit      => 'days',
+                renewalsallowed => 1,
+                renewalperiod   => 7,
+                norenewalbefore => undef,
+                auto_renew      => 0,
+                fine            => .10,
+                chargeperiod    => 1,
+                maxissueqty     => 20
+            }
+        }
+    );
 
     set_userenv($holdingbranch);
 
@@ -1743,6 +2094,104 @@ subtest 'AddIssue & AllowReturnToBranch' => sub {
     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
 };
 
+subtest 'AddIssue | recalls' => sub {
+    plan tests => 3;
+
+    t::lib::Mocks::mock_preference("UseRecalls", 1);
+    t::lib::Mocks::mock_preference("item-level_itypes", 1);
+    my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
+    my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
+    my $item = $builder->build_sample_item;
+    Koha::CirculationRules->set_rules({
+        branchcode => undef,
+        itemtype => undef,
+        categorycode => undef,
+        rules => {
+            recalls_allowed => 10,
+        },
+    });
+
+    # checking out item that they have recalled
+    my $recall1 = Koha::Recall->new(
+        {   patron_id         => $patron1->borrowernumber,
+            biblio_id         => $item->biblionumber,
+            item_id           => $item->itemnumber,
+            item_level        => 1,
+            pickup_library_id => $patron1->branchcode,
+        }
+    )->store;
+    AddIssue( $patron1->unblessed, $item->barcode, undef, undef, undef, undef, { recall_id => $recall1->id } );
+    $recall1 = Koha::Recalls->find( $recall1->id );
+    is( $recall1->fulfilled, 1, 'Recall was fulfilled when patron checked out item' );
+    AddReturn( $item->barcode, $item->homebranch );
+
+    # this item is has a recall request. cancel recall
+    my $recall2 = Koha::Recall->new(
+        {   patron_id         => $patron2->borrowernumber,
+            biblio_id         => $item->biblionumber,
+            item_id           => $item->itemnumber,
+            item_level        => 1,
+            pickup_library_id => $patron2->branchcode,
+        }
+    )->store;
+    AddIssue( $patron1->unblessed, $item->barcode, undef, undef, undef, undef, { recall_id => $recall2->id, cancel_recall => 'cancel' } );
+    $recall2 = Koha::Recalls->find( $recall2->id );
+    is( $recall2->cancelled, 1, 'Recall was cancelled when patron checked out item' );
+    AddReturn( $item->barcode, $item->homebranch );
+
+    # this item is waiting to fulfill a recall. revert recall
+    my $recall3 = Koha::Recall->new(
+        {   patron_id         => $patron2->borrowernumber,
+            biblio_id         => $item->biblionumber,
+            item_id           => $item->itemnumber,
+            item_level        => 1,
+            pickup_library_id => $patron2->branchcode,
+        }
+    )->store;
+    $recall3->set_waiting;
+    AddIssue( $patron1->unblessed, $item->barcode, undef, undef, undef, undef, { recall_id => $recall3->id, cancel_recall => 'revert' } );
+    $recall3 = Koha::Recalls->find( $recall3->id );
+    is( $recall3->requested, 1, 'Recall was reverted from waiting when patron checked out item' );
+    AddReturn( $item->barcode, $item->homebranch );
+};
+
+subtest 'AddIssue & illrequests.due_date' => sub {
+    plan tests => 2;
+
+    t::lib::Mocks::mock_preference( 'ILLModule', 1 );
+    my $library = $builder->build( { source => 'Branch' } );
+    my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
+    my $item = $builder->build_sample_item();
+
+    set_userenv($library);
+
+    my $custom_date_due = '9999-12-18 12:34:56';
+    my $expected_date_due = '9999-12-18 23:59:00';
+    my $illrequest = Koha::Illrequest->new({
+        borrowernumber => $patron->borrowernumber,
+        biblio_id => $item->biblionumber,
+        branchcode => $library->{'branchcode'},
+        due_date => $custom_date_due,
+    })->store;
+
+    my $issue = AddIssue( $patron->unblessed, $item->barcode );
+    is( $issue->date_due, $expected_date_due, 'Custom illrequest date due has been set for this issue');
+
+    $patron = $builder->build_object( { class => 'Koha::Patrons' } );
+    $item = $builder->build_sample_item();
+    $custom_date_due = '9999-12-19';
+    $expected_date_due = '9999-12-19 23:59:00';
+    $illrequest = Koha::Illrequest->new({
+        borrowernumber => $patron->borrowernumber,
+        biblio_id => $item->biblionumber,
+        branchcode => $library->{'branchcode'},
+        due_date => $custom_date_due,
+    })->store;
+
+    $issue = AddIssue( $patron->unblessed, $item->barcode );
+    is( $issue->date_due, $expected_date_due, 'Custom illrequest date due has been set for this issue');
+};
+
 subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
     plan tests => 8;
 
@@ -1758,6 +2207,26 @@ subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
             library => $library->{branchcode},
         }
     );
+    Koha::CirculationRules->set_rules(
+        {
+            categorycode => undef,
+            itemtype     => undef,
+            branchcode   => $library->{branchcode},
+            rules        => {
+                reservesallowed => 25,
+                issuelength     => 14,
+                lengthunit      => 'days',
+                renewalsallowed => 1,
+                renewalperiod   => 7,
+                norenewalbefore => undef,
+                auto_renew      => 0,
+                fine            => .10,
+                chargeperiod    => 1,
+                maxissueqty     => 20
+            }
+        }
+    );
+
 
     my ( $error, $question, $alerts );
 
@@ -1955,6 +2424,26 @@ subtest 'CanBookBeIssued + AllowMultipleIssuesOnABiblio' => sub {
         }
     );
 
+    Koha::CirculationRules->set_rules(
+        {
+            categorycode => undef,
+            itemtype     => undef,
+            branchcode   => $library->{branchcode},
+            rules        => {
+                reservesallowed => 25,
+                issuelength     => 14,
+                lengthunit      => 'days',
+                renewalsallowed => 1,
+                renewalperiod   => 7,
+                norenewalbefore => undef,
+                auto_renew      => 0,
+                fine            => .10,
+                chargeperiod    => 1,
+                maxissueqty     => 20
+            }
+        }
+    );
+
     my ( $error, $question, $alerts );
     my $issue = AddIssue( $patron->unblessed, $item_1->barcode, dt_from_string->add( days => 1 ) );
 
@@ -2548,8 +3037,7 @@ subtest 'AddReturn | is_overdue' => sub {
                 interface => 'test',
             }
         );
-        $credit->apply(
-            { debits => [ $debit ], offset_type => 'Payment' } );
+        $credit->apply( { debits => [$debit] } );
 
         is( int( $patron->account->balance() ),
             0, "Overdue fine should be paid off" );
@@ -2675,7 +3163,7 @@ subtest 'AddReturn | is_overdue' => sub {
                 interface => 'test',
             }
         );
-        $credit->apply( { debits => [$debit], offset_type => 'Payment' } );
+        $credit->apply( { debits => [$debit] } );
 
         is( $patron->account->balance(), .05, 'Overdue fine reduced to $0.05' );
 
@@ -3012,8 +3500,7 @@ subtest 'AddReturn | is_overdue' => sub {
                     item_id    => $item->itemnumber
                 }
             );
-            $overdue_forgive->apply(
-                { debits => [$overdue_fee], offset_type => 'Forgiven' } );
+            $overdue_forgive->apply( { debits => [$overdue_fee] } );
             $overdue_fee->discard_changes;
             is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
 
@@ -3110,8 +3597,7 @@ subtest 'AddReturn | is_overdue' => sub {
                     item_id    => $item->itemnumber
                 }
             );
-            $overdue_forgive->apply(
-                { debits => [$overdue_fee], offset_type => 'Forgiven' } );
+            $overdue_forgive->apply( { debits => [$overdue_fee] } );
             $overdue_fee->discard_changes;
             is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
 
@@ -3208,7 +3694,7 @@ subtest '_FixOverduesOnReturn' => sub {
     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
 
     $accountline->_result()->discard_changes();
-    my $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
+    my $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'APPLY' })->next();
 
     is( $accountline->amountoutstanding + 0, 0, 'Fine amountoutstanding has been reduced to 0' );
     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
@@ -3233,7 +3719,7 @@ subtest '_FixOverduesOnReturn' => sub {
     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
 
     $accountline->_result()->discard_changes();
-    $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
+    $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'CREATE' })->next();
     is( $offset, undef, "No offset created when trying to forgive fine with no outstanding balance" );
     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
     is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
@@ -3272,7 +3758,7 @@ subtest 'Set waiting flag' => sub {
     is( $hold->found, 'T', 'Hold is in transit' );
 
     my ( $status ) = CheckReserves($item->itemnumber);
-    is( $status, 'Reserved', 'Hold is not waiting yet');
+    is( $status, 'Transferred', 'Hold is not waiting yet');
 
     set_userenv( $library_2 );
     $do_transfer = 0;
@@ -3298,55 +3784,73 @@ subtest 'Set waiting flag' => sub {
 
 subtest 'Cancel transfers on lost items' => sub {
     plan tests => 6;
-    my $library_1 = $builder->build( { source => 'Branch' } );
-    my $patron_1 = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
-    my $library_2 = $builder->build( { source => 'Branch' } );
-    my $patron_2  = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
-    my $biblio = $builder->build_sample_biblio({branchcode => $library->{branchcode}});
-    my $item   = $builder->build_sample_item({
-        biblionumber  => $biblio->biblionumber,
-        library    => $library_1->{branchcode},
-    });
 
-    set_userenv( $library_2 );
-    my $reserve_id = AddReserve(
+    my $library_to = $builder->build_object( { class => 'Koha::Libraries' } );
+    my $item   = $builder->build_sample_item();
+    my $holdingbranch = $item->holdingbranch;
+    # Historic transfer (datearrived is defined)
+    my $old_transfer = $builder->build_object(
         {
-            branchcode     => $library_2->{branchcode},
-            borrowernumber => $patron_2->{borrowernumber},
-            biblionumber   => $item->biblionumber,
-            priority       => 1,
-            itemnumber     => $item->itemnumber,
+            class => 'Koha::Item::Transfers',
+            value => {
+                itemnumber    => $item->itemnumber,
+                frombranch    => $holdingbranch,
+                tobranch      => $library_to->branchcode,
+                reason        => 'Manual',
+                datesent      => \'NOW()',
+                datearrived   => \'NOW()',
+                datecancelled => undef,
+                daterequested => \'NOW()'
+            }
+        }
+    );
+    # Queued transfer (datesent is undefined)
+    my $transfer_1 = $builder->build_object(
+        {
+            class => 'Koha::Item::Transfers',
+            value => {
+                itemnumber    => $item->itemnumber,
+                frombranch    => $holdingbranch,
+                tobranch      => $library_to->branchcode,
+                reason        => 'Manual',
+                datesent      => undef,
+                datearrived   => undef,
+                datecancelled => undef,
+                daterequested => \'NOW()'
+            }
+        }
+    );
+    # In transit transfer (datesent is defined, datearrived and datecancelled are both undefined)
+    my $transfer_2 = $builder->build_object(
+        {
+            class => 'Koha::Item::Transfers',
+            value => {
+                itemnumber    => $item->itemnumber,
+                frombranch    => $holdingbranch,
+                tobranch      => $library_to->branchcode,
+                reason        => 'Manual',
+                datesent      => \'NOW()',
+                datearrived   => undef,
+                datecancelled => undef,
+                daterequested => \'NOW()'
+            }
         }
     );
 
-    #Return book and add transfer
-    set_userenv( $library_1 );
-    my $do_transfer = 1;
-    my ( $res, $rr ) = AddReturn( $item->barcode, $library_1->{branchcode} );
-    ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
-    C4::Circulation::transferbook({
-        from_branch => $library_1->{branchcode},
-        to_branch => $library_2->{branchcode},
-        barcode   => $item->barcode,
-    });
-    my $hold = Koha::Holds->find( $reserve_id );
-    is( $hold->found, 'T', 'Hold is in transit' );
-
-    #Check transfer exists and the items holding branch is the transfer destination branch before marking it as lost
-    my ($datesent,$frombranch,$tobranch) = GetTransfers($item->itemnumber);
-    is( $frombranch, $library_1->{branchcode}, 'The transfer is generated from the correct library');
-    is( $tobranch, $library_2->{branchcode}, 'The transfer is generated to the correct library');
-    my $itemcheck = Koha::Items->find($item->itemnumber);
-    is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Items holding branch is the transfers origination branch before it is marked as lost' );
-
-    #Simulate item being marked as lost and confirm the transfer is deleted and the items holding branch is the transfers source branch
+    # Simulate item being marked as lost
     $item->itemlost(1)->store;
     LostItem( $item->itemnumber, 'test', 1 );
-    ($datesent,$frombranch,$tobranch) = GetTransfers($item->itemnumber);
-    is( $tobranch, undef, 'The transfer on the lost item has been deleted as the LostItemCancelOutstandingTransfer is enabled');
-    $itemcheck = Koha::Items->find($item->itemnumber);
-    is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Lost item with cancelled hold has holding branch equallying the transfers source branch' );
 
+    $transfer_1->discard_changes;
+    isnt($transfer_1->datecancelled, undef, "Queud transfer was cancelled upon item lost");
+    is($transfer_1->cancellation_reason, 'ItemLost', "Cancellation reason was set to 'ItemLost'");
+    $transfer_2->discard_changes;
+    isnt($transfer_2->datecancelled, undef, "Active transfer was cancelled upon item lost");
+    is($transfer_2->cancellation_reason, 'ItemLost', "Cancellation reason was set to 'ItemLost'");
+    $old_transfer->discard_changes;
+    is($old_transfer->datecancelled, undef, "Old transfers are unaffected");
+    $item->discard_changes;
+    is($item->holdingbranch, $holdingbranch, "Items holding branch remains unchanged");
 };
 
 subtest 'CanBookBeIssued | is_overdue' => sub {
@@ -3373,9 +3877,9 @@ subtest 'CanBookBeIssued | is_overdue' => sub {
         }
     );
 
-    my $now   = dt_from_string;
-    my $five_days_go = output_pref({ dt => $now->clone->add( days => 5 ), dateonly => 1});
-    my $ten_days_go  = output_pref({ dt => $now->clone->add( days => 10), dateonly => 1 });
+    my $now   = dt_from_string()->truncate( to => 'day' );
+    my $five_days_go = $now->clone->add( days => 5 );
+    my $ten_days_go  = $now->clone->add( days => 10);
     my $library = $builder->build( { source => 'Branch' } );
     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
 
@@ -3387,7 +3891,7 @@ subtest 'CanBookBeIssued | is_overdue' => sub {
 
     my $issue = AddIssue( $patron->unblessed, $item->barcode, $five_days_go ); # date due was 10d ago
     my $actualissue = Koha::Checkouts->find( { itemnumber => $item->itemnumber } );
-    is( output_pref({ str => $actualissue->date_due, dateonly => 1}), $five_days_go, "First issue works");
+    is( output_pref({ str => $actualissue->date_due, dateonly => 1}), output_pref({ str => $five_days_go, dateonly => 1}), "First issue works");
     my ($issuingimpossible, $needsconfirmation) = CanBookBeIssued($patron,$item->barcode,$ten_days_go, undef, undef, undef);
     is( $needsconfirmation->{RENEW_ISSUE}, 1, "This is a renewal");
     is( $needsconfirmation->{TOO_MANY}, undef, "Not too many, is a renewal");
@@ -3440,26 +3944,34 @@ subtest 'ItemsDeniedRenewal preference' => sub {
         }
     });
     my $future = dt_from_string->add( days => 1 );
-    my $deny_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
-        returndate => undef,
-        renewals => 0,
-        auto_renew => 0,
-        borrowernumber => $idr_borrower->borrowernumber,
-        itemnumber => $deny_book->itemnumber,
-        onsite_checkout => 0,
-        date_due => $future,
+    my $deny_issue = $builder->build_object(
+        {
+            class => 'Koha::Checkouts',
+            value => {
+                returndate      => undef,
+                renewals_count  => 0,
+                auto_renew      => 0,
+                borrowernumber  => $idr_borrower->borrowernumber,
+                itemnumber      => $deny_book->itemnumber,
+                onsite_checkout => 0,
+                date_due        => $future,
+            }
         }
-    });
-    my $allow_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
-        returndate => undef,
-        renewals => 0,
-        auto_renew => 0,
-        borrowernumber => $idr_borrower->borrowernumber,
-        itemnumber => $allow_book->itemnumber,
-        onsite_checkout => 0,
-        date_due => $future,
+    );
+    my $allow_issue = $builder->build_object(
+        {
+            class => 'Koha::Checkouts',
+            value => {
+                returndate      => undef,
+                renewals_count  => 0,
+                auto_renew      => 0,
+                borrowernumber  => $idr_borrower->borrowernumber,
+                itemnumber      => $allow_book->itemnumber,
+                onsite_checkout => 0,
+                date_due        => $future,
+            }
         }
-    });
+    );
 
     my $idr_rules;
 
@@ -3646,6 +4158,70 @@ subtest 'CanBookBeIssued | notforloan' => sub {
     # TODO test with AllowNotForLoanOverride = 1
 };
 
+subtest 'CanBookBeIssued | recalls' => sub {
+    plan tests => 3;
+
+    t::lib::Mocks::mock_preference("UseRecalls", 1);
+    t::lib::Mocks::mock_preference("item-level_itypes", 1);
+    my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
+    my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
+    my $item = $builder->build_sample_item;
+    Koha::CirculationRules->set_rules({
+        branchcode => undef,
+        itemtype => undef,
+        categorycode => undef,
+        rules => {
+            recalls_allowed => 10,
+        },
+    });
+
+    # item-level recall
+    my $recall = Koha::Recall->new(
+        {   patron_id         => $patron1->borrowernumber,
+            biblio_id         => $item->biblionumber,
+            item_id           => $item->itemnumber,
+            item_level        => 1,
+            pickup_library_id => $patron1->branchcode,
+        }
+    )->store;
+
+    my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron2, $item->barcode, undef, undef, undef, undef );
+    is( $needsconfirmation->{RECALLED}->id, $recall->id, "Another patron has placed an item-level recall on this item" );
+
+    $recall->set_cancelled;
+
+    # biblio-level recall
+    $recall = Koha::Recall->new(
+        {   patron_id         => $patron1->borrowernumber,
+            biblio_id         => $item->biblionumber,
+            item_id           => undef,
+            item_level        => 0,
+            pickup_library_id => $patron1->branchcode,
+        }
+    )->store;
+
+    ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron2, $item->barcode, undef, undef, undef, undef );
+    is( $needsconfirmation->{RECALLED}->id, $recall->id, "Another patron has placed a biblio-level recall and this item is eligible to fill it" );
+
+    $recall->set_cancelled;
+
+    # biblio-level recall
+    $recall = Koha::Recall->new(
+        {   patron_id         => $patron1->borrowernumber,
+            biblio_id         => $item->biblionumber,
+            item_id           => undef,
+            item_level        => 0,
+            pickup_library_id => $patron1->branchcode,
+        }
+    )->store;
+    $recall->set_waiting( { item => $item, expirationdate => dt_from_string() } );
+
+    my ( undef, undef, undef, $messages ) = CanBookBeIssued( $patron1, $item->barcode, undef, undef, undef, undef );
+    is( $messages->{RECALLED}, $recall->id, "This book can be issued by this patron and they have placed a recall" );
+
+    $recall->set_cancelled;
+};
+
 subtest 'AddReturn should clear items.onloan for unissued items' => sub {
     plan tests => 1;
 
@@ -3661,10 +4237,89 @@ subtest 'AddReturn should clear items.onloan for unissued items' => sub {
     is( $item->onloan, undef, 'AddReturn did clear items.onloan' );
 };
 
+subtest 'AddReturn | recalls' => sub {
+    plan tests => 3;
+
+    t::lib::Mocks::mock_preference("UseRecalls", 1);
+    t::lib::Mocks::mock_preference("item-level_itypes", 1);
+    my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
+    my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
+    my $item1 = $builder->build_sample_item;
+    Koha::CirculationRules->set_rules({
+        branchcode => undef,
+        itemtype => undef,
+        categorycode => undef,
+        rules => {
+            recalls_allowed => 10,
+        },
+    });
+
+    # this item can fill a recall with pickup at this branch
+    AddIssue( $patron1->unblessed, $item1->barcode );
+    my $recall1 = Koha::Recall->new(
+        {   patron_id         => $patron2->borrowernumber,
+            biblio_id         => $item1->biblionumber,
+            item_id           => $item1->itemnumber,
+            item_level        => 1,
+            pickup_library_id => $item1->homebranch,
+        }
+    )->store;
+    my ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $item1->homebranch );
+    is( $messages->{RecallFound}->id, $recall1->id, "Recall found" );
+    $recall1->set_cancelled;
+
+    # this item can fill a recall but needs transfer
+    AddIssue( $patron1->unblessed, $item1->barcode );
+    $recall1 = Koha::Recall->new(
+        {   patron_id         => $patron2->borrowernumber,
+            biblio_id         => $item1->biblionumber,
+            item_id           => $item1->itemnumber,
+            item_level        => 1,
+            pickup_library_id => $patron2->branchcode,
+        }
+    )->store;
+    ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $item1->homebranch );
+    is( $messages->{RecallNeedsTransfer}, $item1->homebranch, "Recall requiring transfer found" );
+    $recall1->set_cancelled;
+
+    # this item is already in transit, do not ask to transfer
+    AddIssue( $patron1->unblessed, $item1->barcode );
+    $recall1 = Koha::Recall->new(
+        {   patron_id         => $patron2->borrowernumber,
+            biblio_id         => $item1->biblionumber,
+            item_id           => $item1->itemnumber,
+            item_level        => 1,
+            pickup_library_id => $patron2->branchcode,
+        }
+    )->store;
+    $recall1->start_transfer;
+    ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $patron2->branchcode );
+    is( $messages->{TransferredRecall}->id, $recall1->id, "In transit recall found" );
+    $recall1->set_cancelled;
+};
+
+subtest 'AddReturn | bundles' => sub {
+    plan tests => 1;
+
+    my $schema = Koha::Database->schema;
+    $schema->storage->txn_begin;
+
+    my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
+    my $host_item1 = $builder->build_sample_item;
+    my $bundle_item1 = $builder->build_sample_item;
+    $schema->resultset('ItemBundle')
+      ->create(
+        { host => $host_item1->itemnumber, item => $bundle_item1->itemnumber } );
+
+    my ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $bundle_item1->barcode, $bundle_item1->homebranch );
+    is($messages->{InBundle}->id, $host_item1->id, 'AddReturn returns InBundle host item when item is part of a bundle');
+
+    $schema->storage->txn_rollback;
+};
 
 subtest 'AddRenewal and AddIssuingCharge tests' => sub {
 
-    plan tests => 12;
+    plan tests => 13;
 
 
     t::lib::Mocks::mock_preference('item-level_itypes', 1);
@@ -3707,6 +4362,11 @@ subtest 'AddRenewal and AddIssuingCharge tests' => sub {
 
     # Check the item out
     AddIssue( $patron->unblessed, $item->barcode );
+
+    throws_ok {
+        AddRenewal( $patron->borrowernumber, $item->itemnumber, $library->id, undef, {break=>"the_renewal"} );
+    } 'Koha::Exceptions::Checkout::FailedRenewal', 'Exception is thrown when renewal update to issues fails';
+
     t::lib::Mocks::mock_preference( 'RenewalLog', 0 );
     my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
     my %params_renewal = (
@@ -3769,6 +4429,34 @@ subtest 'AddRenewal and AddIssuingCharge tests' => sub {
 
 };
 
+subtest 'AddRenewal() adds to renewals' => sub {
+    plan tests => 4;
+
+    my $library  = $builder->build_object({ class => 'Koha::Libraries' });
+    my $patron   = $builder->build_object({
+        class => 'Koha::Patrons',
+        value => { branchcode => $library->id }
+    });
+
+    my $item = $builder->build_sample_item();
+
+    set_userenv( $library->unblessed );
+
+    # Check the item out
+    my $issue = AddIssue( $patron->unblessed, $item->barcode );
+    is(ref($issue), 'Koha::Checkout', 'Issue added');
+
+    # Renew item
+    my $duedate = AddRenewal( $patron->id, $item->id, $library->id );
+
+    ok( $duedate, "Renewal added" );
+
+    my $renewals = Koha::Checkouts::Renewals->search({ checkout_id => $issue->issue_id });
+    is($renewals->count, 1, 'One renewal added');
+    my $THE_renewal = $renewals->next;
+    is( $THE_renewal->renewer_id, C4::Context->userenv->{'number'}, 'Renewer recorded from context' );
+};
+
 subtest 'ProcessOfflinePayment() tests' => sub {
 
     plan tests => 4;
@@ -4585,7 +5273,7 @@ subtest 'Checkout should correctly terminate a transfer' => sub {
 
     my $do_transfer = 1;
     ModItemTransfer( $item->itemnumber, $library_1->branchcode,
-        $library_2->branchcode );
+        $library_2->branchcode, 'Manual' );
     ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
     GetOtherReserves( $item->itemnumber )
       ;    # To put the Reason, it's what does returns.pl...
@@ -4646,6 +5334,202 @@ subtest 'AddIssue records staff who checked out item if appropriate' => sub  {
     is( $issue->issuer, $issuer->{borrowernumber}, "Staff who checked out the item recorded when RecordStaffUserOnCheckout turned on" );
 };
 
+subtest "Item's onloan value should be set if checked out item is checked out to a different patron" => sub {
+    plan tests => 2;
+
+    my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
+    my $patron_1 = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { branchcode => $library_1->branchcode }
+        }
+    );
+    my $patron_2 = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { branchcode => $library_1->branchcode }
+        }
+    );
+
+    my $item = $builder->build_sample_item(
+        {
+            library => $library_1->branchcode,
+        }
+    );
+
+    AddIssue( $patron_1->unblessed, $item->barcode );
+    ok( $item->get_from_storage->onloan, "Item's onloan column is set after initial checkout" );
+    AddIssue( $patron_2->unblessed, $item->barcode );
+    ok( $item->get_from_storage->onloan, "Item's onloan column is set after second checkout" );
+};
+
+subtest "updateWrongTransfer tests" => sub {
+    plan tests => 5;
+
+    my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
+    my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
+    my $library3 = $builder->build_object( { class => 'Koha::Libraries' } );
+    my $item     = $builder->build_sample_item(
+        {
+            homebranch    => $library1->branchcode,
+            holdingbranch => $library2->branchcode,
+            datelastseen  => undef
+        }
+    );
+
+    my $transfer = $builder->build_object(
+        {
+            class => 'Koha::Item::Transfers',
+            value => {
+                itemnumber    => $item->itemnumber,
+                frombranch    => $library2->branchcode,
+                tobranch      => $library1->branchcode,
+                daterequested => dt_from_string,
+                datesent      => dt_from_string,
+                datecancelled => undef,
+                datearrived   => undef,
+                reason        => 'Manual'
+            }
+        }
+    );
+    is( ref($transfer), 'Koha::Item::Transfer', 'Mock transfer added' );
+
+    my $new_transfer = C4::Circulation::updateWrongTransfer($item->itemnumber, $library1->branchcode);
+    is(ref($new_transfer), 'Koha::Item::Transfer', "updateWrongTransfer returns a 'Koha::Item::Transfer' object");
+    ok( !$new_transfer->in_transit, "New transfer is NOT created as in transit (or cancelled)");
+
+    my $original_transfer = $transfer->get_from_storage;
+    ok( defined($original_transfer->datecancelled), "Original transfer was cancelled");
+    is( $original_transfer->cancellation_reason, 'WrongTransfer', "Original transfer cancellation reason is 'WrongTransfer'");
+};
+
+subtest "SendCirculationAlert" => sub {
+    plan tests => 3;
+
+    # When you would unsuspectingly call this unit test (with perl, not prove), you will be bitten by LOCK.
+    # LOCK will commit changes and ruin your data
+    # In order to prevent that, we will add KOHA_TESTING to $ENV; see further Circulation.pm
+    $ENV{KOHA_TESTING} = 1;
+
+    # Setup branch, borrowr, and notice
+    my $library = $builder->build_object({ class => 'Koha::Libraries' });
+    set_userenv( $library->unblessed);
+    my $patron = $builder->build_object({ class => 'Koha::Patrons' });
+    C4::Members::Messaging::SetMessagingPreference({
+        borrowernumber => $patron->id,
+        message_transport_types => ['sms'],
+        message_attribute_id => 5
+    });
+    my $item = $builder->build_sample_item();
+    my $checkin_notice = $builder->build_object({
+        class => 'Koha::Notice::Templates',
+        value =>{
+            module => 'circulation',
+            code => 'CHECKIN',
+            branchcode => $library->branchcode,
+            name => 'Test Checkin',
+            is_html => 0,
+            content => "Checkins:\n----\n[% biblio.title %]-[% old_checkout.issue_id %]\n----Thank you.",
+            message_transport_type => 'sms',
+            lang => 'default'
+        }
+    })->store;
+
+    # Checkout an item, mark it returned, generate a notice
+    my $issue_1 = AddIssue( $patron->unblessed, $item->barcode);
+    MarkIssueReturned( $patron->borrowernumber, $item->itemnumber, undef, 0, { skip_record_index => 1} );
+    C4::Circulation::SendCirculationAlert({
+        type => 'CHECKIN',
+        item => $item->unblessed,
+        borrower => $patron->unblessed,
+        branch => $library->branchcode,
+        issue => $issue_1
+    });
+    my $notice = Koha::Notice::Messages->find({ borrowernumber => $patron->id, letter_code => 'CHECKIN' });
+    is($notice->content,"Checkins:\n".$item->biblio->title."-".$issue_1->id."\nThank you.", 'Letter generated with expected output on first checkin' );
+    is($notice->to_address, $patron->smsalertnumber, "Letter has the correct to_address set to smsalertnumber for SMS type notices");
+
+    # Checkout an item, mark it returned, generate a notice
+    my $issue_2 = AddIssue( $patron->unblessed, $item->barcode);
+    MarkIssueReturned( $patron->borrowernumber, $item->itemnumber, undef, 0, { skip_record_index => 1} );
+    C4::Circulation::SendCirculationAlert({
+        type => 'CHECKIN',
+        item => $item->unblessed,
+        borrower => $patron->unblessed,
+        branch => $library->branchcode,
+        issue => $issue_2
+    });
+    $notice->discard_changes();
+    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' );
+
+};
+
+subtest "GetSoonestRenewDate tests" => sub {
+    plan tests => 5;
+    Koha::CirculationRules->set_rule(
+        {
+            categorycode => undef,
+            branchcode   => undef,
+            itemtype     => undef,
+            rule_name    => 'norenewalbefore',
+            rule_value   => '7',
+        }
+    );
+    my $patron = $builder->build_object({ class => 'Koha::Patrons' });
+    my $item = $builder->build_sample_item();
+    my $issue = AddIssue( $patron->unblessed, $item->barcode);
+    my $datedue = dt_from_string( $issue->date_due() );
+
+    # Bug 14395
+    # Test 'exact time' setting for syspref NoRenewalBeforePrecision
+    t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact_time' );
+    is(
+        GetSoonestRenewDate( $patron->id, $item->itemnumber ),
+        $datedue->clone->add( days => -7 ),
+        'Bug 14395: Renewals permitted 7 days before due date, as expected'
+    );
+
+    # Bug 14395
+    # Test 'date' setting for syspref NoRenewalBeforePrecision
+    t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
+    is(
+        GetSoonestRenewDate( $patron->id, $item->itemnumber ),
+        $datedue->clone->add( days => -7 )->truncate( to => 'day' ),
+        'Bug 14395: Renewals permitted 7 days before due date, as expected'
+    );
+
+
+    Koha::CirculationRules->set_rule(
+        {
+            categorycode => undef,
+            branchcode   => undef,
+            itemtype     => undef,
+            rule_name    => 'norenewalbefore',
+            rule_value   => undef,
+        }
+    );
+
+    is(
+        GetSoonestRenewDate( $patron->id, $item->itemnumber ),
+        dt_from_string,
+        'Checkouts without auto-renewal can be renewed immediately if no norenewalbefore'
+    );
+
+    t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
+    $issue->auto_renew(1)->store;
+    is(
+        GetSoonestRenewDate( $patron->id, $item->itemnumber ),
+        $datedue->clone->truncate( to => 'day' ),
+        'Checkouts with auto-renewal can be renewed earliest on due date if no renewalbefore'
+    );
+    t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact' );
+    is(
+        GetSoonestRenewDate( $patron->id, $item->itemnumber ),
+        $datedue,
+        'Checkouts with auto-renewal can be renewed earliest on due date if no renewalbefore'
+    );
+};
+
 $schema->storage->txn_rollback;
 C4::Context->clear_syspref_cache();
 $branches = Koha::Libraries->search();