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