From cfd01d2decb1021333b649c247fb8093264d5b7a Mon Sep 17 00:00:00 2001 From: Nick Clemens Date: Tue, 12 Jul 2016 13:16:45 -0400 Subject: [PATCH] Bug 15986: Add a script for sending hold waiting reminder notices 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 DAYS) 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 4 18 - Notices should be sent again 19 - perl misc/cronjobs/holds_reminder.pl -v -lettercode HOLDREMINDER -n -holidays -days 4 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 4 -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 4 -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/) --- C4/Letters.pm | 2 +- Koha/Patron.pm | 70 ++++++++++ misc/cronjobs/holds_reminder.pl | 293 ++++++++++++++++++++++++++++++++++++++++ t/db_dependent/Koha/Patrons.t | 95 ++++++++++++- 4 files changed, 458 insertions(+), 2 deletions(-) create mode 100755 misc/cronjobs/holds_reminder.pl diff --git a/C4/Letters.pm b/C4/Letters.pm index 0233ef52f4..cd830a4feb 100644 --- a/C4/Letters.pm +++ b/C4/Letters.pm @@ -48,7 +48,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 ); } diff --git a/Koha/Patron.pm b/Koha/Patron.pm index e6d7538640..96eaa6d08d 100644 --- a/Koha/Patron.pm +++ b/Koha/Patron.pm @@ -28,6 +28,7 @@ use Unicode::Normalize; use C4::Context; use C4::Log; use Koha::Account; +use C4::Letters qw( GetPreparedLetter EnqueueLetter ); use Koha::AuthUtils; use Koha::Checkouts; use Koha::Club::Enrollments; @@ -1632,6 +1633,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 index 0000000000..01a1063bb8 --- /dev/null +++ b/misc/cronjobs/holds_reminder.pl @@ -0,0 +1,293 @@ +#!/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 . + +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 ][ -library ... ] + [ -days ][ -csv [] ][ -itemscontent ] + [ -email ... ] + + Options: + -help brief help message + -man full documentation + -v verbose + -n No email will be sent + -days days waiting to deal with + -lettercode predefined notice to use + -library 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 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 - With no arguments the simple help is printed + +C 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 - sends no email and +populates F with information about all waiting holds +items. + +C - prepare notices of +holds waiting for 2 weeks for the MAIN library. + +C - 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 ); + $waiting_date = $calendar->addDays($date_to_run,-$days); #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 diff --git a/t/db_dependent/Koha/Patrons.t b/t/db_dependent/Koha/Patrons.t index a275f03c0a..00f884cc9d 100644 --- a/t/db_dependent/Koha/Patrons.t +++ b/t/db_dependent/Koha/Patrons.t @@ -19,7 +19,7 @@ use Modern::Perl; -use Test::More tests => 4; +use Test::More tests => 5; use Test::Warn; use Test::Exception; use Test::MockModule; @@ -43,6 +43,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; @@ -2066,4 +2067,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; -- 2.11.0