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