]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
adding handling for various copy statuses
[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 '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         # fetch and build the circulation environment
397         if( !( $ctx = $params->{_ctx}) ) {
398
399                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
400                         patron                                                  => $patron, 
401                         requestor                                               => $requestor, 
402                         type                                                            => 'circ',
403                         #fetch_patron_circ_summary      => 1,
404                         fetch_copy_statuses                     => 1, 
405                         fetch_copy_locations                    => 1, 
406                         );
407                 return $evt if $evt;
408         }
409
410         $ctx->{authtoken} = $authtoken;
411
412         $evt = undef;
413         if( $ctx->{copy} and ($evt = _handle_claims_returned($ctx)) ) {
414                 return $evt unless $U->event_equals($evt, 'SUCCESS');
415         }
416
417         if($evt) { 
418                 $evt = undef;
419
420         } else { 
421
422                 # no claims returned circ was found, check if there is any open circ
423                 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
424                         ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
425                         return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
426                 }
427         }
428
429
430         $ctx->{permit_key} = _cache_permit_key();
431         my $events = _run_permit_scripts($ctx);
432
433         if( $override ) {
434                 $evt = override_events($requestor, $requestor->ws_ou, $events);
435                 return $evt if $evt;
436                 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
437         }
438
439         return $events;
440 }
441
442 sub override_events {
443
444         my( $requestor, $org, $events ) = @_;
445         $events = [ $events ] unless ref($events) eq 'ARRAY';
446         my @failed;
447
448         for my $e (@$events) {
449                 my $tc = $e->{textcode};
450                 next if $tc eq 'SUCCESS';
451                 my $ov = "$tc.override";
452                 $logger->info("attempting to override event $ov");
453                 my $evt = $U->check_perms( $requestor->id, $org, $ov );
454                 return $evt if $evt;
455         }
456
457         return undef;
458 }
459
460
461 __PACKAGE__->register_method(
462         method  => "check_title_hold",
463         api_name        => "open-ils.circ.title_hold.is_possible",
464         notes           => q/
465                 Determines if a hold were to be placed by a given user,
466                 whether or not said hold would have any potential copies
467                 to fulfill it.
468                 @param authtoken The login session key
469                 @param params A hash of named params including:
470                         patronid  - the id of the hold recipient
471                         titleid (brn) - the id of the title to be held
472                         depth   - the hold range depth (defaults to 0)
473         /);
474
475 sub check_title_hold {
476         my( $self, $client, $authtoken, $params ) = @_;
477         my %params = %$params;
478         my $titleid = $params{titleid};
479
480         my ( $requestor, $patron, $evt ) = $U->checkses_requestor( 
481                 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
482         return $evt if $evt;
483
484         my $rangelib    = $patron->home_ou;
485         my $depth               = $params{depth} || 0;
486
487         $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
488
489         my $org = $U->simplereq(
490                 'open-ils.actor', 
491                 'open-ils.actor.org_unit.retrieve', 
492                 $authtoken, $requestor->home_ou );
493
494         my $limit       = 10;
495         my $offset      = 0;
496         my $title;
497
498         while( $title = $U->storagereq(
499                                 'open-ils.storage.biblio.record_entry.ranged_tree', 
500                                 $titleid, $rangelib, $depth, $limit, $offset ) ) {
501
502                 last unless ref($title);
503
504                 for my $cn (@{$title->call_numbers}) {
505         
506                         $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
507         
508                         for my $copy (@{$cn->copies}) {
509         
510                                 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
511         
512                                 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
513                                         {       patron                          => $patron, 
514                                                 requestor                       => $requestor, 
515                                                 copy                                    => $copy,
516                                                 title                                   => $title, 
517                                                 title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
518                                                 request_lib                     => $org } );
519         
520                                 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
521                         }
522                 }
523
524                 $offset += $limit;
525         }
526
527         return 0;
528 }
529
530
531 # Runs the patron and copy permit scripts
532 # if this is a non-cat circulation, the copy permit script 
533 # is not run
534 sub _run_permit_scripts {
535
536         my $ctx                 = shift;
537         my $runner              = $ctx->{runner};
538         my $patronid    = $ctx->{patron}->id;
539         my $barcode             = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
540         my $key                 = $ctx->{permit_key};
541
542         my $penalties = $U->update_patron_penalties( 
543                 authtoken => $ctx->{authtoken}, 
544                 patron    => $ctx->{patron} 
545         );
546
547         $penalties = $penalties->{fatal_penalties};
548
549         $logger->info("circ patron penalties user $patronid: @$penalties");
550
551         if( $ctx->{noncat} ) {
552                 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
553                 return OpenILS::Event->new('SUCCESS', payload => $key);
554         }
555
556         if($ctx->{precat}) {
557                 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
558                 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
559         }
560
561         if($ctx->{ishold}) {
562                 $logger->debug("Exiting circ permit early because request is for hold patron permit");
563                 return OpenILS::Event->new('SUCCESS');
564         }
565
566         $runner->load($scripts{circ_permit_copy});
567         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
568
569         # ---------------------------------------------------------------------
570         # Capture all of the copy permit events
571         # ---------------------------------------------------------------------
572         my $copy_events = $runner->retrieve('result.events');
573         $copy_events = [ split(/,/, $copy_events) ]; 
574         $ctx->{circ_permit_copy_events} = $copy_events;
575         $logger->activity("circ_permit_copy for copy ".
576                 "$barcode returned events: @$copy_events") if @$copy_events;
577
578         my @allevents;
579         push( @allevents, OpenILS::Event->new($_)) for @$penalties;
580         push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
581
582         my $ae = _check_copy_alert($ctx->{copy});
583         push( @allevents, $ae ) if $ae;
584
585         return OpenILS::Event->new('SUCCESS', payload => $key) unless (@allevents);
586
587         # uniquify the events
588         my %hash = map { ($_->{ilsevent} => $_) } @allevents;
589         @allevents = values %hash;
590
591         for (@allevents) {
592                 $_->{payload} = $ctx->{copy}->status->id
593                         if ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
594         }
595
596         return \@allevents;
597 }
598
599 sub _check_copy_alert {
600         my $copy = shift;
601         return OpenILS::Event->new('COPY_ALERT_MESSAGE', 
602                 payload => $copy->alert_message) if $copy->alert_message;
603         return undef;
604 }
605
606 # takes copyid, patronid, and requestor id
607 sub _cache_permit_key {
608         my $key = md5_hex( time() . rand() . "$$" );
609         $logger->debug("Setting circ permit key to $key");
610         $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
611         return $key;
612 }
613
614 sub _check_permit_key {
615         my $key = shift;
616         $logger->debug("Fetching circ permit key $key");
617         my $k = "oils_permit_key_$key";
618         my $one = $cache_handle->get_cache($k);
619         $cache_handle->delete_cache($k);
620         return ($one) ? 1 : 0;
621 }
622
623
624 # ------------------------------------------------------------------------------
625
626 __PACKAGE__->register_method(
627         method  => "checkout",
628         api_name        => "open-ils.circ.checkout",
629         notes => q/
630                 Checks out an item
631                 @param authtoken The login session key
632                 @param params A named hash of params including:
633                         copy                    The copy object
634                         barcode         If no copy is provided, the copy is retrieved via barcode
635                         copyid          If no copy or barcode is provide, the copy id will be use
636                         patron          The patron's id
637                         noncat          True if this is a circulation for a non-cataloted item
638                         noncat_type     The non-cataloged type id
639                         noncat_circ_lib The location for the noncat circ.  
640                         precat          The item has yet to be cataloged
641                         dummy_title The temporary title of the pre-cataloded item
642                         dummy_author The temporary authr of the pre-cataloded item
643                                 Default is the home org of the staff member
644                 @return The SUCCESS event on success, any other event depending on the error
645         /);
646
647 sub checkout {
648         my( $self, $client, $authtoken, $params ) = @_;
649         $U->logmark;
650
651         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
652         my $key = $params->{permit_key};
653
654         # if this is a renewal, then the requestor does not have to
655         # have checkout privelages
656         ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
657         ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
658         return $evt if $evt;
659
660         if( $params->{patron} ) {
661                 ( $patron, $evt ) = $U->fetch_user($params->{patron});
662                 return $evt if $evt;
663         } else {
664                 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
665                 return $evt if $evt;
666         }
667
668         # set the circ lib to the home org of the requestor if not specified
669         my $circlib = (defined($params->{circ_lib})) ? 
670                 $params->{circ_lib} : $requestor->ws_ou;
671
672
673         # Make sure the caller has a valid permit key or is 
674         # overriding the permit can
675         if( $params->{permit_override} ) {
676                 $evt = $U->check_perms(
677                         $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
678                 return $evt if $evt;
679
680         } else {
681                 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY') 
682                         unless _check_permit_key($key);
683         }
684
685         # if this is a non-cataloged item, check it out and return
686         return _checkout_noncat( 
687                 $key, $requestor, $patron, %$params ) if $params->{noncat};
688
689         # if this item has yet to be cataloged, make sure a dummy copy exists
690         ( $params->{copy}, $evt ) = _make_precat_copy(
691                 $requestor, $circlib, $params ) if $params->{precat};
692         return $evt if $evt;
693
694
695         # fetch and build the circulation environment
696         if( !( $ctx = $params->{_ctx}) ) {
697                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
698                         patron                                                  => $patron, 
699                         requestor                                               => $requestor, 
700                         session                                                 => $U->start_db_session(),
701                         type                                                            => 'circ',
702                         #fetch_patron_circ_summary      => 1,
703                         fetch_copy_statuses                     => 1, 
704                         fetch_copy_locations                    => 1, 
705                         );
706                 return $evt if $evt;
707         }
708         $ctx->{session} = $U->start_db_session() unless $ctx->{session};
709
710         # if the call doesn't know it's not cataloged..
711         if(!$params->{precat}) {
712                 if( $ctx->{copy}->call_number eq '-1' ) {
713                         return OpenILS::Event->new('ITEM_NOT_CATALOGED');
714                 }
715         }
716
717         # this happens in permit.. but we need to check here for 'offline' requests
718         ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
719         return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
720
721         my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
722
723
724         $ctx->{circ_lib} = $circlib;
725
726         $evt = _run_checkout_scripts($ctx);
727         return $evt if $evt;
728
729
730         _build_checkout_circ_object($ctx);
731
732         $evt = _apply_modified_due_date($ctx);
733         return $evt if $evt;
734
735         $evt = _commit_checkout_circ_object($ctx);
736         return $evt if $evt;
737
738         $evt = _update_checkout_copy($ctx);
739         return $evt if $evt;
740
741         my $holds;
742         ($holds, $evt) = _handle_related_holds($ctx);
743         return $evt if $evt;
744
745
746         $logger->debug("Checkout committing objects with session thread trace: ".$ctx->{session}->session_id);
747         $U->commit_db_session($ctx->{session});
748         my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
749
750         $logger->activity("user ".$requestor->id." successfully checked out item ".
751                 $ctx->{copy}->barcode." to user ".$ctx->{patron}->id );
752
753
754         # ------------------------------------------------------------------------------
755         # Update the patron penalty info in the DB
756         # ------------------------------------------------------------------------------
757         $U->update_patron_penalties( 
758                 authtoken => $authtoken, 
759                 patron    => $ctx->{patron} ,
760                 background      => 1,
761         );
762
763         return OpenILS::Event->new('SUCCESS', 
764                 payload => { 
765                         copy                                    => $U->unflesh_copy($ctx->{copy}),
766                         circ                                    => $ctx->{circ},
767                         record                          => $record,
768                         holds_fulfilled => $holds,
769                 } 
770         )
771 }
772
773
774 sub _make_precat_copy {
775         my ( $requestor, $circlib, $params ) =  @_;
776         $U->logmark;
777         my( $copy, undef ) = _find_copy_by_attr(%$params);
778
779         if($copy) {
780                 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
781                 return ($copy, undef);
782         }
783
784         $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
785
786         my $evt = OpenILS::Event->new(
787                 'BAD_PARAMS', desc => "Dummy title or author not provided" ) 
788                 unless ( $params->{dummy_title} and $params->{dummy_author} );
789         return (undef, $evt) if $evt;
790
791         $copy = Fieldmapper::asset::copy->new;
792         $copy->circ_lib($circlib);
793         $copy->creator($requestor->id);
794         $copy->editor($requestor->id);
795         $copy->barcode($params->{barcode});
796         $copy->call_number(-1); #special CN for precat materials
797         $copy->loan_duration(&PRECAT_LOAN_DURATION); 
798         $copy->fine_level(&PRECAT_FINE_LEVEL);
799
800         $copy->dummy_title($params->{dummy_title});
801         $copy->dummy_author($params->{dummy_author});
802
803         my $id = $U->storagereq(
804                 'open-ils.storage.direct.asset.copy.create', $copy );
805         return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
806
807         $logger->debug("Pre-cataloged copy successfully created");
808         return $U->fetch_copy($id);
809 }
810
811
812 sub _run_checkout_scripts {
813         my $ctx = shift;
814         $U->logmark;
815         my $evt;
816         my $circ;
817
818         my $runner = $ctx->{runner};
819
820         $runner->insert('result.durationLevel');
821         $runner->insert('result.durationRule');
822         $runner->insert('result.recurringFinesRule');
823         $runner->insert('result.recurringFinesLevel');
824         $runner->insert('result.maxFine');
825
826         $runner->load($scripts{circ_duration});
827         $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
828         my $duration = $runner->retrieve('result.durationRule');
829         $logger->debug("Circ duration script yielded a duration rule of: $duration");
830
831         $runner->load($scripts{circ_recurring_fines});
832         $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
833         my $recurring = $runner->retrieve('result.recurringFinesRule');
834         $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
835
836         $runner->load($scripts{circ_max_fines});
837         $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
838         my $max_fine = $runner->retrieve('result.maxFine');
839         $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
840
841         ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
842         return $evt if $evt;
843         ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
844         return $evt if $evt;
845         ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
846         return $evt if $evt;
847
848         $ctx->{duration_level}                  = $runner->retrieve('result.durationLevel');
849         $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
850         $ctx->{duration_rule}                   = $duration;
851         $ctx->{recurring_fines_rule}    = $recurring;
852         $ctx->{max_fine_rule}                   = $max_fine;
853
854         return undef;
855 }
856
857 sub _build_checkout_circ_object {
858         my $ctx = shift;
859         $U->logmark;
860
861         my $circ                        = new Fieldmapper::action::circulation;
862         my $duration    = $ctx->{duration_rule};
863         my $max                 = $ctx->{max_fine_rule};
864         my $recurring   = $ctx->{recurring_fines_rule};
865         my $copy                        = $ctx->{copy};
866         my $patron              = $ctx->{patron};
867         my $dur_level   = $ctx->{duration_level};
868         my $rec_level   = $ctx->{recurring_fines_level};
869
870         $circ->duration( $duration->shrt ) if ($dur_level == 1);
871         $circ->duration( $duration->normal ) if ($dur_level == 2);
872         $circ->duration( $duration->extended ) if ($dur_level == 3);
873
874         $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
875         $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
876         $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
877
878         $circ->duration_rule( $duration->name );
879         $circ->recuring_fine_rule( $recurring->name );
880         $circ->max_fine_rule( $max->name );
881         $circ->max_fine( $max->amount );
882
883         $circ->fine_interval($recurring->recurance_interval);
884         $circ->renewal_remaining( $duration->max_renewals );
885         $circ->target_copy( $copy->id );
886         $circ->usr( $patron->id );
887         $circ->circ_lib( $ctx->{circ_lib} );
888
889         if( $__isrenewal ) {
890                 $logger->debug("Circ is a renewal.  Setting renewal_remaining to " . $ctx->{renewal_remaining} );
891                 $circ->opac_renewal(1); 
892                 $circ->renewal_remaining($ctx->{renewal_remaining});
893                 $circ->circ_staff($ctx->{requestor}->id);
894         } 
895
896
897         # if the user provided an overiding checkout time, 
898         # (e.g. the checkout really happened several hours ago), then
899         # we apply that here.  Does this need a perm??
900         if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
901                 $logger->debug("circ setting checkout_time to $ds");
902                 $circ->xact_start($ds);
903         }
904
905         # if a patron is renewing, 'requestor' will be the patron
906         $circ->circ_staff($ctx->{requestor}->id ); 
907         _set_circ_due_date($circ);
908         $ctx->{circ} = $circ;
909 }
910
911 sub _apply_modified_due_date {
912         my $ctx = shift;
913         my $circ = $ctx->{circ};
914
915         if( $ctx->{due_date} ) {
916
917                 my $evt = $U->check_perms(
918                         $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
919                 return $evt if $evt;
920
921                 my $ds = _create_date_stamp($ctx->{due_date});
922                 $logger->debug("circ modifying  due_date to $ds");
923                 $circ->due_date($ds);
924
925         }
926         return undef;
927 }
928
929 sub _create_date_stamp {
930         my $datestring = shift;
931         return undef unless $datestring;
932         $datestring = clense_ISO8601($datestring);
933         $logger->debug("circ created date stamp => $datestring");
934         return $datestring;
935 }
936
937 sub _create_due_date {
938         my $duration = shift;
939         $U->logmark;
940         my ($sec,$min,$hour,$mday,$mon,$year) = 
941                 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
942         $year += 1900; $mon += 1;
943         my $due_date = sprintf(
944         '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
945         $year, $mon, $mday, $hour, $min, $sec);
946         return $due_date;
947 }
948
949 sub _set_circ_due_date {
950         my $circ = shift;
951         $U->logmark;
952         my $dd = _create_due_date($circ->duration);
953         $logger->debug("Checkout setting due date on circ to: $dd");
954         $circ->due_date($dd);
955 }
956
957 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
958 sub _update_checkout_copy {
959         my $ctx = shift;
960         $U->logmark;
961         my $copy = $ctx->{copy};
962
963         my $s = $U->copy_status_from_name('checked out');
964         $copy->status( $s->id ) if $s;
965
966         my $evt = $U->update_copy( session => $ctx->{session}, 
967                 copy => $copy, editor => $ctx->{requestor}->id );
968         return (undef,$evt) if $evt;
969
970         return undef;
971 }
972
973 # commits the circ object to the db then fleshes the circ with rules objects
974 sub _commit_checkout_circ_object {
975
976         my $ctx = shift;
977         my $circ = $ctx->{circ};
978         $U->logmark;
979
980         $circ->clear_id;
981         my $r = $ctx->{session}->request(
982                 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
983
984         return $U->DB_UPDATE_FAILED($circ) unless $r;
985
986         $logger->debug("Created a new circ object in checkout: $r");
987
988         $circ->id($r);
989         $circ->duration_rule($ctx->{duration_rule});
990         $circ->max_fine_rule($ctx->{max_fine_rule});
991         $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
992
993         return undef;
994 }
995
996
997 # sees if there are any holds that this copy 
998 sub _handle_related_holds {
999
1000         my $ctx         = shift;
1001         my $copy                = $ctx->{copy};
1002         my $patron      = $ctx->{patron};
1003         my $holds       = $holdcode->fetch_related_holds($copy->id);
1004         $U->logmark;
1005         my @fulfilled;
1006
1007         # XXX We should only fulfill one hold here...
1008         # XXX If a hold was transited to the user who is checking out
1009         # the item, we need to make sure that hold is what's grabbed
1010         if(ref($holds) && @$holds) {
1011
1012                 # for now, just sort by id to get what should be the oldest hold
1013                 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1014                 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
1015
1016                 if(@$holds) {
1017                         my $hold = $holds->[0];
1018
1019                         $logger->debug("Related hold found in checkout: " . $hold->id );
1020
1021                         $hold->current_copy($copy->id); # just make sure it's set
1022                         # if the hold was never officially captured, capture it.
1023                         $hold->capture_time('now') unless $hold->capture_time;
1024                         $hold->fulfillment_time('now');
1025                         my $r = $ctx->{session}->request(
1026                                 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1027                         return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1028                         push( @fulfilled, $hold->id );
1029                 }
1030         }
1031
1032         return (\@fulfilled, undef);
1033 }
1034
1035 sub _checkout_noncat {
1036         my ( $key, $requestor, $patron, %params ) = @_;
1037         my( $circ, $circlib, $evt );
1038         $U->logmark;
1039
1040         $circlib = $params{noncat_circ_lib} || $requestor->ws_ou;
1041
1042         my $count = $params{noncat_count} || 1;
1043         my $cotime = _create_date_stamp($params{checkout_time}) || "";
1044         $logger->info("circ creating $count noncat circs with checkout time $cotime");
1045         for(1..$count) {
1046                 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1047                         $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1048                 return $evt if $evt;
1049         }
1050
1051         return OpenILS::Event->new( 
1052                 'SUCCESS', payload => { noncat_circ => $circ } );
1053 }
1054
1055
1056 __PACKAGE__->register_method(
1057         method  => "generic_receive",
1058         api_name        => "open-ils.circ.checkin",
1059         argc            => 2,
1060         signature       => q/
1061                 Generic super-method for handling all copies
1062                 @param authtoken The login session key
1063                 @param params Hash of named parameters including:
1064                         barcode - The copy barcode
1065                         force           - If true, copies in bad statuses will be checked in and give good statuses
1066                         ...
1067         /
1068 );
1069
1070 __PACKAGE__->register_method(
1071         method  => "generic_receive",
1072         api_name        => "open-ils.circ.checkin.override",
1073         signature       => q/@see open-ils.circ.checkin/
1074 );
1075
1076 sub generic_receive {
1077         my( $self, $conn, $authtoken, $params ) = @_;
1078         my( $ctx, $requestor, $evt );
1079
1080         ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1081         ( $requestor, $evt ) = $U->checksesperm( 
1082                 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1083         return $evt if $evt;
1084
1085         # load up the circ objects
1086         if( !( $ctx = $params->{_ctx}) ) {
1087                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
1088                         requestor                                               => $requestor, 
1089                         session                                                 => $U->start_db_session(),
1090                         type                                                            => 'circ',
1091                         fetch_copy_statuses                     => 1, 
1092                         fetch_copy_locations                    => 1, 
1093                         no_runner                                               => 1,  
1094                         );
1095                 return $evt if $evt;
1096         }
1097         $ctx->{override} = 1 if $self->api_name =~ /override/o;
1098         $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1099         $ctx->{authtoken} = $authtoken;
1100         my $session = $ctx->{session};
1101
1102         my $copy = $ctx->{copy};
1103         $U->unflesh_copy($copy);
1104         return OpenILS::Event->new('COPY_NOT_FOUND') unless $copy;
1105
1106         $logger->info("Checkin copy called by user ".
1107                 $requestor->id." for copy ".$copy->id);
1108
1109         # ------------------------------------------------------------------------------
1110         # Update the patron penalty info in the DB
1111         # ------------------------------------------------------------------------------
1112         $U->update_patron_penalties( 
1113                 authtoken => $authtoken, 
1114                 patron    => $ctx->{patron},
1115                 background => 1
1116         );
1117
1118         return $self->checkin_do_receive($conn, $ctx);
1119 }
1120
1121 sub checkin_do_receive {
1122
1123         my( $self, $connection, $ctx ) = @_;
1124
1125         my $evt;
1126         my $copy                        = $ctx->{copy};
1127         my $session             = $ctx->{session};
1128         my $requestor   = $ctx->{requestor};
1129         my $change              = 0; # did we actually do anything?
1130         my $circ;
1131
1132         my @eventlist;
1133
1134         # does the copy have an attached alert message?
1135         my $ae = _check_copy_alert($copy);
1136         push(@eventlist, $ae) if $ae;
1137
1138         # is the copy is an a status we can't automatically resolve?
1139         $evt = _checkin_check_copy_status($ctx);
1140         push( @eventlist, $evt ) if $evt;
1141
1142
1143         # - see if the copy has an open circ attached
1144         ($ctx->{circ}, $evt)    = $U->fetch_open_circulation($copy->id);
1145         return $evt if ($evt and $__isrenewal); # renewals require a circulation
1146         $evt = undef;
1147         $circ = $ctx->{circ};
1148
1149         # if the circ is marked as 'claims returned', add the event to the list
1150         push( @eventlist, 'CIRC_CLAIMS_RETURNED' ) 
1151                 if ($circ and $circ->stop_fines eq 'CLAIMSRETURNED');
1152
1153         # override or die
1154         if(@eventlist) {
1155                 if($ctx->{override}) {
1156                         $evt = override_events($requestor, $requestor->ws_ou, \@eventlist );
1157                         return $evt if $evt;
1158                 } else {
1159                         return \@eventlist;
1160                 }
1161         }
1162
1163         ($ctx->{transit})       = $U->fetch_open_transit_by_copy($copy->id);
1164
1165         if( $ctx->{circ} ) {
1166
1167                 # There is an open circ on this item, close it out.
1168                 $change = 1;
1169                 $evt            = _checkin_handle_circ($ctx);
1170                 return $evt if $evt;
1171
1172         } elsif( $ctx->{transit} ) {
1173
1174                 # is this item currently in transit?
1175                 $change                 = 1;
1176                 $evt                            = $transcode->transit_receive( $copy, $requestor, $session );
1177                 my $holdtrans   = $evt->{holdtransit};
1178                 ($ctx->{hold})  = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1179
1180                 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1181
1182                         # either an error occurred or a ROUTE_ITEM was generated and the 
1183                         # item must be forwarded on to its destination.
1184                         return _checkin_flesh_event($ctx, $evt);
1185
1186                 } else {
1187
1188                         if($holdtrans) {
1189
1190                                 # copy was received as a hold transit.  Copy is at target lib
1191                                 # and hold transit is complete.  We're done here...
1192                                 $U->commit_db_session($session);
1193                                 return _checkin_flesh_event($ctx, $evt);
1194                         }
1195                         $evt = undef;
1196                 }
1197         }
1198
1199         # ------------------------------------------------------------------------------
1200         # Circulations and transits are now closed where necessary.  Now go on to see if
1201         # this copy can fulfill a hold or needs to be routed to a different location
1202         # ------------------------------------------------------------------------------
1203
1204
1205         # If it's a renewal, we're done
1206         if($__isrenewal) {
1207                 $U->commit_db_session($session);
1208                 return OpenILS::Event->new('SUCCESS');
1209         }
1210
1211         # Now, let's see if this copy is needed for a hold
1212         my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor ); 
1213
1214         if($hold) {
1215
1216                 $ctx->{hold}    = $hold;
1217                 $change                 = 1;
1218                 
1219                 # Capture the hold with this copy
1220                 return $evt if ($evt = _checkin_capture_hold($ctx));
1221
1222                 if( $hold->pickup_lib == $requestor->ws_ou ) {
1223
1224                         # This hold was captured in the correct location
1225                         $evt = OpenILS::Event->new('SUCCESS');
1226
1227                 } else {
1228
1229                         # Hold needs to be picked up elsewhere.  Build a hold 
1230                         # transit and route the item.
1231                         return $evt if ($evt =_checkin_build_hold_transit($ctx));
1232                         $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1233                 }
1234
1235         } else { # not needed for a hold
1236
1237                 if( $copy->circ_lib == $requestor->ws_ou ) {
1238
1239                         # Copy is in the right place.
1240                         $evt = OpenILS::Event->new('SUCCESS');
1241
1242                         # if the item happens to be a pre-cataloged item, send it
1243                         # to cataloging and return the event
1244                         my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1245                         return $err if $err;
1246                         $change         = 1 if $c;
1247                         $evt                    = $e if $e;
1248
1249                 } else {
1250
1251                         # Copy wants to go home. Transit it there.
1252                         return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1253                         $evt                    = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1254                         $change         = 1;
1255                 }
1256         }
1257
1258
1259         # ------------------------------------------------------------------
1260         # if the copy is not in a state that should persist,
1261         # set the copy to reshelving if it's not already there
1262         # ------------------------------------------------------------------
1263         my ($c, $e) = _reshelve_copy($ctx);
1264         return $e if $e;
1265         $change = $c unless $change;
1266
1267         if(!$change) {
1268
1269                 $evt = OpenILS::Event->new('NO_CHANGE');
1270                 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id) 
1271                         if( $copy->status == $U->copy_status_from_name('on holds shelf')->id );
1272
1273         } else {
1274
1275                 $U->commit_db_session($session);
1276         }
1277
1278         $logger->activity("checkin by user ".$requestor->id." on item ".
1279                 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1280
1281         return _checkin_flesh_event($ctx, $evt);
1282 }
1283
1284 sub _reshelve_copy {
1285
1286         my $ctx = shift;
1287         my $copy                = $ctx->{copy};
1288         my $reqr                = $ctx->{requestor};
1289         my $session     = $ctx->{session};
1290
1291         my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1292
1293         if($stat != $U->copy_status_from_name('on holds shelf')->id and 
1294                 $stat != $U->copy_status_from_name('available')->id and 
1295                 $stat != $U->copy_status_from_name('cataloging')->id and 
1296                 $stat != $U->copy_status_from_name('in transit')->id and 
1297                 $stat != $U->copy_status_from_name('reshelving')->id ) {
1298
1299                 $copy->status( $U->copy_status_from_name('reshelving')->id );
1300
1301                 my $evt = $U->update_copy( 
1302                         copy            => $copy,
1303                         editor  => $reqr->id,
1304                         session => $session,
1305                         );
1306
1307                 return( 1, $evt );
1308         }
1309         return undef;
1310 }
1311
1312
1313
1314
1315 # returns undef if there are no 'open' claims-returned circs attached
1316 # to the given copy.  if there is an open claims-returned circ, 
1317 # then we check for override mode.  if in override, mark the claims-returned
1318 # circ as checked in.  if not, return event.
1319 sub _handle_claims_returned {
1320         my $ctx = shift;
1321         my $copy = $ctx->{copy};
1322
1323         my $CR  = _fetch_open_claims_returned($copy->id);
1324         return undef unless $CR;
1325
1326         # - If the caller has set the override flag, we will check the item in
1327         if($ctx->{override}) {
1328
1329                 $CR->checkin_time('now');       
1330                 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1331                 $CR->checkin_staff($ctx->{requestor}->id);
1332
1333                 my $stat = $U->storagereq(
1334                         'open-ils.storage.direct.action.circulation.update', $CR);
1335                 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1336                 return OpenILS::Event->new('SUCCESS');
1337
1338         } else {
1339                 # - if not in override mode, return the CR event
1340                 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1341         }
1342 }
1343
1344
1345 sub _fetch_open_claims_returned {
1346         my $copyid = shift;
1347         my $trans = $U->storagereq(
1348                 'open-ils.storage.direct.action.circulation.search_where',
1349                 {       
1350                         target_copy             => $copyid, 
1351                         stop_fines              => 'CLAIMSRETURNED',
1352                         checkin_time    => undef,
1353                 }
1354         );
1355         return $$trans[0] if $trans && $$trans[0];
1356         return undef;
1357 }
1358
1359 # - if the copy is has the 'in process' status, set it to reshelving
1360 #sub _check_in_process {
1361         #my $ctx = shift;
1362 #
1363         #my $copy = $ctx->{copy};
1364         #my $reqr       = $ctx->{requestor};
1365         #my $ses        = $ctx->{session};
1366 ##
1367         #my $stat = $U->copy_status_from_name('in process');
1368         #my $rstat = $U->copy_status_from_name('reshelving');
1369 #
1370         #if( $stat->id == $copy->status->id ) {
1371                 #$logger->info("marking 'in-process' copy ".$copy->id." as 'reshelving'");
1372                 #$copy->status( $rstat->id );
1373                 #my $evt = $U->update_copy( 
1374                         #copy           => $copy,
1375                         #editor => $reqr->id,
1376                         #session        => $ses
1377                         #);
1378                 #return $evt if $evt;
1379 #
1380                 #$copy->status( $rstat ); # - reflesh the copy status
1381         #}
1382         #return undef;
1383 #}
1384
1385
1386 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1387 sub _checkin_handle_precat {
1388
1389         my $ctx         = shift;
1390         my $copy                = $ctx->{copy};
1391         my $evt         = undef;
1392         my $errevt      = undef;
1393         my $change      = 0;
1394
1395         my $catstat = $U->copy_status_from_name('cataloging');
1396
1397         if( $ctx->{precat} ) {
1398
1399                 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1400
1401                 if( $copy->status != $catstat->id ) {
1402                         $copy->status($catstat->id);
1403
1404                         return (undef, 0, $errevt) if (
1405                                 $errevt = $U->update_copy(
1406                                         copy            => $copy, 
1407                                         editor  => $ctx->{requestor}->id, 
1408                                         session => $ctx->{session} ));
1409                         $change = 1;
1410
1411                 }
1412         }
1413
1414         return ($evt, $change, undef);
1415 }
1416
1417
1418 # returns the appropriate event for the given copy status
1419 # if the copy is not in a 'special' status, undef is returned
1420 sub _checkin_check_copy_status {
1421         my $ctx = shift;
1422         my $copy = $ctx->{copy};
1423         my $reqr        = $ctx->{requestor};
1424         my $ses = $ctx->{session};
1425
1426         my $islost              = 0;
1427         my $ismissing   = 0;
1428         my $evt                 = undef;
1429
1430         my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1431
1432         return undef 
1433                 if(     $status == $U->copy_status_from_name('available')->id           ||
1434                                 $status == $U->copy_status_from_name('checked out')->id ||
1435                                 $status == $U->copy_status_from_name('in transit')->id  ||
1436                                 $status == $U->copy_status_from_name('reshelving')->id );
1437
1438         return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy ) 
1439                 if( $status == $U->copy_status_from_name('lost')->id );
1440
1441         return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy ) 
1442                 if( $status == $U->copy_status_from_name('missing')->id );
1443
1444         return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1445
1446
1447
1448 #       my $rstat = $U->copy_status_from_name('reshelving');
1449 #       my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1450 #
1451 #       if( $stat == $U->copy_status_from_name('lost')->id ) {
1452 #               $islost = 1;
1453 #               $evt = OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy );
1454 #
1455 #       } elsif( $stat == $U->copy_status_from_name('missing')->id) {
1456 #               $ismissing = 1;
1457 #               $evt = OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy );
1458 #       }
1459 #
1460 #       return (undef,$evt) if(!$ctx->{override});
1461 #
1462 #       # we're are now going to attempt to override the failure 
1463 #       # and set the copy to reshelving
1464 #       my $e;
1465 #       my $copyid = $copy->id;
1466 #       my $userid = $reqr->id;
1467 #       if( $islost ) {
1468 #
1469 #               # - make sure we have permission
1470 #               $e = $U->check_perms( $reqr->id, 
1471 #                       $copy->circ_lib, 'COPY_STATUS_LOST.override');
1472 #               return (undef,$e) if $e;
1473 #               $copy->status( $rstat->id );
1474 #
1475 #               # XXX if no fines are owed in the circ, close it out - will this happen later anyway?
1476 #               #my $circ = $U->storagereq(
1477 #               #       'open-ils.storage.direct.action.circulation
1478 #
1479 #               $logger->activity("user $userid overriding 'lost' copy status for copy $copyid");
1480 #
1481 #       } elsif( $ismissing ) {
1482 #
1483 #               # - make sure we have permission
1484 #               $e = $U->check_perms( $reqr->id, 
1485 #                       $copy->circ_lib, 'COPY_STATUS_MISSING.override');
1486 #               return (undef,$e) if $e;
1487 #               $copy->status( $rstat->id );
1488 #               $logger->activity("user $userid overriding 'missing' copy status for copy $copyid");
1489 #       }
1490 #
1491 #       if( $islost or $ismissing ) {
1492 #
1493 #               # - update the copy with the new status
1494 #               $evt = $U->update_copy(
1495 #                       copy            => $copy,
1496 #                       editor  => $reqr->id,
1497 #                       session => $ses
1498 #               );
1499 #               return (undef,$evt) if $evt;
1500 #               $copy->status( $rstat );
1501 #       }
1502 #
1503 #       return (1);
1504
1505
1506 }
1507
1508 # Just gets the copy back home.  Returns undef on success, event on error
1509 sub _checkin_build_generic_copy_transit {
1510
1511         my $ctx                 = shift;
1512         my $requestor   = $ctx->{requestor};
1513         my $copy                        = $ctx->{copy};
1514         my $transit             = Fieldmapper::action::transit_copy->new;
1515         my $session             = $ctx->{session};
1516
1517         $logger->activity("User ". $requestor->id ." creating a ".
1518                 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1519
1520         $transit->source($requestor->ws_ou);
1521         $transit->dest($copy->circ_lib);
1522         $transit->target_copy($copy->id);
1523         $transit->source_send_time('now');
1524         $transit->copy_status($copy->status);
1525         
1526         $logger->debug("Creating new copy_transit in DB");
1527
1528         my $s = $session->request(
1529                 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1530         return $U->DB_UPDATE_FAILED($transit) unless $s;
1531
1532         $logger->info("Checkin copy successfully created new transit: $s");
1533
1534         $copy->status($U->copy_status_from_name('in transit')->id );
1535
1536         return $U->update_copy( copy => $copy, 
1537                         editor => $requestor->id, session => $session );
1538         
1539 }
1540
1541
1542 # returns event on error, undef on success
1543 sub _checkin_build_hold_transit {
1544         my $ctx = shift;
1545
1546         my $copy = $ctx->{copy};
1547         my $hold = $ctx->{hold};
1548         my $trans = Fieldmapper::action::hold_transit_copy->new;
1549
1550         $trans->hold($hold->id);
1551         $trans->source($ctx->{requestor}->ws_ou);
1552         $trans->dest($hold->pickup_lib);
1553         $trans->source_send_time("now");
1554         $trans->target_copy($copy->id);
1555         $trans->copy_status($copy->status);
1556
1557         my $id = $ctx->{session}->request(
1558                 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1559         return $U->DB_UPDATE_FAILED($trans) unless $id;
1560
1561         $logger->info("Checkin copy successfully created hold transit: $id");
1562
1563         $copy->status($U->copy_status_from_name('in transit')->id );
1564         return $U->update_copy( copy => $copy, 
1565                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1566 }
1567
1568 # Returns event on error, undef on success
1569 sub _checkin_capture_hold {
1570         my $ctx = shift;
1571         my $copy = $ctx->{copy};
1572         my $hold = $ctx->{hold}; 
1573
1574         $logger->debug("Checkin copy capturing hold ".$hold->id);
1575
1576         $hold->current_copy($copy->id);
1577         $hold->capture_time('now'); 
1578
1579         my $stat = $ctx->{session}->request(
1580                 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1581         return $U->DB_UPDATE_FAILED($hold) unless $stat;
1582
1583         $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1584
1585         return $U->update_copy( copy => $copy, 
1586                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1587 }
1588
1589 # fleshes an event with the relevant objects from the context
1590 sub _checkin_flesh_event {
1591         my $ctx = shift;
1592         my $evt = shift;
1593
1594         my $payload                             = {};
1595         $payload->{copy}                = $U->unflesh_copy($ctx->{copy});
1596         $payload->{record}      = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1597         $payload->{circ}                = $ctx->{circ} if $ctx->{circ};
1598         $payload->{transit}     = $ctx->{transit} if $ctx->{transit};
1599         $payload->{hold}                = $ctx->{hold} if $ctx->{hold};
1600
1601         $evt->{payload} = $payload;
1602         return $evt;
1603 }
1604
1605
1606 # Closes out the circulation, puts the copy into reshelving.
1607 # Voids any bills attached to this circ after the backdate time 
1608 # if a backdate is provided
1609 sub _checkin_handle_circ { 
1610
1611         my $ctx = shift;
1612
1613         my $circ = $ctx->{circ};
1614         my $copy = $ctx->{copy};
1615         my $requestor   = $ctx->{requestor};
1616         my $session             = $ctx->{session};
1617         my $evt;
1618         my $obt;
1619
1620         $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1621
1622         #$ctx->{longoverdue}            = 1 if ($circ->stop_fines =~ /longoverdue/io);
1623         #$ctx->{claimsreturned} = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1624
1625         # backdate the circ if necessary
1626         if(my $backdate = $ctx->{backdate}) {
1627                 return $evt if ($evt = 
1628                         _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1629         }
1630
1631
1632         if(!$circ->stop_fines) {
1633                 $circ->stop_fines('CHECKIN');
1634                 $circ->stop_fines('RENEW') if $__isrenewal;
1635                 $circ->stop_fines_time('now');
1636         }
1637
1638         # see if there are any fines owed on this circ.  if not, close it
1639         ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1640         return $evt if $evt;
1641         $circ->xact_finish('now') if( $obt->balance_owed <= 0 );
1642
1643         # Set the checkin vars since we have the item
1644         $circ->checkin_time('now');
1645         $circ->checkin_staff($requestor->id);
1646         $circ->checkin_lib($requestor->ws_ou);
1647
1648
1649 #       $copy->status($U->copy_status_from_name('reshelving')->id);
1650 #       $evt = $U->update_copy( session => $session, 
1651 #               copy => $copy, editor => $requestor->id );
1652 #       return $evt if $evt;
1653
1654         $ctx->{session}->request(
1655                 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1656
1657         return undef;
1658 }
1659
1660 sub _set_copy_reshelving {
1661         my( $copy, $reqr, $session ) = @_;
1662
1663         $logger->info("Setting copy ".$copy->id." to reshelving");
1664         $copy->status($U->copy_status_from_name('reshelving')->id);
1665
1666         my $evt = $U->update_copy( 
1667                 session => $session, 
1668                 copy            => $copy, 
1669                 editor  => $reqr
1670                 );
1671         return $evt if $evt;
1672 }
1673
1674 # returns event on error, undef on success
1675 # This voids all bills attached to the given circulation that occurred
1676 # after the backdate 
1677 # THIS DOES NOT CLOSE THE CIRC if there are no more fines on the item
1678 sub _checkin_handle_backdate {
1679         my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1680
1681         $logger->activity("User ".$requestor->id.
1682                 " backdating circ [".$circ->target_copy."] to date: $backdate");
1683
1684         my $bills = $session->request( # XXX Verify this call is correct
1685                 "open-ils.storage.direct.money.billing.search_where.atomic",
1686                 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1687
1688         if($bills) {
1689                 for my $bill (@$bills) {
1690                         $bill->voided('t');
1691                         my $s = $session->request(
1692                                 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1693                         return $U->DB_UPDATE_FAILED($bill) unless $s;
1694                 }
1695         }
1696
1697         # if the caller elects to attempt to close the circulation
1698         # transaction, then it will be closed if there are not further
1699         # charges on the transaction
1700         #if( $closecirc ) {
1701                 #my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1702            #return $evt if $evt;
1703                 #$circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1704         #}
1705
1706         return undef;
1707 }
1708
1709
1710 sub _find_patron_from_params {
1711         my $params = shift;
1712
1713         my $patron;
1714         my $copy;
1715         my $circ;
1716         my $evt;
1717
1718         if(my $barcode = $params->{barcode}) {
1719                 $logger->debug("circ finding user from params with barcode $barcode");
1720                 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1721                 return (undef, undef, $evt) if $evt;
1722                 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1723                 return (undef, undef, $evt) if $evt;
1724                 ($patron, $evt) = $U->fetch_user($circ->usr);
1725                 return (undef, undef, $evt) if $evt;
1726         }
1727         return ($patron, $copy);
1728 }
1729
1730
1731 # ------------------------------------------------------------------------------
1732
1733 __PACKAGE__->register_method(
1734         method  => "renew",
1735         api_name        => "open-ils.circ.renew",
1736         notes           => <<"  NOTES");
1737         PARAMS( authtoken, circ => circ_id );
1738         open-ils.circ.renew(login_session, circ_object);
1739         Renews the provided circulation.  login_session is the requestor of the
1740         renewal and if the logged in user is not the same as circ->usr, then
1741         the logged in user must have RENEW_CIRC permissions.
1742         NOTES
1743
1744 sub renew {
1745         my( $self, $client, $authtoken, $params ) = @_;
1746         $U->logmark;
1747
1748         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1749         $__isrenewal = 1;
1750
1751         # fetch the patron object one way or another
1752         if( $params->{patron} ) {
1753                 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1754                 if($evt) { $__isrenewal = 0; return $evt; }
1755
1756         } elsif( $params->{patron_barcode} ) {
1757                 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1758                 if($evt) { $__isrenewal = 0; return $evt; }
1759
1760         } else {
1761                 ($patron, $copy, $evt) = _find_patron_from_params($params);
1762                 return $evt if $evt;
1763                 $params->{copy} = $copy;
1764         }
1765
1766         # verify our login session
1767         ($requestor, $evt) = $U->checkses($authtoken);
1768         if($evt) { $__isrenewal = 0; return $evt; }
1769
1770         # make sure we have permission to perform a renewal
1771         if( $requestor->id ne $patron->id ) {
1772                 $evt = $U->check_perms($requestor->id, $patron->ws_ou, 'RENEW_CIRC');
1773                 if($evt) { $__isrenewal = 0; return $evt; }
1774         }
1775
1776
1777         # fetch and build the circulation environment
1778         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1779                 patron                                                  => $patron, 
1780                 requestor                                               => $requestor, 
1781                 patron                                                  => $patron, 
1782                 type                                                            => 'circ',
1783                 #fetch_patron_circ_summary      => 1,
1784                 fetch_copy_statuses                     => 1, 
1785                 fetch_copy_locations                    => 1, 
1786                 );
1787         if($evt) { $__isrenewal = 0; return $evt; }
1788         $params->{_ctx} = $ctx;
1789
1790         # make sure they have some renewals left and make sure the circulation exists
1791         ($circ, $evt) = _check_renewal_remaining($ctx);
1792         if($evt) { $__isrenewal = 0; return $evt; }
1793         $ctx->{old_circ} = $circ;
1794         my $renewals = $circ->renewal_remaining - 1;
1795
1796         # run the renew permit script
1797         $evt = _run_renew_scripts($ctx);
1798         if($evt) { $__isrenewal = 0; return $evt; }
1799
1800         # checkin the cop
1801         #$ctx->{patron} = $ctx->{patron}->id;
1802         $evt = $self->generic_receive($client, $authtoken, $ctx );
1803                 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1804
1805         if( !$U->event_equals($evt, 'SUCCESS') ) {
1806                 $__isrenewal = 0; return $evt; 
1807         }
1808
1809         # re-fetch the context since objects have changed in the checkin
1810         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1811                 patron                                                  => $patron, 
1812                 requestor                                               => $requestor, 
1813                 patron                                                  => $patron, 
1814                 type                                                            => 'circ',
1815                 #fetch_patron_circ_summary      => 1,
1816                 fetch_copy_statuses                     => 1, 
1817                 fetch_copy_locations                    => 1, 
1818                 );
1819         if($evt) { $__isrenewal = 0; return $evt; }
1820         $params->{_ctx} = $ctx;
1821         $ctx->{renewal_remaining} = $renewals;
1822
1823         # run the circ permit scripts
1824         if( $ctx->{permit_override} ) {
1825                 $evt = $U->check_perms(
1826                         $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1827                 if($evt) { $__isrenewal = 0; return $evt; }
1828
1829         } else {
1830                 $evt = $self->permit_circ( $client, $authtoken, $params );
1831                 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1832                         #$ctx->{precat} = 1;
1833                         $params->{precat} = 1;
1834
1835                 } else {
1836                         if(!$U->event_equals($evt, 'SUCCESS')) {
1837                                 if($evt) { $__isrenewal = 0; return $evt; }
1838                         }
1839                 }
1840                 $params->{permit_key} = $evt->{payload};
1841         }
1842
1843
1844         # checkout the item again
1845         $params->{patron} = $ctx->{patron}->id;
1846         $evt = $self->checkout($client, $authtoken, $params );
1847
1848         $logger->activity("user ".$requestor->id." renewl of item ".
1849                 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1850
1851         $__isrenewal = 0;
1852         return $evt;
1853 }
1854
1855 sub _check_renewal_remaining {
1856         my $ctx = shift;
1857         $U->logmark;
1858         my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1859         return (undef, $evt) if $evt;
1860         $evt = OpenILS::Event->new(
1861                 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1862         return ($circ, $evt);
1863 }
1864
1865 sub _run_renew_scripts {
1866         my $ctx = shift;
1867         my $runner = $ctx->{runner};
1868         $U->logmark;
1869
1870         $runner->load($scripts{circ_permit_renew});
1871         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1872
1873         my $events = $runner->retrieve('result.events');
1874         $events = [ split(/,/, $events) ]; 
1875         $logger->activity("circ_permit_renew for user ".
1876                 $ctx->{patron}->id." returned events: @$events") if @$events;
1877
1878         my @allevents;
1879         push( @allevents, OpenILS::Event->new($_)) for @$events;
1880         return \@allevents if  @allevents;
1881
1882         return undef;
1883 }
1884
1885         
1886
1887
1888 1;
1889