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