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