]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Fix Lp bug 965656 WRT back dated checkins with fines.
[working/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'], 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         if ($circulator->generate_lost_overdue) {
352             # Generating additional overdue billings has to happen after the 
353             # main commit and before the final respond() so the caller can
354             # receive the latest transaction summary.
355             my $evt = $circulator->generate_lost_overdue_fines;
356             $circulator->bail_on_events($evt) if $evt;
357         }
358     }
359     
360     $conn->respond_complete(circ_events($circulator));
361
362     $circulator->script_runner->cleanup if $circulator->script_runner;
363
364     return undef if $circulator->bail_out;
365
366     $circulator->do_hold_notify($circulator->notify_hold)
367         if $circulator->notify_hold;
368     $circulator->retarget_holds if $circulator->retarget;
369     $circulator->append_reading_list;
370     $circulator->make_trigger_events;
371     
372     return undef;
373 }
374
375 sub circ_events {
376     my $circ = shift;
377     my @e = @{$circ->events};
378     # if we have multiple events, SUCCESS should not be one of them;
379     @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
380     return (@e == 1) ? $e[0] : \@e;
381 }
382
383
384 sub translate_legacy_args {
385     my $args = shift;
386
387     if( $$args{barcode} ) {
388         $$args{copy_barcode} = $$args{barcode};
389         delete $$args{barcode};
390     }
391
392     if( $$args{copyid} ) {
393         $$args{copy_id} = $$args{copyid};
394         delete $$args{copyid};
395     }
396
397     if( $$args{patronid} ) {
398         $$args{patron_id} = $$args{patronid};
399         delete $$args{patronid};
400     }
401
402     if( $$args{patron} and !ref($$args{patron}) ) {
403         $$args{patron_id} = $$args{patron};
404         delete $$args{patron};
405     }
406
407
408     if( $$args{noncat} ) {
409         $$args{is_noncat} = $$args{noncat};
410         delete $$args{noncat};
411     }
412
413     if( $$args{precat} ) {
414         $$args{is_precat} = $$args{request_precat} = $$args{precat};
415         delete $$args{precat};
416     }
417 }
418
419
420
421 # --------------------------------------------------------------------------
422 # This package actually manages all of the circulation logic
423 # --------------------------------------------------------------------------
424 package OpenILS::Application::Circ::Circulator;
425 use strict; use warnings;
426 use vars q/$AUTOLOAD/;
427 use DateTime;
428 use OpenILS::Utils::Fieldmapper;
429 use OpenSRF::Utils::Cache;
430 use Digest::MD5 qw(md5_hex);
431 use DateTime::Format::ISO8601;
432 use OpenILS::Utils::PermitHold;
433 use OpenSRF::Utils qw/:datetime/;
434 use OpenSRF::Utils::SettingsClient;
435 use OpenILS::Application::Circ::Holds;
436 use OpenILS::Application::Circ::Transit;
437 use OpenSRF::Utils::Logger qw(:logger);
438 use OpenILS::Utils::CStoreEditor qw/:funcs/;
439 use OpenILS::Application::Circ::ScriptBuilder;
440 use OpenILS::Const qw/:const/;
441 use OpenILS::Utils::Penalty;
442 use OpenILS::Application::Circ::CircCommon;
443 use Time::Local;
444
445 my $holdcode    = "OpenILS::Application::Circ::Holds";
446 my $transcode   = "OpenILS::Application::Circ::Transit";
447 my %user_groups;
448
449 sub DESTROY { }
450
451
452 # --------------------------------------------------------------------------
453 # Add a pile of automagic getter/setter methods
454 # --------------------------------------------------------------------------
455 my @AUTOLOAD_FIELDS = qw/
456     notify_hold
457     remote_hold
458     backdate
459     reservation
460     copy
461     copy_id
462     copy_barcode
463     patron
464     patron_id
465     patron_barcode
466     script_runner
467     volume
468     title
469     is_renewal
470     is_checkout
471     is_res_checkout
472     is_precat
473     is_noncat
474     request_precat
475     is_checkin
476     is_res_checkin
477     noncat_type
478     editor
479     events
480     cache_handle
481     override
482     circ_permit_patron
483     circ_permit_copy
484     circ_duration
485     circ_recurring_fines
486     circ_max_fines
487     circ_permit_renew
488     circ
489     transit
490     hold
491     permit_key
492     noncat_circ_lib
493     noncat_count
494     checkout_time
495     dummy_title
496     dummy_author
497     dummy_isbn
498     circ_modifier
499     circ_lib
500     barcode
501     duration_level
502     recurring_fines_level
503     duration_rule
504     recurring_fines_rule
505     max_fine_rule
506     renewal_remaining
507     hard_due_date
508     due_date
509     fulfilled_holds
510     transit
511     checkin_changed
512     force
513     permit_override
514     pending_checkouts
515     cancelled_hold_transit
516     opac_renewal
517     phone_renewal
518     desk_renewal
519     sip_renewal
520     retarget
521     matrix_test_result
522     circ_matrix_matchpoint
523     circ_test_success
524     legacy_script_support
525     is_deposit
526     is_rental
527     deposit_billing
528     rental_billing
529     capture
530     noop
531     void_overdues
532     parent_circ
533     return_patron
534     claims_never_checked_out
535     skip_permit_key
536     skip_deposit_fee
537     skip_rental_fee
538     use_booking
539     generate_lost_overdue
540     clear_expired
541     retarget_mode
542     hold_as_transit
543     fake_hold_dest
544     limit_groups
545     override_args
546     checkout_is_for_hold
547 /;
548
549
550 sub AUTOLOAD {
551     my $self = shift;
552     my $type = ref($self) or die "$self is not an object";
553     my $data = shift;
554     my $name = $AUTOLOAD;
555     $name =~ s/.*://o;   
556
557     unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
558         $logger->error("circulator: $type: invalid autoload field: $name");
559         die "$type: invalid autoload field: $name\n" 
560     }
561
562     {
563         no strict 'refs';
564         *{"${type}::${name}"} = sub {
565             my $s = shift;
566             my $v = shift;
567             $s->{$name} = $v if defined $v;
568             return $s->{$name};
569         }
570     }
571     return $self->$name($data);
572 }
573
574
575 sub new {
576     my( $class, $auth, %args ) = @_;
577     $class = ref($class) || $class;
578     my $self = bless( {}, $class );
579
580     $self->events([]);
581     $self->editor(new_editor(xact => 1, authtoken => $auth));
582
583     unless( $self->editor->checkauth ) {
584         $self->bail_on_events($self->editor->event);
585         return $self;
586     }
587
588     $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
589
590     $self->$_($args{$_}) for keys %args;
591
592     $self->circ_lib(
593         ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
594
595     # if this is a renewal, default to desk_renewal
596     $self->desk_renewal(1) unless 
597         $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
598
599     $self->capture('') unless $self->capture;
600
601     unless(%user_groups) {
602         my $gps = $self->editor->retrieve_all_permission_grp_tree;
603         %user_groups = map { $_->id => $_ } @$gps;
604     }
605
606     return $self;
607 }
608
609
610 # --------------------------------------------------------------------------
611 # True if we should discontinue processing
612 # --------------------------------------------------------------------------
613 sub bail_out {
614     my( $self, $bool ) = @_;
615     if( defined $bool ) {
616         $logger->info("circulator: BAILING OUT") if $bool;
617         $self->{bail_out} = $bool;
618     }
619     return $self->{bail_out};
620 }
621
622
623 sub push_events {
624     my( $self, @evts ) = @_;
625     for my $e (@evts) {
626         next unless $e;
627         $e->{payload} = $self->copy if 
628               ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
629
630         $logger->info("circulator: pushing event ".$e->{textcode});
631         push( @{$self->events}, $e ) unless
632             grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
633     }
634 }
635
636 sub mk_permit_key {
637     my $self = shift;
638     return '' if $self->skip_permit_key;
639     my $key = md5_hex( time() . rand() . "$$" );
640     $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
641     return $self->permit_key($key);
642 }
643
644 sub check_permit_key {
645     my $self = shift;
646     return 1 if $self->skip_permit_key;
647     my $key = $self->permit_key;
648     return 0 unless $key;
649     my $k = "oils_permit_key_$key";
650     my $one = $self->cache_handle->get_cache($k);
651     $self->cache_handle->delete_cache($k);
652     return ($one) ? 1 : 0;
653 }
654
655 sub seems_like_reservation {
656     my $self = shift;
657
658     # Some words about the following method:
659     # 1) It requires the VIEW_USER permission, but that's not an
660     # issue, right, since all staff should have that?
661     # 2) It returns only one reservation at a time, even if an item can be
662     # and is currently overbooked.  Hmmm....
663     my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
664     my $result = $booking_ses->request(
665         "open-ils.booking.reservations.by_returnable_resource_barcode",
666         $self->editor->authtoken,
667         $self->copy_barcode
668     )->gather(1);
669     $booking_ses->disconnect;
670
671     return $self->bail_on_events($result) if defined $U->event_code($result);
672
673     if (@$result > 0) {
674         $self->reservation(shift @$result);
675         return 1;
676     } else {
677         return 0;
678     }
679
680 }
681
682 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
683 sub save_trimmed_copy {
684     my ($self, $copy) = @_;
685
686     $self->copy($copy);
687     $self->volume($copy->call_number);
688     $self->title($self->volume->record);
689     $self->copy->call_number($self->volume->id);
690     $self->volume->record($self->title->id);
691     $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
692     if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
693         $self->is_deposit(1) if $U->is_true($self->copy->deposit);
694         $self->is_rental(1) unless $U->is_true($self->copy->deposit);
695     }
696 }
697
698 sub mk_env {
699     my $self = shift;
700     my $e = $self->editor;
701
702     # --------------------------------------------------------------------------
703     # Grab the fleshed copy
704     # --------------------------------------------------------------------------
705     unless($self->is_noncat) {
706         my $copy;
707         if($self->copy_id) {
708             $copy = $e->retrieve_asset_copy(
709                 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
710     
711         } elsif( $self->copy_barcode ) {
712     
713             $copy = $e->search_asset_copy(
714                 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
715         } elsif( $self->reservation ) {
716             my $res = $e->json_query(
717                 {
718                     "select" => {"acp" => ["id"]},
719                     "from" => {
720                         "acp" => {
721                             "brsrc" => {
722                                 "fkey" => "barcode",
723                                 "field" => "barcode",
724                                 "join" => {
725                                     "bresv" => {
726                                         "fkey" => "id",
727                                         "field" => "current_resource"
728                                     }
729                                 }
730                             }
731                         }
732                     },
733                     "where" => {
734                         "+bresv" => {
735                             "id" => (ref $self->reservation) ?
736                                 $self->reservation->id : $self->reservation
737                         }
738                     }
739                 }
740             );
741             if (ref $res eq "ARRAY" and scalar @$res) {
742                 $logger->info("circulator: mapped reservation " .
743                     $self->reservation . " to copy " . $res->[0]->{"id"});
744                 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
745             }
746         }
747     
748         if($copy) {
749             $self->save_trimmed_copy($copy);
750         } else {
751             # We can't renew if there is no copy
752             return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
753                 if $self->is_renewal;
754             $self->is_precat(1);
755         }
756     }
757
758     # --------------------------------------------------------------------------
759     # Grab the patron
760     # --------------------------------------------------------------------------
761     my $patron;
762     my $flesh = {
763         flesh => 1,
764         flesh_fields => {au => [ qw/ card / ]}
765     };
766
767     if( $self->patron_id ) {
768         $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
769             or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
770
771     } elsif( $self->patron_barcode ) {
772
773         # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
774         my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0] 
775             or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
776
777         $patron = $e->retrieve_actor_user($card->usr)
778             or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
779
780         # Use the card we looked up, not the patron's primary, for card active checks
781         $patron->card($card);
782
783     } else {
784         if( my $copy = $self->copy ) {
785
786             $flesh->{flesh} = 2;
787             $flesh->{flesh_fields}->{circ} = ['usr'];
788
789             my $circ = $e->search_action_circulation([
790                 {target_copy => $copy->id, checkin_time => undef}, $flesh
791             ])->[0];
792
793             if($circ) {
794                 $patron = $circ->usr;
795                 $circ->usr($patron->id); # de-flesh for consistency
796                 $self->circ($circ); 
797             }
798         }
799     }
800
801     return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
802         unless $self->patron($patron) or $self->is_checkin;
803
804     unless($self->is_checkin) {
805
806         # Check for inactivity and patron reg. expiration
807
808         $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
809             unless $U->is_true($patron->active);
810     
811         $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
812             unless $U->is_true($patron->card->active);
813     
814         my $expire = DateTime::Format::ISO8601->new->parse_datetime(
815             cleanse_ISO8601($patron->expire_date));
816     
817         $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
818             if( CORE::time > $expire->epoch ) ;
819     }
820 }
821
822 # --------------------------------------------------------------------------
823 # This builds the script runner environment and fetches most of the
824 # objects we need
825 # --------------------------------------------------------------------------
826 sub mk_script_runner {
827     my $self = shift;
828     my $args = {};
829
830
831     my @fields = 
832         qw/copy copy_barcode copy_id patron 
833             patron_id patron_barcode volume title editor/;
834
835     # Translate our objects into the ScriptBuilder args hash
836     $$args{$_} = $self->$_() for @fields;
837
838     $args->{ignore_user_status} = 1 if $self->is_checkin;
839     $$args{fetch_patron_by_circ_copy} = 1;
840     $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
841
842     if( my $pco = $self->pending_checkouts ) {
843         $logger->info("circulator: we were given a pending checkouts number of $pco");
844         $$args{patronItemsOut} = $pco;
845     }
846
847     # This fetches most of the objects we need
848     $self->script_runner(
849         OpenILS::Application::Circ::ScriptBuilder->build($args));
850
851     # Now we translate the ScriptBuilder objects back into self
852     $self->$_($$args{$_}) for @fields;
853
854     my @evts = @{$args->{_events}} if $args->{_events};
855
856     $logger->debug("circulator: script builder returned events: @evts") if @evts;
857
858
859     if(@evts) {
860         # Anything besides ASSET_COPY_NOT_FOUND will stop processing
861         if(!$self->is_noncat and 
862             @evts == 1 and 
863             $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
864                 $self->is_precat(1);
865
866         } else {
867             my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
868             return $self->bail_on_events(@e);
869         }
870     }
871
872     if($self->copy) {
873         $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
874         if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
875             $self->is_deposit(1) if $U->is_true($self->copy->deposit);
876             $self->is_rental(1) unless $U->is_true($self->copy->deposit);
877         }
878     }
879
880     # We can't renew if there is no copy
881     return $self->bail_on_events(@evts) if 
882         $self->is_renewal and !$self->copy;
883
884     # Set some circ-specific flags in the script environment
885     my $evt = "environment";
886     $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
887
888     if( $self->is_noncat ) {
889       $self->script_runner->insert("$evt.isNonCat", 1);
890       $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
891     }
892
893     if( $self->is_precat ) {
894         $self->script_runner->insert("environment.isPrecat", 1, 1);
895     }
896
897     $self->script_runner->add_path( $_ ) for @$script_libs;
898
899     return 1;
900 }
901
902 # --------------------------------------------------------------------------
903 # Does the circ permit work
904 # --------------------------------------------------------------------------
905 sub do_permit {
906     my $self = shift;
907
908     $self->log_me("do_permit()");
909
910     unless( $self->editor->requestor->id == $self->patron->id ) {
911         return $self->bail_on_events($self->editor->event)
912             unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
913     }
914
915     $self->check_captured_holds();
916     $self->do_copy_checks();
917     return if $self->bail_out;
918     $self->run_patron_permit_scripts();
919     $self->run_copy_permit_scripts() 
920         unless $self->is_precat or $self->is_noncat;
921     $self->check_item_deposit_events();
922     $self->override_events();
923     return if $self->bail_out;
924
925     if($self->is_precat and not $self->request_precat) {
926         $self->push_events(
927             OpenILS::Event->new(
928                 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
929         return $self->bail_out(1) unless $self->is_renewal;
930     }
931
932     $self->push_events(
933         OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
934 }
935
936 sub check_item_deposit_events {
937     my $self = shift;
938     $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy)) 
939         if $self->is_deposit and not $self->is_deposit_exempt;
940     $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy)) 
941         if $self->is_rental and not $self->is_rental_exempt;
942 }
943
944 # returns true if the user is not required to pay deposits
945 sub is_deposit_exempt {
946     my $self = shift;
947     my $pid = (ref $self->patron->profile) ?
948         $self->patron->profile->id : $self->patron->profile;
949     my $groups = $U->ou_ancestor_setting_value(
950         $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
951     for my $grp (@$groups) {
952         return 1 if $self->is_group_descendant($grp, $pid);
953     }
954     return 0;
955 }
956
957 # returns true if the user is not required to pay rental fees
958 sub is_rental_exempt {
959     my $self = shift;
960     my $pid = (ref $self->patron->profile) ?
961         $self->patron->profile->id : $self->patron->profile;
962     my $groups = $U->ou_ancestor_setting_value(
963         $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
964     for my $grp (@$groups) {
965         return 1 if $self->is_group_descendant($grp, $pid);
966     }
967     return 0;
968 }
969
970 sub is_group_descendant {
971     my($self, $p_id, $c_id) = @_;
972     return 0 unless defined $p_id and defined $c_id;
973     return 1 if $c_id == $p_id;
974     while(my $grp = $user_groups{$c_id}) {
975         $c_id = $grp->parent;
976         return 0 unless defined $c_id;
977         return 1 if $c_id == $p_id;
978     }
979     return 0;
980 }
981
982 sub check_captured_holds {
983     my $self    = shift;
984     my $copy    = $self->copy;
985     my $patron  = $self->patron;
986
987     return undef unless $copy;
988
989     my $s = $U->copy_status($copy->status)->id;
990     return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
991     $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
992
993     # Item is on the holds shelf, make sure it's going to the right person
994     my $hold = $self->editor->search_action_hold_request(
995         [
996             { 
997                 current_copy        => $copy->id , 
998                 capture_time        => { '!=' => undef },
999                 cancel_time         => undef, 
1000                 fulfillment_time    => undef 
1001             },
1002             { limit => 1 }
1003         ]
1004     )->[0];
1005
1006     if ($hold and $hold->usr == $patron->id) {
1007         $self->checkout_is_for_hold(1);
1008         return undef;
1009     }
1010
1011     $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1012
1013     $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1014 }
1015
1016
1017 sub do_copy_checks {
1018     my $self = shift;
1019     my $copy = $self->copy;
1020     return unless $copy;
1021
1022     my $stat = $U->copy_status($copy->status)->id;
1023
1024     # We cannot check out a copy if it is in-transit
1025     if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1026         return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1027     }
1028
1029     $self->handle_claims_returned();
1030     return if $self->bail_out;
1031
1032     # no claims returned circ was found, check if there is any open circ
1033     unless( $self->is_renewal ) {
1034
1035         my $circs = $self->editor->search_action_circulation(
1036             { target_copy => $copy->id, checkin_time => undef }
1037         );
1038
1039         if(my $old_circ = $circs->[0]) { # an open circ was found
1040
1041             my $payload = {copy => $copy};
1042
1043             if($old_circ->usr == $self->patron->id) {
1044                 
1045                 $payload->{old_circ} = $old_circ;
1046
1047                 # If there is an open circulation on the checkout item and an auto-renew 
1048                 # interval is defined, inform the caller that they should go 
1049                 # ahead and renew the item instead of warning about open circulations.
1050     
1051                 my $auto_renew_intvl = $U->ou_ancestor_setting_value(        
1052                     $self->circ_lib,
1053                     'circ.checkout_auto_renew_age', 
1054                     $self->editor
1055                 );
1056
1057                 if($auto_renew_intvl) {
1058                     my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1059                     my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1060
1061                     if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1062                         $payload->{auto_renew} = 1;
1063                     }
1064                 }
1065             }
1066
1067             return $self->bail_on_events(
1068                 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1069             );
1070         }
1071     }
1072 }
1073
1074 my $LEGACY_CIRC_EVENT_MAP = {
1075     'no_item' => 'ITEM_NOT_CATALOGED',
1076     'actor.usr.barred' => 'PATRON_BARRED',
1077     'asset.copy.circulate' =>  'COPY_CIRC_NOT_ALLOWED',
1078     'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1079     'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1080     'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1081     'config.circ_matrix_test.max_items_out' =>  'PATRON_EXCEEDS_CHECKOUT_COUNT',
1082     'config.circ_matrix_test.max_overdue' =>  'PATRON_EXCEEDS_OVERDUE_COUNT',
1083     'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1084     'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
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    # pre/non-cats can't fulfill a hold
1720    return if $self->is_precat or $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     $holdcode->delete_hold_copy_maps($e, $hold->id);
1771     return $self->fulfilled_holds([$hold->id]);
1772 }
1773
1774
1775 # ------------------------------------------------------------------------------
1776 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1777 # the patron directly targets the checked out item, see if there is another hold 
1778 # for the patron that could be fulfilled by the checked out item.  Fulfill the
1779 # oldest hold and only fulfill 1 of them.
1780
1781 # For "another hold":
1782 #
1783 # First, check for one that the copy matches via hold_copy_map, ensuring that
1784 # *any* hold type that this copy could fill may end up filled.
1785 #
1786 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1787 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1788 # that are non-requestable to count as capturing those hold types.
1789 # ------------------------------------------------------------------------------
1790 sub find_related_user_hold {
1791     my($self, $copy, $patron) = @_;
1792     my $e = $self->editor;
1793
1794     return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER; 
1795
1796     return undef unless $U->ou_ancestor_setting_value(        
1797         $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1798
1799     # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1800     my $args = {
1801         select => {ahr => ['id']}, 
1802         from => {
1803             ahr => {
1804                 ahcm => {
1805                     field => 'hold',
1806                     fkey => 'id'
1807                 },
1808                 acp => {
1809                     field => 'id', 
1810                     fkey => 'current_copy',
1811                     type => 'left' # there may be no current_copy
1812                 }
1813             }
1814         }, 
1815         where => {
1816             '+ahr' => {
1817                 usr => $patron->id,
1818                 fulfillment_time => undef,
1819                 cancel_time => undef,
1820                '-or' => [
1821                     {expire_time => undef},
1822                     {expire_time => {'>' => 'now'}}
1823                 ]
1824             },
1825             '+ahcm' => {
1826                 target_copy => $self->copy->id
1827             },
1828             '+acp' => {
1829                 '-or' => [
1830                     {id => undef}, # left-join copy may be nonexistent
1831                     {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1832                 ]
1833             }
1834         },
1835         order_by => {ahr => {request_time => {direction => 'asc'}}},
1836         limit => 1
1837     };
1838
1839     my $hold_info = $e->json_query($args)->[0];
1840     return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1841     return undef if $U->ou_ancestor_setting_value(        
1842         $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1843
1844     # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1845     $args = {
1846         select => {ahr => ['id']}, 
1847         from => {
1848             ahr => {
1849                 acp => {
1850                     field => 'id', 
1851                     fkey => 'current_copy',
1852                     type => 'left' # there may be no current_copy
1853                 }
1854             }
1855         }, 
1856         where => {
1857             '+ahr' => {
1858                 usr => $patron->id,
1859                 fulfillment_time => undef,
1860                 cancel_time => undef,
1861                '-or' => [
1862                     {expire_time => undef},
1863                     {expire_time => {'>' => 'now'}}
1864                 ]
1865             },
1866             '-or' => [
1867                 {
1868                     '+ahr' => { 
1869                         hold_type => 'V',
1870                         target => $self->volume->id
1871                     }
1872                 },
1873                 { 
1874                     '+ahr' => { 
1875                         hold_type => 'T',
1876                         target => $self->title->id
1877                     }
1878                 },
1879             ],
1880             '+acp' => {
1881                 '-or' => [
1882                     {id => undef}, # left-join copy may be nonexistent
1883                     {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1884                 ]
1885             }
1886         },
1887         order_by => {ahr => {request_time => {direction => 'asc'}}},
1888         limit => 1
1889     };
1890
1891     $hold_info = $e->json_query($args)->[0];
1892     return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1893     return undef;
1894 }
1895
1896
1897 sub run_checkout_scripts {
1898     my $self = shift;
1899     my $nobail = shift;
1900
1901     my $evt;
1902     my $runner = $self->script_runner;
1903
1904     my $duration;
1905     my $recurring;
1906     my $max_fine;
1907     my $hard_due_date;
1908     my $duration_name;
1909     my $recurring_name;
1910     my $max_fine_name;
1911     my $hard_due_date_name;
1912
1913     if(!$self->legacy_script_support) {
1914         $self->run_indb_circ_test();
1915         $duration = $self->circ_matrix_matchpoint->duration_rule;
1916         $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1917         $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1918         $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1919
1920     } else {
1921
1922        $runner->load($self->circ_duration);
1923
1924        my $result = $runner->run or 
1925             throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1926
1927        $duration_name   = $result->{durationRule};
1928        $recurring_name  = $result->{recurringFinesRule};
1929        $max_fine_name   = $result->{maxFine};
1930        $hard_due_date_name  = $result->{hardDueDate};
1931     }
1932
1933     $duration_name = $duration->name if $duration;
1934     if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1935
1936         unless($duration) {
1937             ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1938             return $self->bail_on_events($evt) if ($evt && !$nobail);
1939         
1940             ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1941             return $self->bail_on_events($evt) if ($evt && !$nobail);
1942         
1943             ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1944             return $self->bail_on_events($evt) if ($evt && !$nobail);
1945
1946             if($hard_due_date_name) {
1947                 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1948                 return $self->bail_on_events($evt) if ($evt && !$nobail);
1949             }
1950         }
1951
1952     } else {
1953
1954         # The item circulates with an unlimited duration
1955         $duration   = undef;
1956         $recurring  = undef;
1957         $max_fine   = undef;
1958         $hard_due_date = undef;
1959     }
1960
1961    $self->duration_rule($duration);
1962    $self->recurring_fines_rule($recurring);
1963    $self->max_fine_rule($max_fine);
1964    $self->hard_due_date($hard_due_date);
1965 }
1966
1967
1968 sub build_checkout_circ_object {
1969     my $self = shift;
1970
1971    my $circ       = Fieldmapper::action::circulation->new;
1972    my $duration   = $self->duration_rule;
1973    my $max        = $self->max_fine_rule;
1974    my $recurring  = $self->recurring_fines_rule;
1975    my $hard_due_date    = $self->hard_due_date;
1976    my $copy       = $self->copy;
1977    my $patron     = $self->patron;
1978    my $duration_date_ceiling;
1979    my $duration_date_ceiling_force;
1980
1981     if( $duration ) {
1982
1983         my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1984         $duration_date_ceiling = $policy->{duration_date_ceiling};
1985         $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1986
1987         my $dname = $duration->name;
1988         my $mname = $max->name;
1989         my $rname = $recurring->name;
1990         my $hdname = ''; 
1991         if($hard_due_date) {
1992             $hdname = $hard_due_date->name;
1993         }
1994
1995         $logger->debug("circulator: building circulation ".
1996             "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1997     
1998         $circ->duration($policy->{duration});
1999         $circ->recurring_fine($policy->{recurring_fine});
2000         $circ->duration_rule($duration->name);
2001         $circ->recurring_fine_rule($recurring->name);
2002         $circ->max_fine_rule($max->name);
2003         $circ->max_fine($policy->{max_fine});
2004         $circ->fine_interval($recurring->recurrence_interval);
2005         $circ->renewal_remaining($duration->max_renewals);
2006         $circ->grace_period($policy->{grace_period});
2007
2008     } else {
2009
2010         $logger->info("circulator: copy found with an unlimited circ duration");
2011         $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2012         $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2013         $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2014         $circ->renewal_remaining(0);
2015         $circ->grace_period(0);
2016     }
2017
2018    $circ->target_copy( $copy->id );
2019    $circ->usr( $patron->id );
2020    $circ->circ_lib( $self->circ_lib );
2021    $circ->workstation($self->editor->requestor->wsid) 
2022     if defined $self->editor->requestor->wsid;
2023
2024     # renewals maintain a link to the parent circulation
2025     $circ->parent_circ($self->parent_circ);
2026
2027    if( $self->is_renewal ) {
2028       $circ->opac_renewal('t') if $self->opac_renewal;
2029       $circ->phone_renewal('t') if $self->phone_renewal;
2030       $circ->desk_renewal('t') if $self->desk_renewal;
2031       $circ->renewal_remaining($self->renewal_remaining);
2032       $circ->circ_staff($self->editor->requestor->id);
2033    }
2034
2035
2036     # if the user provided an overiding checkout time,
2037     # (e.g. the checkout really happened several hours ago), then
2038     # we apply that here.  Does this need a perm??
2039     $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2040         if $self->checkout_time;
2041
2042     # if a patron is renewing, 'requestor' will be the patron
2043     $circ->circ_staff($self->editor->requestor->id);
2044     $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
2045
2046     $self->circ($circ);
2047 }
2048
2049 sub do_reservation_pickup {
2050     my $self = shift;
2051
2052     $self->log_me("do_reservation_pickup()");
2053
2054     $self->reservation->pickup_time('now');
2055
2056     if (
2057         $self->reservation->current_resource &&
2058         $U->is_true($self->reservation->target_resource_type->catalog_item)
2059     ) {
2060         # We used to try to set $self->copy and $self->patron here,
2061         # but that should already be done.
2062
2063         $self->run_checkout_scripts(1);
2064
2065         my $duration   = $self->duration_rule;
2066         my $max        = $self->max_fine_rule;
2067         my $recurring  = $self->recurring_fines_rule;
2068
2069         if ($duration && $max && $recurring) {
2070             my $policy = $self->get_circ_policy($duration, $recurring, $max);
2071
2072             my $dname = $duration->name;
2073             my $mname = $max->name;
2074             my $rname = $recurring->name;
2075
2076             $logger->debug("circulator: updating reservation ".
2077                 "with duration=$dname, maxfine=$mname, recurring=$rname");
2078
2079             $self->reservation->fine_amount($policy->{recurring_fine});
2080             $self->reservation->max_fine($policy->{max_fine});
2081             $self->reservation->fine_interval($recurring->recurrence_interval);
2082         }
2083
2084         $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2085         $self->update_copy();
2086
2087     } else {
2088         $self->reservation->fine_amount(
2089             $self->reservation->target_resource_type->fine_amount
2090         );
2091         $self->reservation->max_fine(
2092             $self->reservation->target_resource_type->max_fine
2093         );
2094         $self->reservation->fine_interval(
2095             $self->reservation->target_resource_type->fine_interval
2096         );
2097     }
2098
2099     $self->update_reservation();
2100 }
2101
2102 sub do_reservation_return {
2103     my $self = shift;
2104     my $request = shift;
2105
2106     $self->log_me("do_reservation_return()");
2107
2108     if (not ref $self->reservation) {
2109         my ($reservation, $evt) =
2110             $U->fetch_booking_reservation($self->reservation);
2111         return $self->bail_on_events($evt) if $evt;
2112         $self->reservation($reservation);
2113     }
2114
2115     $self->generate_fines(1);
2116     $self->reservation->return_time('now');
2117     $self->update_reservation();
2118     $self->reshelve_copy if $self->copy;
2119
2120     if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2121         $self->copy( $self->reservation->current_resource->catalog_item );
2122     }
2123 }
2124
2125 sub booking_adjusted_due_date {
2126     my $self = shift;
2127     my $circ = $self->circ;
2128     my $copy = $self->copy;
2129
2130     return undef unless $self->use_booking;
2131
2132     my $changed;
2133
2134     if( $self->due_date ) {
2135
2136         return $self->bail_on_events($self->editor->event)
2137             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2138
2139        $circ->due_date(cleanse_ISO8601($self->due_date));
2140
2141     } else {
2142
2143         return unless $copy and $circ->due_date;
2144     }
2145
2146     my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2147     if (@$booking_items) {
2148         my $booking_item = $booking_items->[0];
2149         my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2150
2151         my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2152         my $shorten_circ_setting = $resource_type->elbow_room ||
2153             $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2154             '0 seconds';
2155
2156         my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2157         my $bookings = $booking_ses->request(
2158             'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2159             { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2160         )->gather(1);
2161         $booking_ses->disconnect;
2162         
2163         my $dt_parser = DateTime::Format::ISO8601->new;
2164         my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2165
2166         for my $bid (@$bookings) {
2167
2168             my $booking = $self->editor->retrieve_booking_reservation( $bid );
2169
2170             my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2171             my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2172
2173             return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2174                 if ($booking_start < DateTime->now);
2175
2176
2177             if ($U->is_true($stop_circ_setting)) {
2178                 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ); 
2179             } else {
2180                 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2181                 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now); 
2182             }
2183             
2184             # We set the circ duration here only to affect the logic that will
2185             # later (in a DB trigger) mangle the time part of the due date to
2186             # 11:59pm. Having any circ duration that is not a whole number of
2187             # days is enough to prevent the "correction."
2188             my $new_circ_duration = $due_date->epoch - time;
2189             $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2190             $circ->duration("$new_circ_duration seconds");
2191
2192             $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2193             $changed = 1;
2194         }
2195
2196         return $self->bail_on_events($self->editor->event)
2197             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2198     }
2199
2200     return $changed;
2201 }
2202
2203 sub apply_modified_due_date {
2204     my $self = shift;
2205     my $shift_earlier = shift;
2206     my $circ = $self->circ;
2207     my $copy = $self->copy;
2208
2209    if( $self->due_date ) {
2210
2211         return $self->bail_on_events($self->editor->event)
2212             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2213
2214       $circ->due_date(cleanse_ISO8601($self->due_date));
2215
2216    } else {
2217
2218       # if the due_date lands on a day when the location is closed
2219       return unless $copy and $circ->due_date;
2220
2221         #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2222
2223         # due-date overlap should be determined by the location the item
2224         # is checked out from, not the owning or circ lib of the item
2225         my $org = $self->circ_lib;
2226
2227       $logger->info("circulator: circ searching for closed date overlap on lib $org".
2228             " with an item due date of ".$circ->due_date );
2229
2230       my $dateinfo = $U->storagereq(
2231          'open-ils.storage.actor.org_unit.closed_date.overlap', 
2232             $org, $circ->due_date );
2233
2234       if($dateinfo) {
2235          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2236             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2237
2238             # XXX make the behavior more dynamic
2239             # for now, we just push the due date to after the close date
2240             if ($shift_earlier) {
2241                 $circ->due_date($dateinfo->{start});
2242             } else {
2243                 $circ->due_date($dateinfo->{end});
2244             }
2245       }
2246    }
2247 }
2248
2249
2250
2251 sub create_due_date {
2252     my( $self, $duration, $date_ceiling, $force_date ) = @_;
2253
2254     # if there is a raw time component (e.g. from postgres), 
2255     # turn it into an interval that interval_to_seconds can parse
2256     $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2257
2258     # for now, use the server timezone.  TODO: use workstation org timezone
2259     my $due_date = DateTime->now(time_zone => 'local');
2260
2261     # add the circ duration
2262     $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2263
2264     if($date_ceiling) {
2265         my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2266         if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2267             $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2268             $due_date = $cdate;
2269         }
2270     }
2271
2272     # return ISO8601 time with timezone
2273     return $due_date->strftime('%FT%T%z');
2274 }
2275
2276
2277
2278 sub make_precat_copy {
2279     my $self = shift;
2280     my $copy = $self->copy;
2281
2282    if($copy) {
2283         $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2284
2285         $copy->editor($self->editor->requestor->id);
2286         $copy->edit_date('now');
2287         $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2288         $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2289         $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2290         $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2291         $self->update_copy();
2292         return;
2293    }
2294
2295     $logger->info("circulator: Creating a new precataloged ".
2296         "copy in checkout with barcode " . $self->copy_barcode);
2297
2298     $copy = Fieldmapper::asset::copy->new;
2299     $copy->circ_lib($self->circ_lib);
2300     $copy->creator($self->editor->requestor->id);
2301     $copy->editor($self->editor->requestor->id);
2302     $copy->barcode($self->copy_barcode);
2303     $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
2304     $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2305     $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2306
2307     $copy->dummy_title($self->dummy_title || "");
2308     $copy->dummy_author($self->dummy_author || "");
2309     $copy->dummy_isbn($self->dummy_isbn || "");
2310     $copy->circ_modifier($self->circ_modifier);
2311
2312
2313     # See if we need to override the circ_lib for the copy with a configured circ_lib
2314     # Setting is shortname of the org unit
2315     my $precat_circ_lib = $U->ou_ancestor_setting_value(
2316         $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2317
2318     if($precat_circ_lib) {
2319         my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2320
2321         if(!$org) {
2322             $self->bail_on_events($self->editor->event);
2323             return;
2324         }
2325
2326         $copy->circ_lib($org->id);
2327     }
2328
2329
2330     unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2331         $self->bail_out(1);
2332         $self->push_events($self->editor->event);
2333         return;
2334     }   
2335
2336     # this is a little bit of a hack, but we need to 
2337     # get the copy into the script runner
2338     $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2339 }
2340
2341
2342 sub checkout_noncat {
2343     my $self = shift;
2344
2345     my $circ;
2346     my $evt;
2347
2348    my $lib      = $self->noncat_circ_lib || $self->circ_lib;
2349    my $count    = $self->noncat_count || 1;
2350    my $cotime   = cleanse_ISO8601($self->checkout_time) || "";
2351
2352    $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2353
2354    for(1..$count) {
2355
2356       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2357          $self->editor->requestor->id, 
2358             $self->patron->id, 
2359             $lib, 
2360             $self->noncat_type, 
2361             $cotime,
2362             $self->editor );
2363
2364         if( $evt ) {
2365             $self->push_events($evt);
2366             $self->bail_out(1);
2367             return; 
2368         }
2369         $self->circ($circ);
2370    }
2371 }
2372
2373 # If a copy goes into transit and is then checked in before the transit checkin 
2374 # interval has expired, push an event onto the overridable events list.
2375 sub check_transit_checkin_interval {
2376     my $self = shift;
2377
2378     # only concerned with in-transit items
2379     return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2380
2381     # no interval, no problem
2382     my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2383     return unless $interval;
2384
2385     # capture the transit so we don't have to fetch it again later during checkin
2386     $self->transit(
2387         $self->editor->search_action_transit_copy(
2388             {target_copy => $self->copy->id, dest_recv_time => undef}
2389         )->[0]
2390     ); 
2391
2392     # transit from X to X for whatever reason has no min interval
2393     return if $self->transit->source == $self->transit->dest;
2394
2395     my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2396     my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2397     my $horizon = $t_start->add(seconds => $seconds);
2398
2399     # See if we are still within the transit checkin forbidden range
2400     $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK')) 
2401         if $horizon > DateTime->now;
2402 }
2403
2404 # Retarget local holds at checkin
2405 sub checkin_retarget {
2406     my $self = shift;
2407     return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2408     return unless $self->is_checkin; # Renewals need not be checked
2409     return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2410     return if $self->is_precat; # No holds for precats
2411     return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2412     return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2413     my $status = $U->copy_status($self->copy->status);
2414     return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2415     # Specifically target items that are likely new (by status ID)
2416     return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2417     my $location = $self->copy->location;
2418     if(!ref($location)) {
2419         $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2420         $self->copy->location($location);
2421     }
2422     return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2423
2424     # Fetch holds for the bib
2425     my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2426                     $self->editor->authtoken,
2427                     $self->title->id,
2428                     {
2429                         capture_time => undef, # No touching captured holds
2430                         frozen => 'f', # Don't bother with frozen holds
2431                         pickup_lib => $self->circ_lib # Only holds actually here
2432                     }); 
2433
2434     # Error? Skip the step.
2435     return if exists $result->{"ilsevent"};
2436
2437     # Assemble holds
2438     my $holds = [];
2439     foreach my $holdlist (keys %{$result}) {
2440         push @$holds, @{$result->{$holdlist}};
2441     }
2442
2443     return if scalar(@$holds) == 0; # No holds, no retargeting
2444
2445     # Check for parts on this copy
2446     my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2447     my %parts_hash = ();
2448     %parts_hash = map {$_->part, 1} @$parts if @$parts;
2449
2450     # Loop over holds in request-ish order
2451     # Stage 1: Get them into request-ish order
2452     # Also grab type and target for skipping low hanging ones
2453     $result = $self->editor->json_query({
2454         "select" => { "ahr" => ["id", "hold_type", "target"] },
2455         "from" => { "ahr" => { "au" => { "fkey" => "usr",  "join" => "pgt"} } },
2456         "where" => { "id" => $holds },
2457         "order_by" => [
2458             { "class" => "pgt", "field" => "hold_priority"},
2459             { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2460             { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2461             { "class" => "ahr", "field" => "request_time"}
2462         ]
2463     });
2464
2465     # Stage 2: Loop!
2466     if (ref $result eq "ARRAY" and scalar @$result) {
2467         foreach (@{$result}) {
2468             # Copy level, but not this copy?
2469             next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2470                 and $_->{target} != $self->copy->id);
2471             # Volume level, but not this volume?
2472             next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2473             if(@$parts) { # We have parts?
2474                 # Skip title holds
2475                 next if ($_->{hold_type} eq 'T');
2476                 # Skip part holds for parts not on this copy
2477                 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2478             } else {
2479                 # No parts, no part holds
2480                 next if ($_->{hold_type} eq 'P');
2481             }
2482             # So much for easy stuff, attempt a retarget!
2483             my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2484             if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2485                 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2486             }
2487         }
2488     }
2489 }
2490
2491 sub do_checkin {
2492     my $self = shift;
2493     $self->log_me("do_checkin()");
2494
2495     return $self->bail_on_events(
2496         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
2497         unless $self->copy;
2498
2499     $self->check_transit_checkin_interval;
2500     $self->checkin_retarget;
2501
2502     # the renew code and mk_env should have already found our circulation object
2503     unless( $self->circ ) {
2504
2505         my $circs = $self->editor->search_action_circulation(
2506             { target_copy => $self->copy->id, checkin_time => undef });
2507
2508         $self->circ($$circs[0]);
2509
2510         # for now, just warn if there are multiple open circs on a copy
2511         $logger->warn("circulator: we have ".scalar(@$circs).
2512             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2513     }
2514
2515     # run the fine generator against this circ, if this circ is there
2516     $self->generate_fines_start if $self->circ;
2517
2518     if( $self->checkin_check_holds_shelf() ) {
2519         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2520         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2521         if($self->fake_hold_dest) {
2522             $self->hold->pickup_lib($self->circ_lib);
2523         }
2524         $self->checkin_flesh_events;
2525         return;
2526     }
2527
2528     unless( $self->is_renewal ) {
2529         return $self->bail_on_events($self->editor->event)
2530             unless $self->editor->allowed('COPY_CHECKIN');
2531     }
2532
2533     $self->push_events($self->check_copy_alert());
2534     $self->push_events($self->check_checkin_copy_status());
2535
2536     # if the circ is marked as 'claims returned', add the event to the list
2537     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2538         if ($self->circ and $self->circ->stop_fines 
2539                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2540
2541     $self->check_circ_deposit();
2542
2543     # handle the overridable events 
2544     $self->override_events unless $self->is_renewal;
2545     return if $self->bail_out;
2546     
2547     if( $self->copy and !$self->transit ) {
2548         $self->transit(
2549             $self->editor->search_action_transit_copy(
2550                 { target_copy => $self->copy->id, dest_recv_time => undef }
2551             )->[0]
2552         ); 
2553     }
2554
2555     if( $self->circ ) {
2556         $self->generate_fines_finish;
2557         $self->checkin_handle_circ;
2558         return if $self->bail_out;
2559         $self->checkin_changed(1);
2560
2561     } elsif( $self->transit ) {
2562         my $hold_transit = $self->process_received_transit;
2563         $self->checkin_changed(1);
2564
2565         if( $self->bail_out ) { 
2566             $self->checkin_flesh_events;
2567             return;
2568         }
2569         
2570         if( my $e = $self->check_checkin_copy_status() ) {
2571             # If the original copy status is special, alert the caller
2572             my $ev = $self->events;
2573             $self->events([$e]);
2574             $self->override_events;
2575             return if $self->bail_out;
2576             $self->events($ev);
2577         }
2578
2579         if( $hold_transit or 
2580                 $U->copy_status($self->copy->status)->id 
2581                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2582
2583             my $hold;
2584             if( $hold_transit ) {
2585                $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2586             } else {
2587                    ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2588             }
2589
2590             $self->hold($hold);
2591
2592             if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2593
2594                 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2595                 $self->reshelve_copy(1);
2596                 $self->cancelled_hold_transit(1);
2597                 $self->notify_hold(0); # don't notify for cancelled holds
2598                 $self->fake_hold_dest(0);
2599                 return if $self->bail_out;
2600
2601             } elsif ($hold and $hold->hold_type eq 'R') {
2602
2603                 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2604                 $self->notify_hold(0); # No need to notify
2605                 $self->fake_hold_dest(0);
2606                 $self->noop(1); # Don't try and capture for other holds/transits now
2607                 $self->update_copy();
2608                 $hold->fulfillment_time('now');
2609                 $self->bail_on_events($self->editor->event)
2610                     unless $self->editor->update_action_hold_request($hold);
2611
2612             } else {
2613
2614                 # hold transited to correct location
2615                 if($self->fake_hold_dest) {
2616                     $hold->pickup_lib($self->circ_lib);
2617                 }
2618                 $self->checkin_flesh_events;
2619                 return;
2620             }
2621         } 
2622
2623     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2624
2625         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2626             " that is in-transit, but there is no transit.. repairing");
2627         $self->reshelve_copy(1);
2628         return if $self->bail_out;
2629     }
2630
2631     if( $self->is_renewal ) {
2632         $self->finish_fines_and_voiding;
2633         return if $self->bail_out;
2634         $self->push_events(OpenILS::Event->new('SUCCESS'));
2635         return;
2636     }
2637
2638    # ------------------------------------------------------------------------------
2639    # Circulations and transits are now closed where necessary.  Now go on to see if
2640    # this copy can fulfill a hold or needs to be routed to a different location
2641    # ------------------------------------------------------------------------------
2642
2643     my $needed_for_something = 0; # formerly "needed_for_hold"
2644
2645     if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2646
2647         if (!$self->remote_hold) {
2648             if ($self->use_booking) {
2649                 my $potential_hold = $self->hold_capture_is_possible;
2650                 my $potential_reservation = $self->reservation_capture_is_possible;
2651
2652                 if ($potential_hold and $potential_reservation) {
2653                     $logger->info("circulator: item could fulfill either hold or reservation");
2654                     $self->push_events(new OpenILS::Event(
2655                         "HOLD_RESERVATION_CONFLICT",
2656                         "hold" => $potential_hold,
2657                         "reservation" => $potential_reservation
2658                     ));
2659                     return if $self->bail_out;
2660                 } elsif ($potential_hold) {
2661                     $needed_for_something =
2662                         $self->attempt_checkin_hold_capture;
2663                 } elsif ($potential_reservation) {
2664                     $needed_for_something =
2665                         $self->attempt_checkin_reservation_capture;
2666                 }
2667             } else {
2668                 $needed_for_something = $self->attempt_checkin_hold_capture;
2669             }
2670         }
2671         return if $self->bail_out;
2672     
2673         unless($needed_for_something) {
2674             my $circ_lib = (ref $self->copy->circ_lib) ? 
2675                     $self->copy->circ_lib->id : $self->copy->circ_lib;
2676     
2677             if( $self->remote_hold ) {
2678                 $circ_lib = $self->remote_hold->pickup_lib;
2679                 $logger->warn("circulator: Copy ".$self->copy->barcode.
2680                     " is on a remote hold's shelf, sending to $circ_lib");
2681             }
2682     
2683             $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2684
2685             my $suppress_transit = 0;
2686
2687             if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2688                 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2689                 if($suppress_transit_source && $suppress_transit_source->{value}) {
2690                     my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2691                     if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2692                         $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2693                         $suppress_transit = 1;
2694                     }
2695                 }
2696             }
2697  
2698             if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2699                 # copy is where it needs to be, either for hold or reshelving
2700     
2701                 $self->checkin_handle_precat();
2702                 return if $self->bail_out;
2703     
2704             } else {
2705                 # copy needs to transit "home", or stick here if it's a floating copy
2706     
2707                 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2708                     $self->checkin_changed(1);
2709                     $self->copy->circ_lib( $self->circ_lib );
2710                     $self->update_copy;
2711                 } else {
2712                     my $bc = $self->copy->barcode;
2713                     $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2714                     $self->checkin_build_copy_transit($circ_lib);
2715                     return if $self->bail_out;
2716                     $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2717                 }
2718             }
2719         }
2720     } else { # no-op checkin
2721         if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2722             $self->checkin_changed(1);
2723             $self->copy->circ_lib( $self->circ_lib );
2724             $self->update_copy;
2725         }
2726     }
2727
2728     if($self->claims_never_checked_out and 
2729             $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2730
2731         # the item was not supposed to be checked out to the user and should now be marked as missing
2732         $self->copy->status(OILS_COPY_STATUS_MISSING);
2733         $self->update_copy;
2734
2735     } else {
2736         $self->reshelve_copy unless $needed_for_something;
2737     }
2738
2739     return if $self->bail_out;
2740
2741     unless($self->checkin_changed) {
2742
2743         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2744         my $stat = $U->copy_status($self->copy->status)->id;
2745
2746         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2747          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2748         $self->bail_out(1); # no need to commit anything
2749
2750     } else {
2751
2752         $self->push_events(OpenILS::Event->new('SUCCESS')) 
2753             unless @{$self->events};
2754     }
2755
2756     $self->finish_fines_and_voiding;
2757
2758     OpenILS::Utils::Penalty->calculate_penalties(
2759         $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2760
2761     $self->checkin_flesh_events;
2762     return;
2763 }
2764
2765 sub finish_fines_and_voiding {
2766     my $self = shift;
2767     return unless $self->circ;
2768
2769     # gather any updates to the circ after fine generation, if there was a circ
2770     $self->generate_fines_finish;
2771
2772     return unless $self->backdate or $self->void_overdues;
2773
2774     # void overdues after fine generation to prevent concurrent DB access to overdue billings
2775     my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2776
2777     my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2778         $self->editor, $self->circ, $self->backdate, $note);
2779
2780     return $self->bail_on_events($evt) if $evt;
2781
2782     # Make sure the circ is open or closed as necessary.
2783     $evt = $U->check_open_xact($self->editor, $self->circ->id);
2784     return $self->bail_on_events($evt) if $evt;
2785
2786     return undef;
2787 }
2788
2789
2790 # if a deposit was payed for this item, push the event
2791 sub check_circ_deposit {
2792     my $self = shift;
2793     return unless $self->circ;
2794     my $deposit = $self->editor->search_money_billing(
2795         {   btype => 5, 
2796             xact => $self->circ->id, 
2797             voided => 'f'
2798         }, {idlist => 1})->[0];
2799
2800     $self->push_events(OpenILS::Event->new(
2801         'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2802 }
2803
2804 sub reshelve_copy {
2805    my $self    = shift;
2806    my $force   = $self->force || shift;
2807    my $copy    = $self->copy;
2808
2809    my $stat = $U->copy_status($copy->status)->id;
2810
2811    if($force || (
2812       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2813       $stat != OILS_COPY_STATUS_CATALOGING and
2814       $stat != OILS_COPY_STATUS_IN_TRANSIT and
2815       $stat != OILS_COPY_STATUS_RESHELVING  )) {
2816
2817         $copy->status( OILS_COPY_STATUS_RESHELVING );
2818             $self->update_copy;
2819             $self->checkin_changed(1);
2820     }
2821 }
2822
2823
2824 # Returns true if the item is at the current location
2825 # because it was transited there for a hold and the 
2826 # hold has not been fulfilled
2827 sub checkin_check_holds_shelf {
2828     my $self = shift;
2829     return 0 unless $self->copy;
2830
2831     return 0 unless 
2832         $U->copy_status($self->copy->status)->id ==
2833             OILS_COPY_STATUS_ON_HOLDS_SHELF;
2834
2835     # Attempt to clear shelf expired holds for this copy
2836     $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2837         if($self->clear_expired);
2838
2839     # find the hold that put us on the holds shelf
2840     my $holds = $self->editor->search_action_hold_request(
2841         { 
2842             current_copy => $self->copy->id,
2843             capture_time => { '!=' => undef },
2844             fulfillment_time => undef,
2845             cancel_time => undef,
2846         }
2847     );
2848
2849     unless(@$holds) {
2850         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2851         $self->reshelve_copy(1);
2852         return 0;
2853     }
2854
2855     my $hold = $$holds[0];
2856
2857     $logger->info("circulator: we found a captured, un-fulfilled hold [".
2858         $hold->id. "] for copy ".$self->copy->barcode);
2859
2860     if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2861         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2862         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2863             my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2864             if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2865                 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2866                 $self->fake_hold_dest(1);
2867                 return 1;
2868             }
2869         }
2870     }
2871
2872     if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2873         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2874         return 1;
2875     }
2876
2877     $logger->info("circulator: hold is not for here..");
2878     $self->remote_hold($hold);
2879     return 0;
2880 }
2881
2882
2883 sub checkin_handle_precat {
2884     my $self    = shift;
2885    my $copy    = $self->copy;
2886
2887    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2888         $copy->status(OILS_COPY_STATUS_CATALOGING);
2889         $self->update_copy();
2890         $self->checkin_changed(1);
2891         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2892    }
2893 }
2894
2895
2896 sub checkin_build_copy_transit {
2897     my $self            = shift;
2898     my $dest            = shift;
2899     my $copy       = $self->copy;
2900     my $transit    = Fieldmapper::action::transit_copy->new;
2901
2902     # if we are transiting an item to the shelf shelf, it's a hold transit
2903     if (my $hold = $self->remote_hold) {
2904         $transit = Fieldmapper::action::hold_transit_copy->new;
2905         $transit->hold($hold->id);
2906
2907         # the item is going into transit, remove any shelf-iness
2908         if ($hold->current_shelf_lib or $hold->shelf_time) {
2909             $hold->clear_current_shelf_lib;
2910             $hold->clear_shelf_time;
2911             return $self->bail_on_events($self->editor->event)
2912                 unless $self->editor->update_action_hold_request($hold);
2913         }
2914     }
2915
2916     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2917     $logger->info("circulator: transiting copy to $dest");
2918
2919     $transit->source($self->circ_lib);
2920     $transit->dest($dest);
2921     $transit->target_copy($copy->id);
2922     $transit->source_send_time('now');
2923     $transit->copy_status( $U->copy_status($copy->status)->id );
2924
2925     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2926
2927     if ($self->remote_hold) {
2928         return $self->bail_on_events($self->editor->event)
2929             unless $self->editor->create_action_hold_transit_copy($transit);
2930     } else {
2931         return $self->bail_on_events($self->editor->event)
2932             unless $self->editor->create_action_transit_copy($transit);
2933     }
2934
2935     # ensure the transit is returned to the caller
2936     $self->transit($transit);
2937
2938     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2939     $self->update_copy;
2940     $self->checkin_changed(1);
2941 }
2942
2943
2944 sub hold_capture_is_possible {
2945     my $self = shift;
2946     my $copy = $self->copy;
2947
2948     # we've been explicitly told not to capture any holds
2949     return 0 if $self->capture eq 'nocapture';
2950
2951     # See if this copy can fulfill any holds
2952     my $hold = $holdcode->find_nearest_permitted_hold(
2953         $self->editor, $copy, $self->editor->requestor, 1 # check_only
2954     );
2955     return undef if ref $hold eq "HASH" and
2956         $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2957     return $hold;
2958 }
2959
2960 sub reservation_capture_is_possible {
2961     my $self = shift;
2962     my $copy = $self->copy;
2963
2964     # we've been explicitly told not to capture any holds
2965     return 0 if $self->capture eq 'nocapture';
2966
2967     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2968     my $resv = $booking_ses->request(
2969         "open-ils.booking.reservations.could_capture",
2970         $self->editor->authtoken, $copy->barcode
2971     )->gather(1);
2972     $booking_ses->disconnect;
2973     if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2974         $self->push_events($resv);
2975     } else {
2976         return $resv;
2977     }
2978 }
2979
2980 # returns true if the item was used (or may potentially be used 
2981 # in subsequent calls) to capture a hold.
2982 sub attempt_checkin_hold_capture {
2983     my $self = shift;
2984     my $copy = $self->copy;
2985
2986     # we've been explicitly told not to capture any holds
2987     return 0 if $self->capture eq 'nocapture';
2988
2989     # See if this copy can fulfill any holds
2990     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
2991         $self->editor, $copy, $self->editor->requestor );
2992
2993     if(!$hold) {
2994         $logger->debug("circulator: no potential permitted".
2995             "holds found for copy ".$copy->barcode);
2996         return 0;
2997     }
2998
2999     if($self->capture ne 'capture') {
3000         # see if this item is in a hold-capture-delay location
3001         my $location = $self->copy->location;
3002         if(!ref($location)) {
3003             $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
3004             $self->copy->location($location);
3005         }
3006         if($U->is_true($location->hold_verify)) {
3007             $self->bail_on_events(
3008                 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3009             return 1;
3010         }
3011     }
3012
3013     $self->retarget($retarget);
3014
3015     my $suppress_transit = 0;
3016     if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3017         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3018         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3019             my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3020             if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3021                 $suppress_transit = 1;
3022                 $self->hold->pickup_lib($self->circ_lib);
3023             }
3024         }
3025     }
3026
3027     $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3028
3029     $hold->current_copy($copy->id);
3030     $hold->capture_time('now');
3031     $self->put_hold_on_shelf($hold) 
3032         if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3033
3034     # prevent DB errors caused by fetching 
3035     # holds from storage, and updating through cstore
3036     $hold->clear_fulfillment_time;
3037     $hold->clear_fulfillment_staff;
3038     $hold->clear_fulfillment_lib;
3039     $hold->clear_expire_time; 
3040     $hold->clear_cancel_time;
3041     $hold->clear_prev_check_time unless $hold->prev_check_time;
3042
3043     $self->bail_on_events($self->editor->event)
3044         unless $self->editor->update_action_hold_request($hold);
3045     $self->hold($hold);
3046     $self->checkin_changed(1);
3047
3048     return 0 if $self->bail_out;
3049
3050     if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3051
3052         if ($hold->hold_type eq 'R') {
3053             $copy->status(OILS_COPY_STATUS_CATALOGING);
3054             $hold->fulfillment_time('now');
3055             $self->noop(1); # Block other transit/hold checks
3056             $self->bail_on_events($self->editor->event)
3057                 unless $self->editor->update_action_hold_request($hold);
3058         } else {
3059             # This hold was captured in the correct location
3060             $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3061             $self->push_events(OpenILS::Event->new('SUCCESS'));
3062
3063             #$self->do_hold_notify($hold->id);
3064             $self->notify_hold($hold->id);
3065         }
3066
3067     } else {
3068     
3069         # Hold needs to be picked up elsewhere.  Build a hold
3070         # transit and route the item.
3071         $self->checkin_build_hold_transit();
3072         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3073         return 0 if $self->bail_out;
3074         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3075     }
3076
3077     # make sure we save the copy status
3078     $self->update_copy;
3079     return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3080     return 1;
3081 }
3082
3083 sub attempt_checkin_reservation_capture {
3084     my $self = shift;
3085     my $copy = $self->copy;
3086
3087     # we've been explicitly told not to capture any holds
3088     return 0 if $self->capture eq 'nocapture';
3089
3090     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3091     my $evt = $booking_ses->request(
3092         "open-ils.booking.resources.capture_for_reservation",
3093         $self->editor->authtoken,
3094         $copy->barcode,
3095         1 # don't update copy - we probably have it locked
3096     )->gather(1);
3097     $booking_ses->disconnect;
3098
3099     if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3100         $logger->warn(
3101             "open-ils.booking.resources.capture_for_reservation " .
3102             "didn't return an event!"
3103         );
3104     } else {
3105         if (
3106             $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3107             $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3108         ) {
3109             # not-transferable is an error event we'll pass on the user
3110             $logger->warn("reservation capture attempted against non-transferable item");
3111             $self->push_events($evt);
3112             return 0;
3113         } elsif ($evt->{"textcode"} eq "SUCCESS") {
3114             # Re-retrieve copy as reservation capture may have changed
3115             # its status and whatnot.
3116             $logger->info(
3117                 "circulator: booking capture win on copy " . $self->copy->id
3118             );
3119             if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3120                 $logger->info(
3121                     "circulator: changing copy " . $self->copy->id .
3122                     "'s status from " . $self->copy->status . " to " .
3123                     $new_copy_status
3124                 );
3125                 $self->copy->status($new_copy_status);
3126                 $self->update_copy;
3127             }
3128             $self->reservation($evt->{"payload"}->{"reservation"});
3129
3130             if (exists $evt->{"payload"}->{"transit"}) {
3131                 $self->push_events(
3132                     new OpenILS::Event(
3133                         "ROUTE_ITEM",
3134                         "org" => $evt->{"payload"}->{"transit"}->dest
3135                     )
3136                 );
3137             }
3138             $self->checkin_changed(1);
3139             return 1;
3140         }
3141     }
3142     # other results are treated as "nothing to capture"
3143     return 0;
3144 }
3145
3146 sub do_hold_notify {
3147     my( $self, $holdid ) = @_;
3148
3149     my $e = new_editor(xact => 1);
3150     my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3151     $e->rollback;
3152     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3153     $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3154
3155     $logger->info("circulator: running delayed hold notify process");
3156
3157 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3158 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3159
3160     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3161         hold_id => $holdid, requestor => $self->editor->requestor);
3162
3163     $logger->debug("circulator: built hold notifier");
3164
3165     if(!$notifier->event) {
3166
3167         $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3168
3169         my $stat = $notifier->send_email_notify;
3170         if( $stat == '1' ) {
3171             $logger->info("circulator: hold notify succeeded for hold $holdid");
3172             return;
3173         } 
3174
3175         $logger->debug("circulator:  * hold notify cancelled or failed for hold $holdid");
3176
3177     } else {
3178         $logger->info("circulator: Not sending hold notification since the patron has no email address");
3179     }
3180 }
3181
3182 sub retarget_holds {
3183     my $self = shift;
3184     $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3185     my $ses = OpenSRF::AppSession->create('open-ils.storage');
3186     $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3187     # no reason to wait for the return value
3188     return;
3189 }
3190
3191 sub checkin_build_hold_transit {
3192     my $self = shift;
3193
3194    my $copy = $self->copy;
3195    my $hold = $self->hold;
3196    my $trans = Fieldmapper::action::hold_transit_copy->new;
3197
3198     $logger->debug("circulator: building hold transit for ".$copy->barcode);
3199
3200    $trans->hold($hold->id);
3201    $trans->source($self->circ_lib);
3202    $trans->dest($hold->pickup_lib);
3203    $trans->source_send_time("now");
3204    $trans->target_copy($copy->id);
3205
3206     # when the copy gets to its destination, it will recover
3207     # this status - put it onto the holds shelf
3208    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3209
3210     return $self->bail_on_events($self->editor->event)
3211         unless $self->editor->create_action_hold_transit_copy($trans);
3212 }
3213
3214
3215
3216 sub process_received_transit {
3217     my $self = shift;
3218     my $copy = $self->copy;
3219     my $copyid = $self->copy->id;
3220
3221     my $status_name = $U->copy_status($copy->status)->name;
3222     $logger->debug("circulator: attempting transit receive on ".
3223         "copy $copyid. Copy status is $status_name");
3224
3225     my $transit = $self->transit;
3226
3227     # Check if we are in a transit suppress range
3228     my $suppress_transit = 0;
3229     if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3230         my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ?  'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3231         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3232         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3233             my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3234             if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3235                 $suppress_transit = 1;
3236                 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3237             }
3238         }
3239     }
3240     if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3241         # - this item is in-transit to a different location
3242         # - Or we are capturing holds as transits, so why create a new transit?
3243
3244         my $tid = $transit->id; 
3245         my $loc = $self->circ_lib;
3246         my $dest = $transit->dest;
3247
3248         $logger->info("circulator: Fowarding transit on copy which is destined ".
3249             "for a different location. transit=$tid, copy=$copyid, current ".
3250             "location=$loc, destination location=$dest");
3251
3252         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3253
3254         # grab the associated hold object if available
3255         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3256         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3257
3258         return $self->bail_on_events($evt);
3259     }
3260
3261     # The transit is received, set the receive time
3262     $transit->dest_recv_time('now');
3263     $self->bail_on_events($self->editor->event)
3264         unless $self->editor->update_action_transit_copy($transit);
3265
3266     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3267
3268     $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3269     $copy->status( $transit->copy_status );
3270     $self->update_copy();
3271     return if $self->bail_out;
3272
3273     my $ishold = 0;
3274     if($hold_transit) { 
3275         my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3276
3277         if ($hold) {
3278             # hold has arrived at destination, set shelf time
3279             $self->put_hold_on_shelf($hold);
3280             $self->bail_on_events($self->editor->event)
3281                 unless $self->editor->update_action_hold_request($hold);
3282             return if $self->bail_out;
3283
3284             $self->notify_hold($hold_transit->hold);
3285             $ishold = 1;
3286         } else {
3287             $hold_transit = undef;
3288             $self->cancelled_hold_transit(1);
3289             $self->reshelve_copy(1);
3290             $self->fake_hold_dest(0);
3291         }
3292     }
3293
3294     $self->push_events( 
3295         OpenILS::Event->new(
3296         'SUCCESS', 
3297         ishold => $ishold,
3298       payload => { transit => $transit, holdtransit => $hold_transit } ));
3299
3300     return $hold_transit;
3301 }
3302
3303
3304 # ------------------------------------------------------------------
3305 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3306 # ------------------------------------------------------------------
3307 sub put_hold_on_shelf {
3308     my($self, $hold) = @_;
3309     $hold->shelf_time('now');
3310     $hold->current_shelf_lib($self->circ_lib);
3311     $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3312     return undef;
3313 }
3314
3315
3316
3317 sub generate_fines {
3318    my $self = shift;
3319    my $reservation = shift;
3320
3321    $self->generate_fines_start($reservation);
3322    $self->generate_fines_finish($reservation);
3323
3324    return undef;
3325 }
3326
3327 sub generate_fines_start {
3328    my $self = shift;
3329    my $reservation = shift;
3330    my $dt_parser = DateTime::Format::ISO8601->new;
3331
3332    my $obj = $reservation ? $self->reservation : $self->circ;
3333
3334    # If we have a grace period
3335    if($obj->can('grace_period')) {
3336       # Parse out the due date
3337       my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3338       # Add the grace period to the due date
3339       $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3340       # Don't generate fines on circs still in grace period
3341       return undef if ($due_date > DateTime->now);
3342    }
3343
3344    if (!exists($self->{_gen_fines_req})) {
3345       $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage') 
3346           ->request(
3347              'open-ils.storage.action.circulation.overdue.generate_fines',
3348              $obj->id
3349           );
3350    }
3351
3352    return undef;
3353 }
3354
3355 sub generate_fines_finish {
3356    my $self = shift;
3357    my $reservation = shift;
3358
3359    return undef unless $self->{_gen_fines_req};
3360
3361    my $id = $reservation ? $self->reservation->id : $self->circ->id;
3362
3363    $self->{_gen_fines_req}->wait_complete;
3364    delete($self->{_gen_fines_req});
3365
3366    # refresh the circ in case the fine generator set the stop_fines field
3367    $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3368    $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3369
3370    return undef;
3371 }
3372
3373 sub checkin_handle_circ {
3374    my $self = shift;
3375    my $circ = $self->circ;
3376    my $copy = $self->copy;
3377    my $evt;
3378    my $obt;
3379
3380    $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3381
3382    # backdate the circ if necessary
3383    if($self->backdate) {
3384         my $evt = $self->checkin_handle_backdate;
3385         return $self->bail_on_events($evt) if $evt;
3386    }
3387
3388    if(!$circ->stop_fines) {
3389       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3390       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3391       $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3392       $circ->stop_fines_time('now');
3393       $circ->stop_fines_time($self->backdate) if $self->backdate;
3394    }
3395
3396     # Set the checkin vars since we have the item
3397     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3398
3399     # capture the true scan time for back-dated checkins
3400     $circ->checkin_scan_time('now');
3401
3402     $circ->checkin_staff($self->editor->requestor->id);
3403     $circ->checkin_lib($self->circ_lib);
3404     $circ->checkin_workstation($self->editor->requestor->wsid);
3405
3406     my $circ_lib = (ref $self->copy->circ_lib) ?  
3407         $self->copy->circ_lib->id : $self->copy->circ_lib;
3408     my $stat = $U->copy_status($self->copy->status)->id;
3409
3410     if ($stat == OILS_COPY_STATUS_LOST) {
3411         # we will now handle lost fines, but the copy will retain its 'lost'
3412         # status if it needs to transit home unless lost_immediately_available
3413         # is true
3414         #
3415         # if we decide to also delay fine handling until the item arrives home,
3416         # we will need to call lost fine handling code both when checking items
3417         # in and also when receiving transits
3418         $self->checkin_handle_lost($circ_lib);
3419     } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3420         # same process as above.
3421         $self->checkin_handle_long_overdue($circ_lib);
3422     } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3423         $logger->info("circulator: not updating copy status on checkin because copy is missing");
3424     } else {
3425         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3426         $self->update_copy;
3427     }
3428
3429
3430     # see if there are any fines owed on this circ.  if not, close it
3431     ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3432     $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3433
3434     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3435
3436     return $self->bail_on_events($self->editor->event)
3437         unless $self->editor->update_action_circulation($circ);
3438
3439     return undef;
3440 }
3441
3442 # ------------------------------------------------------------------
3443 # See if we need to void billings, etc. for lost checkin
3444 # ------------------------------------------------------------------
3445 sub checkin_handle_lost {
3446     my $self = shift;
3447     my $circ_lib = shift;
3448
3449     my $max_return = $U->ou_ancestor_setting_value($circ_lib, 
3450         OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3451
3452     return $self->checkin_handle_lost_or_longoverdue(
3453         circ_lib => $circ_lib,
3454         max_return => $max_return,
3455         ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3456         ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3457         ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3458         ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3459         ous_use_last_activity => undef, # not supported for LOST checkin
3460         void_cost_btype => 3, 
3461         void_fee_btype => 4 
3462     );
3463 }
3464
3465 # ------------------------------------------------------------------
3466 # See if we need to void billings, etc. for long-overdue checkin
3467 # note: not using constants below since they serve little purpose 
3468 # for single-use strings that are descriptive in their own right 
3469 # and mostly just complicate debugging.
3470 # ------------------------------------------------------------------
3471 sub checkin_handle_long_overdue {
3472     my $self = shift;
3473     my $circ_lib = shift;
3474
3475     $logger->info("circulator: processing long-overdue checkin...");
3476
3477     my $max_return = $U->ou_ancestor_setting_value($circ_lib, 
3478         'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3479
3480     return $self->checkin_handle_lost_or_longoverdue(
3481         circ_lib => $circ_lib,
3482         max_return => $max_return,
3483         is_longoverdue => 1,
3484         ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3485         ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3486         ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3487         ous_immediately_available => 'circ.longoverdue_immediately_available',
3488         ous_use_last_activity => 
3489             'circ.longoverdue.use_last_activity_date_on_return',
3490         void_cost_btype => 10,
3491         void_fee_btype => 11
3492     )
3493 }
3494
3495 # last billing activity is last payment time, last billing time, or the 
3496 # circ due date.  If the relevant "use last activity" org unit setting is 
3497 # false/unset, then last billing activity is always the due date.
3498 sub get_circ_last_billing_activity {
3499     my $self = shift;
3500     my $circ_lib = shift;
3501     my $setting = shift;
3502     my $date = $self->circ->due_date;
3503
3504     return $date unless $setting and 
3505         $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3506
3507     my $xact = $self->editor->retrieve_money_billable_transaction([
3508         $self->circ->id,
3509         {flesh => 1, flesh_fields => {mbt => ['summary']}}
3510     ]);
3511
3512     if ($xact->summary) {
3513         $date = $xact->summary->last_payment_ts || 
3514                 $xact->summary->last_billing_ts || 
3515                 $self->circ->due_date;
3516     }
3517
3518     return $date;
3519 }
3520
3521
3522 sub checkin_handle_lost_or_longoverdue {
3523     my ($self, %args) = @_;
3524
3525     my $circ = $self->circ;
3526     my $max_return = $args{max_return};
3527     my $circ_lib = $args{circ_lib};
3528
3529     if ($max_return) {
3530
3531         my $last_activity = 
3532             $self->get_circ_last_billing_activity(
3533                 $circ_lib, $args{ous_use_last_activity});
3534
3535         my $today = time();
3536         my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3537         $tm[5] -= 1 if $tm[5] > 0;
3538         my $due = timelocal(int($tm[1]), int($tm[2]), 
3539             int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3540
3541         my $last_chance = 
3542             OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3543
3544         $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3545             "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3546                 "DUE: $due LAST: $last_chance");
3547
3548         $max_return = 0 if $today < $last_chance;
3549     }
3550
3551
3552     if ($max_return) {
3553
3554         $logger->info("circulator: check-in of lost/lo item exceeds max ". 
3555             "return interval.  skipping fine/fee voiding, etc.");
3556
3557     } else { # within max-return interval or no interval defined
3558
3559         $logger->info("circulator: check-in of lost/lo item is within the ".
3560             "max return interval (or no interval is defined).  Proceeding ".
3561             "with fine/fee voiding, etc.");
3562
3563         my $void_cost = $U->ou_ancestor_setting_value(
3564             $circ_lib, $args{ous_void_item_cost}, $self->editor) || 0;
3565         my $void_proc_fee = $U->ou_ancestor_setting_value(
3566             $circ_lib, $args{ous_void_proc_fee}, $self->editor) || 0;
3567         my $restore_od = $U->ou_ancestor_setting_value(
3568             $circ_lib, $args{ous_restore_overdue}, $self->editor) || 0;
3569
3570         # for reference: generate-overdues-on-long-overdue-checkin is not 
3571         # supported because it doesn't make any sense that a circ would be 
3572         # marked as long-overdue before it was done being regular-overdue
3573         if (!$args{is_longoverdue}) {
3574             $self->generate_lost_overdue(1) if 
3575                 $U->ou_ancestor_setting_value($circ_lib, 
3576                     OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, 
3577                     $self->editor);
3578         }
3579
3580         $self->checkin_handle_lost_or_lo_now_found(
3581             $args{void_cost_btype}, $args{is_longoverdue}) if $void_cost;
3582         $self->checkin_handle_lost_or_lo_now_found(
3583             $args{void_fee_btype}, $args{is_longoverdue}) if $void_proc_fee;
3584         $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib) 
3585             if $restore_od && ! $self->void_overdues;
3586     }
3587
3588     if ($circ_lib != $self->circ_lib) {
3589         # if the item is not home, check to see if we want to retain the
3590         # lost/longoverdue status at this point in the process
3591
3592         my $immediately_available = $U->ou_ancestor_setting_value($circ_lib, 
3593             $args{ous_immediately_available}, $self->editor) || 0;
3594
3595         if ($immediately_available) {
3596             # item status does not need to be retained, so give it a
3597             # reshelving status as if it were a normal checkin
3598             $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3599             $self->update_copy;
3600         } else {
3601             $logger->info("circulator: leaving lost/longoverdue copy".
3602                 " status in place on checkin");
3603         }
3604     } else {
3605         # lost/longoverdue item is home and processed, treat like a normal 
3606         # checkin from this point on
3607         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3608         $self->update_copy;
3609     }
3610 }
3611
3612
3613 sub checkin_handle_backdate {
3614     my $self = shift;
3615
3616     # ------------------------------------------------------------------
3617     # clean up the backdate for date comparison
3618     # XXX We are currently taking the due-time from the original due-date,
3619     # not the input.  Do we need to do this?  This certainly interferes with
3620     # backdating of hourly checkouts, but that is likely a very rare case.
3621     # ------------------------------------------------------------------
3622     my $bd = cleanse_ISO8601($self->backdate);
3623     my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3624     my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3625     $new_date->set_hour($original_date->hour());
3626     $new_date->set_minute($original_date->minute());
3627     $bd = cleanse_ISO8601($new_date->datetime());
3628
3629     $self->backdate($bd);
3630     return undef;
3631 }
3632
3633
3634 sub check_checkin_copy_status {
3635     my $self = shift;
3636    my $copy = $self->copy;
3637
3638    my $status = $U->copy_status($copy->status)->id;
3639
3640    return undef
3641       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
3642             $status == OILS_COPY_STATUS_CHECKED_OUT ||
3643             $status == OILS_COPY_STATUS_IN_PROCESS  ||
3644             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
3645             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
3646             $status == OILS_COPY_STATUS_CATALOGING  ||
3647             $status == OILS_COPY_STATUS_ON_RESV_SHELF  ||
3648             $status == OILS_COPY_STATUS_RESHELVING );
3649
3650    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3651       if( $status == OILS_COPY_STATUS_LOST );
3652
3653    return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3654       if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3655
3656    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3657       if( $status == OILS_COPY_STATUS_MISSING );
3658
3659    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3660 }
3661
3662
3663
3664 # --------------------------------------------------------------------------
3665 # On checkin, we need to return as many relevant objects as we can
3666 # --------------------------------------------------------------------------
3667 sub checkin_flesh_events {
3668     my $self = shift;
3669
3670     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
3671         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3672             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3673     }
3674
3675     my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3676
3677     my $hold;
3678     if($self->hold and !$self->hold->cancel_time) {
3679         $hold = $self->hold;
3680         $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3681     }
3682
3683     if($self->circ) {
3684         # update our copy of the circ object and 
3685         # flesh the billing summary data
3686         $self->circ(
3687             $self->editor->retrieve_action_circulation([
3688                 $self->circ->id, {
3689                     flesh => 2,
3690                     flesh_fields => {
3691                         circ => ['billable_transaction'],
3692                         mbt => ['summary']
3693                     }
3694                 }
3695             ])
3696         );
3697     }
3698
3699     if($self->patron) {
3700         # flesh some patron fields before returning
3701         $self->patron(
3702             $self->editor->retrieve_actor_user([
3703                 $self->patron->id,
3704                 {
3705                     flesh => 1,
3706                     flesh_fields => {
3707                         au => ['card', 'billing_address', 'mailing_address']
3708                     }
3709                 }
3710             ])
3711         );
3712     }
3713
3714     for my $evt (@{$self->events}) {
3715
3716         my $payload         = {};
3717         $payload->{copy}    = $U->unflesh_copy($self->copy);
3718         $payload->{volume}  = $self->volume;
3719         $payload->{record}  = $record,
3720         $payload->{circ}    = $self->circ;
3721         $payload->{transit} = $self->transit;
3722         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3723         $payload->{hold}    = $hold;
3724         $payload->{patron}  = $self->patron;
3725         $payload->{reservation} = $self->reservation
3726             unless (not $self->reservation or $self->reservation->cancel_time);
3727
3728         $evt->{payload}     = $payload;
3729     }
3730 }
3731
3732 sub log_me {
3733     my( $self, $msg ) = @_;
3734     my $bc = ($self->copy) ? $self->copy->barcode :
3735         $self->barcode;
3736     $bc ||= "";
3737     my $usr = ($self->patron) ? $self->patron->id : "";
3738     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3739         ", recipient=$usr, copy=$bc");
3740 }
3741
3742
3743 sub do_renew {
3744     my $self = shift;
3745     $self->log_me("do_renew()");
3746
3747     # Make sure there is an open circ to renew that is not
3748     # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3749     my $usrid = $self->patron->id if $self->patron;
3750     my $circ = $self->editor->search_action_circulation({
3751         target_copy => $self->copy->id,
3752         xact_finish => undef,
3753         checkin_time => undef,
3754         ($usrid ? (usr => $usrid) : ()),
3755         '-or' => [
3756             {stop_fines => undef},
3757             {stop_fines => OILS_STOP_FINES_MAX_FINES}
3758         ]
3759     })->[0];
3760
3761     return $self->bail_on_events($self->editor->event) unless $circ;
3762
3763     # A user is not allowed to renew another user's items without permission
3764     unless( $circ->usr eq $self->editor->requestor->id ) {
3765         return $self->bail_on_events($self->editor->events)
3766             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3767     }   
3768
3769     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3770         if $circ->renewal_remaining < 1;
3771
3772     # -----------------------------------------------------------------
3773
3774     $self->parent_circ($circ->id);
3775     $self->renewal_remaining( $circ->renewal_remaining - 1 );
3776     $self->circ($circ);
3777
3778     # Opac renewal - re-use circ library from original circ (unless told not to)
3779     if($self->opac_renewal) {
3780         unless(defined($opac_renewal_use_circ_lib)) {
3781             my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3782             if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3783                 $opac_renewal_use_circ_lib = 1;
3784             }
3785             else {
3786                 $opac_renewal_use_circ_lib = 0;
3787             }
3788         }
3789         $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3790     }
3791
3792     # Desk renewal - re-use circ library from original circ (unless told not to)
3793     if($self->desk_renewal) {
3794         unless(defined($desk_renewal_use_circ_lib)) {
3795             my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3796             if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3797                 $desk_renewal_use_circ_lib = 1;
3798             }
3799             else {
3800                 $desk_renewal_use_circ_lib = 0;
3801             }
3802         }
3803         $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3804     }
3805
3806     # Run the fine generator against the old circ
3807     $self->generate_fines_start;
3808
3809     $self->run_renew_permit;
3810
3811     # Check the item in
3812     $self->do_checkin();
3813     return if $self->bail_out;
3814
3815     unless( $self->permit_override ) {
3816         $self->do_permit();
3817         return if $self->bail_out;
3818         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3819         $self->remove_event('ITEM_NOT_CATALOGED');
3820     }   
3821
3822     $self->override_events;
3823     return if $self->bail_out;
3824
3825     $self->events([]);
3826     $self->do_checkout();
3827 }
3828
3829
3830 sub remove_event {
3831     my( $self, $evt ) = @_;
3832     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3833     $logger->debug("circulator: removing event from list: $evt");
3834     my @events = @{$self->events};
3835     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3836 }
3837
3838
3839 sub have_event {
3840     my( $self, $evt ) = @_;
3841     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3842     return grep { $_->{textcode} eq $evt } @{$self->events};
3843 }
3844
3845
3846
3847 sub run_renew_permit {
3848     my $self = shift;
3849
3850     if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3851         my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3852             $self->editor, $self->copy, $self->editor->requestor, 1
3853         );
3854         $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3855     }
3856
3857     if(!$self->legacy_script_support) {
3858         my $results = $self->run_indb_circ_test;
3859         $self->push_events($self->matrix_test_result_events)
3860             unless $self->circ_test_success;
3861     } else {
3862
3863         my $runner = $self->script_runner;
3864
3865         $runner->load($self->circ_permit_renew);
3866         my $result = $runner->run or 
3867             throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3868         if ($result->{"events"}) {
3869             $self->push_events(
3870                 map { new OpenILS::Event($_) } @{$result->{"events"}}
3871             );
3872             $logger->activity(
3873                 "circulator: circ_permit_renew for user " .
3874                 $self->patron->id . " returned " .
3875                 scalar(@{$result->{"events"}}) . " event(s)"
3876             );
3877         }
3878
3879         $self->mk_script_runner;
3880     }
3881
3882     $logger->debug("circulator: re-creating script runner to be safe");
3883 }
3884
3885
3886 # XXX: The primary mechanism for storing circ history is now handled
3887 # by tracking real circulation objects instead of bibs in a bucket.
3888 # However, this code is disabled by default and could be useful 
3889 # some day, so may as well leave it for now.
3890 sub append_reading_list {
3891     my $self = shift;
3892
3893     return undef unless 
3894         $self->is_checkout and 
3895         $self->patron and 
3896         $self->copy and 
3897         !$self->is_noncat;
3898
3899
3900     # verify history is globally enabled and uses the bucket mechanism
3901     my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3902         apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3903
3904     return undef unless $htype and $htype eq 'bucket';
3905
3906     my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3907
3908     # verify the patron wants to retain the hisory
3909     my $setting = $e->search_actor_user_setting(
3910         {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3911     
3912     unless($setting and $setting->value) {
3913         $e->rollback;
3914         return undef;
3915     }
3916
3917     my $bkt = $e->search_container_copy_bucket(
3918         {owner => $self->patron->id, btype => 'circ_history'})->[0];
3919
3920     my $pos = 1;
3921
3922     if($bkt) {
3923         # find the next item position
3924         my $last_item = $e->search_container_copy_bucket_item(
3925             {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3926         $pos = $last_item->pos + 1 if $last_item;
3927
3928     } else {
3929         # create the history bucket if necessary
3930         $bkt = Fieldmapper::container::copy_bucket->new;
3931         $bkt->owner($self->patron->id);
3932         $bkt->name('');
3933         $bkt->btype('circ_history');
3934         $bkt->pub('f');
3935         $e->create_container_copy_bucket($bkt) or return $e->die_event;
3936     }
3937
3938     my $item = Fieldmapper::container::copy_bucket_item->new;
3939
3940     $item->bucket($bkt->id);
3941     $item->target_copy($self->copy->id);
3942     $item->pos($pos);
3943
3944     $e->create_container_copy_bucket_item($item) or return $e->die_event;
3945     $e->commit;
3946
3947     return undef;
3948 }
3949
3950
3951 sub make_trigger_events {
3952     my $self = shift;
3953     return unless $self->circ;
3954     $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3955     $U->create_events_for_hook('checkin',  $self->circ, $self->circ_lib) if $self->is_checkin;
3956     $U->create_events_for_hook('renewal',  $self->circ, $self->circ_lib) if $self->is_renewal;
3957 }
3958
3959
3960
3961 sub checkin_handle_lost_or_lo_now_found {
3962     my ($self, $bill_type, $is_longoverdue) = @_;
3963
3964     # ------------------------------------------------------------------
3965     # remove charge from patron's account if lost item is returned
3966     # ------------------------------------------------------------------
3967
3968     my $bills = $self->editor->search_money_billing(
3969         {
3970             xact => $self->circ->id,
3971             btype => $bill_type
3972         }
3973     );
3974
3975     my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3976     
3977     $logger->debug("voiding ".scalar(@$bills)." $tag item billings");
3978     for my $bill (@$bills) {
3979         if( !$U->is_true($bill->voided) ) {
3980             $logger->info("$tag item returned - voiding bill ".$bill->id);
3981             $bill->voided('t');
3982             $bill->void_time('now');
3983             $bill->voider($self->editor->requestor->id);
3984             my $note = ($bill->note) ? $bill->note . "\n" : '';
3985             $bill->note("${note}System: VOIDED FOR $tag ITEM RETURNED");
3986
3987             $self->bail_on_events($self->editor->event)
3988                 unless $self->editor->update_money_billing($bill);
3989         }
3990     }
3991 }
3992
3993 sub checkin_handle_lost_or_lo_now_found_restore_od {
3994     my $self = shift;
3995     my $circ_lib = shift;
3996     my $is_longoverdue = shift;
3997     my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3998
3999     # ------------------------------------------------------------------
4000     # restore those overdue charges voided when item was set to lost
4001     # ------------------------------------------------------------------
4002
4003     my $ods = $self->editor->search_money_billing(
4004         {
4005             xact => $self->circ->id,
4006             btype => 1
4007         }
4008     );
4009
4010     $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
4011     for my $bill (@$ods) {
4012         if( $U->is_true($bill->voided) ) {
4013                 $logger->info("$tag item returned - restoring overdue ".$bill->id);
4014                 $bill->voided('f');
4015                 $bill->clear_void_time;
4016                 $bill->voider($self->editor->requestor->id);
4017                 my $note = ($bill->note) ? $bill->note . "\n" : '';
4018                 $bill->note("${note}System: $tag RETURNED - OVERDUES REINSTATED");
4019
4020                 $self->bail_on_events($self->editor->event)
4021                         unless $self->editor->update_money_billing($bill);
4022         }
4023     }
4024 }
4025
4026 # ------------------------------------------------------------------
4027 # Lost-then-found item checked in.  This sub generates new overdue
4028 # fines, beyond the point of any existing and possibly voided 
4029 # overdue fines, up to the point of final checkin time (or max fine
4030 # amount).  
4031 # ------------------------------------------------------------------
4032 sub generate_lost_overdue_fines {
4033     my $self = shift;
4034     my $circ = $self->circ;
4035     my $e = $self->editor;
4036
4037     # Re-open the transaction so the fine generator can see it
4038     if($circ->xact_finish or $circ->stop_fines) {
4039         $e->xact_begin;
4040         $circ->clear_xact_finish;
4041         $circ->clear_stop_fines;
4042         $circ->clear_stop_fines_time;
4043         $e->update_action_circulation($circ) or return $e->die_event;
4044         $e->xact_commit;
4045     }
4046
4047     $e->xact_begin; # generate_fines expects an in-xact editor
4048     $self->generate_fines;
4049     $circ = $self->circ; # generate fines re-fetches the circ
4050     
4051     my $update = 0;
4052
4053     # Re-close the transaction if no money is owed
4054     my ($obt) = $U->fetch_mbts($circ->id, $e);
4055     if ($obt and $obt->balance_owed == 0) {
4056         $circ->xact_finish('now');
4057         $update = 1;
4058     }
4059
4060     # Set stop fines if the fine generator didn't have to
4061     unless($circ->stop_fines) {
4062         $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
4063         $circ->stop_fines_time('now');
4064         $update = 1;
4065     }
4066
4067     # update the event data sent to the caller within the transaction
4068     $self->checkin_flesh_events;
4069
4070     if ($update) {
4071         $e->update_action_circulation($circ) or return $e->die_event;
4072         $e->commit;
4073     } else {
4074         $e->rollback;
4075     }
4076
4077     return undef;
4078 }
4079
4080 1;