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