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