LP#1552778: copy some date/time utils from OpenSRF
[working/Evergreen.git] / Open-ILS / src / offline / offline.pl
1 #!/usr/bin/perl
2 use strict; use warnings;
3 use CGI;
4 use OpenSRF::Utils::JSON;
5 use OpenSRF::System;
6 use OpenSRF::Utils::Logger qw/$logger/;
7 use OpenILS::Application::AppUtils;
8 use OpenILS::Event;
9 use OpenSRF::EX qw/:try/;
10 use Data::Dumper;
11 use OpenILS::Utils::Fieldmapper;
12 use Digest::MD5 qw/md5_hex/;
13 use OpenSRF::Utils qw/:daemon/;
14 use OpenILS::Utils::DateTime qw/clean_ISO8601/;
15 use OpenILS::Utils::OfflineStore;
16 use OpenSRF::Utils::SettingsClient;
17 use OpenSRF::Utils;
18 use DateTime;
19 use DateTime::Format::ISO8601;
20
21 use DBI;
22 $DBI::trace = 1;
23
24 my $U = "OpenILS::Application::AppUtils";
25 my $DB = "OpenILS::Utils::OfflineStore";
26 my $SES = "${DB}::Session";
27 my $SCRIPT = "OpenILS::Utils::OfflineStore::Script";
28 my $user_groups;
29
30 # Used by the functionality that produces SKIP_ASSET_CHANGED events
31 my %seen_barcode = ();
32 my %skip_barcode_for_status_changed = ();
33
34 # --------------------------------------------------------------------
35 # Load the config
36 # --------------------------------------------------------------------
37 our %config;
38 do '##CONFIG##/offline-config.pl';
39
40
41 my $cgi                 = new CGI;
42 my $basedir             = $config{base_dir} || die "Offline config error: no base_dir defined\n";
43 my $bootstrap   = $config{bootstrap} || die "Offline config error: no bootstrap defined\n";
44 my $webclient   = $cgi->param('wc');
45 my $wsname              = $cgi->param('ws');
46 my $org                 = $cgi->param('org');
47 my $authtoken   = $cgi->param('ses') || "";
48 my $seskey              = $cgi->param('seskey');
49 my $action              = $cgi->param('action'); # - create, load, execute, status
50 my $requestor; 
51 my $wsobj;
52 my $orgobj;
53 my $evt;
54
55
56 &ol_init;
57 &ol_runtime_init;
58 &ol_do_action;
59
60
61 # --------------------------------------------------------------------
62 # Set it all up
63 # This function should behave as a child_init might behave in case 
64 # this is moved to mod_perl
65 # --------------------------------------------------------------------
66 sub ol_init {
67         $DB->DBFile($config{dsn}, $config{usr}, $config{pw});
68         ol_connect();
69 }
70
71 sub ol_connect {
72         OpenSRF::System->bootstrap_client(config_file => $bootstrap ); 
73         Fieldmapper->import(IDL => 
74                 OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
75
76 }
77
78
79 sub _ol_debug_params {
80         my $s = "";
81         my @params = $cgi->param;
82         @params = sort { $a cmp $b } @params;
83         $s .= "$_=" . $cgi->param($_) . "\n" for @params;
84         $s =~ s/\n$//o;
85         warn '-'x60 ."\n$s\n";
86 }
87
88
89 # --------------------------------------------------------------------
90 # Finds the requestor and other info specific to this request
91 # --------------------------------------------------------------------
92 sub ol_runtime_init {
93
94         # fetch the requestor object
95         ($requestor, $evt) = $U->checkses($authtoken);
96         ol_handle_result($evt) if $evt;
97
98         # try the param, the workstation, and finally the user's ws org
99         if(!$org) { 
100                 $wsobj = ol_fetch_workstation($wsname);
101                 $org = $wsobj->owning_lib if $wsobj;
102                 $org = $requestor->ws_ou unless $org;
103                 ol_handle_result(OpenILS::Event->new('OFFLINE_NO_ORG')) unless $org;
104         }
105
106     $user_groups = $U->simplereq(
107         'open-ils.actor', 'open-ils.actor.groups.retrieve');
108 }
109
110
111 # --------------------------------------------------------------------
112 # Runs the requested action
113 # --------------------------------------------------------------------
114 sub ol_do_action {
115
116         my $payload;
117
118         if( $action eq 'create' ) {
119                 
120                 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_UPLOAD');
121                 ol_handle_result($evt) if $evt;
122                 $payload = ol_create_session();
123
124         } elsif( $action eq 'load' ) {
125
126                 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_UPLOAD');
127                 ol_handle_result($evt) if $evt;
128                 $payload = ol_load();
129
130         } elsif( $action eq 'execute' ) {
131
132                 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_EXECUTE');
133                 ol_handle_result($evt) if $evt;
134                 $payload = ol_execute();
135
136         } elsif( $action eq 'status' ) {
137
138                 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_VIEW');
139                 ol_handle_result($evt) if $evt;
140                 $payload = ol_status();
141         }
142
143         ol_handle_event('SUCCESS', payload => $payload );
144 }
145
146
147 # --------------------------------------------------------------------
148 # Creates a new session
149 # --------------------------------------------------------------------
150 sub ol_create_session {
151
152         my $desc = $cgi->param('desc') || "";
153         $seskey = time . "_${$}_" . int(rand() * 1000);
154
155         $logger->debug("offline: user ".$requestor->id.
156                 " creating new session with key $seskey and description $desc");
157
158         $SES->create(
159                 {       
160                         key                             => $seskey,
161                         org                             => $org,
162                         description             => $desc,
163                         creator                 => $requestor->id,
164                         create_time             => CORE::time(), 
165                         num_complete    => 0,
166                 } 
167         );
168
169         return $seskey;
170 }
171
172
173 # --------------------------------------------------------------------
174 # Holds the meta-info for a script file
175 # --------------------------------------------------------------------
176 sub ol_create_script {
177         my $count = shift;
178
179         my $session = ol_find_session($seskey);
180         my $delta = $cgi->param('delta') || 0;
181
182         my $script = $session->add_to_scripts( 
183                 {
184                         requestor       => $requestor->id,
185                         create_time     => CORE::time,
186                         workstation     => $wsname,
187                         logfile         => "$basedir/pending/$org/$seskey/$wsname.log",
188                         time_delta      => $delta,
189                         count                   => $count,
190                 }
191         );
192 }
193
194 # --------------------------------------------------------------------
195 # Finds the current session in the db
196 # --------------------------------------------------------------------
197 sub ol_find_session {
198         my $ses = $SES->retrieve($seskey);
199         ol_handle_event('OFFLINE_INVALID_SESSION', payload => $seskey) unless $ses;
200         return $ses;
201 }
202
203 # --------------------------------------------------------------------
204 # Finds a script object in the DB based on workstation and seskey
205 # --------------------------------------------------------------------
206 sub ol_find_script {
207         my $ws = shift || $wsname;
208         my $sk = shift || $seskey;
209         my ($script) = $SCRIPT->search( session => $seskey, workstation => $ws );
210         return $script;
211 }
212
213 # --------------------------------------------------------------------
214 # Creates a new script in the database and loads the new script file
215 # --------------------------------------------------------------------
216 sub ol_load {
217
218         my $session = ol_find_session;
219         my $handle      = $cgi->upload('file');
220         my $outdir      = "$basedir/pending/$org/$seskey";
221         my $outfile = "$outdir/$wsname.log";
222
223         ol_handle_event('OFFLINE_SESSION_FILE_EXISTS') if ol_find_script();
224         ol_handle_event('OFFLINE_SESSION_ACTIVE') if $session->in_process;
225         ol_handle_event('OFFLINE_SESSION_COMPLETE') if $session->end_time;
226
227         qx/mkdir -p $outdir/;
228         my $x = 0;
229         open(FILE, ">>$outfile") or ol_handle_event('OFFLINE_FILE_ERROR');
230         while( <$handle> ) { print FILE; $x++;}
231         close(FILE);
232
233         ol_create_script($x);
234
235         return undef;
236 }
237
238
239 # --------------------------------------------------------------------
240 sub ol_handle_result {
241         my $obj = shift;
242         my $json = OpenSRF::Utils::JSON->perl2JSON($obj);
243
244         # Clear this so it's not remembered
245         $evt = undef;
246
247         if( $cgi->param('html')) {
248                 my $html = "<html><body onload='xulG.handle_event($json)'></body></html>";
249                 print "content-type: text/html\n\n";
250                 print "$html\n";
251
252         } else {
253
254                 print "content-type: text/plain\n\n";
255                 print "$json\n";
256         }
257
258         exit(0);
259 }
260
261 # --------------------------------------------------------------------
262 sub ol_handle_event {
263         my( $evt, @args ) = @_;
264         ol_handle_result(OpenILS::Event->new($evt, @args));
265 }
266
267
268 # --------------------------------------------------------------------
269 sub ol_flesh_session {
270         my $session = shift;
271         my %data;
272
273         map { $data{$_} = $session->$_ } $session->columns;
274         $data{scripts} = [];
275
276         for my $script ($session->scripts) {
277                 my %sdata;
278                 map { $sdata{$_} = $script->$_ } $script->columns;
279
280                 # the client doesn't need this info
281                 delete $sdata{session};
282                 delete $sdata{id};
283                 delete $sdata{logfile};
284
285                 push( @{$data{scripts}}, \%sdata );
286         }
287
288         return \%data;
289 }
290
291
292 # --------------------------------------------------------------------
293 # Returns various information on the sessions and scripts
294 # --------------------------------------------------------------------
295 sub ol_status {
296
297         my $type = $cgi->param('status_type') || "scripts";
298
299         # --------------------------------------------------------------------
300         # Returns info on every script upload attached to the current session
301         # --------------------------------------------------------------------
302         if( $type eq 'scripts' ) {
303                 my $session = ol_find_session();
304                 ol_handle_result(ol_flesh_session($session));
305
306
307         # --------------------------------------------------------------------
308         # Returns all scripts and sessions for the given org
309         # --------------------------------------------------------------------
310         } elsif( $type eq 'sessions' ) {
311                 my @sessions = $SES->search( org => $org );
312
313                 # can I do this in the DB without raw SQL?
314                 @sessions = sort { $a->create_time <=> $b->create_time } @sessions; 
315                 my @data;
316                 push( @data, ol_flesh_session($_) ) for @sessions;
317                 ol_handle_result(\@data);
318
319
320         # --------------------------------------------------------------------
321         # Returns total commands and completed commands counts
322         # --------------------------------------------------------------------
323         } elsif( $type eq 'summary' ) {
324                 my $session = ol_find_session();
325
326                 $logger->debug("offline: retrieving summary info ".
327                         "for session ".$session->key." with completed=".$session->num_complete);
328
329                 my $count = 0;
330                 $count += $_->count for ($session->scripts);
331                 ol_handle_result(
332                         { total => $count, num_complete => $session->num_complete });
333
334
335
336         # --------------------------------------------------------------------
337         # Returns the list of non-SUCCESS events that have occurred so far for 
338         # this set of commands
339         # --------------------------------------------------------------------
340         } elsif( $type eq 'exceptions' ) {
341
342                 my $session = ol_find_session();
343                 my $resfile = "$basedir/pending/$org/$seskey/results";
344                 if( $session->end_time ) {
345                         $resfile = "$basedir/archive/$org/$seskey/results";
346                 }
347                 my $data = ol_file_to_perl($resfile);
348         my $data2 = [];
349         for my $d (@$data) {
350             my $evt = $d->{event};
351             $evt = $evt->[0] if ref $evt eq 'ARRAY';
352             push(@$data2, $d) if $evt->{ilsevent} ne '0';
353         }
354                 #$data = [ grep { $_->{event}->{ilsevent} ne '0' } @$data ];
355                 ol_handle_result($data2);
356         }
357 }
358
359
360 sub ol_fetch_workstation {
361         my $name = shift;
362         $logger->debug("offline: Fetching workstation $name");
363         my $ws = $U->storagereq(
364                 'open-ils.storage.direct.actor.workstation.search.name', $name);
365         ol_handle_result(OpenILS::Event->new('ACTOR_WORKSTATION_NOT_FOUND')) unless $ws;
366         return $ws;
367 }
368
369
370
371
372 # --------------------------------------------------------------------
373 # Sorts the script commands then forks a child to executes them.
374 # --------------------------------------------------------------------
375 sub ol_execute {
376
377         my $session = ol_find_session();
378         ol_handle_event('OFFLINE_SESSION_ACTIVE') if $session->in_process;
379         ol_handle_event('OFFLINE_SESSION_COMPLETE') if $session->end_time;
380
381         my $commands = ol_collect_commands();
382
383         # --------------------------------------------------------------------
384         # Note that we must disconnect from opensrf before forking or the 
385         # connection will be borked...
386         # --------------------------------------------------------------------
387         OpenSRF::Transport::PeerHandle->retrieve->disconnect;
388         $DB->disconnect;
389
390
391         if( safe_fork() ) {
392
393                 # --------------------------------------------------------------------
394                 # Tell the client all is well
395                 # --------------------------------------------------------------------
396                 ol_handle_event('SUCCESS'); # - this exits
397
398         } else {
399
400
401                 # --------------------------------------------------------------------
402                 # close stdout/stderr or apache will wait on the child to finish
403                 # --------------------------------------------------------------------
404                 close(STDOUT);
405                 close(STDERR);
406
407                 $logger->debug("offline: child $$ processing data...");
408
409                 # --------------------------------------------------------------------
410                 # The child re-connects to the opensrf network and processes
411                 # the script requests 
412                 # --------------------------------------------------------------------
413                 OpenSRF::System->bootstrap_client(config_file => $bootstrap);
414         
415                 try {
416
417                         #use Class::DBI
418                         #Class::DBI->autoupdate(1);
419
420                         $DB->autoupdate(1);
421
422                         my $sesion = ol_find_session();
423                         $session->in_process(1);
424                         ol_process_commands($session, $commands);
425                         ol_archive_files($session);
426
427                 } catch Error with {
428                         my $e = shift;
429                         $logger->error("offline: child process error $e");
430                 };
431         }
432 }
433
434 sub ol_file_to_perl {
435         my $fname = shift;
436         open(F, "$fname") or ol_handle_event('OFFLINE_FILE_ERROR');
437         my @d = <F>;
438         my @p;
439         push(@p, OpenSRF::Utils::JSON->JSON2perl($_)) for @d;
440         close(F);
441         return \@p;
442 }
443
444 # collects the commands and sorts them on timestamp+delta
445 sub ol_collect_commands {
446         my $ses = ol_find_session();
447         my @commands;
448
449         # cycle through every script loaded to this session
450         for my $script ($ses->scripts) {
451                 my $coms = ol_file_to_perl($script->logfile);
452
453                 # cycle through all of the commands for this script
454                 for my $com (@$coms) {
455                         $$com{_workstation} = $script->workstation;
456                         $$com{_realtime} = $script->time_delta + $com->{timestamp};
457                         push( @commands, $com );
458                 }
459         }
460
461         # make sure thera are no blank commands
462         @commands = grep { ($_ and $_->{type}) } @commands;
463
464         # sort on realtime
465         @commands = sort { $a->{_realtime} <=> $b->{_realtime} } @commands;
466
467         # push user registrations to the front
468         my @regs                = grep { $_->{type} eq 'register' } @commands;
469         my @others      = grep { $_->{type} ne 'register' } @commands;
470
471         return [ @regs, @others ];
472 }
473
474 sub ol_date {
475         my $time = shift || CORE::time;
476         my (undef,undef, undef, $mday,$mon,$year) = localtime($time);
477         $mon++; $year   += 1900;
478         $mday   = "0$mday" unless $mday =~ /\d{2}/o;
479         $mon    = "0$mon" unless $mon   =~ /\d{2}/o;
480         return ($year, $mon, $mday);
481 }
482
483
484 # --------------------------------------------------------------------
485 # Moves all files from the pending directory to the archive directory
486 # and removes the pending directory
487 # --------------------------------------------------------------------
488 sub ol_archive_files {
489         my $session = shift;
490         my ($y, $m, $d) = ol_date();
491
492         my $dir = "$basedir/pending/$org/$seskey";
493         my $archdir = "$basedir/archive/$org/$seskey";
494         $logger->debug("offline: archiving files to $archdir");
495
496         # Tell the db the files are moving
497         $_->logfile($archdir.'/'.$_->workstation.'.log') for ($session->scripts);
498
499         qx/mkdir -p $archdir/;
500         qx/mv $_ $archdir/ for <$dir/*>;
501         qx/rmdir $dir/;
502 }
503
504
505 # --------------------------------------------------------------------
506 # Appends results to the results file.
507 # --------------------------------------------------------------------
508 my $rhandle;
509 sub ol_append_result {
510
511         my $obj = shift;
512         my $last = shift;
513
514         $obj = OpenSRF::Utils::JSON->perl2JSON($obj);
515
516         if(!$rhandle) {
517                 open($rhandle, ">>$basedir/pending/$org/$seskey/results") 
518                         or ol_handle_event('OFFLINE_FILE_ERROR');
519         }
520
521         print $rhandle "$obj\n";
522         close($rhandle) if $last;
523 }
524
525
526
527 # --------------------------------------------------------------------
528 # Runs the commands and returns the list of errors
529 # --------------------------------------------------------------------
530 sub ol_process_commands {
531
532         my $session      = shift;
533         my $commands = shift;
534         my $x        = 0;
535
536         $session->start_time(CORE::time);
537
538         for my $d ( @$commands ) {
539
540                 my $t           = $d->{type};
541                 my $last = ($x++ == scalar(@$commands) - 1) ? 1 : 0;
542                 my $res = { command => $d };
543                 my $err;
544
545                 while( 1 ) {
546
547                         $err = undef;
548                         $logger->debug("offline: top of execute loop : $t");
549
550                         try {
551                                 $res->{event} = ol_handle_checkin($d)   if $t eq 'checkin';
552                                 $res->{event} = ol_handle_inhouse($d)   if $t eq 'in_house_use';
553                                 $res->{event} = ol_handle_checkout($d) if $t eq 'checkout';
554                                 $res->{event} = ol_handle_renew($d)             if $t eq 'renew';
555                                 $res->{event} = ol_handle_register($d) if $t eq 'register';
556         
557                         } catch Error with { $err = shift; };
558
559                         if( $err ) {
560
561                                 if( ref($err) eq 'OpenSRF::EX::JabberDisconnected' ) {
562                                         $logger->error("offline: we lost jabber .. trying to reconnect");
563                                         ol_connect();
564
565                                 } else {
566                                         $res->{event} = OpenILS::Event->new('INTERNAL_SERVER_ERROR', debug => "$err");
567                                         last;
568                                 }
569
570                         } else { last; }
571
572                         sleep(1);
573                 }
574
575                 ol_append_result($res, $last);
576                 $session->num_complete( $session->num_complete + 1 );
577
578                 $logger->debug("offline: set session [".$session->key."] num_complete to ".$session->num_complete);
579         }
580
581         $session->end_time(CORE::time);
582         $session->in_process(0);
583 }
584
585
586 # --------------------------------------------------------------------
587 # Runs an in_house_use action
588 # --------------------------------------------------------------------
589 sub ol_handle_inhouse {
590
591         my $command             = shift;
592         my $realtime    = $command->{_realtime};
593         my $ws                  = $command->{_workstation};
594         my $barcode             = $command->{barcode};
595         my $count               = $command->{count} || 1;
596         my $use_time    = $command->{use_time} || "";
597
598         $logger->activity("offline: in_house_use : requestor=". $requestor->id.", realtime=$realtime, ".  
599                 "workstation=$ws, barcode=$barcode, count=$count, use_time=$use_time");
600
601         if( $count > 99 ) {
602                 return OpenILS::Event->new(
603                         'INTERNAL_SERVER_ERROR', payload => 'TOO MANY IN HOUSE USE');
604         }
605
606         my $ids = $U->simplereq(
607                 'open-ils.circ', 
608                 'open-ils.circ.in_house_use.create', $authtoken, 
609                 { barcode => $barcode, count => $count, location => $org, use_time => $use_time } );
610         
611         return OpenILS::Event->new('SUCCESS', payload => $ids) if( ref($ids) eq 'ARRAY' );
612         return $ids;
613 }
614
615
616
617 # --------------------------------------------------------------------
618 # Pulls the relevant circ args from the command, fetches data where 
619 # necessary
620 # --------------------------------------------------------------------
621 my %user_id_cache;
622 sub ol_circ_args_from_command {
623         my $command = shift;
624
625         my $type                        = $command->{type};
626         my $realtime    = $command->{_realtime};
627         my $ws                  = $command->{_workstation};
628         my $barcode             = $command->{barcode} || "";
629         my $cotime              = $command->{checkout_time} || "";
630         my $pbc                 = $command->{patron_barcode};
631         my $due_date    = $command->{due_date} || "";
632         my $noncat              = ($command->{noncat}) ? "yes" : "no"; # for logging
633
634         $logger->activity("offline: $type : requestor=". $requestor->id.
635                 ", realtime=$realtime, workstation=$ws, checkout_time=$cotime, ".
636                 "patron=$pbc, due_date=$due_date, noncat=$noncat");
637
638
639         my $args = { 
640                 permit_override => 1, 
641                 barcode         => $barcode,            
642                 checkout_time   => $cotime, 
643                 patron_barcode  => $pbc,
644                 due_date        => $due_date 
645     };
646
647     if(ol_get_org_setting('circ.offline.username_allowed')) {
648
649         my $format = ol_get_org_setting('opac.barcode_regex');
650         if($format) {
651
652             $format =~ s/^\/|\/$//g; # remove any couching //'s
653             if($pbc !~ qr/$format/) {
654
655                 # the patron barcode does not match the configured barcode format
656                 # assume they passed a username instead
657
658                 my $user_id = $user_id_cache{$pbc} ||
659                     $U->simplereq(
660                         'open-ils.actor', 
661                         'open-ils.actor.username.exists', 
662                         $authtoken, $pbc);
663
664                 
665                 if($user_id) {
666                     # a valid username was provided, update the args and cache
667                     $user_id_cache{$pbc} = $user_id;
668                     $args->{patron_id} = $user_id;
669                     delete $args->{patron_barcode};
670                 }
671             }
672         }
673     }
674
675
676         if( $command->{noncat} ) {
677                 $args->{noncat} = 1;
678                 $args->{noncat_type} = $command->{noncat_type};
679                 $args->{noncat_count} = $command->{noncat_count};
680         }
681
682         return $args;
683 }
684
685 sub ol_get_org_setting {
686     my $name = shift;
687     return $U->simplereq(
688         'open-ils.actor',
689         'open-ils.actor.ou_setting.ancestor_default',
690         $org, $name, $authtoken);
691 }
692
693
694
695 # --------------------------------------------------------------------
696 # Performs a checkout action
697 # --------------------------------------------------------------------
698 sub ol_handle_checkout {
699         my $command     = shift;
700         my $args = ol_circ_args_from_command($command);
701
702         if( $args->{noncat} and $args->{noncat_count} > 99 ) {
703                 return OpenILS::Event->new(
704                         'INTERNAL_SERVER_ERROR', payload => 'TOO MANY NON CATS');
705         }
706
707     if( $args->{barcode} ) {
708
709         # $c becomes the Copy
710         # $e possibily becomes the Exception
711         my( $c, $e ) = $U->fetch_copy_by_barcode($args->{barcode});
712         return $e if $e;
713
714         my $barcode = $args->{barcode};
715         # Have to have this config option (or org setting) and a
716         # status_changed_time for skippage, and barcode not seen before
717         if ((
718                 ol_get_org_setting(
719                     'circ.offline.skip_checkout_if_newer_status_changed_time'
720                 )
721                 || $config{skip_late}
722             )
723             && length($c->status_changed_time())
724             && ! $seen_barcode{$barcode}
725         ) {
726             $seen_barcode{$barcode} = 1;
727             my $cts = DateTime::Format::ISO8601->parse_datetime( clean_ISO8601($c->status_changed_time()) )->epoch();
728             my $xts = $command->{timestamp}; # Transaction Time Stamp
729             $logger->activity("offline: ol_handle_checkout: considering status_changed_time for barcode=$barcode, cts=$cts, xts=$xts");
730
731             # Asset has changed after this transaction, ignore
732             if ($cts >= $xts) {
733                 $skip_barcode_for_status_changed{$barcode} = 1;
734             }
735         } else {
736             $logger->activity("No skip check: barcode=$barcode seen_barcode=".$seen_barcode{$1}." status_changed_time=".$c->status_changed_time." ou_setting=".ol_get_org_setting('circ.offline.skip_checkout_if_newer_status_changed_time'));
737         }
738         if ($skip_barcode_for_status_changed{$barcode}) {
739             $logger->activity("offline: ol_handle_checkout: barcode=$barcode has SKIP_ASSET_CHANGED");
740             return OpenILS::Event->new(
741                 'SKIP_ASSET_CHANGED'
742             );
743         }
744     }
745
746     my $evt = $U->simplereq(
747                 'open-ils.circ', 'open-ils.circ.checkout', $authtoken, $args );
748
749     # if the item is already checked out to this user and we are past 
750     # the configured auto-renewal interval, try to renew the circ.
751     if( ref $evt ne 'ARRAY' and
752         $evt->{textcode} == 'OPEN_CIRCULATION_EXISTS' and 
753         $evt->{payload}->{auto_renew}) {
754
755             return ol_handle_renew($command);
756     }
757
758     return $evt;
759 }
760
761
762 # --------------------------------------------------------------------
763 # Performs the renewal action
764 # --------------------------------------------------------------------
765 sub ol_handle_renew {
766         my $command = shift;
767         my $args = ol_circ_args_from_command($command);
768         my $t = time;
769
770     if( $args->{barcode} ) {
771
772         # $c becomes the Copy
773         # $e possibily becomes the Exception
774         my( $c, $e ) = $U->fetch_copy_by_barcode($args->{barcode});
775         return $e if $e;
776
777         my $barcode = $args->{barcode};
778         # Have to have this config option (or org setting) and a
779         # status_changed_time for skippage, and barcode not seen before
780         if ((
781                 ol_get_org_setting(
782                     'circ.offline.skip_renew_if_newer_status_changed_time'
783                 )
784                 || $config{skip_late}
785             )
786             && length($c->status_changed_time())
787             && ! $seen_barcode{$barcode}
788         ) {
789             $seen_barcode{$barcode} = 1;
790             my $cts = DateTime::Format::ISO8601->parse_datetime( clean_ISO8601($c->status_changed_time()) )->epoch();
791             my $xts = $command->{timestamp}; # Transaction Time Stamp
792             $logger->activity("offline: ol_handle_renew: considering status_changed_time for barcode=$barcode, cts=$cts, xts=$xts");
793
794             # Asset has changed after this transaction, ignore
795             if ($cts >= $xts) {
796                 $skip_barcode_for_status_changed{$barcode} = 1;
797             }
798         } else {
799             $logger->activity("No skip check: barcode=$barcode seen_barcode=".$seen_barcode{$1}." status_changed_time=".$c->status_changed_time." ou_setting=".ol_get_org_setting('circ.offline.skip_renew_if_newer_status_changed_time'));
800         }
801         if ($skip_barcode_for_status_changed{$barcode}) {
802             $logger->activity("offline: ol_handle_renew: barcode=$barcode has SKIP_ASSET_CHANGED");
803             return OpenILS::Event->new(
804                 'SKIP_ASSET_CHANGED'
805             );
806         }
807     }
808
809         return $U->simplereq(
810                 'open-ils.circ', 'open-ils.circ.renew.override', $authtoken, $args );
811 }
812
813
814 # --------------------------------------------------------------------
815 # Runs a checkin action
816 # --------------------------------------------------------------------
817 sub ol_handle_checkin {
818
819         my $command             = shift;
820         my $realtime    = $command->{_realtime};
821         my $ws                  = $command->{_workstation};
822         my $barcode             = $command->{barcode};
823         my $backdate    = $command->{backdate} || "";
824
825         $logger->activity("offline: checkin : requestor=". $requestor->id.
826                 ", realtime=$realtime, ".  "workstation=$ws, barcode=$barcode, backdate=$backdate");
827
828     if( $barcode ) {
829
830         # $c becomes the Copy
831         # $e possibily becomes the Exception
832         my( $c, $e ) = $U->fetch_copy_by_barcode($barcode);
833         return $e if $e;
834
835         # Have to have this config option (or org setting) and a
836         # status_changed_time for skippage, and barcode not seen before
837         if ((
838                 ol_get_org_setting(
839                     'circ.offline.skip_checkin_if_newer_status_changed_time'
840                 )
841                 || $config{skip_late}
842             )
843             && length($c->status_changed_time())
844             && ! $seen_barcode{$barcode}
845         ) {
846             $seen_barcode{$barcode} = 1;
847             my $cts = DateTime::Format::ISO8601->parse_datetime( clean_ISO8601($c->status_changed_time()) )->epoch();
848             my $xts = $command->{timestamp}; # Transaction Time Stamp
849             $logger->activity("offline: ol_handle_checkin: considering status_changed_time for barcode=$barcode, cts=$cts, xts=$xts");
850
851             # Asset has changed after this transaction, ignore
852             if ($cts >= $xts) {
853                 $skip_barcode_for_status_changed{$barcode} = 1;
854             }
855         } else {
856             $logger->activity("No skip check: barcode=$barcode seen_barcode=".$seen_barcode{$1}." status_changed_time=".$c->status_changed_time." ou_setting=".ol_get_org_setting('circ.offline.skip_checkin_if_newer_status_changed_time'));
857         }
858         if ($skip_barcode_for_status_changed{$barcode}) {
859             $logger->activity("offline: ol_handle_checkin: barcode=$barcode has SKIP_ASSET_CHANGED");
860             return OpenILS::Event->new(
861                 'SKIP_ASSET_CHANGED'
862             );
863         }
864     }
865
866         return $U->simplereq(
867                 'open-ils.circ', 
868                 'open-ils.circ.checkin', $authtoken,
869                 { barcode => $barcode, backdate => $backdate } );
870 }
871
872
873
874 # --------------------------------------------------------------------
875 # Registers a new patron
876 # --------------------------------------------------------------------
877 sub ol_handle_register {
878         my $command = shift;
879
880         my $barcode = $command->{user}->{card}->{barcode};
881         delete $command->{user}->{card}; 
882         delete $command->{user}->{cards} if $command->{user}->{cards}; 
883
884         $logger->info("offline: creating new user with barcode $barcode");
885
886         # now, create the user
887         my $actor       = Fieldmapper::actor::user->new;
888         my $card                = Fieldmapper::actor::card->new;
889
890
891         # username defaults to the barcode
892         $actor->usrname( ($actor->usrname) ? $actor->usrname : $barcode );
893
894         # Set up all of the virtual IDs, isnew, etc.
895         $actor->isnew(1);
896         $actor->id(-1);
897         $actor->card(-1);
898         $actor->cards([$card]);
899
900         $card->isnew(1);
901         $card->id(-1);
902         $card->usr(-1);
903         $card->barcode($barcode);
904
905         my $billing_address;
906         my $mailing_address;
907
908         my @sresp;
909         for my $resp (@{$command->{user}->{survey_responses}}) {
910                 my $sr = Fieldmapper::action::survey_response->new;
911                 $sr->$_( $resp->{$_} ) for keys %$resp;
912                 $sr->isnew(1);
913                 $sr->usr(-1);
914                 push(@sresp, $sr);
915                 $logger->debug("offline: created new survey response for survey ".$sr->survey);
916         }
917         delete $command->{user}->{survey_responses};
918         $actor->survey_responses(\@sresp) if @sresp;
919
920     my $bid = undef;
921         # extract the billing address
922         if( my $addr = $command->{user}->{billing_address} ) {
923         $bid = $command->{user}->{billing_address}->{id};
924                 $billing_address = Fieldmapper::actor::user_address->new;
925                 $billing_address->$_($addr->{$_}) for keys %$addr;
926                 $billing_address->isnew(1);
927                 $billing_address->id(-1);
928                 $billing_address->usr(-1);
929                 delete $command->{user}->{billing_address};
930                 $logger->debug("offline: read billing address ".$billing_address->street1);
931         }
932
933     my $mid = undef;
934         # extract the mailing address
935         if( my $addr = $command->{user}->{mailing_address} ) {
936         $mid = $command->{user}->{mailing_address}->{id};
937         if ($webclient && $mid != $bid) {
938                     $mailing_address = Fieldmapper::actor::user_address->new;
939                     $mailing_address->$_($addr->{$_}) for keys %$addr;
940                     $mailing_address->isnew(1);
941                     $mailing_address->id(-2);
942                     $mailing_address->usr(-1);
943                     $logger->debug("offline: read mailing address ".$mailing_address->street1);
944         } elsif (!$webclient) {
945                     $mailing_address = Fieldmapper::actor::user_address->new;
946                     $mailing_address->$_($addr->{$_}) for keys %$addr;
947                     $mailing_address->isnew(1);
948                     $mailing_address->id(-2);
949                     $mailing_address->usr(-1);
950                     $logger->debug("offline: read mailing address ".$mailing_address->street1);
951         }
952                 delete $command->{user}->{mailing_address};
953         }
954
955         # make sure we have values for both
956         $billing_address ||= $mailing_address;
957         $mailing_address ||= $billing_address;
958
959         $actor->billing_address($billing_address->id);
960         $actor->mailing_address($mailing_address->id);
961         $actor->addresses([$mailing_address]);
962
963         push( @{$actor->addresses}, $billing_address ) 
964                 unless $billing_address->id eq $mailing_address->id;
965
966     my $aid = -3;
967     for my $a ( @{$command->{user}->{addresses}} ) {
968         next if ($a->{id} == $bid || $a->{id} == $mid);
969         # extract all other addresses
970         my $addr = Fieldmapper::actor::user_address->new;
971             $addr->$_($a->{$_}) for keys %$a;
972                 $addr->isnew(1);
973         $addr->id($aid);
974             $addr->usr(-1);
975         $logger->debug("offline: read other address ".$addr->street1);
976         $aid--;
977         push( @{$actor->addresses}, $addr );
978     }
979         
980         # pull all of the rest of the data from the command blob
981         $actor->$_( $command->{user}->{$_} ) for grep { $_ ne 'addresses' } keys %{$command->{user}};
982
983     # calculate the expire date for the patron based on the profile group
984     my ($grp) = grep {$_->id == $actor->profile} @$user_groups;
985     if($grp) {
986         my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($grp->perm_interval);
987         my $expire_date = DateTime->from_epoch(epoch => DateTime->now->epoch + $seconds)->epoch;
988                 $logger->debug("offline: setting expire date to $expire_date");
989         $actor->expire_date($U->epoch2ISO8601($expire_date));
990     }
991
992         $logger->debug("offline: creating user object...");
993         $actor = $U->simplereq(
994                 'open-ils.actor', 
995                 'open-ils.actor.patron.update', $authtoken, $actor);
996
997         return $actor if(ref($actor) eq 'HASH'); # an event occurred
998
999         return OpenILS::Event->new('SUCCESS', payload => $actor);
1000 }
1001
1002
1003
1004
1005
1006
1007