LP#1779920 - Autorenew Feature
authorCesar Velez <cesar.velez@equinoxinitiative.org>
Tue, 28 Aug 2018 20:13:51 +0000 (16:13 -0400)
committerGalen Charlton <gmc@equinoxinitiative.org>
Tue, 4 Sep 2018 17:52:48 +0000 (13:52 -0400)
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 <cesar.velez@equinoxinitiative.org>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/extras/ils_events.xml
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/config.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor/Circ/AutoRenew.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator.pm
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.autorenewals_acp_and_circ_duration.sql [new file with mode: 0644]
Open-ILS/src/templates/conify/global/config/rule_circ_duration.tt2

index 9436ceb..c9a60dd 100644 (file)
@@ -3446,6 +3446,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field name="name" reporter:datatype="text"/>
                        <field name="normal" reporter:datatype="interval"/>
                        <field name="shrt" reporter:datatype="interval"/>
                        <field name="name" reporter:datatype="text"/>
                        <field name="normal" reporter:datatype="interval"/>
                        <field name="shrt" reporter:datatype="interval"/>
+                       <field name="max_auto_renewals" reporter:datatype="int" />
                </fields>
                <links>
                </links>
                </fields>
                <links>
                </links>
@@ -4562,6 +4563,8 @@ SELECT  usr,
                        <field reporter:label="Shelving Location" name="copy_location" reporter:datatype="link"/>
                        <field reporter:label="Archived Patron Stat-Cat Entries" name="aaactsc_entries" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Archived Copy Stat-Cat Entries" name="aaasc_entries" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Shelving Location" name="copy_location" reporter:datatype="link"/>
                        <field reporter:label="Archived Patron Stat-Cat Entries" name="aaactsc_entries" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Archived Copy Stat-Cat Entries" name="aaasc_entries" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Auto Renewal" name="auto_renewal" reporter:datatype="bool"/>
+                       <field reporter:label="Remaining Auto Renewals" name="auto_renewal_remaining" reporter:datatype="int" />
                </fields>
                <links>
                        <link field="billable_transaction" reltype="might_have" key="id" map="" class="mbt"/>
                </fields>
                <links>
                        <link field="billable_transaction" reltype="might_have" key="id" map="" class="mbt"/>
index 473b2d6..9f92944 100644 (file)
        <event code='7028' textcode='PATRON_CIRC_MISMATCH'>
                <desc xml:lang="en-US">Potentially notified patron does not own the circulation.</desc>
        </event>
        <event code='7028' textcode='PATRON_CIRC_MISMATCH'>
                <desc xml:lang="en-US">Potentially notified patron does not own the circulation.</desc>
        </event>
+       <event code='7029' textcode='MAX_AUTO_RENEWALS_REACHED'>
+               <desc xml:lang="en-US">Circulation has no more auto-renewals remaining</desc>
+       </event>
 
 
        <!-- ================================================================ -->
 
 
        <!-- ================================================================ -->
index c2d5518..ab9d178 100644 (file)
@@ -124,6 +124,11 @@ __PACKAGE__->register_method(
     signature => q/@see open-ils.circ.renew/,
 );
 
     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",
 
 __PACKAGE__->register_method(
     method  => "run_method",
@@ -239,6 +244,7 @@ sub run_method {
     }
 
     $circulator->is_renewal(1) if $api =~ /renew/;
     }
 
     $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;
     $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_checkin();
 
     } elsif( $api =~ /renew/ ) {
-        $circulator->do_renew();
+        $circulator->do_renew($api);
     }
 
     if( $circulator->bail_out ) {
     }
 
     if( $circulator->bail_out ) {
@@ -427,6 +433,7 @@ my @AUTOLOAD_FIELDS = qw/
     volume
     title
     is_renewal
     volume
     title
     is_renewal
+    is_autorenewal
     is_checkout
     is_res_checkout
     is_precat
     is_checkout
     is_res_checkout
     is_precat
@@ -464,6 +471,7 @@ my @AUTOLOAD_FIELDS = qw/
     recurring_fines_rule
     max_fine_rule
     renewal_remaining
     recurring_fines_rule
     max_fine_rule
     renewal_remaining
+    auto_renewal_remaining
     hard_due_date
     due_date
     fulfilled_holds
     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,
         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
     };
 
         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->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 {
         $circ->grace_period($policy->{grace_period});
 
     } else {
@@ -2147,6 +2157,10 @@ sub build_checkout_circ_object {
       $circ->circ_staff($self->editor->requestor->id);
    }
 
       $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
 
     # 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;
 
 sub do_renew {
     my $self = shift;
+    my $api = shift;
     $self->log_me("do_renew()");
 
     # Make sure there is an open circ to renew
     $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_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->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)
     $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)) {
         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)) {
index e91ff20..c1c2f9c 100644 (file)
@@ -52,7 +52,7 @@ package config::rules::circ_duration;
 use base qw/config/;
 __PACKAGE__->table('config_rule_circ_duration');
 __PACKAGE__->columns(Primary => 'id');
 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;
 #-------------------------------------------------------------------------------
 
 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 (file)
index 0000000..71eeb65
--- /dev/null
@@ -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 <<ABOUT;
+This Autorenew reactor will auto renew a circulation on the day it is due.
+ABOUT
+}
+
+sub handler {
+    my $self = shift;
+    my $env = shift;
+
+    # 1. get a session token for circ user
+
+    my $circs = $env->{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;
index 1a53dfb..c45d1ea 100644 (file)
@@ -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 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 }
 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));
     }
 
         return 0 if (!$self->MinPassiveTargetAge($env));
     }
 
+    $logger->info("AUTORENEW: CircIsOpen is TRUE!");
     return 1;
 }
 
     return 1;
 }
 
@@ -188,5 +191,34 @@ sub PatronNotInCollections {
     return @$existing ? 0 : 1;
 }
 
     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;
 
 1;
index d4ffab6..8412225 100644 (file)
@@ -10291,6 +10291,58 @@ INSERT INTO action_trigger.hook (key,core_type,description) VALUES (
     'A hold is cancelled by the patron'
 );
 
     '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 (
 
 -- 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 (file)
index 0000000..9fb38e3
--- /dev/null
@@ -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;
index 1cdd497..aeff5eb 100644 (file)
@@ -10,7 +10,7 @@
     </div>
     <table  jsId="ruleCircDurationGrid"
             dojoType="openils.widget.AutoGrid"
     </div>
     <table  jsId="ruleCircDurationGrid"
             dojoType="openils.widget.AutoGrid"
-            fieldOrder="['name', 'max_renewals', 'shrt', 'normal', 'extended']"
+            fieldOrder="['name', 'max_renewals', 'max_auto_renewals', 'shrt', 'normal', 'extended']"
             suppressFields="['id']"
             query="{id: '*'}"
             fmClass='crcd'
             suppressFields="['id']"
             query="{id: '*'}"
             fmClass='crcd'