2 use strict; use warnings;
4 use OpenSRF::Utils::JSON;
6 use OpenSRF::Utils::Logger qw/$logger/;
7 use OpenILS::Application::AppUtils;
9 use OpenSRF::EX qw/:try/;
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;
18 use DateTime::Format::ISO8601;
23 my $U = "OpenILS::Application::AppUtils";
24 my $DB = "OpenILS::Utils::OfflineStore";
25 my $SES = "${DB}::Session";
26 my $SCRIPT = "OpenILS::Utils::OfflineStore::Script";
29 # --------------------------------------------------------------------
31 # --------------------------------------------------------------------
33 do '##CONFIG##/offline-config.pl';
37 my $basedir = $config{base_dir} || die "Offline config error: no base_dir defined\n";
38 my $bootstrap = $config{bootstrap} || die "Offline config error: no bootstrap defined\n";
39 my $wsname = $cgi->param('ws');
40 my $org = $cgi->param('org');
41 my $authtoken = $cgi->param('ses') || "";
42 my $seskey = $cgi->param('seskey');
43 my $action = $cgi->param('action'); # - create, load, execute, status
55 # --------------------------------------------------------------------
57 # This function should behave as a child_init might behave in case
58 # this is moved to mod_perl
59 # --------------------------------------------------------------------
61 $DB->DBFile($config{dsn}, $config{usr}, $config{pw});
66 OpenSRF::System->bootstrap_client(config_file => $bootstrap );
67 Fieldmapper->import(IDL =>
68 OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
73 sub _ol_debug_params {
75 my @params = $cgi->param;
76 @params = sort { $a cmp $b } @params;
77 $s .= "$_=" . $cgi->param($_) . "\n" for @params;
79 warn '-'x60 ."\n$s\n";
83 # --------------------------------------------------------------------
84 # Finds the requestor and other info specific to this request
85 # --------------------------------------------------------------------
88 # fetch the requestor object
89 ($requestor, $evt) = $U->checkses($authtoken);
90 ol_handle_result($evt) if $evt;
92 # try the param, the workstation, and finally the user's ws org
94 $wsobj = ol_fetch_workstation($wsname);
95 $org = $wsobj->owning_lib if $wsobj;
96 $org = $requestor->ws_ou unless $org;
97 ol_handle_result(OpenILS::Event->new('OFFLINE_NO_ORG')) unless $org;
100 $user_groups = $U->simplereq(
101 'open-ils.actor', 'open-ils.actor.groups.retrieve');
105 # --------------------------------------------------------------------
106 # Runs the requested action
107 # --------------------------------------------------------------------
112 if( $action eq 'create' ) {
114 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_UPLOAD');
115 ol_handle_result($evt) if $evt;
116 $payload = ol_create_session();
118 } elsif( $action eq 'load' ) {
120 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_UPLOAD');
121 ol_handle_result($evt) if $evt;
122 $payload = ol_load();
124 } elsif( $action eq 'execute' ) {
126 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_EXECUTE');
127 ol_handle_result($evt) if $evt;
128 $payload = ol_execute();
130 } elsif( $action eq 'status' ) {
132 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_VIEW');
133 ol_handle_result($evt) if $evt;
134 $payload = ol_status();
137 ol_handle_event('SUCCESS', payload => $payload );
141 # --------------------------------------------------------------------
142 # Creates a new session
143 # --------------------------------------------------------------------
144 sub ol_create_session {
146 my $desc = $cgi->param('desc') || "";
147 $seskey = time . "_${$}_" . int(rand() * 1000);
149 $logger->debug("offline: user ".$requestor->id.
150 " creating new session with key $seskey and description $desc");
156 description => $desc,
157 creator => $requestor->id,
158 create_time => CORE::time(),
167 # --------------------------------------------------------------------
168 # Holds the meta-info for a script file
169 # --------------------------------------------------------------------
170 sub ol_create_script {
173 my $session = ol_find_session($seskey);
174 my $delta = $cgi->param('delta') || 0;
176 my $script = $session->add_to_scripts(
178 requestor => $requestor->id,
179 create_time => CORE::time,
180 workstation => $wsname,
181 logfile => "$basedir/pending/$org/$seskey/$wsname.log",
182 time_delta => $delta,
188 # --------------------------------------------------------------------
189 # Finds the current session in the db
190 # --------------------------------------------------------------------
191 sub ol_find_session {
192 my $ses = $SES->retrieve($seskey);
193 ol_handle_event('OFFLINE_INVALID_SESSION', payload => $seskey) unless $ses;
197 # --------------------------------------------------------------------
198 # Finds a script object in the DB based on workstation and seskey
199 # --------------------------------------------------------------------
201 my $ws = shift || $wsname;
202 my $sk = shift || $seskey;
203 my ($script) = $SCRIPT->search( session => $seskey, workstation => $ws );
207 # --------------------------------------------------------------------
208 # Creates a new script in the database and loads the new script file
209 # --------------------------------------------------------------------
212 my $session = ol_find_session;
213 my $handle = $cgi->upload('file');
214 my $outdir = "$basedir/pending/$org/$seskey";
215 my $outfile = "$outdir/$wsname.log";
217 ol_handle_event('OFFLINE_SESSION_FILE_EXISTS') if ol_find_script();
218 ol_handle_event('OFFLINE_SESSION_ACTIVE') if $session->in_process;
219 ol_handle_event('OFFLINE_SESSION_COMPLETE') if $session->end_time;
221 qx/mkdir -p $outdir/;
223 open(FILE, ">>$outfile") or ol_handle_event('OFFLINE_FILE_ERROR');
224 while( <$handle> ) { print FILE; $x++;}
227 ol_create_script($x);
233 # --------------------------------------------------------------------
234 sub ol_handle_result {
236 my $json = OpenSRF::Utils::JSON->perl2JSON($obj);
238 # Clear this so it's not remembered
241 if( $cgi->param('html')) {
242 my $html = "<html><body onload='xulG.handle_event($json)'></body></html>";
243 print "content-type: text/html\n\n";
248 print "content-type: text/plain\n\n";
255 # --------------------------------------------------------------------
256 sub ol_handle_event {
257 my( $evt, @args ) = @_;
258 ol_handle_result(OpenILS::Event->new($evt, @args));
262 # --------------------------------------------------------------------
263 sub ol_flesh_session {
267 map { $data{$_} = $session->$_ } $session->columns;
270 for my $script ($session->scripts) {
272 map { $sdata{$_} = $script->$_ } $script->columns;
274 # the client doesn't need this info
275 delete $sdata{session};
277 delete $sdata{logfile};
279 push( @{$data{scripts}}, \%sdata );
286 # --------------------------------------------------------------------
287 # Returns various information on the sessions and scripts
288 # --------------------------------------------------------------------
291 my $type = $cgi->param('status_type') || "scripts";
293 # --------------------------------------------------------------------
294 # Returns info on every script upload attached to the current session
295 # --------------------------------------------------------------------
296 if( $type eq 'scripts' ) {
297 my $session = ol_find_session();
298 ol_handle_result(ol_flesh_session($session));
301 # --------------------------------------------------------------------
302 # Returns all scripts and sessions for the given org
303 # --------------------------------------------------------------------
304 } elsif( $type eq 'sessions' ) {
305 my @sessions = $SES->search( org => $org );
307 # can I do this in the DB without raw SQL?
308 @sessions = sort { $a->create_time <=> $b->create_time } @sessions;
310 push( @data, ol_flesh_session($_) ) for @sessions;
311 ol_handle_result(\@data);
314 # --------------------------------------------------------------------
315 # Returns total commands and completed commands counts
316 # --------------------------------------------------------------------
317 } elsif( $type eq 'summary' ) {
318 my $session = ol_find_session();
320 $logger->debug("offline: retrieving summary info ".
321 "for session ".$session->key." with completed=".$session->num_complete);
324 $count += $_->count for ($session->scripts);
326 { total => $count, num_complete => $session->num_complete });
330 # --------------------------------------------------------------------
331 # Returns the list of non-SUCCESS events that have occurred so far for
332 # this set of commands
333 # --------------------------------------------------------------------
334 } elsif( $type eq 'exceptions' ) {
336 my $session = ol_find_session();
337 my $resfile = "$basedir/pending/$org/$seskey/results";
338 if( $session->end_time ) {
339 $resfile = "$basedir/archive/$org/$seskey/results";
341 my $data = ol_file_to_perl($resfile);
344 my $evt = $d->{event};
345 $evt = $evt->[0] if ref $evt eq 'ARRAY';
346 push(@$data2, $d) if $evt->{ilsevent} ne '0';
348 #$data = [ grep { $_->{event}->{ilsevent} ne '0' } @$data ];
349 ol_handle_result($data2);
354 sub ol_fetch_workstation {
356 $logger->debug("offline: Fetching workstation $name");
357 my $ws = $U->storagereq(
358 'open-ils.storage.direct.actor.workstation.search.name', $name);
359 ol_handle_result(OpenILS::Event->new('ACTOR_WORKSTATION_NOT_FOUND')) unless $ws;
366 # --------------------------------------------------------------------
367 # Sorts the script commands then forks a child to executes them.
368 # --------------------------------------------------------------------
371 my $session = ol_find_session();
372 ol_handle_event('OFFLINE_SESSION_ACTIVE') if $session->in_process;
373 ol_handle_event('OFFLINE_SESSION_COMPLETE') if $session->end_time;
375 my $commands = ol_collect_commands();
377 # --------------------------------------------------------------------
378 # Note that we must disconnect from opensrf before forking or the
379 # connection will be borked...
380 # --------------------------------------------------------------------
381 OpenSRF::Transport::PeerHandle->retrieve->disconnect;
387 # --------------------------------------------------------------------
388 # Tell the client all is well
389 # --------------------------------------------------------------------
390 ol_handle_event('SUCCESS'); # - this exits
395 # --------------------------------------------------------------------
396 # close stdout/stderr or apache will wait on the child to finish
397 # --------------------------------------------------------------------
401 $logger->debug("offline: child $$ processing data...");
403 # --------------------------------------------------------------------
404 # The child re-connects to the opensrf network and processes
405 # the script requests
406 # --------------------------------------------------------------------
407 OpenSRF::System->bootstrap_client(config_file => $bootstrap);
412 #Class::DBI->autoupdate(1);
416 my $sesion = ol_find_session();
417 $session->in_process(1);
418 ol_process_commands($session, $commands);
419 ol_archive_files($session);
423 $logger->error("offline: child process error $e");
428 sub ol_file_to_perl {
430 open(F, "$fname") or ol_handle_event('OFFLINE_FILE_ERROR');
433 push(@p, OpenSRF::Utils::JSON->JSON2perl($_)) for @d;
438 # collects the commands and sorts them on timestamp+delta
439 sub ol_collect_commands {
440 my $ses = ol_find_session();
443 # cycle through every script loaded to this session
444 for my $script ($ses->scripts) {
445 my $coms = ol_file_to_perl($script->logfile);
447 # cycle through all of the commands for this script
448 for my $com (@$coms) {
449 $$com{_workstation} = $script->workstation;
450 $$com{_realtime} = $script->time_delta + $com->{timestamp};
451 push( @commands, $com );
455 # make sure thera are no blank commands
456 @commands = grep { ($_ and $_->{type}) } @commands;
459 @commands = sort { $a->{_realtime} <=> $b->{_realtime} } @commands;
461 # push user registrations to the front
462 my @regs = grep { $_->{type} eq 'register' } @commands;
463 my @others = grep { $_->{type} ne 'register' } @commands;
465 return [ @regs, @others ];
469 my $time = shift || CORE::time;
470 my (undef,undef, undef, $mday,$mon,$year) = localtime($time);
471 $mon++; $year += 1900;
472 $mday = "0$mday" unless $mday =~ /\d{2}/o;
473 $mon = "0$mon" unless $mon =~ /\d{2}/o;
474 return ($year, $mon, $mday);
478 # --------------------------------------------------------------------
479 # Moves all files from the pending directory to the archive directory
480 # and removes the pending directory
481 # --------------------------------------------------------------------
482 sub ol_archive_files {
484 my ($y, $m, $d) = ol_date();
486 my $dir = "$basedir/pending/$org/$seskey";
487 my $archdir = "$basedir/archive/$org/$seskey";
488 $logger->debug("offline: archiving files to $archdir");
490 # Tell the db the files are moving
491 $_->logfile($archdir.'/'.$_->workstation.'.log') for ($session->scripts);
493 qx/mkdir -p $archdir/;
494 qx/mv $_ $archdir/ for <$dir/*>;
499 # --------------------------------------------------------------------
500 # Appends results to the results file.
501 # --------------------------------------------------------------------
503 sub ol_append_result {
508 $obj = OpenSRF::Utils::JSON->perl2JSON($obj);
511 open($rhandle, ">>$basedir/pending/$org/$seskey/results")
512 or ol_handle_event('OFFLINE_FILE_ERROR');
515 print $rhandle "$obj\n";
516 close($rhandle) if $last;
521 # --------------------------------------------------------------------
522 # Runs the commands and returns the list of errors
523 # --------------------------------------------------------------------
524 sub ol_process_commands {
527 my $commands = shift;
530 $session->start_time(CORE::time);
532 for my $d ( @$commands ) {
535 my $last = ($x++ == scalar(@$commands) - 1) ? 1 : 0;
536 my $res = { command => $d };
542 $logger->debug("offline: top of execute loop : $t");
545 $res->{event} = ol_handle_checkin($d) if $t eq 'checkin';
546 $res->{event} = ol_handle_inhouse($d) if $t eq 'in_house_use';
547 $res->{event} = ol_handle_checkout($d) if $t eq 'checkout';
548 $res->{event} = ol_handle_renew($d) if $t eq 'renew';
549 $res->{event} = ol_handle_register($d) if $t eq 'register';
551 } catch Error with { $err = shift; };
555 if( ref($err) eq 'OpenSRF::EX::JabberDisconnected' ) {
556 $logger->error("offline: we lost jabber .. trying to reconnect");
560 $res->{event} = OpenILS::Event->new('INTERNAL_SERVER_ERROR', debug => "$err");
569 ol_append_result($res, $last);
570 $session->num_complete( $session->num_complete + 1 );
572 $logger->debug("offline: set session [".$session->key."] num_complete to ".$session->num_complete);
575 $session->end_time(CORE::time);
576 $session->in_process(0);
580 # --------------------------------------------------------------------
581 # Runs an in_house_use action
582 # --------------------------------------------------------------------
583 sub ol_handle_inhouse {
586 my $realtime = $command->{_realtime};
587 my $ws = $command->{_workstation};
588 my $barcode = $command->{barcode};
589 my $count = $command->{count} || 1;
590 my $use_time = $command->{use_time} || "";
592 $logger->activity("offline: in_house_use : requestor=". $requestor->id.", realtime=$realtime, ".
593 "workstation=$ws, barcode=$barcode, count=$count, use_time=$use_time");
596 return OpenILS::Event->new(
597 'INTERNAL_SERVER_ERROR', payload => 'TOO MANY IN HOUSE USE');
600 my $ids = $U->simplereq(
602 'open-ils.circ.in_house_use.create', $authtoken,
603 { barcode => $barcode, count => $count, location => $org, use_time => $use_time } );
605 return OpenILS::Event->new('SUCCESS', payload => $ids) if( ref($ids) eq 'ARRAY' );
611 # --------------------------------------------------------------------
612 # Pulls the relevant circ args from the command, fetches data where
614 # --------------------------------------------------------------------
616 sub ol_circ_args_from_command {
619 my $type = $command->{type};
620 my $realtime = $command->{_realtime};
621 my $ws = $command->{_workstation};
622 my $barcode = $command->{barcode} || "";
623 my $cotime = $command->{checkout_time} || "";
624 my $pbc = $command->{patron_barcode};
625 my $due_date = $command->{due_date} || "";
626 my $noncat = ($command->{noncat}) ? "yes" : "no"; # for logging
628 $logger->activity("offline: $type : requestor=". $requestor->id.
629 ", realtime=$realtime, workstation=$ws, checkout_time=$cotime, ".
630 "patron=$pbc, due_date=$due_date, noncat=$noncat");
634 permit_override => 1,
636 checkout_time => $cotime,
637 patron_barcode => $pbc,
638 due_date => $due_date
641 if(ol_get_org_setting('circ.offline.username_allowed')) {
643 my $format = ol_get_org_setting('opac.barcode_regex');
646 $format =~ s/^\/|\/$//g; # remove any couching //'s
647 if($pbc !~ qr/$format/) {
649 # the patron barcode does not match the configured barcode format
650 # assume they passed a username instead
652 my $user_id = $user_id_cache{$pbc} ||
655 'open-ils.actor.username.exists',
660 # a valid username was provided, update the args and cache
661 $user_id_cache{$pbc} = $user_id;
662 $args->{patron_id} = $user_id;
663 delete $args->{patron_barcode};
670 if( $command->{noncat} ) {
672 $args->{noncat_type} = $command->{noncat_type};
673 $args->{noncat_count} = $command->{noncat_count};
679 sub ol_get_org_setting {
681 return $U->simplereq(
683 'open-ils.actor.ou_setting.ancestor_default',
684 $org, $name, $authtoken);
689 # --------------------------------------------------------------------
690 # Performs a checkout action
691 # --------------------------------------------------------------------
692 sub ol_handle_checkout {
694 my $args = ol_circ_args_from_command($command);
696 if( $args->{noncat} and $args->{noncat_count} > 99 ) {
697 return OpenILS::Event->new(
698 'INTERNAL_SERVER_ERROR', payload => 'TOO MANY NON CATS');
701 if( $args->{barcode} ) {
703 # $c becomes the Copy
704 # $e possibily becomes the Exception
705 my( $c, $e ) = $U->fetch_copy_by_barcode($args->{barcode});
708 my $barcode = $args->{barcode};
709 # Have to have this config option (or org setting) and a
710 # status_changed_time for skippage
713 'circ.offline.skip_checkout_if_newer_status_changed_time'
715 || $config{skip_late}
717 && length($c->status_changed_time())
719 my $cts = DateTime::Format::ISO8601->parse_datetime( cleanse_ISO8601($c->status_changed_time()) )->epoch();
720 my $xts = $command->{timestamp}; # Transaction Time Stamp
721 $logger->activity("offline: ol_handle_checkout: barcode=$barcode, cts=$cts, xts=$xts");
723 # Asset has changed after this transaction, ignore
725 return OpenILS::Event->new(
729 # $logger->activity("offline: fetch_copy_by_barcode: " . Dumper($c->real_fields()));
733 my $evt = $U->simplereq(
734 'open-ils.circ', 'open-ils.circ.checkout', $authtoken, $args );
736 # if the item is already checked out to this user and we are past
737 # the configured auto-renewal interval, try to renew the circ.
738 if( ref $evt ne 'ARRAY' and
739 $evt->{textcode} == 'OPEN_CIRCULATION_EXISTS' and
740 $evt->{payload}->{auto_renew}) {
742 return ol_handle_renew($command);
749 # --------------------------------------------------------------------
750 # Performs the renewal action
751 # --------------------------------------------------------------------
752 sub ol_handle_renew {
754 my $args = ol_circ_args_from_command($command);
757 if( $args->{barcode} ) {
759 # $c becomes the Copy
760 # $e possibily becomes the Exception
761 my( $c, $e ) = $U->fetch_copy_by_barcode($args->{barcode});
764 my $barcode = $args->{barcode};
765 # Have to have this config option (or org setting) and a
766 # status_changed_time for skippage
769 'circ.offline.skip_renew_if_newer_status_changed_time'
771 || $config{skip_late}
773 && length($c->status_changed_time())
775 my $cts = DateTime::Format::ISO8601->parse_datetime( cleanse_ISO8601($c->status_changed_time()) )->epoch();
776 my $xts = $command->{timestamp}; # Transaction Time Stamp
777 $logger->activity("offline: ol_handle_renew: barcode=$barcode, cts=$cts, xts=$xts");
779 # Asset has changed after this transaction, ignore
781 return OpenILS::Event->new(
788 return $U->simplereq(
789 'open-ils.circ', 'open-ils.circ.renew.override', $authtoken, $args );
793 # --------------------------------------------------------------------
794 # Runs a checkin action
795 # --------------------------------------------------------------------
796 sub ol_handle_checkin {
799 my $realtime = $command->{_realtime};
800 my $ws = $command->{_workstation};
801 my $barcode = $command->{barcode};
802 my $backdate = $command->{backdate} || "";
804 $logger->activity("offline: checkin : requestor=". $requestor->id.
805 ", realtime=$realtime, ". "workstation=$ws, barcode=$barcode, backdate=$backdate");
809 # $c becomes the Copy
810 # $e possibily becomes the Exception
811 my( $c, $e ) = $U->fetch_copy_by_barcode($barcode);
814 # Have to have this config option (or org setting) and a
815 # status_changed_time for skippage
818 'circ.offline.skip_checkin_if_newer_status_changed_time'
820 || $config{skip_late}
822 && length($c->status_changed_time())
824 my $cts = DateTime::Format::ISO8601->parse_datetime( cleanse_ISO8601($c->status_changed_time()) )->epoch();
825 my $xts = $command->{timestamp}; # Transaction Time Stamp
826 $logger->activity("offline: ol_handle_checkin: barcode=$barcode, cts=$cts, xts=$xts");
828 # Asset has changed after this transaction, ignore
830 return OpenILS::Event->new(
837 return $U->simplereq(
839 'open-ils.circ.checkin', $authtoken,
840 { barcode => $barcode, backdate => $backdate } );
845 # --------------------------------------------------------------------
846 # Registers a new patron
847 # --------------------------------------------------------------------
848 sub ol_handle_register {
851 my $barcode = $command->{user}->{card}->{barcode};
852 delete $command->{user}->{card};
854 $logger->info("offline: creating new user with barcode $barcode");
856 # now, create the user
857 my $actor = Fieldmapper::actor::user->new;
858 my $card = Fieldmapper::actor::card->new;
861 # username defaults to the barcode
862 $actor->usrname( ($actor->usrname) ? $actor->usrname : $barcode );
864 # Set up all of the virtual IDs, isnew, etc.
868 $actor->cards([$card]);
873 $card->barcode($barcode);
879 for my $resp (@{$command->{user}->{survey_responses}}) {
880 my $sr = Fieldmapper::action::survey_response->new;
881 $sr->$_( $resp->{$_} ) for keys %$resp;
885 $logger->debug("offline: created new survey response for survey ".$sr->survey);
887 delete $command->{user}->{survey_responses};
888 $actor->survey_responses(\@sresp) if @sresp;
890 # extract the billing address
891 if( my $addr = $command->{user}->{billing_address} ) {
892 $billing_address = Fieldmapper::actor::user_address->new;
893 $billing_address->$_($addr->{$_}) for keys %$addr;
894 $billing_address->isnew(1);
895 $billing_address->id(-1);
896 $billing_address->usr(-1);
897 delete $command->{user}->{billing_address};
898 $logger->debug("offline: read billing address ".$billing_address->street1);
901 # extract the mailing address
902 if( my $addr = $command->{user}->{mailing_address} ) {
903 $mailing_address = Fieldmapper::actor::user_address->new;
904 $mailing_address->$_($addr->{$_}) for keys %$addr;
905 $mailing_address->isnew(1);
906 $mailing_address->id(-2);
907 $mailing_address->usr(-1);
908 delete $command->{user}->{mailing_address};
909 $logger->debug("offline: read mailing address ".$mailing_address->street1);
912 # make sure we have values for both
913 $billing_address ||= $mailing_address;
914 $mailing_address ||= $billing_address;
916 $actor->billing_address($billing_address->id);
917 $actor->mailing_address($mailing_address->id);
918 $actor->addresses([$mailing_address]);
920 push( @{$actor->addresses}, $billing_address )
921 unless $billing_address->id eq $mailing_address->id;
923 # pull all of the rest of the data from the command blob
924 $actor->$_( $command->{user}->{$_} ) for keys %{$command->{user}};
926 # calculate the expire date for the patron based on the profile group
927 my ($grp) = grep {$_->id == $actor->profile} @$user_groups;
929 my $seconds = OpenSRF::Utils->interval_to_seconds($grp->perm_interval);
930 my $expire_date = DateTime->from_epoch(epoch => DateTime->now->epoch + $seconds)->epoch;
931 $logger->debug("offline: setting expire date to $expire_date");
932 $actor->expire_date($U->epoch2ISO8601($expire_date));
935 $logger->debug("offline: creating user object...");
936 $actor = $U->simplereq(
938 'open-ils.actor.patron.update', $authtoken, $actor);
940 return $actor if(ref($actor) eq 'HASH'); # an event occurred
942 return OpenILS::Event->new('SUCCESS', payload => $actor);