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