]> git.evergreen-ils.org Git - Evergreen.git/blob - Evergreen/src/support-scripts/eg_gen_overdue.pl
updated to not send mail notices to invalid addresses
[Evergreen.git] / Evergreen / src / support-scripts / eg_gen_overdue.pl
1 #!/usr/bin/perl
2 # ---------------------------------------------------------------
3 # Generates the overdue notices XML file
4 # ./eg_gen_overdue.pl <bootstap> 0
5 #               generates today's notices
6 # ./eg_gen_overdue.pl <bootstap> 1 0
7 #               generates notices for today - 1 and today
8 # ./eg_gen_overdue.pl <bootstap> 2 1 0  
9 # ./eg_gen_overdue.pl <bootstap> 3 2 1 0  etc...
10 # ---------------------------------------------------------------
11
12
13
14 use strict; use warnings;
15 require '../../../Open-ILS/src/support-scripts/oils_header.pl';
16 use vars qw/$logger $apputils/;
17 use Data::Dumper;
18 use OpenILS::Const qw/:const/;
19 use OpenILS::Application::AppUtils;
20 use DateTime;
21 use Email::Send;
22 use DateTime::Format::ISO8601;
23 use OpenSRF::Utils qw/:datetime/;
24 use Unicode::Normalize;
25 use OpenILS::Const qw/:const/;
26
27 my $U = 'OpenILS::Application::AppUtils';
28
29 my $SEND_EMAILS = 1;
30
31 my $bsconfig = shift || die "usage: $0 <bootstrap_config>\n";
32 my @goback = @ARGV;
33 @goback = (0) unless @goback;
34 osrf_connect($bsconfig);
35 my $e = OpenILS::Utils::CStoreEditor->new;
36
37 my $smtp = $ENV{EG_OVERDUE_SMTP_HOST};
38 my $mail_sender = $ENV{EG_OVERDUE_EMAIL_SENDER};
39
40 # ---------------------------------------------------------------
41 # Set up the email template
42 my $etmpl = $ENV{EG_OVERDUE_EMAIL_TEMPLATE};
43 my $email_template;
44 if( open(F,"$etmpl") ) {
45         my @etmpl = <F>;
46         $email_template = join('',@etmpl);
47         close(F);
48 }
49 # ---------------------------------------------------------------
50
51
52
53 my @date = CORE::localtime;
54 my $sec  = $date[0];
55 my $min  = $date[1];
56 my $hour = $date[2];
57 my $day  = $date[3];
58 my $mon  = $date[4] + 1;
59 my $year = $date[5] + 1900;
60
61 my %USER_CACHE;
62 my %ORG_CACHE;
63
64
65 print <<XML;
66 <?xml version='1.0' encoding='UTF-8'?>
67 <file type="notice" date="$mon/$day/$year" time="$hour:$min:$sec">
68         <agency name="PINES">
69 XML
70
71 print_notices($_) for @goback;
72
73 print <<XML;
74         </agency>
75 </file>
76 XML
77
78
79 # -----------------------------------------------------------------------
80 # -----------------------------------------------------------------------
81
82
83 sub print_notices {
84         my $goback = shift || 0;
85
86         for my $day ( qw/ 7 14 30 / ) {
87                 my ($start, $end) = make_date_range($day + $goback);
88
89                 $logger->info("OD_notice: process date range $start -> $end");
90
91                 my $query = [
92                         {
93                                 checkin_time => undef,
94                                 due_date => { between => [ $start, $end ] },
95                         },
96                         { order_by => { circ => 'usr, circ_lib' } }
97                 ];
98                 my $circs = $e->search_action_circulation($query, {idlist=>1});
99
100                 process_circs( $circs, "${day}day" );
101         }
102 }
103
104
105 sub process_circs {
106         my $circs = shift;
107         my $range = shift;
108
109         return unless @$circs;
110
111         $logger->info("OD_notice: processing range $range and ".scalar(@$circs)." potential circs");
112
113         my $org; 
114         my $patron;
115         my @current;
116
117         my $x = 0;
118         for my $circ (@$circs) {
119                 $circ = $e->retrieve_action_circulation($circ);
120
121                 if( !defined $org or 
122                                 $circ->circ_lib != $org  or $circ->usr ne $patron ) {
123                         $org = $circ->circ_lib;
124                         $patron = $circ->usr;
125                         print_notice( $range, \@current ) if @current;
126                         @current = ();
127                 }
128
129                 push( @current, $circ );
130                 $x++;
131         }
132
133         $logger->info("OD_notice: processed $x circs");
134         print_notice( $range, \@current );
135 }
136
137 sub make_date_range {
138         my $daysback = shift;
139
140         my $epoch = CORE::time - ($daysback * 24 * 60 * 60);
141         my $date = DateTime->from_epoch( epoch => $epoch, time_zone => 'local');
142
143         $date->set_hour(0);
144         $date->set_minute(0);
145         $date->set_second(0);
146         my $start = "$date";
147         
148         $date->set_hour(23);
149         $date->set_minute(59);
150         $date->set_second(59);
151
152         return ($start, "$date");
153 }
154
155
156 sub print_notice {
157         my( $range, $circs ) = @_;
158         return unless @$circs;
159
160         my $s1 = scalar(@$circs);
161         
162         # we don't charge for lost or claimsreturned
163         $circs = [ 
164                 grep {
165                         !$_->stop_fines or (
166                                 $_->stop_fines ne OILS_STOP_FINES_LOST and
167                                 $_->stop_fines ne OILS_STOP_FINES_CLAIMSRETURNED 
168                         )
169                 } @$circs 
170         ];
171
172         return unless @$circs;
173
174         my $s2 = $s1 - scalar(@$circs);
175         $logger->info("OD_notice: dropped $s2 lost/CR from processing...") if $s2;
176
177         my $org = $circs->[0]->circ_lib;
178         my $usr = $circs->[0]->usr;
179         $logger->debug("OD_notice: printing $range user:$usr org:$org");
180
181         my @patron_data = fetch_patron_data($usr);
182         my @org_data = fetch_org_data($org);
183
184         my $email;
185
186         if( $email = $patron_data[0]->email 
187                 and $email =~ /.+\@.+/ 
188                 and ($range eq '7day' or $range eq '14day') ) {
189
190                         send_email($range, \@patron_data, \@org_data, $circs);
191
192         } else {
193
194                 if( $patron_data[9] ) {
195
196                         print "\t\t<notice type='overdue' count='$range'>\n";
197                         print_patron_xml_chunk(@patron_data);
198                         print_org_xml_chunk(@org_data);
199                         print_circ_chunk($_) for @$circs;
200                         print "\t\t</notice>\n";
201
202                 } else {
203                         # There is no zip, therefore no address.
204                         $logger->warn("OD_notice: unable to send mail notification for $usr due to lack of valid address");
205                 }
206         }
207 }
208
209
210 sub fetch_patron_data {
211         my $user_id = shift;
212
213         my $patron = $USER_CACHE{$user_id};
214
215         if( ! $patron ) {
216                 $logger->debug("OD_notice:   fetching patron $user_id");
217
218                 $patron = $e->retrieve_actor_user(
219                         [
220                                 $user_id,
221                                 {
222                                         flesh => 1,
223                                         flesh_fields => { 
224                                                 'au' => [qw/ card billing_address mailing_address /] 
225                                         }
226                                 }
227                         ]
228                 ) or return handle_event($e->event);
229
230                 $USER_CACHE{$user_id} = $patron;
231         }
232
233         my $bc = $patron->card->barcode;
234         my $fn = $patron->first_given_name;
235         my $mn = $patron->second_given_name;
236         my $ln = $patron->family_name;
237
238         my ( $s1, $s2, $city, $state, $zip );
239
240         my $baddr = $patron->mailing_address;
241         unless( $baddr and $U->is_true($baddr->valid) ) {
242                 $baddr = $patron->billing_address;
243                 $baddr = undef unless( $baddr and $U->is_true($baddr->valid) );
244         }
245
246         if( $baddr ) {
247                 $s1             = $baddr->street1;
248                 $s2             = $baddr->street2;
249                 $city           = $baddr->city;
250                 $state  = $baddr->state;
251                 $zip            = $baddr->post_code;
252         }
253
254         $bc = entityize($bc);
255         $fn = entityize($fn);
256         $mn = entityize($mn);
257         $ln = entityize($ln);
258         $s1 = entityize($s1);
259         $s2 = entityize($s2);
260         $city  = entityize($city);
261         $state = entityize($state);
262         $zip     = entityize($zip);
263
264         return ( $patron, $bc, $fn, $mn, $ln, $s1, $s2, $city, $state, $zip );
265 }
266
267         
268 sub print_patron_xml_chunk {
269         my( $patron, $bc, $fn, $mn, $ln, $s1, $s2, $city, $state, $zip ) = @_;
270         my $pid = $patron->id;
271         print <<"       XML";
272                         <patron>
273                                 <id type="barcode">$bc</id>
274                                 <fullname>$fn $mn $ln</fullname>
275                                 <street1>$s1 $s2</street1>
276                                 <city_state_zip>$city, $state $zip</city_state_zip>
277                                 <sys_id>$pid</sys_id>
278                         </patron>
279         XML
280 }
281
282
283 sub fetch_org_data {
284         my $org_id = shift;
285
286         my $org = $ORG_CACHE{$org_id};
287
288         if( ! $org ) {
289                 $logger->debug("OD_notice:   fetching org $org_id");
290
291                 $org = $e->retrieve_actor_org_unit(
292                         [
293                                 $org_id,
294                                 {
295                                         flesh => 1, 
296                                         flesh_fields => 
297                                                 { aou => [ qw/billing_address mailing_address/ ] }
298                                 }
299                         ]
300                 ) or return handle_event($e->event);
301
302                 $ORG_CACHE{$org_id} = $org;
303         }
304
305         my $name = $org->name;
306         my $email = $org->email;
307         my $phone = $org->phone;
308
309         my( $s1, $s2, $city, $state, $zip );
310         my $baddr = $org->billing_address || $org->mailing_address;
311         if( $baddr ) {
312                 $s1             = $baddr->street1;
313                 $s2             = $baddr->street2;
314                 $city           = $baddr->city;
315                 $state  = $baddr->state;
316                 $zip            = $baddr->post_code;
317         }
318
319         $name  = entityize($name);
320         $phone = entityize($phone);
321         $s1      = entityize($s1);
322         $s2      = entityize($s2);
323         $city  = entityize($city);
324         $state = entityize($state);
325         $zip     = entityize($zip);
326         $email = entityize($email);
327
328         return ( $org, $name, $phone, $s1, $s2, $city, $state, $zip, $email );
329 }
330
331
332 sub print_org_xml_chunk {
333         my( $org, $name, $phone, $s1, $s2, $city, $state, $zip, $email ) = @_;
334         print <<"       XML";
335                         <library>
336                                 <libname>$name</libname>
337                                 <libphone>$phone</libphone>
338                                 <libstreet1>$s1 $s2</libstreet1>
339                                 <libcity_state_zip>$city, $state $zip</libcity_state_zip>
340                         </library>
341         XML
342 }
343
344
345 sub fetch_circ_data {
346         my $circ = shift;
347
348         my $title;
349         my $author;
350         my $cn;
351
352         my $d = $circ->due_date;
353         $d =~ s/[T ].*//og; # just for logging
354         $logger->debug("OD_notice:   processing circ ".$circ->id." $d");
355
356         my $due = DateTime::Format::ISO8601->new->parse_datetime(
357                 clense_ISO8601($circ->due_date));
358
359         my $day  = $due->day;
360         my $mon  = $due->month;
361         my $year = $due->year;
362
363         my $copy = $e->retrieve_asset_copy($circ->target_copy)
364                 or return handle_event($e->event);
365
366         my $bc = $copy->barcode;
367
368         if( $copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
369                 $title = $copy->dummy_title || "";
370                 $author = $copy->dummy_author || "";
371
372         } else {
373
374                 my $volume = $e->retrieve_asset_call_number(
375                         [
376                                 $copy->call_number,
377                                 {
378                                         flesh => 1,
379                                         flesh_fields => {
380                                                 acn => [ qw/record/ ]
381                                         }
382                                 }
383                         ]
384                 ) or return handle_event($e->event);
385
386                 $cn = $volume->label;
387                 my $mods = $apputils->record_to_mvr($volume->record);
388                 if( $mods ) {
389                         $title = $mods->title || "";
390                         $author = $mods->author || "";
391                 }
392         }
393
394         $title = entityize($title);
395         $author = entityize($author);
396         $cn = entityize($cn);
397         $bc = entityize($bc);
398
399         return( $title, $author, $cn, $bc, $day, $mon, $year );
400 }
401
402
403 sub print_circ_chunk {
404         my $circ = shift;
405         my ( $title, $author, $cn, $bc, $day, $mon, $year ) = fetch_circ_data($circ);
406         my $cid = $circ->id;
407         print <<"       XML";
408                         <item>
409                                 <title>$title</title>
410                                 <author>$author</author>
411                                 <duedate>$mon/$day/$year</duedate>
412                                 <callno>$cn</callno>
413                                 <barcode>$bc</barcode>
414                                 <circ_id>$cid</circ_id>
415                         </item>
416         XML
417 }
418
419
420
421 sub send_email {
422         my( $range, $patron_data, $org_data, $circs ) = @_;
423         my( $org, $org_name, $org_phone, $org_s1, $org_s2, $org_city, $org_state, $org_zip, $org_email ) = @$org_data;
424         my( $patron, $bc, $fn, $mn, $ln, $user_s1, $user_s2, $user_city, $user_state, $user_zip ) = @$patron_data;
425
426         return unless $SEND_EMAILS;
427
428         my $pemail = $patron_data->[0]->email;
429
430         my $tmpl = $email_template;
431         my @time = localtime;
432         my $year = $time[5] + 1900;
433         my $mon  = $time[4] + 1;
434         my $day  = $time[3];
435
436         my $r = ($range eq '7day') ? 7 : 14;
437
438         $org_email ||= $mail_sender;
439
440         $tmpl =~ s/\${EMAIL_RECIPIENT}/$pemail/;
441         $tmpl =~ s/\${EMAIL_SENDER}/$mail_sender/o;
442         $tmpl =~ s/\${EMAIL_REPLY_TO}/$org_email/;
443    $tmpl =~ s/\${EMAIL_HEADERS}//;
444
445    $tmpl =~ s/\${RANGE}/$r/;
446    $tmpl =~ s/\${DATE}/$mon\/$day\/$year/;
447    $tmpl =~ s/\${FIRST_NAME}/$fn/;
448    $tmpl =~ s/\${MIDDLE_NAME}/$mn/;
449    $tmpl =~ s/\${LAST_NAME}/$ln/;
450
451         my ($itmpl) = $tmpl =~ /\${OVERDUE_ITEMS\[(.*)\]}/ms;
452
453         my $items = '';
454         for my $circ (@$circs) {
455                 my $circtmpl = $itmpl;
456                 my ( $title, $author, $cn, $bc, $due_day, $due_mon, $due_year ) = fetch_circ_data($circ);
457                 $circtmpl =~ s/\${TITLE}/$title/o;
458                 $circtmpl =~ s/\${AUTHOR}/$author/o;
459                 $circtmpl =~ s/\${CALL_NUMBER}/$cn/o;
460                 $circtmpl =~ s/\${DUE_DAY}/$due_day/o;
461                 $circtmpl =~ s/\${DUE_MONTH}/$due_mon/o;
462                 $circtmpl =~ s/\${DUE_YEAR}/$due_year/o;
463                 $circtmpl =~ s/\${ITEM_BARCODE}/$bc/o;
464                 $items .= "$circtmpl\n";
465         }
466
467         $tmpl =~ s/\${OVERDUE_ITEMS\[.*\]}/$items/ms;
468
469         my $org_addr = "$org_s1 $org_s2 $org_city, $org_state $org_zip";
470         $tmpl =~ s/\${ORG_NAME}/$org_name/o;
471         $tmpl =~ s/\${ORG_ADDRESS}/$org_addr/o;
472         $tmpl =~ s/\${ORG_PHONE}/$org_phone/o;
473
474         $logger->debug("OD_notice: sending email to $pemail: $tmpl");
475
476         my $sender = Email::Send->new({mailer => 'SMTP'});
477         $sender->mailer_args([Host => $smtp]);
478
479         my $stat = $sender->send($tmpl);
480
481         if( $stat and $stat->type eq 'success' ) {
482                 $logger->info("OD_notice:   successfully sent overdue email");
483         } else {
484                 $logger->warn("OD_notice:   unable to send hold overdue email: ".Dumper($stat));
485         }
486
487         $logger->info("OD_notice:   sending email to".$patron_data->[0]->email);
488 }
489
490 sub handle_event {
491         my $evt = shift;
492         warn "OD_notice: ".Dumper($evt) . "\n";
493         $logger->error("OD_notice: ".Dumper($evt));
494 }
495
496
497 sub entityize {
498         my $stuff = shift || return "";
499         $stuff =~ s/\</&lt;/og;
500         $stuff =~ s/\>/&gt;/og;
501         $stuff =~ s/\&/&amp;/og;
502         $stuff = NFC($stuff);
503         $stuff =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
504         return $stuff;
505 }
506
507
508
509