LP#1198465 Initial copy of generate_fines to CircCommon.pm
authorDan Wells <dbw2@calvin.edu>
Mon, 24 Nov 2014 22:10:31 +0000 (17:10 -0500)
committerDan Wells <dbw2@calvin.edu>
Tue, 17 Feb 2015 21:04:22 +0000 (16:04 -0500)
Copy over generate_fines, including the necessary 'ceil' and $parser
bits, and the seconds_to_interval_hash helper function.

Signed-off-by: Dan Wells <dbw2@calvin.edu>
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm

index f2d5aa5..e7acd5f 100644 (file)
@@ -8,8 +8,10 @@ use OpenILS::Event;
 use OpenSRF::Utils::Logger qw(:logger);
 use OpenILS::Utils::CStoreEditor q/:funcs/;
 use OpenILS::Const qw/:const/;
+use POSIX qw(ceil);
 
 my $U = "OpenILS::Application::AppUtils";
+my $parser = DateTime::Format::ISO8601->new;
 
 # -----------------------------------------------------------------
 # Do not publish methods here.  This code is shared across apps.
@@ -284,4 +286,328 @@ sub can_close_circ {
     return $can_close;
 }
 
+sub seconds_to_interval_hash {
+        my $interval = shift;
+        my $limit = shift || 's';
+        $limit =~ s/^(.)/$1/o;
+
+        my %output;
+
+        my ($y,$ym,$M,$Mm,$w,$wm,$d,$dm,$h,$hm,$m,$mm,$s);
+        my ($year, $month, $week, $day, $hour, $minute, $second) =
+                ('years','months','weeks','days', 'hours', 'minutes', 'seconds');
+
+        if ($y = int($interval / (60 * 60 * 24 * 365))) {
+                $output{$year} = $y;
+                $ym = $interval % (60 * 60 * 24 * 365);
+        } else {
+                $ym = $interval;
+        }
+        return %output if ($limit eq 'y');
+
+        if ($M = int($ym / ((60 * 60 * 24 * 365)/12))) {
+                $output{$month} = $M;
+                $Mm = $ym % ((60 * 60 * 24 * 365)/12);
+        } else {
+                $Mm = $ym;
+        }
+        return %output if ($limit eq 'M');
+
+        if ($w = int($Mm / 604800)) {
+                $output{$week} = $w;
+                $wm = $Mm % 604800;
+        } else {
+                $wm = $Mm;
+        }
+        return %output if ($limit eq 'w');
+
+        if ($d = int($wm / 86400)) {
+                $output{$day} = $d;
+                $dm = $wm % 86400;
+        } else {
+                $dm = $wm;
+        }
+        return %output if ($limit eq 'd');
+
+        if ($h = int($dm / 3600)) {
+                $output{$hour} = $h;
+                $hm = $dm % 3600;
+        } else {
+                $hm = $dm;
+        }
+        return %output if ($limit eq 'h');
+
+        if ($m = int($hm / 60)) {
+                $output{$minute} = $m;
+                $mm = $hm % 60;
+        } else {
+                $mm = $hm;
+        }
+        return %output if ($limit eq 'm');
+
+        if ($s = int($mm)) {
+                $output{$second} = $s;
+        } else {
+                $output{$second} = 0 unless (keys %output);
+        }
+        return %output;
+}
+
+sub generate_fines {
+    my $self = shift;
+    my $client = shift;
+    my $circ = shift;
+    my $stop_fines_reasons = shift;
+
+    local $OpenILS::Application::Storage::WRITE = 1;
+
+    my @circs;
+    if ($circ) {
+        push @circs,
+            action::circulation->search_where( { id => $circ, stop_fines => $stop_fines_reasons } ),
+            booking::reservation->search_where( { id => $circ, return_time => undef, cancel_time => undef } );
+    } else {
+        push @circs, overdue_circs();
+    }
+
+    my %hoo = map { ( $_->id => $_ ) } actor::org_unit::hours_of_operation->retrieve_all;
+
+    my $penalty = OpenSRF::AppSession->create('open-ils.penalty');
+    my $handling_resvs = 0;
+    for my $c (@$circs) {
+
+        my $ctype = ref($c);
+
+        if (!$ctype) { # fetched via idlist
+            if ($handling_resvs) {
+                $c = booking::reservation->retrieve($c);
+            } elsif (not defined $c) {
+                # an undef value is the indicator that we are moving
+                # from processing circulations to reservations.
+                $handling_resvs = 1;
+                next;
+            } else {
+                $c = action::circulation->retrieve($c);
+            }
+            $ctype = ref($c);
+        }
+
+        $ctype =~ s/^.+::(\w+)$/$1/;
+    
+        my $due_date_method = 'due_date';
+        my $target_copy_method = 'target_copy';
+        my $circ_lib_method = 'circ_lib';
+        my $recurring_fine_method = 'recurring_fine';
+        my $is_reservation = 0;
+        if ($ctype eq 'reservation') {
+            $is_reservation = 1;
+            $due_date_method = 'end_time';
+            $target_copy_method = 'current_resource';
+            $circ_lib_method = 'pickup_lib';
+            $recurring_fine_method = 'fine_amount';
+            next unless ($c->fine_interval);
+        }
+        #TODO: reservation grace periods
+        my $grace_period = ($is_reservation ? 0 : interval_to_seconds($c->grace_period));
+
+        eval {
+            if ($self->method_lookup('open-ils.storage.transaction.current')->run) {
+                $log->debug("Cleaning up after previous transaction\n");
+                $self->method_lookup('open-ils.storage.transaction.rollback')->run;
+            }
+            $self->method_lookup('open-ils.storage.transaction.begin')->run( $client );
+            $log->info(
+                sprintf("Processing %s %d...",
+                    ($is_reservation ? "reservation" : "circ"), $c->id
+                )
+            );
+
+
+            my $due_dt = $parser->parse_datetime( cleanse_ISO8601( $c->$due_date_method ) );
+    
+            my $due = $due_dt->epoch;
+            my $now = time;
+
+            my $fine_interval = $c->fine_interval;
+            $fine_interval =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
+            $fine_interval = interval_to_seconds( $fine_interval );
+    
+            if ( $fine_interval == 0 || int($c->$recurring_fine_method * 100) == 0 || int($c->max_fine * 100) == 0 ) {
+                $client->respond( "Fine Generator skipping circ due to 0 fine interval, 0 fine rate, or 0 max fine.\n" );
+                $log->info( "Fine Generator skipping circ " . $c->id . " due to 0 fine interval, 0 fine rate, or 0 max fine." );
+                return;
+            }
+
+            if ( $is_reservation and $fine_interval >= interval_to_seconds('1d') ) {    
+                my $tz_offset_s = 0;
+                if ($due_dt->strftime('%z') =~ /(-|\+)(\d{2}):?(\d{2})/) {
+                    $tz_offset_s = $1 . interval_to_seconds( "${2}h ${3}m"); 
+                }
+    
+                $due -= ($due % $fine_interval) + $tz_offset_s;
+                $now -= ($now % $fine_interval) + $tz_offset_s;
+            }
+    
+            $client->respond(
+                "ARG! Overdue $ctype ".$c->id.
+                " for item ".$c->$target_copy_method.
+                " (user ".$c->usr.").\n".
+                "\tItem was due on or before: ".localtime($due)."\n");
+    
+            my @fines = money::billing->search_where(
+                { xact => $c->id,
+                  btype => 1,
+                  billing_ts => { '>' => $c->$due_date_method } },
+                { order_by => 'billing_ts DESC'}
+            );
+
+            my $f_idx = 0;
+            my $fine = $fines[$f_idx] if (@fines);
+            my $current_fine_total = 0;
+            $current_fine_total += int($_->amount * 100) for (grep { $_ and !$_->voided } @fines);
+    
+            my $last_fine;
+            if ($fine) {
+                $client->respond( "Last billing time: ".$fine->billing_ts." (clensed format: ".cleanse_ISO8601( $fine->billing_ts ).")");
+                $last_fine = $parser->parse_datetime( cleanse_ISO8601( $fine->billing_ts ) )->epoch;
+            } else {
+                $log->info( "Potential first billing for circ ".$c->id );
+                $last_fine = $due;
+
+                $grace_period = OpenILS::Application::Circ::CircCommon->extend_grace_period($c->$circ_lib_method->to_fieldmapper->id,$c->$due_date_method,$grace_period,undef,$hoo{$c->$circ_lib_method});
+            }
+
+            return if ($last_fine > $now);
+            # Generate fines for each past interval, including the one we are inside
+            my $pending_fine_count = ceil( ($now - $last_fine) / $fine_interval );
+
+            if ( $last_fine == $due                         # we have no fines yet
+                 && $grace_period                           # and we have a grace period
+                 && $now < $due + $grace_period             # and some date math says were are within the grace period
+            ) {
+                $client->respond( "Still inside grace period of: ". seconds_to_interval( $grace_period )."\n" );
+                $log->info( "Circ ".$c->id." is still inside grace period of: $grace_period [". seconds_to_interval( $grace_period ).']' );
+                return;
+            }
+
+            $client->respond( "\t$pending_fine_count pending fine(s)\n" );
+            return unless ($pending_fine_count);
+
+            my $recurring_fine = int($c->$recurring_fine_method * 100);
+            my $max_fine = int($c->max_fine * 100);
+
+            my $skip_closed_check = $U->ou_ancestor_setting_value(
+                $c->$circ_lib_method->to_fieldmapper->id, 'circ.fines.charge_when_closed');
+            $skip_closed_check = $U->is_true($skip_closed_check);
+
+            my $truncate_to_max_fine = $U->ou_ancestor_setting_value(
+                $c->$circ_lib_method->to_fieldmapper->id, 'circ.fines.truncate_to_max_fine');
+            $truncate_to_max_fine = $U->is_true($truncate_to_max_fine);
+
+            my ($latest_billing_ts, $latest_amount) = ('',0);
+            for (my $bill = 1; $bill <= $pending_fine_count; $bill++) {
+    
+                if ($current_fine_total >= $max_fine) {
+                    $c->update({stop_fines => 'MAXFINES', stop_fines_time => 'now'}) if ($ctype eq 'circulation');
+                    $client->respond(
+                        "\tMaximum fine level of ".$c->max_fine.
+                        " reached for this $ctype.\n".
+                        "\tNo more fines will be generated.\n" );
+                    last;
+                }
+                
+                # XXX Use org time zone (or default to 'local') once we have the ou setting built for that
+                my $billing_ts = DateTime->from_epoch( epoch => $last_fine, time_zone => 'local' );
+                my $current_bill_count = $bill;
+                while ( $current_bill_count ) {
+                    $billing_ts->add( seconds_to_interval_hash( $fine_interval ) );
+                    $current_bill_count--;
+                }
+
+                my $timestamptz = $billing_ts->strftime('%FT%T%z');
+                if (!$skip_closed_check) {
+                    my $dow = $billing_ts->day_of_week_0();
+                    my $dow_open = "dow_${dow}_open";
+                    my $dow_close = "dow_${dow}_close";
+
+                    if (my $h = $hoo{$c->$circ_lib_method}) {
+                        next if ( $h->$dow_open eq '00:00:00' and $h->$dow_close eq '00:00:00');
+                    }
+    
+                    my @cl = actor::org_unit::closed_date->search_where(
+                            { close_start   => { '<=' => $timestamptz },
+                              close_end => { '>=' => $timestamptz },
+                              org_unit  => $c->$circ_lib_method }
+                    );
+                    next if (@cl);
+                }
+
+                # The billing amount for this billing normally ought to be the recurring fine amount.
+                # However, if the recurring fine amount would cause total fines to exceed the max fine amount,
+                # we may wish to reduce the amount for this billing (if circ.fines.truncate_to_max_fine is true).
+                my $this_billing_amount = $recurring_fine;
+                if ( $truncate_to_max_fine && ($current_fine_total + $this_billing_amount) > $max_fine ) {
+                    $this_billing_amount = ($max_fine - $current_fine_total);
+                }
+                $current_fine_total += $this_billing_amount;
+                $latest_amount += $this_billing_amount;
+                $latest_billing_ts = $timestamptz;
+
+                money::billing->create(
+                    { xact      => ''.$c->id,
+                      note      => "System Generated Overdue Fine",
+                      billing_type  => "Overdue materials",
+                      btype     => 1,
+                      amount    => sprintf('%0.2f', $this_billing_amount/100),
+                      billing_ts    => $timestamptz,
+                    }
+                );
+
+            }
+
+            $client->respond( "\t\tAdding fines totaling $latest_amount for overdue up to $latest_billing_ts\n" )
+                if ($latest_billing_ts and $latest_amount);
+
+            $self->method_lookup('open-ils.storage.transaction.commit')->run;
+
+            if(1) { 
+
+                # Caluclate penalties inline
+                OpenILS::Utils::Penalty->calculate_penalties(
+                    undef, $c->usr->to_fieldmapper->id.'', $c->$circ_lib_method->to_fieldmapper->id.'');
+
+            } else {
+
+                # Calculate penalties with an aysnc call to the penalty server.  This approach
+                # may lead to duplicate penalties since multiple penalty processes for a
+                # given user may be running at the same time. Leave this here for reference 
+                # in case we later find that asyc calls are needed in some environments.
+                $penalty->request(
+                    'open-ils.penalty.patron_penalty.calculate',
+                    { patronid  => ''.$c->usr,
+                    context_org => ''.$c->$circ_lib_method,
+                    update  => 1,
+                    background  => 1,
+                    }
+                )->gather(1);
+            }
+
+        };
+
+        if ($@) {
+            my $e = $@;
+            $client->respond( "Error processing overdue $ctype [".$c->id."]:\n\n$e\n" );
+            $log->error("Error processing overdue $ctype [".$c->id."]:\n$e\n");
+            $self->method_lookup('open-ils.storage.transaction.rollback')->run;
+            last if ($e =~ /IS NOT CONNECTED TO THE NETWORK/o);
+        }
+    }
+}
+__PACKAGE__->register_method(
+    api_name        => 'open-ils.storage.action.circulation.overdue.generate_fines',
+    api_level       => 1,
+    stream      => 1,
+    method          => 'generate_fines',
+);
+
 1;