]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
added handling for claims returned items and copy alert messages, updated some home_o...
[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 my $__islost            = 0;
41
42 # ------------------------------------------------------------------------------
43 # Load the circ script from the config
44 # ------------------------------------------------------------------------------
45 sub initialize {
46
47         my $self = shift;
48         $cache_handle = OpenSRF::Utils::Cache->new('global');
49         my $conf = OpenSRF::Utils::SettingsClient->new;
50         my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
51         my @pfx = ( @pfx2, "scripts" );
52
53         my $p           = $conf->config_value(  @pfx, 'circ_permit_patron' );
54         my $c           = $conf->config_value(  @pfx, 'circ_permit_copy' );
55         my $d           = $conf->config_value(  @pfx, 'circ_duration' );
56         my $f           = $conf->config_value(  @pfx, 'circ_recurring_fines' );
57         my $m           = $conf->config_value(  @pfx, 'circ_max_fines' );
58         my $pr  = $conf->config_value(  @pfx, 'circ_permit_renew' );
59         my $lb  = $conf->config_value(  @pfx2, 'script_path' );
60
61         $logger->error( "Missing circ script(s)" ) 
62                 unless( $p and $c and $d and $f and $m and $pr );
63
64         $scripts{circ_permit_patron}    = $p;
65         $scripts{circ_permit_copy}              = $c;
66         $scripts{circ_duration}                 = $d;
67         $scripts{circ_recurring_fines}= $f;
68         $scripts{circ_max_fines}                = $m;
69         $scripts{circ_permit_renew}     = $pr;
70
71         $lb = [ $lb ] unless ref($lb);
72         $script_libs = $lb;
73
74         $logger->debug("Loaded rules scripts for circ: " .
75                 "circ permit patron: $p, circ permit copy: $c, ".
76                 "circ duration :$d , circ recurring fines : $f, " .
77                 "circ max fines : $m, circ renew permit : $pr");
78 }
79
80
81 # ------------------------------------------------------------------------------
82 # Loads the necessary circ objects and pushes them into the script environment
83 # Returns ( $data, $evt ).  if $evt is defined, then an
84 # unexpedted event occurred and should be dealt with / returned to the caller
85 # ------------------------------------------------------------------------------
86 sub create_circ_ctx {
87         my %params = @_;
88         $U->logmark;
89
90         my $evt;
91         my $ctx = \%params;
92
93         $evt = _ctx_add_patron_objects($ctx, %params);
94         return (undef,$evt) if $evt;
95
96         if(!$params{noncat}) {
97                 if( $evt = _ctx_add_copy_objects($ctx, %params) ) {
98                         $ctx->{precat} = 1 if($evt->{textcode} eq 'COPY_NOT_FOUND')
99                 } else {
100                         $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
101                 }
102         }
103
104         _doctor_patron_object($ctx) if $ctx->{patron};
105         _doctor_copy_object($ctx) if $ctx->{copy};
106
107         if(!$ctx->{no_runner}) {
108                 _build_circ_script_runner($ctx);
109                 _add_script_runner_methods($ctx);
110         }
111
112         return $ctx;
113 }
114
115 sub _ctx_add_patron_objects {
116         my( $ctx, %params) = @_;
117         $U->logmark;
118
119         # - patron standings are now handled in the penalty server...
120
121         #if(!defined($cache{patron_standings})) {
122         #       $cache{patron_standings} = $U->fetch_patron_standings();
123         #}
124         #$ctx->{patron_standings} = $cache{patron_standings};
125
126         $cache{group_tree} = $U->fetch_permission_group_tree() unless $cache{group_tree};
127         $ctx->{group_tree} = $cache{group_tree};
128
129         $ctx->{patron_circ_summary} = 
130                 $U->fetch_patron_circ_summary($ctx->{patron}->id) 
131                 if $params{fetch_patron_circsummary};
132
133         return undef;
134 }
135
136
137 sub _find_copy_by_attr {
138         my %params = @_;
139         $U->logmark;
140         my $evt;
141
142         my $copy = $params{copy} || undef;
143
144         if(!$copy) {
145
146                 ( $copy, $evt ) = 
147                         $U->fetch_copy($params{copyid}) if $params{copyid};
148                 return (undef,$evt) if $evt;
149
150                 if(!$copy) {
151                         ( $copy, $evt ) = 
152                                 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
153                         return (undef,$evt) if $evt;
154                 }
155         }
156         return ( $copy, $evt );
157 }
158
159 sub _ctx_add_copy_objects {
160         my($ctx, %params)  = @_;
161         $U->logmark;
162         my $evt;
163         my $copy;
164
165         $cache{copy_statuses} = $U->fetch_copy_statuses 
166                 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
167
168         $cache{copy_locations} = $U->fetch_copy_locations 
169                 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
170
171         $ctx->{copy_statuses} = $cache{copy_statuses};
172         $ctx->{copy_locations} = $cache{copy_locations};
173
174         ($copy, $evt) = _find_copy_by_attr(%params);
175         return $evt if $evt;
176
177         if( $copy and !$ctx->{title} ) {
178                 $logger->debug("Copy status: " . $copy->status);
179
180                 my $r = $RECORD_FROM_COPY_CACHE{$copy->id};
181                 ($r, $evt) = $U->fetch_record_by_copy( $copy->id ) unless $r;
182                 return $evt if $evt;
183                 $RECORD_FROM_COPY_CACHE{$copy->id} = $r;
184
185                 $ctx->{title} = $r;
186                 $ctx->{copy} = $copy;
187         }
188
189         return undef;
190 }
191
192
193 # ------------------------------------------------------------------------------
194 # Fleshes parts of the patron object
195 # ------------------------------------------------------------------------------
196 sub _doctor_copy_object {
197         my $ctx = shift;
198         $U->logmark;
199         my $copy = $ctx->{copy} || return undef;
200
201         $logger->debug("Doctoring copy object...");
202
203         # set the copy status to a status name
204         $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
205
206         # set the copy location to the location object
207         $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
208
209         $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
210 }
211
212
213 # ------------------------------------------------------------------------------
214 # Fleshes parts of the patron object
215 # ------------------------------------------------------------------------------
216 sub _doctor_patron_object {
217         my $ctx = shift;
218         $U->logmark;
219         my $patron = $ctx->{patron} || return undef;
220
221         # push the standing object into the patron
222 #       if(ref($ctx->{patron_standings})) {
223 #               for my $s (@{$ctx->{patron_standings}}) {
224 #                       if( $s->id eq $ctx->{patron}->standing ) {
225 #                               $patron->standing($s);
226 #                               $logger->debug("Set patron standing to ". $s->value);
227 #                       }
228 #               }
229 #       }
230
231         # set the patron ptofile to the profile name
232         $patron->profile( _get_patron_profile( 
233                 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
234
235         # flesh the org unit
236         $patron->home_ou( 
237                 $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
238
239 }
240
241 # recurse and find the patron profile name from the tree
242 # another option would be to grab the groups for the patron
243 # and cycle through those until the "profile" group has been found
244 sub _get_patron_profile { 
245         my( $patron, $group_tree ) = @_;
246         return $group_tree if ($group_tree->id eq $patron->profile);
247         return undef unless ($group_tree->children);
248
249         for my $child (@{$group_tree->children}) {
250                 my $ret = _get_patron_profile( $patron, $child );
251                 return $ret if $ret;
252         }
253         return undef;
254 }
255
256 sub _get_copy_status {
257         my( $copy, $cstatus ) = @_;
258         $U->logmark;
259         my $s = undef;
260         for my $status (@$cstatus) {
261                 $s = $status if( $status->id eq $copy->status ) 
262         }
263         $logger->debug("Retrieving copy status: " . $s->name) if $s;
264         return $s;
265 }
266
267 sub _get_copy_location {
268         my( $copy, $locations ) = @_;
269         $U->logmark;
270         my $l = undef;
271         for my $loc (@$locations) {
272                 $l = $loc if $loc->id eq $copy->location;
273         }
274         $logger->debug("Retrieving copy location: " . $l->name ) if $l;
275         return $l;
276 }
277
278
279 # ------------------------------------------------------------------------------
280 # Constructs and shoves data into the script environment
281 # ------------------------------------------------------------------------------
282 sub _build_circ_script_runner {
283         my $ctx = shift;
284         $U->logmark;
285
286         $logger->debug("Loading script environment for circulation");
287
288         my $runner;
289         if( $runner = $contexts{$ctx->{type}} ) {
290                 $runner->refresh_context;
291         } else {
292                 $runner = OpenILS::Utils::ScriptRunner->new;
293                 $contexts{type} = $runner;
294         }
295
296         for(@$script_libs) {
297                 $logger->debug("Loading circ script lib path $_");
298                 $runner->add_path( $_ );
299         }
300
301         # Note: inserting the number 0 into the script turns into the
302         # string "0", and thus evaluates to true in JS land
303         # inserting undef will insert "", which evaluates to false
304
305         $runner->insert( 'environment.patron',  $ctx->{patron}, 1);
306         $runner->insert( 'environment.title',   $ctx->{title}, 1);
307         $runner->insert( 'environment.copy',    $ctx->{copy}, 1);
308
309         # circ script result
310         $runner->insert( 'result', {} );
311         #$runner->insert( 'result.event', 'SUCCESS' );
312         $runner->insert( 'result.events', [] );
313
314         if($__isrenewal) {
315                 $runner->insert('environment.isRenewal', 1);
316         } else {
317                 $runner->insert('environment.isRenewal', undef);
318         }
319
320         if($ctx->{ishold} ) { 
321                 $runner->insert('environment.isHold', 1); 
322         } else{ 
323                 $runner->insert('environment.isHold', undef) 
324         }
325
326         if( $ctx->{noncat} ) {
327                 $runner->insert('environment.isNonCat', 1);
328                 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
329         } else {
330                 $runner->insert('environment.isNonCat', undef);
331         }
332
333 #       if(ref($ctx->{patron_circ_summary})) {
334 #               $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
335 #               $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
336 #       }
337
338         $ctx->{runner} = $runner;
339         return $runner;
340 }
341
342
343 sub _add_script_runner_methods {
344         my $ctx = shift;
345         $U->logmark;
346         my $runner = $ctx->{runner};
347
348         if( $ctx->{copy} ) {
349                 
350                 # allows a script to fetch a hold that is currently targeting the
351                 # copy in question
352                 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
353                                 my $key = shift;
354                                 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
355                                 $hold = undef unless $hold;
356                                 $runner->insert( $key, $hold, 1 );
357                         }
358                 );
359         }
360 }
361
362 # ------------------------------------------------------------------------------
363
364 __PACKAGE__->register_method(
365         method  => "permit_circ",
366         api_name        => "open-ils.circ.checkout.permit",
367         notes           => q/
368                 Determines if the given checkout can occur
369                 @param authtoken The login session key
370                 @param params A trailing hash of named params including 
371                         barcode : The copy barcode, 
372                         patron : The patron the checkout is occurring for, 
373                         renew : true or false - whether or not this is a renewal
374                 @return The event that occurred during the permit check.  
375         /);
376
377 __PACKAGE__->register_method (
378         method          => 'permit_circ',
379         api_name                => 'open-ils.circ.checkout.permit.override',
380         signature       => q/@see open-ils.circ.checkout.permit/,
381 );
382
383 sub permit_circ {
384         my( $self, $client, $authtoken, $params ) = @_;
385         $U->logmark;
386
387         my $override = $params->{override} = 1 if $self->api_name =~ /override/o;
388
389         my ( $requestor, $patron, $ctx, $evt, $circ );
390
391         # check permisson of the requestor
392         ( $requestor, $patron, $evt ) = 
393                 $U->checkses_requestor( 
394                 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
395         return $evt if $evt;
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         $ctx->{authtoken} = $authtoken;
412
413         $evt = undef;
414         if( $ctx->{copy} and ($evt = _handle_claims_returned($ctx)) ) {
415                 return $evt unless $U->event_equals($evt, 'SUCCESS');
416         }
417
418         if($evt) { 
419                 $evt = undef;
420
421         } else { 
422
423                 # no claims returned circ was found, check if there is any open circ
424                 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
425                         ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
426                         return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
427                 }
428         }
429
430
431         $ctx->{permit_key} = _cache_permit_key();
432         my $events = _run_permit_scripts($ctx);
433
434         if( $override ) {
435                 $evt = override_events($requestor, $requestor->ws_ou, $events);
436                 return $evt if $evt;
437                 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
438         }
439
440         return $events;
441 }
442
443 sub override_events {
444
445         my( $requestor, $org, $events ) = @_;
446         $events = [ $events ] unless ref($events) eq 'ARRAY';
447         my @failed;
448
449         for my $e (@$events) {
450                 my $tc = $e->{textcode};
451                 next if $tc eq 'SUCCESS';
452                 my $ov = "$tc.override";
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 $ischecked   = 0;
1131
1132         my @eventlist;
1133
1134         my $ae = _check_copy_alert($copy);
1135         push(@eventlist, $ae) if $ae;
1136
1137         # if the copy is an a status we can't automatically
1138         # resolve, handle the appropriate event
1139         push( @eventlist, $evt ) if ($evt = _checkin_check_copy_status($copy));
1140
1141         if( $evt = _handle_claims_returned($ctx) ) {
1142                 if( $U->event_equals($evt, 'SUCCESS') ) {
1143                         $ischecked = 1;
1144                         $change = 1;
1145                 } else {
1146                         push( @eventlist, $evt );
1147                 }
1148         }
1149
1150         $logger->debug("checkin collected ".scalar(@eventlist) ." overridable events");
1151
1152         if( @eventlist ) {
1153
1154                 my @retevents;
1155
1156                 if( $ctx->{override} ) {
1157                         for( @eventlist ) {
1158                                 my $evtname = $_->{textcode};
1159                                 $logger->info("checkin attempting to override event $evtname");
1160                                 push( @retevents, $evt ) if ($evt = $U->check_perms( 
1161                                         $requestor->id, $copy->circ_lib, "$evtname.override"));
1162                         }
1163                         return \@retevents if @retevents;
1164
1165                 } else {
1166                         $logger->info("checkin returning non-success events");
1167                         return \@eventlist;
1168                 }
1169         }
1170
1171         ($ctx->{circ}, $evt)    = $U->fetch_open_circulation($copy->id);
1172         return $evt if ($evt and $__isrenewal); # renewals require a circulation
1173
1174         $evt = undef;
1175         ($ctx->{transit})       = $U->fetch_open_transit_by_copy($copy->id);
1176
1177
1178         if( !$ischecked && $ctx->{circ} ) {
1179
1180                 # There is an open circ on this item, close it out.
1181                 $change = 1;
1182                 $evt            = _checkin_handle_circ($ctx);
1183                 return $evt if $evt;
1184
1185         } elsif( !$ischecked && $ctx->{transit} ) {
1186
1187                 # is this item currently in transit?
1188                 $change                 = 1;
1189                 $evt                            = $transcode->transit_receive( $copy, $requestor, $session );
1190                 my $holdtrans   = $evt->{holdtransit};
1191                 ($ctx->{hold})  = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1192
1193                 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1194
1195                         # either an error occurred or a ROUTE_ITEM was generated and the 
1196                         # item must be forwarded on to its destination.
1197                         return _checkin_flesh_event($ctx, $evt);
1198
1199                 } else {
1200
1201                         if($holdtrans) {
1202
1203                                 # copy was received as a hold transit.  Copy is at target lib
1204                                 # and hold transit is complete.  We're done here...
1205                                 $U->commit_db_session($session);
1206                                 return _checkin_flesh_event($ctx, $evt);
1207                         }
1208                         $evt = undef;
1209                 }
1210         }
1211
1212         # ------------------------------------------------------------------------------
1213         # Circulations and transits are now closed where necessary.  Now go on to see if
1214         # this copy can fulfill a hold or needs to be routed to a different location
1215         # ------------------------------------------------------------------------------
1216
1217
1218         # If it's a renewal, we're done
1219         if($__isrenewal) {
1220                 $U->commit_db_session($session);
1221                 return OpenILS::Event->new('SUCCESS');
1222         }
1223
1224
1225         # Now, let's see if this copy is needed for a hold
1226         my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor ); 
1227
1228         if($hold) {
1229
1230                 $ctx->{hold}    = $hold;
1231                 $change                 = 1;
1232                 
1233                 # Capture the hold with this copy
1234                 return $evt if ($evt = _checkin_capture_hold($ctx));
1235
1236                 if( $hold->pickup_lib == $requestor->ws_ou ) {
1237
1238                         # This hold was captured in the correct location
1239                         $evt = OpenILS::Event->new('SUCCESS');
1240
1241                 } else {
1242
1243                         # Hold needs to be picked up elsewhere.  Build a hold 
1244                         # transit and route the item.
1245                         return $evt if ($evt =_checkin_build_hold_transit($ctx));
1246                         $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1247                 }
1248
1249         } else { # not needed for a hold
1250
1251                 if( $copy->circ_lib == $requestor->ws_ou ) {
1252
1253                         # Copy is in the right place.
1254                         $evt = OpenILS::Event->new('SUCCESS');
1255
1256                         # if the item happens to be a pre-cataloged item, send it
1257                         # to cataloging and return the event
1258                         my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1259                         return $err if $err;
1260                         $change         = 1 if $c;
1261                         $evt                    = $e if $e;
1262
1263                 } else {
1264
1265                         # Copy wants to go home. Transit it there.
1266                         return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1267                         $evt                    = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1268                         $change         = 1;
1269                 }
1270         }
1271
1272         if(!$change) {
1273
1274                 $evt = OpenILS::Event->new('NO_CHANGE');
1275                 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id) 
1276                         if ($copy->status == $U->copy_status_from_name('on holds shelf')->id);
1277
1278         } else {
1279
1280                 $U->commit_db_session($session);
1281         }
1282
1283         $logger->activity("checkin by user ".$requestor->id." on item ".
1284                 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1285
1286         return _checkin_flesh_event($ctx, $evt);
1287 }
1288
1289 # returns undef if there are no 'open' claims-returned circs attached
1290 # to the given copy.  if there is an open claims-returned circ, 
1291 # then we check for override mode.  if in override, mark the claims-returned
1292 # circ as checked in.  if not, return event.
1293 sub _handle_claims_returned {
1294         my $ctx = shift;
1295         my $copy = $ctx->{copy};
1296
1297         my $CR  = _fetch_open_claims_returned($copy->id);
1298         return undef unless $CR;
1299
1300         # - If the caller has set the override flag, we will check the item in
1301         if($ctx->{override}) {
1302
1303                 $CR->checkin_time('now');       
1304                 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1305                 $CR->checkin_staff($ctx->{requestor}->id);
1306
1307                 my $stat = $U->storagereq(
1308                         'open-ils.storage.direct.action.circulation.update', $CR);
1309                 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1310                 return OpenILS::Event->new('SUCCESS');
1311
1312         } else {
1313                 # - if not in override mode, return the CR event
1314                 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1315         }
1316 }
1317
1318
1319 sub _fetch_open_claims_returned {
1320         my $copyid = shift;
1321         my $trans = $U->storagereq(
1322                 'open-ils.storage.direct.action.circulation.search_where',
1323                 {       
1324                         target_copy             => $copyid, 
1325                         stop_fines              => 'CLAIMSRETURNED',
1326                         checkin_time    => undef,
1327                 }
1328         );
1329         return $$trans[0] if $trans && $$trans[0];
1330         return undef;
1331 }
1332
1333
1334 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1335 sub _checkin_handle_precat {
1336
1337         my $ctx         = shift;
1338         my $copy                = $ctx->{copy};
1339         my $evt         = undef;
1340         my $errevt      = undef;
1341         my $change      = 0;
1342
1343         my $catstat = $U->copy_status_from_name('cataloging');
1344
1345         if( $ctx->{precat} ) {
1346
1347                 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1348
1349                 if( $copy->status != $catstat->id ) {
1350                         $copy->status($catstat->id);
1351
1352                         return (undef, 0, $errevt) if (
1353                                 $errevt = $U->update_copy(
1354                                         copy            => $copy, 
1355                                         editor  => $ctx->{requestor}->id, 
1356                                         session => $ctx->{session} ));
1357                         $change = 1;
1358
1359                 }
1360         }
1361
1362         return ($evt, $change, undef);
1363 }
1364
1365
1366 sub _checkin_check_copy_status {
1367         my $copy = shift;
1368         my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1369         my $evt = OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1370         $logger->info("checking copy status id is ".$stat);
1371         return $evt if ($stat == $U->copy_status_from_name('lost')->id);
1372         return $evt if ($stat == $U->copy_status_from_name('missing')->id);
1373         return undef;
1374 }
1375
1376 # Just gets the copy back home.  Returns undef on success, event on error
1377 sub _checkin_build_generic_copy_transit {
1378
1379         my $ctx                 = shift;
1380         my $requestor   = $ctx->{requestor};
1381         my $copy                        = $ctx->{copy};
1382         my $transit             = Fieldmapper::action::transit_copy->new;
1383         my $session             = $ctx->{session};
1384
1385         $logger->activity("User ". $requestor->id ." creating a ".
1386                 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1387
1388         $transit->source($requestor->ws_ou);
1389         $transit->dest($copy->circ_lib);
1390         $transit->target_copy($copy->id);
1391         $transit->source_send_time('now');
1392         $transit->copy_status($copy->status);
1393         
1394         $logger->debug("Creating new copy_transit in DB");
1395
1396         my $s = $session->request(
1397                 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1398         return $U->DB_UPDATE_FAILED($transit) unless $s;
1399
1400         $logger->info("Checkin copy successfully created new transit: $s");
1401
1402         $copy->status($U->copy_status_from_name('in transit')->id );
1403
1404         return $U->update_copy( copy => $copy, 
1405                         editor => $requestor->id, session => $session );
1406         
1407 }
1408
1409
1410 # returns event on error, undef on success
1411 sub _checkin_build_hold_transit {
1412         my $ctx = shift;
1413
1414         my $copy = $ctx->{copy};
1415         my $hold = $ctx->{hold};
1416         my $trans = Fieldmapper::action::hold_transit_copy->new;
1417
1418         $trans->hold($hold->id);
1419         $trans->source($ctx->{requestor}->ws_ou);
1420         $trans->dest($hold->pickup_lib);
1421         $trans->source_send_time("now");
1422         $trans->target_copy($copy->id);
1423         $trans->copy_status($copy->status);
1424
1425         my $id = $ctx->{session}->request(
1426                 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1427         return $U->DB_UPDATE_FAILED($trans) unless $id;
1428
1429         $logger->info("Checkin copy successfully created hold transit: $id");
1430
1431         $copy->status($U->copy_status_from_name('in transit')->id );
1432         return $U->update_copy( copy => $copy, 
1433                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1434 }
1435
1436 # Returns event on error, undef on success
1437 sub _checkin_capture_hold {
1438         my $ctx = shift;
1439         my $copy = $ctx->{copy};
1440         my $hold = $ctx->{hold}; 
1441
1442         $logger->debug("Checkin copy capturing hold ".$hold->id);
1443
1444         $hold->current_copy($copy->id);
1445         $hold->capture_time('now'); 
1446
1447         my $stat = $ctx->{session}->request(
1448                 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1449         return $U->DB_UPDATE_FAILED($hold) unless $stat;
1450
1451         $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1452
1453         return $U->update_copy( copy => $copy, 
1454                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1455 }
1456
1457 # fleshes an event with the relevant objects from the context
1458 sub _checkin_flesh_event {
1459         my $ctx = shift;
1460         my $evt = shift;
1461
1462         my $payload                             = {};
1463         $payload->{copy}                = $U->unflesh_copy($ctx->{copy});
1464         $payload->{record}      = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1465         $payload->{circ}                = $ctx->{circ} if $ctx->{circ};
1466         $payload->{transit}     = $ctx->{transit} if $ctx->{transit};
1467         $payload->{hold}                = $ctx->{hold} if $ctx->{hold};
1468
1469         $evt->{payload} = $payload;
1470         return $evt;
1471 }
1472
1473
1474 # Closes out the circulation, puts the copy into reshelving.
1475 # Voids any bills attached to this circ after the backdate time 
1476 # if a backdate is provided
1477 sub _checkin_handle_circ { 
1478
1479         my $ctx = shift;
1480
1481         my $circ = $ctx->{circ};
1482         my $copy = $ctx->{copy};
1483         my $requestor = $ctx->{requestor};
1484         my $session = $ctx->{session};
1485
1486         $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1487
1488         $ctx->{longoverdue}             = 1 if ($circ->stop_fines =~ /longoverdue/io);
1489         $ctx->{claimsreturned}  = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1490
1491         my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1492         return $evt if $evt;
1493
1494         $circ->stop_fines('CHECKIN');
1495         $circ->stop_fines('RENEW') if $__isrenewal;
1496         $circ->stop_fines('LOST') if($__islost);
1497         $circ->xact_finish('now') if($obt->balance_owed <= 0 and !$__islost);
1498         $circ->stop_fines_time('now');
1499         $circ->checkin_time('now');
1500         $circ->checkin_staff($requestor->id);
1501
1502         if(my $backdate = $ctx->{backdate}) {
1503                 return $evt if ($evt = 
1504                         _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1505         }
1506
1507         $logger->info("Checkin copy setting status to 'reshelving' and committing...");
1508         $copy->status($U->copy_status_from_name('reshelving')->id);
1509         $evt = $U->update_copy( session => $session, 
1510                 copy => $copy, editor => $requestor->id );
1511         return $evt if $evt;
1512
1513         $ctx->{session}->request(
1514                 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1515
1516         return undef;
1517 }
1518
1519 # returns event on error, undef on success
1520 # This voids all bills attached to the given circulation that occurred
1521 # after the backdate 
1522 sub _checkin_handle_backdate {
1523         my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1524
1525         $logger->activity("User ".$requestor->id.
1526                 " backdating circ [".$circ->target_copy."] to date: $backdate");
1527
1528         my $bills = $session->request( # XXX Verify this call is correct
1529                 "open-ils.storage.direct.money.billing.search_where.atomic",
1530                 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1531
1532         if($bills) {
1533                 for my $bill (@$bills) {
1534                         $bill->voided('t');
1535                         my $s = $session->request(
1536                                 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1537                         return $U->DB_UPDATE_FAILED($bill) unless $s;
1538                 }
1539         }
1540
1541
1542         # if the caller elects to attempt to close the circulation
1543         # transaction, then it will be closed if there are not further
1544         # charges on the transaction
1545         if( $closecirc ) {
1546                 my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1547            return $evt if $evt;
1548                 $circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1549         }
1550
1551         return undef;
1552 }
1553
1554
1555 sub _find_patron_from_params {
1556         my $params = shift;
1557
1558         my $patron;
1559         my $copy;
1560         my $circ;
1561         my $evt;
1562
1563         if(my $barcode = $params->{barcode}) {
1564                 $logger->debug("circ finding user from params with barcode $barcode");
1565                 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1566                 return (undef, undef, $evt) if $evt;
1567                 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1568                 return (undef, undef, $evt) if $evt;
1569                 ($patron, $evt) = $U->fetch_user($circ->usr);
1570                 return (undef, undef, $evt) if $evt;
1571         }
1572         return ($patron, $copy);
1573 }
1574
1575
1576 # ------------------------------------------------------------------------------
1577
1578 __PACKAGE__->register_method(
1579         method  => "renew",
1580         api_name        => "open-ils.circ.renew",
1581         notes           => <<"  NOTES");
1582         PARAMS( authtoken, circ => circ_id );
1583         open-ils.circ.renew(login_session, circ_object);
1584         Renews the provided circulation.  login_session is the requestor of the
1585         renewal and if the logged in user is not the same as circ->usr, then
1586         the logged in user must have RENEW_CIRC permissions.
1587         NOTES
1588
1589 sub renew {
1590         my( $self, $client, $authtoken, $params ) = @_;
1591         $U->logmark;
1592
1593         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1594         $__isrenewal = 1;
1595
1596         # fetch the patron object one way or another
1597         if( $params->{patron} ) {
1598                 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1599                 if($evt) { $__isrenewal = 0; return $evt; }
1600
1601         } elsif( $params->{patron_barcode} ) {
1602                 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1603                 if($evt) { $__isrenewal = 0; return $evt; }
1604
1605         } else {
1606                 ($patron, $copy, $evt) = _find_patron_from_params($params);
1607                 return $evt if $evt;
1608                 $params->{copy} = $copy;
1609         }
1610
1611         # verify our login session
1612         ($requestor, $evt) = $U->checkses($authtoken);
1613         if($evt) { $__isrenewal = 0; return $evt; }
1614
1615         # make sure we have permission to perform a renewal
1616         if( $requestor->id ne $patron->id ) {
1617                 $evt = $U->check_perms($requestor->id, $patron->ws_ou, 'RENEW_CIRC');
1618                 if($evt) { $__isrenewal = 0; return $evt; }
1619         }
1620
1621
1622         # fetch and build the circulation environment
1623         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1624                 patron                                                  => $patron, 
1625                 requestor                                               => $requestor, 
1626                 patron                                                  => $patron, 
1627                 type                                                            => 'circ',
1628                 #fetch_patron_circ_summary      => 1,
1629                 fetch_copy_statuses                     => 1, 
1630                 fetch_copy_locations                    => 1, 
1631                 );
1632         if($evt) { $__isrenewal = 0; return $evt; }
1633         $params->{_ctx} = $ctx;
1634
1635         # make sure they have some renewals left and make sure the circulation exists
1636         ($circ, $evt) = _check_renewal_remaining($ctx);
1637         if($evt) { $__isrenewal = 0; return $evt; }
1638         $ctx->{old_circ} = $circ;
1639         my $renewals = $circ->renewal_remaining - 1;
1640
1641         # run the renew permit script
1642         $evt = _run_renew_scripts($ctx);
1643         if($evt) { $__isrenewal = 0; return $evt; }
1644
1645         # checkin the cop
1646         #$ctx->{patron} = $ctx->{patron}->id;
1647         $evt = $self->generic_receive($client, $authtoken, $ctx );
1648                 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1649
1650         if( !$U->event_equals($evt, 'SUCCESS') ) {
1651                 $__isrenewal = 0; return $evt; 
1652         }
1653
1654         # re-fetch the context since objects have changed in the checkin
1655         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1656                 patron                                                  => $patron, 
1657                 requestor                                               => $requestor, 
1658                 patron                                                  => $patron, 
1659                 type                                                            => 'circ',
1660                 #fetch_patron_circ_summary      => 1,
1661                 fetch_copy_statuses                     => 1, 
1662                 fetch_copy_locations                    => 1, 
1663                 );
1664         if($evt) { $__isrenewal = 0; return $evt; }
1665         $params->{_ctx} = $ctx;
1666         $ctx->{renewal_remaining} = $renewals;
1667
1668         # run the circ permit scripts
1669         if( $ctx->{permit_override} ) {
1670                 $evt = $U->check_perms(
1671                         $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1672                 if($evt) { $__isrenewal = 0; return $evt; }
1673
1674         } else {
1675                 $evt = $self->permit_circ( $client, $authtoken, $params );
1676                 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1677                         #$ctx->{precat} = 1;
1678                         $params->{precat} = 1;
1679
1680                 } else {
1681                         if(!$U->event_equals($evt, 'SUCCESS')) {
1682                                 if($evt) { $__isrenewal = 0; return $evt; }
1683                         }
1684                 }
1685                 $params->{permit_key} = $evt->{payload};
1686         }
1687
1688
1689         # checkout the item again
1690         $params->{patron} = $ctx->{patron}->id;
1691         $evt = $self->checkout($client, $authtoken, $params );
1692
1693         $logger->activity("user ".$requestor->id." renewl of item ".
1694                 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1695
1696         $__isrenewal = 0;
1697         return $evt;
1698 }
1699
1700 sub _check_renewal_remaining {
1701         my $ctx = shift;
1702         $U->logmark;
1703         my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1704         return (undef, $evt) if $evt;
1705         $evt = OpenILS::Event->new(
1706                 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1707         return ($circ, $evt);
1708 }
1709
1710 sub _run_renew_scripts {
1711         my $ctx = shift;
1712         my $runner = $ctx->{runner};
1713         $U->logmark;
1714
1715         $runner->load($scripts{circ_permit_renew});
1716         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1717
1718         my $events = $runner->retrieve('result.events');
1719         $events = [ split(/,/, $events) ]; 
1720         $logger->activity("circ_permit_renew for user ".
1721                 $ctx->{patron}->id." returned events: @$events") if @$events;
1722
1723         my @allevents;
1724         push( @allevents, OpenILS::Event->new($_)) for @$events;
1725         return \@allevents if  @allevents;
1726
1727         return undef;
1728 }
1729
1730         
1731
1732
1733 1;
1734