From a9c3660f2b7a61069b445b06a4805121d3cf2cda Mon Sep 17 00:00:00 2001 From: Cesar Velez Date: Tue, 28 Aug 2018 16:13:51 -0400 Subject: [PATCH] LP#1779920 - Autorenew Feature This branch adds the necessary changes to allow Evergreen via the Action/Trigger system to generate daily automatic renewals of outstanding loans. Implemented as pair of A/T definitions: Autorenew and AutorenewNotify. Signed-off by: Cesar Velez Signed-off-by: Bill Erickson Signed-off-by: Galen Charlton --- Open-ILS/examples/fm_IDL.xml | 3 + Open-ILS/src/extras/ils_events.xml | 3 + .../lib/OpenILS/Application/Circ/Circulate.pm | 22 ++++- .../Application/Storage/CDBI/config.pm | 2 +- .../Trigger/Reactor/Circ/AutoRenew.pm | 86 +++++++++++++++++++ .../OpenILS/Application/Trigger/Validator.pm | 32 +++++++ Open-ILS/src/sql/Pg/950.data.seed-values.sql | 52 +++++++++++ ...XXX.autorenewals_acp_and_circ_duration.sql | 53 ++++++++++++ .../global/config/rule_circ_duration.tt2 | 2 +- 9 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor/Circ/AutoRenew.pm create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.autorenewals_acp_and_circ_duration.sql diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 9436ceb14d..c9a60dd31a 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -3446,6 +3446,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + @@ -4562,6 +4563,8 @@ SELECT usr, + + diff --git a/Open-ILS/src/extras/ils_events.xml b/Open-ILS/src/extras/ils_events.xml index 473b2d65dd..9f92944cc5 100644 --- a/Open-ILS/src/extras/ils_events.xml +++ b/Open-ILS/src/extras/ils_events.xml @@ -930,6 +930,9 @@ Potentially notified patron does not own the circulation. + + Circulation has no more auto-renewals remaining + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm index c2d55186cf..ab9d17828c 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm @@ -124,6 +124,11 @@ __PACKAGE__->register_method( signature => q/@see open-ils.circ.renew/, ); +__PACKAGE__->register_method( + method => "run_method", + api_name => "open-ils.circ.renew.auto", + signature => q/@see open-ils.circ.renew/, +); __PACKAGE__->register_method( method => "run_method", @@ -239,6 +244,7 @@ sub run_method { } $circulator->is_renewal(1) if $api =~ /renew/; + $circulator->is_autorenewal(1) if $api =~ /renew.auto/; $circulator->is_checkin(1) if $api =~ /checkin/; $circulator->is_checkout(1) if $api =~ /checkout/; $circulator->override(1) if $api =~ /override/o; @@ -282,7 +288,7 @@ sub run_method { $circulator->do_checkin(); } elsif( $api =~ /renew/ ) { - $circulator->do_renew(); + $circulator->do_renew($api); } if( $circulator->bail_out ) { @@ -427,6 +433,7 @@ my @AUTOLOAD_FIELDS = qw/ volume title is_renewal + is_autorenewal is_checkout is_res_checkout is_precat @@ -464,6 +471,7 @@ my @AUTOLOAD_FIELDS = qw/ recurring_fines_rule max_fine_rule renewal_remaining + auto_renewal_remaining hard_due_date due_date fulfilled_holds @@ -1395,6 +1403,7 @@ sub get_circ_policy { max_fine => $self->get_max_fine_amount($max_fine_rule), fine_interval => $recurring_fine_rule->recurrence_interval, renewal_remaining => $duration_rule->max_renewals, + auto_renewal_remaining => $duration_rule->max_auto_renewals, grace_period => $recurring_fine_rule->grace_period }; @@ -2118,6 +2127,7 @@ sub build_checkout_circ_object { $circ->max_fine($policy->{max_fine}); $circ->fine_interval($recurring->recurrence_interval); $circ->renewal_remaining($duration->max_renewals); + $circ->auto_renewal_remaining($duration->max_auto_renewals); $circ->grace_period($policy->{grace_period}); } else { @@ -2147,6 +2157,10 @@ sub build_checkout_circ_object { $circ->circ_staff($self->editor->requestor->id); } + if ( $self->is_autorenewal ){ + $circ->auto_renewal_remaining($self->auto_renewal_remaining); + $circ->auto_renewal('t'); + } # if the user provided an overiding checkout time, # (e.g. the checkout really happened several hours ago), then @@ -3970,6 +3984,7 @@ sub log_me { sub do_renew { my $self = shift; + my $api = shift; $self->log_me("do_renew()"); # Make sure there is an open circ to renew @@ -3992,14 +4007,17 @@ sub do_renew { $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED')) if $circ->renewal_remaining < 1; + $self->push_events(OpenILS::Event->new('MAX_AUTO_RENEWALS_REACHED')) + if $api =~ /renew.auto/ and $circ->auto_renewal_remaining < 1; # ----------------------------------------------------------------- $self->parent_circ($circ->id); $self->renewal_remaining( $circ->renewal_remaining - 1 ); + $self->auto_renewal_remaining( $circ->auto_renewal_remaining - 1 ) if (defined($circ->auto_renewal_remaining)); $self->circ($circ); # Opac renewal - re-use circ library from original circ (unless told not to) - if($self->opac_renewal) { + if($self->opac_renewal or $api =~ /renew.auto/) { unless(defined($opac_renewal_use_circ_lib)) { my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib'); if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) { diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/config.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/config.pm index e91ff207ea..c1c2f9cb98 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/config.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/config.pm @@ -52,7 +52,7 @@ package config::rules::circ_duration; use base qw/config/; __PACKAGE__->table('config_rule_circ_duration'); __PACKAGE__->columns(Primary => 'id'); -__PACKAGE__->columns(Essential => qw/name extended normal shrt max_renewals/); +__PACKAGE__->columns(Essential => qw/name extended normal shrt max_renewals max_auto_renewals/); #------------------------------------------------------------------------------- package config::rules::max_fine; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor/Circ/AutoRenew.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor/Circ/AutoRenew.pm new file mode 100644 index 0000000000..71eeb65883 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor/Circ/AutoRenew.pm @@ -0,0 +1,86 @@ +package OpenILS::Application::Trigger::Reactor::Circ::AutoRenew; +use strict; use warnings; +use Error qw/:try/; +use Data::Dumper; +use OpenSRF::Utils::SettingsClient; +use OpenILS::Application::Trigger::Reactor; +use OpenSRF::Utils::Logger qw/:logger/; +use OpenILS::Utils::CStoreEditor qw/:funcs/; +use OpenILS::Application::AppUtils; +my $AppUtils = 'OpenILS::Application::AppUtils'; + +use Encode; +$Data::Dumper::Indent = 0; + +use base 'OpenILS::Application::Trigger::Reactor'; + +my $log = 'OpenSRF::Utils::Logger'; + +sub ABOUT { + return <{target}; + my $svc = "open-ils.auth_internal"; + my $api = $svc . '.session.create'; + + my $auth_internal_svc = OpenSRF::AppSession->create($svc); + + my $userid = $circs->[0]->usr(); + # fetch user + my $userObj = new_editor()->retrieve_actor_user($userid); + my %args = + ( + user_id => $userid, + org_unit => $userObj->home_ou(), # all autorenewals occur from patron's Home OU. + login_type => "opac" + ); + + my $token = $auth_internal_svc->request($api, \%args)->gather(1)->{payload}->{authtoken}; + + # 2. carry out renewal: + my $ses = OpenSRF::AppSession->connect('open-ils.trigger'); + for (@$circs){ + + $logger->info( "AUTORENEW: circ.target_copy: " . Dumper($_->target_copy()) ); + my $evt = $AppUtils->simplereq( + 'open-ils.circ', + 'open-ils.circ.renew.auto', + $token, + { + patron_id => $_->usr(), + copy_id => $_->target_copy(), + opac_renewal => 0 + } + ); + + $evt = $evt->[0] if ref($evt) eq "ARRAY"; # we got two resp events, likely renewal errors, grab the first. + my $is_renewed = $evt->{textcode} eq 'SUCCESS' ? 1 : 0; + + my $new_circ_due = $is_renewed ? $evt->{payload}->{circ}->due_date : ''; + + my %user_data = ( + copy => $_->target_copy(), + is_renewed => $is_renewed, + reason => !$is_renewed ? sprintf("%s : %s", $evt->{textcode}, substr($evt->{desc}, 0, 140)) : '', + new_due_date => $is_renewed ? $evt->{payload}->{circ}->due_date : '', + old_due_date => !$is_renewed ? $_->due_date() : '', + ); + + $ses->request('open-ils.trigger.event.autocreate', 'autorenewal', $_, $_->circ_lib(), 'system_autorenewal', \%user_data); + } + + $ses->disconnect; + + return 1; +} + +1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator.pm index 1a53dfbc40..c45d1eae5f 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator.pm @@ -7,6 +7,8 @@ use OpenSRF::Utils::Logger qw/:logger/; use OpenILS::Const qw/:const/; use OpenILS::Application::AppUtils; use OpenILS::Utils::CStoreEditor qw/:funcs/; +use Data::Dumper; + sub fourty_two { return 42 } sub NOOP_True { return 1 } sub NOOP_False { return 0 } @@ -25,6 +27,7 @@ sub CircIsOpen { return 0 if (!$self->MinPassiveTargetAge($env)); } + $logger->info("AUTORENEW: CircIsOpen is TRUE!"); return 1; } @@ -188,5 +191,34 @@ sub PatronNotInCollections { return @$existing ? 0 : 1; } +# core type circ in $env->{target} +sub CircIsAutoRenewable { + my $self = shift; + my $env = shift; + + my $circ = $env->{target}; + my $userId = $env->{target}->usr; + # 1. check if circ is open + if (!$self->CircIsOpen($env)){ + return 0; + } + + # 2. Check if patron is barred + + my ($user, $res) = $U->fetch_user($userId); + if ( $U->is_true($user->barred()) ){ + + my %user_data = ( + is_renewed => 0, + reason => 'Please contact your library about your account.', + ); + + $U->create_events_for_hook('autorenewal', $circ, $user->home_ou(), 'system_autorenewal', \%user_data); + + return 0; + } + + return 1; +} 1; diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index d4ffab653a..841222555b 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -10291,6 +10291,58 @@ INSERT INTO action_trigger.hook (key,core_type,description) VALUES ( 'A hold is cancelled by the patron' ); +-- AUTORENEWAL Action Trigger definitions and email notification template + +ALTER TABLE config.rule_circ_duration +ADD column max_auto_renewals INTEGER; + +ALTER TABLE action.circulation +ADD column auto_renewal BOOLEAN; + +ALTER TABLE action.circulation +ADD column auto_renewal_remaining INTEGER; + +INSERT INTO action_trigger.validator values('CircIsAutoRenewable', 'Checks whether the circulation is able to be autorenewed.'); +INSERT INTO action_trigger.reactor values('Circ::AutoRenew', 'Auto-Renews a circulation.'); +INSERT INTO action_trigger.hook(key, core_type, description) values('autorenewal', 'circ', 'Item was auto-renewed to patron.'); + +-- AutoRenewer A/T Def: +INSERT INTO action_trigger.event_definition(active, owner, name, hook, validator, reactor, delay, max_delay, delay_field, group_field) + values (false, 1, 'Autorenew', 'checkout.due', 'NOOP_True', 'Circ::AutoRenew', '-23 hours'::interval,'-1 minute'::interval, 'due_date', 'usr'); + +-- AutoRenewal outcome Email notifier A/T Def: +INSERT INTO action_trigger.event_definition(active, owner, name, hook, validator, reactor, group_field, template) + values (false, 1, 'AutorenewNotify', 'autorenewal', 'NOOP_True', 'SendEmail', 'usr', + $$ + [%- USE date -%] + [%- user = target.0.usr -%] + To: [%- params.recipient_email || user.email %] + From: [%- params.sender_email || default_sender %] + Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %] + Subject: Items Out Auto-Renewal Notification Auto-Submitted: auto-generated + + Dear [% user.family_name %], [% user.first_given_name %] + Your library would like to let you know about your currently borrowed item(s): + + [% FOR circ IN target %] + [%- SET idx = loop.count - 1; SET udata = user_data.$idx -%] + Item# [%+ loop.count -%] + [%- SET cid = circ.target_copy || udata.copy -%] + [%- SET copy_details = helpers.get_copy_bib_basics(cid) +%] + Title: [% copy_details.title %] + Author: [% copy_details.author %] + Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %] + Status: [%- IF udata.is_renewed %] Loan Renewed. Now Due: [%- date.format(helpers.format_date(udata.new_due_date), '%Y-%m-%d') %] + [% ELSE %] Not Renewed. Reason: [% udata.reason %] [% END %] + [% END %] + $$ +); + +INSERT INTO action_trigger.environment (event_def, path ) VALUES +( currval('action_trigger.event_definition_id_seq'), 'usr' ), +( currval('action_trigger.event_definition_id_seq'), 'circ_lib' ); + +-- END of autorenwal trigger def stuff -- in-db indexing normalizers INSERT INTO config.index_normalizer (name, description, func, param_count) VALUES ( diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.autorenewals_acp_and_circ_duration.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.autorenewals_acp_and_circ_duration.sql new file mode 100644 index 0000000000..9fb38e307f --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.autorenewals_acp_and_circ_duration.sql @@ -0,0 +1,53 @@ +BEGIN; + -- SELECT evergreen.upgrade_deps_block_check('xxxx', :eg_version); + + ALTER TABLE config.rule_circ_duration + ADD column max_auto_renewals INTEGER; + + ALTER TABLE action.circulation + ADD column auto_renewal BOOLEAN; + + ALTER TABLE action.circulation + ADD column auto_renewal_remaining INTEGER; + + INSERT INTO action_trigger.validator values('CircIsAutoRenewable', 'Checks whether the circulation is able to be autorenewed.'); + INSERT INTO action_trigger.reactor values('Circ::AutoRenew', 'Auto-Renews a circulation.'); + INSERT INTO action_trigger.hook(key, core_type, description) values('autorenewal', 'circ', 'Item was auto-renewed to patron.'); + + -- AutoRenewer A/T Def: + INSERT INTO action_trigger.event_definition(active, owner, name, hook, validator, reactor, delay, max_delay, delay_field, group_field) + values (true, 1, 'Autorenew', 'checkout.due', 'NOOP_True', 'Circ::AutoRenew', '-23 hours'::interval,'-1 minute'::interval, 'due_date', 'usr'); + + -- AutoRenewal outcome Email notifier A/T Def: + INSERT INTO action_trigger.event_definition(active, owner, name, hook, validator, reactor, group_field, template) + values (true, 1, 'AutorenewNotify', 'autorenewal', 'NOOP_True', 'SendEmail', 'usr', + $$ + [%- USE date -%] + [%- user = target.0.usr -%] + To: [%- params.recipient_email || user.email %] + From: [%- params.sender_email || default_sender %] + Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %] + Subject: Items Out Auto-Renewal Notification Auto-Submitted: auto-generated + + Dear [% user.family_name %], [% user.first_given_name %] (UserID: [%- user.id +%]) + Your library would like to let you know about your currently borrowed item(s): + + [% FOR circ IN target %] + [%- SET idx = loop.count - 1; SET udata = user_data.$idx -%] + Item# [%+ loop.count -%] (circ_id: [%- circ.id -%]) + [%- SET cid = circ.target_copy || udata.copy -%] + [%- SET copy_details = helpers.get_copy_bib_basics(cid) +%] + Title: [% copy_details.title %] + Author: [% copy_details.author %] + Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %] + Status: [%- IF udata.is_renewed %] Loan Renewed. Now Due: [%- date.format(helpers.format_date(udata.new_due_date), '%Y-%m-%d') %] + [% ELSE %] Not Renewed. Reason: [% udata.reason %] [% END %] + [% END %] + $$ + ); + + INSERT INTO action_trigger.environment (event_def, path ) VALUES + ( currval('action_trigger.event_definition_id_seq'), 'usr' ), + ( currval('action_trigger.event_definition_id_seq'), 'circ_lib' ); + +COMMIT; diff --git a/Open-ILS/src/templates/conify/global/config/rule_circ_duration.tt2 b/Open-ILS/src/templates/conify/global/config/rule_circ_duration.tt2 index 1cdd497b08..aeff5eb40a 100644 --- a/Open-ILS/src/templates/conify/global/config/rule_circ_duration.tt2 +++ b/Open-ILS/src/templates/conify/global/config/rule_circ_duration.tt2 @@ -10,7 +10,7 @@