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