]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
LP#638509: Remove the stop_fines checks from do_renew's circulation search.
[Evergreen.git] / Open-ILS / src / perlmods / lib / 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::AppSession;
6 use OpenSRF::Utils::SettingsClient;
7 use OpenSRF::Utils::Logger qw(:logger);
8 use OpenILS::Const qw/:const/;
9 use OpenILS::Application::AppUtils;
10 use DateTime;
11 my $U = "OpenILS::Application::AppUtils";
12
13 my %scripts;
14 my $script_libs;
15 my $legacy_script_support = 0;
16 my $booking_status;
17 my $opac_renewal_use_circ_lib;
18 my $desk_renewal_use_circ_lib;
19
20 sub determine_booking_status {
21     unless (defined $booking_status) {
22         my $ses = create OpenSRF::AppSession("router");
23         $booking_status = grep {$_ eq "open-ils.booking"} @{
24             $ses->request("opensrf.router.info.class.list")->gather(1)
25         };
26         $ses->disconnect;
27         $logger->info("booking status: " . ($booking_status ? "on" : "off"));
28     }
29
30     return $booking_status;
31 }
32
33
34 my $MK_ENV_FLESH = { 
35     flesh => 2, 
36     flesh_fields => {acp => ['call_number','parts','floating'], acn => ['record']}
37 };
38
39 sub initialize {
40
41     my $self = shift;
42     my $conf = OpenSRF::Utils::SettingsClient->new;
43     my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
44
45     $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
46     $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
47
48     my $lb  = $conf->config_value(  @pfx2, 'script_path' );
49     $lb = [ $lb ] unless ref($lb);
50     $script_libs = $lb;
51
52     return unless $legacy_script_support;
53
54     my @pfx = ( @pfx2, "scripts" );
55     my $p   = $conf->config_value(  @pfx, 'circ_permit_patron' );
56     my $c   = $conf->config_value(  @pfx, 'circ_permit_copy' );
57     my $d   = $conf->config_value(  @pfx, 'circ_duration' );
58     my $f   = $conf->config_value(  @pfx, 'circ_recurring_fines' );
59     my $m   = $conf->config_value(  @pfx, 'circ_max_fines' );
60     my $pr  = $conf->config_value(  @pfx, 'circ_permit_renew' );
61
62     $logger->error( "Missing circ script(s)" ) 
63         unless( $p and $c and $d and $f and $m and $pr );
64
65     $scripts{circ_permit_patron}   = $p;
66     $scripts{circ_permit_copy}     = $c;
67     $scripts{circ_duration}        = $d;
68     $scripts{circ_recurring_fines} = $f;
69     $scripts{circ_max_fines}       = $m;
70     $scripts{circ_permit_renew}    = $pr;
71
72     $logger->debug(
73         "circulator: Loaded rules scripts for circ: " .
74         "circ permit patron = $p, ".
75         "circ permit copy = $c, ".
76         "circ duration = $d, ".
77         "circ recurring fines = $f, " .
78         "circ max fines = $m, ".
79         "circ renew permit = $pr.  ".
80         "lib paths = @$lb. ".
81         "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
82         );
83 }
84
85 __PACKAGE__->register_method(
86     method  => "run_method",
87     api_name    => "open-ils.circ.checkout.permit",
88     notes       => q/
89         Determines if the given checkout can occur
90         @param authtoken The login session key
91         @param params A trailing hash of named params including 
92             barcode : The copy barcode, 
93             patron : The patron the checkout is occurring for, 
94             renew : true or false - whether or not this is a renewal
95         @return The event that occurred during the permit check.  
96     /);
97
98
99 __PACKAGE__->register_method (
100     method      => 'run_method',
101     api_name        => 'open-ils.circ.checkout.permit.override',
102     signature   => q/@see open-ils.circ.checkout.permit/,
103 );
104
105
106 __PACKAGE__->register_method(
107     method  => "run_method",
108     api_name    => "open-ils.circ.checkout",
109     notes => q/
110         Checks out an item
111         @param authtoken The login session key
112         @param params A named hash of params including:
113             copy            The copy object
114             barcode     If no copy is provided, the copy is retrieved via barcode
115             copyid      If no copy or barcode is provide, the copy id will be use
116             patron      The patron's id
117             noncat      True if this is a circulation for a non-cataloted item
118             noncat_type The non-cataloged type id
119             noncat_circ_lib The location for the noncat circ.  
120             precat      The item has yet to be cataloged
121             dummy_title The temporary title of the pre-cataloded item
122             dummy_author The temporary authr of the pre-cataloded item
123                 Default is the home org of the staff member
124         @return The SUCCESS event on success, any other event depending on the error
125     /);
126
127 __PACKAGE__->register_method(
128     method  => "run_method",
129     api_name    => "open-ils.circ.checkin",
130     argc        => 2,
131     signature   => q/
132         Generic super-method for handling all copies
133         @param authtoken The login session key
134         @param params Hash of named parameters including:
135             barcode - The copy barcode
136             force   - If true, copies in bad statuses will be checked in and give good statuses
137             noop    - don't capture holds or put items into transit
138             void_overdues - void all overdues for the circulation (aka amnesty)
139             ...
140     /
141 );
142
143 __PACKAGE__->register_method(
144     method    => "run_method",
145     api_name  => "open-ils.circ.checkin.override",
146     signature => q/@see open-ils.circ.checkin/
147 );
148
149 __PACKAGE__->register_method(
150     method    => "run_method",
151     api_name  => "open-ils.circ.renew.override",
152     signature => q/@see open-ils.circ.renew/,
153 );
154
155
156 __PACKAGE__->register_method(
157     method  => "run_method",
158     api_name    => "open-ils.circ.renew",
159     notes       => <<"    NOTES");
160     PARAMS( authtoken, circ => circ_id );
161     open-ils.circ.renew(login_session, circ_object);
162     Renews the provided circulation.  login_session is the requestor of the
163     renewal and if the logged in user is not the same as circ->usr, then
164     the logged in user must have RENEW_CIRC permissions.
165     NOTES
166
167 __PACKAGE__->register_method(
168     method   => "run_method",
169     api_name => "open-ils.circ.checkout.full"
170 );
171 __PACKAGE__->register_method(
172     method   => "run_method",
173     api_name => "open-ils.circ.checkout.full.override"
174 );
175 __PACKAGE__->register_method(
176     method   => "run_method",
177     api_name => "open-ils.circ.reservation.pickup"
178 );
179 __PACKAGE__->register_method(
180     method   => "run_method",
181     api_name => "open-ils.circ.reservation.return"
182 );
183 __PACKAGE__->register_method(
184     method   => "run_method",
185     api_name => "open-ils.circ.reservation.return.override"
186 );
187 __PACKAGE__->register_method(
188     method   => "run_method",
189     api_name => "open-ils.circ.checkout.inspect",
190     desc     => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
191 );
192
193
194 sub run_method {
195     my( $self, $conn, $auth, $args ) = @_;
196     translate_legacy_args($args);
197     $args->{override_args} = { all => 1 } unless defined $args->{override_args};
198     my $api = $self->api_name;
199
200     my $circulator = 
201         OpenILS::Application::Circ::Circulator->new($auth, %$args);
202
203     return circ_events($circulator) if $circulator->bail_out;
204
205     $circulator->use_booking(determine_booking_status());
206
207     # --------------------------------------------------------------------------
208     # First, check for a booking transit, as the barcode may not be a copy
209     # barcode, but a resource barcode, and nothing else in here will work
210     # --------------------------------------------------------------------------
211
212     if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
213         my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
214         if (@$resources) { # yes!
215
216             my $res_id_list = [ map { $_->id } @$resources ];
217             my $transit = $circulator->editor->search_action_reservation_transit_copy(
218                 [
219                     { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
220                     { order_by => { artc => 'source_send_time' }, limit => 1 }
221                 ]
222             )->[0]; # Any transit for this barcode?
223
224             if ($transit) { # yes! unwrap it.
225
226                 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
227                 my $res_type    = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
228
229                 my $success_event = new OpenILS::Event(
230                     "SUCCESS", "payload" => {"reservation" => $reservation}
231                 );
232                 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
233                     if (my $copy = $circulator->editor->search_asset_copy([
234                         { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
235                     ])->[0]) { # got a copy
236                         $copy->status( $transit->copy_status );
237                         $copy->editor($circulator->editor->requestor->id);
238                         $copy->edit_date('now');
239                         $circulator->editor->update_asset_copy($copy);
240                         $success_event->{"payload"}->{"record"} =
241                             $U->record_to_mvr($copy->call_number->record);
242                         $success_event->{"payload"}->{"volume"} = $copy->call_number;
243                         $copy->call_number($copy->call_number->id);
244                         $success_event->{"payload"}->{"copy"} = $copy;
245                     }
246                 }
247
248                 $transit->dest_recv_time('now');
249                 $circulator->editor->update_action_reservation_transit_copy( $transit );
250
251                 $circulator->editor->commit;
252                 # Formerly this branch just stopped here. Argh!
253                 $conn->respond_complete($success_event);
254                 return;
255             }
256         }
257     }
258             
259     
260
261     # --------------------------------------------------------------------------
262     # Go ahead and load the script runner to make sure we have all 
263     # of the objects we need
264     # --------------------------------------------------------------------------
265
266     if ($circulator->use_booking) {
267         $circulator->is_res_checkin($circulator->is_checkin(1))
268             if $api =~ /reservation.return/ or (
269                 $api =~ /checkin/ and $circulator->seems_like_reservation()
270             );
271
272         $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
273     }
274
275     $circulator->is_renewal(1) if $api =~ /renew/;
276     $circulator->is_checkin(1) if $api =~ /checkin/;
277
278     $circulator->mk_env();
279     $circulator->noop(1) if $circulator->claims_never_checked_out;
280
281     if($legacy_script_support and not $circulator->is_checkin) {
282         $circulator->mk_script_runner();
283         $circulator->legacy_script_support(1);
284         $circulator->circ_permit_patron($scripts{circ_permit_patron});
285         $circulator->circ_permit_copy($scripts{circ_permit_copy});      
286         $circulator->circ_duration($scripts{circ_duration});             
287         $circulator->circ_permit_renew($scripts{circ_permit_renew});
288     }
289     return circ_events($circulator) if $circulator->bail_out;
290
291     
292     $circulator->override(1) if $api =~ /override/o;
293
294     if( $api =~ /checkout\.permit/ ) {
295         $circulator->do_permit();
296
297     } elsif( $api =~ /checkout.full/ ) {
298
299         # requesting a precat checkout implies that any required
300         # overrides have been performed.  Go ahead and re-override.
301         $circulator->skip_permit_key(1);
302         $circulator->override(1) if $circulator->request_precat;
303         $circulator->do_permit();
304         $circulator->is_checkout(1);
305         unless( $circulator->bail_out ) {
306             $circulator->events([]);
307             $circulator->do_checkout();
308         }
309
310     } elsif( $circulator->is_res_checkout ) {
311         $circulator->do_reservation_pickup();
312
313     } elsif( $api =~ /inspect/ ) {
314         my $data = $circulator->do_inspect();
315         $circulator->editor->rollback;
316         return $data;
317
318     } elsif( $api =~ /checkout/ ) {
319         $circulator->is_checkout(1);
320         $circulator->do_checkout();
321
322     } elsif( $circulator->is_res_checkin ) {
323         $circulator->do_reservation_return();
324         $circulator->do_checkin() if ($circulator->copy());
325     } elsif( $api =~ /checkin/ ) {
326         $circulator->do_checkin();
327
328     } elsif( $api =~ /renew/ ) {
329         $circulator->is_renewal(1);
330         $circulator->do_renew();
331     }
332
333     if( $circulator->bail_out ) {
334
335         my @ee;
336         # make sure no success event accidentally slip in
337         $circulator->events(
338             [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
339
340         # Log the events
341         my @e = @{$circulator->events};
342         push( @ee, $_->{textcode} ) for @e;
343         $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
344
345         $circulator->editor->rollback;
346
347     } else {
348
349         $circulator->editor->commit;
350     }
351     
352     $conn->respond_complete(circ_events($circulator));
353
354     $circulator->script_runner->cleanup if $circulator->script_runner;
355
356     return undef if $circulator->bail_out;
357
358     $circulator->do_hold_notify($circulator->notify_hold)
359         if $circulator->notify_hold;
360     $circulator->retarget_holds if $circulator->retarget;
361     $circulator->append_reading_list;
362     $circulator->make_trigger_events;
363     
364     return undef;
365 }
366
367 sub circ_events {
368     my $circ = shift;
369     my @e = @{$circ->events};
370     # if we have multiple events, SUCCESS should not be one of them;
371     @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
372     return (@e == 1) ? $e[0] : \@e;
373 }
374
375
376 sub translate_legacy_args {
377     my $args = shift;
378
379     if( $$args{barcode} ) {
380         $$args{copy_barcode} = $$args{barcode};
381         delete $$args{barcode};
382     }
383
384     if( $$args{copyid} ) {
385         $$args{copy_id} = $$args{copyid};
386         delete $$args{copyid};
387     }
388
389     if( $$args{patronid} ) {
390         $$args{patron_id} = $$args{patronid};
391         delete $$args{patronid};
392     }
393
394     if( $$args{patron} and !ref($$args{patron}) ) {
395         $$args{patron_id} = $$args{patron};
396         delete $$args{patron};
397     }
398
399
400     if( $$args{noncat} ) {
401         $$args{is_noncat} = $$args{noncat};
402         delete $$args{noncat};
403     }
404
405     if( $$args{precat} ) {
406         $$args{is_precat} = $$args{request_precat} = $$args{precat};
407         delete $$args{precat};
408     }
409 }
410
411
412
413 # --------------------------------------------------------------------------
414 # This package actually manages all of the circulation logic
415 # --------------------------------------------------------------------------
416 package OpenILS::Application::Circ::Circulator;
417 use strict; use warnings;
418 use vars q/$AUTOLOAD/;
419 use DateTime;
420 use OpenILS::Utils::Fieldmapper;
421 use OpenSRF::Utils::Cache;
422 use Digest::MD5 qw(md5_hex);
423 use DateTime::Format::ISO8601;
424 use OpenILS::Utils::PermitHold;
425 use OpenSRF::Utils qw/:datetime/;
426 use OpenSRF::Utils::SettingsClient;
427 use OpenILS::Application::Circ::Holds;
428 use OpenILS::Application::Circ::Transit;
429 use OpenSRF::Utils::Logger qw(:logger);
430 use OpenILS::Utils::CStoreEditor qw/:funcs/;
431 use OpenILS::Application::Circ::ScriptBuilder;
432 use OpenILS::Const qw/:const/;
433 use OpenILS::Utils::Penalty;
434 use OpenILS::Application::Circ::CircCommon;
435 use Time::Local;
436
437 my $CC = "OpenILS::Application::Circ::CircCommon";
438 my $holdcode    = "OpenILS::Application::Circ::Holds";
439 my $transcode   = "OpenILS::Application::Circ::Transit";
440 my %user_groups;
441
442 sub DESTROY { }
443
444
445 # --------------------------------------------------------------------------
446 # Add a pile of automagic getter/setter methods
447 # --------------------------------------------------------------------------
448 my @AUTOLOAD_FIELDS = qw/
449     notify_hold
450     remote_hold
451     backdate
452     reservation
453     copy
454     copy_id
455     copy_barcode
456     patron
457     patron_id
458     patron_barcode
459     script_runner
460     volume
461     title
462     is_renewal
463     is_checkout
464     is_res_checkout
465     is_precat
466     is_noncat
467     request_precat
468     is_checkin
469     is_res_checkin
470     noncat_type
471     editor
472     events
473     cache_handle
474     override
475     circ_permit_patron
476     circ_permit_copy
477     circ_duration
478     circ_recurring_fines
479     circ_max_fines
480     circ_permit_renew
481     circ
482     transit
483     hold
484     permit_key
485     noncat_circ_lib
486     noncat_count
487     checkout_time
488     dummy_title
489     dummy_author
490     dummy_isbn
491     circ_modifier
492     circ_lib
493     barcode
494     duration_level
495     recurring_fines_level
496     duration_rule
497     recurring_fines_rule
498     max_fine_rule
499     renewal_remaining
500     hard_due_date
501     due_date
502     fulfilled_holds
503     transit
504     checkin_changed
505     force
506     permit_override
507     pending_checkouts
508     cancelled_hold_transit
509     opac_renewal
510     phone_renewal
511     desk_renewal
512     sip_renewal
513     retarget
514     matrix_test_result
515     circ_matrix_matchpoint
516     circ_test_success
517     legacy_script_support
518     is_deposit
519     is_rental
520     deposit_billing
521     rental_billing
522     capture
523     noop
524     void_overdues
525     parent_circ
526     return_patron
527     claims_never_checked_out
528     skip_permit_key
529     skip_deposit_fee
530     skip_rental_fee
531     use_booking
532     clear_expired
533     retarget_mode
534     hold_as_transit
535     fake_hold_dest
536     limit_groups
537     override_args
538     checkout_is_for_hold
539     manual_float
540     dont_change_lost_zero
541     lost_bill_options
542     needs_lost_bill_handling
543 /;
544
545
546 sub AUTOLOAD {
547     my $self = shift;
548     my $type = ref($self) or die "$self is not an object";
549     my $data = shift;
550     my $name = $AUTOLOAD;
551     $name =~ s/.*://o;   
552
553     unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
554         $logger->error("circulator: $type: invalid autoload field: $name");
555         die "$type: invalid autoload field: $name\n" 
556     }
557
558     {
559         no strict 'refs';
560         *{"${type}::${name}"} = sub {
561             my $s = shift;
562             my $v = shift;
563             $s->{$name} = $v if defined $v;
564             return $s->{$name};
565         }
566     }
567     return $self->$name($data);
568 }
569
570
571 sub new {
572     my( $class, $auth, %args ) = @_;
573     $class = ref($class) || $class;
574     my $self = bless( {}, $class );
575
576     $self->events([]);
577     $self->editor(new_editor(xact => 1, authtoken => $auth));
578
579     unless( $self->editor->checkauth ) {
580         $self->bail_on_events($self->editor->event);
581         return $self;
582     }
583
584     $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
585
586     $self->$_($args{$_}) for keys %args;
587
588     $self->circ_lib(
589         ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
590
591     # if this is a renewal, default to desk_renewal
592     $self->desk_renewal(1) unless 
593         $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
594
595     $self->capture('') unless $self->capture;
596
597     unless(%user_groups) {
598         my $gps = $self->editor->retrieve_all_permission_grp_tree;
599         %user_groups = map { $_->id => $_ } @$gps;
600     }
601
602     return $self;
603 }
604
605
606 # --------------------------------------------------------------------------
607 # True if we should discontinue processing
608 # --------------------------------------------------------------------------
609 sub bail_out {
610     my( $self, $bool ) = @_;
611     if( defined $bool ) {
612         $logger->info("circulator: BAILING OUT") if $bool;
613         $self->{bail_out} = $bool;
614     }
615     return $self->{bail_out};
616 }
617
618
619 sub push_events {
620     my( $self, @evts ) = @_;
621     for my $e (@evts) {
622         next unless $e;
623         $e->{payload} = $self->copy if 
624               ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
625
626         $logger->info("circulator: pushing event ".$e->{textcode});
627         push( @{$self->events}, $e ) unless
628             grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
629     }
630 }
631
632 sub mk_permit_key {
633     my $self = shift;
634     return '' if $self->skip_permit_key;
635     my $key = md5_hex( time() . rand() . "$$" );
636     $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
637     return $self->permit_key($key);
638 }
639
640 sub check_permit_key {
641     my $self = shift;
642     return 1 if $self->skip_permit_key;
643     my $key = $self->permit_key;
644     return 0 unless $key;
645     my $k = "oils_permit_key_$key";
646     my $one = $self->cache_handle->get_cache($k);
647     $self->cache_handle->delete_cache($k);
648     return ($one) ? 1 : 0;
649 }
650
651 sub seems_like_reservation {
652     my $self = shift;
653
654     # Some words about the following method:
655     # 1) It requires the VIEW_USER permission, but that's not an
656     # issue, right, since all staff should have that?
657     # 2) It returns only one reservation at a time, even if an item can be
658     # and is currently overbooked.  Hmmm....
659     my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
660     my $result = $booking_ses->request(
661         "open-ils.booking.reservations.by_returnable_resource_barcode",
662         $self->editor->authtoken,
663         $self->copy_barcode
664     )->gather(1);
665     $booking_ses->disconnect;
666
667     return $self->bail_on_events($result) if defined $U->event_code($result);
668
669     if (@$result > 0) {
670         $self->reservation(shift @$result);
671         return 1;
672     } else {
673         return 0;
674     }
675
676 }
677
678 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
679 sub save_trimmed_copy {
680     my ($self, $copy) = @_;
681
682     $self->copy($copy);
683     $self->volume($copy->call_number);
684     $self->title($self->volume->record);
685     $self->copy->call_number($self->volume->id);
686     $self->volume->record($self->title->id);
687     $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
688     if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
689         $self->is_deposit(1) if $U->is_true($self->copy->deposit);
690         $self->is_rental(1) unless $U->is_true($self->copy->deposit);
691     }
692 }
693
694 sub mk_env {
695     my $self = shift;
696     my $e = $self->editor;
697
698     # --------------------------------------------------------------------------
699     # Grab the fleshed copy
700     # --------------------------------------------------------------------------
701     unless($self->is_noncat) {
702         my $copy;
703         if($self->copy_id) {
704             $copy = $e->retrieve_asset_copy(
705                 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
706     
707         } elsif( $self->copy_barcode ) {
708     
709             $copy = $e->search_asset_copy(
710                 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
711         } elsif( $self->reservation ) {
712             my $res = $e->json_query(
713                 {
714                     "select" => {"acp" => ["id"]},
715                     "from" => {
716                         "acp" => {
717                             "brsrc" => {
718                                 "fkey" => "barcode",
719                                 "field" => "barcode",
720                                 "join" => {
721                                     "bresv" => {
722                                         "fkey" => "id",
723                                         "field" => "current_resource"
724                                     }
725                                 }
726                             }
727                         }
728                     },
729                     "where" => {
730                         "+bresv" => {
731                             "id" => (ref $self->reservation) ?
732                                 $self->reservation->id : $self->reservation
733                         }
734                     }
735                 }
736             );
737             if (ref $res eq "ARRAY" and scalar @$res) {
738                 $logger->info("circulator: mapped reservation " .
739                     $self->reservation . " to copy " . $res->[0]->{"id"});
740                 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
741             }
742         }
743     
744         if($copy) {
745             $self->save_trimmed_copy($copy);
746         } else {
747             # We can't renew if there is no copy
748             return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
749                 if $self->is_renewal;
750             $self->is_precat(1);
751         }
752     }
753
754     # --------------------------------------------------------------------------
755     # Grab the patron
756     # --------------------------------------------------------------------------
757     my $patron;
758     my $flesh = {
759         flesh => 1,
760         flesh_fields => {au => [ qw/ card / ]}
761     };
762
763     if( $self->patron_id ) {
764         $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
765             or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
766
767     } elsif( $self->patron_barcode ) {
768
769         # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
770         my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0] 
771             or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
772
773         $patron = $e->retrieve_actor_user($card->usr)
774             or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
775
776         # Use the card we looked up, not the patron's primary, for card active checks
777         $patron->card($card);
778
779     } else {
780         if( my $copy = $self->copy ) {
781
782             $flesh->{flesh} = 2;
783             $flesh->{flesh_fields}->{circ} = ['usr'];
784
785             my $circ = $e->search_action_circulation([
786                 {target_copy => $copy->id, checkin_time => undef}, $flesh
787             ])->[0];
788
789             if($circ) {
790                 $patron = $circ->usr;
791                 $circ->usr($patron->id); # de-flesh for consistency
792                 $self->circ($circ); 
793             }
794         }
795     }
796
797     return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
798         unless $self->patron($patron) or $self->is_checkin;
799
800     unless($self->is_checkin) {
801
802         # Check for inactivity and patron reg. expiration
803
804         $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
805             unless $U->is_true($patron->active);
806     
807         $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
808             unless $U->is_true($patron->card->active);
809     
810         my $expire = DateTime::Format::ISO8601->new->parse_datetime(
811             cleanse_ISO8601($patron->expire_date));
812     
813         $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
814             if( CORE::time > $expire->epoch ) ;
815     }
816 }
817
818 # --------------------------------------------------------------------------
819 # This builds the script runner environment and fetches most of the
820 # objects we need
821 # --------------------------------------------------------------------------
822 sub mk_script_runner {
823     my $self = shift;
824     my $args = {};
825
826
827     my @fields = 
828         qw/copy copy_barcode copy_id patron 
829             patron_id patron_barcode volume title editor/;
830
831     # Translate our objects into the ScriptBuilder args hash
832     $$args{$_} = $self->$_() for @fields;
833
834     $args->{ignore_user_status} = 1 if $self->is_checkin;
835     $$args{fetch_patron_by_circ_copy} = 1;
836     $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
837
838     if( my $pco = $self->pending_checkouts ) {
839         $logger->info("circulator: we were given a pending checkouts number of $pco");
840         $$args{patronItemsOut} = $pco;
841     }
842
843     # This fetches most of the objects we need
844     $self->script_runner(
845         OpenILS::Application::Circ::ScriptBuilder->build($args));
846
847     # Now we translate the ScriptBuilder objects back into self
848     $self->$_($$args{$_}) for @fields;
849
850     my @evts = @{$args->{_events}} if $args->{_events};
851
852     $logger->debug("circulator: script builder returned events: @evts") if @evts;
853
854
855     if(@evts) {
856         # Anything besides ASSET_COPY_NOT_FOUND will stop processing
857         if(!$self->is_noncat and 
858             @evts == 1 and 
859             $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
860                 $self->is_precat(1);
861
862         } else {
863             my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
864             return $self->bail_on_events(@e);
865         }
866     }
867
868     if($self->copy) {
869         $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
870         if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
871             $self->is_deposit(1) if $U->is_true($self->copy->deposit);
872             $self->is_rental(1) unless $U->is_true($self->copy->deposit);
873         }
874     }
875
876     # We can't renew if there is no copy
877     return $self->bail_on_events(@evts) if 
878         $self->is_renewal and !$self->copy;
879
880     # Set some circ-specific flags in the script environment
881     my $evt = "environment";
882     $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
883
884     if( $self->is_noncat ) {
885       $self->script_runner->insert("$evt.isNonCat", 1);
886       $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
887     }
888
889     if( $self->is_precat ) {
890         $self->script_runner->insert("environment.isPrecat", 1, 1);
891     }
892
893     $self->script_runner->add_path( $_ ) for @$script_libs;
894
895     return 1;
896 }
897
898 # --------------------------------------------------------------------------
899 # Does the circ permit work
900 # --------------------------------------------------------------------------
901 sub do_permit {
902     my $self = shift;
903
904     $self->log_me("do_permit()");
905
906     unless( $self->editor->requestor->id == $self->patron->id ) {
907         return $self->bail_on_events($self->editor->event)
908             unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
909     }
910
911     $self->check_captured_holds();
912     $self->do_copy_checks();
913     return if $self->bail_out;
914     $self->run_patron_permit_scripts();
915     $self->run_copy_permit_scripts() 
916         unless $self->is_precat or $self->is_noncat;
917     $self->check_item_deposit_events();
918     $self->override_events();
919     return if $self->bail_out;
920
921     if($self->is_precat and not $self->request_precat) {
922         $self->push_events(
923             OpenILS::Event->new(
924                 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
925         return $self->bail_out(1) unless $self->is_renewal;
926     }
927
928     $self->push_events(
929         OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
930 }
931
932 sub check_item_deposit_events {
933     my $self = shift;
934     $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy)) 
935         if $self->is_deposit and not $self->is_deposit_exempt;
936     $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy)) 
937         if $self->is_rental and not $self->is_rental_exempt;
938 }
939
940 # returns true if the user is not required to pay deposits
941 sub is_deposit_exempt {
942     my $self = shift;
943     my $pid = (ref $self->patron->profile) ?
944         $self->patron->profile->id : $self->patron->profile;
945     my $groups = $U->ou_ancestor_setting_value(
946         $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
947     for my $grp (@$groups) {
948         return 1 if $self->is_group_descendant($grp, $pid);
949     }
950     return 0;
951 }
952
953 # returns true if the user is not required to pay rental fees
954 sub is_rental_exempt {
955     my $self = shift;
956     my $pid = (ref $self->patron->profile) ?
957         $self->patron->profile->id : $self->patron->profile;
958     my $groups = $U->ou_ancestor_setting_value(
959         $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
960     for my $grp (@$groups) {
961         return 1 if $self->is_group_descendant($grp, $pid);
962     }
963     return 0;
964 }
965
966 sub is_group_descendant {
967     my($self, $p_id, $c_id) = @_;
968     return 0 unless defined $p_id and defined $c_id;
969     return 1 if $c_id == $p_id;
970     while(my $grp = $user_groups{$c_id}) {
971         $c_id = $grp->parent;
972         return 0 unless defined $c_id;
973         return 1 if $c_id == $p_id;
974     }
975     return 0;
976 }
977
978 sub check_captured_holds {
979     my $self    = shift;
980     my $copy    = $self->copy;
981     my $patron  = $self->patron;
982
983     return undef unless $copy;
984
985     my $s = $U->copy_status($copy->status)->id;
986     return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
987     $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
988
989     # Item is on the holds shelf, make sure it's going to the right person
990     my $hold = $self->editor->search_action_hold_request(
991         [
992             { 
993                 current_copy        => $copy->id , 
994                 capture_time        => { '!=' => undef },
995                 cancel_time         => undef, 
996                 fulfillment_time    => undef 
997             },
998             { limit => 1 }
999         ]
1000     )->[0];
1001
1002     if ($hold and $hold->usr == $patron->id) {
1003         $self->checkout_is_for_hold(1);
1004         return undef;
1005     }
1006
1007     $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1008
1009     $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1010 }
1011
1012
1013 sub do_copy_checks {
1014     my $self = shift;
1015     my $copy = $self->copy;
1016     return unless $copy;
1017
1018     my $stat = $U->copy_status($copy->status)->id;
1019
1020     # We cannot check out a copy if it is in-transit
1021     if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1022         return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1023     }
1024
1025     $self->handle_claims_returned();
1026     return if $self->bail_out;
1027
1028     # no claims returned circ was found, check if there is any open circ
1029     unless( $self->is_renewal ) {
1030
1031         my $circs = $self->editor->search_action_circulation(
1032             { target_copy => $copy->id, checkin_time => undef }
1033         );
1034
1035         if(my $old_circ = $circs->[0]) { # an open circ was found
1036
1037             my $payload = {copy => $copy};
1038
1039             if($old_circ->usr == $self->patron->id) {
1040                 
1041                 $payload->{old_circ} = $old_circ;
1042
1043                 # If there is an open circulation on the checkout item and an auto-renew 
1044                 # interval is defined, inform the caller that they should go 
1045                 # ahead and renew the item instead of warning about open circulations.
1046     
1047                 my $auto_renew_intvl = $U->ou_ancestor_setting_value(        
1048                     $self->circ_lib,
1049                     'circ.checkout_auto_renew_age', 
1050                     $self->editor
1051                 );
1052
1053                 if($auto_renew_intvl) {
1054                     my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1055                     my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1056
1057                     if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1058                         $payload->{auto_renew} = 1;
1059                     }
1060                 }
1061             }
1062
1063             return $self->bail_on_events(
1064                 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1065             );
1066         }
1067     }
1068 }
1069
1070 my $LEGACY_CIRC_EVENT_MAP = {
1071     'no_item' => 'ITEM_NOT_CATALOGED',
1072     'actor.usr.barred' => 'PATRON_BARRED',
1073     'asset.copy.circulate' =>  'COPY_CIRC_NOT_ALLOWED',
1074     'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1075     'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1076     'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1077     'config.circ_matrix_test.max_items_out' =>  'PATRON_EXCEEDS_CHECKOUT_COUNT',
1078     'config.circ_matrix_test.max_overdue' =>  'PATRON_EXCEEDS_OVERDUE_COUNT',
1079     'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1080     'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1081     'config.circ_matrix_test.total_copy_hold_ratio' => 
1082         'TOTAL_HOLD_COPY_RATIO_EXCEEDED',
1083     'config.circ_matrix_test.available_copy_hold_ratio' => 
1084         'AVAIL_HOLD_COPY_RATIO_EXCEEDED'
1085 };
1086
1087
1088 # ---------------------------------------------------------------------
1089 # This pushes any patron-related events into the list but does not
1090 # set bail_out for any events
1091 # ---------------------------------------------------------------------
1092 sub run_patron_permit_scripts {
1093     my $self        = shift;
1094     my $runner      = $self->script_runner;
1095     my $patronid    = $self->patron->id;
1096
1097     my @allevents; 
1098
1099     if(!$self->legacy_script_support) {
1100
1101         my $results = $self->run_indb_circ_test;
1102         unless($self->circ_test_success) {
1103             my @trimmed_results;
1104
1105             if ($self->is_noncat) {
1106                 # no_item result is OK during noncat checkout
1107                 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
1108
1109             } else {
1110
1111                 if ($self->checkout_is_for_hold) {
1112                     # if this checkout will fulfill a hold, ignore CIRC blocks
1113                     # and rely instead on the (later-checked) FULFILL block
1114
1115                     my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
1116                     my $fblock_pens = $self->editor->search_config_standing_penalty(
1117                         {name => [@pen_names], block_list => {like => '%CIRC%'}});
1118
1119                     for my $res (@$results) {
1120                         my $name = $res->{fail_part} || '';
1121                         next if grep {$_->name eq $name} @$fblock_pens;
1122                         push(@trimmed_results, $res);
1123                     }
1124
1125                 } else { 
1126                     # not for hold or noncat
1127                     @trimmed_results = @$results;
1128                 }
1129             }
1130
1131             # update the final set of test results
1132             $self->matrix_test_result(\@trimmed_results); 
1133
1134             push @allevents, $self->matrix_test_result_events;
1135         }
1136
1137     } else {
1138
1139         # --------------------------------------------------------------------- 
1140         # # Now run the patron permit script 
1141         # ---------------------------------------------------------------------
1142         $runner->load($self->circ_permit_patron);
1143         my $result = $runner->run or 
1144             throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1145
1146         my $patron_events = $result->{events};
1147
1148         OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1149         my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1150         my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1151         $penalties = $penalties->{fatal_penalties};
1152
1153         for my $pen (@$penalties) {
1154             # CIRC blocks are ignored if this is a FULFILL scenario
1155             next if $mask eq 'CIRC' and $self->checkout_is_for_hold;
1156             my $event = OpenILS::Event->new($pen->name);
1157             $event->{desc} = $pen->label;
1158             push(@allevents, $event);
1159         }
1160
1161         push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1162     }
1163
1164     for (@allevents) {
1165        $_->{payload} = $self->copy if 
1166              ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1167     }
1168
1169     $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1170
1171     $self->push_events(@allevents);
1172 }
1173
1174 sub matrix_test_result_codes {
1175     my $self = shift;
1176     map { $_->{"fail_part"} } @{$self->matrix_test_result};
1177 }
1178
1179 sub matrix_test_result_events {
1180     my $self = shift;
1181     map {
1182         my $event = new OpenILS::Event(
1183             $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1184         );
1185         $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1186         $event;
1187     } (@{$self->matrix_test_result});
1188 }
1189
1190 sub run_indb_circ_test {
1191     my $self = shift;
1192     return $self->matrix_test_result if $self->matrix_test_result;
1193
1194     my $dbfunc = ($self->is_renewal) ? 
1195         'action.item_user_renew_test' : 'action.item_user_circ_test';
1196
1197     if( $self->is_precat && $self->request_precat) {
1198         $self->make_precat_copy;
1199         return if $self->bail_out;
1200     }
1201
1202     my $results = $self->editor->json_query(
1203         {   from => [
1204                 $dbfunc,
1205                 $self->circ_lib,
1206                 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id, 
1207                 $self->patron->id,
1208             ]
1209         }
1210     );
1211
1212     $self->circ_test_success($U->is_true($results->[0]->{success}));
1213
1214     if(my $mp = $results->[0]->{matchpoint}) {
1215         $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1216         $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1217         $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1218         if(defined($results->[0]->{renewals})) {
1219             $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1220         }
1221         $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1222         if(defined($results->[0]->{grace_period})) {
1223             $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1224         }
1225         $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1226         $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1227         # Grab the *last* response for limit_groups, where it is more likely to be filled
1228         $self->limit_groups($results->[-1]->{limit_groups});
1229     }
1230
1231     return $self->matrix_test_result($results);
1232 }
1233
1234 # ---------------------------------------------------------------------
1235 # given a use and copy, this will calculate the circulation policy
1236 # parameters.  Only works with in-db circ.
1237 # ---------------------------------------------------------------------
1238 sub do_inspect {
1239     my $self = shift;
1240
1241     return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1242
1243     $self->run_indb_circ_test;
1244
1245     my $results = {
1246         circ_test_success => $self->circ_test_success,
1247         failure_events => [],
1248         failure_codes => [],
1249         matchpoint => $self->circ_matrix_matchpoint
1250     };
1251
1252     unless($self->circ_test_success) {
1253         $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1254         $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1255     }
1256
1257     if($self->circ_matrix_matchpoint) {
1258         my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1259         my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1260         my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1261         my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1262     
1263         my $policy = $self->get_circ_policy(
1264             $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1265     
1266         $$results{$_} = $$policy{$_} for keys %$policy;
1267     }
1268
1269     return $results;
1270 }
1271
1272 # ---------------------------------------------------------------------
1273 # Loads the circ policy info for duration, recurring fine, and max
1274 # fine based on the current copy
1275 # ---------------------------------------------------------------------
1276 sub get_circ_policy {
1277     my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1278
1279     my $policy = {
1280         duration_rule => $duration_rule->name,
1281         recurring_fine_rule => $recurring_fine_rule->name,
1282         max_fine_rule => $max_fine_rule->name,
1283         max_fine => $self->get_max_fine_amount($max_fine_rule),
1284         fine_interval => $recurring_fine_rule->recurrence_interval,
1285         renewal_remaining => $duration_rule->max_renewals,
1286         grace_period => $recurring_fine_rule->grace_period
1287     };
1288
1289     if($hard_due_date) {
1290         $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1291         $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1292     }
1293     else {
1294         $policy->{duration_date_ceiling} = undef;
1295         $policy->{duration_date_ceiling_force} = undef;
1296     }
1297
1298     $policy->{duration} = $duration_rule->shrt
1299         if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1300     $policy->{duration} = $duration_rule->normal
1301         if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1302     $policy->{duration} = $duration_rule->extended
1303         if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1304
1305     $policy->{recurring_fine} = $recurring_fine_rule->low
1306         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1307     $policy->{recurring_fine} = $recurring_fine_rule->normal
1308         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1309     $policy->{recurring_fine} = $recurring_fine_rule->high
1310         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1311
1312     return $policy;
1313 }
1314
1315 sub get_max_fine_amount {
1316     my $self = shift;
1317     my $max_fine_rule = shift;
1318     my $max_amount = $max_fine_rule->amount;
1319
1320     # if is_percent is true then the max->amount is
1321     # use as a percentage of the copy price
1322     if ($U->is_true($max_fine_rule->is_percent)) {
1323         my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1324         $max_amount = $price * $max_fine_rule->amount / 100;
1325     } elsif (
1326         $U->ou_ancestor_setting_value(
1327             $self->circ_lib,
1328             'circ.max_fine.cap_at_price',
1329             $self->editor
1330         )
1331     ) {
1332         my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1333         $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1334     }
1335
1336     return $max_amount;
1337 }
1338
1339
1340
1341 sub run_copy_permit_scripts {
1342     my $self = shift;
1343     my $copy = $self->copy || return;
1344     my $runner = $self->script_runner;
1345
1346     my @allevents;
1347
1348     if(!$self->legacy_script_support) {
1349         my $results = $self->run_indb_circ_test;
1350         push @allevents, $self->matrix_test_result_events
1351             unless $self->circ_test_success;
1352     } else {
1353     
1354        # ---------------------------------------------------------------------
1355        # Capture all of the copy permit events
1356        # ---------------------------------------------------------------------
1357        $runner->load($self->circ_permit_copy);
1358        my $result = $runner->run or 
1359             throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1360        my $copy_events = $result->{events};
1361
1362        # ---------------------------------------------------------------------
1363        # Now collect all of the events together
1364        # ---------------------------------------------------------------------
1365        push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1366     }
1367
1368     # See if this copy has an alert message
1369     my $ae = $self->check_copy_alert();
1370     push( @allevents, $ae ) if $ae;
1371
1372    # uniquify the events
1373    my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1374    @allevents = values %hash;
1375
1376     $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1377
1378     $self->push_events(@allevents);
1379 }
1380
1381
1382 sub check_copy_alert {
1383     my $self = shift;
1384     return undef if $self->is_renewal;
1385     return OpenILS::Event->new(
1386         'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1387         if $self->copy and $self->copy->alert_message;
1388     return undef;
1389 }
1390
1391
1392
1393 # --------------------------------------------------------------------------
1394 # If the call is overriding and has permissions to override every collected
1395 # event, the are cleared.  Any event that the caller does not have
1396 # permission to override, will be left in the event list and bail_out will
1397 # be set
1398 # XXX We need code in here to cancel any holds/transits on copies 
1399 # that are being force-checked out
1400 # --------------------------------------------------------------------------
1401 sub override_events {
1402     my $self = shift;
1403     my @events = @{$self->events};
1404     return unless @events;
1405     my $oargs = $self->override_args;
1406
1407     if(!$self->override) {
1408         return $self->bail_out(1) 
1409             if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1410     }   
1411
1412     $self->events([]);
1413     
1414     for my $e (@events) {
1415         my $tc = $e->{textcode};
1416         next if $tc eq 'SUCCESS';
1417         if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1418             my $ov = "$tc.override";
1419             $logger->info("circulator: attempting to override event: $ov");
1420
1421             return $self->bail_on_events($self->editor->event)
1422                 unless( $self->editor->allowed($ov) );
1423         } else {
1424             return $self->bail_out(1);
1425         }
1426    }
1427 }
1428     
1429
1430 # --------------------------------------------------------------------------
1431 # If there is an open claimsreturn circ on the requested copy, close the 
1432 # circ if overriding, otherwise bail out
1433 # --------------------------------------------------------------------------
1434 sub handle_claims_returned {
1435     my $self = shift;
1436     my $copy = $self->copy;
1437
1438     my $CR = $self->editor->search_action_circulation(
1439         {   
1440             target_copy     => $copy->id,
1441             stop_fines      => OILS_STOP_FINES_CLAIMSRETURNED,
1442             checkin_time    => undef,
1443         }
1444     );
1445
1446     return unless ($CR = $CR->[0]); 
1447
1448     my $evt;
1449
1450     # - If the caller has set the override flag, we will check the item in
1451     if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1452
1453         $CR->checkin_time('now');   
1454         $CR->checkin_scan_time('now');   
1455         $CR->checkin_lib($self->circ_lib);
1456         $CR->checkin_workstation($self->editor->requestor->wsid);
1457         $CR->checkin_staff($self->editor->requestor->id);
1458
1459         $evt = $self->editor->event 
1460             unless $self->editor->update_action_circulation($CR);
1461
1462     } else {
1463         $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1464     }
1465
1466     $self->bail_on_events($evt) if $evt;
1467     return;
1468 }
1469
1470
1471 # --------------------------------------------------------------------------
1472 # This performs the checkout
1473 # --------------------------------------------------------------------------
1474 sub do_checkout {
1475     my $self = shift;
1476
1477     $self->log_me("do_checkout()");
1478
1479     # make sure perms are good if this isn't a renewal
1480     unless( $self->is_renewal ) {
1481         return $self->bail_on_events($self->editor->event)
1482             unless( $self->editor->allowed('COPY_CHECKOUT') );
1483     }
1484
1485     # verify the permit key
1486     unless( $self->check_permit_key ) {
1487         if( $self->permit_override ) {
1488             return $self->bail_on_events($self->editor->event)
1489                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1490         } else {
1491             return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1492         }   
1493     }
1494
1495     # if this is a non-cataloged circ, build the circ and finish
1496     if( $self->is_noncat ) {
1497         $self->checkout_noncat;
1498         $self->push_events(
1499             OpenILS::Event->new('SUCCESS', 
1500             payload => { noncat_circ => $self->circ }));
1501         return;
1502     }
1503
1504     if( $self->is_precat ) {
1505         $self->make_precat_copy;
1506         return if $self->bail_out;
1507
1508     } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1509         return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1510     }
1511
1512     $self->do_copy_checks;
1513     return if $self->bail_out;
1514
1515     $self->run_checkout_scripts();
1516     return if $self->bail_out;
1517
1518     $self->build_checkout_circ_object();
1519     return if $self->bail_out;
1520
1521     my $modify_to_start = $self->booking_adjusted_due_date();
1522     return if $self->bail_out;
1523
1524     $self->apply_modified_due_date($modify_to_start);
1525     return if $self->bail_out;
1526
1527     return $self->bail_on_events($self->editor->event)
1528         unless $self->editor->create_action_circulation($self->circ);
1529
1530     # refresh the circ to force local time zone for now
1531     $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1532
1533     if($self->limit_groups) {
1534         $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1535     }
1536
1537     $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1538     $self->update_copy;
1539     return if $self->bail_out;
1540
1541     $self->apply_deposit_fee();
1542     return if $self->bail_out;
1543
1544     $self->handle_checkout_holds();
1545     return if $self->bail_out;
1546
1547     # ------------------------------------------------------------------------------
1548     # Update the patron penalty info in the DB.  Run it for permit-overrides 
1549     # since the penalties are not updated during the permit phase
1550     # ------------------------------------------------------------------------------
1551     OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1552
1553     my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1554     
1555     my $pcirc;
1556     if($self->is_renewal) {
1557         # flesh the billing summary for the checked-in circ
1558         $pcirc = $self->editor->retrieve_action_circulation([
1559             $self->parent_circ,
1560             {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1561         ]);
1562     }
1563
1564     $self->push_events(
1565         OpenILS::Event->new('SUCCESS',
1566             payload  => {
1567                 copy             => $U->unflesh_copy($self->copy),
1568                 volume           => $self->volume,
1569                 circ             => $self->circ,
1570                 record           => $record,
1571                 holds_fulfilled  => $self->fulfilled_holds,
1572                 deposit_billing  => $self->deposit_billing,
1573                 rental_billing   => $self->rental_billing,
1574                 parent_circ      => $pcirc,
1575                 patron           => ($self->return_patron) ? $self->patron : undef,
1576                 patron_money     => $self->editor->retrieve_money_user_summary($self->patron->id)
1577             }
1578         )
1579     );
1580 }
1581
1582 sub apply_deposit_fee {
1583     my $self = shift;
1584     my $copy = $self->copy;
1585     return unless 
1586         ($self->is_deposit and not $self->is_deposit_exempt) or 
1587         ($self->is_rental and not $self->is_rental_exempt);
1588
1589     return if $self->is_deposit and $self->skip_deposit_fee;
1590     return if $self->is_rental and $self->skip_rental_fee;
1591
1592     my $bill = Fieldmapper::money::billing->new;
1593     my $amount = $copy->deposit_amount;
1594     my $billing_type;
1595     my $btype;
1596
1597     if($self->is_deposit) {
1598         $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1599         $btype = 5;
1600         $self->deposit_billing($bill);
1601     } else {
1602         $billing_type = OILS_BILLING_TYPE_RENTAL;
1603         $btype = 6;
1604         $self->rental_billing($bill);
1605     }
1606
1607     $bill->xact($self->circ->id);
1608     $bill->amount($amount);
1609     $bill->note(OILS_BILLING_NOTE_SYSTEM);
1610     $bill->billing_type($billing_type);
1611     $bill->btype($btype);
1612     $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1613
1614     $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1615 }
1616
1617 sub update_copy {
1618     my $self = shift;
1619     my $copy = $self->copy;
1620
1621     my $stat = $copy->status if ref $copy->status;
1622     my $loc = $copy->location if ref $copy->location;
1623     my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1624
1625     $copy->status($stat->id) if $stat;
1626     $copy->location($loc->id) if $loc;
1627     $copy->circ_lib($circ_lib->id) if $circ_lib;
1628     $copy->editor($self->editor->requestor->id);
1629     $copy->edit_date('now');
1630     $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1631
1632     return $self->bail_on_events($self->editor->event)
1633         unless $self->editor->update_asset_copy($self->copy);
1634
1635     $copy->status($U->copy_status($copy->status));
1636     $copy->location($loc) if $loc;
1637     $copy->circ_lib($circ_lib) if $circ_lib;
1638 }
1639
1640 sub update_reservation {
1641     my $self = shift;
1642     my $reservation = $self->reservation;
1643
1644     my $usr = $reservation->usr;
1645     my $target_rt = $reservation->target_resource_type;
1646     my $target_r = $reservation->target_resource;
1647     my $current_r = $reservation->current_resource;
1648
1649     $reservation->usr($usr->id) if ref $usr;
1650     $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1651     $reservation->target_resource($target_r->id) if ref $target_r;
1652     $reservation->current_resource($current_r->id) if ref $current_r;
1653
1654     return $self->bail_on_events($self->editor->event)
1655         unless $self->editor->update_booking_reservation($self->reservation);
1656
1657     my $evt;
1658     ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1659     $self->reservation($reservation);
1660 }
1661
1662
1663 sub bail_on_events {
1664     my( $self, @evts ) = @_;
1665     $self->push_events(@evts);
1666     $self->bail_out(1);
1667 }
1668
1669 # ------------------------------------------------------------------------------
1670 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1671 # affects copies that will fulfill holds and CIRC affects all other copies.
1672 # If blocks exists, bail, push Events onto the event pile, and return true.
1673 # ------------------------------------------------------------------------------
1674 sub check_hold_fulfill_blocks {
1675     my $self = shift;
1676
1677     # See if the user has any penalties applied that prevent hold fulfillment
1678     my $pens = $self->editor->json_query({
1679         select => {csp => ['name', 'label']},
1680         from => {ausp => {csp => {}}},
1681         where => {
1682             '+ausp' => {
1683                 usr => $self->patron->id,
1684                 org_unit => $U->get_org_full_path($self->circ_lib),
1685                 '-or' => [
1686                     {stop_date => undef},
1687                     {stop_date => {'>' => 'now'}}
1688                 ]
1689             },
1690             '+csp' => {block_list => {'like' => '%FULFILL%'}}
1691         }
1692     });
1693
1694     return 0 unless @$pens;
1695
1696     for my $pen (@$pens) {
1697         $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1698         my $event = OpenILS::Event->new($pen->{name});
1699         $event->{desc} = $pen->{label};
1700         $self->push_events($event);
1701     }
1702
1703     $self->override_events;
1704     return $self->bail_out;
1705 }
1706
1707
1708 # ------------------------------------------------------------------------------
1709 # When an item is checked out, see if we can fulfill a hold for this patron
1710 # ------------------------------------------------------------------------------
1711 sub handle_checkout_holds {
1712    my $self    = shift;
1713    my $copy    = $self->copy;
1714    my $patron  = $self->patron;
1715
1716    my $e = $self->editor;
1717    $self->fulfilled_holds([]);
1718
1719    # non-cats can't fulfill a hold
1720    return if $self->is_noncat;
1721
1722     my $hold = $e->search_action_hold_request({   
1723         current_copy        => $copy->id , 
1724         cancel_time         => undef, 
1725         fulfillment_time    => undef,
1726         '-or' => [
1727             {expire_time => undef},
1728             {expire_time => {'>' => 'now'}}
1729         ]
1730     })->[0];
1731
1732     if($hold and $hold->usr != $patron->id) {
1733         # reset the hold since the copy is now checked out
1734     
1735         $logger->info("circulator: un-targeting hold ".$hold->id.
1736             " because copy ".$copy->id." is getting checked out");
1737
1738         $hold->clear_prev_check_time; 
1739         $hold->clear_current_copy;
1740         $hold->clear_capture_time;
1741         $hold->clear_shelf_time;
1742         $hold->clear_shelf_expire_time;
1743         $hold->clear_current_shelf_lib;
1744
1745         return $self->bail_on_event($e->event)
1746             unless $e->update_action_hold_request($hold);
1747
1748         $hold = undef;
1749     }
1750
1751     unless($hold) {
1752         $hold = $self->find_related_user_hold($copy, $patron) or return;
1753         $logger->info("circulator: found related hold to fulfill in checkout");
1754     }
1755
1756     return if $self->check_hold_fulfill_blocks;
1757
1758     $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1759
1760     # if the hold was never officially captured, capture it.
1761     $hold->current_copy($copy->id);
1762     $hold->capture_time('now') unless $hold->capture_time;
1763     $hold->fulfillment_time('now');
1764     $hold->fulfillment_staff($e->requestor->id);
1765     $hold->fulfillment_lib($self->circ_lib);
1766
1767     return $self->bail_on_events($e->event)
1768         unless $e->update_action_hold_request($hold);
1769
1770     return $self->fulfilled_holds([$hold->id]);
1771 }
1772
1773
1774 # ------------------------------------------------------------------------------
1775 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1776 # the patron directly targets the checked out item, see if there is another hold 
1777 # for the patron that could be fulfilled by the checked out item.  Fulfill the
1778 # oldest hold and only fulfill 1 of them.
1779
1780 # For "another hold":
1781 #
1782 # First, check for one that the copy matches via hold_copy_map, ensuring that
1783 # *any* hold type that this copy could fill may end up filled.
1784 #
1785 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1786 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1787 # that are non-requestable to count as capturing those hold types.
1788 # ------------------------------------------------------------------------------
1789 sub find_related_user_hold {
1790     my($self, $copy, $patron) = @_;
1791     my $e = $self->editor;
1792
1793     # holds on precat copies are always copy-level, so this call will
1794     # always return undef.  Exit early.
1795     return undef if $self->is_precat;
1796
1797     return undef unless $U->ou_ancestor_setting_value(        
1798         $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1799
1800     # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1801     my $args = {
1802         select => {ahr => ['id']}, 
1803         from => {
1804             ahr => {
1805                 ahcm => {
1806                     field => 'hold',
1807                     fkey => 'id'
1808                 },
1809                 acp => {
1810                     field => 'id', 
1811                     fkey => 'current_copy',
1812                     type => 'left' # there may be no current_copy
1813                 }
1814             }
1815         }, 
1816         where => {
1817             '+ahr' => {
1818                 usr => $patron->id,
1819                 fulfillment_time => undef,
1820                 cancel_time => undef,
1821                '-or' => [
1822                     {expire_time => undef},
1823                     {expire_time => {'>' => 'now'}}
1824                 ]
1825             },
1826             '+ahcm' => {
1827                 target_copy => $self->copy->id
1828             },
1829             '+acp' => {
1830                 '-or' => [
1831                     {id => undef}, # left-join copy may be nonexistent
1832                     {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1833                 ]
1834             }
1835         },
1836         order_by => {ahr => {request_time => {direction => 'asc'}}},
1837         limit => 1
1838     };
1839
1840     my $hold_info = $e->json_query($args)->[0];
1841     return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1842     return undef if $U->ou_ancestor_setting_value(        
1843         $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1844
1845     # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1846     $args = {
1847         select => {ahr => ['id']}, 
1848         from => {
1849             ahr => {
1850                 acp => {
1851                     field => 'id', 
1852                     fkey => 'current_copy',
1853                     type => 'left' # there may be no current_copy
1854                 }
1855             }
1856         }, 
1857         where => {
1858             '+ahr' => {
1859                 usr => $patron->id,
1860                 fulfillment_time => undef,
1861                 cancel_time => undef,
1862                '-or' => [
1863                     {expire_time => undef},
1864                     {expire_time => {'>' => 'now'}}
1865                 ]
1866             },
1867             '-or' => [
1868                 {
1869                     '+ahr' => { 
1870                         hold_type => 'V',
1871                         target => $self->volume->id
1872                     }
1873                 },
1874                 { 
1875                     '+ahr' => { 
1876                         hold_type => 'T',
1877                         target => $self->title->id
1878                     }
1879                 },
1880             ],
1881             '+acp' => {
1882                 '-or' => [
1883                     {id => undef}, # left-join copy may be nonexistent
1884                     {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1885                 ]
1886             }
1887         },
1888         order_by => {ahr => {request_time => {direction => 'asc'}}},
1889         limit => 1
1890     };
1891
1892     $hold_info = $e->json_query($args)->[0];
1893     return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1894     return undef;
1895 }
1896
1897
1898 sub run_checkout_scripts {
1899     my $self = shift;
1900     my $nobail = shift;
1901
1902     my $evt;
1903     my $runner = $self->script_runner;
1904
1905     my $duration;
1906     my $recurring;
1907     my $max_fine;
1908     my $hard_due_date;
1909     my $duration_name;
1910     my $recurring_name;
1911     my $max_fine_name;
1912     my $hard_due_date_name;
1913
1914     if(!$self->legacy_script_support) {
1915         $self->run_indb_circ_test();
1916         $duration = $self->circ_matrix_matchpoint->duration_rule;
1917         $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1918         $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1919         $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1920
1921     } else {
1922
1923        $runner->load($self->circ_duration);
1924
1925        my $result = $runner->run or 
1926             throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1927
1928        $duration_name   = $result->{durationRule};
1929        $recurring_name  = $result->{recurringFinesRule};
1930        $max_fine_name   = $result->{maxFine};
1931        $hard_due_date_name  = $result->{hardDueDate};
1932     }
1933
1934     $duration_name = $duration->name if $duration;
1935     if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1936
1937         unless($duration) {
1938             ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1939             return $self->bail_on_events($evt) if ($evt && !$nobail);
1940         
1941             ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1942             return $self->bail_on_events($evt) if ($evt && !$nobail);
1943         
1944             ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1945             return $self->bail_on_events($evt) if ($evt && !$nobail);
1946
1947             if($hard_due_date_name) {
1948                 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1949                 return $self->bail_on_events($evt) if ($evt && !$nobail);
1950             }
1951         }
1952
1953     } else {
1954
1955         # The item circulates with an unlimited duration
1956         $duration   = undef;
1957         $recurring  = undef;
1958         $max_fine   = undef;
1959         $hard_due_date = undef;
1960     }
1961
1962    $self->duration_rule($duration);
1963    $self->recurring_fines_rule($recurring);
1964    $self->max_fine_rule($max_fine);
1965    $self->hard_due_date($hard_due_date);
1966 }
1967
1968
1969 sub build_checkout_circ_object {
1970     my $self = shift;
1971
1972    my $circ       = Fieldmapper::action::circulation->new;
1973    my $duration   = $self->duration_rule;
1974    my $max        = $self->max_fine_rule;
1975    my $recurring  = $self->recurring_fines_rule;
1976    my $hard_due_date    = $self->hard_due_date;
1977    my $copy       = $self->copy;
1978    my $patron     = $self->patron;
1979    my $duration_date_ceiling;
1980    my $duration_date_ceiling_force;
1981
1982     if( $duration ) {
1983
1984         my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1985         $duration_date_ceiling = $policy->{duration_date_ceiling};
1986         $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1987
1988         my $dname = $duration->name;
1989         my $mname = $max->name;
1990         my $rname = $recurring->name;
1991         my $hdname = ''; 
1992         if($hard_due_date) {
1993             $hdname = $hard_due_date->name;
1994         }
1995
1996         $logger->debug("circulator: building circulation ".
1997             "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1998     
1999         $circ->duration($policy->{duration});
2000         $circ->recurring_fine($policy->{recurring_fine});
2001         $circ->duration_rule($duration->name);
2002         $circ->recurring_fine_rule($recurring->name);
2003         $circ->max_fine_rule($max->name);
2004         $circ->max_fine($policy->{max_fine});
2005         $circ->fine_interval($recurring->recurrence_interval);
2006         $circ->renewal_remaining($duration->max_renewals);
2007         $circ->grace_period($policy->{grace_period});
2008
2009     } else {
2010
2011         $logger->info("circulator: copy found with an unlimited circ duration");
2012         $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2013         $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2014         $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2015         $circ->renewal_remaining(0);
2016         $circ->grace_period(0);
2017     }
2018
2019    $circ->target_copy( $copy->id );
2020    $circ->usr( $patron->id );
2021    $circ->circ_lib( $self->circ_lib );
2022    $circ->workstation($self->editor->requestor->wsid) 
2023     if defined $self->editor->requestor->wsid;
2024
2025     # renewals maintain a link to the parent circulation
2026     $circ->parent_circ($self->parent_circ);
2027
2028    if( $self->is_renewal ) {
2029       $circ->opac_renewal('t') if $self->opac_renewal;
2030       $circ->phone_renewal('t') if $self->phone_renewal;
2031       $circ->desk_renewal('t') if $self->desk_renewal;
2032       $circ->renewal_remaining($self->renewal_remaining);
2033       $circ->circ_staff($self->editor->requestor->id);
2034    }
2035
2036
2037     # if the user provided an overiding checkout time,
2038     # (e.g. the checkout really happened several hours ago), then
2039     # we apply that here.  Does this need a perm??
2040     $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2041         if $self->checkout_time;
2042
2043     # if a patron is renewing, 'requestor' will be the patron
2044     $circ->circ_staff($self->editor->requestor->id);
2045     $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
2046
2047     $self->circ($circ);
2048 }
2049
2050 sub do_reservation_pickup {
2051     my $self = shift;
2052
2053     $self->log_me("do_reservation_pickup()");
2054
2055     $self->reservation->pickup_time('now');
2056
2057     if (
2058         $self->reservation->current_resource &&
2059         $U->is_true($self->reservation->target_resource_type->catalog_item)
2060     ) {
2061         # We used to try to set $self->copy and $self->patron here,
2062         # but that should already be done.
2063
2064         $self->run_checkout_scripts(1);
2065
2066         my $duration   = $self->duration_rule;
2067         my $max        = $self->max_fine_rule;
2068         my $recurring  = $self->recurring_fines_rule;
2069
2070         if ($duration && $max && $recurring) {
2071             my $policy = $self->get_circ_policy($duration, $recurring, $max);
2072
2073             my $dname = $duration->name;
2074             my $mname = $max->name;
2075             my $rname = $recurring->name;
2076
2077             $logger->debug("circulator: updating reservation ".
2078                 "with duration=$dname, maxfine=$mname, recurring=$rname");
2079
2080             $self->reservation->fine_amount($policy->{recurring_fine});
2081             $self->reservation->max_fine($policy->{max_fine});
2082             $self->reservation->fine_interval($recurring->recurrence_interval);
2083         }
2084
2085         $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2086         $self->update_copy();
2087
2088     } else {
2089         $self->reservation->fine_amount(
2090             $self->reservation->target_resource_type->fine_amount
2091         );
2092         $self->reservation->max_fine(
2093             $self->reservation->target_resource_type->max_fine
2094         );
2095         $self->reservation->fine_interval(
2096             $self->reservation->target_resource_type->fine_interval
2097         );
2098     }
2099
2100     $self->update_reservation();
2101 }
2102
2103 sub do_reservation_return {
2104     my $self = shift;
2105     my $request = shift;
2106
2107     $self->log_me("do_reservation_return()");
2108
2109     if (not ref $self->reservation) {
2110         my ($reservation, $evt) =
2111             $U->fetch_booking_reservation($self->reservation);
2112         return $self->bail_on_events($evt) if $evt;
2113         $self->reservation($reservation);
2114     }
2115
2116     $self->handle_fines(1);
2117     $self->reservation->return_time('now');
2118     $self->update_reservation();
2119     $self->reshelve_copy if $self->copy;
2120
2121     if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2122         $self->copy( $self->reservation->current_resource->catalog_item );
2123     }
2124 }
2125
2126 sub booking_adjusted_due_date {
2127     my $self = shift;
2128     my $circ = $self->circ;
2129     my $copy = $self->copy;
2130
2131     return undef unless $self->use_booking;
2132
2133     my $changed;
2134
2135     if( $self->due_date ) {
2136
2137         return $self->bail_on_events($self->editor->event)
2138             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2139
2140        $circ->due_date(cleanse_ISO8601($self->due_date));
2141
2142     } else {
2143
2144         return unless $copy and $circ->due_date;
2145     }
2146
2147     my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2148     if (@$booking_items) {
2149         my $booking_item = $booking_items->[0];
2150         my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2151
2152         my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2153         my $shorten_circ_setting = $resource_type->elbow_room ||
2154             $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2155             '0 seconds';
2156
2157         my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2158         my $bookings = $booking_ses->request(
2159             'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2160             { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2161         )->gather(1);
2162         $booking_ses->disconnect;
2163         
2164         my $dt_parser = DateTime::Format::ISO8601->new;
2165         my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2166
2167         for my $bid (@$bookings) {
2168
2169             my $booking = $self->editor->retrieve_booking_reservation( $bid );
2170
2171             my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2172             my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2173
2174             return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2175                 if ($booking_start < DateTime->now);
2176
2177
2178             if ($U->is_true($stop_circ_setting)) {
2179                 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ); 
2180             } else {
2181                 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2182                 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now); 
2183             }
2184             
2185             # We set the circ duration here only to affect the logic that will
2186             # later (in a DB trigger) mangle the time part of the due date to
2187             # 11:59pm. Having any circ duration that is not a whole number of
2188             # days is enough to prevent the "correction."
2189             my $new_circ_duration = $due_date->epoch - time;
2190             $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2191             $circ->duration("$new_circ_duration seconds");
2192
2193             $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2194             $changed = 1;
2195         }
2196
2197         return $self->bail_on_events($self->editor->event)
2198             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2199     }
2200
2201     return $changed;
2202 }
2203
2204 sub apply_modified_due_date {
2205     my $self = shift;
2206     my $shift_earlier = shift;
2207     my $circ = $self->circ;
2208     my $copy = $self->copy;
2209
2210    if( $self->due_date ) {
2211
2212         return $self->bail_on_events($self->editor->event)
2213             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2214
2215       $circ->due_date(cleanse_ISO8601($self->due_date));
2216
2217    } else {
2218
2219       # if the due_date lands on a day when the location is closed
2220       return unless $copy and $circ->due_date;
2221
2222         #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2223
2224         # due-date overlap should be determined by the location the item
2225         # is checked out from, not the owning or circ lib of the item
2226         my $org = $self->circ_lib;
2227
2228       $logger->info("circulator: circ searching for closed date overlap on lib $org".
2229             " with an item due date of ".$circ->due_date );
2230
2231       my $dateinfo = $U->storagereq(
2232          'open-ils.storage.actor.org_unit.closed_date.overlap', 
2233             $org, $circ->due_date );
2234
2235       if($dateinfo) {
2236          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2237             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2238
2239             # XXX make the behavior more dynamic
2240             # for now, we just push the due date to after the close date
2241             if ($shift_earlier) {
2242                 $circ->due_date($dateinfo->{start});
2243             } else {
2244                 $circ->due_date($dateinfo->{end});
2245             }
2246       }
2247    }
2248 }
2249
2250
2251
2252 sub create_due_date {
2253     my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2254
2255     # if there is a raw time component (e.g. from postgres), 
2256     # turn it into an interval that interval_to_seconds can parse
2257     $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2258
2259     # for now, use the server timezone.  TODO: use workstation org timezone
2260     my $due_date = DateTime->now(time_zone => 'local');
2261     $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2262
2263     # add the circ duration
2264     $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2265
2266     if($date_ceiling) {
2267         my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2268         if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2269             $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2270             $due_date = $cdate;
2271         }
2272     }
2273
2274     # return ISO8601 time with timezone
2275     return $due_date->strftime('%FT%T%z');
2276 }
2277
2278
2279
2280 sub make_precat_copy {
2281     my $self = shift;
2282     my $copy = $self->copy;
2283
2284    if($copy) {
2285         $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2286
2287         $copy->editor($self->editor->requestor->id);
2288         $copy->edit_date('now');
2289         $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2290         $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2291         $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2292         $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2293         $self->update_copy();
2294         return;
2295    }
2296
2297     $logger->info("circulator: Creating a new precataloged ".
2298         "copy in checkout with barcode " . $self->copy_barcode);
2299
2300     $copy = Fieldmapper::asset::copy->new;
2301     $copy->circ_lib($self->circ_lib);
2302     $copy->creator($self->editor->requestor->id);
2303     $copy->editor($self->editor->requestor->id);
2304     $copy->barcode($self->copy_barcode);
2305     $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
2306     $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2307     $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2308
2309     $copy->dummy_title($self->dummy_title || "");
2310     $copy->dummy_author($self->dummy_author || "");
2311     $copy->dummy_isbn($self->dummy_isbn || "");
2312     $copy->circ_modifier($self->circ_modifier);
2313
2314
2315     # See if we need to override the circ_lib for the copy with a configured circ_lib
2316     # Setting is shortname of the org unit
2317     my $precat_circ_lib = $U->ou_ancestor_setting_value(
2318         $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2319
2320     if($precat_circ_lib) {
2321         my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2322
2323         if(!$org) {
2324             $self->bail_on_events($self->editor->event);
2325             return;
2326         }
2327
2328         $copy->circ_lib($org->id);
2329     }
2330
2331
2332     unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2333         $self->bail_out(1);
2334         $self->push_events($self->editor->event);
2335         return;
2336     }   
2337
2338     # this is a little bit of a hack, but we need to 
2339     # get the copy into the script runner
2340     $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2341 }
2342
2343
2344 sub checkout_noncat {
2345     my $self = shift;
2346
2347     my $circ;
2348     my $evt;
2349
2350    my $lib      = $self->noncat_circ_lib || $self->circ_lib;
2351    my $count    = $self->noncat_count || 1;
2352    my $cotime   = cleanse_ISO8601($self->checkout_time) || "";
2353
2354    $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2355
2356    for(1..$count) {
2357
2358       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2359          $self->editor->requestor->id, 
2360             $self->patron->id, 
2361             $lib, 
2362             $self->noncat_type, 
2363             $cotime,
2364             $self->editor );
2365
2366         if( $evt ) {
2367             $self->push_events($evt);
2368             $self->bail_out(1);
2369             return; 
2370         }
2371         $self->circ($circ);
2372    }
2373 }
2374
2375 # If a copy goes into transit and is then checked in before the transit checkin 
2376 # interval has expired, push an event onto the overridable events list.
2377 sub check_transit_checkin_interval {
2378     my $self = shift;
2379
2380     # only concerned with in-transit items
2381     return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2382
2383     # no interval, no problem
2384     my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2385     return unless $interval;
2386
2387     # capture the transit so we don't have to fetch it again later during checkin
2388     $self->transit(
2389         $self->editor->search_action_transit_copy(
2390             {target_copy => $self->copy->id, dest_recv_time => undef}
2391         )->[0]
2392     ); 
2393
2394     # transit from X to X for whatever reason has no min interval
2395     return if $self->transit->source == $self->transit->dest;
2396
2397     my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2398     my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2399     my $horizon = $t_start->add(seconds => $seconds);
2400
2401     # See if we are still within the transit checkin forbidden range
2402     $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK')) 
2403         if $horizon > DateTime->now;
2404 }
2405
2406 # Retarget local holds at checkin
2407 sub checkin_retarget {
2408     my $self = shift;
2409     return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2410     return unless $self->is_checkin; # Renewals need not be checked
2411     return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2412     return if $self->is_precat; # No holds for precats
2413     return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2414     return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2415     my $status = $U->copy_status($self->copy->status);
2416     return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2417     # Specifically target items that are likely new (by status ID)
2418     return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2419     my $location = $self->copy->location;
2420     if(!ref($location)) {
2421         $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2422         $self->copy->location($location);
2423     }
2424     return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2425
2426     # Fetch holds for the bib
2427     my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2428                     $self->editor->authtoken,
2429                     $self->title->id,
2430                     {
2431                         capture_time => undef, # No touching captured holds
2432                         frozen => 'f', # Don't bother with frozen holds
2433                         pickup_lib => $self->circ_lib # Only holds actually here
2434                     }); 
2435
2436     # Error? Skip the step.
2437     return if exists $result->{"ilsevent"};
2438
2439     # Assemble holds
2440     my $holds = [];
2441     foreach my $holdlist (keys %{$result}) {
2442         push @$holds, @{$result->{$holdlist}};
2443     }
2444
2445     return if scalar(@$holds) == 0; # No holds, no retargeting
2446
2447     # Check for parts on this copy
2448     my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2449     my %parts_hash = ();
2450     %parts_hash = map {$_->part, 1} @$parts if @$parts;
2451
2452     # Loop over holds in request-ish order
2453     # Stage 1: Get them into request-ish order
2454     # Also grab type and target for skipping low hanging ones
2455     $result = $self->editor->json_query({
2456         "select" => { "ahr" => ["id", "hold_type", "target"] },
2457         "from" => { "ahr" => { "au" => { "fkey" => "usr",  "join" => "pgt"} } },
2458         "where" => { "id" => $holds },
2459         "order_by" => [
2460             { "class" => "pgt", "field" => "hold_priority"},
2461             { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2462             { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2463             { "class" => "ahr", "field" => "request_time"}
2464         ]
2465     });
2466
2467     # Stage 2: Loop!
2468     if (ref $result eq "ARRAY" and scalar @$result) {
2469         foreach (@{$result}) {
2470             # Copy level, but not this copy?
2471             next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2472                 and $_->{target} != $self->copy->id);
2473             # Volume level, but not this volume?
2474             next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2475             if(@$parts) { # We have parts?
2476                 # Skip title holds
2477                 next if ($_->{hold_type} eq 'T');
2478                 # Skip part holds for parts not on this copy
2479                 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2480             } else {
2481                 # No parts, no part holds
2482                 next if ($_->{hold_type} eq 'P');
2483             }
2484             # So much for easy stuff, attempt a retarget!
2485             my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2486             if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2487                 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2488             }
2489         }
2490     }
2491 }
2492
2493 sub do_checkin {
2494     my $self = shift;
2495     $self->log_me("do_checkin()");
2496
2497     return $self->bail_on_events(
2498         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
2499         unless $self->copy;
2500
2501     $self->check_transit_checkin_interval;
2502     $self->checkin_retarget;
2503
2504     # the renew code and mk_env should have already found our circulation object
2505     unless( $self->circ ) {
2506
2507         my $circs = $self->editor->search_action_circulation(
2508             { target_copy => $self->copy->id, checkin_time => undef });
2509
2510         $self->circ($$circs[0]);
2511
2512         # for now, just warn if there are multiple open circs on a copy
2513         $logger->warn("circulator: we have ".scalar(@$circs).
2514             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2515     }
2516
2517     my $stat = $U->copy_status($self->copy->status)->id;
2518
2519     # LOST (and to some extent, LONGOVERDUE) may optionally be handled
2520     # differently if they are already paid for.  We need to check for this
2521     # early since overdue generation is potentially affected.
2522     my $dont_change_lost_zero = 0;
2523     if ($stat == OILS_COPY_STATUS_LOST
2524         || $stat == OILS_COPY_STATUS_LOST_AND_PAID
2525         || $stat == OILS_COPY_STATUS_LONG_OVERDUE) {
2526
2527         # LOST fine settings are controlled by the copy's circ lib, not the the
2528         # circulation's
2529         my $copy_circ_lib = (ref $self->copy->circ_lib) ?
2530                 $self->copy->circ_lib->id : $self->copy->circ_lib;
2531         $dont_change_lost_zero = $U->ou_ancestor_setting_value(
2532             $copy_circ_lib, 'circ.checkin.lost_zero_balance.do_not_change',
2533             $self->editor) || 0;
2534
2535         if ($dont_change_lost_zero) {
2536             my ($obt) = $U->fetch_mbts($self->circ->id, $self->editor);
2537             $dont_change_lost_zero = 0 if( $obt and $obt->balance_owed != 0 );
2538         }
2539
2540         $self->dont_change_lost_zero($dont_change_lost_zero);
2541     }
2542
2543     if( $self->checkin_check_holds_shelf() ) {
2544         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2545         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2546         if($self->fake_hold_dest) {
2547             $self->hold->pickup_lib($self->circ_lib);
2548         }
2549         $self->checkin_flesh_events;
2550         return;
2551     }
2552
2553     unless( $self->is_renewal ) {
2554         return $self->bail_on_events($self->editor->event)
2555             unless $self->editor->allowed('COPY_CHECKIN');
2556     }
2557
2558     $self->push_events($self->check_copy_alert());
2559     $self->push_events($self->check_checkin_copy_status());
2560
2561     # if the circ is marked as 'claims returned', add the event to the list
2562     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2563         if ($self->circ and $self->circ->stop_fines 
2564                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2565
2566     $self->check_circ_deposit();
2567
2568     # handle the overridable events 
2569     $self->override_events unless $self->is_renewal;
2570     return if $self->bail_out;
2571     
2572     if( $self->copy and !$self->transit ) {
2573         $self->transit(
2574             $self->editor->search_action_transit_copy(
2575                 { target_copy => $self->copy->id, dest_recv_time => undef }
2576             )->[0]
2577         ); 
2578     }
2579
2580     if( $self->circ ) {
2581         $self->checkin_handle_circ;
2582         return if $self->bail_out;
2583         $self->checkin_changed(1);
2584
2585         if (!$dont_change_lost_zero) {
2586             # if this circ is LOST and we are configured to generate overdue
2587             # fines for lost items on checkin (to fill the gap between mark
2588             # lost time and when the fines would have naturally stopped), then
2589             # stop_fines is no longer valid and should be cleared.
2590             #
2591             # stop_fines will be set again during the handle_fines() stage.
2592             # XXX should this setting come from the copy circ lib (like other
2593             # LOST settings), instead of the circulation circ lib?
2594             if ($stat == OILS_COPY_STATUS_LOST) {
2595                 $self->circ->clear_stop_fines if
2596                     $U->ou_ancestor_setting_value(
2597                         $self->circ_lib,
2598                         OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2599                         $self->editor
2600                     );
2601             }
2602
2603             # handle fines for this circ, including overdue gen if needed
2604             $self->handle_fines;
2605         }
2606     } elsif( $self->transit ) {
2607         my $hold_transit = $self->process_received_transit;
2608         $self->checkin_changed(1);
2609
2610         if( $self->bail_out ) { 
2611             $self->checkin_flesh_events;
2612             return;
2613         }
2614         
2615         if( my $e = $self->check_checkin_copy_status() ) {
2616             # If the original copy status is special, alert the caller
2617             my $ev = $self->events;
2618             $self->events([$e]);
2619             $self->override_events;
2620             return if $self->bail_out;
2621             $self->events($ev);
2622         }
2623
2624         if( $hold_transit or 
2625                 $U->copy_status($self->copy->status)->id 
2626                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2627
2628             my $hold;
2629             if( $hold_transit ) {
2630                $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2631             } else {
2632                    ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2633             }
2634
2635             $self->hold($hold);
2636
2637             if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2638
2639                 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2640                 $self->reshelve_copy(1);
2641                 $self->cancelled_hold_transit(1);
2642                 $self->notify_hold(0); # don't notify for cancelled holds
2643                 $self->fake_hold_dest(0);
2644                 return if $self->bail_out;
2645
2646             } elsif ($hold and $hold->hold_type eq 'R') {
2647
2648                 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2649                 $self->notify_hold(0); # No need to notify
2650                 $self->fake_hold_dest(0);
2651                 $self->noop(1); # Don't try and capture for other holds/transits now
2652                 $self->update_copy();
2653                 $hold->fulfillment_time('now');
2654                 $self->bail_on_events($self->editor->event)
2655                     unless $self->editor->update_action_hold_request($hold);
2656
2657             } else {
2658
2659                 # hold transited to correct location
2660                 if($self->fake_hold_dest) {
2661                     $hold->pickup_lib($self->circ_lib);
2662                 }
2663                 $self->checkin_flesh_events;
2664                 return;
2665             }
2666         } 
2667
2668     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2669
2670         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2671             " that is in-transit, but there is no transit.. repairing");
2672         $self->reshelve_copy(1);
2673         return if $self->bail_out;
2674     }
2675
2676     if( $self->is_renewal ) {
2677         $self->finish_fines_and_voiding;
2678         return if $self->bail_out;
2679         $self->push_events(OpenILS::Event->new('SUCCESS'));
2680         return;
2681     }
2682
2683    # ------------------------------------------------------------------------------
2684    # Circulations and transits are now closed where necessary.  Now go on to see if
2685    # this copy can fulfill a hold or needs to be routed to a different location
2686    # ------------------------------------------------------------------------------
2687
2688     my $needed_for_something = 0; # formerly "needed_for_hold"
2689
2690     if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2691
2692         if (!$self->remote_hold) {
2693             if ($self->use_booking) {
2694                 my $potential_hold = $self->hold_capture_is_possible;
2695                 my $potential_reservation = $self->reservation_capture_is_possible;
2696
2697                 if ($potential_hold and $potential_reservation) {
2698                     $logger->info("circulator: item could fulfill either hold or reservation");
2699                     $self->push_events(new OpenILS::Event(
2700                         "HOLD_RESERVATION_CONFLICT",
2701                         "hold" => $potential_hold,
2702                         "reservation" => $potential_reservation
2703                     ));
2704                     return if $self->bail_out;
2705                 } elsif ($potential_hold) {
2706                     $needed_for_something =
2707                         $self->attempt_checkin_hold_capture;
2708                 } elsif ($potential_reservation) {
2709                     $needed_for_something =
2710                         $self->attempt_checkin_reservation_capture;
2711                 }
2712             } else {
2713                 $needed_for_something = $self->attempt_checkin_hold_capture;
2714             }
2715         }
2716         return if $self->bail_out;
2717     
2718         unless($needed_for_something) {
2719             my $circ_lib = (ref $self->copy->circ_lib) ? 
2720                     $self->copy->circ_lib->id : $self->copy->circ_lib;
2721     
2722             if( $self->remote_hold ) {
2723                 $circ_lib = $self->remote_hold->pickup_lib;
2724                 $logger->warn("circulator: Copy ".$self->copy->barcode.
2725                     " is on a remote hold's shelf, sending to $circ_lib");
2726             }
2727     
2728             $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2729
2730             my $suppress_transit = 0;
2731
2732             if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2733                 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2734                 if($suppress_transit_source && $suppress_transit_source->{value}) {
2735                     my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2736                     if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2737                         $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2738                         $suppress_transit = 1;
2739                     }
2740                 }
2741             }
2742  
2743             if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2744                 # copy is where it needs to be, either for hold or reshelving
2745     
2746                 $self->checkin_handle_precat();
2747                 return if $self->bail_out;
2748     
2749             } else {
2750                 # copy needs to transit "home", or stick here if it's a floating copy
2751                 my $can_float = 0;
2752                 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2753                     my $res = $self->editor->json_query(
2754                         {   from => [
2755                                 'evergreen.can_float',
2756                                 $self->copy->floating->id,
2757                                 $self->copy->circ_lib,
2758                                 $self->circ_lib
2759                             ]
2760                         }
2761                     );
2762                     $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res; 
2763                 }
2764                 if ($can_float) { # Yep, floating, stick here
2765                     $self->checkin_changed(1);
2766                     $self->copy->circ_lib( $self->circ_lib );
2767                     $self->update_copy;
2768                 } else {
2769                     my $bc = $self->copy->barcode;
2770                     $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2771                     $self->checkin_build_copy_transit($circ_lib);
2772                     return if $self->bail_out;
2773                     $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2774                 }
2775             }
2776         }
2777     } else { # no-op checkin
2778         if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2779             $self->checkin_changed(1);
2780             $self->copy->circ_lib( $self->circ_lib );
2781             $self->update_copy;
2782         }
2783     }
2784
2785     if($self->claims_never_checked_out and 
2786             $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2787
2788         # the item was not supposed to be checked out to the user and should now be marked as missing
2789         $self->copy->status(OILS_COPY_STATUS_MISSING);
2790         $self->update_copy;
2791
2792     } else {
2793         $self->reshelve_copy unless $needed_for_something;
2794     }
2795
2796     return if $self->bail_out;
2797
2798     unless($self->checkin_changed) {
2799
2800         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2801         my $stat = $U->copy_status($self->copy->status)->id;
2802
2803         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2804          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2805         $self->bail_out(1); # no need to commit anything
2806
2807     } else {
2808
2809         $self->push_events(OpenILS::Event->new('SUCCESS')) 
2810             unless @{$self->events};
2811     }
2812
2813     $self->finish_fines_and_voiding;
2814
2815     OpenILS::Utils::Penalty->calculate_penalties(
2816         $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2817
2818     $self->checkin_flesh_events;
2819     return;
2820 }
2821
2822 sub finish_fines_and_voiding {
2823     my $self = shift;
2824     return unless $self->circ;
2825
2826     return unless $self->backdate or $self->void_overdues;
2827
2828     # void overdues after fine generation to prevent concurrent DB access to overdue billings
2829     my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2830
2831     my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2832         $self->editor, $self->circ, $self->backdate, $note);
2833
2834     return $self->bail_on_events($evt) if $evt;
2835
2836     # Make sure the circ is open or closed as necessary.
2837     $evt = $U->check_open_xact($self->editor, $self->circ->id);
2838     return $self->bail_on_events($evt) if $evt;
2839
2840     return undef;
2841 }
2842
2843
2844 # if a deposit was payed for this item, push the event
2845 sub check_circ_deposit {
2846     my $self = shift;
2847     return unless $self->circ;
2848     my $deposit = $self->editor->search_money_billing(
2849         {   btype => 5, 
2850             xact => $self->circ->id, 
2851             voided => 'f'
2852         }, {idlist => 1})->[0];
2853
2854     $self->push_events(OpenILS::Event->new(
2855         'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2856 }
2857
2858 sub reshelve_copy {
2859    my $self    = shift;
2860    my $force   = $self->force || shift;
2861    my $copy    = $self->copy;
2862
2863    my $stat = $U->copy_status($copy->status)->id;
2864
2865    if($force || (
2866       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2867       $stat != OILS_COPY_STATUS_CATALOGING and
2868       $stat != OILS_COPY_STATUS_IN_TRANSIT and
2869       $stat != OILS_COPY_STATUS_RESHELVING  )) {
2870
2871         $copy->status( OILS_COPY_STATUS_RESHELVING );
2872             $self->update_copy;
2873             $self->checkin_changed(1);
2874     }
2875 }
2876
2877
2878 # Returns true if the item is at the current location
2879 # because it was transited there for a hold and the 
2880 # hold has not been fulfilled
2881 sub checkin_check_holds_shelf {
2882     my $self = shift;
2883     return 0 unless $self->copy;
2884
2885     return 0 unless 
2886         $U->copy_status($self->copy->status)->id ==
2887             OILS_COPY_STATUS_ON_HOLDS_SHELF;
2888
2889     # Attempt to clear shelf expired holds for this copy
2890     $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2891         if($self->clear_expired);
2892
2893     # find the hold that put us on the holds shelf
2894     my $holds = $self->editor->search_action_hold_request(
2895         { 
2896             current_copy => $self->copy->id,
2897             capture_time => { '!=' => undef },
2898             fulfillment_time => undef,
2899             cancel_time => undef,
2900         }
2901     );
2902
2903     unless(@$holds) {
2904         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2905         $self->reshelve_copy(1);
2906         return 0;
2907     }
2908
2909     my $hold = $$holds[0];
2910
2911     $logger->info("circulator: we found a captured, un-fulfilled hold [".
2912         $hold->id. "] for copy ".$self->copy->barcode);
2913
2914     if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2915         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2916         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2917             my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2918             if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2919                 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2920                 $self->fake_hold_dest(1);
2921                 return 1;
2922             }
2923         }
2924     }
2925
2926     if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2927         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2928         return 1;
2929     }
2930
2931     $logger->info("circulator: hold is not for here..");
2932     $self->remote_hold($hold);
2933     return 0;
2934 }
2935
2936
2937 sub checkin_handle_precat {
2938     my $self    = shift;
2939    my $copy    = $self->copy;
2940
2941    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2942         $copy->status(OILS_COPY_STATUS_CATALOGING);
2943         $self->update_copy();
2944         $self->checkin_changed(1);
2945         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2946    }
2947 }
2948
2949
2950 sub checkin_build_copy_transit {
2951     my $self            = shift;
2952     my $dest            = shift;
2953     my $copy       = $self->copy;
2954     my $transit    = Fieldmapper::action::transit_copy->new;
2955
2956     # if we are transiting an item to the shelf shelf, it's a hold transit
2957     if (my $hold = $self->remote_hold) {
2958         $transit = Fieldmapper::action::hold_transit_copy->new;
2959         $transit->hold($hold->id);
2960
2961         # the item is going into transit, remove any shelf-iness
2962         if ($hold->current_shelf_lib or $hold->shelf_time) {
2963             $hold->clear_current_shelf_lib;
2964             $hold->clear_shelf_time;
2965             return $self->bail_on_events($self->editor->event)
2966                 unless $self->editor->update_action_hold_request($hold);
2967         }
2968     }
2969
2970     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2971     $logger->info("circulator: transiting copy to $dest");
2972
2973     $transit->source($self->circ_lib);
2974     $transit->dest($dest);
2975     $transit->target_copy($copy->id);
2976     $transit->source_send_time('now');
2977     $transit->copy_status( $U->copy_status($copy->status)->id );
2978
2979     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2980
2981     if ($self->remote_hold) {
2982         return $self->bail_on_events($self->editor->event)
2983             unless $self->editor->create_action_hold_transit_copy($transit);
2984     } else {
2985         return $self->bail_on_events($self->editor->event)
2986             unless $self->editor->create_action_transit_copy($transit);
2987     }
2988
2989     # ensure the transit is returned to the caller
2990     $self->transit($transit);
2991
2992     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2993     $self->update_copy;
2994     $self->checkin_changed(1);
2995 }
2996
2997
2998 sub hold_capture_is_possible {
2999     my $self = shift;
3000     my $copy = $self->copy;
3001
3002     # we've been explicitly told not to capture any holds
3003     return 0 if $self->capture eq 'nocapture';
3004
3005     # See if this copy can fulfill any holds
3006     my $hold = $holdcode->find_nearest_permitted_hold(
3007         $self->editor, $copy, $self->editor->requestor, 1 # check_only
3008     );
3009     return undef if ref $hold eq "HASH" and
3010         $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
3011     return $hold;
3012 }
3013
3014 sub reservation_capture_is_possible {
3015     my $self = shift;
3016     my $copy = $self->copy;
3017
3018     # we've been explicitly told not to capture any holds
3019     return 0 if $self->capture eq 'nocapture';
3020
3021     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3022     my $resv = $booking_ses->request(
3023         "open-ils.booking.reservations.could_capture",
3024         $self->editor->authtoken, $copy->barcode
3025     )->gather(1);
3026     $booking_ses->disconnect;
3027     if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
3028         $self->push_events($resv);
3029     } else {
3030         return $resv;
3031     }
3032 }
3033
3034 # returns true if the item was used (or may potentially be used 
3035 # in subsequent calls) to capture a hold.
3036 sub attempt_checkin_hold_capture {
3037     my $self = shift;
3038     my $copy = $self->copy;
3039
3040     # we've been explicitly told not to capture any holds
3041     return 0 if $self->capture eq 'nocapture';
3042
3043     # See if this copy can fulfill any holds
3044     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
3045         $self->editor, $copy, $self->editor->requestor );
3046
3047     if(!$hold) {
3048         $logger->debug("circulator: no potential permitted".
3049             "holds found for copy ".$copy->barcode);
3050         return 0;
3051     }
3052
3053     if($self->capture ne 'capture') {
3054         # see if this item is in a hold-capture-delay location
3055         my $location = $self->copy->location;
3056         if(!ref($location)) {
3057             $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
3058             $self->copy->location($location);
3059         }
3060         if($U->is_true($location->hold_verify)) {
3061             $self->bail_on_events(
3062                 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3063             return 1;
3064         }
3065     }
3066
3067     $self->retarget($retarget);
3068
3069     my $suppress_transit = 0;
3070     if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3071         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3072         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3073             my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3074             if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3075                 $suppress_transit = 1;
3076                 $hold->pickup_lib($self->circ_lib);
3077             }
3078         }
3079     }
3080
3081     $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3082
3083     $hold->current_copy($copy->id);
3084     $hold->capture_time('now');
3085     $self->put_hold_on_shelf($hold) 
3086         if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3087
3088     # prevent DB errors caused by fetching 
3089     # holds from storage, and updating through cstore
3090     $hold->clear_fulfillment_time;
3091     $hold->clear_fulfillment_staff;
3092     $hold->clear_fulfillment_lib;
3093     $hold->clear_expire_time; 
3094     $hold->clear_cancel_time;
3095     $hold->clear_prev_check_time unless $hold->prev_check_time;
3096
3097     $self->bail_on_events($self->editor->event)
3098         unless $self->editor->update_action_hold_request($hold);
3099     $self->hold($hold);
3100     $self->checkin_changed(1);
3101
3102     return 0 if $self->bail_out;
3103
3104     if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3105
3106         if ($hold->hold_type eq 'R') {
3107             $copy->status(OILS_COPY_STATUS_CATALOGING);
3108             $hold->fulfillment_time('now');
3109             $self->noop(1); # Block other transit/hold checks
3110             $self->bail_on_events($self->editor->event)
3111                 unless $self->editor->update_action_hold_request($hold);
3112         } else {
3113             # This hold was captured in the correct location
3114             $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3115             $self->push_events(OpenILS::Event->new('SUCCESS'));
3116
3117             #$self->do_hold_notify($hold->id);
3118             $self->notify_hold($hold->id);
3119         }
3120
3121     } else {
3122     
3123         # Hold needs to be picked up elsewhere.  Build a hold
3124         # transit and route the item.
3125         $self->checkin_build_hold_transit();
3126         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3127         return 0 if $self->bail_out;
3128         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3129     }
3130
3131     # make sure we save the copy status
3132     $self->update_copy;
3133     return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3134     return 1;
3135 }
3136
3137 sub attempt_checkin_reservation_capture {
3138     my $self = shift;
3139     my $copy = $self->copy;
3140
3141     # we've been explicitly told not to capture any holds
3142     return 0 if $self->capture eq 'nocapture';
3143
3144     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3145     my $evt = $booking_ses->request(
3146         "open-ils.booking.resources.capture_for_reservation",
3147         $self->editor->authtoken,
3148         $copy->barcode,
3149         1 # don't update copy - we probably have it locked
3150     )->gather(1);
3151     $booking_ses->disconnect;
3152
3153     if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3154         $logger->warn(
3155             "open-ils.booking.resources.capture_for_reservation " .
3156             "didn't return an event!"
3157         );
3158     } else {
3159         if (
3160             $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3161             $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3162         ) {
3163             # not-transferable is an error event we'll pass on the user
3164             $logger->warn("reservation capture attempted against non-transferable item");
3165             $self->push_events($evt);
3166             return 0;
3167         } elsif ($evt->{"textcode"} eq "SUCCESS") {
3168             # Re-retrieve copy as reservation capture may have changed
3169             # its status and whatnot.
3170             $logger->info(
3171                 "circulator: booking capture win on copy " . $self->copy->id
3172             );
3173             if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3174                 $logger->info(
3175                     "circulator: changing copy " . $self->copy->id .
3176                     "'s status from " . $self->copy->status . " to " .
3177                     $new_copy_status
3178                 );
3179                 $self->copy->status($new_copy_status);
3180                 $self->update_copy;
3181             }
3182             $self->reservation($evt->{"payload"}->{"reservation"});
3183
3184             if (exists $evt->{"payload"}->{"transit"}) {
3185                 $self->push_events(
3186                     new OpenILS::Event(
3187                         "ROUTE_ITEM",
3188                         "org" => $evt->{"payload"}->{"transit"}->dest
3189                     )
3190                 );
3191             }
3192             $self->checkin_changed(1);
3193             return 1;
3194         }
3195     }
3196     # other results are treated as "nothing to capture"
3197     return 0;
3198 }
3199
3200 sub do_hold_notify {
3201     my( $self, $holdid ) = @_;
3202
3203     my $e = new_editor(xact => 1);
3204     my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3205     $e->rollback;
3206     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3207     $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3208
3209     $logger->info("circulator: running delayed hold notify process");
3210
3211 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3212 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3213
3214     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3215         hold_id => $holdid, requestor => $self->editor->requestor);
3216
3217     $logger->debug("circulator: built hold notifier");
3218
3219     if(!$notifier->event) {
3220
3221         $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3222
3223         my $stat = $notifier->send_email_notify;
3224         if( $stat == '1' ) {
3225             $logger->info("circulator: hold notify succeeded for hold $holdid");
3226             return;
3227         } 
3228
3229         $logger->debug("circulator:  * hold notify cancelled or failed for hold $holdid");
3230
3231     } else {
3232         $logger->info("circulator: Not sending hold notification since the patron has no email address");
3233     }
3234 }
3235
3236 sub retarget_holds {
3237     my $self = shift;
3238     $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3239     my $ses = OpenSRF::AppSession->create('open-ils.storage');
3240     $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3241     # no reason to wait for the return value
3242     return;
3243 }
3244
3245 sub checkin_build_hold_transit {
3246     my $self = shift;
3247
3248    my $copy = $self->copy;
3249    my $hold = $self->hold;
3250    my $trans = Fieldmapper::action::hold_transit_copy->new;
3251
3252     $logger->debug("circulator: building hold transit for ".$copy->barcode);
3253
3254    $trans->hold($hold->id);
3255    $trans->source($self->circ_lib);
3256    $trans->dest($hold->pickup_lib);
3257    $trans->source_send_time("now");
3258    $trans->target_copy($copy->id);
3259
3260     # when the copy gets to its destination, it will recover
3261     # this status - put it onto the holds shelf
3262    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3263
3264     return $self->bail_on_events($self->editor->event)
3265         unless $self->editor->create_action_hold_transit_copy($trans);
3266 }
3267
3268
3269
3270 sub process_received_transit {
3271     my $self = shift;
3272     my $copy = $self->copy;
3273     my $copyid = $self->copy->id;
3274
3275     my $status_name = $U->copy_status($copy->status)->name;
3276     $logger->debug("circulator: attempting transit receive on ".
3277         "copy $copyid. Copy status is $status_name");
3278
3279     my $transit = $self->transit;
3280
3281     # Check if we are in a transit suppress range
3282     my $suppress_transit = 0;
3283     if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3284         my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ?  'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3285         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3286         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3287             my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3288             if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3289                 $suppress_transit = 1;
3290                 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3291             }
3292         }
3293     }
3294     if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3295         # - this item is in-transit to a different location
3296         # - Or we are capturing holds as transits, so why create a new transit?
3297
3298         my $tid = $transit->id; 
3299         my $loc = $self->circ_lib;
3300         my $dest = $transit->dest;
3301
3302         $logger->info("circulator: Fowarding transit on copy which is destined ".
3303             "for a different location. transit=$tid, copy=$copyid, current ".
3304             "location=$loc, destination location=$dest");
3305
3306         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3307
3308         # grab the associated hold object if available
3309         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3310         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3311
3312         return $self->bail_on_events($evt);
3313     }
3314
3315     # The transit is received, set the receive time
3316     $transit->dest_recv_time('now');
3317     $self->bail_on_events($self->editor->event)
3318         unless $self->editor->update_action_transit_copy($transit);
3319
3320     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3321
3322     $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3323     $copy->status( $transit->copy_status );
3324     $self->update_copy();
3325     return if $self->bail_out;
3326
3327     my $ishold = 0;
3328     if($hold_transit) { 
3329         my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3330
3331         if ($hold) {
3332             # hold has arrived at destination, set shelf time
3333             $self->put_hold_on_shelf($hold);
3334             $self->bail_on_events($self->editor->event)
3335                 unless $self->editor->update_action_hold_request($hold);
3336             return if $self->bail_out;
3337
3338             $self->notify_hold($hold_transit->hold);
3339             $ishold = 1;
3340         } else {
3341             $hold_transit = undef;
3342             $self->cancelled_hold_transit(1);
3343             $self->reshelve_copy(1);
3344             $self->fake_hold_dest(0);
3345         }
3346     }
3347
3348     $self->push_events( 
3349         OpenILS::Event->new(
3350         'SUCCESS', 
3351         ishold => $ishold,
3352       payload => { transit => $transit, holdtransit => $hold_transit } ));
3353
3354     return $hold_transit;
3355 }
3356
3357
3358 # ------------------------------------------------------------------
3359 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3360 # ------------------------------------------------------------------
3361 sub put_hold_on_shelf {
3362     my($self, $hold) = @_;
3363     $hold->shelf_time('now');
3364     $hold->current_shelf_lib($self->circ_lib);
3365     $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3366     return undef;
3367 }
3368
3369 sub handle_fines {
3370    my $self = shift;
3371    my $reservation = shift;
3372    my $dt_parser = DateTime::Format::ISO8601->new;
3373
3374    my $obj = $reservation ? $self->reservation : $self->circ;
3375
3376     my $lost_bill_opts = $self->lost_bill_options;
3377     my $circ_lib = $lost_bill_opts->{circ_lib} if $lost_bill_opts;
3378     # first, restore any voided overdues for lost, if needed
3379     if ($self->needs_lost_bill_handling and !$self->void_overdues) {
3380         my $restore_od = $U->ou_ancestor_setting_value(
3381             $circ_lib, $lost_bill_opts->{ous_restore_overdue},
3382             $self->editor) || 0;
3383         $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3384             if $restore_od;
3385     }
3386
3387     # next, handle normal overdue generation and apply stop_fines
3388     # XXX reservations don't have stop_fines
3389     # TODO revisit booking_reservation re: stop_fines support
3390     if ($reservation or !$obj->stop_fines) {
3391         my $skip_for_grace;
3392
3393         # This is a crude check for whether we are in a grace period. The code
3394         # in generate_fines() does a more thorough job, so this exists solely
3395         # as a small optimization, and might be better off removed.
3396
3397         # If we have a grace period
3398         if($obj->can('grace_period')) {
3399             # Parse out the due date
3400             my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3401             # Add the grace period to the due date
3402             $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3403             # Don't generate fines on circs still in grace period
3404             $skip_for_grace = $due_date > DateTime->now;
3405         }
3406         $CC->generate_fines({circs => [$obj], editor => $self->editor})
3407             unless $skip_for_grace;
3408
3409         if (!$reservation and !$obj->stop_fines) {
3410             $obj->stop_fines(OILS_STOP_FINES_CHECKIN);
3411             $obj->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3412             $obj->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3413             $obj->stop_fines_time('now');
3414             $obj->stop_fines_time($self->backdate) if $self->backdate;
3415             $self->editor->update_action_circulation($obj);
3416         }
3417     }
3418
3419     # finally, handle voiding of lost item and processing fees
3420     if ($self->needs_lost_bill_handling) {
3421         my $void_cost = $U->ou_ancestor_setting_value(
3422             $circ_lib, $lost_bill_opts->{ous_void_item_cost},
3423             $self->editor) || 0;
3424         my $void_proc_fee = $U->ou_ancestor_setting_value(
3425             $circ_lib, $lost_bill_opts->{ous_void_proc_fee},
3426             $self->editor) || 0;
3427         $self->checkin_handle_lost_or_lo_now_found(
3428             $lost_bill_opts->{void_cost_btype},
3429             $lost_bill_opts->{is_longoverdue}) if $void_cost;
3430         $self->checkin_handle_lost_or_lo_now_found(
3431             $lost_bill_opts->{void_fee_btype},
3432             $lost_bill_opts->{is_longoverdue}) if $void_proc_fee;
3433     }
3434
3435    return undef;
3436 }
3437
3438 sub checkin_handle_circ {
3439    my $self = shift;
3440    my $circ = $self->circ;
3441    my $copy = $self->copy;
3442    my $evt;
3443    my $obt;
3444
3445    $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3446
3447    # backdate the circ if necessary
3448    if($self->backdate) {
3449         my $evt = $self->checkin_handle_backdate;
3450         return $self->bail_on_events($evt) if $evt;
3451    }
3452
3453     # Set the checkin vars since we have the item
3454     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3455
3456     # capture the true scan time for back-dated checkins
3457     $circ->checkin_scan_time('now');
3458
3459     $circ->checkin_staff($self->editor->requestor->id);
3460     $circ->checkin_lib($self->circ_lib);
3461     $circ->checkin_workstation($self->editor->requestor->wsid);
3462
3463     my $circ_lib = (ref $self->copy->circ_lib) ?  
3464         $self->copy->circ_lib->id : $self->copy->circ_lib;
3465     my $stat = $U->copy_status($self->copy->status)->id;
3466
3467     if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3468         # we will now handle lost fines, but the copy will retain its 'lost'
3469         # status if it needs to transit home unless lost_immediately_available
3470         # is true
3471         #
3472         # if we decide to also delay fine handling until the item arrives home,
3473         # we will need to call lost fine handling code both when checking items
3474         # in and also when receiving transits
3475         $self->checkin_handle_lost($circ_lib);
3476     } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3477         # same process as above.
3478         $self->checkin_handle_long_overdue($circ_lib);
3479     } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3480         $logger->info("circulator: not updating copy status on checkin because copy is missing");
3481     } else {
3482         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3483         $self->update_copy;
3484     }
3485
3486
3487     # see if there are any fines owed on this circ.  if not, close it
3488     ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3489     $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3490
3491     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3492
3493     return $self->bail_on_events($self->editor->event)
3494         unless $self->editor->update_action_circulation($circ);
3495
3496     return undef;
3497 }
3498
3499 # ------------------------------------------------------------------
3500 # See if we need to void billings, etc. for lost checkin
3501 # ------------------------------------------------------------------
3502 sub checkin_handle_lost {
3503     my $self = shift;
3504     my $circ_lib = shift;
3505
3506     my $max_return = $U->ou_ancestor_setting_value($circ_lib, 
3507         OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3508
3509     $self->lost_bill_options({
3510         circ_lib => $circ_lib,
3511         ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3512         ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3513         ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3514         void_cost_btype => 3, 
3515         void_fee_btype => 4 
3516     });
3517
3518     return $self->checkin_handle_lost_or_longoverdue(
3519         circ_lib => $circ_lib,
3520         max_return => $max_return,
3521         ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3522         ous_use_last_activity => undef # not supported for LOST checkin
3523     );
3524 }
3525
3526 # ------------------------------------------------------------------
3527 # See if we need to void billings, etc. for long-overdue checkin
3528 # note: not using constants below since they serve little purpose 
3529 # for single-use strings that are descriptive in their own right 
3530 # and mostly just complicate debugging.
3531 # ------------------------------------------------------------------
3532 sub checkin_handle_long_overdue {
3533     my $self = shift;
3534     my $circ_lib = shift;
3535
3536     $logger->info("circulator: processing long-overdue checkin...");
3537
3538     my $max_return = $U->ou_ancestor_setting_value($circ_lib, 
3539         'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3540
3541     $self->lost_bill_options({
3542         circ_lib => $circ_lib,
3543         ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3544         ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3545         is_longoverdue => 1,
3546         ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3547         void_cost_btype => 10,
3548         void_fee_btype => 11
3549     });
3550
3551     return $self->checkin_handle_lost_or_longoverdue(
3552         circ_lib => $circ_lib,
3553         max_return => $max_return,
3554         ous_immediately_available => 'circ.longoverdue_immediately_available',
3555         ous_use_last_activity => 
3556             'circ.longoverdue.use_last_activity_date_on_return'
3557     )
3558 }
3559
3560 # last billing activity is last payment time, last billing time, or the 
3561 # circ due date.  If the relevant "use last activity" org unit setting is 
3562 # false/unset, then last billing activity is always the due date.
3563 sub get_circ_last_billing_activity {
3564     my $self = shift;
3565     my $circ_lib = shift;
3566     my $setting = shift;
3567     my $date = $self->circ->due_date;
3568
3569     return $date unless $setting and 
3570         $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3571
3572     my $xact = $self->editor->retrieve_money_billable_transaction([
3573         $self->circ->id,
3574         {flesh => 1, flesh_fields => {mbt => ['summary']}}
3575     ]);
3576
3577     if ($xact->summary) {
3578         $date = $xact->summary->last_payment_ts || 
3579                 $xact->summary->last_billing_ts || 
3580                 $self->circ->due_date;
3581     }
3582
3583     return $date;
3584 }
3585
3586
3587 sub checkin_handle_lost_or_longoverdue {
3588     my ($self, %args) = @_;
3589
3590     my $circ = $self->circ;
3591     my $max_return = $args{max_return};
3592     my $circ_lib = $args{circ_lib};
3593
3594     if ($max_return) {
3595
3596         my $last_activity = 
3597             $self->get_circ_last_billing_activity(
3598                 $circ_lib, $args{ous_use_last_activity});
3599
3600         my $today = time();
3601         my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3602         $tm[5] -= 1 if $tm[5] > 0;
3603         my $due = timelocal(int($tm[1]), int($tm[2]), 
3604             int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3605
3606         my $last_chance = 
3607             OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3608
3609         $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3610             "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3611                 "DUE: $due LAST: $last_chance");
3612
3613         $max_return = 0 if $today < $last_chance;
3614     }
3615
3616
3617     if ($max_return) {
3618
3619         $logger->info("circulator: check-in of lost/lo item exceeds max ". 
3620             "return interval.  skipping fine/fee voiding, etc.");
3621
3622     } elsif ($self->dont_change_lost_zero) { # we leave lost zero balance alone
3623
3624         $logger->info("circulator: check-in of lost/lo item having a balance ".
3625             "of zero, skipping fine/fee voiding and reinstatement.");
3626
3627     } else { # within max-return interval or no interval defined
3628
3629         $logger->info("circulator: check-in of lost/lo item is within the ".
3630             "max return interval (or no interval is defined).  Proceeding ".
3631             "with fine/fee voiding, etc.");
3632
3633         $self->needs_lost_bill_handling(1);
3634     }
3635
3636     if ($circ_lib != $self->circ_lib) {
3637         # if the item is not home, check to see if we want to retain the
3638         # lost/longoverdue status at this point in the process
3639
3640         my $immediately_available = $U->ou_ancestor_setting_value($circ_lib, 
3641             $args{ous_immediately_available}, $self->editor) || 0;
3642
3643         if ($immediately_available) {
3644             # item status does not need to be retained, so give it a
3645             # reshelving status as if it were a normal checkin
3646             $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3647             $self->update_copy;
3648         } else {
3649             $logger->info("circulator: leaving lost/longoverdue copy".
3650                 " status in place on checkin");
3651         }
3652     } else {
3653         # lost/longoverdue item is home and processed, treat like a normal 
3654         # checkin from this point on
3655         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3656         $self->update_copy;
3657     }
3658 }
3659
3660
3661 sub checkin_handle_backdate {
3662     my $self = shift;
3663
3664     # ------------------------------------------------------------------
3665     # clean up the backdate for date comparison
3666     # XXX We are currently taking the due-time from the original due-date,
3667     # not the input.  Do we need to do this?  This certainly interferes with
3668     # backdating of hourly checkouts, but that is likely a very rare case.
3669     # ------------------------------------------------------------------
3670     my $bd = cleanse_ISO8601($self->backdate);
3671     my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3672     my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3673     $new_date->set_hour($original_date->hour());
3674     $new_date->set_minute($original_date->minute());
3675     if ($new_date >= DateTime->now) {
3676         # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3677         $bd = undef;
3678     } else {
3679         $bd = cleanse_ISO8601($new_date->datetime());
3680     }
3681
3682     $self->backdate($bd);
3683     return undef;
3684 }
3685
3686
3687 sub check_checkin_copy_status {
3688     my $self = shift;
3689    my $copy = $self->copy;
3690
3691    my $status = $U->copy_status($copy->status)->id;
3692
3693    return undef
3694       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
3695             $status == OILS_COPY_STATUS_CHECKED_OUT ||
3696             $status == OILS_COPY_STATUS_IN_PROCESS  ||
3697             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
3698             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
3699             $status == OILS_COPY_STATUS_CATALOGING  ||
3700             $status == OILS_COPY_STATUS_ON_RESV_SHELF  ||
3701             $status == OILS_COPY_STATUS_RESHELVING );
3702
3703    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3704       if( $status == OILS_COPY_STATUS_LOST );
3705
3706     return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3707         if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3708
3709    return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3710       if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3711
3712    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3713       if( $status == OILS_COPY_STATUS_MISSING );
3714
3715    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3716 }
3717
3718
3719
3720 # --------------------------------------------------------------------------
3721 # On checkin, we need to return as many relevant objects as we can
3722 # --------------------------------------------------------------------------
3723 sub checkin_flesh_events {
3724     my $self = shift;
3725
3726     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
3727         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3728             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3729     }
3730
3731     my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3732
3733     my $hold;
3734     if($self->hold and !$self->hold->cancel_time) {
3735         $hold = $self->hold;
3736         $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3737     }
3738
3739     if($self->circ) {
3740         # update our copy of the circ object and 
3741         # flesh the billing summary data
3742         $self->circ(
3743             $self->editor->retrieve_action_circulation([
3744                 $self->circ->id, {
3745                     flesh => 2,
3746                     flesh_fields => {
3747                         circ => ['billable_transaction'],
3748                         mbt => ['summary']
3749                     }
3750                 }
3751             ])
3752         );
3753     }
3754
3755     if($self->patron) {
3756         # flesh some patron fields before returning
3757         $self->patron(
3758             $self->editor->retrieve_actor_user([
3759                 $self->patron->id,
3760                 {
3761                     flesh => 1,
3762                     flesh_fields => {
3763                         au => ['card', 'billing_address', 'mailing_address']
3764                     }
3765                 }
3766             ])
3767         );
3768     }
3769
3770     for my $evt (@{$self->events}) {
3771
3772         my $payload         = {};
3773         $payload->{copy}    = $U->unflesh_copy($self->copy);
3774         $payload->{volume}  = $self->volume;
3775         $payload->{record}  = $record,
3776         $payload->{circ}    = $self->circ;
3777         $payload->{transit} = $self->transit;
3778         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3779         $payload->{hold}    = $hold;
3780         $payload->{patron}  = $self->patron;
3781         $payload->{reservation} = $self->reservation
3782             unless (not $self->reservation or $self->reservation->cancel_time);
3783
3784         $evt->{payload}     = $payload;
3785     }
3786 }
3787
3788 sub log_me {
3789     my( $self, $msg ) = @_;
3790     my $bc = ($self->copy) ? $self->copy->barcode :
3791         $self->barcode;
3792     $bc ||= "";
3793     my $usr = ($self->patron) ? $self->patron->id : "";
3794     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3795         ", recipient=$usr, copy=$bc");
3796 }
3797
3798
3799 sub do_renew {
3800     my $self = shift;
3801     $self->log_me("do_renew()");
3802
3803     # Make sure there is an open circ to renew
3804     my $usrid = $self->patron->id if $self->patron;
3805     my $circ = $self->editor->search_action_circulation({
3806         target_copy => $self->copy->id,
3807         xact_finish => undef,
3808         checkin_time => undef,
3809         ($usrid ? (usr => $usrid) : ())
3810     })->[0];
3811
3812     return $self->bail_on_events($self->editor->event) unless $circ;
3813
3814     # A user is not allowed to renew another user's items without permission
3815     unless( $circ->usr eq $self->editor->requestor->id ) {
3816         return $self->bail_on_events($self->editor->events)
3817             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3818     }   
3819
3820     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3821         if $circ->renewal_remaining < 1;
3822
3823     # -----------------------------------------------------------------
3824
3825     $self->parent_circ($circ->id);
3826     $self->renewal_remaining( $circ->renewal_remaining - 1 );
3827     $self->circ($circ);
3828
3829     # Opac renewal - re-use circ library from original circ (unless told not to)
3830     if($self->opac_renewal) {
3831         unless(defined($opac_renewal_use_circ_lib)) {
3832             my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3833             if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3834                 $opac_renewal_use_circ_lib = 1;
3835             }
3836             else {
3837                 $opac_renewal_use_circ_lib = 0;
3838             }
3839         }
3840         $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3841     }
3842
3843     # Desk renewal - re-use circ library from original circ (unless told not to)
3844     if($self->desk_renewal) {
3845         unless(defined($desk_renewal_use_circ_lib)) {
3846             my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3847             if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3848                 $desk_renewal_use_circ_lib = 1;
3849             }
3850             else {
3851                 $desk_renewal_use_circ_lib = 0;
3852             }
3853         }
3854         $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3855     }
3856
3857     # Run the fine generator against the old circ
3858     # XXX This seems unnecessary, given that handle_fines runs in do_checkin
3859     # a few lines down.  Commenting out, for now.
3860     #$self->handle_fines;
3861
3862     $self->run_renew_permit;
3863
3864     # Check the item in
3865     $self->do_checkin();
3866     return if $self->bail_out;
3867
3868     unless( $self->permit_override ) {
3869         $self->do_permit();
3870         return if $self->bail_out;
3871         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3872         $self->remove_event('ITEM_NOT_CATALOGED');
3873     }   
3874
3875     $self->override_events;
3876     return if $self->bail_out;
3877
3878     $self->events([]);
3879     $self->do_checkout();
3880 }
3881
3882
3883 sub remove_event {
3884     my( $self, $evt ) = @_;
3885     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3886     $logger->debug("circulator: removing event from list: $evt");
3887     my @events = @{$self->events};
3888     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3889 }
3890
3891
3892 sub have_event {
3893     my( $self, $evt ) = @_;
3894     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3895     return grep { $_->{textcode} eq $evt } @{$self->events};
3896 }
3897
3898
3899
3900 sub run_renew_permit {
3901     my $self = shift;
3902
3903     if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3904         my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3905             $self->editor, $self->copy, $self->editor->requestor, 1
3906         );
3907         $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3908     }
3909
3910     if(!$self->legacy_script_support) {
3911         my $results = $self->run_indb_circ_test;
3912         $self->push_events($self->matrix_test_result_events)
3913             unless $self->circ_test_success;
3914     } else {
3915
3916         my $runner = $self->script_runner;
3917
3918         $runner->load($self->circ_permit_renew);
3919         my $result = $runner->run or 
3920             throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3921         if ($result->{"events"}) {
3922             $self->push_events(
3923                 map { new OpenILS::Event($_) } @{$result->{"events"}}
3924             );
3925             $logger->activity(
3926                 "circulator: circ_permit_renew for user " .
3927                 $self->patron->id . " returned " .
3928                 scalar(@{$result->{"events"}}) . " event(s)"
3929             );
3930         }
3931
3932         $self->mk_script_runner;
3933     }
3934
3935     $logger->debug("circulator: re-creating script runner to be safe");
3936 }
3937
3938
3939 # XXX: The primary mechanism for storing circ history is now handled
3940 # by tracking real circulation objects instead of bibs in a bucket.
3941 # However, this code is disabled by default and could be useful 
3942 # some day, so may as well leave it for now.
3943 sub append_reading_list {
3944     my $self = shift;
3945
3946     return undef unless 
3947         $self->is_checkout and 
3948         $self->patron and 
3949         $self->copy and 
3950         !$self->is_noncat;
3951
3952
3953     # verify history is globally enabled and uses the bucket mechanism
3954     my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3955         apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3956
3957     return undef unless $htype and $htype eq 'bucket';
3958
3959     my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3960
3961     # verify the patron wants to retain the hisory
3962     my $setting = $e->search_actor_user_setting(
3963         {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3964     
3965     unless($setting and $setting->value) {
3966         $e->rollback;
3967         return undef;
3968     }
3969
3970     my $bkt = $e->search_container_copy_bucket(
3971         {owner => $self->patron->id, btype => 'circ_history'})->[0];
3972
3973     my $pos = 1;
3974
3975     if($bkt) {
3976         # find the next item position
3977         my $last_item = $e->search_container_copy_bucket_item(
3978             {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3979         $pos = $last_item->pos + 1 if $last_item;
3980
3981     } else {
3982         # create the history bucket if necessary
3983         $bkt = Fieldmapper::container::copy_bucket->new;
3984         $bkt->owner($self->patron->id);
3985         $bkt->name('');
3986         $bkt->btype('circ_history');
3987         $bkt->pub('f');
3988         $e->create_container_copy_bucket($bkt) or return $e->die_event;
3989     }
3990
3991     my $item = Fieldmapper::container::copy_bucket_item->new;
3992
3993     $item->bucket($bkt->id);
3994     $item->target_copy($self->copy->id);
3995     $item->pos($pos);
3996
3997     $e->create_container_copy_bucket_item($item) or return $e->die_event;
3998     $e->commit;
3999
4000     return undef;
4001 }
4002
4003
4004 sub make_trigger_events {
4005     my $self = shift;
4006     return unless $self->circ;
4007     $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
4008     $U->create_events_for_hook('checkin',  $self->circ, $self->circ_lib) if $self->is_checkin;
4009     $U->create_events_for_hook('renewal',  $self->circ, $self->circ_lib) if $self->is_renewal;
4010 }
4011
4012
4013
4014 sub checkin_handle_lost_or_lo_now_found {
4015     my ($self, $bill_type, $is_longoverdue) = @_;
4016
4017     # ------------------------------------------------------------------
4018     # remove charge from patron's account if lost item is returned
4019     # ------------------------------------------------------------------
4020
4021     my $bills = $self->editor->search_money_billing(
4022         {
4023             xact => $self->circ->id,
4024             btype => $bill_type
4025         }
4026     );
4027
4028     my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4029     
4030     $logger->debug("voiding ".scalar(@$bills)." $tag item billings");
4031     for my $bill (@$bills) {
4032         if( !$U->is_true($bill->voided) ) {
4033             $logger->info("$tag item returned - voiding bill ".$bill->id);
4034             $bill->voided('t');
4035             $bill->void_time('now');
4036             $bill->voider($self->editor->requestor->id);
4037             my $note = ($bill->note) ? $bill->note . "\n" : '';
4038             $bill->note("${note}System: VOIDED FOR $tag ITEM RETURNED");
4039
4040             $self->bail_on_events($self->editor->event)
4041                 unless $self->editor->update_money_billing($bill);
4042         }
4043     }
4044 }
4045
4046 sub checkin_handle_lost_or_lo_now_found_restore_od {
4047     my $self = shift;
4048     my $circ_lib = shift;
4049     my $is_longoverdue = shift;
4050     my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4051
4052     # ------------------------------------------------------------------
4053     # restore those overdue charges voided when item was set to lost
4054     # ------------------------------------------------------------------
4055
4056     my $ods = $self->editor->search_money_billing(
4057         {
4058             xact => $self->circ->id,
4059             btype => 1
4060         }
4061     );
4062
4063     $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
4064     for my $bill (@$ods) {
4065         if( $U->is_true($bill->voided) ) {
4066                 $logger->info("$tag item returned - restoring overdue ".$bill->id);
4067                 $bill->voided('f');
4068                 $bill->clear_void_time;
4069                 $bill->voider($self->editor->requestor->id);
4070                 my $note = ($bill->note) ? $bill->note . "\n" : '';
4071                 $bill->note("${note}System: $tag RETURNED - OVERDUES REINSTATED");
4072
4073                 $self->bail_on_events($self->editor->event)
4074                         unless $self->editor->update_money_billing($bill);
4075         }
4076     }
4077 }
4078
4079 1;