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