Bug 15986: Add a script for sending hold waiting reminder notices
authorNick Clemens <nick@bywatersolutions.com>
Tue, 12 Jul 2016 17:16:45 +0000 (13:16 -0400)
committerJonathan Druart <jonathan.druart@bugs.koha-community.org>
Fri, 16 Apr 2021 12:15:37 +0000 (14:15 +0200)
This patch adds a script for sending holds reminder notice to patrons.

We add a 'send_notice' routine to Koha::Patrons - this will either send using the patron's
email prefs, or allow forcing of a single method via the cron

To test:
 1 - Create an email hold reminder notice for a single library (Koha module: Holds, code HOLDREMINDER, branch: CPL)
 2 - Set some waiting holds today for patrons at CPL, ensure those patrons have 'email' as the transport for hold filled notices
 3 - perl misc/cronjobs/holds_reminder.pl -v -lettercode HOLDREMINDER -n -li CPL
 4 - You should see the patrons here would have received emails
 5 - perl misc/cronjobs/holds_reminder.pl -v -lettercode HOLDREMINDER -li CPL
 6 - You should see the emails that were sent
 7 - Check the patron notices tab to confirm
 8 - Note a ptron with two holds waiting receives only one notice
 9 - perl misc/cronjobs/holds_reminder.pl -v -lettercode HOLDREMINDER -li CPL -days 3
10 - No notices are sent
11 - Adjust the waiting date for the holds:
    UPDATE reserves SET waitingdate=DATE_SUB(CURDATE(), INTERVAL 3 DAY) WHERE waitingdate = CURDATE();
12 - perl misc/cronjobs/holds_reminder.pl -v -lettercode HOLDREMINDER -li CPL -days 3
13 - Confirm the holds are now reminded
14 - Set yesterday as a holiday for CPL
15 - perl misc/cronjobs/holds_reminder.pl -v -lettercode HOLDREMINDER -n -li CPL -holidays -days 3
16 - Notices should not be sent
17 - perl misc/cronjobs/holds_reminder.pl -v -lettercode HOLDREMINDER -n -li CPL -holidays -days 2
18 - Notices should be sent again
19 - perl misc/cronjobs/holds_reminder.pl -v -lettercode HOLDREMINDER -n -holidays -days 2
20 - Should get feedback that notice was not found for other libraries
21 - perl misc/cronjobs/holds_reminder.pl -v -lettercode HOLDREMINDER -n -holidays -days 2 -mtt sms
22 - Notice is not found
23 - Add the notice for sms
24 - perl misc/cronjobs/holds_reminder.pl -v -lettercode HOLDREMINDER -n -holidays -days 2 -mtt sms
25 - The notice should be sent
26 - Check patrons messaging tab to confirm
27 - prove -v t/db_dependent/Koha/Patrons.t

Sponsored by: The Hotchkiss School (http://www.hotchkiss.org/)

Signed-off-by: Kim Gnerre <kgnerre@hotchkiss.org>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
C4/Letters.pm
Koha/Patron.pm
misc/cronjobs/holds_reminder.pl [new file with mode: 0755]
t/db_dependent/Koha/Patrons.t

index 28b90fd..fe34c66 100644 (file)
@@ -49,7 +49,7 @@ BEGIN {
     require Exporter;
     @ISA = qw(Exporter);
     @EXPORT = qw(
-        &GetLetters &GetLettersAvailableForALibrary &GetLetterTemplates &DelLetter &GetPreparedLetter &GetWrappedLetter &SendAlerts &GetPrintMessages &GetMessageTransportTypes
+        &EnqueueLetter &GetLetters &GetLettersAvailableForALibrary &GetLetterTemplates &DelLetter &GetPreparedLetter &GetWrappedLetter &SendAlerts &GetPrintMessages &GetMessageTransportTypes
     );
 }
 
index a538533..06c4a34 100644 (file)
@@ -29,6 +29,7 @@ use C4::Context;
 use C4::Log;
 use Koha::Account;
 use Koha::ArticleRequests;
+use C4::Letters qw( GetPreparedLetter EnqueueLetter );
 use Koha::AuthUtils;
 use Koha::Checkouts;
 use Koha::Club::Enrollments;
@@ -1809,6 +1810,75 @@ sub to_api_mapping {
     };
 }
 
+=head3 send_notice
+
+    Koha::Patrons->send_notice({ letter_params => $letter_params, message_name => 'DUE'});
+    Koha::Patrons->send_notice({ letter_params => $letter_params, message_transports => \@message_transports });
+    Koha::Patrons->send_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
+
+    Queue messages to a patron. Can pass a message that is part of the message_attributes
+    table or supply the transport to use.
+
+    If passed a message name we retrieve the patrons preferences for transports
+    Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
+    we have no address/number for sending
+
+    $letter_params is a hashref of the values to be passed to GetPreparedLetter
+
+    test_mode will only report which notices would be sent, but nothign will be queued
+
+=cut
+
+sub send_notice {
+    my ( $self, $params ) = @_;
+    my $letter_params = $params->{letter_params};
+    my $test_mode = $params->{test_mode};
+
+    return unless $letter_params;
+    return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
+
+    my $library = Koha::Libraries->find( $letter_params->{branchcode} )->unblessed;
+    my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
+
+    my @message_transports;
+    my $letter_code;
+    $letter_code = $letter_params->{letter_code};
+    if( $params->{message_name} ){
+        my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
+                borrowernumber => $letter_params->{borrowernumber},
+                message_name => $params->{message_name}
+        } );
+        @message_transports = ( keys %{ $messaging_prefs->{transports} } );
+        $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
+    } else {
+        @message_transports = @{$params->{message_transports}};
+    }
+    return unless defined $letter_code;
+    $letter_params->{letter_code} = $letter_code;
+    my $print_sent = 0;
+    my %return;
+    foreach my $mtt (@message_transports){
+        next if ($mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
+        # Phone notices are handled by TalkingTech_itiva_outbound.pl
+        if( ($mtt eq 'email' and not $self->notice_email_address) or ($mtt eq 'sms' and not $self->smsalertnumber) ){
+            push @{$return{fallback}}, $mtt;
+            $mtt = 'print';
+        }
+        next if $mtt eq 'print' && $print_sent;
+        $letter_params->{message_transport_type} = $mtt;
+        my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
+        C4::Letters::EnqueueLetter({
+            letter => $letter,
+            borrowernumber => $self->borrowernumber,
+            from_address   => $admin_email_address,
+            message_transport_type => $mtt
+        }) unless $test_mode;
+        push @{$return{sent}}, $mtt;
+        $print_sent = 1 if $mtt eq 'print';
+    }
+    return \%return;
+}
+
 =head2 Internal methods
 
 =head3 _type
diff --git a/misc/cronjobs/holds_reminder.pl b/misc/cronjobs/holds_reminder.pl
new file mode 100755 (executable)
index 0000000..c409e40
--- /dev/null
@@ -0,0 +1,294 @@
+#!/usr/bin/perl
+
+# This file is part of Koha.
+#
+# Koha 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 3 of the License, or
+# (at your option) any later version.
+#
+# Koha 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+BEGIN {
+
+    # find Koha's Perl modules
+    # test carefully before changing this
+    use FindBin;
+    eval { require "$FindBin::Bin/../kohalib.pl" };
+}
+
+use Getopt::Long;
+use Pod::Usage;
+use Text::CSV_XS;
+use DateTime;
+use DateTime::Duration;
+
+use C4::Context;
+use C4::Letters;
+use C4::Log;
+use Koha::DateUtils;
+use Koha::Calendar;
+use Koha::Libraries;
+use Koha::Script -cron;
+
+=head1 NAME
+
+holds_reminder.pl - prepare reminder messages to be sent to patrons with waiting holds
+
+=head1 SYNOPSIS
+
+holds_reminder.pl
+  [ -n ][ -library <branchcode> ][ -library <branchcode> ... ]
+  [ -days <number of days> ][ -csv [<filename>] ][ -itemscontent <field list> ]
+  [ -email <email_type> ... ]
+
+ Options:
+   -help                          brief help message
+   -man                           full documentation
+   -v                             verbose
+   -n                             No email will be sent
+   -days          <days>          days waiting to deal with
+   -lettercode   <lettercode>     predefined notice to use
+   -library      <branchname>     only deal with holds from this library (repeatable : several libraries can be given)
+   -holidays                      use the calendar to not count holidays as waiting days
+   -mtt          <message_transport_type> type of messages to send, default is to use patrons messaging preferences for Hold filled
+                                  populating this will force send even if patron has not chosen to receive hold notices
+                                  email and sms will fallback to print if borrower does not have an address/phone
+   -date                          Send notices as would have been sent on a specific date
+
+=head1 OPTIONS
+
+=over 8
+
+=item B<-help>
+
+Print a brief help message and exits.
+
+=item B<-man>
+
+Prints the manual page and exits.
+
+=item B<-v>
+
+Verbose. Without this flag set, only fatal errors are reported.
+
+=item B<-n>
+
+Do not send any email (test-mode) . If verbose a list of notices that would have been sent to
+the patrons are printed to standard out.
+
+=item B<-days>
+
+Optional parameter, number of days an items has been 'waiting' on hold
+to send a message for. If not included a notice will be sent to all
+patrons with waiting holds.
+
+=item B<-library>
+
+select notices for one specific library. Use the value in the
+branches.branchcode table. This option can be repeated in order
+to select notices for a group of libraries.
+
+=item B<-holidays>
+
+This option determines whether library holidays are used when calculating how
+long an item has been waiting. If enabled the count will skip closed days.
+
+=item B<-date>
+
+use it in order to send notices on a specific date and not Now. Format: YYYY-MM-DD.
+
+=item B<-mtt>
+
+send a notices via a specific transport, this can be repeated to send various notices.
+If omitted the patron's messaging preferences for Hold notices will be used.
+If supplied the notice types will be force sent even if patron has not selected hold notices
+Email and SMS will fall back to print if there is no valid info in the patron's account
+
+
+=back
+
+=head1 DESCRIPTION
+
+This script is designed to alert patrons of waiting
+holds.
+
+=head2 Configuration
+
+This script sends reminders to patrons with waiting holds using a notice
+defined in the Tools->Notices & slips module within Koha. The lettercode
+is passed into this script and, along with other options, determine the content
+of the notices sent to patrons.
+
+
+=head1 USAGE EXAMPLES
+
+C<holds_reminder.pl> - With no arguments the simple help is printed
+
+C<holds_reminder.pl -lettercode CODE > In this most basic usage all
+libraries are processed individually, and notices are prepared for
+all patrons with waiting holds for whom we have email addresses.
+Messages for those patrons for whom we have no email
+address are sent in a single attachment to the library administrator's
+email address, or to the address in the KohaAdminEmailAddress system
+preference.
+
+C<holds_reminder.pl -lettercode CODE -n -csv /tmp/holds_reminder.csv> - sends no email and
+populates F</tmp/holds_reminder.csv> with information about all waiting holds
+items.
+
+C<holds_reminder.pl -lettercode CODE -library MAIN -days 14> - prepare notices of
+holds waiting for 2 weeks for the MAIN library.
+
+C<holds_reminder.pl -library MAIN -days 14 -list-all> - prepare notices
+of holds waiting for 2 weeks for the MAIN library and include all the
+patron's waiting hold
+
+=cut
+
+# These variables are set by command line options.
+# They are initially set to default values.
+my $dbh = C4::Context->dbh();
+my $help    = 0;
+my $man     = 0;
+my $verbose = 0;
+my $nomail  = 0;
+my $days    ;
+my $lettercode;
+my @branchcodes; # Branch(es) passed as parameter
+my $use_calendar = 0;
+my $date_input;
+my $opt_out = 0;
+my @mtts;
+
+GetOptions(
+    'help|?'         => \$help,
+    'man'            => \$man,
+    'v'              => \$verbose,
+    'n'              => \$nomail,
+    'days=s'         => \$days,
+    'lettercode=s'   => \$lettercode,
+    'library=s'      => \@branchcodes,
+    'date=s'         => \$date_input,
+    'holidays'       => \$use_calendar,
+    'mtt=s'          => \@mtts
+);
+pod2usage(1) if $help;
+pod2usage( -verbose => 2 ) if $man;
+
+if ( !$lettercode ) {
+    pod2usage({
+        -exitval => 1,
+        -msg => qq{\nError: You must specify a lettercode to send reminders.\n},
+    });
+}
+
+
+cronlogaction();
+
+# Unless a delay is specified by the user we target all waiting holds
+unless (defined $days) {
+    $days=0;
+}
+
+# Unless one ore more branchcodes are passed we use all the branches
+if (scalar @branchcodes > 0) {
+    my $branchcodes_word = scalar @branchcodes > 1 ? 'branches' : 'branch';
+    $verbose and warn "$branchcodes_word @branchcodes passed on parameter\n";
+}
+else {
+    @branchcodes = Koha::Libraries->search()->get_column('branchcode');
+}
+
+# If provided we run the report as if it had run on a specified date
+my $date_to_run;
+if ( $date_input ){
+    eval {
+        $date_to_run = dt_from_string( $date_input, 'iso' );
+    };
+    die "$date_input is not a valid date, aborting! Use a date in format YYYY-MM-DD."
+        if $@ or not $date_to_run;
+}
+else {
+    $date_to_run = dt_from_string();
+}
+
+# Loop through each branch
+foreach my $branchcode (@branchcodes) { #BEGIN BRANCH LOOP
+    # Check that this branch has the letter code specified or skip this branch
+    my $letter = C4::Letters::getletter( 'reserves', $lettercode , $branchcode );
+    unless ($letter) {
+        $verbose and print qq|Message '$lettercode' content not found for $branchcode\n|;
+        next;
+    }
+
+    # If respecting calendar get the correct waiting since date
+    my $waiting_date;
+    if( $use_calendar ){
+        my $calendar = Koha::Calendar->new( branchcode => $branchcode );
+        my $duration = DateTime::Duration->new( days => -$days );
+        $waiting_date = $calendar->addDays($date_to_run,$duration); #Add negative of days
+    } else {
+        $waiting_date = $date_to_run->subtract( days => $days );
+    }
+
+    # Find all the holds waiting since this date for the current branch
+    my $dtf = Koha::Database->new->schema->storage->datetime_parser;
+    my $waiting_since = $dtf->format_date( $waiting_date );
+    my $reserves = Koha::Holds->search({
+        waitingdate => {'<=' => $waiting_since },
+        branchcode  => $branchcode,
+    });
+
+    $verbose and warn "No reserves found for $branchcode\n" unless $reserves->count;
+    next unless $reserves->count;
+    $verbose and warn $reserves->count . " reserves waiting since $waiting_since for $branchcode\n";
+
+    # We only want to send one notice per patron per branch - this variable will hold the completed borrowers
+    my %done;
+
+    # If passed message transports we force use those, otherwise we will use the patrons preferences
+    # for the 'Hold_Filled' notice
+    my $sending_params = @mtts ? { message_transports => \@mtts } : { message_name => "Hold_Filled" };
+
+
+    while ( my $reserve = $reserves->next ) {
+
+        my $patron = $reserve->borrower;
+        # Skip if we already dealt with this borrower
+        next if ( $done{$patron->borrowernumber} );
+        $verbose and print "  borrower " . $patron->surname . ", " . $patron->firstname . " has holds triggering notice.\n";
+
+        # Setup the notice information
+        my $letter_params = {
+            module          => 'reserves',
+            letter_code     => $lettercode,
+            borrowernumber  => $patron->borrowernumber,
+            branchcode      => $branchcode,
+            tables          => {
+                 borrowers  => $patron->borrowernumber,
+                 branches   => $reserve->branchcode,
+                 reserves   => $reserve->unblessed
+            },
+        };
+        $sending_params->{letter_params} = $letter_params;
+        $sending_params->{test_mode} = $nomail;
+        my $result_text = $nomail ? "would have been sent" : "was sent";
+        # send_notice queues the notices, falling back to print for email or SMS, and ignores phone (they are handled by Itiva)
+        my $result = $patron->send_notice( $sending_params );
+        $verbose and print "   borrower " . $patron->surname . ", " . $patron->firstname . " $result_text notices via: @{$result->{sent}}\n" if defined $result->{sent};
+        $verbose and print "   borrower " . $patron->surname . ", " . $patron->firstname . " $result_text print fallback for: @{$result->{fallback}}\n" if defined $result->{fallback};
+        # Mark this borrower as completed
+        $done{$patron->borrowernumber} = 1;
+    }
+
+
+} #END BRANCH LOOP
index e2bc736..0b310dd 100755 (executable)
@@ -44,6 +44,7 @@ use Koha::Patron::Relationship;
 use Koha::Database;
 use Koha::DateUtils;
 use Koha::Virtualshelves;
+use Koha::Notice::Messages;
 
 use t::lib::TestBuilder;
 use t::lib::Mocks;
@@ -1979,4 +1980,96 @@ subtest 'anonymize' => sub {
     $patron2->discard_changes; # refresh
     is( $patron2->firstname, undef, 'First name patron2 cleared' );
 };
+
+subtest 'send_notice' => sub {
+    plan tests => 11;
+
+    my $dbh = C4::Context->dbh;
+    t::lib::Mocks::mock_preference( 'AutoEmailPrimaryAddress', 'email' );
+    my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
+    my $branch = $builder->build_object( { class => 'Koha::Libraries' } );
+    my $letter_e = $builder->build_object( {
+        class => 'Koha::Notice::Templates',
+        value => {
+            branchcode => $branch->branchcode,
+            message_transport_type => 'email',
+            lang => 'default'
+        }
+    });
+    my $letter_p = $builder->build_object( {
+        class => 'Koha::Notice::Templates',
+        value => {
+            code => $letter_e->code,
+            module => $letter_e->module,
+            branchcode => $branch->branchcode,
+            message_transport_type => 'print',
+            lang => 'default'
+        }
+    });
+    my $letter_s = $builder->build_object( {
+        class => 'Koha::Notice::Templates',
+        value => {
+            code => $letter_e->code,
+            module => $letter_e->module,
+            branchcode => $branch->branchcode,
+            message_transport_type => 'sms',
+            lang => 'default'
+        }
+    });
+
+    my $letter_params = {
+        letter_code => $letter_e->code,
+        branchcode  => $letter_e->branchcode,
+        module      => $letter_e->module,
+        borrowernumber => $patron->borrowernumber,
+        tables => {
+            borrowers => $patron->borrowernumber,
+        }
+    };
+    my @mtts = ('email');
+
+    is( $patron->send_notice(), undef, "Nothing is done if no params passed");
+    is( $patron->send_notice({ letter_params => $letter_params }),undef, "Nothing done if only letter");
+    is_deeply(
+        $patron->send_notice({ letter_params => $letter_params, message_transports => \@mtts }),
+        {sent => ['email'] }, "Email sent"
+    );
+    $patron->email("")->store;
+    is_deeply(
+        $patron->send_notice({ letter_params => $letter_params, message_transports => \@mtts }),
+        {sent => ['print'],fallback => ['email']}, "Email fallsback to print if no email"
+    );
+    push @mtts, 'sms';
+    is_deeply(
+        $patron->send_notice({ letter_params => $letter_params, message_transports => \@mtts }),
+        {sent => ['print','sms'],fallback => ['email']}, "Email fallsback to print if no email, sms sent"
+    );
+    $patron->smsalertnumber("")->store;
+    my $counter = Koha::Notice::Messages->search({borrowernumber => $patron->borrowernumber })->count;
+    is_deeply(
+        $patron->send_notice({ letter_params => $letter_params, message_transports => \@mtts }),
+        {sent => ['print'],fallback => ['email','sms']}, "Email fallsback to print if no emai, sms fallsback to print if no sms, only one print sent"
+    );
+    is( Koha::Notice::Messages->search({borrowernumber => $patron->borrowernumber })->count, $counter+1,"Count of queued notices went up by one");
+
+    # Enable notification for Hold_Filled - Things are hardcoded here but should work with default data
+    $dbh->do(q|INSERT INTO borrower_message_preferences( borrowernumber, message_attribute_id ) VALUES ( ?, ?)|, undef, $patron->borrowernumber, 4 );
+    my $borrower_message_preference_id = $dbh->last_insert_id(undef, undef, "borrower_message_preferences", undef);
+    $dbh->do(q|INSERT INTO borrower_message_transport_preferences( borrower_message_preference_id, message_transport_type) VALUES ( ?, ? )|, undef, $borrower_message_preference_id, 'email' );
+
+    is( $patron->send_notice({ letter_params => $letter_params, message_transports => \@mtts, message_name => 'Hold_Filled' }),undef, "Nothing done if transports and name sent");
+
+    $patron->email(q|awesome@ismymiddle.name|)->store;
+    is_deeply(
+        $patron->send_notice({ letter_params => $letter_params, message_name => 'Hold_Filled' }),
+        {sent => ['email'] }, "Email sent when using borrower preferences"
+    );
+    $counter = Koha::Notice::Messages->search({borrowernumber => $patron->borrowernumber })->count;
+    is_deeply(
+        $patron->send_notice({ letter_params => $letter_params, message_name => 'Hold_Filled', test_mode => 1 }),
+        {sent => ['email'] }, "Report that email sent when using borrower preferences in test_mode"
+    );
+    is( Koha::Notice::Messages->search({borrowernumber => $patron->borrowernumber })->count, $counter,"Count of queued notices not increased in test mode");
+};
+
 $schema->storage->txn_rollback;