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