1 package OpenILS::Application::Circ::Circulate;
2 use base 'OpenSRF::Application';
3 use strict; use warnings;
4 use OpenSRF::EX qw(:try);
6 use OpenSRF::Utils::Cache;
7 use OpenSRF::AppSession;
8 use Digest::MD5 qw(md5_hex);
9 use OpenILS::Utils::ScriptRunner;
10 use OpenILS::Application::AppUtils;
11 use OpenILS::Application::Circ::Holds;
12 use OpenILS::Application::Circ::Transit;
13 use OpenILS::Utils::PermitHold;
14 use OpenSRF::Utils::Logger qw(:logger);
16 use DateTime::Format::ISO8601;
17 use OpenSRF::Utils qw/:datetime/;
19 $Data::Dumper::Indent = 0;
20 my $apputils = "OpenILS::Application::AppUtils";
22 my $holdcode = "OpenILS::Application::Circ::Holds";
23 my $transcode = "OpenILS::Application::Circ::Transit";
25 my %scripts; # - circulation script filenames
26 my $script_libs; # - any additional script libraries
27 my %cache; # - db objects cache
28 my %contexts; # - Script runner contexts
29 my $cache_handle; # - memcache handle
31 sub PRECAT_FINE_LEVEL { return 2; }
32 sub PRECAT_LOAN_DURATION { return 2; }
34 my %RECORD_FROM_COPY_CACHE;
37 # for security, this is a process-defined and not
38 # a client-defined variable
42 # ------------------------------------------------------------------------------
43 # Load the circ script from the config
44 # ------------------------------------------------------------------------------
48 $cache_handle = OpenSRF::Utils::Cache->new('global');
49 my $conf = OpenSRF::Utils::SettingsClient->new;
50 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
51 my @pfx = ( @pfx2, "scripts" );
53 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
54 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
55 my $d = $conf->config_value( @pfx, 'circ_duration' );
56 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
57 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
58 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
59 my $lb = $conf->config_value( @pfx2, 'script_path' );
61 $logger->error( "Missing circ script(s)" )
62 unless( $p and $c and $d and $f and $m and $pr );
64 $scripts{circ_permit_patron} = $p;
65 $scripts{circ_permit_copy} = $c;
66 $scripts{circ_duration} = $d;
67 $scripts{circ_recurring_fines}= $f;
68 $scripts{circ_max_fines} = $m;
69 $scripts{circ_permit_renew} = $pr;
71 $lb = [ $lb ] unless ref($lb);
74 $logger->debug("Loaded rules scripts for circ: " .
75 "circ permit patron: $p, circ permit copy: $c, ".
76 "circ duration :$d , circ recurring fines : $f, " .
77 "circ max fines : $m, circ renew permit : $pr");
81 # ------------------------------------------------------------------------------
82 # Loads the necessary circ objects and pushes them into the script environment
83 # Returns ( $data, $evt ). if $evt is defined, then an
84 # unexpedted event occurred and should be dealt with / returned to the caller
85 # ------------------------------------------------------------------------------
93 $evt = _ctx_add_patron_objects($ctx, %params);
94 return (undef,$evt) if $evt;
96 if(!$params{noncat}) {
97 if( $evt = _ctx_add_copy_objects($ctx, %params) ) {
98 $ctx->{precat} = 1 if($evt->{textcode} eq 'COPY_NOT_FOUND')
100 $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
104 _doctor_patron_object($ctx) if $ctx->{patron};
105 _doctor_copy_object($ctx) if $ctx->{copy};
107 if(!$ctx->{no_runner}) {
108 _build_circ_script_runner($ctx);
109 _add_script_runner_methods($ctx);
115 sub _ctx_add_patron_objects {
116 my( $ctx, %params) = @_;
119 # - patron standings are now handled in the penalty server...
121 #if(!defined($cache{patron_standings})) {
122 # $cache{patron_standings} = $U->fetch_patron_standings();
124 #$ctx->{patron_standings} = $cache{patron_standings};
126 $cache{group_tree} = $U->fetch_permission_group_tree() unless $cache{group_tree};
127 $ctx->{group_tree} = $cache{group_tree};
129 $ctx->{patron_circ_summary} =
130 $U->fetch_patron_circ_summary($ctx->{patron}->id)
131 if $params{fetch_patron_circsummary};
137 sub _find_copy_by_attr {
142 my $copy = $params{copy} || undef;
147 $U->fetch_copy($params{copyid}) if $params{copyid};
148 return (undef,$evt) if $evt;
152 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
153 return (undef,$evt) if $evt;
156 return ( $copy, $evt );
159 sub _ctx_add_copy_objects {
160 my($ctx, %params) = @_;
165 $cache{copy_statuses} = $U->fetch_copy_statuses
166 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
168 $cache{copy_locations} = $U->fetch_copy_locations
169 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
171 $ctx->{copy_statuses} = $cache{copy_statuses};
172 $ctx->{copy_locations} = $cache{copy_locations};
174 ($copy, $evt) = _find_copy_by_attr(%params);
177 if( $copy and !$ctx->{title} ) {
178 $logger->debug("Copy status: " . $copy->status);
180 my $r = $RECORD_FROM_COPY_CACHE{$copy->id};
181 ($r, $evt) = $U->fetch_record_by_copy( $copy->id ) unless $r;
183 $RECORD_FROM_COPY_CACHE{$copy->id} = $r;
186 $ctx->{copy} = $copy;
193 # ------------------------------------------------------------------------------
194 # Fleshes parts of the patron object
195 # ------------------------------------------------------------------------------
196 sub _doctor_copy_object {
199 my $copy = $ctx->{copy} || return undef;
201 $logger->debug("Doctoring copy object...");
203 # set the copy status to a status name
204 $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
206 # set the copy location to the location object
207 $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
209 $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
213 # ------------------------------------------------------------------------------
214 # Fleshes parts of the patron object
215 # ------------------------------------------------------------------------------
216 sub _doctor_patron_object {
219 my $patron = $ctx->{patron} || return undef;
221 # push the standing object into the patron
222 # if(ref($ctx->{patron_standings})) {
223 # for my $s (@{$ctx->{patron_standings}}) {
224 # if( $s->id eq $ctx->{patron}->standing ) {
225 # $patron->standing($s);
226 # $logger->debug("Set patron standing to ". $s->value);
231 # set the patron ptofile to the profile name
232 $patron->profile( _get_patron_profile(
233 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
237 $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
241 # recurse and find the patron profile name from the tree
242 # another option would be to grab the groups for the patron
243 # and cycle through those until the "profile" group has been found
244 sub _get_patron_profile {
245 my( $patron, $group_tree ) = @_;
246 return $group_tree if ($group_tree->id eq $patron->profile);
247 return undef unless ($group_tree->children);
249 for my $child (@{$group_tree->children}) {
250 my $ret = _get_patron_profile( $patron, $child );
256 sub _get_copy_status {
257 my( $copy, $cstatus ) = @_;
260 for my $status (@$cstatus) {
261 $s = $status if( $status->id eq $copy->status )
263 $logger->debug("Retrieving copy status: " . $s->name) if $s;
267 sub _get_copy_location {
268 my( $copy, $locations ) = @_;
271 for my $loc (@$locations) {
272 $l = $loc if $loc->id eq $copy->location;
274 $logger->debug("Retrieving copy location: " . $l->name ) if $l;
279 # ------------------------------------------------------------------------------
280 # Constructs and shoves data into the script environment
281 # ------------------------------------------------------------------------------
282 sub _build_circ_script_runner {
286 $logger->debug("Loading script environment for circulation");
289 if( $runner = $contexts{$ctx->{type}} ) {
290 $runner->refresh_context;
292 $runner = OpenILS::Utils::ScriptRunner->new;
293 $contexts{type} = $runner;
297 $logger->debug("Loading circ script lib path $_");
298 $runner->add_path( $_ );
301 # Note: inserting the number 0 into the script turns into the
302 # string "0", and thus evaluates to true in JS land
303 # inserting undef will insert "", which evaluates to false
305 $runner->insert( 'environment.patron', $ctx->{patron}, 1);
306 $runner->insert( 'environment.title', $ctx->{title}, 1);
307 $runner->insert( 'environment.copy', $ctx->{copy}, 1);
310 $runner->insert( 'result', {} );
311 #$runner->insert( 'result.event', 'SUCCESS' );
312 $runner->insert( 'result.events', [] );
315 $runner->insert('environment.isRenewal', 1);
317 $runner->insert('environment.isRenewal', undef);
320 if($ctx->{ishold} ) {
321 $runner->insert('environment.isHold', 1);
323 $runner->insert('environment.isHold', undef)
326 if( $ctx->{noncat} ) {
327 $runner->insert('environment.isNonCat', 1);
328 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
330 $runner->insert('environment.isNonCat', undef);
333 # if(ref($ctx->{patron_circ_summary})) {
334 # $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
335 # $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
338 $ctx->{runner} = $runner;
343 sub _add_script_runner_methods {
346 my $runner = $ctx->{runner};
350 # allows a script to fetch a hold that is currently targeting the
352 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
354 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
355 $hold = undef unless $hold;
356 $runner->insert( $key, $hold, 1 );
362 # ------------------------------------------------------------------------------
364 __PACKAGE__->register_method(
365 method => "permit_circ",
366 api_name => "open-ils.circ.checkout.permit",
368 Determines if the given checkout can occur
369 @param authtoken The login session key
370 @param params A trailing hash of named params including
371 barcode : The copy barcode,
372 patron : The patron the checkout is occurring for,
373 renew : true or false - whether or not this is a renewal
374 @return The event that occurred during the permit check.
377 __PACKAGE__->register_method (
378 method => 'permit_circ',
379 api_name => 'open-ils.circ.checkout.permit.override',
380 signature => q/@see open-ils.circ.checkout.permit/,
384 my( $self, $client, $authtoken, $params ) = @_;
387 my ( $requestor, $patron, $ctx, $evt, $circ );
389 my $override = ($self->api_name =~ /override/) ? 1 : 0;
391 # check permisson of the requestor
392 ( $requestor, $patron, $evt ) =
393 $U->checkses_requestor(
394 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
397 # fetch and build the circulation environment
398 if( !( $ctx = $params->{_ctx}) ) {
400 ( $ctx, $evt ) = create_circ_ctx( %$params,
402 requestor => $requestor,
404 #fetch_patron_circ_summary => 1,
405 fetch_copy_statuses => 1,
406 fetch_copy_locations => 1,
411 $ctx->{authtoken} = $authtoken;
413 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
414 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
415 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
419 $ctx->{permit_key} = _cache_permit_key();
420 my $events = _run_permit_scripts($ctx);
423 $evt = override_events($requestor, $requestor->ws_ou, $events);
425 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
431 sub override_events {
433 my( $requestor, $org, $events ) = @_;
434 $events = [ $events ] unless ref($events) eq 'ARRAY';
437 for my $e (@$events) {
438 my $tc = $e->{textcode};
439 next if $tc eq 'SUCCESS';
440 my $ov = "$tc.override";
441 my $evt = $U->check_perms( $requestor->id, $org, $ov );
449 __PACKAGE__->register_method(
450 method => "check_title_hold",
451 api_name => "open-ils.circ.title_hold.is_possible",
453 Determines if a hold were to be placed by a given user,
454 whether or not said hold would have any potential copies
456 @param authtoken The login session key
457 @param params A hash of named params including:
458 patronid - the id of the hold recipient
459 titleid (brn) - the id of the title to be held
460 depth - the hold range depth (defaults to 0)
463 sub check_title_hold {
464 my( $self, $client, $authtoken, $params ) = @_;
465 my %params = %$params;
466 my $titleid = $params{titleid};
468 my ( $requestor, $patron, $evt ) = $U->checkses_requestor(
469 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
472 my $rangelib = $patron->home_ou;
473 my $depth = $params{depth} || 0;
475 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
477 my $org = $U->simplereq(
479 'open-ils.actor.org_unit.retrieve',
480 $authtoken, $requestor->home_ou );
486 while( $title = $U->storagereq(
487 'open-ils.storage.biblio.record_entry.ranged_tree',
488 $titleid, $rangelib, $depth, $limit, $offset ) ) {
490 last unless ref($title);
492 for my $cn (@{$title->call_numbers}) {
494 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
496 for my $copy (@{$cn->copies}) {
498 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
500 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
502 requestor => $requestor,
505 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
506 request_lib => $org } );
508 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
519 # Runs the patron and copy permit scripts
520 # if this is a non-cat circulation, the copy permit script
522 sub _run_permit_scripts {
525 my $runner = $ctx->{runner};
526 my $patronid = $ctx->{patron}->id;
527 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
528 my $key = $ctx->{permit_key};
530 my $penalties = $U->update_patron_penalties(
531 authtoken => $ctx->{authtoken},
532 patron => $ctx->{patron}
535 $penalties = $penalties->{fatal_penalties};
537 $logger->info("circ patron penalties user $patronid: @$penalties");
539 if( $ctx->{noncat} ) {
540 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
541 return OpenILS::Event->new('SUCCESS', payload => $key);
545 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
546 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
550 $logger->debug("Exiting circ permit early because request is for hold patron permit");
551 return OpenILS::Event->new('SUCCESS');
554 $runner->load($scripts{circ_permit_copy});
555 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
557 # ---------------------------------------------------------------------
558 # Capture all of the copy permit events
559 # ---------------------------------------------------------------------
560 my $copy_events = $runner->retrieve('result.events');
561 $copy_events = [ split(/,/, $copy_events) ];
562 $ctx->{circ_permit_copy_events} = $copy_events;
563 $logger->activity("circ_permit_copy for copy ".
564 "$barcode returned events: @$copy_events") if @$copy_events;
567 push( @allevents, OpenILS::Event->new($_)) for @$penalties;
568 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
570 return OpenILS::Event->new('SUCCESS', payload => $key)
571 unless (@$copy_events or @$penalties);
573 # uniquify the events
574 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
575 @allevents = values %hash;
579 # takes copyid, patronid, and requestor id
580 sub _cache_permit_key {
581 my $key = md5_hex( time() . rand() . "$$" );
582 $logger->debug("Setting circ permit key to $key");
583 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
587 sub _check_permit_key {
589 $logger->debug("Fetching circ permit key $key");
590 my $k = "oils_permit_key_$key";
591 my $one = $cache_handle->get_cache($k);
592 $cache_handle->delete_cache($k);
593 return ($one) ? 1 : 0;
597 # ------------------------------------------------------------------------------
599 __PACKAGE__->register_method(
600 method => "checkout",
601 api_name => "open-ils.circ.checkout",
604 @param authtoken The login session key
605 @param params A named hash of params including:
607 barcode If no copy is provided, the copy is retrieved via barcode
608 copyid If no copy or barcode is provide, the copy id will be use
609 patron The patron's id
610 noncat True if this is a circulation for a non-cataloted item
611 noncat_type The non-cataloged type id
612 noncat_circ_lib The location for the noncat circ.
613 precat The item has yet to be cataloged
614 dummy_title The temporary title of the pre-cataloded item
615 dummy_author The temporary authr of the pre-cataloded item
616 Default is the home org of the staff member
617 @return The SUCCESS event on success, any other event depending on the error
621 my( $self, $client, $authtoken, $params ) = @_;
624 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
625 my $key = $params->{permit_key};
627 # if this is a renewal, then the requestor does not have to
628 # have checkout privelages
629 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
630 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
633 if( $params->{patron} ) {
634 ( $patron, $evt ) = $U->fetch_user($params->{patron});
637 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
641 # set the circ lib to the home org of the requestor if not specified
642 my $circlib = (defined($params->{circ_lib})) ?
643 $params->{circ_lib} : $requestor->home_ou;
646 # Make sure the caller has a valid permit key or is
647 # overriding the permit can
648 if( $params->{permit_override} ) {
649 $evt = $U->check_perms(
650 $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
654 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
655 unless _check_permit_key($key);
658 # if this is a non-cataloged item, check it out and return
659 return _checkout_noncat(
660 $key, $requestor, $patron, %$params ) if $params->{noncat};
662 # if this item has yet to be cataloged, make sure a dummy copy exists
663 ( $params->{copy}, $evt ) = _make_precat_copy(
664 $requestor, $circlib, $params ) if $params->{precat};
668 # fetch and build the circulation environment
669 if( !( $ctx = $params->{_ctx}) ) {
670 ( $ctx, $evt ) = create_circ_ctx( %$params,
672 requestor => $requestor,
673 session => $U->start_db_session(),
675 #fetch_patron_circ_summary => 1,
676 fetch_copy_statuses => 1,
677 fetch_copy_locations => 1,
681 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
683 # if the call doesn't know it's not cataloged..
684 if(!$params->{precat}) {
685 if( $ctx->{copy}->call_number eq '-1' ) {
686 return OpenILS::Event->new('ITEM_NOT_CATALOGED');
690 # this happens in permit.. but we need to check here for 'offline' requests
691 ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
692 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
694 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
697 $ctx->{circ_lib} = $circlib;
699 $evt = _run_checkout_scripts($ctx);
703 _build_checkout_circ_object($ctx);
705 $evt = _apply_modified_due_date($ctx);
708 $evt = _commit_checkout_circ_object($ctx);
711 $evt = _update_checkout_copy($ctx);
715 ($holds, $evt) = _handle_related_holds($ctx);
719 $logger->debug("Checkout committing objects with session thread trace: ".$ctx->{session}->session_id);
720 $U->commit_db_session($ctx->{session});
721 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
723 $logger->activity("user ".$requestor->id." successfully checked out item ".
724 $ctx->{copy}->barcode." to user ".$ctx->{patron}->id );
727 # ------------------------------------------------------------------------------
728 # Update the patron penalty info in the DB
729 # ------------------------------------------------------------------------------
730 $U->update_patron_penalties(
731 authtoken => $authtoken,
732 patron => $ctx->{patron} ,
736 return OpenILS::Event->new('SUCCESS',
738 copy => $U->unflesh_copy($ctx->{copy}),
739 circ => $ctx->{circ},
741 holds_fulfilled => $holds,
747 sub _make_precat_copy {
748 my ( $requestor, $circlib, $params ) = @_;
750 my( $copy, undef ) = _find_copy_by_attr(%$params);
753 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
754 return ($copy, undef);
757 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
759 my $evt = OpenILS::Event->new(
760 'BAD_PARAMS', desc => "Dummy title or author not provided" )
761 unless ( $params->{dummy_title} and $params->{dummy_author} );
762 return (undef, $evt) if $evt;
764 $copy = Fieldmapper::asset::copy->new;
765 $copy->circ_lib($circlib);
766 $copy->creator($requestor->id);
767 $copy->editor($requestor->id);
768 $copy->barcode($params->{barcode});
769 $copy->call_number(-1); #special CN for precat materials
770 $copy->loan_duration(&PRECAT_LOAN_DURATION);
771 $copy->fine_level(&PRECAT_FINE_LEVEL);
773 $copy->dummy_title($params->{dummy_title});
774 $copy->dummy_author($params->{dummy_author});
776 my $id = $U->storagereq(
777 'open-ils.storage.direct.asset.copy.create', $copy );
778 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
780 $logger->debug("Pre-cataloged copy successfully created");
781 return $U->fetch_copy($id);
785 sub _run_checkout_scripts {
791 my $runner = $ctx->{runner};
793 $runner->insert('result.durationLevel');
794 $runner->insert('result.durationRule');
795 $runner->insert('result.recurringFinesRule');
796 $runner->insert('result.recurringFinesLevel');
797 $runner->insert('result.maxFine');
799 $runner->load($scripts{circ_duration});
800 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
801 my $duration = $runner->retrieve('result.durationRule');
802 $logger->debug("Circ duration script yielded a duration rule of: $duration");
804 $runner->load($scripts{circ_recurring_fines});
805 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
806 my $recurring = $runner->retrieve('result.recurringFinesRule');
807 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
809 $runner->load($scripts{circ_max_fines});
810 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
811 my $max_fine = $runner->retrieve('result.maxFine');
812 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
814 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
816 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
818 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
821 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
822 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
823 $ctx->{duration_rule} = $duration;
824 $ctx->{recurring_fines_rule} = $recurring;
825 $ctx->{max_fine_rule} = $max_fine;
830 sub _build_checkout_circ_object {
834 my $circ = new Fieldmapper::action::circulation;
835 my $duration = $ctx->{duration_rule};
836 my $max = $ctx->{max_fine_rule};
837 my $recurring = $ctx->{recurring_fines_rule};
838 my $copy = $ctx->{copy};
839 my $patron = $ctx->{patron};
840 my $dur_level = $ctx->{duration_level};
841 my $rec_level = $ctx->{recurring_fines_level};
843 $circ->duration( $duration->shrt ) if ($dur_level == 1);
844 $circ->duration( $duration->normal ) if ($dur_level == 2);
845 $circ->duration( $duration->extended ) if ($dur_level == 3);
847 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
848 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
849 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
851 $circ->duration_rule( $duration->name );
852 $circ->recuring_fine_rule( $recurring->name );
853 $circ->max_fine_rule( $max->name );
854 $circ->max_fine( $max->amount );
856 $circ->fine_interval($recurring->recurance_interval);
857 $circ->renewal_remaining( $duration->max_renewals );
858 $circ->target_copy( $copy->id );
859 $circ->usr( $patron->id );
860 $circ->circ_lib( $ctx->{circ_lib} );
863 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
864 $circ->opac_renewal(1);
865 $circ->renewal_remaining($ctx->{renewal_remaining});
866 $circ->circ_staff($ctx->{requestor}->id);
870 # if the user provided an overiding checkout time,
871 # (e.g. the checkout really happened several hours ago), then
872 # we apply that here. Does this need a perm??
873 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
874 $logger->debug("circ setting checkout_time to $ds");
875 $circ->xact_start($ds);
878 # if a patron is renewing, 'requestor' will be the patron
879 $circ->circ_staff($ctx->{requestor}->id );
880 _set_circ_due_date($circ);
881 $ctx->{circ} = $circ;
884 sub _apply_modified_due_date {
886 my $circ = $ctx->{circ};
888 if( $ctx->{due_date} ) {
890 my $evt = $U->check_perms(
891 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
894 my $ds = _create_date_stamp($ctx->{due_date});
895 $logger->debug("circ modifying due_date to $ds");
896 $circ->due_date($ds);
902 sub _create_date_stamp {
903 my $datestring = shift;
904 return undef unless $datestring;
905 $datestring = clense_ISO8601($datestring);
906 $logger->debug("circ created date stamp => $datestring");
910 sub _create_due_date {
911 my $duration = shift;
913 my ($sec,$min,$hour,$mday,$mon,$year) =
914 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
915 $year += 1900; $mon += 1;
916 my $due_date = sprintf(
917 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
918 $year, $mon, $mday, $hour, $min, $sec);
922 sub _set_circ_due_date {
925 my $dd = _create_due_date($circ->duration);
926 $logger->debug("Checkout setting due date on circ to: $dd");
927 $circ->due_date($dd);
930 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
931 sub _update_checkout_copy {
934 my $copy = $ctx->{copy};
936 my $s = $U->copy_status_from_name('checked out');
937 $copy->status( $s->id ) if $s;
939 my $evt = $U->update_copy( session => $ctx->{session},
940 copy => $copy, editor => $ctx->{requestor}->id );
941 return (undef,$evt) if $evt;
946 # commits the circ object to the db then fleshes the circ with rules objects
947 sub _commit_checkout_circ_object {
950 my $circ = $ctx->{circ};
954 my $r = $ctx->{session}->request(
955 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
957 return $U->DB_UPDATE_FAILED($circ) unless $r;
959 $logger->debug("Created a new circ object in checkout: $r");
962 $circ->duration_rule($ctx->{duration_rule});
963 $circ->max_fine_rule($ctx->{max_fine_rule});
964 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
970 # sees if there are any holds that this copy
971 sub _handle_related_holds {
974 my $copy = $ctx->{copy};
975 my $patron = $ctx->{patron};
976 my $holds = $holdcode->fetch_related_holds($copy->id);
980 # XXX We should only fulfill one hold here...
981 # XXX If a hold was transited to the user who is checking out
982 # the item, we need to make sure that hold is what's grabbed
983 if(ref($holds) && @$holds) {
985 # for now, just sort by id to get what should be the oldest hold
986 $holds = [ sort { $a->id <=> $b->id } @$holds ];
987 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
990 my $hold = $holds->[0];
992 $logger->debug("Related hold found in checkout: " . $hold->id );
994 $hold->current_copy($copy->id); # just make sure it's set
995 # if the hold was never officially captured, capture it.
996 $hold->capture_time('now') unless $hold->capture_time;
997 $hold->fulfillment_time('now');
998 my $r = $ctx->{session}->request(
999 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1000 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1001 push( @fulfilled, $hold->id );
1005 return (\@fulfilled, undef);
1008 sub _checkout_noncat {
1009 my ( $key, $requestor, $patron, %params ) = @_;
1010 my( $circ, $circlib, $evt );
1013 $circlib = $params{noncat_circ_lib} || $requestor->home_ou;
1015 my $count = $params{noncat_count} || 1;
1016 my $cotime = _create_date_stamp($params{checkout_time}) || "";
1017 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1019 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1020 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1021 return $evt if $evt;
1024 return OpenILS::Event->new(
1025 'SUCCESS', payload => { noncat_circ => $circ } );
1029 __PACKAGE__->register_method(
1030 method => "generic_receive",
1031 api_name => "open-ils.circ.checkin",
1034 Generic super-method for handling all copies
1035 @param authtoken The login session key
1036 @param params Hash of named parameters including:
1037 barcode - The copy barcode
1038 force - If true, copies in bad statuses will be checked in and give good statuses
1043 sub generic_receive {
1044 my( $self, $conn, $authtoken, $params ) = @_;
1045 my( $ctx, $requestor, $evt );
1047 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1048 ( $requestor, $evt ) = $U->checksesperm(
1049 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1050 return $evt if $evt;
1052 # load up the circ objects
1053 if( !( $ctx = $params->{_ctx}) ) {
1054 ( $ctx, $evt ) = create_circ_ctx( %$params,
1055 requestor => $requestor,
1056 session => $U->start_db_session(),
1058 fetch_copy_statuses => 1,
1059 fetch_copy_locations => 1,
1062 return $evt if $evt;
1064 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1065 $ctx->{authtoken} = $authtoken;
1066 my $session = $ctx->{session};
1068 my $copy = $ctx->{copy};
1069 $U->unflesh_copy($copy);
1070 return OpenILS::Event->new('COPY_NOT_FOUND') unless $copy;
1072 $logger->info("Checkin copy called by user ".
1073 $requestor->id." for copy ".$copy->id);
1075 # ------------------------------------------------------------------------------
1076 # Update the patron penalty info in the DB
1077 # ------------------------------------------------------------------------------
1078 $U->update_patron_penalties(
1079 authtoken => $authtoken,
1080 patron => $ctx->{patron},
1084 return $self->checkin_do_receive($conn, $ctx);
1087 sub checkin_do_receive {
1089 my( $self, $connection, $ctx ) = @_;
1092 my $copy = $ctx->{copy};
1093 my $session = $ctx->{session};
1094 my $requestor = $ctx->{requestor};
1095 my $change = 0; # did we actually do anything?
1097 if(!$ctx->{force}) {
1098 return $evt if ($evt = _checkin_check_copy_status($copy));
1101 ($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
1102 return $evt if ($evt and $__isrenewal); # renewals require a circulation
1104 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
1106 if( $ctx->{circ} ) {
1108 # There is an open circ on this item, close it out.
1110 $evt = _checkin_handle_circ($ctx);
1111 return $evt if $evt;
1113 } elsif( $ctx->{transit} ) {
1115 # is this item currently in transit?
1117 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1118 my $holdtrans = $evt->{holdtransit};
1119 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1121 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1123 # either an error occurred or a ROUTE_ITEM was generated and the
1124 # item must be forwarded on to its destination.
1125 return _checkin_flesh_event($ctx, $evt);
1131 # copy was received as a hold transit. Copy is at target lib
1132 # and hold transit is complete. We're done here...
1133 $U->commit_db_session($session);
1134 return _checkin_flesh_event($ctx, $evt);
1140 # ------------------------------------------------------------------------------
1141 # Circulations and transits are now closed where necessary. Now go on to see if
1142 # this copy can fulfill a hold or needs to be routed to a different location
1143 # ------------------------------------------------------------------------------
1146 # If it's a renewal, we're done
1148 $U->commit_db_session($session);
1149 return OpenILS::Event->new('SUCCESS');
1153 # Now, let's see if this copy is needed for a hold
1154 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1158 $ctx->{hold} = $hold;
1161 # Capture the hold with this copy
1162 return $evt if ($evt = _checkin_capture_hold($ctx));
1164 if( $hold->pickup_lib == $requestor->home_ou ) {
1166 # This hold was captured in the correct location
1167 $evt = OpenILS::Event->new('SUCCESS');
1171 # Hold needs to be picked up elsewhere. Build a hold
1172 # transit and route the item.
1173 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1174 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1177 } else { # not needed for a hold
1179 if( $copy->circ_lib == $requestor->home_ou ) {
1181 # Copy is in the right place.
1182 $evt = OpenILS::Event->new('SUCCESS');
1184 # if the item happens to be a pre-cataloged item, send it
1185 # to cataloging and return the event
1186 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1187 return $err if $err;
1193 # Copy wants to go home. Transit it there.
1194 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1195 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1202 $evt = OpenILS::Event->new('NO_CHANGE');
1203 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1204 if ($copy->status == $U->copy_status_from_name('on holds shelf')->id);
1208 $U->commit_db_session($session);
1211 $logger->activity("checkin by user ".$requestor->id." on item ".
1212 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1214 return _checkin_flesh_event($ctx, $evt);
1217 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1218 sub _checkin_handle_precat {
1221 my $copy = $ctx->{copy};
1226 my $catstat = $U->copy_status_from_name('cataloging');
1228 if( $ctx->{precat} ) {
1230 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1232 if( $copy->status != $catstat->id ) {
1233 $copy->status($catstat->id);
1235 return (undef, 0, $errevt) if (
1236 $errevt = $U->update_copy(
1238 editor => $ctx->{requestor}->id,
1239 session => $ctx->{session} ));
1245 return ($evt, $change, undef);
1249 sub _checkin_check_copy_status {
1251 my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1252 my $evt = OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1253 $logger->info("checking copy status id is ".$stat);
1254 return $evt if ($stat == $U->copy_status_from_name('lost')->id);
1255 return $evt if ($stat == $U->copy_status_from_name('missing')->id);
1259 # Just gets the copy back home. Returns undef on success, event on error
1260 sub _checkin_build_generic_copy_transit {
1263 my $requestor = $ctx->{requestor};
1264 my $copy = $ctx->{copy};
1265 my $transit = Fieldmapper::action::transit_copy->new;
1266 my $session = $ctx->{session};
1268 $logger->activity("User ". $requestor->id ." creating a ".
1269 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1271 $transit->source($requestor->home_ou);
1272 $transit->dest($copy->circ_lib);
1273 $transit->target_copy($copy->id);
1274 $transit->source_send_time('now');
1275 $transit->copy_status($copy->status);
1277 $logger->debug("Creating new copy_transit in DB");
1279 my $s = $session->request(
1280 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1281 return $U->DB_UPDATE_FAILED($transit) unless $s;
1283 $logger->info("Checkin copy successfully created new transit: $s");
1285 $copy->status($U->copy_status_from_name('in transit')->id );
1287 return $U->update_copy( copy => $copy,
1288 editor => $requestor->id, session => $session );
1293 # returns event on error, undef on success
1294 sub _checkin_build_hold_transit {
1297 my $copy = $ctx->{copy};
1298 my $hold = $ctx->{hold};
1299 my $trans = Fieldmapper::action::hold_transit_copy->new;
1301 $trans->hold($hold->id);
1302 $trans->source($ctx->{requestor}->home_ou);
1303 $trans->dest($hold->pickup_lib);
1304 $trans->source_send_time("now");
1305 $trans->target_copy($copy->id);
1306 $trans->copy_status($copy->status);
1308 my $id = $ctx->{session}->request(
1309 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1310 return $U->DB_UPDATE_FAILED($trans) unless $id;
1312 $logger->info("Checkin copy successfully created hold transit: $id");
1314 $copy->status($U->copy_status_from_name('in transit')->id );
1315 return $U->update_copy( copy => $copy,
1316 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1319 # Returns event on error, undef on success
1320 sub _checkin_capture_hold {
1322 my $copy = $ctx->{copy};
1323 my $hold = $ctx->{hold};
1325 $logger->debug("Checkin copy capturing hold ".$hold->id);
1327 $hold->current_copy($copy->id);
1328 $hold->capture_time('now');
1330 my $stat = $ctx->{session}->request(
1331 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1332 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1334 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1336 return $U->update_copy( copy => $copy,
1337 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1340 # fleshes an event with the relevant objects from the context
1341 sub _checkin_flesh_event {
1346 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1347 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1348 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1349 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1350 $payload->{hold} = $ctx->{hold} if $ctx->{hold};
1352 $evt->{payload} = $payload;
1357 # Closes out the circulation, puts the copy into reshelving.
1358 # Voids any bills attached to this circ after the backdate time
1359 # if a backdate is provided
1360 sub _checkin_handle_circ {
1364 my $circ = $ctx->{circ};
1365 my $copy = $ctx->{copy};
1366 my $requestor = $ctx->{requestor};
1367 my $session = $ctx->{session};
1369 $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1371 $ctx->{longoverdue} = 1 if ($circ->stop_fines =~ /longoverdue/io);
1372 $ctx->{claimsreturned} = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1374 my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1375 return $evt if $evt;
1377 $circ->stop_fines('CHECKIN');
1378 $circ->stop_fines('RENEW') if $__isrenewal;
1379 $circ->stop_fines('LOST') if($__islost);
1380 $circ->xact_finish('now') if($obt->balance_owed <= 0 and !$__islost);
1381 $circ->stop_fines_time('now');
1382 $circ->checkin_time('now');
1383 $circ->checkin_staff($requestor->id);
1385 if(my $backdate = $ctx->{backdate}) {
1386 return $evt if ($evt =
1387 _checkin_handle_backdate($backdate, $circ, $requestor, $session));
1390 $logger->info("Checkin copy setting status to 'reshelving' and committing...");
1391 $copy->status($U->copy_status_from_name('reshelving')->id);
1392 $evt = $U->update_copy( session => $session,
1393 copy => $copy, editor => $requestor->id );
1394 return $evt if $evt;
1396 $ctx->{session}->request(
1397 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1402 # returns event on error, undef on success
1403 # This voids all bills attached to the given circulation that occurred
1404 # after the backdate
1405 sub _checkin_handle_backdate {
1406 my( $backdate, $circ, $requestor, $session ) = @_;
1408 $logger->activity("User ".$requestor->id.
1409 " backdating circ [".$circ->target_copy."] to date: $backdate");
1411 $circ->xact_finish($backdate);
1413 my $bills = $session->request( # XXX Verify this call is correct
1414 "open-ils.storage.direct.money.billing.search_where.atomic",
1415 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1418 for my $bill (@$bills) {
1420 my $s = $session->request(
1421 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1422 return $U->DB_UPDATE_FAILED($bill) unless $s;
1430 sub _find_patron_from_params {
1438 if(my $barcode = $params->{barcode}) {
1439 $logger->debug("circ finding user from params with barcode $barcode");
1440 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1441 return (undef, undef, $evt) if $evt;
1442 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1443 return (undef, undef, $evt) if $evt;
1444 ($patron, $evt) = $U->fetch_user($circ->usr);
1445 return (undef, undef, $evt) if $evt;
1447 return ($patron, $copy);
1451 # ------------------------------------------------------------------------------
1453 __PACKAGE__->register_method(
1455 api_name => "open-ils.circ.renew",
1456 notes => <<" NOTES");
1457 PARAMS( authtoken, circ => circ_id );
1458 open-ils.circ.renew(login_session, circ_object);
1459 Renews the provided circulation. login_session is the requestor of the
1460 renewal and if the logged in user is not the same as circ->usr, then
1461 the logged in user must have RENEW_CIRC permissions.
1465 my( $self, $client, $authtoken, $params ) = @_;
1468 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1471 # fetch the patron object one way or another
1472 if( $params->{patron} ) {
1473 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1474 if($evt) { $__isrenewal = 0; return $evt; }
1476 } elsif( $params->{patron_barcode} ) {
1477 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1478 if($evt) { $__isrenewal = 0; return $evt; }
1481 ($patron, $copy, $evt) = _find_patron_from_params($params);
1482 return $evt if $evt;
1483 $params->{copy} = $copy;
1486 # verify our login session
1487 ($requestor, $evt) = $U->checkses($authtoken);
1488 if($evt) { $__isrenewal = 0; return $evt; }
1490 # make sure we have permission to perform a renewal
1491 if( $requestor->id ne $patron->id ) {
1492 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'RENEW_CIRC');
1493 if($evt) { $__isrenewal = 0; return $evt; }
1497 # fetch and build the circulation environment
1498 ( $ctx, $evt ) = create_circ_ctx( %$params,
1500 requestor => $requestor,
1503 #fetch_patron_circ_summary => 1,
1504 fetch_copy_statuses => 1,
1505 fetch_copy_locations => 1,
1507 if($evt) { $__isrenewal = 0; return $evt; }
1508 $params->{_ctx} = $ctx;
1510 # make sure they have some renewals left and make sure the circulation exists
1511 ($circ, $evt) = _check_renewal_remaining($ctx);
1512 if($evt) { $__isrenewal = 0; return $evt; }
1513 $ctx->{old_circ} = $circ;
1514 my $renewals = $circ->renewal_remaining - 1;
1516 # run the renew permit script
1517 $evt = _run_renew_scripts($ctx);
1518 if($evt) { $__isrenewal = 0; return $evt; }
1521 #$ctx->{patron} = $ctx->{patron}->id;
1522 $evt = $self->generic_receive($client, $authtoken, $ctx );
1523 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1525 if( !$U->event_equals($evt, 'SUCCESS') ) {
1526 $__isrenewal = 0; return $evt;
1529 # re-fetch the context since objects have changed in the checkin
1530 ( $ctx, $evt ) = create_circ_ctx( %$params,
1532 requestor => $requestor,
1535 #fetch_patron_circ_summary => 1,
1536 fetch_copy_statuses => 1,
1537 fetch_copy_locations => 1,
1539 if($evt) { $__isrenewal = 0; return $evt; }
1540 $params->{_ctx} = $ctx;
1541 $ctx->{renewal_remaining} = $renewals;
1543 # run the circ permit scripts
1544 if( $ctx->{permit_override} ) {
1545 $evt = $U->check_perms(
1546 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1547 if($evt) { $__isrenewal = 0; return $evt; }
1550 $evt = $self->permit_circ( $client, $authtoken, $params );
1551 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1555 if(!$U->event_equals($evt, 'SUCCESS')) {
1556 if($evt) { $__isrenewal = 0; return $evt; }
1559 $params->{permit_key} = $evt->{payload};
1563 # checkout the item again
1564 $params->{patron} = $ctx->{patron}->id;
1565 $evt = $self->checkout($client, $authtoken, $params );
1567 $logger->activity("user ".$requestor->id." renewl of item ".
1568 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1574 sub _check_renewal_remaining {
1577 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1578 return (undef, $evt) if $evt;
1579 $evt = OpenILS::Event->new(
1580 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1581 return ($circ, $evt);
1584 sub _run_renew_scripts {
1586 my $runner = $ctx->{runner};
1589 $runner->load($scripts{circ_permit_renew});
1590 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1592 my $events = $runner->retrieve('result.events');
1593 $events = [ split(/,/, $events) ];
1594 $logger->activity("circ_permit_renew for user ".
1595 $ctx->{patron}->id." returned events: @$events") if @$events;
1598 push( @allevents, OpenILS::Event->new($_)) for @$events;
1599 return \@allevents if @allevents;