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