]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
fixing bugs, makeing in-transit a non-overridable event
[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         if( $ctx->{due_date} ) {
947
948                 my $evt = $U->check_perms(
949                         $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
950                 return $evt if $evt;
951
952                 my $ds = _create_date_stamp($ctx->{due_date});
953                 $logger->debug("circ modifying  due_date to $ds");
954                 $circ->due_date($ds);
955
956         }
957         return undef;
958 }
959
960 sub _create_date_stamp {
961         my $datestring = shift;
962         return undef unless $datestring;
963         $datestring = clense_ISO8601($datestring);
964         $logger->debug("circ created date stamp => $datestring");
965         return $datestring;
966 }
967
968 sub _create_due_date {
969         my $duration = shift;
970         $U->logmark;
971         my ($sec,$min,$hour,$mday,$mon,$year) = 
972                 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
973         $year += 1900; $mon += 1;
974         my $due_date = sprintf(
975         '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
976         $year, $mon, $mday, $hour, $min, $sec);
977         return $due_date;
978 }
979
980 sub _set_circ_due_date {
981         my $circ = shift;
982         $U->logmark;
983         my $dd = _create_due_date($circ->duration);
984         $logger->debug("Checkout setting due date on circ to: $dd");
985         $circ->due_date($dd);
986 }
987
988 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
989 sub _update_checkout_copy {
990         my $ctx = shift;
991         $U->logmark;
992         my $copy = $ctx->{copy};
993
994         my $s = $U->copy_status_from_name('checked out');
995         $copy->status( $s->id ) if $s;
996
997         my $evt = $U->update_copy( session => $ctx->{session}, 
998                 copy => $copy, editor => $ctx->{requestor}->id );
999         return (undef,$evt) if $evt;
1000
1001         return undef;
1002 }
1003
1004 # commits the circ object to the db then fleshes the circ with rules objects
1005 sub _commit_checkout_circ_object {
1006
1007         my $ctx = shift;
1008         my $circ = $ctx->{circ};
1009         $U->logmark;
1010
1011         $circ->clear_id;
1012         my $r = $ctx->{session}->request(
1013                 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
1014
1015         return $U->DB_UPDATE_FAILED($circ) unless $r;
1016
1017         $logger->debug("Created a new circ object in checkout: $r");
1018
1019         $circ->id($r);
1020         $circ->duration_rule($ctx->{duration_rule});
1021         $circ->max_fine_rule($ctx->{max_fine_rule});
1022         $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
1023
1024         return undef;
1025 }
1026
1027
1028 # sees if there are any holds that this copy 
1029 sub _handle_related_holds {
1030
1031         my $ctx         = shift;
1032         my $copy                = $ctx->{copy};
1033         my $patron      = $ctx->{patron};
1034         my $holds       = $holdcode->fetch_related_holds($copy->id);
1035         $U->logmark;
1036         my @fulfilled;
1037
1038         # XXX We should only fulfill one hold here...
1039         # XXX If a hold was transited to the user who is checking out
1040         # the item, we need to make sure that hold is what's grabbed
1041         if(ref($holds) && @$holds) {
1042
1043                 # for now, just sort by id to get what should be the oldest hold
1044                 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1045                 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
1046
1047                 if(@$holds) {
1048                         my $hold = $holds->[0];
1049
1050                         $logger->debug("Related hold found in checkout: " . $hold->id );
1051
1052                         $hold->current_copy($copy->id); # just make sure it's set
1053                         # if the hold was never officially captured, capture it.
1054                         $hold->capture_time('now') unless $hold->capture_time;
1055                         $hold->fulfillment_time('now');
1056                         my $r = $ctx->{session}->request(
1057                                 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1058                         return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1059                         push( @fulfilled, $hold->id );
1060                 }
1061         }
1062
1063         return (\@fulfilled, undef);
1064 }
1065
1066 sub _checkout_noncat {
1067         my ( $key, $requestor, $patron, %params ) = @_;
1068         my( $circ, $circlib, $evt );
1069         $U->logmark;
1070
1071         $circlib = $params{noncat_circ_lib} || $requestor->ws_ou;
1072
1073         my $count = $params{noncat_count} || 1;
1074         my $cotime = _create_date_stamp($params{checkout_time}) || "";
1075         $logger->info("circ creating $count noncat circs with checkout time $cotime");
1076         for(1..$count) {
1077                 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1078                         $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1079                 return $evt if $evt;
1080         }
1081
1082         return OpenILS::Event->new( 
1083                 'SUCCESS', payload => { noncat_circ => $circ } );
1084 }
1085
1086
1087 __PACKAGE__->register_method(
1088         method  => "generic_receive",
1089         api_name        => "open-ils.circ.checkin",
1090         argc            => 2,
1091         signature       => q/
1092                 Generic super-method for handling all copies
1093                 @param authtoken The login session key
1094                 @param params Hash of named parameters including:
1095                         barcode - The copy barcode
1096                         force           - If true, copies in bad statuses will be checked in and give good statuses
1097                         ...
1098         /
1099 );
1100
1101 __PACKAGE__->register_method(
1102         method  => "generic_receive",
1103         api_name        => "open-ils.circ.checkin.override",
1104         signature       => q/@see open-ils.circ.checkin/
1105 );
1106
1107 sub generic_receive {
1108         my( $self, $conn, $authtoken, $params ) = @_;
1109         my( $ctx, $requestor, $evt );
1110
1111         ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1112         ( $requestor, $evt ) = $U->checksesperm( 
1113                 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1114         return $evt if $evt;
1115
1116         # load up the circ objects
1117         if( !( $ctx = $params->{_ctx}) ) {
1118                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
1119                         requestor                                               => $requestor, 
1120                         session                                                 => $U->start_db_session(),
1121                         type                                                            => 'circ',
1122                         fetch_copy_statuses                     => 1, 
1123                         fetch_copy_locations                    => 1, 
1124                         no_runner                                               => 1,  
1125                         );
1126                 return $evt if $evt;
1127         }
1128         $ctx->{override} = 1 if $self->api_name =~ /override/o;
1129         $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1130         $ctx->{authtoken} = $authtoken;
1131         my $session = $ctx->{session};
1132
1133         my $copy = $ctx->{copy};
1134         $U->unflesh_copy($copy);
1135         return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $copy;
1136
1137         $logger->info("Checkin copy called by user ".
1138                 $requestor->id." for copy ".$copy->id);
1139
1140         # ------------------------------------------------------------------------------
1141         # Update the patron penalty info in the DB
1142         # ------------------------------------------------------------------------------
1143         $U->update_patron_penalties( 
1144                 authtoken => $authtoken, 
1145                 patron    => $ctx->{patron},
1146                 background => 1
1147         );
1148
1149         return $self->checkin_do_receive($conn, $ctx);
1150 }
1151
1152 sub checkin_do_receive {
1153
1154         my( $self, $connection, $ctx ) = @_;
1155
1156         my $evt;
1157         my $copy                        = $ctx->{copy};
1158         my $session             = $ctx->{session};
1159         my $requestor   = $ctx->{requestor};
1160         my $change              = 0; # did we actually do anything?
1161         my $circ;
1162
1163         my @eventlist;
1164
1165         # does the copy have an attached alert message?
1166         my $ae = _check_copy_alert($copy);
1167         push(@eventlist, $ae) if $ae;
1168
1169         # is the copy is an a status we can't automatically resolve?
1170         $evt = _checkin_check_copy_status($ctx);
1171         push( @eventlist, $evt ) if $evt;
1172
1173
1174         # - see if the copy has an open circ attached
1175         ($ctx->{circ}, $evt)    = $U->fetch_open_circulation($copy->id);
1176         return $evt if ($evt and $__isrenewal); # renewals require a circulation
1177         $evt = undef;
1178         $circ = $ctx->{circ};
1179
1180         # if the circ is marked as 'claims returned', add the event to the list
1181         push( @eventlist, 'CIRC_CLAIMS_RETURNED' ) 
1182                 if ($circ and $circ->stop_fines and $circ->stop_fines eq 'CLAIMSRETURNED');
1183
1184         # override or die
1185         if(@eventlist) {
1186                 if($ctx->{override}) {
1187                         $evt = override_events($requestor, $requestor->ws_ou, \@eventlist );
1188                         return $evt if $evt;
1189                 } else {
1190                         return \@eventlist;
1191                 }
1192         }
1193
1194         ($ctx->{transit})       = $U->fetch_open_transit_by_copy($copy->id);
1195
1196         if( $ctx->{circ} ) {
1197
1198                 # There is an open circ on this item, close it out.
1199                 $change = 1;
1200                 $evt            = _checkin_handle_circ($ctx);
1201                 return $evt if $evt;
1202
1203         } elsif( $ctx->{transit} ) {
1204
1205                 # is this item currently in transit?
1206                 $change                 = 1;
1207                 $evt                            = $transcode->transit_receive( $copy, $requestor, $session );
1208                 my $holdtrans   = $evt->{holdtransit};
1209                 ($ctx->{hold})  = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1210
1211                 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1212
1213                         # either an error occurred or a ROUTE_ITEM was generated and the 
1214                         # item must be forwarded on to its destination.
1215                         return _checkin_flesh_event($ctx, $evt);
1216
1217                 } else {
1218
1219                         # Transit has been closed, now let's see if the copy's original
1220                         # status is something the staff should be warned of
1221                         my $e = _checkin_check_copy_status($ctx);
1222                         $evt = $e if $e;
1223
1224                         if($holdtrans) {
1225
1226                                 # copy was received as a hold transit.  Copy is at target lib
1227                                 # and hold transit is complete.  We're done here...
1228                                 $U->commit_db_session($session);
1229                                 return _checkin_flesh_event($ctx, $evt);
1230                         }
1231                         $evt = undef;
1232                 }
1233         }
1234
1235         # ------------------------------------------------------------------------------
1236         # Circulations and transits are now closed where necessary.  Now go on to see if
1237         # this copy can fulfill a hold or needs to be routed to a different location
1238         # ------------------------------------------------------------------------------
1239
1240
1241         # If it's a renewal, we're done
1242         if($__isrenewal) {
1243                 $$ctx{force} = 1;
1244                 my ($c, $e) = _reshelve_copy($ctx);
1245                 return $e if $e;
1246                 delete $$ctx{force};
1247                 $U->commit_db_session($session);
1248                 return OpenILS::Event->new('SUCCESS');
1249         }
1250
1251         # Now, let's see if this copy is needed for a hold
1252         my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor ); 
1253
1254         if($hold) {
1255
1256                 $ctx->{hold}    = $hold;
1257                 $change                 = 1;
1258                 
1259                 # Capture the hold with this copy
1260                 return $evt if ($evt = _checkin_capture_hold($ctx));
1261
1262                 if( $hold->pickup_lib == $requestor->ws_ou ) {
1263
1264                         # This hold was captured in the correct location
1265                         $evt = OpenILS::Event->new('SUCCESS');
1266
1267                 } else {
1268
1269                         # Hold needs to be picked up elsewhere.  Build a hold 
1270                         # transit and route the item.
1271                         return $evt if ($evt =_checkin_build_hold_transit($ctx));
1272                         $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1273                 }
1274
1275         } else { # not needed for a hold
1276
1277                 if( $copy->circ_lib == $requestor->ws_ou ) {
1278
1279                         # Copy is in the right place.
1280                         $evt = OpenILS::Event->new('SUCCESS');
1281
1282                         # if the item happens to be a pre-cataloged item, send it
1283                         # to cataloging and return the event
1284                         my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1285                         return $err if $err;
1286                         $change         = 1 if $c;
1287                         $evt                    = $e if $e;
1288
1289                 } else {
1290
1291                         # Copy wants to go home. Transit it there.
1292                         return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1293                         $evt                    = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1294                         $change         = 1;
1295                 }
1296         }
1297
1298
1299         # ------------------------------------------------------------------
1300         # if the copy is not in a state that should persist,
1301         # set the copy to reshelving if it's not already there
1302         # ------------------------------------------------------------------
1303         my ($c, $e) = _reshelve_copy($ctx);
1304         return $e if $e;
1305         $change = $c unless $change;
1306
1307         if(!$change) {
1308
1309                 $evt = OpenILS::Event->new('NO_CHANGE');
1310                 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id) 
1311                         if( $copy->status == $U->copy_status_from_name('on holds shelf')->id );
1312
1313         } else {
1314
1315                 $U->commit_db_session($session);
1316         }
1317
1318         $logger->activity("checkin by user ".$requestor->id." on item ".
1319                 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1320
1321         return _checkin_flesh_event($ctx, $evt);
1322 }
1323
1324 sub _reshelve_copy {
1325
1326         my $ctx = shift;
1327         my $copy                = $ctx->{copy};
1328         my $reqr                = $ctx->{requestor};
1329         my $session     = $ctx->{session};
1330         my $force       = $ctx->{force};
1331
1332         my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1333
1334         if($force || (
1335                 $stat != $U->copy_status_from_name('on holds shelf')->id and 
1336                 $stat != $U->copy_status_from_name('available')->id and 
1337                 $stat != $U->copy_status_from_name('cataloging')->id and 
1338                 $stat != $U->copy_status_from_name('in transit')->id and 
1339                 $stat != $U->copy_status_from_name('reshelving')->id) ) {
1340
1341                 $copy->status( $U->copy_status_from_name('reshelving')->id );
1342
1343                 my $evt = $U->update_copy( 
1344                         copy            => $copy,
1345                         editor  => $reqr->id,
1346                         session => $session,
1347                         );
1348
1349                 return( 1, $evt );
1350         }
1351         return undef;
1352 }
1353
1354
1355
1356
1357 # returns undef if there are no 'open' claims-returned circs attached
1358 # to the given copy.  if there is an open claims-returned circ, 
1359 # then we check for override mode.  if in override, mark the claims-returned
1360 # circ as checked in.  if not, return event.
1361 sub _handle_claims_returned {
1362         my $ctx = shift;
1363         my $copy = $ctx->{copy};
1364
1365         my $CR  = _fetch_open_claims_returned($copy->id);
1366         return undef unless $CR;
1367
1368         # - If the caller has set the override flag, we will check the item in
1369         if($ctx->{override}) {
1370
1371                 $CR->checkin_time('now');       
1372                 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1373                 $CR->checkin_staff($ctx->{requestor}->id);
1374
1375                 my $stat = $U->storagereq(
1376                         'open-ils.storage.direct.action.circulation.update', $CR);
1377                 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1378                 return OpenILS::Event->new('SUCCESS');
1379
1380         } else {
1381                 # - if not in override mode, return the CR event
1382                 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1383         }
1384 }
1385
1386
1387 sub _fetch_open_claims_returned {
1388         my $copyid = shift;
1389         my $trans = $U->storagereq(
1390                 'open-ils.storage.direct.action.circulation.search_where',
1391                 {       
1392                         target_copy             => $copyid, 
1393                         stop_fines              => 'CLAIMSRETURNED',
1394                         checkin_time    => undef,
1395                 }
1396         );
1397         return $$trans[0] if $trans && $$trans[0];
1398         return undef;
1399 }
1400
1401 # - if the copy is has the 'in process' status, set it to reshelving
1402 #sub _check_in_process {
1403         #my $ctx = shift;
1404 #
1405         #my $copy = $ctx->{copy};
1406         #my $reqr       = $ctx->{requestor};
1407         #my $ses        = $ctx->{session};
1408 ##
1409         #my $stat = $U->copy_status_from_name('in process');
1410         #my $rstat = $U->copy_status_from_name('reshelving');
1411 #
1412         #if( $stat->id == $copy->status->id ) {
1413                 #$logger->info("marking 'in-process' copy ".$copy->id." as 'reshelving'");
1414                 #$copy->status( $rstat->id );
1415                 #my $evt = $U->update_copy( 
1416                         #copy           => $copy,
1417                         #editor => $reqr->id,
1418                         #session        => $ses
1419                         #);
1420                 #return $evt if $evt;
1421 #
1422                 #$copy->status( $rstat ); # - reflesh the copy status
1423         #}
1424         #return undef;
1425 #}
1426
1427
1428 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1429 sub _checkin_handle_precat {
1430
1431         my $ctx         = shift;
1432         my $copy                = $ctx->{copy};
1433         my $evt         = undef;
1434         my $errevt      = undef;
1435         my $change      = 0;
1436
1437         my $catstat = $U->copy_status_from_name('cataloging');
1438
1439         if( $ctx->{precat} ) {
1440
1441                 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1442
1443                 if( $copy->status != $catstat->id ) {
1444                         $copy->status($catstat->id);
1445
1446                         return (undef, 0, $errevt) if (
1447                                 $errevt = $U->update_copy(
1448                                         copy            => $copy, 
1449                                         editor  => $ctx->{requestor}->id, 
1450                                         session => $ctx->{session} ));
1451                         $change = 1;
1452
1453                 }
1454         }
1455
1456         return ($evt, $change, undef);
1457 }
1458
1459
1460 # returns the appropriate event for the given copy status
1461 # if the copy is not in a 'special' status, undef is returned
1462 sub _checkin_check_copy_status {
1463         my $ctx = shift;
1464         my $copy = $ctx->{copy};
1465         my $reqr        = $ctx->{requestor};
1466         my $ses = $ctx->{session};
1467
1468         my $islost              = 0;
1469         my $ismissing   = 0;
1470         my $evt                 = undef;
1471
1472         my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1473
1474         return undef 
1475                 if(     $status == $U->copy_status_from_name('available')->id           ||
1476                                 $status == $U->copy_status_from_name('checked out')->id ||
1477                                 $status == $U->copy_status_from_name('in process')->id  ||
1478                                 $status == $U->copy_status_from_name('in transit')->id  ||
1479                                 $status == $U->copy_status_from_name('reshelving')->id );
1480
1481         return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy ) 
1482                 if( $status == $U->copy_status_from_name('lost')->id );
1483
1484         return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy ) 
1485                 if( $status == $U->copy_status_from_name('missing')->id );
1486
1487         return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1488
1489
1490
1491 #       my $rstat = $U->copy_status_from_name('reshelving');
1492 #       my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1493 #
1494 #       if( $stat == $U->copy_status_from_name('lost')->id ) {
1495 #               $islost = 1;
1496 #               $evt = OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy );
1497 #
1498 #       } elsif( $stat == $U->copy_status_from_name('missing')->id) {
1499 #               $ismissing = 1;
1500 #               $evt = OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy );
1501 #       }
1502 #
1503 #       return (undef,$evt) if(!$ctx->{override});
1504 #
1505 #       # we're are now going to attempt to override the failure 
1506 #       # and set the copy to reshelving
1507 #       my $e;
1508 #       my $copyid = $copy->id;
1509 #       my $userid = $reqr->id;
1510 #       if( $islost ) {
1511 #
1512 #               # - make sure we have permission
1513 #               $e = $U->check_perms( $reqr->id, 
1514 #                       $copy->circ_lib, 'COPY_STATUS_LOST.override');
1515 #               return (undef,$e) if $e;
1516 #               $copy->status( $rstat->id );
1517 #
1518 #               # XXX if no fines are owed in the circ, close it out - will this happen later anyway?
1519 #               #my $circ = $U->storagereq(
1520 #               #       'open-ils.storage.direct.action.circulation
1521 #
1522 #               $logger->activity("user $userid overriding 'lost' copy status for copy $copyid");
1523 #
1524 #       } elsif( $ismissing ) {
1525 #
1526 #               # - make sure we have permission
1527 #               $e = $U->check_perms( $reqr->id, 
1528 #                       $copy->circ_lib, 'COPY_STATUS_MISSING.override');
1529 #               return (undef,$e) if $e;
1530 #               $copy->status( $rstat->id );
1531 #               $logger->activity("user $userid overriding 'missing' copy status for copy $copyid");
1532 #       }
1533 #
1534 #       if( $islost or $ismissing ) {
1535 #
1536 #               # - update the copy with the new status
1537 #               $evt = $U->update_copy(
1538 #                       copy            => $copy,
1539 #                       editor  => $reqr->id,
1540 #                       session => $ses
1541 #               );
1542 #               return (undef,$evt) if $evt;
1543 #               $copy->status( $rstat );
1544 #       }
1545 #
1546 #       return (1);
1547
1548
1549 }
1550
1551 # Just gets the copy back home.  Returns undef on success, event on error
1552 sub _checkin_build_generic_copy_transit {
1553
1554         my $ctx                 = shift;
1555         my $requestor   = $ctx->{requestor};
1556         my $copy                        = $ctx->{copy};
1557         my $transit             = Fieldmapper::action::transit_copy->new;
1558         my $session             = $ctx->{session};
1559
1560         $logger->activity("User ". $requestor->id ." creating a ".
1561                 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1562
1563         $transit->source($requestor->ws_ou);
1564         $transit->dest($copy->circ_lib);
1565         $transit->target_copy($copy->id);
1566         $transit->source_send_time('now');
1567         $transit->copy_status($copy->status);
1568         
1569         $logger->debug("Creating new copy_transit in DB");
1570
1571         my $s = $session->request(
1572                 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1573         return $U->DB_UPDATE_FAILED($transit) unless $s;
1574
1575         $logger->info("Checkin copy successfully created new transit: $s");
1576
1577         $copy->status($U->copy_status_from_name('in transit')->id );
1578
1579         return $U->update_copy( copy => $copy, 
1580                         editor => $requestor->id, session => $session );
1581         
1582 }
1583
1584
1585 # returns event on error, undef on success
1586 sub _checkin_build_hold_transit {
1587         my $ctx = shift;
1588
1589         my $copy = $ctx->{copy};
1590         my $hold = $ctx->{hold};
1591         my $trans = Fieldmapper::action::hold_transit_copy->new;
1592
1593         $trans->hold($hold->id);
1594         $trans->source($ctx->{requestor}->ws_ou);
1595         $trans->dest($hold->pickup_lib);
1596         $trans->source_send_time("now");
1597         $trans->target_copy($copy->id);
1598         $trans->copy_status($copy->status);
1599
1600         my $id = $ctx->{session}->request(
1601                 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1602         return $U->DB_UPDATE_FAILED($trans) unless $id;
1603
1604         $logger->info("Checkin copy successfully created hold transit: $id");
1605
1606         $copy->status($U->copy_status_from_name('in transit')->id );
1607         return $U->update_copy( copy => $copy, 
1608                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1609 }
1610
1611 # Returns event on error, undef on success
1612 sub _checkin_capture_hold {
1613         my $ctx = shift;
1614         my $copy = $ctx->{copy};
1615         my $hold = $ctx->{hold}; 
1616
1617         $logger->debug("Checkin copy capturing hold ".$hold->id);
1618
1619         $hold->current_copy($copy->id);
1620         $hold->capture_time('now'); 
1621
1622         my $stat = $ctx->{session}->request(
1623                 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1624         return $U->DB_UPDATE_FAILED($hold) unless $stat;
1625
1626         $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1627
1628         return $U->update_copy( copy => $copy, 
1629                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1630 }
1631
1632 # fleshes an event with the relevant objects from the context
1633 sub _checkin_flesh_event {
1634         my $ctx = shift;
1635         my $evt = shift;
1636
1637         my $payload                             = {};
1638         $payload->{copy}                = $U->unflesh_copy($ctx->{copy});
1639         $payload->{record}      = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1640         $payload->{circ}                = $ctx->{circ} if $ctx->{circ};
1641         $payload->{transit}     = $ctx->{transit} if $ctx->{transit};
1642         $payload->{hold}                = $ctx->{hold} if $ctx->{hold};
1643
1644         $evt->{payload} = $payload;
1645         return $evt;
1646 }
1647
1648
1649 # Closes out the circulation, puts the copy into reshelving.
1650 # Voids any bills attached to this circ after the backdate time 
1651 # if a backdate is provided
1652 sub _checkin_handle_circ { 
1653
1654         my $ctx = shift;
1655
1656         my $circ = $ctx->{circ};
1657         my $copy = $ctx->{copy};
1658         my $requestor   = $ctx->{requestor};
1659         my $session             = $ctx->{session};
1660         my $evt;
1661         my $obt;
1662
1663         $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1664
1665         #$ctx->{longoverdue}            = 1 if ($circ->stop_fines =~ /longoverdue/io);
1666         #$ctx->{claimsreturned} = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1667
1668         # backdate the circ if necessary
1669         if(my $backdate = $ctx->{backdate}) {
1670                 return $evt if ($evt = 
1671                         _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1672         }
1673
1674
1675         if(!$circ->stop_fines) {
1676                 $circ->stop_fines('CHECKIN');
1677                 $circ->stop_fines('RENEW') if $__isrenewal;
1678                 $circ->stop_fines_time('now');
1679         }
1680
1681         # see if there are any fines owed on this circ.  if not, close it
1682         ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1683         return $evt if $evt;
1684         $circ->xact_finish('now') if( $obt->balance_owed <= 0 );
1685
1686         # Set the checkin vars since we have the item
1687         $circ->checkin_time('now');
1688         $circ->checkin_staff($requestor->id);
1689         $circ->checkin_lib($requestor->ws_ou);
1690
1691
1692 #       $copy->status($U->copy_status_from_name('reshelving')->id);
1693 #       $evt = $U->update_copy( session => $session, 
1694 #               copy => $copy, editor => $requestor->id );
1695 #       return $evt if $evt;
1696
1697         $ctx->{session}->request(
1698                 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1699
1700         return undef;
1701 }
1702
1703 sub _set_copy_reshelving {
1704         my( $copy, $reqr, $session ) = @_;
1705
1706         $logger->info("Setting copy ".$copy->id." to reshelving");
1707         $copy->status($U->copy_status_from_name('reshelving')->id);
1708
1709         my $evt = $U->update_copy( 
1710                 session => $session, 
1711                 copy            => $copy, 
1712                 editor  => $reqr
1713                 );
1714         return $evt if $evt;
1715 }
1716
1717 # returns event on error, undef on success
1718 # This voids all bills attached to the given circulation that occurred
1719 # after the backdate 
1720 # THIS DOES NOT CLOSE THE CIRC if there are no more fines on the item
1721 sub _checkin_handle_backdate {
1722         my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1723
1724         $logger->activity("User ".$requestor->id.
1725                 " backdating circ [".$circ->target_copy."] to date: $backdate");
1726
1727         my $bills = $session->request( # XXX Verify this call is correct
1728                 "open-ils.storage.direct.money.billing.search_where.atomic",
1729                 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1730
1731         if($bills) {
1732                 for my $bill (@$bills) {
1733                         $bill->voided('t');
1734                         my $s = $session->request(
1735                                 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1736                         return $U->DB_UPDATE_FAILED($bill) unless $s;
1737                 }
1738         }
1739
1740         # if the caller elects to attempt to close the circulation
1741         # transaction, then it will be closed if there are not further
1742         # charges on the transaction
1743         #if( $closecirc ) {
1744                 #my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1745            #return $evt if $evt;
1746                 #$circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1747         #}
1748
1749         return undef;
1750 }
1751
1752
1753 sub _find_patron_from_params {
1754         my $params = shift;
1755
1756         my $patron;
1757         my $copy;
1758         my $circ;
1759         my $evt;
1760
1761         if(my $barcode = $params->{barcode}) {
1762                 $logger->debug("circ finding user from params with barcode $barcode");
1763                 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1764                 return (undef, undef, $evt) if $evt;
1765                 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1766                 return (undef, undef, $evt) if $evt;
1767                 ($patron, $evt) = $U->fetch_user($circ->usr);
1768                 return (undef, undef, $evt) if $evt;
1769         }
1770         return ($patron, $copy);
1771 }
1772
1773
1774 # ------------------------------------------------------------------------------
1775
1776 __PACKAGE__->register_method(
1777         method  => "renew",
1778         api_name        => "open-ils.circ.renew.override",
1779         signature       => q/@see open-ils.circ.renew/,
1780 );
1781
1782
1783 __PACKAGE__->register_method(
1784         method  => "renew",
1785         api_name        => "open-ils.circ.renew",
1786         notes           => <<"  NOTES");
1787         PARAMS( authtoken, circ => circ_id );
1788         open-ils.circ.renew(login_session, circ_object);
1789         Renews the provided circulation.  login_session is the requestor of the
1790         renewal and if the logged in user is not the same as circ->usr, then
1791         the logged in user must have RENEW_CIRC permissions.
1792         NOTES
1793
1794 sub renew {
1795         my( $self, $client, $authtoken, $params ) = @_;
1796         $U->logmark;
1797
1798         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1799         $__isrenewal = 1;
1800
1801         $params->{override} = 1 if $self->api_name =~ /override/o;
1802
1803         # fetch the patron object one way or another
1804         if( $params->{patron} ) {
1805                 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1806                 if($evt) { $__isrenewal = 0; return $evt; }
1807
1808         } elsif( $params->{patron_barcode} ) {
1809                 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1810                 if($evt) { $__isrenewal = 0; return $evt; }
1811
1812         } else {
1813                 ($patron, $copy, $evt) = _find_patron_from_params($params);
1814                 return $evt if $evt;
1815                 $params->{copy} = $copy;
1816         }
1817
1818         # verify our login session
1819         ($requestor, $evt) = $U->checkses($authtoken);
1820         if($evt) { $__isrenewal = 0; return $evt; }
1821
1822         # make sure we have permission to perform a renewal
1823         if( $requestor->id ne $patron->id ) {
1824                 $evt = $U->check_perms($requestor->id, $requestor->ws_ou, 'RENEW_CIRC');
1825                 if($evt) { $__isrenewal = 0; return $evt; }
1826         }
1827
1828
1829         # fetch and build the circulation environment
1830         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1831                 patron                                                  => $patron, 
1832                 requestor                                               => $requestor, 
1833                 patron                                                  => $patron, 
1834                 type                                                            => 'circ',
1835                 #fetch_patron_circ_summary      => 1,
1836                 fetch_copy_statuses                     => 1, 
1837                 fetch_copy_locations                    => 1, 
1838                 );
1839         if($evt) { $__isrenewal = 0; return $evt; }
1840         $params->{_ctx} = $ctx;
1841
1842         # make sure they have some renewals left and make sure the circulation exists
1843         ($circ, $evt) = _check_renewal_remaining($ctx);
1844         if($evt) { $__isrenewal = 0; return $evt; }
1845         $ctx->{old_circ} = $circ;
1846         my $renewals = $circ->renewal_remaining - 1;
1847
1848         # run the renew permit script
1849         $evt = _run_renew_scripts($ctx);
1850         if($evt) { $__isrenewal = 0; return $evt; }
1851
1852         # checkin the cop
1853         #$ctx->{patron} = $ctx->{patron}->id;
1854         $evt = $self->generic_receive($client, $authtoken, $ctx );
1855                 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1856
1857         if( !$U->event_equals($evt, 'SUCCESS') ) {
1858                 $__isrenewal = 0; return $evt; 
1859         }
1860
1861         # re-fetch the context since objects have changed in the checkin
1862         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1863                 patron                                                  => $patron, 
1864                 requestor                                               => $requestor, 
1865                 patron                                                  => $patron, 
1866                 type                                                            => 'circ',
1867                 #fetch_patron_circ_summary      => 1,
1868                 fetch_copy_statuses                     => 1, 
1869                 fetch_copy_locations                    => 1, 
1870                 );
1871         if($evt) { $__isrenewal = 0; return $evt; }
1872         $params->{_ctx} = $ctx;
1873         $ctx->{renewal_remaining} = $renewals;
1874
1875         # run the circ permit scripts
1876         if( $ctx->{permit_override} ) {
1877                 $evt = $U->check_perms(
1878                         $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1879                 if($evt) { $__isrenewal = 0; return $evt; }
1880
1881         } else {
1882                 $evt = $self->permit_circ( $client, $authtoken, $params );
1883                 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1884                         #$ctx->{precat} = 1;
1885                         $params->{precat} = 1;
1886
1887                 } else {
1888                         if(!$U->event_equals($evt, 'SUCCESS')) {
1889                                 if($evt) { $__isrenewal = 0; return $evt; }
1890                         }
1891                 }
1892                 $params->{permit_key} = $evt->{payload};
1893         }
1894
1895
1896         # checkout the item again
1897         $params->{patron} = $ctx->{patron}->id;
1898         $evt = $self->checkout($client, $authtoken, $params );
1899
1900         $logger->activity("user ".$requestor->id." renewl of item ".
1901                 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1902
1903         $__isrenewal = 0;
1904         return $evt;
1905 }
1906
1907 sub _check_renewal_remaining {
1908         my $ctx = shift;
1909         $U->logmark;
1910         my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1911         return (undef, $evt) if $evt;
1912         $evt = OpenILS::Event->new(
1913                 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1914         return ($circ, $evt);
1915 }
1916
1917 sub _run_renew_scripts {
1918         my $ctx = shift;
1919         my $runner = $ctx->{runner};
1920         $U->logmark;
1921
1922         $runner->load($scripts{circ_permit_renew});
1923         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1924
1925         my $events = $runner->retrieve('result.events');
1926         $events = [ split(/,/, $events) ]; 
1927         $logger->activity("circ_permit_renew for user ".
1928                 $ctx->{patron}->id." returned events: @$events") if @$events;
1929
1930         my @allevents;
1931         push( @allevents, OpenILS::Event->new($_)) for @$events;
1932         return \@allevents if  @allevents;
1933
1934         return undef;
1935 }
1936
1937         
1938
1939
1940 1;
1941