LP#1198465 Move fine generation into CircCommon
authorDan Wells <dbw2@calvin.edu>
Mon, 24 Nov 2014 22:10:31 +0000 (17:10 -0500)
committerBill Erickson <berickxx@gmail.com>
Fri, 20 Feb 2015 21:20:43 +0000 (16:20 -0500)
Allows circulation code to generate fines within the main circulation
database transaction.

Squahed commits:

LP#1198465 Initial copy of generate_fines to CircCommon.pm

Copy over generate_fines, including the necessary 'ceil' and $parser
bits, and the seconds_to_interval_hash helper function.

LP#1198465 First pass at generate_fines cstore conversion

Broadly speaking, this commit does two things:

1) Rewrite bits of the generate_fines copy in CircCommon.pm to use
cstore in place of the DBI code bits.
2) Gut the old generate_fines in Publisher/action.pm, including helper
bits, then make it a wrapper for the CircCommon verison.

This is a rough pass, but works in basic testing.

LP#1198465 Make generate_fines() chattiness optional

The old storage generate_fines() was really chatty with it's client. We
generally won't want that, so make it optional.

Also, remove one response which I added earlier while debugging.

LP#1198465 Stop using storage wrapper for generate_fines()

With pieces now in place, let's stop using the storage wrapper for
generate_fines() and call the code directly.

Also, to avoid potential confusion, rename the calling function in
Circulate.pm from generate_fines() to handle_fines().  This function
may also be taking on more than just fine generation in the near future.

Finally, since the new generate_fines() expects one or more pre-filtered
circ objects, add the requisite stop_fines check to handle_fines().

LP#1198465 Put penalty generation in the same xact

The penalties need to "see" any fines we may have just added, so they
need to happen in the same transaction.  Passing in the optional editor
makes this an easy fix.

Signed-off-by: Dan Wells <dbw2@calvin.edu>
Signed-off-by: Bill Erickson <berickxx@gmail.com>

Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm

index f2d5aa5..db82c49 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,309 @@ 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 ($class, $args) = @_;
+    my $circs = $args->{circs};
+      return unless $circs and @$circs;
+    my $e = $args->{editor};
+    # if a client connection is passed in, this will be chatty like
+    # the old storage version
+    my $conn = $args->{conn};
+
+    my $commit = 0;
+    unless ($e) {
+        $e = new_editor(xact => 1);
+        $commit = 1;
+    }
+
+    my %hoo = map { ( $_->id => $_ ) } @{ $e->retrieve_all_actor_org_unit_hours_of_operation };
+
+    my $penalty = OpenSRF::AppSession->create('open-ils.penalty');
+    my $handling_resvs = 0;
+    for my $c (@$circs) {
+
+        my $ctype = ref($c);
+
+        if (!$ctype) { # we received only an idlist, not objects
+            if ($handling_resvs) {
+                $c = $e->retrieve_booking_reservation($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 = $e->retrieve_action_circulation($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) {
+#                $logger->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 );
+            $logger->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 ) {
+                $conn->respond( "Fine Generator skipping circ due to 0 fine interval, 0 fine rate, or 0 max fine.\n" ) if $conn;
+                $logger->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;
+            }
+    
+            $conn->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") if $conn;
+    
+            my @fines = @{$e->search_money_billing(
+                { 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 !$U->is_true($_->voided) } @fines);
+    
+            my $last_fine;
+            if ($fine) {
+                $conn->respond( "Last billing time: ".$fine->billing_ts." (clensed format: ".cleanse_ISO8601( $fine->billing_ts ).")") if $conn;
+                $last_fine = $parser->parse_datetime( cleanse_ISO8601( $fine->billing_ts ) )->epoch;
+            } else {
+                $logger->info( "Potential first billing for circ ".$c->id );
+                $last_fine = $due;
+
+                $grace_period = extend_grace_period($class, $c->$circ_lib_method,$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
+            ) {
+                $conn->respond( "Still inside grace period of: ". seconds_to_interval( $grace_period )."\n" ) if $conn;
+                $logger->info( "Circ ".$c->id." is still inside grace period of: $grace_period [". seconds_to_interval( $grace_period ).']' );
+                return;
+            }
+
+            $conn->respond( "\t$pending_fine_count pending fine(s)\n" ) if $conn;
+            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, '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, '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) {
+                    if ($ctype eq 'circulation') {
+                        $c->stop_fines('MAXFINES');
+                        $c->stop_fines_time('now');
+                        $e->update_action_circulation($c);
+                    }
+                    $conn->respond(
+                        "\tMaximum fine level of ".$c->max_fine.
+                        " reached for this $ctype.\n".
+                        "\tNo more fines will be generated.\n" ) if $conn;
+                    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 = @{$e->search_actor_org_unit_closed_date(
+                            { 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;
+
+                my $bill = Fieldmapper::money::billing->new;
+                $bill->xact($c->id);
+                $bill->note("System Generated Overdue Fine");
+                $bill->billing_type("Overdue materials");
+                $bill->btype(1);
+                $bill->amount(sprintf('%0.2f', $this_billing_amount/100));
+                $bill->billing_ts($timestamptz);
+                $e->create_money_billing($bill);
+
+            }
+
+            $conn->respond( "\t\tAdding fines totaling $latest_amount for overdue up to $latest_billing_ts\n" )
+                if ($conn and $latest_billing_ts and $latest_amount);
+
+#            $self->method_lookup('open-ils.storage.transaction.commit')->run;
+
+            # Calculate penalties inline
+            OpenILS::Utils::Penalty->calculate_penalties(
+                $e, $c->usr, $c->$circ_lib_method);
+
+        };
+
+        if ($@) {
+            my $e = $@;
+            $conn->respond( "Error processing overdue $ctype [".$c->id."]:\n\n$e\n" ) if $conn;
+            $logger->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);
+        }
+    }
+
+    $e->commit if ($commit);
+
+    return undef;
+}
+
 1;
index 9f57779..1a2e677 100644 (file)
@@ -434,6 +434,7 @@ use OpenILS::Utils::Penalty;
 use OpenILS::Application::Circ::CircCommon;
 use Time::Local;
 
+my $CC = "OpenILS::Application::Circ::CircCommon";
 my $holdcode    = "OpenILS::Application::Circ::Holds";
 my $transcode   = "OpenILS::Application::Circ::Transit";
 my %user_groups;
@@ -2105,7 +2106,7 @@ sub do_reservation_return {
         $self->reservation($reservation);
     }
 
-    $self->generate_fines(1);
+    $self->handle_fines(1);
     $self->reservation->return_time('now');
     $self->update_reservation();
     $self->reshelve_copy if $self->copy;
@@ -2524,7 +2525,7 @@ sub do_checkin {
         }
 
         # run the fine generator against this circ
-        $self->generate_fines(undef, $ignore_stop_fines);
+        $self->handle_fines(undef, $ignore_stop_fines);
     }
 
     if( $self->checkin_check_holds_shelf() ) {
@@ -3334,7 +3335,7 @@ sub put_hold_on_shelf {
 
 
 
-sub generate_fines {
+sub handle_fines {
    my $self = shift;
    my $reservation = shift;
    my $ignore_stop_fines = shift;
@@ -3342,6 +3343,8 @@ sub generate_fines {
 
    my $obj = $reservation ? $self->reservation : $self->circ;
 
+   return undef if (!$ignore_stop_fines and $obj->stop_fines);
+
    # If we have a grace period
    if($obj->can('grace_period')) {
       # Parse out the due date
@@ -3352,19 +3355,7 @@ sub generate_fines {
       return undef if ($due_date > DateTime->now);
    }
 
-   if (!exists($self->{_gen_fines_req})) {
-      $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage') 
-          ->request(
-             'open-ils.storage.action.circulation.overdue.generate_fines',
-             $obj->id, $ignore_stop_fines
-          );
-   }
-   $self->{_gen_fines_req}->wait_complete;
-   delete($self->{_gen_fines_req});
-
-   # refresh the circ in case the fine generator set the stop_fines field
-   $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
-   $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
+   $CC->generate_fines({circs => [$obj], editor => $self->editor});
 
    return undef;
 }
@@ -3814,7 +3805,9 @@ sub do_renew {
     }
 
     # Run the fine generator against the old circ
-    $self->generate_fines;
+    # XXX This seems unnecessary, given that handle_fines runs in do_checkin
+    # a few lines down.  Commenting out, for now.
+    #$self->handle_fines;
 
     $self->run_renew_permit;
 
index b619711..8d272a0 100644 (file)
@@ -13,7 +13,6 @@ use OpenILS::Utils::CStoreEditor qw/:funcs/;
 use DateTime;
 use DateTime::Format::ISO8601;
 use OpenILS::Utils::Penalty;
-use POSIX qw(ceil);
 use OpenILS::Application::Circ::CircCommon;
 use OpenILS::Application::AppUtils;
 my $U = "OpenILS::Application::AppUtils";
@@ -993,329 +992,30 @@ __PACKAGE__->register_method(
     method          => 'find_usr_summary_surveys',
 );
 
-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 $overbill = shift;
+    my $circ_id = shift;
 
-    local $OpenILS::Application::Storage::WRITE = 1;
-
-    my @circs;
-    if ($circ) {
-        push @circs,
-            action::circulation->search_where( { id => $circ, stop_fines => undef } ),
-            booking::reservation->search_where( { id => $circ, return_time => undef, cancel_time => undef } );
+    my $circs;
+    my $editor = new_editor;
+    if ($circ_id) {
+#        my $circ;
+#        if ($circ = action::circulation->search_where( { id => $circ_id, stop_fines => undef } )) {
+#            $circ = action::circulation->retrieve($circ_id)->to_fieldmapper;
+#        } elsif ($circ = booking::reservation->search_where( { id => $circ_id, return_time => undef, cancel_time => undef } )) {
+#            $circ = booking::reservation->retrieve($circ_id)->to_fieldmapper;
+#        }
+#        $circs = [$circ] if ($circ);
+        $circs = $editor->search_action_circulation( { id => $circ_id, stop_fines => undef } );
+        unless (@$circs) {
+            $circs = $editor->search_booking_reservation->search_where( { id => $circ_id, return_time => undef, cancel_time => undef } );
+        }
     } else {
-        push @circs, overdue_circs(undef, 1, 1, 1);
+        $circs = [map { $_->to_fieldmapper } overdue_circs(undef, 1, 1, 1)];
     }
 
-    $logger->info("fine generator processing ".scalar(@circs)." transactions");
-
-    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);
-            if ($overbill) {
-                $fine = $fines[++$f_idx] while ($fine and $fine->voided);
-            }
-
-            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);
-        }
-    }
+    return OpenILS::Application::Circ::CircCommon->generate_fines({circs => $circs, conn => $client})
 }
 __PACKAGE__->register_method(
     api_name        => 'open-ils.storage.action.circulation.overdue.generate_fines',