Skip to content

Instantly share code, notes, and snippets.

@Dyrcona
Created October 12, 2025 23:43
Show Gist options
  • Select an option

  • Save Dyrcona/1f2e304db5dd5812702151fd20ace844 to your computer and use it in GitHub Desktop.

Select an option

Save Dyrcona/1f2e304db5dd5812702151fd20ace844 to your computer and use it in GitHub Desktop.
A Perl program to check and fix Evergreen ILS auto-renewal events.
#!/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;
}
@Dyrcona
Copy link
Author

Dyrcona commented Oct 12, 2025

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:

  1. Change all event_def and id values of 124 to the appropriate value(s) for your Autorenew event definition id.
  2. Change all event_def and id values of 125 to the appropriate values(s) for you AutorenewNotify event definition id.
  3. Change 'Daily-PD-2' to the granularity value of your Autorenew event definition.
  4. Change the value of 1 on line 130 to a value appropriate for your Autorenew event's delay.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment