Created
October 12, 2025 23:43
-
-
Save Dyrcona/1f2e304db5dd5812702151fd20ace844 to your computer and use it in GitHub Desktop.
A Perl program to check and fix Evergreen ILS auto-renewal events.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/perl | |
| # --------------------------------------------------------------- | |
| # Copyright © 2025 C/W MARS, Inc. | |
| # Jason J.A. Stephenson <jstephenson@cwmars.org> | |
| # | |
| # This program is free software; you can redistribute it and/or modify | |
| # it under the terms of the GNU General Public License as published by | |
| # the Free Software Foundation; either version 2 of the License, or | |
| # (at your option) any later version. | |
| # | |
| # This program is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| # GNU General Public License for more details. | |
| # --------------------------------------------------------------- | |
| use strict; | |
| use warnings; | |
| use OpenILS::Utils::Cronscript; | |
| use DateTime qw( ); | |
| my $U = 'OpenILS::Application::AppUtils'; | |
| # This program is run for a previous day, normally yesterday. The | |
| # --daysback option can be used to run it for the day before yesterday | |
| # with a parameter of 2 (--daysback=2). Running it much farther back | |
| # than that is not so useful. | |
| my %defaults = ( | |
| nolockfile => 1, | |
| 'daysback=i' => 1 | |
| ); | |
| my $script = OpenILS::Utils::Cronscript->new(\%defaults); | |
| my $opts = $script->MyGetOptions(); | |
| # Setting daysback to 0 is dangerous because we may interfere with | |
| # running events. | |
| if ($opts->{daysback} == 0) { | |
| die "DANGER! Setting --daysback to 0 could interfere with running events. Exiting..."; | |
| } elsif ($opts->{daysback} < 0) { | |
| print STDERR "A negative --daysback value moves into the future.\n"; | |
| die "Forward time travel is not permitted. Exiting..."; | |
| } | |
| my $yesterday = DateTime->now( time_zone => 'local' ) | |
| ->set_time_zone('floating') | |
| ->truncate( to => 'day' ) | |
| ->subtract( days => $opts->{daysback} ) | |
| ->strftime('%Y-%m-%d'); | |
| # NB: This global editor object is reused in several of the functions | |
| # below. | |
| my $editor = $script->editor(); | |
| # Gather up the information that we need. We get the event id and | |
| # target for any Autorenew event (124) added on the day of the | |
| # daysback option that is in a state other than complete, invalid or | |
| # pending. Complete and invalid state events are OK and don't need to | |
| # be processed. If there are events in a pending state, that probably | |
| # means we ran this program already. | |
| # | |
| # We also grab data for any AutorenewNotify events (125) through a | |
| # left join because the event may have been created and even fired. | |
| # | |
| # We also get some information about a renewal circulation, if any, | |
| # that we will need later if we have to create a new AutorenewNotify | |
| # event. | |
| my $json_query = { | |
| select => { | |
| atev => ['id', 'target'], | |
| notify => [{column => "id", alias => "notify_id"}, | |
| {column => "state", alias => "notify_state"}], | |
| renewal => ["due_date", "auto_renewal", "renewal_remaining", | |
| "auto_renewal_remaining"] | |
| }, | |
| from => {atev => { | |
| circ => {field => "id", fkey => "target", | |
| join => { | |
| renewal => { | |
| type => "left", | |
| class => "circ", | |
| field => "parent_circ" | |
| } | |
| } | |
| }, | |
| notify => {class => "atev", fkey => "target", field => "target", | |
| type => "left", filter => {event_def => 125}} | |
| }}, | |
| where => { | |
| event_def => 124, | |
| state => {"not in" => ["complete", "invalid", "pending"]}, | |
| add_time => { "=" => { | |
| transform => "date", | |
| value => $yesterday | |
| }} | |
| } | |
| }; | |
| my $events = $editor->json_query($json_query); | |
| foreach (@$events) { | |
| my $event = $editor->retrieve_action_trigger_event($_->{id}); | |
| if ($_->{notify_id}) { | |
| # If there is a corresponding AutorenewNotify event, we handle | |
| # it and set the Autorenew event to complete. | |
| handle_notify_event($_); | |
| complete_event($event); | |
| } else { | |
| if ($_->{auto_renewal} && $U->is_true($_->{auto_renewal})) { | |
| # If we don't have the AutorenewNotify event, but we do | |
| # have an autorenewal circulation, then we create the | |
| # AutorenewNotify event and set the Autorenew event to | |
| # complete. | |
| create_notify_event($_); | |
| complete_event($event); | |
| } else { | |
| # Otherwise, we reset the Autorenew event to a pending | |
| # state so that the process will attempt it again. | |
| reset_event($event, 'Autorenew'); | |
| } | |
| } | |
| } | |
| ######################################## | |
| # This part handles circulations that should have autorenewals but | |
| # were missed. | |
| my $due_date = DateTime->now()->add(days=>1)->strftime("%Y-%m-%d"); | |
| # This query retrieves the minimal amount of data necessary to avoid a | |
| # segfault that happens with large production databases if we retrieve | |
| # all of the circ data at once. | |
| my $circ_query = { | |
| "select" => {"circ" => ["id","usr"]}, | |
| "from" => "circ", | |
| "where" => | |
| { | |
| "due_date" => {"=" => { | |
| "transform" => "date", | |
| "value" => ["date", $due_date] | |
| }}, | |
| "checkin_time" => undef, | |
| "xact_finish" => undef, | |
| "-or" => [ | |
| {"stop_fines" => undef}, | |
| {"stop_fines" => "MAXFINES"} | |
| ], | |
| "-not" => { | |
| "-exists" => { | |
| "select" => {"atev" => ["id"]}, | |
| "from" => "atev", | |
| "where" => { | |
| "event_def" => 124, | |
| "target" => {"=" => {"+circ" => "id"}} | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| my $circs = $editor->json_query($circ_query); | |
| if (@$circs) { | |
| my $session = $script->session('open-ils.trigger'); | |
| foreach (@$circs) { | |
| my $target = $editor->retrieve_action_circulation($_->{id}); | |
| my $req = $session->request('open-ils.trigger.event.autocreate', | |
| 'checkout.due', | |
| $target, | |
| $target->circ_lib, | |
| 'Daily-PD-2', | |
| undef); | |
| while (my $resp = $req->recv(timeout => 300)) { | |
| my $event = $resp->content; | |
| printf("Created event $event for circulation %d\n", $target->id); | |
| } | |
| } | |
| } | |
| ######################################## | |
| # If the AutorenewNotify event is not in a complete or pending state, | |
| # we reset it to a pending state and let the normal action_trigger | |
| # process deal with it. | |
| sub handle_notify_event { | |
| my $data = shift; | |
| if ($data->{notify_state} ne 'complete' && $data->{notify_state} ne 'pending') { | |
| my $notify = $editor->retrieve_action_trigger_event($data->{notify_id}); | |
| reset_event($notify, 'AutorenewNotify') | |
| } | |
| } | |
| sub create_notify_event { | |
| my $data = shift; | |
| my $circ = $editor->retrieve_action_circulation($data->{target}); | |
| # Use the renewal to set user_data. It's easier than calculating | |
| # the values from the old circ. | |
| my $user_data = { | |
| copy => $circ->target_copy(), | |
| is_renewed => 1, | |
| reason => '', | |
| new_due_date => $data->{due_date}, | |
| old_due_date => $circ->due_date(), | |
| textcode => 'SUCCESS', | |
| total_renewal_remaining => $data->{renewal_remaining}, | |
| auto_renewal_remaining => $data->{auto_renewal_remaining} | |
| }; | |
| # Use the old circ to create the event. | |
| my $result = $U->simplereq( | |
| 'open-ils.trigger', | |
| 'open-ils.trigger.event.autocreate', | |
| 'autorenewal', | |
| $circ, | |
| $circ->circ_lib(), | |
| undef, | |
| $user_data | |
| ); | |
| if ($result) { | |
| printf("Created AutorenewNotify event %d\n", $result); | |
| } else { | |
| printf("Failed to create AutorenewNotify event for Autorenew event %d\n", $_->{id}); | |
| } | |
| } | |
| sub complete_event { | |
| my $event = shift; | |
| $event->complete_time('now()'); | |
| $event->state('complete'); | |
| $editor->xact_begin; | |
| my $result = $editor->update_action_trigger_event($event); | |
| $editor->commit; | |
| printf("Completed Autorenew event %d\n", $event->id()); | |
| return $result; | |
| } | |
| # Resets the passed in event to a pending state. The second argument | |
| # is used as a descriptor in the diagnostic output. | |
| sub reset_event { | |
| my $event = shift; | |
| my $def = shift; | |
| my $output = get_event_output($event); | |
| eval { | |
| $event->clear_start_time; | |
| $event->clear_update_time; | |
| $event->clear_complete_time; | |
| $event->clear_update_process; | |
| $event->clear_template_output; | |
| $event->clear_error_output; | |
| $event->clear_async_output; | |
| $event->state('pending'); | |
| $editor->xact_begin; | |
| $event = $editor->update_action_trigger_event($event); | |
| if ($output) { | |
| $editor->delete_action_trigger_event_output($output); | |
| } | |
| $editor->xact_commit; | |
| }; | |
| if ($@) { | |
| warn($@); | |
| $editor->xact_rollback; | |
| } else { | |
| $event = $editor->retrieve_action_trigger_event($event); | |
| printf("Reset %s event %d\n", $def, $event->id()); | |
| } | |
| } | |
| sub get_event_output { | |
| my $event = shift; | |
| my $output = $event->template_output; | |
| $output = $event->error_output unless ($output); | |
| $output = $event->async_output unless ($output); | |
| if ($output) { | |
| $output = $editor->retrieve_action_trigger_event_output($output); | |
| } | |
| return $output; | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This program uses hard-coded values for the Autorenew and AutorenewNotify events as well as for the Autorenew event's granularity as they appear in the C/W MARS database. In order to use this program with your Evergreen installation, you will need to make the following changes:
The value on line 130 assumes a delay of -2 days, i.e. the autorenewals happen 2 days before the due date. The value of 1 therefore checks for items due tomorrow, assuming we run this the day after the autornewals happen. If you use a different delay, then you will need to adjust the value as appropriate.