]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
stripping success events if other events exist in the return set
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Circ / Circulate.pm
1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenSRF::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::Utils::SettingsClient;
6 use OpenSRF::Utils::Logger qw(:logger);
7 use OpenILS::Const qw/:const/;
8
9 my %scripts;
10 my $script_libs;
11
12 sub initialize {
13
14         my $self = shift;
15         my $conf = OpenSRF::Utils::SettingsClient->new;
16         my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
17         my @pfx = ( @pfx2, "scripts" );
18
19         my $p           = $conf->config_value(  @pfx, 'circ_permit_patron' );
20         my $c           = $conf->config_value(  @pfx, 'circ_permit_copy' );
21         my $d           = $conf->config_value(  @pfx, 'circ_duration' );
22         my $f           = $conf->config_value(  @pfx, 'circ_recurring_fines' );
23         my $m           = $conf->config_value(  @pfx, 'circ_max_fines' );
24         my $pr  = $conf->config_value(  @pfx, 'circ_permit_renew' );
25         my $lb  = $conf->config_value(  @pfx2, 'script_path' );
26
27         $logger->error( "Missing circ script(s)" ) 
28                 unless( $p and $c and $d and $f and $m and $pr );
29
30         $scripts{circ_permit_patron}    = $p;
31         $scripts{circ_permit_copy}              = $c;
32         $scripts{circ_duration}                 = $d;
33         $scripts{circ_recurring_fines}= $f;
34         $scripts{circ_max_fines}                = $m;
35         $scripts{circ_permit_renew}     = $pr;
36
37         $lb = [ $lb ] unless ref($lb);
38         $script_libs = $lb;
39
40         $logger->debug(
41                 "circulator: Loaded rules scripts for circ: " .
42                 "circ permit patron = $p, ".
43                 "circ permit copy = $c, ".
44                 "circ duration = $d, ".
45                 "circ recurring fines = $f, " .
46                 "circ max fines = $m, ".
47                 "circ renew permit = $pr.  ".
48                 "lib paths = @$lb");
49 }
50
51
52 __PACKAGE__->register_method(
53         method  => "run_method",
54         api_name        => "open-ils.circ.checkout.permit",
55         notes           => q/
56                 Determines if the given checkout can occur
57                 @param authtoken The login session key
58                 @param params A trailing hash of named params including 
59                         barcode : The copy barcode, 
60                         patron : The patron the checkout is occurring for, 
61                         renew : true or false - whether or not this is a renewal
62                 @return The event that occurred during the permit check.  
63         /);
64
65
66 __PACKAGE__->register_method (
67         method          => 'run_method',
68         api_name                => 'open-ils.circ.checkout.permit.override',
69         signature       => q/@see open-ils.circ.checkout.permit/,
70 );
71
72
73 __PACKAGE__->register_method(
74         method  => "run_method",
75         api_name        => "open-ils.circ.checkout",
76         notes => q/
77                 Checks out an item
78                 @param authtoken The login session key
79                 @param params A named hash of params including:
80                         copy                    The copy object
81                         barcode         If no copy is provided, the copy is retrieved via barcode
82                         copyid          If no copy or barcode is provide, the copy id will be use
83                         patron          The patron's id
84                         noncat          True if this is a circulation for a non-cataloted item
85                         noncat_type     The non-cataloged type id
86                         noncat_circ_lib The location for the noncat circ.  
87                         precat          The item has yet to be cataloged
88                         dummy_title The temporary title of the pre-cataloded item
89                         dummy_author The temporary authr of the pre-cataloded item
90                                 Default is the home org of the staff member
91                 @return The SUCCESS event on success, any other event depending on the error
92         /);
93
94 __PACKAGE__->register_method(
95         method  => "run_method",
96         api_name        => "open-ils.circ.checkin",
97         argc            => 2,
98         signature       => q/
99                 Generic super-method for handling all copies
100                 @param authtoken The login session key
101                 @param params Hash of named parameters including:
102                         barcode - The copy barcode
103                         force           - If true, copies in bad statuses will be checked in and give good statuses
104                         ...
105         /
106 );
107
108 __PACKAGE__->register_method(
109         method  => "run_method",
110         api_name        => "open-ils.circ.checkin.override",
111         signature       => q/@see open-ils.circ.checkin/
112 );
113
114 __PACKAGE__->register_method(
115         method  => "run_method",
116         api_name        => "open-ils.circ.renew.override",
117         signature       => q/@see open-ils.circ.renew/,
118 );
119
120
121 __PACKAGE__->register_method(
122         method  => "run_method",
123         api_name        => "open-ils.circ.renew",
124         notes           => <<"  NOTES");
125         PARAMS( authtoken, circ => circ_id );
126         open-ils.circ.renew(login_session, circ_object);
127         Renews the provided circulation.  login_session is the requestor of the
128         renewal and if the logged in user is not the same as circ->usr, then
129         the logged in user must have RENEW_CIRC permissions.
130         NOTES
131
132 __PACKAGE__->register_method(
133         method  => "run_method",
134         api_name        => "open-ils.circ.checkout.full");
135 __PACKAGE__->register_method(
136         method  => "run_method",
137         api_name        => "open-ils.circ.checkout.full.override");
138
139
140
141 sub run_method {
142         my( $self, $conn, $auth, $args ) = @_;
143         translate_legacy_args($args);
144         my $api = $self->api_name;
145
146         my $circulator = 
147                 OpenILS::Application::Circ::Circulator->new($auth, %$args);
148
149         return circ_events($circulator) if $circulator->bail_out;
150
151         # --------------------------------------------------------------------------
152         # Go ahead and load the script runner to make sure we have all 
153         # of the objects we need
154         # --------------------------------------------------------------------------
155         $circulator->is_renewal(1) if $api =~ /renew/;
156         $circulator->is_checkin(1) if $api =~ /checkin/;
157         $circulator->mk_script_runner;
158         return circ_events($circulator) if $circulator->bail_out;
159
160         $circulator->circ_permit_patron($scripts{circ_permit_patron});
161         $circulator->circ_permit_copy($scripts{circ_permit_copy});              
162         $circulator->circ_duration($scripts{circ_duration});                     
163         $circulator->circ_permit_renew($scripts{circ_permit_renew});
164         
165         $circulator->override(1) if $api =~ /override/o;
166
167         if( $api =~ /checkout\.permit/ ) {
168                 $circulator->do_permit();
169
170         } elsif( $api =~ /checkout.full/ ) {
171
172                 $circulator->do_permit();
173                 unless( $circulator->bail_out ) {
174                         $circulator->events([]);
175                         $circulator->do_checkout();
176                 }
177
178         } elsif( $api =~ /checkout/ ) {
179                 $circulator->do_checkout();
180
181         } elsif( $api =~ /checkin/ ) {
182                 $circulator->do_checkin();
183
184         } elsif( $api =~ /renew/ ) {
185                 $circulator->is_renewal(1);
186                 $circulator->do_renew();
187         }
188
189         if( $circulator->bail_out ) {
190
191                 my @ee;
192                 # make sure no success event accidentally slip in
193                 $circulator->events(
194                         [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
195
196                 # Log the events
197                 my @e = @{$circulator->events};
198                 push( @ee, $_->{textcode} ) for @e;
199                 $logger->info("circulator: bailing out with events: @ee");
200
201                 $circulator->editor->rollback;
202
203         } else {
204                 $circulator->editor->commit;
205         }
206
207         $circulator->script_runner->cleanup;
208         
209         $conn->respond_complete(circ_events($circulator));
210
211         unless($circulator->bail_out) {
212                 $circulator->do_hold_notify($circulator->notify_hold)
213                         if $circulator->notify_hold;
214         }
215 }
216
217 sub circ_events {
218         my $circ = shift;
219         my @e = @{$circ->events};
220         # if we have multiple events, SUCCESS should not be one of them;
221         @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
222         return (@e == 1) ? $e[0] : \@e;
223 }
224
225
226
227 sub translate_legacy_args {
228         my $args = shift;
229
230         if( $$args{barcode} ) {
231                 $$args{copy_barcode} = $$args{barcode};
232                 delete $$args{barcode};
233         }
234
235         if( $$args{copyid} ) {
236                 $$args{copy_id} = $$args{copyid};
237                 delete $$args{copyid};
238         }
239
240         if( $$args{patronid} ) {
241                 $$args{patron_id} = $$args{patronid};
242                 delete $$args{patronid};
243         }
244
245         if( $$args{patron} and !ref($$args{patron}) ) {
246                 $$args{patron_id} = $$args{patron};
247                 delete $$args{patron};
248         }
249
250
251         if( $$args{noncat} ) {
252                 $$args{is_noncat} = $$args{noncat};
253                 delete $$args{noncat};
254         }
255
256         if( $$args{precat} ) {
257                 $$args{is_precat} = $$args{precat};
258                 delete $$args{precat};
259         }
260 }
261
262
263
264 # --------------------------------------------------------------------------
265 # This package actually manages all of the circulation logic
266 # --------------------------------------------------------------------------
267 package OpenILS::Application::Circ::Circulator;
268 use strict; use warnings;
269 use vars q/$AUTOLOAD/;
270 use DateTime;
271 use OpenILS::Utils::Fieldmapper;
272 use OpenSRF::Utils::Cache;
273 use Digest::MD5 qw(md5_hex);
274 use DateTime::Format::ISO8601;
275 use OpenILS::Utils::PermitHold;
276 use OpenSRF::Utils qw/:datetime/;
277 use OpenSRF::Utils::SettingsClient;
278 use OpenILS::Application::Circ::Holds;
279 use OpenILS::Application::Circ::Transit;
280 use OpenSRF::Utils::Logger qw(:logger);
281 use OpenILS::Utils::CStoreEditor qw/:funcs/;
282 use OpenILS::Application::Circ::ScriptBuilder;
283 use OpenILS::Const qw/:const/;
284
285 my $U                           = "OpenILS::Application::AppUtils";
286 my $holdcode    = "OpenILS::Application::Circ::Holds";
287 my $transcode   = "OpenILS::Application::Circ::Transit";
288
289 sub DESTROY { }
290
291
292 # --------------------------------------------------------------------------
293 # Add a pile of automagic getter/setter methods
294 # --------------------------------------------------------------------------
295 my @AUTOLOAD_FIELDS = qw/
296         notify_hold
297         penalty_request
298         remote_hold
299         backdate
300         copy
301         copy_id
302         copy_barcode
303         patron
304         patron_id
305         patron_barcode
306         script_runner
307         volume
308         title
309         is_renewal
310         is_noncat
311         is_precat
312         is_checkin
313         noncat_type
314         editor
315         events
316         cache_handle
317         override
318         circ_permit_patron
319         circ_permit_copy
320         circ_duration
321         circ_recurring_fines
322         circ_max_fines
323         circ_permit_renew
324         circ
325         transit
326         hold
327         permit_key
328         noncat_circ_lib
329         noncat_count
330         checkout_time
331         dummy_title
332         dummy_author
333         circ_lib
334         barcode
335    duration_level
336    recurring_fines_level
337    duration_rule
338    recurring_fines_rule
339    max_fine_rule
340         renewal_remaining
341         due_date
342         fulfilled_holds
343         transit
344         checkin_changed
345         force
346         old_circ
347         permit_override
348         pending_checkouts
349 /;
350
351
352 sub AUTOLOAD {
353         my $self = shift;
354         my $type = ref($self) or die "$self is not an object";
355         my $data = shift;
356         my $name = $AUTOLOAD;
357         $name =~ s/.*://o;   
358
359         unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
360                 $logger->error("circulator: $type: invalid autoload field: $name");
361                 die "$type: invalid autoload field: $name\n" 
362         }
363
364         {
365                 no strict 'refs';
366                 *{"${type}::${name}"} = sub {
367                         my $s = shift;
368                         my $v = shift;
369                         $s->{$name} = $v if defined $v;
370                         return $s->{$name};
371                 }
372         }
373         return $self->$name($data);
374 }
375
376
377 sub new {
378         my( $class, $auth, %args ) = @_;
379         $class = ref($class) || $class;
380         my $self = bless( {}, $class );
381
382         $self->events([]);
383         $self->editor( 
384                 new_editor(xact => 1, authtoken => $auth) );
385
386         unless( $self->editor->checkauth ) {
387                 $self->bail_on_events($self->editor->event);
388                 return $self;
389         }
390
391         $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
392
393         $self->$_($args{$_}) for keys %args;
394
395         $self->circ_lib(
396                 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
397
398         return $self;
399 }
400
401
402 # --------------------------------------------------------------------------
403 # True if we should discontinue processing
404 # --------------------------------------------------------------------------
405 sub bail_out {
406         my( $self, $bool ) = @_;
407         if( defined $bool ) {
408                 $logger->info("circulator: BAILING OUT") if $bool;
409                 $self->{bail_out} = $bool;
410         }
411         return $self->{bail_out};
412 }
413
414
415 sub push_events {
416         my( $self, @evts ) = @_;
417         for my $e (@evts) {
418                 next unless $e;
419                 $logger->info("circulator: pushing event ".$e->{textcode});
420                 push( @{$self->events}, $e ) unless
421                         grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
422         }
423 }
424
425 sub mk_permit_key {
426         my $self = shift;
427         my $key = md5_hex( time() . rand() . "$$" );
428         $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
429         return $self->permit_key($key);
430 }
431
432 sub check_permit_key {
433         my $self = shift;
434         my $key = $self->permit_key;
435         return 0 unless $key;
436         my $k = "oils_permit_key_$key";
437         my $one = $self->cache_handle->get_cache($k);
438         $self->cache_handle->delete_cache($k);
439         return ($one) ? 1 : 0;
440 }
441
442
443 # --------------------------------------------------------------------------
444 # This builds the script runner environment and fetches most of the
445 # objects we need
446 # --------------------------------------------------------------------------
447 sub mk_script_runner {
448         my $self = shift;
449         my $args = {};
450
451
452         my @fields = 
453                 qw/copy copy_barcode copy_id patron 
454                         patron_id patron_barcode volume title editor/;
455
456         # Translate our objects into the ScriptBuilder args hash
457         $$args{$_} = $self->$_() for @fields;
458
459         $args->{ignore_user_status} = 1 if $self->is_checkin;
460         $$args{fetch_patron_by_circ_copy} = 1;
461         $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
462
463         if( my $pco = $self->pending_checkouts ) {
464                 $logger->info("circulator: we were given a pending checkouts number of $pco");
465                 $$args{patronItemsOut} = $pco;
466         }
467
468         # This fetches most of the objects we need
469         $self->script_runner(
470                 OpenILS::Application::Circ::ScriptBuilder->build($args));
471
472         # Now we translate the ScriptBuilder objects back into self
473         $self->$_($$args{$_}) for @fields;
474
475         my @evts = @{$args->{_events}} if $args->{_events};
476
477         $logger->debug("circulator: script builder returned events: @evts") if @evts;
478
479
480         if(@evts) {
481                 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
482                 if(!$self->is_noncat and 
483                         @evts == 1 and 
484                         $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
485                                 $self->is_precat(1);
486
487                 } else {
488                         my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
489                         return $self->bail_on_events(@e);
490                 }
491         }
492
493         $self->is_precat(1) if $self->copy 
494                 and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
495
496         # We can't renew if there is no copy
497         return $self->bail_on_events(@evts) if 
498                 $self->is_renewal and !$self->copy;
499
500         # Set some circ-specific flags in the script environment
501         my $evt = "environment";
502         $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
503
504         if( $self->is_noncat ) {
505       $self->script_runner->insert("$evt.isNonCat", 1);
506       $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
507         }
508
509         if( $self->is_precat ) {
510                 $self->script_runner->insert("environment.isPrecat", 1, 1);
511         }
512
513         $self->script_runner->add_path( $_ ) for @$script_libs;
514
515         return 1;
516 }
517
518
519
520
521 # --------------------------------------------------------------------------
522 # Does the circ permit work
523 # --------------------------------------------------------------------------
524 sub do_permit {
525         my $self = shift;
526
527         $self->log_me("do_permit()");
528
529         unless( $self->editor->requestor->id == $self->patron->id ) {
530                 return $self->bail_on_events($self->editor->event)
531                         unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
532         }
533
534         $self->check_captured_holds();
535         $self->do_copy_checks();
536         return if $self->bail_out;
537         $self->run_patron_permit_scripts();
538         $self->run_copy_permit_scripts() 
539                 unless $self->is_precat or $self->is_noncat;
540         $self->override_events() unless $self->is_renewal;
541         return if $self->bail_out;
542
543         if( $self->is_precat ) {
544                 $self->push_events(
545                         OpenILS::Event->new(
546                                 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
547                 return $self->bail_out(1) unless $self->is_renewal;
548         }
549
550         $self->push_events(
551       OpenILS::Event->new(
552                         'SUCCESS', 
553                         payload => $self->mk_permit_key));
554 }
555
556
557 sub check_captured_holds {
558    my $self    = shift;
559    my $copy    = $self->copy;
560    my $patron  = $self->patron;
561
562         return undef unless $copy;
563
564         my $s = $U->copy_status($copy->status)->id;
565         return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
566         $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
567
568         # Item is on the holds shelf, make sure it's going to the right person
569         my $holds       = $self->editor->search_action_hold_request(
570                 [
571                         { 
572                                 current_copy            => $copy->id , 
573                                 capture_time            => { '!=' => undef },
574                                 cancel_time                     => undef, 
575                                 fulfillment_time        => undef 
576                         },
577                         { limit => 1 }
578                 ]
579         );
580
581         if( $holds and $$holds[0] ) {
582                 return undef if $$holds[0]->usr == $patron->id;
583         }
584
585         $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
586
587         $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
588 }
589
590
591 sub do_copy_checks {
592         my $self = shift;
593         my $copy = $self->copy;
594         return unless $copy;
595
596         my $stat = $U->copy_status($copy->status)->id;
597
598         # We cannot check out a copy if it is in-transit
599         if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
600                 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
601         }
602
603         $self->handle_claims_returned();
604         return if $self->bail_out;
605
606         # no claims returned circ was found, check if there is any open circ
607         unless( $self->is_renewal ) {
608                 my $circs = $self->editor->search_action_circulation(
609                         { target_copy => $copy->id, checkin_time => undef }
610                 );
611
612                 return $self->bail_on_events(
613                         OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
614         }
615 }
616
617
618 sub send_penalty_request {
619         my $self = shift;
620         my $ses = OpenSRF::AppSession->create('open-ils.penalty');
621         $self->penalty_request(
622                 $ses->request(  
623                         'open-ils.penalty.patron_penalty.calculate', 
624                         {       update => 1, 
625                                 authtoken => $self->editor->authtoken,
626                                 patron => $self->patron } ) );
627 }
628
629 sub gather_penalty_request {
630         my $self = shift;
631         return [] unless $self->penalty_request;
632         my $data = $self->penalty_request->recv;
633         if( ref $data ) {
634                 throw $data if UNIVERSAL::isa($data,'Error');
635                 $data = $data->content;
636                 return $data->{fatal_penalties};
637         }
638         $logger->error("circulator: penalty request returned no data");
639         return [];
640 }
641
642 # ---------------------------------------------------------------------
643 # This pushes any patron-related events into the list but does not
644 # set bail_out for any events
645 # ---------------------------------------------------------------------
646 sub run_patron_permit_scripts {
647         my $self                = shift;
648         my $runner              = $self->script_runner;
649         my $patronid    = $self->patron->id;
650
651         $self->send_penalty_request() unless $self->is_renewal;
652
653         # ---------------------------------------------------------------------
654         # Now run the patron permit script 
655         # ---------------------------------------------------------------------
656         $runner->load($self->circ_permit_patron);
657         my $result = $runner->run or 
658                 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
659
660         my $patron_events = $result->{events};
661         my @allevents; 
662
663
664         # ---------------------------------------------------------------------
665         # this is policy directly in the code, not a good idea in general, but
666         # the penalty server doesn't know anything about renewals, so we
667         # have to strip the event out here
668         my $penalties = ($self->is_renewal) ? [] : $self->gather_penalty_request();
669         # ---------------------------------------------------------------------
670
671         push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
672
673         $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
674
675         $self->push_events(@allevents);
676 }
677
678
679 sub run_copy_permit_scripts {
680         my $self = shift;
681         my $copy = $self->copy || return;
682         my $runner = $self->script_runner;
683         
684    # ---------------------------------------------------------------------
685    # Capture all of the copy permit events
686    # ---------------------------------------------------------------------
687    $runner->load($self->circ_permit_copy);
688    my $result = $runner->run or 
689                 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
690    my $copy_events = $result->{events};
691
692    # ---------------------------------------------------------------------
693    # Now collect all of the events together
694    # ---------------------------------------------------------------------
695         my @allevents;
696    push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
697
698         # See if this copy has an alert message
699         my $ae = $self->check_copy_alert();
700         push( @allevents, $ae ) if $ae;
701
702    # uniquify the events
703    my %hash = map { ($_->{ilsevent} => $_) } @allevents;
704    @allevents = values %hash;
705
706    for (@allevents) {
707       $_->{payload} = $copy if 
708                         ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
709    }
710
711         $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
712
713         $self->push_events(@allevents);
714 }
715
716
717 sub check_copy_alert {
718         my $self = shift;
719         return undef if $self->is_renewal;
720         return OpenILS::Event->new(
721                 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
722                 if $self->copy and $self->copy->alert_message;
723         return undef;
724 }
725
726
727
728 # --------------------------------------------------------------------------
729 # If the call is overriding and has permissions to override every collected
730 # event, the are cleared.  Any event that the caller does not have
731 # permission to override, will be left in the event list and bail_out will
732 # be set
733 # XXX We need code in here to cancel any holds/transits on copies 
734 # that are being force-checked out
735 # --------------------------------------------------------------------------
736 sub override_events {
737         my $self = shift;
738         my @events = @{$self->events};
739         return unless @events;
740
741         if(!$self->override) {
742                 return $self->bail_out(1) 
743                         if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
744         }       
745
746         $self->events([]);
747         
748    for my $e (@events) {
749       my $tc = $e->{textcode};
750       next if $tc eq 'SUCCESS';
751       my $ov = "$tc.override";
752       $logger->info("circulator: attempting to override event: $ov");
753
754                 return $self->bail_on_events($self->editor->event)
755                         unless( $self->editor->allowed($ov)     );
756    }
757 }
758         
759
760 # --------------------------------------------------------------------------
761 # If there is an open claimsreturn circ on the requested copy, close the 
762 # circ if overriding, otherwise bail out
763 # --------------------------------------------------------------------------
764 sub handle_claims_returned {
765         my $self = shift;
766         my $copy = $self->copy;
767
768         my $CR = $self->editor->search_action_circulation(
769                 {       
770                         target_copy             => $copy->id,
771                         stop_fines              => OILS_STOP_FINES_CLAIMSRETURNED,
772                         checkin_time    => undef,
773                 }
774         );
775
776         return unless ($CR = $CR->[0]); 
777
778         my $evt;
779
780         # - If the caller has set the override flag, we will check the item in
781         if($self->override) {
782
783                 $CR->checkin_time('now');       
784                 $CR->checkin_lib($self->editor->requestor->ws_ou);
785                 $CR->checkin_staff($self->editor->requestor->id);
786
787                 $evt = $self->editor->event 
788                         unless $self->editor->update_action_circulation($CR);
789
790         } else {
791                 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
792         }
793
794         $self->bail_on_events($evt) if $evt;
795         return;
796 }
797
798
799 # --------------------------------------------------------------------------
800 # This performs the checkout
801 # --------------------------------------------------------------------------
802 sub do_checkout {
803         my $self = shift;
804
805         $self->log_me("do_checkout()");
806
807         # make sure perms are good if this isn't a renewal
808         unless( $self->is_renewal ) {
809                 return $self->bail_on_events($self->editor->event)
810                         unless( $self->editor->allowed('COPY_CHECKOUT') );
811         }
812
813         # verify the permit key
814         unless( $self->check_permit_key ) {
815                 if( $self->permit_override ) {
816                         return $self->bail_on_events($self->editor->event)
817                                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
818                 } else {
819                         return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
820                 }       
821         }
822
823         # if this is a non-cataloged circ, build the circ and finish
824         if( $self->is_noncat ) {
825                 $self->checkout_noncat;
826                 $self->push_events(
827                         OpenILS::Event->new('SUCCESS', 
828                         payload => { noncat_circ => $self->circ }));
829                 return;
830         }
831
832         if( $self->is_precat ) {
833                 $self->script_runner->insert("environment.isPrecat", 1, 1);
834                 $self->make_precat_copy;
835                 return if $self->bail_out;
836
837         } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
838                 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
839         }
840
841         $self->do_copy_checks;
842         return if $self->bail_out;
843
844         $self->run_checkout_scripts();
845         return if $self->bail_out;
846
847         $self->build_checkout_circ_object();
848         return if $self->bail_out;
849
850         $self->apply_modified_due_date();
851         return if $self->bail_out;
852
853         return $self->bail_on_events($self->editor->event)
854                 unless $self->editor->create_action_circulation($self->circ);
855
856         # refresh the circ to force local time zone for now
857         $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
858
859         $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
860         $self->update_copy;
861         return if $self->bail_out;
862
863         $self->handle_checkout_holds();
864         return if $self->bail_out;
865
866    # ------------------------------------------------------------------------------
867    # Update the patron penalty info in the DB.  Run it for permit-overrides or
868         # renewals since both of those cases do not require the penalty server to
869         # run during the permit phase of the checkout
870    # ------------------------------------------------------------------------------
871         if( $self->permit_override or $self->is_renewal ) {
872                 $U->update_patron_penalties(
873                         authtoken => $self->editor->authtoken,
874                         patron    => $self->patron,
875                         background  => 1,
876                 );
877         }
878
879         my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
880         $self->push_events(
881                 OpenILS::Event->new('SUCCESS',
882                         payload  => {
883                                 copy              => $U->unflesh_copy($self->copy),
884                                 circ              => $self->circ,
885                                 record            => $record,
886                                 holds_fulfilled   => $self->fulfilled_holds,
887                         }
888                 )
889         );
890 }
891
892 sub update_copy {
893         my $self = shift;
894         my $copy = $self->copy;
895
896         my $stat = $copy->status if ref $copy->status;
897         my $loc = $copy->location if ref $copy->location;
898         my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
899
900         $copy->status($stat->id) if $stat;
901         $copy->location($loc->id) if $loc;
902         $copy->circ_lib($circ_lib->id) if $circ_lib;
903         $copy->editor($self->editor->requestor->id);
904         $copy->edit_date('now');
905         $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
906
907         return $self->bail_on_events($self->editor->event)
908                 unless $self->editor->update_asset_copy($self->copy);
909
910         $copy->status($U->copy_status($copy->status));
911         $copy->location($loc) if $loc;
912         $copy->circ_lib($circ_lib) if $circ_lib;
913 }
914
915
916 sub bail_on_events {
917         my( $self, @evts ) = @_;
918         $self->push_events(@evts);
919         $self->bail_out(1);
920 }
921
922 sub handle_checkout_holds {
923    my $self    = shift;
924
925    my $copy    = $self->copy;
926    my $patron  = $self->patron;
927
928         my $holds       = $self->editor->search_action_hold_request(
929                 { 
930                         current_copy            => $copy->id , 
931                         cancel_time                     => undef, 
932                         fulfillment_time        => undef 
933                 }
934         );
935
936    my @fulfilled;
937
938    # XXX We should only fulfill one hold here...
939    # XXX If a hold was transited to the user who is checking out
940    # the item, we need to make sure that hold is what's grabbed
941    if(@$holds) {
942
943       # for now, just sort by id to get what should be the oldest hold
944       $holds = [ sort { $a->id <=> $b->id } @$holds ];
945       my @myholds = grep { $_->usr eq $patron->id } @$holds;
946       my @altholds   = grep { $_->usr ne $patron->id } @$holds;
947
948       if(@myholds) {
949          my $hold = $myholds[0];
950
951          $logger->debug("circulator: related hold found in checkout: " . $hold->id );
952
953          # if the hold was never officially captured, capture it.
954          $hold->capture_time('now') unless $hold->capture_time;
955
956                         # just make sure it's set correctly
957          $hold->current_copy($copy->id); 
958
959          $hold->fulfillment_time('now');
960                         $hold->fulfillment_staff($self->editor->requestor->id);
961                         $hold->fulfillment_lib($self->editor->requestor->ws_ou);
962
963                         return $self->bail_on_events($self->editor->event)
964                                 unless $self->editor->update_action_hold_request($hold);
965
966                         $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
967
968          push( @fulfilled, $hold->id );
969       }
970
971       # If there are any holds placed for other users that point to this copy,
972       # then we need to un-target those holds so the targeter can pick a new copy
973       for(@altholds) {
974
975          $logger->info("circulator: un-targeting hold ".$_->id.
976             " because copy ".$copy->id." is getting checked out");
977
978                         # - make the targeter process this hold at next run
979          $_->clear_prev_check_time; 
980
981                         # - clear out the targetted copy
982          $_->clear_current_copy;
983          $_->clear_capture_time;
984
985                         return $self->bail_on_event($self->editor->event)
986                                 unless $self->editor->update_action_hold_request($_);
987       }
988    }
989
990         $self->fulfilled_holds(\@fulfilled);
991 }
992
993
994
995 sub run_checkout_scripts {
996         my $self = shift;
997
998         my $evt;
999    my $runner = $self->script_runner;
1000    $runner->load($self->circ_duration);
1001
1002    my $result = $runner->run or 
1003                 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1004
1005    my $duration   = $result->{durationRule};
1006    my $recurring  = $result->{recurringFinesRule};
1007    my $max_fine   = $result->{maxFine};
1008
1009         if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
1010
1011                 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1012                 return $self->bail_on_events($evt) if $evt;
1013         
1014                 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1015                 return $self->bail_on_events($evt) if $evt;
1016         
1017                 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1018                 return $self->bail_on_events($evt) if $evt;
1019
1020         } else {
1021
1022                 # The item circulates with an unlimited duration
1023                 $duration       = undef;
1024                 $recurring      = undef;
1025                 $max_fine       = undef;
1026         }
1027
1028    $self->duration_rule($duration);
1029    $self->recurring_fines_rule($recurring);
1030    $self->max_fine_rule($max_fine);
1031 }
1032
1033
1034 sub build_checkout_circ_object {
1035         my $self = shift;
1036
1037    my $circ       = Fieldmapper::action::circulation->new;
1038    my $duration   = $self->duration_rule;
1039    my $max        = $self->max_fine_rule;
1040    my $recurring  = $self->recurring_fines_rule;
1041    my $copy       = $self->copy;
1042    my $patron     = $self->patron;
1043
1044         if( $duration ) {
1045
1046                 my $dname = $duration->name;
1047                 my $mname = $max->name;
1048                 my $rname = $recurring->name;
1049         
1050                 $logger->debug("circulator: building circulation ".
1051                         "with duration=$dname, maxfine=$mname, recurring=$rname");
1052         
1053                 $circ->duration( $duration->shrt ) 
1054                         if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1055                 $circ->duration( $duration->normal ) 
1056                         if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1057                 $circ->duration( $duration->extended ) 
1058                         if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1059         
1060                 $circ->recuring_fine( $recurring->low ) 
1061                         if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1062                 $circ->recuring_fine( $recurring->normal ) 
1063                         if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1064                 $circ->recuring_fine( $recurring->high ) 
1065                         if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1066
1067                 $circ->duration_rule( $duration->name );
1068                 $circ->recuring_fine_rule( $recurring->name );
1069                 $circ->max_fine_rule( $max->name );
1070                 $circ->max_fine( $max->amount );
1071
1072                 $circ->fine_interval($recurring->recurance_interval);
1073                 $circ->renewal_remaining( $duration->max_renewals );
1074
1075         } else {
1076
1077                 $logger->info("circulator: copy found with an unlimited circ duration");
1078                 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1079                 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1080                 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1081                 $circ->renewal_remaining(0);
1082         }
1083
1084    $circ->target_copy( $copy->id );
1085    $circ->usr( $patron->id );
1086    $circ->circ_lib( $self->circ_lib );
1087
1088    if( $self->is_renewal ) {
1089       $circ->opac_renewal(1);
1090       $circ->renewal_remaining($self->renewal_remaining);
1091       $circ->circ_staff($self->editor->requestor->id);
1092    }
1093
1094    # if the user provided an overiding checkout time,
1095    # (e.g. the checkout really happened several hours ago), then
1096    # we apply that here.  Does this need a perm??
1097         $circ->xact_start(clense_ISO8601($self->checkout_time))
1098                 if $self->checkout_time;
1099
1100    # if a patron is renewing, 'requestor' will be the patron
1101    $circ->circ_staff($self->editor->requestor->id);
1102         $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1103
1104         $self->circ($circ);
1105 }
1106
1107
1108 sub apply_modified_due_date {
1109         my $self = shift;
1110         my $circ = $self->circ;
1111         my $copy = $self->copy;
1112
1113    if( $self->due_date ) {
1114
1115                 return $self->bail_on_events($self->editor->event)
1116                         unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1117
1118       $circ->due_date(clense_ISO8601($self->due_date));
1119
1120    } else {
1121
1122       # if the due_date lands on a day when the location is closed
1123       return unless $copy and $circ->due_date;
1124
1125                 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1126
1127       $logger->info("circulator: circ searching for closed date overlap on lib $org".
1128                         " with an item due date of ".$circ->due_date );
1129
1130       my $dateinfo = $U->storagereq(
1131          'open-ils.storage.actor.org_unit.closed_date.overlap', 
1132                         $org, $circ->due_date );
1133
1134       if($dateinfo) {
1135          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1136             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1137
1138             # XXX make the behavior more dynamic
1139             # for now, we just push the due date to after the close date
1140             $circ->due_date($dateinfo->{end});
1141       }
1142    }
1143 }
1144
1145
1146
1147 sub create_due_date {
1148         my( $self, $duration ) = @_;
1149    my ($sec,$min,$hour,$mday,$mon,$year) =
1150       gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1151    $year += 1900; $mon += 1;
1152    my $due_date = sprintf(
1153       '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1154       $year, $mon, $mday, $hour, $min, $sec);
1155    return $due_date;
1156 }
1157
1158
1159
1160 sub make_precat_copy {
1161         my $self = shift;
1162         my $copy = $self->copy;
1163
1164    if($copy) {
1165       $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1166
1167       $copy->editor($self->editor->requestor->id);
1168       $copy->edit_date('now');
1169       $copy->dummy_title($self->dummy_title);
1170       $copy->dummy_author($self->dummy_author);
1171
1172                 $self->update_copy();
1173                 return;
1174    }
1175
1176    $logger->info("circulator: Creating a new precataloged ".
1177                 "copy in checkout with barcode " . $self->copy_barcode);
1178
1179    $copy = Fieldmapper::asset::copy->new;
1180    $copy->circ_lib($self->circ_lib);
1181    $copy->creator($self->editor->requestor->id);
1182    $copy->editor($self->editor->requestor->id);
1183    $copy->barcode($self->copy_barcode);
1184    $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
1185    $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1186    $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1187
1188    $copy->dummy_title($self->dummy_title || "");
1189    $copy->dummy_author($self->dummy_author || "");
1190
1191         unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1192                 $self->bail_out(1);
1193                 $self->push_events($self->editor->event);
1194                 return;
1195         }       
1196
1197         # this is a little bit of a hack, but we need to 
1198         # get the copy into the script runner
1199         $self->script_runner->insert("environment.copy", $copy, 1);
1200 }
1201
1202
1203 sub checkout_noncat {
1204         my $self = shift;
1205
1206         my $circ;
1207         my $evt;
1208
1209    my $lib              = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1210    my $count    = $self->noncat_count || 1;
1211    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1212
1213    $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1214
1215    for(1..$count) {
1216
1217       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1218          $self->editor->requestor->id, 
1219                         $self->patron->id, 
1220                         $lib, 
1221                         $self->noncat_type, 
1222                         $cotime,
1223                         $self->editor );
1224
1225                 if( $evt ) {
1226                         $self->push_events($evt);
1227                         $self->bail_out(1);
1228                         return; 
1229                 }
1230                 $self->circ($circ);
1231    }
1232 }
1233
1234
1235 sub do_checkin {
1236         my $self = shift;
1237         $self->log_me("do_checkin()");
1238
1239
1240         return $self->bail_on_events(
1241                 OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
1242                 unless $self->copy;
1243
1244         if( $self->checkin_check_holds_shelf() ) {
1245                 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1246                 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1247                 $self->checkin_flesh_events;
1248                 return;
1249         }
1250
1251         unless( $self->is_renewal ) {
1252                 return $self->bail_on_events($self->editor->event)
1253                         unless $self->editor->allowed('COPY_CHECKIN');
1254         }
1255
1256         $self->push_events($self->check_copy_alert());
1257         $self->push_events($self->check_checkin_copy_status());
1258
1259         # the renew code will have already found our circulation object
1260         unless( $self->is_renewal and $self->circ ) {
1261                 $self->circ(
1262                         $self->editor->search_action_circulation(
1263                         { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1264         }
1265
1266         # if the circ is marked as 'claims returned', add the event to the list
1267         $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1268                 if ($self->circ and $self->circ->stop_fines 
1269                                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1270
1271         # handle the overridable events 
1272         $self->override_events unless $self->is_renewal;
1273         return if $self->bail_out;
1274         
1275         if( $self->copy ) {
1276                 $self->transit(
1277                         $self->editor->search_action_transit_copy(
1278                         { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);     
1279         }
1280
1281         if( $self->circ ) {
1282                 $self->checkin_handle_circ;
1283                 return if $self->bail_out;
1284                 $self->checkin_changed(1);
1285
1286         } elsif( $self->transit ) {
1287                 my $hold_transit = $self->process_received_transit;
1288                 $self->checkin_changed(1);
1289
1290                 if( $self->bail_out ) { 
1291                         $self->checkin_flesh_events;
1292                         return;
1293                 }
1294                 
1295                 if( my $e = $self->check_checkin_copy_status() ) {
1296                         # If the original copy status is special, alert the caller
1297                         my $ev = $self->events;
1298                         $self->events([$e]);
1299                         $self->override_events;
1300                         return if $self->bail_out;
1301                         $self->events($ev);
1302                 }
1303
1304                 if( $hold_transit or 
1305                                 $U->copy_status($self->copy->status)->id 
1306                                         == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1307                         $self->hold(
1308                                 ($hold_transit) ?
1309                                         $self->editor->retrieve_action_hold_request($hold_transit->hold) :
1310                                         $U->fetch_open_hold_by_copy($self->copy->id)
1311                                 );
1312
1313                         $self->checkin_flesh_events;
1314                         return;
1315                 } 
1316
1317         } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1318                 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1319                         " that is in-transit, but there is no transit.. repairing");
1320                 $self->reshelve_copy(1);
1321                 return if $self->bail_out;
1322         }
1323
1324         if( $self->is_renewal ) {
1325                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1326                 return;
1327         }
1328
1329    # ------------------------------------------------------------------------------
1330    # Circulations and transits are now closed where necessary.  Now go on to see if
1331    # this copy can fulfill a hold or needs to be routed to a different location
1332    # ------------------------------------------------------------------------------
1333
1334         if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1335                 return if $self->bail_out;
1336
1337    } else { # not needed for a hold
1338
1339
1340                 my $circ_lib = (ref $self->copy->circ_lib) ? 
1341                                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1342
1343                 if( $self->remote_hold ) {
1344                         $circ_lib = $self->remote_hold->pickup_lib;
1345                         $logger->warn("circulator: Copy ".$self->copy->barcode.
1346                                 " is on a remote hold's shelf, sending to $circ_lib");
1347                 }
1348
1349                 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1350
1351       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1352
1353                         $self->checkin_handle_precat();
1354                         return if $self->bail_out;
1355
1356       } else {
1357
1358                         my $bc = $self->copy->barcode;
1359                         $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1360                         $self->checkin_build_copy_transit($circ_lib);
1361                         return if $self->bail_out;
1362                         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1363       }
1364    }
1365
1366         $self->reshelve_copy;
1367         return if $self->bail_out;
1368
1369         unless($self->checkin_changed) {
1370
1371                 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1372                 my $stat = $U->copy_status($self->copy->status)->id;
1373
1374         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1375          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1376                 $self->bail_out(1); # no need to commit anything
1377
1378         } else {
1379                 $self->push_events(OpenILS::Event->new('SUCCESS')) 
1380                         unless @{$self->events};
1381         }
1382
1383
1384    # ------------------------------------------------------------------------------
1385    # Update the patron penalty info in the DB
1386    # ------------------------------------------------------------------------------
1387    $U->update_patron_penalties(
1388       authtoken => $self->editor->authtoken,
1389       patron    => $self->patron,
1390       background  => 1 ) if $self->is_checkin;
1391
1392         $self->checkin_flesh_events;
1393         return;
1394 }
1395
1396 sub reshelve_copy {
1397    my $self    = shift;
1398    my $force   = $self->force || shift;
1399    my $copy    = $self->copy;
1400
1401    my $stat = $U->copy_status($copy->status)->id;
1402
1403    if($force || (
1404       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1405       $stat != OILS_COPY_STATUS_CATALOGING and
1406       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1407       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1408
1409         $copy->status( OILS_COPY_STATUS_RESHELVING );
1410                         $self->update_copy;
1411                         $self->checkin_changed(1);
1412         }
1413 }
1414
1415
1416 # Returns true if the item is at the current location
1417 # because it was transited there for a hold and the 
1418 # hold has not been fulfilled
1419 sub checkin_check_holds_shelf {
1420         my $self = shift;
1421         return 0 unless $self->copy;
1422
1423         return 0 unless 
1424                 $U->copy_status($self->copy->status)->id ==
1425                         OILS_COPY_STATUS_ON_HOLDS_SHELF;
1426
1427         # find the hold that put us on the holds shelf
1428         my $holds = $self->editor->search_action_hold_request(
1429                 { 
1430                         current_copy => $self->copy->id,
1431                         capture_time => { '!=' => undef },
1432                         fulfillment_time => undef,
1433                         cancel_time => undef,
1434                 }
1435         );
1436
1437         unless(@$holds) {
1438                 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1439                 $self->reshelve_copy(1);
1440                 return 0;
1441         }
1442
1443         my $hold = $$holds[0];
1444
1445         $logger->info("circulator: we found a captured, un-fulfilled hold [".
1446                 $hold->id. "] for copy ".$self->copy->barcode);
1447
1448         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1449                 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1450                 return 1;
1451         }
1452
1453         $logger->info("circulator: hold is not for here..");
1454         $self->remote_hold($hold);
1455         return 0;
1456 }
1457
1458
1459 sub checkin_handle_precat {
1460         my $self        = shift;
1461    my $copy    = $self->copy;
1462
1463    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1464       $copy->status(OILS_COPY_STATUS_CATALOGING);
1465                 $self->update_copy();
1466                 $self->checkin_changed(1);
1467                 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1468    }
1469 }
1470
1471
1472 sub checkin_build_copy_transit {
1473         my $self                        = shift;
1474         my $dest                        = shift;
1475         my $copy       = $self->copy;
1476    my $transit    = Fieldmapper::action::transit_copy->new;
1477
1478         #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1479         $logger->info("circulator: transiting copy to $dest");
1480
1481    $transit->source($self->editor->requestor->ws_ou);
1482    $transit->dest($dest);
1483    $transit->target_copy($copy->id);
1484    $transit->source_send_time('now');
1485    $transit->copy_status( $U->copy_status($copy->status)->id );
1486
1487         $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1488
1489         return $self->bail_on_events($self->editor->event)
1490                 unless $self->editor->create_action_transit_copy($transit);
1491
1492    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1493         $self->update_copy;
1494         $self->checkin_changed(1);
1495 }
1496
1497
1498 sub attempt_checkin_hold_capture {
1499         my $self = shift;
1500         my $copy = $self->copy;
1501
1502         # See if this copy can fulfill any holds
1503         my ($hold) = $holdcode->find_nearest_permitted_hold(
1504                 OpenSRF::AppSession->create('open-ils.storage'), 
1505                 $copy, $self->editor->requestor );
1506
1507         if(!$hold) {
1508                 $logger->debug("circulator: no potential permitted".
1509                         "holds found for copy ".$copy->barcode);
1510                 return undef;
1511         }
1512
1513
1514         $logger->info("circulator: found permitted hold ".
1515                 $hold->id . " for copy, capturing...");
1516
1517         $hold->current_copy($copy->id);
1518         $hold->capture_time('now');
1519
1520         # prevent DB errors caused by fetching 
1521         # holds from storage, and updating through cstore
1522         $hold->clear_fulfillment_time;
1523         $hold->clear_fulfillment_staff;
1524         $hold->clear_fulfillment_lib;
1525         $hold->clear_expire_time; 
1526         $hold->clear_cancel_time;
1527         $hold->clear_prev_check_time unless $hold->prev_check_time;
1528
1529         $self->bail_on_events($self->editor->event)
1530                 unless $self->editor->update_action_hold_request($hold);
1531         $self->hold($hold);
1532         $self->checkin_changed(1);
1533
1534         return 1 if $self->bail_out;
1535
1536         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1537
1538                 # This hold was captured in the correct location
1539         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1540                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1541
1542                 #$self->do_hold_notify($hold->id);
1543                 $self->notify_hold($hold->id);
1544
1545         } else {
1546         
1547                 # Hold needs to be picked up elsewhere.  Build a hold
1548                 # transit and route the item.
1549                 $self->checkin_build_hold_transit();
1550         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1551                 return 1 if $self->bail_out;
1552                 $self->push_events(
1553                         OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1554         }
1555
1556         # make sure we save the copy status
1557         $self->update_copy;
1558         return 1;
1559 }
1560
1561 sub do_hold_notify {
1562         my( $self, $holdid ) = @_;
1563
1564         $logger->info("circulator: running delayed hold notify process");
1565
1566         my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1567                 hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1568
1569         $logger->debug("circulator: built hold notifier");
1570
1571         if(!$notifier->event) {
1572
1573                 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1574
1575                 my $stat = $notifier->send_email_notify;
1576                 if( $stat == '1' ) {
1577                         $logger->info("ciculator: hold notify succeeded for hold $holdid");
1578                         return;
1579                 } 
1580
1581                 $logger->warn("ciculator:  * hold notify failed for hold $holdid");
1582
1583         } else {
1584                 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1585         }
1586 }
1587
1588
1589 sub checkin_build_hold_transit {
1590         my $self = shift;
1591
1592    my $copy = $self->copy;
1593    my $hold = $self->hold;
1594    my $trans = Fieldmapper::action::hold_transit_copy->new;
1595
1596         $logger->debug("circulator: building hold transit for ".$copy->barcode);
1597
1598    $trans->hold($hold->id);
1599    $trans->source($self->editor->requestor->ws_ou);
1600    $trans->dest($hold->pickup_lib);
1601    $trans->source_send_time("now");
1602    $trans->target_copy($copy->id);
1603
1604         # when the copy gets to its destination, it will recover
1605         # this status - put it onto the holds shelf
1606    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1607
1608         return $self->bail_on_events($self->editor->event)
1609                 unless $self->editor->create_action_hold_transit_copy($trans);
1610 }
1611
1612
1613
1614 sub process_received_transit {
1615         my $self = shift;
1616         my $copy = $self->copy;
1617    my $copyid = $self->copy->id;
1618
1619         my $status_name = $U->copy_status($copy->status)->name;
1620    $logger->debug("circulator: attempting transit receive on ".
1621                 "copy $copyid. Copy status is $status_name");
1622
1623         my $transit = $self->transit;
1624
1625    if( $transit->dest != $self->editor->requestor->ws_ou ) {
1626       $logger->info("circulator: Fowarding transit on copy which is destined ".
1627          "for a different location. copy=$copyid,current ".
1628          "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1629
1630                 return $self->bail_on_events(
1631                         OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1632    }
1633
1634    # The transit is received, set the receive time
1635    $transit->dest_recv_time('now');
1636         $self->bail_on_events($self->editor->event)
1637                 unless $self->editor->update_action_transit_copy($transit);
1638
1639         my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1640
1641    $logger->info("ciculator: Recovering original copy status in transit: ".$transit->copy_status);
1642    $copy->status( $transit->copy_status );
1643         $self->update_copy();
1644         return if $self->bail_out;
1645
1646         my $ishold = 0;
1647         if($hold_transit) {     
1648                 #$self->do_hold_notify($hold_transit->hold);
1649                 $self->notify_hold($hold_transit->hold);
1650                 $ishold = 1;
1651         }
1652
1653         $self->push_events( 
1654                 OpenILS::Event->new(
1655                 'SUCCESS', 
1656                 ishold => $ishold,
1657       payload => { transit => $transit, holdtransit => $hold_transit } ));
1658
1659         return $hold_transit;
1660 }
1661
1662
1663 sub checkin_handle_circ {
1664    my $self = shift;
1665         $U->logmark;
1666
1667    my $circ = $self->circ;
1668    my $copy = $self->copy;
1669    my $evt;
1670    my $obt;
1671
1672    # backdate the circ if necessary
1673    if($self->backdate) {
1674                 $self->checkin_handle_backdate;
1675                 return if $self->bail_out;
1676    }
1677
1678    if(!$circ->stop_fines) {
1679       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1680       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1681       $circ->stop_fines_time('now') unless $self->backdate;
1682       $circ->stop_fines_time($self->backdate) if $self->backdate;
1683    }
1684
1685    # see if there are any fines owed on this circ.  if not, close it
1686         ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1687    $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1688
1689         $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1690
1691    # Set the checkin vars since we have the item
1692    $circ->checkin_time('now');
1693    $circ->checkin_staff($self->editor->requestor->id);
1694    $circ->checkin_lib($self->editor->requestor->ws_ou);
1695
1696         my $circ_lib = (ref $self->copy->circ_lib) ?  
1697                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1698         my $stat = $U->copy_status($self->copy->status)->id;
1699
1700         # If the item is lost/missing and it needs to be sent home, don't 
1701         # reshelve the copy, leave it lost/missing so the recipient will know
1702         if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1703                 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1704                 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1705
1706         } else {
1707                 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1708                 $self->update_copy;
1709         }
1710
1711
1712         return $self->bail_on_events($self->editor->event)
1713                 unless $self->editor->update_action_circulation($circ);
1714 }
1715
1716
1717 sub checkin_handle_backdate {
1718         my $self = shift;
1719
1720         my $bd = $self->backdate;
1721         $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1722         $bd = "${bd}T23:59:59";
1723
1724         my $bills = $self->editor->search_money_billing(
1725                 { 
1726                         billing_ts => { '>=' => $bd }, 
1727                         xact => $self->circ->id, 
1728                         billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1729                 }
1730         );
1731
1732         $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1733
1734         for my $bill (@$bills) {        
1735                 unless( $U->is_true($bill->voided) ) {
1736                         $logger->info("backdate voiding bill ".$bill->id);
1737                         $bill->voided('t');
1738                         $bill->void_time('now');
1739                         $bill->voider($self->editor->requestor->id);
1740                         my $n = $bill->note || "";
1741                         $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1742
1743                         $self->bail_on_events($self->editor->event)
1744                                 unless $self->editor->update_money_billing($bill);
1745                 }
1746         }
1747 }
1748
1749
1750
1751 =head
1752 # XXX Legacy version for Circ.pm support
1753 sub _checkin_handle_backdate {
1754    my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1755
1756         my $bd = $backdate;
1757         $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1758         $bd = "${bd}T23:59:59";
1759
1760    my $bills = $session->request(
1761       "open-ils.storage.direct.money.billing.search_where.atomic",
1762                 billing_ts => { '>=' => $bd }, 
1763                 xact => $circ->id,
1764                 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1765         )->gather(1);
1766
1767         $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1768
1769    if($bills) {
1770       for my $bill (@$bills) {
1771                         unless( $U->is_true($bill->voided) ) {
1772                                 $logger->debug("voiding bill ".$bill->id);
1773                                 $bill->voided('t');
1774                                 $bill->void_time('now');
1775                                 $bill->voider($requestor->id);
1776                                 my $n = $bill->note || "";
1777                                 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1778                                 my $s = $session->request(
1779                                         "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1780                                 return $U->DB_UPDATE_FAILED($bill) unless $s;
1781                         }
1782                 }
1783    }
1784
1785         return 100;
1786 }
1787 =cut
1788
1789
1790
1791
1792
1793
1794 sub find_patron_from_copy {
1795         my $self = shift;
1796         my $circs = $self->editor->search_action_circulation(
1797                 { target_copy => $self->copy->id, checkin_time => undef });
1798         my $circ = $circs->[0];
1799         return unless $circ;
1800         my $u = $self->editor->retrieve_actor_user($circ->usr)
1801                 or return $self->bail_on_events($self->editor->event);
1802         $self->patron($u);
1803 }
1804
1805 sub check_checkin_copy_status {
1806         my $self = shift;
1807    my $copy = $self->copy;
1808
1809    my $islost     = 0;
1810    my $ismissing  = 0;
1811    my $evt        = undef;
1812
1813    my $status = $U->copy_status($copy->status)->id;
1814
1815    return undef
1816       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
1817             $status == OILS_COPY_STATUS_CHECKED_OUT ||
1818             $status == OILS_COPY_STATUS_IN_PROCESS  ||
1819             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
1820             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
1821             $status == OILS_COPY_STATUS_CATALOGING  ||
1822             $status == OILS_COPY_STATUS_RESHELVING );
1823
1824    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1825       if( $status == OILS_COPY_STATUS_LOST );
1826
1827    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1828       if( $status == OILS_COPY_STATUS_MISSING );
1829
1830    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1831 }
1832
1833
1834
1835 # --------------------------------------------------------------------------
1836 # On checkin, we need to return as many relevant objects as we can
1837 # --------------------------------------------------------------------------
1838 sub checkin_flesh_events {
1839         my $self = shift;
1840
1841         if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
1842                 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1843                         $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1844         }
1845
1846
1847         for my $evt (@{$self->events}) {
1848
1849                 my $payload          = {};
1850                 $payload->{copy}     = $U->unflesh_copy($self->copy);
1851                 $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1852                 $payload->{circ}     = $self->circ;
1853                 $payload->{transit}  = $self->transit;
1854                 $payload->{hold}     = $self->hold;
1855                 
1856                 $evt->{payload} = $payload;
1857         }
1858 }
1859
1860 sub log_me {
1861         my( $self, $msg ) = @_;
1862         my $bc = ($self->copy) ? $self->copy->barcode :
1863                 $self->barcode;
1864         $bc ||= "";
1865         my $usr = ($self->patron) ? $self->patron->id : "";
1866         $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1867                 ", recipient=$usr, copy=$bc");
1868 }
1869
1870
1871 sub do_renew {
1872         my $self = shift;
1873         $self->log_me("do_renew()");
1874         $self->is_renewal(1);
1875
1876         unless( $self->is_renewal ) {
1877                 return $self->bail_on_events($self->editor->events)
1878                         unless $self->editor->allowed('RENEW_CIRC');
1879         }       
1880
1881         # Make sure there is an open circ to renew that is not
1882         # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1883         my $circ = $self->editor->search_action_circulation(
1884                         { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1885
1886         if(!$circ) {
1887                 $circ = $self->editor->search_action_circulation(
1888                         { 
1889                                 target_copy => $self->copy->id, 
1890                                 stop_fines => OILS_STOP_FINES_MAX_FINES,
1891                                 checkin_time => undef
1892                         } 
1893                 )->[0];
1894         }
1895
1896         return $self->bail_on_events($self->editor->event) unless $circ;
1897
1898         $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1899                 if $circ->renewal_remaining < 1;
1900
1901         # -----------------------------------------------------------------
1902
1903         $self->renewal_remaining( $circ->renewal_remaining - 1 );
1904         $self->circ($circ);
1905
1906         $self->run_renew_permit;
1907
1908         # Check the item in
1909         $self->do_checkin();
1910         return if $self->bail_out;
1911
1912         unless( $self->permit_override ) {
1913                 $self->do_permit();
1914                 return if $self->bail_out;
1915                 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1916                 $self->remove_event('ITEM_NOT_CATALOGED');
1917         }       
1918
1919         $self->override_events;
1920         return if $self->bail_out;
1921
1922         $self->events([]);
1923         $self->do_checkout();
1924 }
1925
1926
1927 sub remove_event {
1928         my( $self, $evt ) = @_;
1929         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1930         $logger->debug("circulator: removing event from list: $evt");
1931         my @events = @{$self->events};
1932         $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1933 }
1934
1935
1936 sub have_event {
1937         my( $self, $evt ) = @_;
1938         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1939         return grep { $_->{textcode} eq $evt } @{$self->events};
1940 }
1941
1942
1943
1944 sub run_renew_permit {
1945         my $self = shift;
1946    my $runner = $self->script_runner;
1947
1948    $runner->load($self->circ_permit_renew);
1949    my $result = $runner->run or 
1950                 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1951    my $events = $result->{events};
1952
1953    $logger->activity("ciculator: circ_permit_renew for user ".
1954       $self->patron->id." returned events: @$events") if @$events;
1955
1956         $self->push_events(OpenILS::Event->new($_)) for @$events;
1957         
1958         $logger->debug("circulator: re-creating script runner to be safe");
1959         $self->mk_script_runner;
1960 }
1961
1962
1963
1964