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