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