From aa9e7af9e2caf6e0334b5afbe4c2d15f39110f21 Mon Sep 17 00:00:00 2001
From: mbeaulieu <mbeaulieu@inlibro.com>
Date: Fri, 27 Jun 2014 16:46:43 -0400
Subject: [PATCH] Bug 11055 - Add 'Minutes' as a load period unit

WARNING: This patch is dependent on http://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=8133.
My patch on bug 8133 ensures proper calculation of overdues.

Minutes are added as a unit in circulation rules.

A test script is provided: 't/Circulation/minuteloan.t'.
---
 C4/Circulation.pm                                  |   11 +-
 C4/Overdues.pm                                     |    4 +-
 Koha/Calendar.pm                                   |    2 +-
 .../prog/en/modules/admin/smart-rules.tt           |    3 +-
 misc/cronjobs/fines.pl                             |    2 +-
 t/Circulation/minuteloan.t                         |  166 ++++++++++++++++++++
 6 files changed, 181 insertions(+), 7 deletions(-)
 create mode 100755 t/Circulation/minuteloan.t

diff --git a/C4/Circulation.pm b/C4/Circulation.pm
index 6650fcb..e782347 100644
--- a/C4/Circulation.pm
+++ b/C4/Circulation.pm
@@ -2104,6 +2104,8 @@ sub _debar_user_on_return {
         # finedays is in days, so hourly loans must multiply by 24
         # thus 1 hour late equals 1 day suspension * finedays rate
         $finedays = $finedays * 24 if ( $unit eq 'hours' );
+        $finedays = $finedays * 24 * 60 if ( $unit eq 'minutes' );
+
 
         # grace period is measured in the same units as the loan
         my $grace =
@@ -3234,8 +3236,8 @@ C<$isrenewal> = Boolean: is true if we want to calculate the date due for a rene
 sub CapDateDue{
     my ( $calcDueDate, $loanlength, $branch) = @_;
 
-    # for hourly loans, we will limit the due date to close time
-    return $calcDueDate unless ($loanlength->{lengthunit} eq 'hours');
+    # for hours and minutes, we will limit the due date to close time
+    return $calcDueDate unless ($loanlength->{lengthunit} eq 'hours' || $loanlength->{lengthunit} eq 'minutes');
 
 #    my $now = DateTime->now( time_zone => C4::Context->tz() );
     my $branchHours = getOpeningHours();
@@ -3286,6 +3288,8 @@ sub CalcDateDue {
             } else {
                 $datedue->add( hours => $loanlength->{issuelength} );
             }
+        } elsif ( $loanlength->{lengthunit} eq 'minutes' ) {
+                $datedue->add( minutes => $loanlength->{issuelength} );
         }else {    # days
             $datedue->add( days => $loanlength->{$length_key} );
             $datedue->set_hour(23);
@@ -3296,6 +3300,9 @@ sub CalcDateDue {
         if ($loanlength->{lengthunit} eq 'hours') {
             $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
         }
+        elsif ($loanlength->{lengthunit} eq 'minutes') {
+            $dur = DateTime::Duration->new( minutes => $loanlength->{issuelength});
+        }
         else { # days
             $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
         }
diff --git a/C4/Overdues.pm b/C4/Overdues.pm
index 3ed86f0..5f81507 100644
--- a/C4/Overdues.pm
+++ b/C4/Overdues.pm
@@ -139,7 +139,7 @@ LEFT JOIN biblioitems USING (biblioitemnumber)
     }
 
     if($params->{'shortrun'}){
-        $statement .= " and issuingrules.lengthunit in ('hours')  " ;
+        $statement .= " and issuingrules.lengthunit in ('hours', 'minutes')  " ;
     }
 
     $statement .= 'ORDER BY borrowernumber';
@@ -292,7 +292,7 @@ sub _get_chargeable_units {
     my ($unit, $dt1, $dt2, $branchcode, $openingHours) = @_;
     my $charge_units = 0;
     my $charge_duration;
-    if ($unit eq 'hours') {
+    if ($unit eq 'hours' || $unit eq 'minutes') {
         if(C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed') {
             if (!defined $openingHours->{$branchcode}{'Calendar'}){
                 $openingHours->{$branchcode}{'Calendar'} = Koha::Calendar->new( branchcode => $branchcode );
diff --git a/Koha/Calendar.pm b/Koha/Calendar.pm
index 7ef78d3..1edb538 100644
--- a/Koha/Calendar.pm
+++ b/Koha/Calendar.pm
@@ -125,7 +125,7 @@ sub addDate {
     $unit ||= 'days'; # default days ?
     my $dt;
 
-    if ( $unit eq 'hours' ) {
+    if ( $unit eq 'hours' || $unit eq 'minutes') {
         # Fixed for legacy support. Should be set as a branch parameter
         Readonly::Scalar my $return_by_hour => 10;
 
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt
index a7fe76e..aa9ffce 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt
@@ -242,6 +242,7 @@ for="tobranch"><strong>Clone these rules to:</strong></label> <input type="hidde
                       <select name="lengthunit" id="lengthunit">
                         <option value="days" selected>Days</option>
                         <option value="hours">Hours</option>
+                        <option value="minutes">Minutes</option>
                       </select>
                     </td>
                     <td>
@@ -253,7 +254,7 @@ for="tobranch"><strong>Clone these rules to:</strong></label> <input type="hidde
                         <input type="text" size="10" id="hardduedate" name="hardduedate" value="[% hardduedate %]" class="datepicker" />
                         <div class="hint">[% INCLUDE 'date-format.inc' %]</div>
                     </td>
-                    <td><input type="text" name="fine" id="fine" size="4" /></td>
+                    <td><input type="text" name="fine" id="fFine" size="4" /></td>
                     <td><input type="text" name="chargeperiod" id="chargeperiod" size="2" /></td>
                     <td><input type="text" name="firstremind" id="firstremind" size="2" /> </td>
                     <td><input type="text" name="overduefinescap" id="overduefinescap" size="6" /> </td>
diff --git a/misc/cronjobs/fines.pl b/misc/cronjobs/fines.pl
index 2e7391e..b4d775b 100755
--- a/misc/cronjobs/fines.pl
+++ b/misc/cronjobs/fines.pl
@@ -65,7 +65,7 @@ This script has the following parameters :
     -l --log: log the output to a file (optional if the -o parameter is given)
     -o --out:  ouput directory for logs (defaults to env or /tmp if !exist)
     -v --verbose
-    -s --short: only verifies the short duration borrowing (hourly)
+    -s --short: only verifies the short duration borrowing (minutes and hourly ones)
 
 ENDUSAGE
 
diff --git a/t/Circulation/minuteloan.t b/t/Circulation/minuteloan.t
new file mode 100755
index 0000000..0da4a7d
--- /dev/null
+++ b/t/Circulation/minuteloan.t
@@ -0,0 +1,166 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use Data::Dumper;
+use DateTime;
+use DateTime::Duration;
+use Test::More tests => 7;
+use Test::MockModule;
+use DBD::Mock;
+use Koha::DateUtils;
+use C4::Calendar qw(getOpeningHours);
+use C4::Circulation qw(CalcDateDue);
+use C4::Overdues qw(_get_chargeable_units Getoverdues);
+
+BEGIN {
+    die "DBD::Mock v$DBD::Mock::VERSION is too old. This test requires v1.45 or higher.", 33
+    unless $DBD::Mock::VERSION >= 1.45;
+
+    use_ok('Koha::Calendar');
+    use_ok('C4::Calendar');
+    use_ok('C4::Circulation');
+    use_ok('C4::Overdues');
+}
+
+my $module_context = new Test::MockModule('C4::Context');
+$module_context->mock(
+    '_new_dbh',
+    sub {
+        my $dbh = DBI->connect( 'DBI:Mock:', '', '' )
+          || die "Cannot create handle: $DBI::errstr\n";
+        return $dbh;
+    }
+);
+
+# Initialize the global $dbh variable
+my $dbh = C4::Context->dbh();
+
+{ # C4::Circulation
+    $module_context->mock('preference',sub {return 'Calendar';});
+
+    my $sessionCirc = DBD::Mock::Session->new('sessionCirc' => (
+        {
+           statement => qr/\s*SELECT issuelength, lengthunit, renewalperiod/,
+           results  => [
+                            [ 'issuelength', 'lengthunit', 'renewalperiod' ],
+                            [ 30, 'minutes' , 0 ]
+                        ]
+        },
+        {
+            statement => 'SELECT weekday FROM repeatable_holidays WHERE branchcode = ? AND weekday IS NOT NULL',
+            results => DBD::Mock->NULL_RESULTSET
+        },
+        {
+            statement => 'SELECT day, month FROM repeatable_holidays WHERE branchcode = ? AND weekday IS NULL',
+            results => [
+                            ['day', 'month'],
+                            [24,6]
+                        ]
+        },
+        {
+            statement => 'SELECT day, month, year FROM special_holidays WHERE branchcode = ? AND isexception = 1',
+            results => DBD::Mock->NULL_RESULTSET
+        },
+        {
+            statement => 'SELECT day, month, year FROM special_holidays WHERE branchcode = ? AND isexception = 0',
+            results => DBD::Mock->NULL_RESULTSET
+        },
+        { #GetIssuingRules called in GetHardDueDate
+            statement => qr/select \* from issuingrules/,
+            results => [
+                            ['hardduedate', 'hardduedatecompare'],
+                            [undef,-1]
+                        ]
+        },
+        { #Opening hours
+            statement => 'SELECT * FROM openinghours ',
+            results => [
+                            ['branchcode', 'weekcode', 'openhour', 'closehour'],
+                            ['BIB', 0, '09:00:00', '17:00:00'],
+                            ['BIB', 1, '09:00:00', '17:00:00'],
+                            ['BIB', 2, '09:00:00', '17:00:00'],
+                            ['BIB', 3, '09:00:00', '17:00:00'],
+                            ['BIB', 4, '09:00:00', '17:00:00'],
+                            ['BIB', 5, '09:00:00', '17:00:00'],
+                            ['BIB', 6, '09:00:00', '17:00:00']
+                        ]
+        }
+    ));
+    $dbh->{ mock_session } = $sessionCirc;
+    print "\nTesting C4::Circulation\n";
+    is(CalcDateDue(dt_from_string('2014-06-23T16:40:00', 'iso'), 0, 'BIB',{categorycode => 0, dateexpiry => '2020'}, 0), dt_from_string('2014-06-23T17:00:00', 'iso'), "Testing minute loans. Due time capped at close time.");
+}
+
+{ #C4::Overdues
+    print "\nTesting C4::Overdues\n";
+    $module_context->mock('preference',sub {return 'noFinesWhenClosed';});
+    print "  finesCalendar syspref set to 'noFinesWhenClosed'\n";
+    my $sessionOver = DBD::Mock::Session->new('sessionOver' =>(
+        { #Opening hours TEST #6
+            statement => 'SELECT * FROM openinghours ',
+            results => [
+                            ['branchcode', 'weekcode', 'openhour', 'closehour'],
+                            ['BIB', 0, '09:00:00', '17:00:00'],
+                            ['BIB', 1, '09:00:00', '17:00:00'],
+                            ['BIB', 2, '09:00:00', '17:00:00'],
+                            ['BIB', 3, '09:00:00', '17:00:00'],
+                            ['BIB', 4, '09:00:00', '17:00:00'],
+                            ['BIB', 5, '09:00:00', '17:00:00'],
+                            ['BIB', 6, '09:00:00', '17:00:00']
+                        ]
+        },
+        {
+            statement => 'SELECT weekday FROM repeatable_holidays WHERE branchcode = ? AND weekday IS NOT NULL',
+            results => DBD::Mock->NULL_RESULTSET
+        },
+        {
+            statement => 'SELECT day, month FROM repeatable_holidays WHERE branchcode = ? AND weekday IS NULL',
+            results => [
+                            ['day', 'month'],
+                            [24,6]
+                       ]
+        },
+        { #Opening hours TEST #7
+            statement => 'SELECT * FROM openinghours ',
+            results => [
+                            ['branchcode', 'weekcode', 'openhour', 'closehour'],
+                            ['BIB', 0, '09:00:00', '17:00:00'],
+                            ['BIB', 1, '09:00:00', '17:00:00'],
+                            ['BIB', 2, '09:00:00', '17:00:00'],
+                            ['BIB', 3, '09:00:00', '17:00:00'],
+                            ['BIB', 4, '09:00:00', '17:00:00'],
+                            ['BIB', 5, '09:00:00', '17:00:00'],
+                            ['BIB', 6, '09:00:00', '17:00:00']
+                        ]
+        },
+        {
+            statement => qr/select \* from issuingrules/,
+            results  => [
+                            [ 'issuelength', 'lengthunit', 'fine', 'chargeperiod', 'firstremind'],
+                            [ 30, 'minutes' , 0.25, 1, 0 ]
+                        ]
+        },
+        {
+            statement => 'SELECT weekday FROM repeatable_holidays WHERE branchcode = ? AND weekday IS NOT NULL',
+            results => DBD::Mock->NULL_RESULTSET
+        },
+        {
+            statement => 'SELECT day, month FROM repeatable_holidays WHERE branchcode = ? AND weekday IS NULL',
+            results => DBD::Mock->NULL_RESULTSET
+        },
+        {
+            statement => 'SELECT day, month, year FROM special_holidays WHERE branchcode = ? AND isexception = 1',
+            results => DBD::Mock->NULL_RESULTSET
+        },
+        {
+            statement => 'SELECT day, month, year FROM special_holidays WHERE branchcode = ? AND isexception = 0',
+            results => DBD::Mock->NULL_RESULTSET
+        }
+    ));
+    $dbh->{ mock_session } = $sessionOver;
+    is(_get_chargeable_units('minutes', dt_from_string('2014-06-23T12:00:00', 'iso'), dt_from_string('2014-06-25T10:00:00', 'iso'), 'BIB', getOpeningHours()), 360, "Test if _get_chargeable_units takes opening hours and holidays into account.");
+
+    my ($fine) = CalcFine( {itemtype=>'item'}, 0, 'BIB' ,dt_from_string('2014-6-23T12:00:00', 'iso'), dt_from_string('2014-6-23T13:00:00', 'iso'), getOpeningHours());
+    is($fine, 15, 'Fines are calculated correctly for minute loans');
+}
-- 
1.7.9.5