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