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