]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
circ due dates are now pushed forward past the closed date
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Circ / Circulate.pm
1 package OpenILS::Application::Circ::Circulate;
2 use base 'OpenSRF::Application';
3 use strict; use warnings;
4 use OpenSRF::EX qw(:try);
5 use Data::Dumper;
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);
15 use DateTime;
16 use DateTime::Format::ISO8601;
17 use OpenSRF::Utils qw/:datetime/;
18
19 $Data::Dumper::Indent = 0;
20 my $apputils    = "OpenILS::Application::AppUtils";
21 my $U                           = $apputils;
22 my $holdcode    = "OpenILS::Application::Circ::Holds";
23 my $transcode   = "OpenILS::Application::Circ::Transit";
24
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
30
31 sub PRECAT_FINE_LEVEL { return 2; }
32 sub PRECAT_LOAN_DURATION { return 2; }
33
34 my %RECORD_FROM_COPY_CACHE;
35
36
37 # for security, this is a process-defined and not
38 # a client-defined variable
39 my $__isrenewal = 0;
40
41 # ------------------------------------------------------------------------------
42 # Load the circ script from the config
43 # ------------------------------------------------------------------------------
44 sub initialize {
45
46         my $self = shift;
47         $cache_handle = OpenSRF::Utils::Cache->new('global');
48         my $conf = OpenSRF::Utils::SettingsClient->new;
49         my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
50         my @pfx = ( @pfx2, "scripts" );
51
52         my $p           = $conf->config_value(  @pfx, 'circ_permit_patron' );
53         my $c           = $conf->config_value(  @pfx, 'circ_permit_copy' );
54         my $d           = $conf->config_value(  @pfx, 'circ_duration' );
55         my $f           = $conf->config_value(  @pfx, 'circ_recurring_fines' );
56         my $m           = $conf->config_value(  @pfx, 'circ_max_fines' );
57         my $pr  = $conf->config_value(  @pfx, 'circ_permit_renew' );
58         my $lb  = $conf->config_value(  @pfx2, 'script_path' );
59
60         $logger->error( "Missing circ script(s)" ) 
61                 unless( $p and $c and $d and $f and $m and $pr );
62
63         $scripts{circ_permit_patron}    = $p;
64         $scripts{circ_permit_copy}              = $c;
65         $scripts{circ_duration}                 = $d;
66         $scripts{circ_recurring_fines}= $f;
67         $scripts{circ_max_fines}                = $m;
68         $scripts{circ_permit_renew}     = $pr;
69
70         $lb = [ $lb ] unless ref($lb);
71         $script_libs = $lb;
72
73         $logger->debug("Loaded rules scripts for circ: " .
74                 "circ permit patron: $p, circ permit copy: $c, ".
75                 "circ duration :$d , circ recurring fines : $f, " .
76                 "circ max fines : $m, circ renew permit : $pr");
77 }
78
79
80 # ------------------------------------------------------------------------------
81 # Loads the necessary circ objects and pushes them into the script environment
82 # Returns ( $data, $evt ).  if $evt is defined, then an
83 # unexpedted event occurred and should be dealt with / returned to the caller
84 # ------------------------------------------------------------------------------
85 sub create_circ_ctx {
86         my %params = @_;
87         $U->logmark;
88
89         my $evt;
90         my $ctx = \%params;
91
92         $evt = _ctx_add_patron_objects($ctx, %params);
93         return (undef,$evt) if $evt;
94
95         if(!$params{noncat}) {
96                 if( $evt = _ctx_add_copy_objects($ctx, %params) ) {
97                         $ctx->{precat} = 1 if($evt->{textcode} eq 'ASSET_COPY_NOT_FOUND')
98                 } else {
99                         $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
100                 }
101         }
102
103         _doctor_patron_object($ctx) if $ctx->{patron};
104         _doctor_copy_object($ctx) if $ctx->{copy};
105
106         if(!$ctx->{no_runner}) {
107                 _build_circ_script_runner($ctx);
108                 _add_script_runner_methods($ctx);
109         }
110
111         return $ctx;
112 }
113
114 sub _ctx_add_patron_objects {
115         my( $ctx, %params) = @_;
116         $U->logmark;
117
118         # - patron standings are now handled in the penalty server...
119
120         #if(!defined($cache{patron_standings})) {
121         #       $cache{patron_standings} = $U->fetch_patron_standings();
122         #}
123         #$ctx->{patron_standings} = $cache{patron_standings};
124
125         $cache{group_tree} = $U->fetch_permission_group_tree() unless $cache{group_tree};
126         $ctx->{group_tree} = $cache{group_tree};
127
128         $ctx->{patron_circ_summary} = 
129                 $U->fetch_patron_circ_summary($ctx->{patron}->id) 
130                 if $params{fetch_patron_circsummary};
131
132         return undef;
133 }
134
135
136 sub _find_copy_by_attr {
137         my %params = @_;
138         $U->logmark;
139         my $evt;
140
141         my $copy = $params{copy} || undef;
142
143         if(!$copy) {
144
145                 ( $copy, $evt ) = 
146                         $U->fetch_copy($params{copyid}) if $params{copyid};
147                 return (undef,$evt) if $evt;
148
149                 if(!$copy) {
150                         ( $copy, $evt ) = 
151                                 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
152                         return (undef,$evt) if $evt;
153                 }
154         }
155         return ( $copy, $evt );
156 }
157
158 sub _ctx_add_copy_objects {
159         my($ctx, %params)  = @_;
160         $U->logmark;
161         my $evt;
162         my $copy;
163
164         $cache{copy_statuses} = $U->fetch_copy_statuses 
165                 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
166
167         $cache{copy_locations} = $U->fetch_copy_locations 
168                 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
169
170         $ctx->{copy_statuses} = $cache{copy_statuses};
171         $ctx->{copy_locations} = $cache{copy_locations};
172
173         ($copy, $evt) = _find_copy_by_attr(%params);
174         return $evt if $evt;
175
176         if( $copy and !$ctx->{title} ) {
177                 $logger->debug("Copy status: " . $copy->status);
178
179                 my $r = $RECORD_FROM_COPY_CACHE{$copy->id};
180                 ($r, $evt) = $U->fetch_record_by_copy( $copy->id ) unless $r;
181                 return $evt if $evt;
182                 $RECORD_FROM_COPY_CACHE{$copy->id} = $r;
183
184                 $ctx->{title} = $r;
185                 $ctx->{copy} = $copy;
186         }
187
188         return undef;
189 }
190
191
192 # ------------------------------------------------------------------------------
193 # Fleshes parts of the patron object
194 # ------------------------------------------------------------------------------
195 sub _doctor_copy_object {
196         my $ctx = shift;
197         $U->logmark;
198         my $copy = $ctx->{copy} || return undef;
199
200         $logger->debug("Doctoring copy object...");
201
202         # set the copy status to a status name
203         $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
204
205         # set the copy location to the location object
206         $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
207
208         $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
209 }
210
211
212 # ------------------------------------------------------------------------------
213 # Fleshes parts of the patron object
214 # ------------------------------------------------------------------------------
215 sub _doctor_patron_object {
216         my $ctx = shift;
217         $U->logmark;
218         my $patron = $ctx->{patron} || return undef;
219
220         # push the standing object into the patron
221 #       if(ref($ctx->{patron_standings})) {
222 #               for my $s (@{$ctx->{patron_standings}}) {
223 #                       if( $s->id eq $ctx->{patron}->standing ) {
224 #                               $patron->standing($s);
225 #                               $logger->debug("Set patron standing to ". $s->value);
226 #                       }
227 #               }
228 #       }
229
230         # set the patron ptofile to the profile name
231         $patron->profile( _get_patron_profile( 
232                 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
233
234         # flesh the org unit
235         $patron->home_ou( 
236                 $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
237
238 }
239
240 # recurse and find the patron profile name from the tree
241 # another option would be to grab the groups for the patron
242 # and cycle through those until the "profile" group has been found
243 sub _get_patron_profile { 
244         my( $patron, $group_tree ) = @_;
245         return $group_tree if ($group_tree->id eq $patron->profile);
246         return undef unless ($group_tree->children);
247
248         for my $child (@{$group_tree->children}) {
249                 my $ret = _get_patron_profile( $patron, $child );
250                 return $ret if $ret;
251         }
252         return undef;
253 }
254
255 sub _get_copy_status {
256         my( $copy, $cstatus ) = @_;
257         $U->logmark;
258         my $s = undef;
259         for my $status (@$cstatus) {
260                 $s = $status if( $status->id eq $copy->status ) 
261         }
262         $logger->debug("Retrieving copy status: " . $s->name) if $s;
263         return $s;
264 }
265
266 sub _get_copy_location {
267         my( $copy, $locations ) = @_;
268         $U->logmark;
269         my $l = undef;
270         for my $loc (@$locations) {
271                 $l = $loc if $loc->id eq $copy->location;
272         }
273         $logger->debug("Retrieving copy location: " . $l->name ) if $l;
274         return $l;
275 }
276
277
278 # ------------------------------------------------------------------------------
279 # Constructs and shoves data into the script environment
280 # ------------------------------------------------------------------------------
281 sub _build_circ_script_runner {
282         my $ctx = shift;
283         $U->logmark;
284
285         $logger->debug("Loading script environment for circulation");
286
287         my $runner;
288         if( $runner = $contexts{$ctx->{type}} ) {
289                 $runner->refresh_context;
290         } else {
291                 $runner = OpenILS::Utils::ScriptRunner->new;
292                 $contexts{type} = $runner;
293         }
294
295         for(@$script_libs) {
296                 $logger->debug("Loading circ script lib path $_");
297                 $runner->add_path( $_ );
298         }
299
300         # Note: inserting the number 0 into the script turns into the
301         # string "0", and thus evaluates to true in JS land
302         # inserting undef will insert "", which evaluates to false
303
304         $runner->insert( 'environment.patron',  $ctx->{patron}, 1);
305         $runner->insert( 'environment.title',   $ctx->{title}, 1);
306         $runner->insert( 'environment.copy',    $ctx->{copy}, 1);
307
308         # circ script result
309         $runner->insert( 'result', {} );
310         #$runner->insert( 'result.event', 'SUCCESS' );
311         $runner->insert( 'result.events', [] );
312
313         if($__isrenewal) {
314                 $runner->insert('environment.isRenewal', 1);
315         } else {
316                 $runner->insert('environment.isRenewal', undef);
317         }
318
319         if($ctx->{ishold} ) { 
320                 $runner->insert('environment.isHold', 1); 
321         } else{ 
322                 $runner->insert('environment.isHold', undef) 
323         }
324
325         if( $ctx->{noncat} ) {
326                 $runner->insert('environment.isNonCat', 1);
327                 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
328         } else {
329                 $runner->insert('environment.isNonCat', undef);
330         }
331
332 #       if(ref($ctx->{patron_circ_summary})) {
333 #               $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
334 #               $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
335 #       }
336
337         $ctx->{runner} = $runner;
338         return $runner;
339 }
340
341
342 sub _add_script_runner_methods {
343         my $ctx = shift;
344         $U->logmark;
345         my $runner = $ctx->{runner};
346
347         if( $ctx->{copy} ) {
348                 
349                 # allows a script to fetch a hold that is currently targeting the
350                 # copy in question
351                 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
352                                 my $key = shift;
353                                 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
354                                 $hold = undef unless $hold;
355                                 $runner->insert( $key, $hold, 1 );
356                         }
357                 );
358         }
359 }
360
361 # ------------------------------------------------------------------------------
362
363 __PACKAGE__->register_method(
364         method  => "permit_circ",
365         api_name        => "open-ils.circ.checkout.permit",
366         notes           => q/
367                 Determines if the given checkout can occur
368                 @param authtoken The login session key
369                 @param params A trailing hash of named params including 
370                         barcode : The copy barcode, 
371                         patron : The patron the checkout is occurring for, 
372                         renew : true or false - whether or not this is a renewal
373                 @return The event that occurred during the permit check.  
374         /);
375
376 __PACKAGE__->register_method (
377         method          => 'permit_circ',
378         api_name                => 'open-ils.circ.checkout.permit.override',
379         signature       => q/@see open-ils.circ.checkout.permit/,
380 );
381
382 sub permit_circ {
383         my( $self, $client, $authtoken, $params ) = @_;
384         $U->logmark;
385
386         my $override = $params->{override} = 1 if $self->api_name =~ /override/o;
387
388         my ( $requestor, $patron, $ctx, $evt, $circ );
389
390         # check permisson of the requestor
391         ( $requestor, $patron, $evt ) = 
392                 $U->checkses_requestor( 
393                 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
394         return $evt if $evt;
395
396
397         # fetch and build the circulation environment
398         if( !( $ctx = $params->{_ctx}) ) {
399
400                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
401                         patron                                                  => $patron, 
402                         requestor                                               => $requestor, 
403                         type                                                            => 'circ',
404                         #fetch_patron_circ_summary      => 1,
405                         fetch_copy_statuses                     => 1, 
406                         fetch_copy_locations                    => 1, 
407                         );
408                 return $evt if $evt;
409         }
410
411         my $copy = $ctx->{copy};
412         if($copy) {
413                 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
414                 return OpenILS::Event->new('COPY_IN_TRANSIT') 
415                         if $stat == $U->copy_status_from_name('in transit')->id;
416         }
417
418         $ctx->{authtoken} = $authtoken;
419
420         $evt = undef;
421         if( $ctx->{copy} and ($evt = _handle_claims_returned($ctx)) ) {
422                 return $evt unless $U->event_equals($evt, 'SUCCESS');
423         }
424
425         if($evt) { 
426                 $evt = undef;
427
428         } else { 
429
430                 # no claims returned circ was found, check if there is any open circ
431                 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
432                         ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
433                         return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
434                 }
435         }
436
437
438         $ctx->{permit_key} = _cache_permit_key();
439         my $events = _run_permit_scripts($ctx);
440
441         if( $override ) {
442                 $evt = override_events($requestor, $requestor->ws_ou, 
443                         $events, $authtoken, $ctx->{copy}->id, $client);
444                 return $evt if $evt;
445                 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
446         }
447
448         return $events;
449 }
450
451 sub override_events {
452
453         my( $requestor, $org, $events, $authtoken, $copyid, $conn ) = @_;
454         $events = [ $events ] unless ref($events) eq 'ARRAY';
455         my @failed;
456
457         for my $e (@$events) {
458                 my $tc = $e->{textcode};
459                 next if $tc eq 'SUCCESS';
460                 my $ov = "$tc.override";
461                 $logger->info("attempting to override event $ov");
462                 my $evt = $U->check_perms( $requestor->id, $org, $ov );
463                 return $evt if $evt;
464         }
465
466         return undef;
467 }
468
469
470 __PACKAGE__->register_method(
471         method  => "check_title_hold",
472         api_name        => "open-ils.circ.title_hold.is_possible",
473         notes           => q/
474                 Determines if a hold were to be placed by a given user,
475                 whether or not said hold would have any potential copies
476                 to fulfill it.
477                 @param authtoken The login session key
478                 @param params A hash of named params including:
479                         patronid  - the id of the hold recipient
480                         titleid (brn) - the id of the title to be held
481                         depth   - the hold range depth (defaults to 0)
482         /);
483
484 # XXX add pickup lib to the call to test for perms
485
486 sub check_title_hold {
487         my( $self, $client, $authtoken, $params ) = @_;
488         my %params = %$params;
489         my $titleid = $params{titleid};
490
491         my ( $requestor, $patron, $evt ) = $U->checkses_requestor( 
492                 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
493         return $evt if $evt;
494
495         my $rangelib    = $patron->home_ou;
496         my $depth               = $params{depth} || 0;
497         my $pickup              = $params{pickup_lib};
498
499         $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
500
501         my $org = $U->simplereq(
502                 'open-ils.actor', 
503                 'open-ils.actor.org_unit.retrieve', 
504                 $authtoken, $requestor->home_ou );
505
506         my $limit       = 10;
507         my $offset      = 0;
508         my $title;
509
510         while( $title = $U->storagereq(
511                                 'open-ils.storage.biblio.record_entry.ranged_tree', 
512                                 $titleid, $rangelib, $depth, $limit, $offset ) ) {
513
514                 last unless ref($title);
515
516                 for my $cn (@{$title->call_numbers}) {
517         
518                         $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
519         
520                         for my $copy (@{$cn->copies}) {
521         
522                                 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
523         
524                                 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
525                                         {       patron                          => $patron, 
526                                                 requestor                       => $requestor, 
527                                                 copy                                    => $copy,
528                                                 title                                   => $title, 
529                                                 title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
530                                                 pickup_lib                      => $pickup,
531                                                 request_lib                     => $org } );
532         
533                                 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
534                         }
535                 }
536
537                 $offset += $limit;
538         }
539
540         return 0;
541 }
542
543
544 # Runs the patron and copy permit scripts
545 # if this is a non-cat circulation, the copy permit script 
546 # is not run
547 sub _run_permit_scripts {
548
549         my $ctx                 = shift;
550         my $runner              = $ctx->{runner};
551         my $patronid    = $ctx->{patron}->id;
552         my $barcode             = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
553         my $key                 = $ctx->{permit_key};
554
555         my $penalties = $U->update_patron_penalties( 
556                 authtoken => $ctx->{authtoken}, 
557                 patron    => $ctx->{patron} 
558         );
559
560         $penalties = $penalties->{fatal_penalties};
561
562         $logger->info("circ patron penalties user $patronid: @$penalties");
563
564         if( $ctx->{noncat} ) {
565                 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
566                 return OpenILS::Event->new('SUCCESS', payload => $key);
567         }
568
569         if($ctx->{precat}) {
570                 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
571                 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
572         }
573
574         if($ctx->{ishold}) {
575                 $logger->debug("Exiting circ permit early because request is for hold patron permit");
576                 return OpenILS::Event->new('SUCCESS');
577         }
578
579         $runner->load($scripts{circ_permit_copy});
580         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
581
582         # ---------------------------------------------------------------------
583         # Capture all of the copy permit events
584         # ---------------------------------------------------------------------
585         my $copy_events = $runner->retrieve('result.events');
586         $copy_events = [ split(/,/, $copy_events) ]; 
587         $ctx->{circ_permit_copy_events} = $copy_events;
588         $logger->activity("circ_permit_copy for copy ".
589                 "$barcode returned events: @$copy_events") if @$copy_events;
590
591         my @allevents;
592         push( @allevents, OpenILS::Event->new($_)) for @$penalties;
593         push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
594
595         my $ae = _check_copy_alert($ctx->{copy});
596         push( @allevents, $ae ) if $ae;
597
598         return OpenILS::Event->new('SUCCESS', payload => $key) unless (@allevents);
599
600         # uniquify the events
601         my %hash = map { ($_->{ilsevent} => $_) } @allevents;
602         @allevents = values %hash;
603
604         for (@allevents) {
605                 $_->{payload} = $ctx->{copy}->status->id
606                         if ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
607         }
608
609         return \@allevents;
610 }
611
612 sub _check_copy_alert {
613         my $copy = shift;
614         return OpenILS::Event->new('COPY_ALERT_MESSAGE', 
615                 payload => $copy->alert_message) if $copy->alert_message;
616         return undef;
617 }
618
619 # takes copyid, patronid, and requestor id
620 sub _cache_permit_key {
621         my $key = md5_hex( time() . rand() . "$$" );
622         $logger->debug("Setting circ permit key to $key");
623         $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
624         return $key;
625 }
626
627 sub _check_permit_key {
628         my $key = shift;
629         $logger->debug("Fetching circ permit key $key");
630         my $k = "oils_permit_key_$key";
631         my $one = $cache_handle->get_cache($k);
632         $cache_handle->delete_cache($k);
633         return ($one) ? 1 : 0;
634 }
635
636
637 # ------------------------------------------------------------------------------
638
639 __PACKAGE__->register_method(
640         method  => "checkout",
641         api_name        => "open-ils.circ.checkout",
642         notes => q/
643                 Checks out an item
644                 @param authtoken The login session key
645                 @param params A named hash of params including:
646                         copy                    The copy object
647                         barcode         If no copy is provided, the copy is retrieved via barcode
648                         copyid          If no copy or barcode is provide, the copy id will be use
649                         patron          The patron's id
650                         noncat          True if this is a circulation for a non-cataloted item
651                         noncat_type     The non-cataloged type id
652                         noncat_circ_lib The location for the noncat circ.  
653                         precat          The item has yet to be cataloged
654                         dummy_title The temporary title of the pre-cataloded item
655                         dummy_author The temporary authr of the pre-cataloded item
656                                 Default is the home org of the staff member
657                 @return The SUCCESS event on success, any other event depending on the error
658         /);
659
660 sub checkout {
661         my( $self, $client, $authtoken, $params ) = @_;
662         $U->logmark;
663
664         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
665         my $key = $params->{permit_key};
666
667         # if this is a renewal, then the requestor does not have to
668         # have checkout privelages
669         ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
670         ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
671         return $evt if $evt;
672
673         if( $params->{patron} ) {
674                 ( $patron, $evt ) = $U->fetch_user($params->{patron});
675                 return $evt if $evt;
676         } else {
677                 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
678                 return $evt if $evt;
679         }
680
681         # set the circ lib to the home org of the requestor if not specified
682         my $circlib = (defined($params->{circ_lib})) ? 
683                 $params->{circ_lib} : $requestor->ws_ou;
684
685
686         # Make sure the caller has a valid permit key or is 
687         # overriding the permit can
688         if( $params->{permit_override} ) {
689                 $evt = $U->check_perms(
690                         $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
691                 return $evt if $evt;
692
693         } else {
694                 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY') 
695                         unless _check_permit_key($key);
696         }
697
698         # if this is a non-cataloged item, check it out and return
699         return _checkout_noncat( 
700                 $key, $requestor, $patron, %$params ) if $params->{noncat};
701
702         # if this item has yet to be cataloged, make sure a dummy copy exists
703         ( $params->{copy}, $evt ) = _make_precat_copy(
704                 $requestor, $circlib, $params ) if $params->{precat};
705         return $evt if $evt;
706
707
708         # fetch and build the circulation environment
709         if( !( $ctx = $params->{_ctx}) ) {
710                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
711                         patron                                                  => $patron, 
712                         requestor                                               => $requestor, 
713                         session                                                 => $U->start_db_session(),
714                         type                                                            => 'circ',
715                         #fetch_patron_circ_summary      => 1,
716                         fetch_copy_statuses                     => 1, 
717                         fetch_copy_locations                    => 1, 
718                         );
719                 return $evt if $evt;
720         }
721         $ctx->{session} = $U->start_db_session() unless $ctx->{session};
722
723         # if the call doesn't know it's not cataloged..
724         if(!$params->{precat}) {
725                 if( $ctx->{copy}->call_number eq '-1' ) {
726                         return OpenILS::Event->new('ITEM_NOT_CATALOGED');
727                 }
728         }
729
730
731         $copy = $ctx->{copy};
732         if($copy) {
733                 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
734                 return OpenILS::Event->new('COPY_IN_TRANSIT') 
735                         if $stat == $U->copy_status_from_name('in transit')->id;
736         }
737
738         # this happens in permit.. but we need to check here for 'offline' requests
739         ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
740         return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
741
742         my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
743
744
745         $ctx->{circ_lib} = $circlib;
746
747         $evt = _run_checkout_scripts($ctx);
748         return $evt if $evt;
749
750
751         _build_checkout_circ_object($ctx);
752
753         $evt = _apply_modified_due_date($ctx);
754         return $evt if $evt;
755
756         $evt = _commit_checkout_circ_object($ctx);
757         return $evt if $evt;
758
759         $evt = _update_checkout_copy($ctx);
760         return $evt if $evt;
761
762         my $holds;
763         ($holds, $evt) = _handle_related_holds($ctx);
764         return $evt if $evt;
765
766
767         $logger->debug("Checkout committing objects with session thread trace: ".$ctx->{session}->session_id);
768         $U->commit_db_session($ctx->{session});
769         my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
770
771         $logger->activity("user ".$requestor->id." successfully checked out item ".
772                 $ctx->{copy}->barcode." to user ".$ctx->{patron}->id );
773
774
775         # ------------------------------------------------------------------------------
776         # Update the patron penalty info in the DB
777         # ------------------------------------------------------------------------------
778         $U->update_patron_penalties( 
779                 authtoken => $authtoken, 
780                 patron    => $ctx->{patron} ,
781                 background      => 1,
782         );
783
784         return OpenILS::Event->new('SUCCESS', 
785                 payload => { 
786                         copy                                    => $U->unflesh_copy($ctx->{copy}),
787                         circ                                    => $ctx->{circ},
788                         record                          => $record,
789                         holds_fulfilled => $holds,
790                 } 
791         )
792 }
793
794
795 sub _make_precat_copy {
796         my ( $requestor, $circlib, $params ) =  @_;
797         $U->logmark;
798         my( $copy, undef ) = _find_copy_by_attr(%$params);
799
800         if($copy) {
801                 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
802
803                 $copy->editor($requestor->id);
804                 $copy->edit_date('now');
805                 $copy->dummy_title($params->{dummy_title});
806                 $copy->dummy_author($params->{dummy_author});
807
808                 my $stat = $U->storagereq(
809                         'open-ils.storage.direct.asset.copy.update', $copy );
810
811                 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $stat;
812                 return ($copy);
813         }
814
815         $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
816
817         my $evt = OpenILS::Event->new(
818                 'BAD_PARAMS', desc => "Dummy title or author not provided" ) 
819                 unless ( $params->{dummy_title} and $params->{dummy_author} );
820         return (undef, $evt) if $evt;
821
822         $copy = Fieldmapper::asset::copy->new;
823         $copy->circ_lib($circlib);
824         $copy->creator($requestor->id);
825         $copy->editor($requestor->id);
826         $copy->barcode($params->{barcode});
827         $copy->call_number(-1); #special CN for precat materials
828         $copy->loan_duration(&PRECAT_LOAN_DURATION); 
829         $copy->fine_level(&PRECAT_FINE_LEVEL);
830
831         $copy->dummy_title($params->{dummy_title});
832         $copy->dummy_author($params->{dummy_author});
833
834         my $id = $U->storagereq(
835                 'open-ils.storage.direct.asset.copy.create', $copy );
836         return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
837
838         $logger->debug("Pre-cataloged copy successfully created");
839         return ($U->fetch_copy($id));
840 }
841
842
843 sub _run_checkout_scripts {
844         my $ctx = shift;
845         $U->logmark;
846         my $evt;
847         my $circ;
848
849         my $runner = $ctx->{runner};
850
851         $runner->insert('result.durationLevel');
852         $runner->insert('result.durationRule');
853         $runner->insert('result.recurringFinesRule');
854         $runner->insert('result.recurringFinesLevel');
855         $runner->insert('result.maxFine');
856
857         $runner->load($scripts{circ_duration});
858         $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
859         my $duration = $runner->retrieve('result.durationRule');
860         $logger->debug("Circ duration script yielded a duration rule of: $duration");
861
862         $runner->load($scripts{circ_recurring_fines});
863         $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
864         my $recurring = $runner->retrieve('result.recurringFinesRule');
865         $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
866
867         $runner->load($scripts{circ_max_fines});
868         $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
869         my $max_fine = $runner->retrieve('result.maxFine');
870         $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
871
872         ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
873         return $evt if $evt;
874         ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
875         return $evt if $evt;
876         ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
877         return $evt if $evt;
878
879         $ctx->{duration_level}                  = $runner->retrieve('result.durationLevel');
880         $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
881         $ctx->{duration_rule}                   = $duration;
882         $ctx->{recurring_fines_rule}    = $recurring;
883         $ctx->{max_fine_rule}                   = $max_fine;
884
885         return undef;
886 }
887
888 sub _build_checkout_circ_object {
889         my $ctx = shift;
890         $U->logmark;
891
892         my $circ                        = new Fieldmapper::action::circulation;
893         my $duration    = $ctx->{duration_rule};
894         my $max                 = $ctx->{max_fine_rule};
895         my $recurring   = $ctx->{recurring_fines_rule};
896         my $copy                        = $ctx->{copy};
897         my $patron              = $ctx->{patron};
898         my $dur_level   = $ctx->{duration_level};
899         my $rec_level   = $ctx->{recurring_fines_level};
900
901         $circ->duration( $duration->shrt ) if ($dur_level == 1);
902         $circ->duration( $duration->normal ) if ($dur_level == 2);
903         $circ->duration( $duration->extended ) if ($dur_level == 3);
904
905         $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
906         $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
907         $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
908
909         $circ->duration_rule( $duration->name );
910         $circ->recuring_fine_rule( $recurring->name );
911         $circ->max_fine_rule( $max->name );
912         $circ->max_fine( $max->amount );
913
914         $circ->fine_interval($recurring->recurance_interval);
915         $circ->renewal_remaining( $duration->max_renewals );
916         $circ->target_copy( $copy->id );
917         $circ->usr( $patron->id );
918         $circ->circ_lib( $ctx->{circ_lib} );
919
920         if( $__isrenewal ) {
921                 $logger->debug("Circ is a renewal.  Setting renewal_remaining to " . $ctx->{renewal_remaining} );
922                 $circ->opac_renewal(1); 
923                 $circ->renewal_remaining($ctx->{renewal_remaining});
924                 $circ->circ_staff($ctx->{requestor}->id);
925         } 
926
927
928         # if the user provided an overiding checkout time, 
929         # (e.g. the checkout really happened several hours ago), then
930         # we apply that here.  Does this need a perm??
931         if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
932                 $logger->debug("circ setting checkout_time to $ds");
933                 $circ->xact_start($ds);
934         }
935
936         # if a patron is renewing, 'requestor' will be the patron
937         $circ->circ_staff($ctx->{requestor}->id ); 
938         _set_circ_due_date($circ);
939         $ctx->{circ} = $circ;
940 }
941
942 sub _apply_modified_due_date {
943         my $ctx = shift;
944         my $circ = $ctx->{circ};
945
946         $U->logmark;
947
948         if( $ctx->{due_date} ) {
949
950                 my $evt = $U->check_perms(
951                         $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
952                 return $evt if $evt;
953
954                 my $ds = _create_date_stamp($ctx->{due_date});
955                 $logger->debug("circ modifying  due_date to $ds");
956                 $circ->due_date($ds);
957
958         } else {
959
960                 # if the due_date lands on a day when the location is closed
961                 my $copy = $ctx->{copy};
962                 return unless $copy;
963
964                 $logger->info("circ searching for closed date overlap on lib ".
965                         $copy->circ_lib->id ." with an item due date of ".$circ->due_date );
966
967                 my $dateinfo = $ctx->{session}->request(
968                         'open-ils.storage.actor.org_unit.closed_date.overlap',
969                         $copy->circ_lib->id, $circ->due_date )->gather(1);
970
971
972                 if($dateinfo) {
973                         $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
974                                 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
975
976                                 # XXX make the behavior more dynamic
977                                 # for now, we just push the due date to after the close date
978                                 $circ->due_date($dateinfo->{end});
979                 }
980
981         }
982         return undef;
983 }
984
985 sub _create_date_stamp {
986         my $datestring = shift;
987         return undef unless $datestring;
988         $datestring = clense_ISO8601($datestring);
989         $logger->debug("circ created date stamp => $datestring");
990         return $datestring;
991 }
992
993 sub _create_due_date {
994         my $duration = shift;
995         $U->logmark;
996         my ($sec,$min,$hour,$mday,$mon,$year) = 
997                 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
998         $year += 1900; $mon += 1;
999         my $due_date = sprintf(
1000         '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1001         $year, $mon, $mday, $hour, $min, $sec);
1002         return $due_date;
1003 }
1004
1005 sub _set_circ_due_date {
1006         my $circ = shift;
1007         $U->logmark;
1008         my $dd = _create_due_date($circ->duration);
1009         $logger->debug("Checkout setting due date on circ to: $dd");
1010         $circ->due_date($dd);
1011 }
1012
1013 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
1014 sub _update_checkout_copy {
1015         my $ctx = shift;
1016         $U->logmark;
1017         my $copy = $ctx->{copy};
1018
1019         my $s = $U->copy_status_from_name('checked out');
1020         $copy->status( $s->id ) if $s;
1021
1022         my $evt = $U->update_copy( session => $ctx->{session}, 
1023                 copy => $copy, editor => $ctx->{requestor}->id );
1024         return (undef,$evt) if $evt;
1025
1026         return undef;
1027 }
1028
1029 # commits the circ object to the db then fleshes the circ with rules objects
1030 sub _commit_checkout_circ_object {
1031
1032         my $ctx = shift;
1033         my $circ = $ctx->{circ};
1034         $U->logmark;
1035
1036         $circ->clear_id;
1037         my $r = $ctx->{session}->request(
1038                 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
1039
1040         return $U->DB_UPDATE_FAILED($circ) unless $r;
1041
1042         $logger->debug("Created a new circ object in checkout: $r");
1043
1044         $circ->id($r);
1045         $circ->duration_rule($ctx->{duration_rule});
1046         $circ->max_fine_rule($ctx->{max_fine_rule});
1047         $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
1048
1049         return undef;
1050 }
1051
1052
1053 # sees if there are any holds that this copy 
1054 sub _handle_related_holds {
1055
1056         my $ctx         = shift;
1057         my $copy                = $ctx->{copy};
1058         my $patron      = $ctx->{patron};
1059         my $holds       = $holdcode->fetch_related_holds($copy->id);
1060         $U->logmark;
1061         my @fulfilled;
1062
1063         # XXX We should only fulfill one hold here...
1064         # XXX If a hold was transited to the user who is checking out
1065         # the item, we need to make sure that hold is what's grabbed
1066         if(ref($holds) && @$holds) {
1067
1068                 # for now, just sort by id to get what should be the oldest hold
1069                 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1070                 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
1071
1072                 if(@$holds) {
1073                         my $hold = $holds->[0];
1074
1075                         $logger->debug("Related hold found in checkout: " . $hold->id );
1076
1077                         $hold->current_copy($copy->id); # just make sure it's set
1078                         # if the hold was never officially captured, capture it.
1079                         $hold->capture_time('now') unless $hold->capture_time;
1080                         $hold->fulfillment_time('now');
1081                         my $r = $ctx->{session}->request(
1082                                 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1083                         return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1084                         push( @fulfilled, $hold->id );
1085                 }
1086         }
1087
1088         return (\@fulfilled, undef);
1089 }
1090
1091 sub _checkout_noncat {
1092         my ( $key, $requestor, $patron, %params ) = @_;
1093         my( $circ, $circlib, $evt );
1094         $U->logmark;
1095
1096         $circlib = $params{noncat_circ_lib} || $requestor->ws_ou;
1097
1098         my $count = $params{noncat_count} || 1;
1099         my $cotime = _create_date_stamp($params{checkout_time}) || "";
1100         $logger->info("circ creating $count noncat circs with checkout time $cotime");
1101         for(1..$count) {
1102                 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1103                         $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1104                 return $evt if $evt;
1105         }
1106
1107         return OpenILS::Event->new( 
1108                 'SUCCESS', payload => { noncat_circ => $circ } );
1109 }
1110
1111
1112 __PACKAGE__->register_method(
1113         method  => "generic_receive",
1114         api_name        => "open-ils.circ.checkin",
1115         argc            => 2,
1116         signature       => q/
1117                 Generic super-method for handling all copies
1118                 @param authtoken The login session key
1119                 @param params Hash of named parameters including:
1120                         barcode - The copy barcode
1121                         force           - If true, copies in bad statuses will be checked in and give good statuses
1122                         ...
1123         /
1124 );
1125
1126 __PACKAGE__->register_method(
1127         method  => "generic_receive",
1128         api_name        => "open-ils.circ.checkin.override",
1129         signature       => q/@see open-ils.circ.checkin/
1130 );
1131
1132 sub generic_receive {
1133         my( $self, $conn, $authtoken, $params ) = @_;
1134         my( $ctx, $requestor, $evt );
1135
1136         ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1137         ( $requestor, $evt ) = $U->checksesperm( 
1138                 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1139         return $evt if $evt;
1140
1141         # load up the circ objects
1142         if( !( $ctx = $params->{_ctx}) ) {
1143                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
1144                         requestor                                               => $requestor, 
1145                         session                                                 => $U->start_db_session(),
1146                         type                                                            => 'circ',
1147                         fetch_copy_statuses                     => 1, 
1148                         fetch_copy_locations                    => 1, 
1149                         no_runner                                               => 1,  
1150                         );
1151                 return $evt if $evt;
1152         }
1153         $ctx->{override} = 1 if $self->api_name =~ /override/o;
1154         $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1155         $ctx->{authtoken} = $authtoken;
1156         my $session = $ctx->{session};
1157
1158         my $copy = $ctx->{copy};
1159         $U->unflesh_copy($copy);
1160         return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $copy;
1161
1162         $logger->info("Checkin copy called by user ".
1163                 $requestor->id." for copy ".$copy->id);
1164
1165         # ------------------------------------------------------------------------------
1166         # Update the patron penalty info in the DB
1167         # ------------------------------------------------------------------------------
1168         $U->update_patron_penalties( 
1169                 authtoken => $authtoken, 
1170                 patron    => $ctx->{patron},
1171                 background => 1
1172         );
1173
1174         return $self->checkin_do_receive($conn, $ctx);
1175 }
1176
1177 sub checkin_do_receive {
1178
1179         my( $self, $connection, $ctx ) = @_;
1180
1181         my $evt;
1182         my $copy                        = $ctx->{copy};
1183         my $session             = $ctx->{session};
1184         my $requestor   = $ctx->{requestor};
1185         my $change              = 0; # did we actually do anything?
1186         my $circ;
1187
1188         my @eventlist;
1189
1190         # does the copy have an attached alert message?
1191         my $ae = _check_copy_alert($copy);
1192         push(@eventlist, $ae) if $ae;
1193
1194         # is the copy is an a status we can't automatically resolve?
1195         $evt = _checkin_check_copy_status($ctx);
1196         push( @eventlist, $evt ) if $evt;
1197
1198
1199         # - see if the copy has an open circ attached
1200         ($ctx->{circ}, $evt)    = $U->fetch_open_circulation($copy->id);
1201         return $evt if ($evt and $__isrenewal); # renewals require a circulation
1202         $evt = undef;
1203         $circ = $ctx->{circ};
1204
1205         # if the circ is marked as 'claims returned', add the event to the list
1206         push( @eventlist, 'CIRC_CLAIMS_RETURNED' ) 
1207                 if ($circ and $circ->stop_fines and $circ->stop_fines eq 'CLAIMSRETURNED');
1208
1209         # override or die
1210         if(@eventlist) {
1211                 if($ctx->{override}) {
1212                         $evt = override_events($requestor, $requestor->ws_ou, \@eventlist );
1213                         return $evt if $evt;
1214                 } else {
1215                         return \@eventlist;
1216                 }
1217         }
1218
1219         ($ctx->{transit})       = $U->fetch_open_transit_by_copy($copy->id);
1220
1221         if( $ctx->{circ} ) {
1222
1223                 # There is an open circ on this item, close it out.
1224                 $change = 1;
1225                 $evt            = _checkin_handle_circ($ctx);
1226                 return $evt if $evt;
1227
1228         } elsif( $ctx->{transit} ) {
1229
1230                 # is this item currently in transit?
1231                 $change                 = 1;
1232                 $evt                            = $transcode->transit_receive( $copy, $requestor, $session );
1233                 my $holdtrans   = $evt->{holdtransit};
1234                 ($ctx->{hold})  = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1235
1236                 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1237
1238                         # either an error occurred or a ROUTE_ITEM was generated and the 
1239                         # item must be forwarded on to its destination.
1240                         return _checkin_flesh_event($ctx, $evt);
1241
1242                 } else {
1243
1244                         # Transit has been closed, now let's see if the copy's original
1245                         # status is something the staff should be warned of
1246                         my $e = _checkin_check_copy_status($ctx);
1247                         $evt = $e if $e;
1248
1249                         if($holdtrans) {
1250
1251                                 # copy was received as a hold transit.  Copy is at target lib
1252                                 # and hold transit is complete.  We're done here...
1253                                 $U->commit_db_session($session);
1254                                 return _checkin_flesh_event($ctx, $evt);
1255                         }
1256                         $evt = undef;
1257                 }
1258         }
1259
1260         # ------------------------------------------------------------------------------
1261         # Circulations and transits are now closed where necessary.  Now go on to see if
1262         # this copy can fulfill a hold or needs to be routed to a different location
1263         # ------------------------------------------------------------------------------
1264
1265
1266         # If it's a renewal, we're done
1267         if($__isrenewal) {
1268                 #$$ctx{force} = 1;
1269                 #my ($cc, $ee) = _reshelve_copy($ctx);
1270                 #return $ee if $ee;
1271                 #delete $$ctx{force};
1272                 $U->commit_db_session($session);
1273                 return OpenILS::Event->new('SUCCESS');
1274         }
1275
1276         # Now, let's see if this copy is needed for a hold
1277         my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor ); 
1278
1279         if($hold) {
1280
1281                 $ctx->{hold}    = $hold;
1282                 $change                 = 1;
1283                 
1284                 # Capture the hold with this copy
1285                 return $evt if ($evt = _checkin_capture_hold($ctx));
1286
1287                 if( $hold->pickup_lib == $requestor->ws_ou ) {
1288
1289                         # This hold was captured in the correct location
1290                         $evt = OpenILS::Event->new('SUCCESS');
1291
1292                 } else {
1293
1294                         # Hold needs to be picked up elsewhere.  Build a hold 
1295                         # transit and route the item.
1296                         return $evt if ($evt =_checkin_build_hold_transit($ctx));
1297                         $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1298                 }
1299
1300         } else { # not needed for a hold
1301
1302                 if( $copy->circ_lib == $requestor->ws_ou ) {
1303
1304                         # Copy is in the right place.
1305                         $evt = OpenILS::Event->new('SUCCESS');
1306
1307                         # if the item happens to be a pre-cataloged item, send it
1308                         # to cataloging and return the event
1309                         my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1310                         return $err if $err;
1311                         $change         = 1 if $c;
1312                         $evt                    = $e if $e;
1313
1314                 } else {
1315
1316                         # Copy wants to go home. Transit it there.
1317                         return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1318                         $evt                    = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1319                         $change         = 1;
1320                 }
1321         }
1322
1323
1324         # ------------------------------------------------------------------
1325         # if the copy is not in a state that should persist,
1326         # set the copy to reshelving if it's not already there
1327         # ------------------------------------------------------------------
1328         my ($c, $e) = _reshelve_copy($ctx);
1329         return $e if $e;
1330         $change = $c unless $change;
1331
1332         if(!$change) {
1333
1334                 $evt = OpenILS::Event->new('NO_CHANGE');
1335                 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id) 
1336                         if( $copy->status == $U->copy_status_from_name('on holds shelf')->id );
1337
1338         } else {
1339
1340                 $U->commit_db_session($session);
1341         }
1342
1343         $logger->activity("checkin by user ".$requestor->id." on item ".
1344                 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1345
1346         return _checkin_flesh_event($ctx, $evt);
1347 }
1348
1349 sub _reshelve_copy {
1350
1351         my $ctx = shift;
1352         my $copy                = $ctx->{copy};
1353         my $reqr                = $ctx->{requestor};
1354         my $session     = $ctx->{session};
1355         my $force       = $ctx->{force};
1356
1357         my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1358
1359         if($force || (
1360                 $stat != $U->copy_status_from_name('on holds shelf')->id and 
1361                 $stat != $U->copy_status_from_name('available')->id and 
1362                 $stat != $U->copy_status_from_name('cataloging')->id and 
1363                 $stat != $U->copy_status_from_name('in transit')->id and 
1364                 $stat != $U->copy_status_from_name('reshelving')->id) ) {
1365
1366                 $copy->status( $U->copy_status_from_name('reshelving')->id );
1367
1368                 my $evt = $U->update_copy( 
1369                         copy            => $copy,
1370                         editor  => $reqr->id,
1371                         session => $session,
1372                         );
1373
1374                 return( 1, $evt );
1375         }
1376         return undef;
1377 }
1378
1379
1380
1381
1382 # returns undef if there are no 'open' claims-returned circs attached
1383 # to the given copy.  if there is an open claims-returned circ, 
1384 # then we check for override mode.  if in override, mark the claims-returned
1385 # circ as checked in.  if not, return event.
1386 sub _handle_claims_returned {
1387         my $ctx = shift;
1388         my $copy = $ctx->{copy};
1389
1390         my $CR  = _fetch_open_claims_returned($copy->id);
1391         return undef unless $CR;
1392
1393         # - If the caller has set the override flag, we will check the item in
1394         if($ctx->{override}) {
1395
1396                 $CR->checkin_time('now');       
1397                 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1398                 $CR->checkin_staff($ctx->{requestor}->id);
1399
1400                 my $stat = $U->storagereq(
1401                         'open-ils.storage.direct.action.circulation.update', $CR);
1402                 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1403                 return OpenILS::Event->new('SUCCESS');
1404
1405         } else {
1406                 # - if not in override mode, return the CR event
1407                 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1408         }
1409 }
1410
1411
1412 sub _fetch_open_claims_returned {
1413         my $copyid = shift;
1414         my $trans = $U->storagereq(
1415                 'open-ils.storage.direct.action.circulation.search_where',
1416                 {       
1417                         target_copy             => $copyid, 
1418                         stop_fines              => 'CLAIMSRETURNED',
1419                         checkin_time    => undef,
1420                 }
1421         );
1422         return $$trans[0] if $trans && $$trans[0];
1423         return undef;
1424 }
1425
1426 # - if the copy is has the 'in process' status, set it to reshelving
1427 #sub _check_in_process {
1428         #my $ctx = shift;
1429 #
1430         #my $copy = $ctx->{copy};
1431         #my $reqr       = $ctx->{requestor};
1432         #my $ses        = $ctx->{session};
1433 ##
1434         #my $stat = $U->copy_status_from_name('in process');
1435         #my $rstat = $U->copy_status_from_name('reshelving');
1436 #
1437         #if( $stat->id == $copy->status->id ) {
1438                 #$logger->info("marking 'in-process' copy ".$copy->id." as 'reshelving'");
1439                 #$copy->status( $rstat->id );
1440                 #my $evt = $U->update_copy( 
1441                         #copy           => $copy,
1442                         #editor => $reqr->id,
1443                         #session        => $ses
1444                         #);
1445                 #return $evt if $evt;
1446 #
1447                 #$copy->status( $rstat ); # - reflesh the copy status
1448         #}
1449         #return undef;
1450 #}
1451
1452
1453 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1454 sub _checkin_handle_precat {
1455
1456         my $ctx         = shift;
1457         my $copy                = $ctx->{copy};
1458         my $evt         = undef;
1459         my $errevt      = undef;
1460         my $change      = 0;
1461
1462         my $catstat = $U->copy_status_from_name('cataloging');
1463
1464         if( $ctx->{precat} ) {
1465
1466                 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1467
1468                 if( $copy->status != $catstat->id ) {
1469                         $copy->status($catstat->id);
1470
1471                         return (undef, 0, $errevt) if (
1472                                 $errevt = $U->update_copy(
1473                                         copy            => $copy, 
1474                                         editor  => $ctx->{requestor}->id, 
1475                                         session => $ctx->{session} ));
1476                         $change = 1;
1477
1478                 }
1479         }
1480
1481         return ($evt, $change, undef);
1482 }
1483
1484
1485 # returns the appropriate event for the given copy status
1486 # if the copy is not in a 'special' status, undef is returned
1487 sub _checkin_check_copy_status {
1488         my $ctx = shift;
1489         my $copy = $ctx->{copy};
1490         my $reqr        = $ctx->{requestor};
1491         my $ses = $ctx->{session};
1492
1493         my $islost              = 0;
1494         my $ismissing   = 0;
1495         my $evt                 = undef;
1496
1497         my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1498
1499         return undef 
1500                 if(     $status == $U->copy_status_from_name('available')->id           ||
1501                                 $status == $U->copy_status_from_name('checked out')->id ||
1502                                 $status == $U->copy_status_from_name('in process')->id  ||
1503                                 $status == $U->copy_status_from_name('in transit')->id  ||
1504                                 $status == $U->copy_status_from_name('reshelving')->id );
1505
1506         return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy ) 
1507                 if( $status == $U->copy_status_from_name('lost')->id );
1508
1509         return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy ) 
1510                 if( $status == $U->copy_status_from_name('missing')->id );
1511
1512         return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1513
1514
1515
1516 #       my $rstat = $U->copy_status_from_name('reshelving');
1517 #       my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1518 #
1519 #       if( $stat == $U->copy_status_from_name('lost')->id ) {
1520 #               $islost = 1;
1521 #               $evt = OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy );
1522 #
1523 #       } elsif( $stat == $U->copy_status_from_name('missing')->id) {
1524 #               $ismissing = 1;
1525 #               $evt = OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy );
1526 #       }
1527 #
1528 #       return (undef,$evt) if(!$ctx->{override});
1529 #
1530 #       # we're are now going to attempt to override the failure 
1531 #       # and set the copy to reshelving
1532 #       my $e;
1533 #       my $copyid = $copy->id;
1534 #       my $userid = $reqr->id;
1535 #       if( $islost ) {
1536 #
1537 #               # - make sure we have permission
1538 #               $e = $U->check_perms( $reqr->id, 
1539 #                       $copy->circ_lib, 'COPY_STATUS_LOST.override');
1540 #               return (undef,$e) if $e;
1541 #               $copy->status( $rstat->id );
1542 #
1543 #               # XXX if no fines are owed in the circ, close it out - will this happen later anyway?
1544 #               #my $circ = $U->storagereq(
1545 #               #       'open-ils.storage.direct.action.circulation
1546 #
1547 #               $logger->activity("user $userid overriding 'lost' copy status for copy $copyid");
1548 #
1549 #       } elsif( $ismissing ) {
1550 #
1551 #               # - make sure we have permission
1552 #               $e = $U->check_perms( $reqr->id, 
1553 #                       $copy->circ_lib, 'COPY_STATUS_MISSING.override');
1554 #               return (undef,$e) if $e;
1555 #               $copy->status( $rstat->id );
1556 #               $logger->activity("user $userid overriding 'missing' copy status for copy $copyid");
1557 #       }
1558 #
1559 #       if( $islost or $ismissing ) {
1560 #
1561 #               # - update the copy with the new status
1562 #               $evt = $U->update_copy(
1563 #                       copy            => $copy,
1564 #                       editor  => $reqr->id,
1565 #                       session => $ses
1566 #               );
1567 #               return (undef,$evt) if $evt;
1568 #               $copy->status( $rstat );
1569 #       }
1570 #
1571 #       return (1);
1572
1573
1574 }
1575
1576 # Just gets the copy back home.  Returns undef on success, event on error
1577 sub _checkin_build_generic_copy_transit {
1578
1579         my $ctx                 = shift;
1580         my $requestor   = $ctx->{requestor};
1581         my $copy                        = $ctx->{copy};
1582         my $transit             = Fieldmapper::action::transit_copy->new;
1583         my $session             = $ctx->{session};
1584
1585         $logger->activity("User ". $requestor->id ." creating a ".
1586                 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1587
1588         $transit->source($requestor->ws_ou);
1589         $transit->dest($copy->circ_lib);
1590         $transit->target_copy($copy->id);
1591         $transit->source_send_time('now');
1592         $transit->copy_status($copy->status);
1593         
1594         $logger->debug("Creating new copy_transit in DB");
1595
1596         my $s = $session->request(
1597                 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1598         return $U->DB_UPDATE_FAILED($transit) unless $s;
1599
1600         $logger->info("Checkin copy successfully created new transit: $s");
1601
1602         $copy->status($U->copy_status_from_name('in transit')->id );
1603
1604         return $U->update_copy( copy => $copy, 
1605                         editor => $requestor->id, session => $session );
1606         
1607 }
1608
1609
1610 # returns event on error, undef on success
1611 sub _checkin_build_hold_transit {
1612         my $ctx = shift;
1613
1614         my $copy = $ctx->{copy};
1615         my $hold = $ctx->{hold};
1616         my $trans = Fieldmapper::action::hold_transit_copy->new;
1617
1618         $trans->hold($hold->id);
1619         $trans->source($ctx->{requestor}->ws_ou);
1620         $trans->dest($hold->pickup_lib);
1621         $trans->source_send_time("now");
1622         $trans->target_copy($copy->id);
1623         $trans->copy_status($copy->status);
1624
1625         my $id = $ctx->{session}->request(
1626                 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1627         return $U->DB_UPDATE_FAILED($trans) unless $id;
1628
1629         $logger->info("Checkin copy successfully created hold transit: $id");
1630
1631         $copy->status($U->copy_status_from_name('in transit')->id );
1632         return $U->update_copy( copy => $copy, 
1633                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1634 }
1635
1636 # Returns event on error, undef on success
1637 sub _checkin_capture_hold {
1638         my $ctx = shift;
1639         my $copy = $ctx->{copy};
1640         my $hold = $ctx->{hold}; 
1641
1642         $logger->debug("Checkin copy capturing hold ".$hold->id);
1643
1644         $hold->current_copy($copy->id);
1645         $hold->capture_time('now'); 
1646
1647         my $stat = $ctx->{session}->request(
1648                 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1649         return $U->DB_UPDATE_FAILED($hold) unless $stat;
1650
1651         $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1652
1653         return $U->update_copy( copy => $copy, 
1654                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1655 }
1656
1657 # fleshes an event with the relevant objects from the context
1658 sub _checkin_flesh_event {
1659         my $ctx = shift;
1660         my $evt = shift;
1661
1662         my $payload                             = {};
1663         $payload->{copy}                = $U->unflesh_copy($ctx->{copy});
1664         $payload->{record}      = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1665         $payload->{circ}                = $ctx->{circ} if $ctx->{circ};
1666         $payload->{transit}     = $ctx->{transit} if $ctx->{transit};
1667         $payload->{hold}                = $ctx->{hold} if $ctx->{hold};
1668
1669         $evt->{payload} = $payload;
1670         return $evt;
1671 }
1672
1673
1674 # Closes out the circulation, puts the copy into reshelving.
1675 # Voids any bills attached to this circ after the backdate time 
1676 # if a backdate is provided
1677 sub _checkin_handle_circ { 
1678
1679         my $ctx = shift;
1680
1681         my $circ = $ctx->{circ};
1682         my $copy = $ctx->{copy};
1683         my $requestor   = $ctx->{requestor};
1684         my $session             = $ctx->{session};
1685         my $evt;
1686         my $obt;
1687
1688         $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1689
1690         # backdate the circ if necessary
1691         if(my $backdate = $ctx->{backdate}) {
1692                 return $evt if ($evt = 
1693                         _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1694         }
1695
1696
1697         if(!$circ->stop_fines) {
1698                 $circ->stop_fines('CHECKIN');
1699                 $circ->stop_fines('RENEW') if $__isrenewal;
1700                 $circ->stop_fines_time('now');
1701         }
1702
1703         # see if there are any fines owed on this circ.  if not, close it
1704         ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1705         return $evt if $evt;
1706         $circ->xact_finish('now') if( $obt->balance_owed != 0 );
1707
1708         # Set the checkin vars since we have the item
1709         $circ->checkin_time('now');
1710         $circ->checkin_staff($requestor->id);
1711         $circ->checkin_lib($requestor->ws_ou);
1712
1713         $evt = _set_copy_reshelving($copy, $requestor->id, $ctx->{session}); 
1714         return $evt if $evt;
1715
1716 #       $copy->status($U->copy_status_from_name('reshelving')->id);
1717 #       $evt = $U->update_copy( session => $session, 
1718 #               copy => $copy, editor => $requestor->id );
1719 #       return $evt if $evt;
1720
1721         $ctx->{session}->request(
1722                 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1723
1724         return undef;
1725 }
1726
1727 sub _set_copy_reshelving {
1728         my( $copy, $reqr, $session ) = @_;
1729
1730         $logger->info("Setting copy ".$copy->id." to reshelving");
1731         $copy->status($U->copy_status_from_name('reshelving')->id);
1732
1733         my $evt = $U->update_copy( 
1734                 session => $session, 
1735                 copy            => $copy, 
1736                 editor  => $reqr
1737                 );
1738         return $evt if $evt;
1739 }
1740
1741 # returns event on error, undef on success
1742 # This voids all bills attached to the given circulation that occurred
1743 # after the backdate 
1744 # THIS DOES NOT CLOSE THE CIRC if there are no more fines on the item
1745 sub _checkin_handle_backdate {
1746         my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1747
1748         $logger->activity("User ".$requestor->id.
1749                 " backdating circ [".$circ->target_copy."] to date: $backdate");
1750
1751         my $bills = $session->request( # XXX Verify this call is correct
1752                 "open-ils.storage.direct.money.billing.search_where.atomic",
1753                 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1754
1755         if($bills) {
1756                 for my $bill (@$bills) {
1757                         $bill->voided('t');
1758                         my $s = $session->request(
1759                                 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1760                         return $U->DB_UPDATE_FAILED($bill) unless $s;
1761                 }
1762         }
1763
1764         # if the caller elects to attempt to close the circulation
1765         # transaction, then it will be closed if there are not further
1766         # charges on the transaction
1767         #if( $closecirc ) {
1768                 #my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1769            #return $evt if $evt;
1770                 #$circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1771         #}
1772
1773         return undef;
1774 }
1775
1776
1777 sub _find_patron_from_params {
1778         my $params = shift;
1779
1780         my $patron;
1781         my $copy;
1782         my $circ;
1783         my $evt;
1784
1785         if(my $barcode = $params->{barcode}) {
1786                 $logger->debug("circ finding user from params with barcode $barcode");
1787                 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1788                 return (undef, undef, $evt) if $evt;
1789                 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1790                 return (undef, undef, $evt) if $evt;
1791                 ($patron, $evt) = $U->fetch_user($circ->usr);
1792                 return (undef, undef, $evt) if $evt;
1793         }
1794         return ($patron, $copy);
1795 }
1796
1797
1798 # ------------------------------------------------------------------------------
1799
1800 __PACKAGE__->register_method(
1801         method  => "renew",
1802         api_name        => "open-ils.circ.renew.override",
1803         signature       => q/@see open-ils.circ.renew/,
1804 );
1805
1806
1807 __PACKAGE__->register_method(
1808         method  => "renew",
1809         api_name        => "open-ils.circ.renew",
1810         notes           => <<"  NOTES");
1811         PARAMS( authtoken, circ => circ_id );
1812         open-ils.circ.renew(login_session, circ_object);
1813         Renews the provided circulation.  login_session is the requestor of the
1814         renewal and if the logged in user is not the same as circ->usr, then
1815         the logged in user must have RENEW_CIRC permissions.
1816         NOTES
1817
1818 sub renew {
1819         my( $self, $client, $authtoken, $params ) = @_;
1820         $U->logmark;
1821
1822         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1823         $__isrenewal = 1;
1824
1825         $params->{override} = 1 if $self->api_name =~ /override/o;
1826
1827         # fetch the patron object one way or another
1828         if( $params->{patron} ) {
1829                 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1830                 if($evt) { $__isrenewal = 0; return $evt; }
1831
1832         } elsif( $params->{patron_barcode} ) {
1833                 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1834                 if($evt) { $__isrenewal = 0; return $evt; }
1835
1836         } else {
1837                 ($patron, $copy, $evt) = _find_patron_from_params($params);
1838                 return $evt if $evt;
1839                 $params->{copy} = $copy;
1840         }
1841
1842         # verify our login session
1843         ($requestor, $evt) = $U->checkses($authtoken);
1844         if($evt) { $__isrenewal = 0; return $evt; }
1845
1846         # make sure we have permission to perform a renewal
1847         if( $requestor->id ne $patron->id ) {
1848                 $evt = $U->check_perms($requestor->id, $requestor->ws_ou, 'RENEW_CIRC');
1849                 if($evt) { $__isrenewal = 0; return $evt; }
1850         }
1851
1852
1853         # fetch and build the circulation environment
1854         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1855                 patron                                                  => $patron, 
1856                 requestor                                               => $requestor, 
1857                 patron                                                  => $patron, 
1858                 type                                                            => 'circ',
1859                 #fetch_patron_circ_summary      => 1,
1860                 fetch_copy_statuses                     => 1, 
1861                 fetch_copy_locations                    => 1, 
1862                 );
1863         if($evt) { $__isrenewal = 0; return $evt; }
1864         $params->{_ctx} = $ctx;
1865
1866         # make sure they have some renewals left and make sure the circulation exists
1867         ($circ, $evt) = _check_renewal_remaining($ctx);
1868         if($evt) { $__isrenewal = 0; return $evt; }
1869         $ctx->{old_circ} = $circ;
1870         my $renewals = $circ->renewal_remaining - 1;
1871
1872         # run the renew permit script
1873         $evt = _run_renew_scripts($ctx);
1874         if($evt) { $__isrenewal = 0; return $evt; }
1875
1876         # checkin the cop
1877         #$ctx->{patron} = $ctx->{patron}->id;
1878         $evt = $self->generic_receive($client, $authtoken, $ctx );
1879                 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1880
1881         if( !$U->event_equals($evt, 'SUCCESS') ) {
1882                 $__isrenewal = 0; return $evt; 
1883         }
1884
1885         # re-fetch the context since objects have changed in the checkin
1886         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1887                 patron                                                  => $patron, 
1888                 requestor                                               => $requestor, 
1889                 patron                                                  => $patron, 
1890                 type                                                            => 'circ',
1891                 #fetch_patron_circ_summary      => 1,
1892                 fetch_copy_statuses                     => 1, 
1893                 fetch_copy_locations                    => 1, 
1894                 );
1895         if($evt) { $__isrenewal = 0; return $evt; }
1896         $params->{_ctx} = $ctx;
1897         $ctx->{renewal_remaining} = $renewals;
1898
1899         # run the circ permit scripts
1900         if( $ctx->{permit_override} ) {
1901                 $evt = $U->check_perms(
1902                         $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1903                 if($evt) { $__isrenewal = 0; return $evt; }
1904
1905         } else {
1906                 $evt = $self->permit_circ( $client, $authtoken, $params );
1907                 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1908                         #$ctx->{precat} = 1;
1909                         $params->{precat} = 1;
1910
1911                 } else {
1912                         if(!$U->event_equals($evt, 'SUCCESS')) {
1913                                 if($evt) { $__isrenewal = 0; return $evt; }
1914                         }
1915                 }
1916                 $params->{permit_key} = $evt->{payload};
1917         }
1918
1919
1920         # checkout the item again
1921         $params->{patron} = $ctx->{patron}->id;
1922         $evt = $self->checkout($client, $authtoken, $params );
1923
1924         $logger->activity("user ".$requestor->id." renewl of item ".
1925                 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1926
1927         $__isrenewal = 0;
1928         return $evt;
1929 }
1930
1931 sub _check_renewal_remaining {
1932         my $ctx = shift;
1933         $U->logmark;
1934         my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1935         return (undef, $evt) if $evt;
1936         $evt = OpenILS::Event->new(
1937                 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1938         return ($circ, $evt);
1939 }
1940
1941 sub _run_renew_scripts {
1942         my $ctx = shift;
1943         my $runner = $ctx->{runner};
1944         $U->logmark;
1945
1946         $runner->load($scripts{circ_permit_renew});
1947         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1948
1949         my $events = $runner->retrieve('result.events');
1950         $events = [ split(/,/, $events) ]; 
1951         $logger->activity("circ_permit_renew for user ".
1952                 $ctx->{patron}->id." returned events: @$events") if @$events;
1953
1954         my @allevents;
1955         push( @allevents, OpenILS::Event->new($_)) for @$events;
1956         return \@allevents if  @allevents;
1957
1958         return undef;
1959 }
1960
1961         
1962
1963
1964 1;
1965