]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
hold CAP/FILL blocks : repair event test in patron permit
[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             } else {
1108                 @trimmed_results = @$results;
1109             }
1110
1111             if ($self->checkout_is_for_hold) {
1112                 # if this checkout will fulfill a hold, ignore CIRC blocks
1113                 # and rely instead on the (later-checked) FULFILL block
1114
1115                 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
1116                 my $fblock_pens = $self->editor->search_config_standing_penalty(
1117                     {name => [@pen_names], block_list => {like => '%CIRC%'}});
1118
1119                 for my $res (@$results) {
1120                     my $name = $res->{fail_part} || '';
1121                     next if grep {$_->name eq $name} @$fblock_pens or 
1122                         ($self->is_noncat and $name eq 'no_item');
1123                     push(@trimmed_results, $res);
1124                 }
1125             }
1126
1127             # update the final set of test results
1128             $self->matrix_test_result(\@trimmed_results); 
1129
1130             push @allevents, $self->matrix_test_result_events;
1131         }
1132
1133     } else {
1134
1135         # --------------------------------------------------------------------- 
1136         # # Now run the patron permit script 
1137         # ---------------------------------------------------------------------
1138         $runner->load($self->circ_permit_patron);
1139         my $result = $runner->run or 
1140             throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1141
1142         my $patron_events = $result->{events};
1143
1144         OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1145         my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1146         my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1147         $penalties = $penalties->{fatal_penalties};
1148
1149         for my $pen (@$penalties) {
1150             # CIRC blocks are ignored if this is a FULFILL scenario
1151             next if $mask eq 'CIRC' and $self->checkout_is_for_hold;
1152             my $event = OpenILS::Event->new($pen->name);
1153             $event->{desc} = $pen->label;
1154             push(@allevents, $event);
1155         }
1156
1157         push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1158     }
1159
1160     for (@allevents) {
1161        $_->{payload} = $self->copy if 
1162              ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1163     }
1164
1165     $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1166
1167     $self->push_events(@allevents);
1168 }
1169
1170 sub matrix_test_result_codes {
1171     my $self = shift;
1172     map { $_->{"fail_part"} } @{$self->matrix_test_result};
1173 }
1174
1175 sub matrix_test_result_events {
1176     my $self = shift;
1177     map {
1178         my $event = new OpenILS::Event(
1179             $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1180         );
1181         $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1182         $event;
1183     } (@{$self->matrix_test_result});
1184 }
1185
1186 sub run_indb_circ_test {
1187     my $self = shift;
1188     return $self->matrix_test_result if $self->matrix_test_result;
1189
1190     my $dbfunc = ($self->is_renewal) ? 
1191         'action.item_user_renew_test' : 'action.item_user_circ_test';
1192
1193     if( $self->is_precat && $self->request_precat) {
1194         $self->make_precat_copy;
1195         return if $self->bail_out;
1196     }
1197
1198     my $results = $self->editor->json_query(
1199         {   from => [
1200                 $dbfunc,
1201                 $self->circ_lib,
1202                 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id, 
1203                 $self->patron->id,
1204             ]
1205         }
1206     );
1207
1208     $self->circ_test_success($U->is_true($results->[0]->{success}));
1209
1210     if(my $mp = $results->[0]->{matchpoint}) {
1211         $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1212         $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1213         $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1214         if(defined($results->[0]->{renewals})) {
1215             $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1216         }
1217         $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1218         if(defined($results->[0]->{grace_period})) {
1219             $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1220         }
1221         $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1222         $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1223         # Grab the *last* response for limit_groups, where it is more likely to be filled
1224         $self->limit_groups($results->[-1]->{limit_groups});
1225     }
1226
1227     return $self->matrix_test_result($results);
1228 }
1229
1230 # ---------------------------------------------------------------------
1231 # given a use and copy, this will calculate the circulation policy
1232 # parameters.  Only works with in-db circ.
1233 # ---------------------------------------------------------------------
1234 sub do_inspect {
1235     my $self = shift;
1236
1237     return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1238
1239     $self->run_indb_circ_test;
1240
1241     my $results = {
1242         circ_test_success => $self->circ_test_success,
1243         failure_events => [],
1244         failure_codes => [],
1245         matchpoint => $self->circ_matrix_matchpoint
1246     };
1247
1248     unless($self->circ_test_success) {
1249         $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1250         $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1251     }
1252
1253     if($self->circ_matrix_matchpoint) {
1254         my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1255         my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1256         my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1257         my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1258     
1259         my $policy = $self->get_circ_policy(
1260             $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1261     
1262         $$results{$_} = $$policy{$_} for keys %$policy;
1263     }
1264
1265     return $results;
1266 }
1267
1268 # ---------------------------------------------------------------------
1269 # Loads the circ policy info for duration, recurring fine, and max
1270 # fine based on the current copy
1271 # ---------------------------------------------------------------------
1272 sub get_circ_policy {
1273     my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1274
1275     my $policy = {
1276         duration_rule => $duration_rule->name,
1277         recurring_fine_rule => $recurring_fine_rule->name,
1278         max_fine_rule => $max_fine_rule->name,
1279         max_fine => $self->get_max_fine_amount($max_fine_rule),
1280         fine_interval => $recurring_fine_rule->recurrence_interval,
1281         renewal_remaining => $duration_rule->max_renewals,
1282         grace_period => $recurring_fine_rule->grace_period
1283     };
1284
1285     if($hard_due_date) {
1286         $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1287         $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1288     }
1289     else {
1290         $policy->{duration_date_ceiling} = undef;
1291         $policy->{duration_date_ceiling_force} = undef;
1292     }
1293
1294     $policy->{duration} = $duration_rule->shrt
1295         if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1296     $policy->{duration} = $duration_rule->normal
1297         if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1298     $policy->{duration} = $duration_rule->extended
1299         if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1300
1301     $policy->{recurring_fine} = $recurring_fine_rule->low
1302         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1303     $policy->{recurring_fine} = $recurring_fine_rule->normal
1304         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1305     $policy->{recurring_fine} = $recurring_fine_rule->high
1306         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1307
1308     return $policy;
1309 }
1310
1311 sub get_max_fine_amount {
1312     my $self = shift;
1313     my $max_fine_rule = shift;
1314     my $max_amount = $max_fine_rule->amount;
1315
1316     # if is_percent is true then the max->amount is
1317     # use as a percentage of the copy price
1318     if ($U->is_true($max_fine_rule->is_percent)) {
1319         my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1320         $max_amount = $price * $max_fine_rule->amount / 100;
1321     } elsif (
1322         $U->ou_ancestor_setting_value(
1323             $self->circ_lib,
1324             'circ.max_fine.cap_at_price',
1325             $self->editor
1326         )
1327     ) {
1328         my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1329         $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1330     }
1331
1332     return $max_amount;
1333 }
1334
1335
1336
1337 sub run_copy_permit_scripts {
1338     my $self = shift;
1339     my $copy = $self->copy || return;
1340     my $runner = $self->script_runner;
1341
1342     my @allevents;
1343
1344     if(!$self->legacy_script_support) {
1345         my $results = $self->run_indb_circ_test;
1346         push @allevents, $self->matrix_test_result_events
1347             unless $self->circ_test_success;
1348     } else {
1349     
1350        # ---------------------------------------------------------------------
1351        # Capture all of the copy permit events
1352        # ---------------------------------------------------------------------
1353        $runner->load($self->circ_permit_copy);
1354        my $result = $runner->run or 
1355             throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1356        my $copy_events = $result->{events};
1357
1358        # ---------------------------------------------------------------------
1359        # Now collect all of the events together
1360        # ---------------------------------------------------------------------
1361        push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1362     }
1363
1364     # See if this copy has an alert message
1365     my $ae = $self->check_copy_alert();
1366     push( @allevents, $ae ) if $ae;
1367
1368    # uniquify the events
1369    my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1370    @allevents = values %hash;
1371
1372     $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1373
1374     $self->push_events(@allevents);
1375 }
1376
1377
1378 sub check_copy_alert {
1379     my $self = shift;
1380     return undef if $self->is_renewal;
1381     return OpenILS::Event->new(
1382         'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1383         if $self->copy and $self->copy->alert_message;
1384     return undef;
1385 }
1386
1387
1388
1389 # --------------------------------------------------------------------------
1390 # If the call is overriding and has permissions to override every collected
1391 # event, the are cleared.  Any event that the caller does not have
1392 # permission to override, will be left in the event list and bail_out will
1393 # be set
1394 # XXX We need code in here to cancel any holds/transits on copies 
1395 # that are being force-checked out
1396 # --------------------------------------------------------------------------
1397 sub override_events {
1398     my $self = shift;
1399     my @events = @{$self->events};
1400     return unless @events;
1401     my $oargs = $self->override_args;
1402
1403     if(!$self->override) {
1404         return $self->bail_out(1) 
1405             if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1406     }   
1407
1408     $self->events([]);
1409     
1410     for my $e (@events) {
1411         my $tc = $e->{textcode};
1412         next if $tc eq 'SUCCESS';
1413         if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1414             my $ov = "$tc.override";
1415             $logger->info("circulator: attempting to override event: $ov");
1416
1417             return $self->bail_on_events($self->editor->event)
1418                 unless( $self->editor->allowed($ov) );
1419         } else {
1420             return $self->bail_out(1);
1421         }
1422    }
1423 }
1424     
1425
1426 # --------------------------------------------------------------------------
1427 # If there is an open claimsreturn circ on the requested copy, close the 
1428 # circ if overriding, otherwise bail out
1429 # --------------------------------------------------------------------------
1430 sub handle_claims_returned {
1431     my $self = shift;
1432     my $copy = $self->copy;
1433
1434     my $CR = $self->editor->search_action_circulation(
1435         {   
1436             target_copy     => $copy->id,
1437             stop_fines      => OILS_STOP_FINES_CLAIMSRETURNED,
1438             checkin_time    => undef,
1439         }
1440     );
1441
1442     return unless ($CR = $CR->[0]); 
1443
1444     my $evt;
1445
1446     # - If the caller has set the override flag, we will check the item in
1447     if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1448
1449         $CR->checkin_time('now');   
1450         $CR->checkin_scan_time('now');   
1451         $CR->checkin_lib($self->circ_lib);
1452         $CR->checkin_workstation($self->editor->requestor->wsid);
1453         $CR->checkin_staff($self->editor->requestor->id);
1454
1455         $evt = $self->editor->event 
1456             unless $self->editor->update_action_circulation($CR);
1457
1458     } else {
1459         $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1460     }
1461
1462     $self->bail_on_events($evt) if $evt;
1463     return;
1464 }
1465
1466
1467 # --------------------------------------------------------------------------
1468 # This performs the checkout
1469 # --------------------------------------------------------------------------
1470 sub do_checkout {
1471     my $self = shift;
1472
1473     $self->log_me("do_checkout()");
1474
1475     # make sure perms are good if this isn't a renewal
1476     unless( $self->is_renewal ) {
1477         return $self->bail_on_events($self->editor->event)
1478             unless( $self->editor->allowed('COPY_CHECKOUT') );
1479     }
1480
1481     # verify the permit key
1482     unless( $self->check_permit_key ) {
1483         if( $self->permit_override ) {
1484             return $self->bail_on_events($self->editor->event)
1485                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1486         } else {
1487             return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1488         }   
1489     }
1490
1491     # if this is a non-cataloged circ, build the circ and finish
1492     if( $self->is_noncat ) {
1493         $self->checkout_noncat;
1494         $self->push_events(
1495             OpenILS::Event->new('SUCCESS', 
1496             payload => { noncat_circ => $self->circ }));
1497         return;
1498     }
1499
1500     if( $self->is_precat ) {
1501         $self->make_precat_copy;
1502         return if $self->bail_out;
1503
1504     } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1505         return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1506     }
1507
1508     $self->do_copy_checks;
1509     return if $self->bail_out;
1510
1511     $self->run_checkout_scripts();
1512     return if $self->bail_out;
1513
1514     $self->build_checkout_circ_object();
1515     return if $self->bail_out;
1516
1517     my $modify_to_start = $self->booking_adjusted_due_date();
1518     return if $self->bail_out;
1519
1520     $self->apply_modified_due_date($modify_to_start);
1521     return if $self->bail_out;
1522
1523     return $self->bail_on_events($self->editor->event)
1524         unless $self->editor->create_action_circulation($self->circ);
1525
1526     # refresh the circ to force local time zone for now
1527     $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1528
1529     if($self->limit_groups) {
1530         $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1531     }
1532
1533     $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1534     $self->update_copy;
1535     return if $self->bail_out;
1536
1537     $self->apply_deposit_fee();
1538     return if $self->bail_out;
1539
1540     $self->handle_checkout_holds();
1541     return if $self->bail_out;
1542
1543     # ------------------------------------------------------------------------------
1544     # Update the patron penalty info in the DB.  Run it for permit-overrides 
1545     # since the penalties are not updated during the permit phase
1546     # ------------------------------------------------------------------------------
1547     OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1548
1549     my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1550     
1551     my $pcirc;
1552     if($self->is_renewal) {
1553         # flesh the billing summary for the checked-in circ
1554         $pcirc = $self->editor->retrieve_action_circulation([
1555             $self->parent_circ,
1556             {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1557         ]);
1558     }
1559
1560     $self->push_events(
1561         OpenILS::Event->new('SUCCESS',
1562             payload  => {
1563                 copy             => $U->unflesh_copy($self->copy),
1564                 volume           => $self->volume,
1565                 circ             => $self->circ,
1566                 record           => $record,
1567                 holds_fulfilled  => $self->fulfilled_holds,
1568                 deposit_billing  => $self->deposit_billing,
1569                 rental_billing   => $self->rental_billing,
1570                 parent_circ      => $pcirc,
1571                 patron           => ($self->return_patron) ? $self->patron : undef,
1572                 patron_money     => $self->editor->retrieve_money_user_summary($self->patron->id)
1573             }
1574         )
1575     );
1576 }
1577
1578 sub apply_deposit_fee {
1579     my $self = shift;
1580     my $copy = $self->copy;
1581     return unless 
1582         ($self->is_deposit and not $self->is_deposit_exempt) or 
1583         ($self->is_rental and not $self->is_rental_exempt);
1584
1585     return if $self->is_deposit and $self->skip_deposit_fee;
1586     return if $self->is_rental and $self->skip_rental_fee;
1587
1588         my $bill = Fieldmapper::money::billing->new;
1589     my $amount = $copy->deposit_amount;
1590     my $billing_type;
1591     my $btype;
1592
1593     if($self->is_deposit) {
1594         $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1595         $btype = 5;
1596         $self->deposit_billing($bill);
1597     } else {
1598         $billing_type = OILS_BILLING_TYPE_RENTAL;
1599         $btype = 6;
1600         $self->rental_billing($bill);
1601     }
1602
1603         $bill->xact($self->circ->id);
1604         $bill->amount($amount);
1605         $bill->note(OILS_BILLING_NOTE_SYSTEM);
1606         $bill->billing_type($billing_type);
1607         $bill->btype($btype);
1608     $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1609
1610         $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1611 }
1612
1613 sub update_copy {
1614     my $self = shift;
1615     my $copy = $self->copy;
1616
1617     my $stat = $copy->status if ref $copy->status;
1618     my $loc = $copy->location if ref $copy->location;
1619     my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1620
1621     $copy->status($stat->id) if $stat;
1622     $copy->location($loc->id) if $loc;
1623     $copy->circ_lib($circ_lib->id) if $circ_lib;
1624     $copy->editor($self->editor->requestor->id);
1625     $copy->edit_date('now');
1626     $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1627
1628     return $self->bail_on_events($self->editor->event)
1629         unless $self->editor->update_asset_copy($self->copy);
1630
1631     $copy->status($U->copy_status($copy->status));
1632     $copy->location($loc) if $loc;
1633     $copy->circ_lib($circ_lib) if $circ_lib;
1634 }
1635
1636 sub update_reservation {
1637     my $self = shift;
1638     my $reservation = $self->reservation;
1639
1640     my $usr = $reservation->usr;
1641     my $target_rt = $reservation->target_resource_type;
1642     my $target_r = $reservation->target_resource;
1643     my $current_r = $reservation->current_resource;
1644
1645     $reservation->usr($usr->id) if ref $usr;
1646     $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1647     $reservation->target_resource($target_r->id) if ref $target_r;
1648     $reservation->current_resource($current_r->id) if ref $current_r;
1649
1650     return $self->bail_on_events($self->editor->event)
1651         unless $self->editor->update_booking_reservation($self->reservation);
1652
1653     my $evt;
1654     ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1655     $self->reservation($reservation);
1656 }
1657
1658
1659 sub bail_on_events {
1660     my( $self, @evts ) = @_;
1661     $self->push_events(@evts);
1662     $self->bail_out(1);
1663 }
1664
1665 # ------------------------------------------------------------------------------
1666 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1667 # affects copies that will fulfill holds and CIRC affects all other copies.
1668 # If blocks exists, bail, push Events onto the event pile, and return true.
1669 # ------------------------------------------------------------------------------
1670 sub check_hold_fulfill_blocks {
1671     my $self = shift;
1672
1673     # See if the user has any penalties applied that prevent hold fulfillment
1674     my $pens = $self->editor->json_query({
1675         select => {csp => ['name', 'label']},
1676         from => {ausp => {csp => {}}},
1677         where => {
1678             '+ausp' => {
1679                 usr => $self->patron->id,
1680                 org_unit => $U->get_org_full_path($self->circ_lib),
1681                 '-or' => [
1682                     {stop_date => undef},
1683                     {stop_date => {'>' => 'now'}}
1684                 ]
1685             },
1686             '+csp' => {block_list => {'like' => '%FULFILL%'}}
1687         }
1688     });
1689
1690     return 0 unless @$pens;
1691
1692     for my $pen (@$pens) {
1693         $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1694         my $event = OpenILS::Event->new($pen->{name});
1695         $event->{desc} = $pen->{label};
1696         $self->push_events($event);
1697     }
1698
1699     $self->override_events;
1700     return $self->bail_out;
1701 }
1702
1703
1704 # ------------------------------------------------------------------------------
1705 # When an item is checked out, see if we can fulfill a hold for this patron
1706 # ------------------------------------------------------------------------------
1707 sub handle_checkout_holds {
1708    my $self    = shift;
1709    my $copy    = $self->copy;
1710    my $patron  = $self->patron;
1711
1712    my $e = $self->editor;
1713    $self->fulfilled_holds([]);
1714
1715    # pre/non-cats can't fulfill a hold
1716    return if $self->is_precat or $self->is_noncat;
1717
1718     my $hold = $e->search_action_hold_request({   
1719         current_copy        => $copy->id , 
1720         cancel_time         => undef, 
1721         fulfillment_time    => undef,
1722         '-or' => [
1723             {expire_time => undef},
1724             {expire_time => {'>' => 'now'}}
1725         ]
1726     })->[0];
1727
1728     if($hold and $hold->usr != $patron->id) {
1729         # reset the hold since the copy is now checked out
1730     
1731         $logger->info("circulator: un-targeting hold ".$hold->id.
1732             " because copy ".$copy->id." is getting checked out");
1733
1734         $hold->clear_prev_check_time; 
1735         $hold->clear_current_copy;
1736         $hold->clear_capture_time;
1737         $hold->clear_shelf_time;
1738         $hold->clear_shelf_expire_time;
1739             $hold->clear_current_shelf_lib;
1740
1741         return $self->bail_on_event($e->event)
1742             unless $e->update_action_hold_request($hold);
1743
1744         $hold = undef;
1745     }
1746
1747     unless($hold) {
1748         $hold = $self->find_related_user_hold($copy, $patron) or return;
1749         $logger->info("circulator: found related hold to fulfill in checkout");
1750     }
1751
1752     return if $self->check_hold_fulfill_blocks;
1753
1754     $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1755
1756     # if the hold was never officially captured, capture it.
1757     $hold->current_copy($copy->id);
1758     $hold->capture_time('now') unless $hold->capture_time;
1759     $hold->fulfillment_time('now');
1760     $hold->fulfillment_staff($e->requestor->id);
1761     $hold->fulfillment_lib($self->circ_lib);
1762
1763     return $self->bail_on_events($e->event)
1764         unless $e->update_action_hold_request($hold);
1765
1766     $holdcode->delete_hold_copy_maps($e, $hold->id);
1767     return $self->fulfilled_holds([$hold->id]);
1768 }
1769
1770
1771 # ------------------------------------------------------------------------------
1772 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1773 # the patron directly targets the checked out item, see if there is another hold 
1774 # for the patron that could be fulfilled by the checked out item.  Fulfill the
1775 # oldest hold and only fulfill 1 of them.
1776
1777 # For "another hold":
1778 #
1779 # First, check for one that the copy matches via hold_copy_map, ensuring that
1780 # *any* hold type that this copy could fill may end up filled.
1781 #
1782 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1783 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1784 # that are non-requestable to count as capturing those hold types.
1785 # ------------------------------------------------------------------------------
1786 sub find_related_user_hold {
1787     my($self, $copy, $patron) = @_;
1788     my $e = $self->editor;
1789
1790     return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER; 
1791
1792     return undef unless $U->ou_ancestor_setting_value(        
1793         $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1794
1795     # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1796     my $args = {
1797         select => {ahr => ['id']}, 
1798         from => {
1799             ahr => {
1800                 ahcm => {
1801                     field => 'hold',
1802                     fkey => 'id'
1803                 },
1804                 acp => {
1805                     field => 'id', 
1806                     fkey => 'current_copy',
1807                     type => 'left' # there may be no current_copy
1808                 }
1809             }
1810         }, 
1811         where => {
1812             '+ahr' => {
1813                 usr => $patron->id,
1814                 fulfillment_time => undef,
1815                 cancel_time => undef,
1816                '-or' => [
1817                     {expire_time => undef},
1818                     {expire_time => {'>' => 'now'}}
1819                 ]
1820             },
1821             '+ahcm' => {
1822                 target_copy => $self->copy->id
1823             },
1824             '+acp' => {
1825                 '-or' => [
1826                     {id => undef}, # left-join copy may be nonexistent
1827                     {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1828                 ]
1829             }
1830         },
1831         order_by => {ahr => {request_time => {direction => 'asc'}}},
1832         limit => 1
1833     };
1834
1835     my $hold_info = $e->json_query($args)->[0];
1836     return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1837     return undef if $U->ou_ancestor_setting_value(        
1838         $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1839
1840     # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1841     $args = {
1842         select => {ahr => ['id']}, 
1843         from => {
1844             ahr => {
1845                 acp => {
1846                     field => 'id', 
1847                     fkey => 'current_copy',
1848                     type => 'left' # there may be no current_copy
1849                 }
1850             }
1851         }, 
1852         where => {
1853             '+ahr' => {
1854                 usr => $patron->id,
1855                 fulfillment_time => undef,
1856                 cancel_time => undef,
1857                '-or' => [
1858                     {expire_time => undef},
1859                     {expire_time => {'>' => 'now'}}
1860                 ]
1861             },
1862             '-or' => [
1863                 {
1864                     '+ahr' => { 
1865                         hold_type => 'V',
1866                         target => $self->volume->id
1867                     }
1868                 },
1869                 { 
1870                     '+ahr' => { 
1871                         hold_type => 'T',
1872                         target => $self->title->id
1873                     }
1874                 },
1875             ],
1876             '+acp' => {
1877                 '-or' => [
1878                     {id => undef}, # left-join copy may be nonexistent
1879                     {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1880                 ]
1881             }
1882         },
1883         order_by => {ahr => {request_time => {direction => 'asc'}}},
1884         limit => 1
1885     };
1886
1887     $hold_info = $e->json_query($args)->[0];
1888     return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1889     return undef;
1890 }
1891
1892
1893 sub run_checkout_scripts {
1894     my $self = shift;
1895     my $nobail = shift;
1896
1897     my $evt;
1898     my $runner = $self->script_runner;
1899
1900     my $duration;
1901     my $recurring;
1902     my $max_fine;
1903     my $hard_due_date;
1904     my $duration_name;
1905     my $recurring_name;
1906     my $max_fine_name;
1907     my $hard_due_date_name;
1908
1909     if(!$self->legacy_script_support) {
1910         $self->run_indb_circ_test();
1911         $duration = $self->circ_matrix_matchpoint->duration_rule;
1912         $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1913         $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1914         $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1915
1916     } else {
1917
1918        $runner->load($self->circ_duration);
1919
1920        my $result = $runner->run or 
1921             throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1922
1923        $duration_name   = $result->{durationRule};
1924        $recurring_name  = $result->{recurringFinesRule};
1925        $max_fine_name   = $result->{maxFine};
1926        $hard_due_date_name  = $result->{hardDueDate};
1927     }
1928
1929     $duration_name = $duration->name if $duration;
1930     if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1931
1932         unless($duration) {
1933             ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1934             return $self->bail_on_events($evt) if ($evt && !$nobail);
1935         
1936             ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1937             return $self->bail_on_events($evt) if ($evt && !$nobail);
1938         
1939             ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1940             return $self->bail_on_events($evt) if ($evt && !$nobail);
1941
1942             if($hard_due_date_name) {
1943                 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1944                 return $self->bail_on_events($evt) if ($evt && !$nobail);
1945             }
1946         }
1947
1948     } else {
1949
1950         # The item circulates with an unlimited duration
1951         $duration   = undef;
1952         $recurring  = undef;
1953         $max_fine   = undef;
1954         $hard_due_date = undef;
1955     }
1956
1957    $self->duration_rule($duration);
1958    $self->recurring_fines_rule($recurring);
1959    $self->max_fine_rule($max_fine);
1960    $self->hard_due_date($hard_due_date);
1961 }
1962
1963
1964 sub build_checkout_circ_object {
1965     my $self = shift;
1966
1967    my $circ       = Fieldmapper::action::circulation->new;
1968    my $duration   = $self->duration_rule;
1969    my $max        = $self->max_fine_rule;
1970    my $recurring  = $self->recurring_fines_rule;
1971    my $hard_due_date    = $self->hard_due_date;
1972    my $copy       = $self->copy;
1973    my $patron     = $self->patron;
1974    my $duration_date_ceiling;
1975    my $duration_date_ceiling_force;
1976
1977     if( $duration ) {
1978
1979         my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1980         $duration_date_ceiling = $policy->{duration_date_ceiling};
1981         $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1982
1983         my $dname = $duration->name;
1984         my $mname = $max->name;
1985         my $rname = $recurring->name;
1986         my $hdname = ''; 
1987         if($hard_due_date) {
1988             $hdname = $hard_due_date->name;
1989         }
1990
1991         $logger->debug("circulator: building circulation ".
1992             "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1993     
1994         $circ->duration($policy->{duration});
1995         $circ->recurring_fine($policy->{recurring_fine});
1996         $circ->duration_rule($duration->name);
1997         $circ->recurring_fine_rule($recurring->name);
1998         $circ->max_fine_rule($max->name);
1999         $circ->max_fine($policy->{max_fine});
2000         $circ->fine_interval($recurring->recurrence_interval);
2001         $circ->renewal_remaining($duration->max_renewals);
2002         $circ->grace_period($policy->{grace_period});
2003
2004     } else {
2005
2006         $logger->info("circulator: copy found with an unlimited circ duration");
2007         $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2008         $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2009         $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2010         $circ->renewal_remaining(0);
2011         $circ->grace_period(0);
2012     }
2013
2014    $circ->target_copy( $copy->id );
2015    $circ->usr( $patron->id );
2016    $circ->circ_lib( $self->circ_lib );
2017    $circ->workstation($self->editor->requestor->wsid) 
2018     if defined $self->editor->requestor->wsid;
2019
2020     # renewals maintain a link to the parent circulation
2021     $circ->parent_circ($self->parent_circ);
2022
2023    if( $self->is_renewal ) {
2024       $circ->opac_renewal('t') if $self->opac_renewal;
2025       $circ->phone_renewal('t') if $self->phone_renewal;
2026       $circ->desk_renewal('t') if $self->desk_renewal;
2027       $circ->renewal_remaining($self->renewal_remaining);
2028       $circ->circ_staff($self->editor->requestor->id);
2029    }
2030
2031
2032     # if the user provided an overiding checkout time,
2033     # (e.g. the checkout really happened several hours ago), then
2034     # we apply that here.  Does this need a perm??
2035     $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2036         if $self->checkout_time;
2037
2038     # if a patron is renewing, 'requestor' will be the patron
2039     $circ->circ_staff($self->editor->requestor->id);
2040     $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
2041
2042     $self->circ($circ);
2043 }
2044
2045 sub do_reservation_pickup {
2046     my $self = shift;
2047
2048     $self->log_me("do_reservation_pickup()");
2049
2050     $self->reservation->pickup_time('now');
2051
2052     if (
2053         $self->reservation->current_resource &&
2054         $U->is_true($self->reservation->target_resource_type->catalog_item)
2055     ) {
2056         # We used to try to set $self->copy and $self->patron here,
2057         # but that should already be done.
2058
2059         $self->run_checkout_scripts(1);
2060
2061         my $duration   = $self->duration_rule;
2062         my $max        = $self->max_fine_rule;
2063         my $recurring  = $self->recurring_fines_rule;
2064
2065         if ($duration && $max && $recurring) {
2066             my $policy = $self->get_circ_policy($duration, $recurring, $max);
2067
2068             my $dname = $duration->name;
2069             my $mname = $max->name;
2070             my $rname = $recurring->name;
2071
2072             $logger->debug("circulator: updating reservation ".
2073                 "with duration=$dname, maxfine=$mname, recurring=$rname");
2074
2075             $self->reservation->fine_amount($policy->{recurring_fine});
2076             $self->reservation->max_fine($policy->{max_fine});
2077             $self->reservation->fine_interval($recurring->recurrence_interval);
2078         }
2079
2080         $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2081         $self->update_copy();
2082
2083     } else {
2084         $self->reservation->fine_amount(
2085             $self->reservation->target_resource_type->fine_amount
2086         );
2087         $self->reservation->max_fine(
2088             $self->reservation->target_resource_type->max_fine
2089         );
2090         $self->reservation->fine_interval(
2091             $self->reservation->target_resource_type->fine_interval
2092         );
2093     }
2094
2095     $self->update_reservation();
2096 }
2097
2098 sub do_reservation_return {
2099     my $self = shift;
2100     my $request = shift;
2101
2102     $self->log_me("do_reservation_return()");
2103
2104     if (not ref $self->reservation) {
2105         my ($reservation, $evt) =
2106             $U->fetch_booking_reservation($self->reservation);
2107         return $self->bail_on_events($evt) if $evt;
2108         $self->reservation($reservation);
2109     }
2110
2111     $self->generate_fines(1);
2112     $self->reservation->return_time('now');
2113     $self->update_reservation();
2114     $self->reshelve_copy if $self->copy;
2115
2116     if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2117         $self->copy( $self->reservation->current_resource->catalog_item );
2118     }
2119 }
2120
2121 sub booking_adjusted_due_date {
2122     my $self = shift;
2123     my $circ = $self->circ;
2124     my $copy = $self->copy;
2125
2126     return undef unless $self->use_booking;
2127
2128     my $changed;
2129
2130     if( $self->due_date ) {
2131
2132         return $self->bail_on_events($self->editor->event)
2133             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2134
2135        $circ->due_date(cleanse_ISO8601($self->due_date));
2136
2137     } else {
2138
2139         return unless $copy and $circ->due_date;
2140     }
2141
2142     my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2143     if (@$booking_items) {
2144         my $booking_item = $booking_items->[0];
2145         my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2146
2147         my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2148         my $shorten_circ_setting = $resource_type->elbow_room ||
2149             $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2150             '0 seconds';
2151
2152         my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2153         my $bookings = $booking_ses->request(
2154             'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2155             { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2156         )->gather(1);
2157         $booking_ses->disconnect;
2158         
2159         my $dt_parser = DateTime::Format::ISO8601->new;
2160         my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2161
2162         for my $bid (@$bookings) {
2163
2164             my $booking = $self->editor->retrieve_booking_reservation( $bid );
2165
2166             my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2167             my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2168
2169             return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2170                 if ($booking_start < DateTime->now);
2171
2172
2173             if ($U->is_true($stop_circ_setting)) {
2174                 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ); 
2175             } else {
2176                 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2177                 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now); 
2178             }
2179             
2180             # We set the circ duration here only to affect the logic that will
2181             # later (in a DB trigger) mangle the time part of the due date to
2182             # 11:59pm. Having any circ duration that is not a whole number of
2183             # days is enough to prevent the "correction."
2184             my $new_circ_duration = $due_date->epoch - time;
2185             $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2186             $circ->duration("$new_circ_duration seconds");
2187
2188             $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2189             $changed = 1;
2190         }
2191
2192         return $self->bail_on_events($self->editor->event)
2193             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2194     }
2195
2196     return $changed;
2197 }
2198
2199 sub apply_modified_due_date {
2200     my $self = shift;
2201     my $shift_earlier = shift;
2202     my $circ = $self->circ;
2203     my $copy = $self->copy;
2204
2205    if( $self->due_date ) {
2206
2207         return $self->bail_on_events($self->editor->event)
2208             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2209
2210       $circ->due_date(cleanse_ISO8601($self->due_date));
2211
2212    } else {
2213
2214       # if the due_date lands on a day when the location is closed
2215       return unless $copy and $circ->due_date;
2216
2217         #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2218
2219         # due-date overlap should be determined by the location the item
2220         # is checked out from, not the owning or circ lib of the item
2221         my $org = $self->circ_lib;
2222
2223       $logger->info("circulator: circ searching for closed date overlap on lib $org".
2224             " with an item due date of ".$circ->due_date );
2225
2226       my $dateinfo = $U->storagereq(
2227          'open-ils.storage.actor.org_unit.closed_date.overlap', 
2228             $org, $circ->due_date );
2229
2230       if($dateinfo) {
2231          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2232             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2233
2234             # XXX make the behavior more dynamic
2235             # for now, we just push the due date to after the close date
2236             if ($shift_earlier) {
2237                 $circ->due_date($dateinfo->{start});
2238             } else {
2239                 $circ->due_date($dateinfo->{end});
2240             }
2241       }
2242    }
2243 }
2244
2245
2246
2247 sub create_due_date {
2248     my( $self, $duration, $date_ceiling, $force_date ) = @_;
2249
2250     # if there is a raw time component (e.g. from postgres), 
2251     # turn it into an interval that interval_to_seconds can parse
2252     $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2253
2254     # for now, use the server timezone.  TODO: use workstation org timezone
2255     my $due_date = DateTime->now(time_zone => 'local');
2256
2257     # add the circ duration
2258     $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2259
2260     if($date_ceiling) {
2261         my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2262         if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2263             $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2264             $due_date = $cdate;
2265         }
2266     }
2267
2268     # return ISO8601 time with timezone
2269     return $due_date->strftime('%FT%T%z');
2270 }
2271
2272
2273
2274 sub make_precat_copy {
2275     my $self = shift;
2276     my $copy = $self->copy;
2277
2278    if($copy) {
2279         $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2280
2281         $copy->editor($self->editor->requestor->id);
2282         $copy->edit_date('now');
2283         $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2284         $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2285         $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2286         $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2287         $self->update_copy();
2288         return;
2289    }
2290
2291     $logger->info("circulator: Creating a new precataloged ".
2292         "copy in checkout with barcode " . $self->copy_barcode);
2293
2294     $copy = Fieldmapper::asset::copy->new;
2295     $copy->circ_lib($self->circ_lib);
2296     $copy->creator($self->editor->requestor->id);
2297     $copy->editor($self->editor->requestor->id);
2298     $copy->barcode($self->copy_barcode);
2299     $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
2300     $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2301     $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2302
2303     $copy->dummy_title($self->dummy_title || "");
2304     $copy->dummy_author($self->dummy_author || "");
2305     $copy->dummy_isbn($self->dummy_isbn || "");
2306     $copy->circ_modifier($self->circ_modifier);
2307
2308
2309     # See if we need to override the circ_lib for the copy with a configured circ_lib
2310     # Setting is shortname of the org unit
2311     my $precat_circ_lib = $U->ou_ancestor_setting_value(
2312         $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2313
2314     if($precat_circ_lib) {
2315         my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2316
2317         if(!$org) {
2318             $self->bail_on_events($self->editor->event);
2319             return;
2320         }
2321
2322         $copy->circ_lib($org->id);
2323     }
2324
2325
2326     unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2327         $self->bail_out(1);
2328         $self->push_events($self->editor->event);
2329         return;
2330     }   
2331
2332     # this is a little bit of a hack, but we need to 
2333     # get the copy into the script runner
2334     $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2335 }
2336
2337
2338 sub checkout_noncat {
2339     my $self = shift;
2340
2341     my $circ;
2342     my $evt;
2343
2344    my $lib      = $self->noncat_circ_lib || $self->circ_lib;
2345    my $count    = $self->noncat_count || 1;
2346    my $cotime   = cleanse_ISO8601($self->checkout_time) || "";
2347
2348    $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2349
2350    for(1..$count) {
2351
2352       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2353          $self->editor->requestor->id, 
2354             $self->patron->id, 
2355             $lib, 
2356             $self->noncat_type, 
2357             $cotime,
2358             $self->editor );
2359
2360         if( $evt ) {
2361             $self->push_events($evt);
2362             $self->bail_out(1);
2363             return; 
2364         }
2365         $self->circ($circ);
2366    }
2367 }
2368
2369 # If a copy goes into transit and is then checked in before the transit checkin 
2370 # interval has expired, push an event onto the overridable events list.
2371 sub check_transit_checkin_interval {
2372     my $self = shift;
2373
2374     # only concerned with in-transit items
2375     return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2376
2377     # no interval, no problem
2378     my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2379     return unless $interval;
2380
2381     # capture the transit so we don't have to fetch it again later during checkin
2382     $self->transit(
2383         $self->editor->search_action_transit_copy(
2384             {target_copy => $self->copy->id, dest_recv_time => undef}
2385         )->[0]
2386     ); 
2387
2388     # transit from X to X for whatever reason has no min interval
2389     return if $self->transit->source == $self->transit->dest;
2390
2391     my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2392     my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2393     my $horizon = $t_start->add(seconds => $seconds);
2394
2395     # See if we are still within the transit checkin forbidden range
2396     $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK')) 
2397         if $horizon > DateTime->now;
2398 }
2399
2400 # Retarget local holds at checkin
2401 sub checkin_retarget {
2402     my $self = shift;
2403     return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2404     return unless $self->is_checkin; # Renewals need not be checked
2405     return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2406     return if $self->is_precat; # No holds for precats
2407     return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2408     return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2409     my $status = $U->copy_status($self->copy->status);
2410     return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2411     # Specifically target items that are likely new (by status ID)
2412     return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2413     my $location = $self->copy->location;
2414     if(!ref($location)) {
2415         $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2416         $self->copy->location($location);
2417     }
2418     return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2419
2420     # Fetch holds for the bib
2421     my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2422                     $self->editor->authtoken,
2423                     $self->title->id,
2424                     {
2425                         capture_time => undef, # No touching captured holds
2426                         frozen => 'f', # Don't bother with frozen holds
2427                         pickup_lib => $self->circ_lib # Only holds actually here
2428                     }); 
2429
2430     # Error? Skip the step.
2431     return if exists $result->{"ilsevent"};
2432
2433     # Assemble holds
2434     my $holds = [];
2435     foreach my $holdlist (keys %{$result}) {
2436         push @$holds, @{$result->{$holdlist}};
2437     }
2438
2439     return if scalar(@$holds) == 0; # No holds, no retargeting
2440
2441     # Check for parts on this copy
2442     my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2443     my %parts_hash = ();
2444     %parts_hash = map {$_->part, 1} @$parts if @$parts;
2445
2446     # Loop over holds in request-ish order
2447     # Stage 1: Get them into request-ish order
2448     # Also grab type and target for skipping low hanging ones
2449     $result = $self->editor->json_query({
2450         "select" => { "ahr" => ["id", "hold_type", "target"] },
2451         "from" => { "ahr" => { "au" => { "fkey" => "usr",  "join" => "pgt"} } },
2452         "where" => { "id" => $holds },
2453         "order_by" => [
2454             { "class" => "pgt", "field" => "hold_priority"},
2455             { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2456             { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2457             { "class" => "ahr", "field" => "request_time"}
2458         ]
2459     });
2460
2461     # Stage 2: Loop!
2462     if (ref $result eq "ARRAY" and scalar @$result) {
2463         foreach (@{$result}) {
2464             # Copy level, but not this copy?
2465             next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2466                 and $_->{target} != $self->copy->id);
2467             # Volume level, but not this volume?
2468             next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2469             if(@$parts) { # We have parts?
2470                 # Skip title holds
2471                 next if ($_->{hold_type} eq 'T');
2472                 # Skip part holds for parts not on this copy
2473                 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2474             } else {
2475                 # No parts, no part holds
2476                 next if ($_->{hold_type} eq 'P');
2477             }
2478             # So much for easy stuff, attempt a retarget!
2479             my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2480             if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2481                 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2482             }
2483         }
2484     }
2485 }
2486
2487 sub do_checkin {
2488     my $self = shift;
2489     $self->log_me("do_checkin()");
2490
2491     return $self->bail_on_events(
2492         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
2493         unless $self->copy;
2494
2495     $self->check_transit_checkin_interval;
2496     $self->checkin_retarget;
2497
2498     # the renew code and mk_env should have already found our circulation object
2499     unless( $self->circ ) {
2500
2501         my $circs = $self->editor->search_action_circulation(
2502             { target_copy => $self->copy->id, checkin_time => undef });
2503
2504         $self->circ($$circs[0]);
2505
2506         # for now, just warn if there are multiple open circs on a copy
2507         $logger->warn("circulator: we have ".scalar(@$circs).
2508             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2509     }
2510
2511     # run the fine generator against this circ, if this circ is there
2512     $self->generate_fines_start if $self->circ;
2513
2514     if( $self->checkin_check_holds_shelf() ) {
2515         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2516         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2517         if($self->fake_hold_dest) {
2518             $self->hold->pickup_lib($self->circ_lib);
2519         }
2520         $self->checkin_flesh_events;
2521         return;
2522     }
2523
2524     unless( $self->is_renewal ) {
2525         return $self->bail_on_events($self->editor->event)
2526             unless $self->editor->allowed('COPY_CHECKIN');
2527     }
2528
2529     $self->push_events($self->check_copy_alert());
2530     $self->push_events($self->check_checkin_copy_status());
2531
2532     # if the circ is marked as 'claims returned', add the event to the list
2533     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2534         if ($self->circ and $self->circ->stop_fines 
2535                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2536
2537     $self->check_circ_deposit();
2538
2539     # handle the overridable events 
2540     $self->override_events unless $self->is_renewal;
2541     return if $self->bail_out;
2542     
2543     if( $self->copy and !$self->transit ) {
2544         $self->transit(
2545             $self->editor->search_action_transit_copy(
2546                 { target_copy => $self->copy->id, dest_recv_time => undef }
2547             )->[0]
2548         ); 
2549     }
2550
2551     if( $self->circ ) {
2552         $self->generate_fines_finish;
2553         $self->checkin_handle_circ;
2554         return if $self->bail_out;
2555         $self->checkin_changed(1);
2556
2557     } elsif( $self->transit ) {
2558         my $hold_transit = $self->process_received_transit;
2559         $self->checkin_changed(1);
2560
2561         if( $self->bail_out ) { 
2562             $self->checkin_flesh_events;
2563             return;
2564         }
2565         
2566         if( my $e = $self->check_checkin_copy_status() ) {
2567             # If the original copy status is special, alert the caller
2568             my $ev = $self->events;
2569             $self->events([$e]);
2570             $self->override_events;
2571             return if $self->bail_out;
2572             $self->events($ev);
2573         }
2574
2575         if( $hold_transit or 
2576                 $U->copy_status($self->copy->status)->id 
2577                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2578
2579             my $hold;
2580             if( $hold_transit ) {
2581                $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2582             } else {
2583                    ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2584             }
2585
2586             $self->hold($hold);
2587
2588             if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2589
2590                 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2591                 $self->reshelve_copy(1);
2592                 $self->cancelled_hold_transit(1);
2593                 $self->notify_hold(0); # don't notify for cancelled holds
2594                 $self->fake_hold_dest(0);
2595                 return if $self->bail_out;
2596
2597             } elsif ($hold and $hold->hold_type eq 'R') {
2598
2599                 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2600                 $self->notify_hold(0); # No need to notify
2601                 $self->fake_hold_dest(0);
2602                 $self->noop(1); # Don't try and capture for other holds/transits now
2603                 $self->update_copy();
2604                 $hold->fulfillment_time('now');
2605                 $self->bail_on_events($self->editor->event)
2606                     unless $self->editor->update_action_hold_request($hold);
2607
2608             } else {
2609
2610                 # hold transited to correct location
2611                 if($self->fake_hold_dest) {
2612                     $hold->pickup_lib($self->circ_lib);
2613                 }
2614                 $self->checkin_flesh_events;
2615                 return;
2616             }
2617         } 
2618
2619     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2620
2621         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2622             " that is in-transit, but there is no transit.. repairing");
2623         $self->reshelve_copy(1);
2624         return if $self->bail_out;
2625     }
2626
2627     if( $self->is_renewal ) {
2628         $self->finish_fines_and_voiding;
2629         return if $self->bail_out;
2630         $self->push_events(OpenILS::Event->new('SUCCESS'));
2631         return;
2632     }
2633
2634    # ------------------------------------------------------------------------------
2635    # Circulations and transits are now closed where necessary.  Now go on to see if
2636    # this copy can fulfill a hold or needs to be routed to a different location
2637    # ------------------------------------------------------------------------------
2638
2639     my $needed_for_something = 0; # formerly "needed_for_hold"
2640
2641     if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2642
2643         if (!$self->remote_hold) {
2644             if ($self->use_booking) {
2645                 my $potential_hold = $self->hold_capture_is_possible;
2646                 my $potential_reservation = $self->reservation_capture_is_possible;
2647
2648                 if ($potential_hold and $potential_reservation) {
2649                     $logger->info("circulator: item could fulfill either hold or reservation");
2650                     $self->push_events(new OpenILS::Event(
2651                         "HOLD_RESERVATION_CONFLICT",
2652                         "hold" => $potential_hold,
2653                         "reservation" => $potential_reservation
2654                     ));
2655                     return if $self->bail_out;
2656                 } elsif ($potential_hold) {
2657                     $needed_for_something =
2658                         $self->attempt_checkin_hold_capture;
2659                 } elsif ($potential_reservation) {
2660                     $needed_for_something =
2661                         $self->attempt_checkin_reservation_capture;
2662                 }
2663             } else {
2664                 $needed_for_something = $self->attempt_checkin_hold_capture;
2665             }
2666         }
2667         return if $self->bail_out;
2668     
2669         unless($needed_for_something) {
2670             my $circ_lib = (ref $self->copy->circ_lib) ? 
2671                     $self->copy->circ_lib->id : $self->copy->circ_lib;
2672     
2673             if( $self->remote_hold ) {
2674                 $circ_lib = $self->remote_hold->pickup_lib;
2675                 $logger->warn("circulator: Copy ".$self->copy->barcode.
2676                     " is on a remote hold's shelf, sending to $circ_lib");
2677             }
2678     
2679             $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2680
2681             my $suppress_transit = 0;
2682
2683             if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2684                 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2685                 if($suppress_transit_source && $suppress_transit_source->{value}) {
2686                     my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2687                     if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2688                         $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2689                         $suppress_transit = 1;
2690                     }
2691                 }
2692             }
2693  
2694             if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2695                 # copy is where it needs to be, either for hold or reshelving
2696     
2697                 $self->checkin_handle_precat();
2698                 return if $self->bail_out;
2699     
2700             } else {
2701                 # copy needs to transit "home", or stick here if it's a floating copy
2702     
2703                 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2704                     $self->checkin_changed(1);
2705                     $self->copy->circ_lib( $self->circ_lib );
2706                     $self->update_copy;
2707                 } else {
2708                     my $bc = $self->copy->barcode;
2709                     $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2710                     $self->checkin_build_copy_transit($circ_lib);
2711                     return if $self->bail_out;
2712                     $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2713                 }
2714             }
2715         }
2716     } else { # no-op checkin
2717         if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2718             $self->checkin_changed(1);
2719             $self->copy->circ_lib( $self->circ_lib );
2720             $self->update_copy;
2721         }
2722     }
2723
2724     if($self->claims_never_checked_out and 
2725             $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2726
2727         # the item was not supposed to be checked out to the user and should now be marked as missing
2728         $self->copy->status(OILS_COPY_STATUS_MISSING);
2729         $self->update_copy;
2730
2731     } else {
2732         $self->reshelve_copy unless $needed_for_something;
2733     }
2734
2735     return if $self->bail_out;
2736
2737     unless($self->checkin_changed) {
2738
2739         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2740         my $stat = $U->copy_status($self->copy->status)->id;
2741
2742         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2743          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2744         $self->bail_out(1); # no need to commit anything
2745
2746     } else {
2747
2748         $self->push_events(OpenILS::Event->new('SUCCESS')) 
2749             unless @{$self->events};
2750     }
2751
2752     $self->finish_fines_and_voiding;
2753
2754     OpenILS::Utils::Penalty->calculate_penalties(
2755         $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2756
2757     $self->checkin_flesh_events;
2758     return;
2759 }
2760
2761 sub finish_fines_and_voiding {
2762     my $self = shift;
2763     return unless $self->circ;
2764
2765     # gather any updates to the circ after fine generation, if there was a circ
2766     $self->generate_fines_finish;
2767
2768     return unless $self->backdate or $self->void_overdues;
2769
2770     # void overdues after fine generation to prevent concurrent DB access to overdue billings
2771     my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2772
2773     my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2774         $self->editor, $self->circ, $self->backdate, $note);
2775
2776     return $self->bail_on_events($evt) if $evt;
2777
2778     # make sure the circ isn't closed if we just voided some fines
2779     $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2780     return $self->bail_on_events($evt) if $evt;
2781
2782     return undef;
2783 }
2784
2785
2786 # if a deposit was payed for this item, push the event
2787 sub check_circ_deposit {
2788     my $self = shift;
2789     return unless $self->circ;
2790     my $deposit = $self->editor->search_money_billing(
2791         {   btype => 5, 
2792             xact => $self->circ->id, 
2793             voided => 'f'
2794         }, {idlist => 1})->[0];
2795
2796     $self->push_events(OpenILS::Event->new(
2797         'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2798 }
2799
2800 sub reshelve_copy {
2801    my $self    = shift;
2802    my $force   = $self->force || shift;
2803    my $copy    = $self->copy;
2804
2805    my $stat = $U->copy_status($copy->status)->id;
2806
2807    if($force || (
2808       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2809       $stat != OILS_COPY_STATUS_CATALOGING and
2810       $stat != OILS_COPY_STATUS_IN_TRANSIT and
2811       $stat != OILS_COPY_STATUS_RESHELVING  )) {
2812
2813         $copy->status( OILS_COPY_STATUS_RESHELVING );
2814             $self->update_copy;
2815             $self->checkin_changed(1);
2816     }
2817 }
2818
2819
2820 # Returns true if the item is at the current location
2821 # because it was transited there for a hold and the 
2822 # hold has not been fulfilled
2823 sub checkin_check_holds_shelf {
2824     my $self = shift;
2825     return 0 unless $self->copy;
2826
2827     return 0 unless 
2828         $U->copy_status($self->copy->status)->id ==
2829             OILS_COPY_STATUS_ON_HOLDS_SHELF;
2830
2831     # Attempt to clear shelf expired holds for this copy
2832     $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2833         if($self->clear_expired);
2834
2835     # find the hold that put us on the holds shelf
2836     my $holds = $self->editor->search_action_hold_request(
2837         { 
2838             current_copy => $self->copy->id,
2839             capture_time => { '!=' => undef },
2840             fulfillment_time => undef,
2841             cancel_time => undef,
2842         }
2843     );
2844
2845     unless(@$holds) {
2846         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2847         $self->reshelve_copy(1);
2848         return 0;
2849     }
2850
2851     my $hold = $$holds[0];
2852
2853     $logger->info("circulator: we found a captured, un-fulfilled hold [".
2854         $hold->id. "] for copy ".$self->copy->barcode);
2855
2856     if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2857         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2858         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2859             my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2860             if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2861                 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2862                 $self->fake_hold_dest(1);
2863                 return 1;
2864             }
2865         }
2866     }
2867
2868     if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2869         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2870         return 1;
2871     }
2872
2873     $logger->info("circulator: hold is not for here..");
2874     $self->remote_hold($hold);
2875     return 0;
2876 }
2877
2878
2879 sub checkin_handle_precat {
2880     my $self    = shift;
2881    my $copy    = $self->copy;
2882
2883    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2884         $copy->status(OILS_COPY_STATUS_CATALOGING);
2885         $self->update_copy();
2886         $self->checkin_changed(1);
2887         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2888    }
2889 }
2890
2891
2892 sub checkin_build_copy_transit {
2893     my $self            = shift;
2894     my $dest            = shift;
2895     my $copy       = $self->copy;
2896     my $transit    = Fieldmapper::action::transit_copy->new;
2897
2898     # if we are transiting an item to the shelf shelf, it's a hold transit
2899     if (my $hold = $self->remote_hold) {
2900         $transit = Fieldmapper::action::hold_transit_copy->new;
2901         $transit->hold($hold->id);
2902
2903         # the item is going into transit, remove any shelf-iness
2904         if ($hold->current_shelf_lib or $hold->shelf_time) {
2905             $hold->clear_current_shelf_lib;
2906             $hold->clear_shelf_time;
2907             return $self->bail_on_events($self->editor->event)
2908                 unless $self->editor->update_action_hold_request($hold);
2909         }
2910     }
2911
2912     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2913     $logger->info("circulator: transiting copy to $dest");
2914
2915     $transit->source($self->circ_lib);
2916     $transit->dest($dest);
2917     $transit->target_copy($copy->id);
2918     $transit->source_send_time('now');
2919     $transit->copy_status( $U->copy_status($copy->status)->id );
2920
2921     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2922
2923     if ($self->remote_hold) {
2924         return $self->bail_on_events($self->editor->event)
2925             unless $self->editor->create_action_hold_transit_copy($transit);
2926     } else {
2927         return $self->bail_on_events($self->editor->event)
2928             unless $self->editor->create_action_transit_copy($transit);
2929     }
2930
2931     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2932     $self->update_copy;
2933     $self->checkin_changed(1);
2934 }
2935
2936
2937 sub hold_capture_is_possible {
2938     my $self = shift;
2939     my $copy = $self->copy;
2940
2941     # we've been explicitly told not to capture any holds
2942     return 0 if $self->capture eq 'nocapture';
2943
2944     # See if this copy can fulfill any holds
2945     my $hold = $holdcode->find_nearest_permitted_hold(
2946         $self->editor, $copy, $self->editor->requestor, 1 # check_only
2947     );
2948     return undef if ref $hold eq "HASH" and
2949         $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2950     return $hold;
2951 }
2952
2953 sub reservation_capture_is_possible {
2954     my $self = shift;
2955     my $copy = $self->copy;
2956
2957     # we've been explicitly told not to capture any holds
2958     return 0 if $self->capture eq 'nocapture';
2959
2960     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2961     my $resv = $booking_ses->request(
2962         "open-ils.booking.reservations.could_capture",
2963         $self->editor->authtoken, $copy->barcode
2964     )->gather(1);
2965     $booking_ses->disconnect;
2966     if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2967         $self->push_events($resv);
2968     } else {
2969         return $resv;
2970     }
2971 }
2972
2973 # returns true if the item was used (or may potentially be used 
2974 # in subsequent calls) to capture a hold.
2975 sub attempt_checkin_hold_capture {
2976     my $self = shift;
2977     my $copy = $self->copy;
2978
2979     # we've been explicitly told not to capture any holds
2980     return 0 if $self->capture eq 'nocapture';
2981
2982     # See if this copy can fulfill any holds
2983     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
2984         $self->editor, $copy, $self->editor->requestor );
2985
2986     if(!$hold) {
2987         $logger->debug("circulator: no potential permitted".
2988             "holds found for copy ".$copy->barcode);
2989         return 0;
2990     }
2991
2992     if($self->capture ne 'capture') {
2993         # see if this item is in a hold-capture-delay location
2994         my $location = $self->copy->location;
2995         if(!ref($location)) {
2996             $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2997             $self->copy->location($location);
2998         }
2999         if($U->is_true($location->hold_verify)) {
3000             $self->bail_on_events(
3001                 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3002             return 1;
3003         }
3004     }
3005
3006     $self->retarget($retarget);
3007
3008     my $suppress_transit = 0;
3009     if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3010         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3011         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3012             my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3013             if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3014                 $suppress_transit = 1;
3015                 $self->hold->pickup_lib($self->circ_lib);
3016             }
3017         }
3018     }
3019
3020     $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3021
3022     $hold->current_copy($copy->id);
3023     $hold->capture_time('now');
3024     $self->put_hold_on_shelf($hold) 
3025         if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3026
3027     # prevent DB errors caused by fetching 
3028     # holds from storage, and updating through cstore
3029     $hold->clear_fulfillment_time;
3030     $hold->clear_fulfillment_staff;
3031     $hold->clear_fulfillment_lib;
3032     $hold->clear_expire_time; 
3033     $hold->clear_cancel_time;
3034     $hold->clear_prev_check_time unless $hold->prev_check_time;
3035
3036     $self->bail_on_events($self->editor->event)
3037         unless $self->editor->update_action_hold_request($hold);
3038     $self->hold($hold);
3039     $self->checkin_changed(1);
3040
3041     return 0 if $self->bail_out;
3042
3043     if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3044
3045         if ($hold->hold_type eq 'R') {
3046             $copy->status(OILS_COPY_STATUS_CATALOGING);
3047             $hold->fulfillment_time('now');
3048             $self->noop(1); # Block other transit/hold checks
3049             $self->bail_on_events($self->editor->event)
3050                 unless $self->editor->update_action_hold_request($hold);
3051         } else {
3052             # This hold was captured in the correct location
3053             $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3054             $self->push_events(OpenILS::Event->new('SUCCESS'));
3055
3056             #$self->do_hold_notify($hold->id);
3057             $self->notify_hold($hold->id);
3058         }
3059
3060     } else {
3061     
3062         # Hold needs to be picked up elsewhere.  Build a hold
3063         # transit and route the item.
3064         $self->checkin_build_hold_transit();
3065         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3066         return 0 if $self->bail_out;
3067         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3068     }
3069
3070     # make sure we save the copy status
3071     $self->update_copy;
3072     return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3073     return 1;
3074 }
3075
3076 sub attempt_checkin_reservation_capture {
3077     my $self = shift;
3078     my $copy = $self->copy;
3079
3080     # we've been explicitly told not to capture any holds
3081     return 0 if $self->capture eq 'nocapture';
3082
3083     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3084     my $evt = $booking_ses->request(
3085         "open-ils.booking.resources.capture_for_reservation",
3086         $self->editor->authtoken,
3087         $copy->barcode,
3088         1 # don't update copy - we probably have it locked
3089     )->gather(1);
3090     $booking_ses->disconnect;
3091
3092     if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3093         $logger->warn(
3094             "open-ils.booking.resources.capture_for_reservation " .
3095             "didn't return an event!"
3096         );
3097     } else {
3098         if (
3099             $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3100             $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3101         ) {
3102             # not-transferable is an error event we'll pass on the user
3103             $logger->warn("reservation capture attempted against non-transferable item");
3104             $self->push_events($evt);
3105             return 0;
3106         } elsif ($evt->{"textcode"} eq "SUCCESS") {
3107             # Re-retrieve copy as reservation capture may have changed
3108             # its status and whatnot.
3109             $logger->info(
3110                 "circulator: booking capture win on copy " . $self->copy->id
3111             );
3112             if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3113                 $logger->info(
3114                     "circulator: changing copy " . $self->copy->id .
3115                     "'s status from " . $self->copy->status . " to " .
3116                     $new_copy_status
3117                 );
3118                 $self->copy->status($new_copy_status);
3119                 $self->update_copy;
3120             }
3121             $self->reservation($evt->{"payload"}->{"reservation"});
3122
3123             if (exists $evt->{"payload"}->{"transit"}) {
3124                 $self->push_events(
3125                     new OpenILS::Event(
3126                         "ROUTE_ITEM",
3127                         "org" => $evt->{"payload"}->{"transit"}->dest
3128                     )
3129                 );
3130             }
3131             $self->checkin_changed(1);
3132             return 1;
3133         }
3134     }
3135     # other results are treated as "nothing to capture"
3136     return 0;
3137 }
3138
3139 sub do_hold_notify {
3140     my( $self, $holdid ) = @_;
3141
3142     my $e = new_editor(xact => 1);
3143     my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3144     $e->rollback;
3145     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3146     $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3147
3148     $logger->info("circulator: running delayed hold notify process");
3149
3150 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3151 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3152
3153     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3154         hold_id => $holdid, requestor => $self->editor->requestor);
3155
3156     $logger->debug("circulator: built hold notifier");
3157
3158     if(!$notifier->event) {
3159
3160         $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3161
3162         my $stat = $notifier->send_email_notify;
3163         if( $stat == '1' ) {
3164             $logger->info("circulator: hold notify succeeded for hold $holdid");
3165             return;
3166         } 
3167
3168         $logger->debug("circulator:  * hold notify cancelled or failed for hold $holdid");
3169
3170     } else {
3171         $logger->info("circulator: Not sending hold notification since the patron has no email address");
3172     }
3173 }
3174
3175 sub retarget_holds {
3176     my $self = shift;
3177     $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3178     my $ses = OpenSRF::AppSession->create('open-ils.storage');
3179     $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3180     # no reason to wait for the return value
3181     return;
3182 }
3183
3184 sub checkin_build_hold_transit {
3185     my $self = shift;
3186
3187    my $copy = $self->copy;
3188    my $hold = $self->hold;
3189    my $trans = Fieldmapper::action::hold_transit_copy->new;
3190
3191     $logger->debug("circulator: building hold transit for ".$copy->barcode);
3192
3193    $trans->hold($hold->id);
3194    $trans->source($self->circ_lib);
3195    $trans->dest($hold->pickup_lib);
3196    $trans->source_send_time("now");
3197    $trans->target_copy($copy->id);
3198
3199     # when the copy gets to its destination, it will recover
3200     # this status - put it onto the holds shelf
3201    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3202
3203     return $self->bail_on_events($self->editor->event)
3204         unless $self->editor->create_action_hold_transit_copy($trans);
3205 }
3206
3207
3208
3209 sub process_received_transit {
3210     my $self = shift;
3211     my $copy = $self->copy;
3212     my $copyid = $self->copy->id;
3213
3214     my $status_name = $U->copy_status($copy->status)->name;
3215     $logger->debug("circulator: attempting transit receive on ".
3216         "copy $copyid. Copy status is $status_name");
3217
3218     my $transit = $self->transit;
3219
3220     # Check if we are in a transit suppress range
3221     my $suppress_transit = 0;
3222     if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3223         my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ?  'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3224         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3225         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3226             my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3227             if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3228                 $suppress_transit = 1;
3229                 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3230             }
3231         }
3232     }
3233     if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3234         # - this item is in-transit to a different location
3235         # - Or we are capturing holds as transits, so why create a new transit?
3236
3237         my $tid = $transit->id; 
3238         my $loc = $self->circ_lib;
3239         my $dest = $transit->dest;
3240
3241         $logger->info("circulator: Fowarding transit on copy which is destined ".
3242             "for a different location. transit=$tid, copy=$copyid, current ".
3243             "location=$loc, destination location=$dest");
3244
3245         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3246
3247         # grab the associated hold object if available
3248         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3249         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3250
3251         return $self->bail_on_events($evt);
3252     }
3253
3254     # The transit is received, set the receive time
3255     $transit->dest_recv_time('now');
3256     $self->bail_on_events($self->editor->event)
3257         unless $self->editor->update_action_transit_copy($transit);
3258
3259     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3260
3261     $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3262     $copy->status( $transit->copy_status );
3263     $self->update_copy();
3264     return if $self->bail_out;
3265
3266     my $ishold = 0;
3267     if($hold_transit) { 
3268         my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3269
3270         # hold has arrived at destination, set shelf time
3271         $self->put_hold_on_shelf($hold);
3272         $self->bail_on_events($self->editor->event)
3273             unless $self->editor->update_action_hold_request($hold);
3274         return if $self->bail_out;
3275
3276         $self->notify_hold($hold_transit->hold);
3277         $ishold = 1;
3278     }
3279
3280     $self->push_events( 
3281         OpenILS::Event->new(
3282         'SUCCESS', 
3283         ishold => $ishold,
3284       payload => { transit => $transit, holdtransit => $hold_transit } ));
3285
3286     return $hold_transit;
3287 }
3288
3289
3290 # ------------------------------------------------------------------
3291 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3292 # ------------------------------------------------------------------
3293 sub put_hold_on_shelf {
3294     my($self, $hold) = @_;
3295     $hold->shelf_time('now');
3296     $hold->current_shelf_lib($self->circ_lib);
3297     $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3298     return undef;
3299 }
3300
3301
3302
3303 sub generate_fines {
3304    my $self = shift;
3305    my $reservation = shift;
3306
3307    $self->generate_fines_start($reservation);
3308    $self->generate_fines_finish($reservation);
3309
3310    return undef;
3311 }
3312
3313 sub generate_fines_start {
3314    my $self = shift;
3315    my $reservation = shift;
3316    my $dt_parser = DateTime::Format::ISO8601->new;
3317
3318    my $obj = $reservation ? $self->reservation : $self->circ;
3319
3320    # If we have a grace period
3321    if($obj->can('grace_period')) {
3322       # Parse out the due date
3323       my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3324       # Add the grace period to the due date
3325       $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3326       # Don't generate fines on circs still in grace period
3327       return undef if ($due_date > DateTime->now);
3328    }
3329
3330    if (!exists($self->{_gen_fines_req})) {
3331       $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage') 
3332           ->request(
3333              'open-ils.storage.action.circulation.overdue.generate_fines',
3334              $obj->id
3335           );
3336    }
3337
3338    return undef;
3339 }
3340
3341 sub generate_fines_finish {
3342    my $self = shift;
3343    my $reservation = shift;
3344
3345    return undef unless $self->{_gen_fines_req};
3346
3347    my $id = $reservation ? $self->reservation->id : $self->circ->id;
3348
3349    $self->{_gen_fines_req}->wait_complete;
3350    delete($self->{_gen_fines_req});
3351
3352    # refresh the circ in case the fine generator set the stop_fines field
3353    $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3354    $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3355
3356    return undef;
3357 }
3358
3359 sub checkin_handle_circ {
3360    my $self = shift;
3361    my $circ = $self->circ;
3362    my $copy = $self->copy;
3363    my $evt;
3364    my $obt;
3365
3366    $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3367
3368    # backdate the circ if necessary
3369    if($self->backdate) {
3370         my $evt = $self->checkin_handle_backdate;
3371         return $self->bail_on_events($evt) if $evt;
3372    }
3373
3374    if(!$circ->stop_fines) {
3375       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3376       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3377       $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3378       $circ->stop_fines_time('now');
3379       $circ->stop_fines_time($self->backdate) if $self->backdate;
3380    }
3381
3382     # Set the checkin vars since we have the item
3383     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3384
3385     # capture the true scan time for back-dated checkins
3386     $circ->checkin_scan_time('now');
3387
3388     $circ->checkin_staff($self->editor->requestor->id);
3389     $circ->checkin_lib($self->circ_lib);
3390     $circ->checkin_workstation($self->editor->requestor->wsid);
3391
3392     my $circ_lib = (ref $self->copy->circ_lib) ?  
3393         $self->copy->circ_lib->id : $self->copy->circ_lib;
3394     my $stat = $U->copy_status($self->copy->status)->id;
3395
3396     if ($stat == OILS_COPY_STATUS_LOST) {
3397         # we will now handle lost fines, but the copy will retain its 'lost'
3398         # status if it needs to transit home unless lost_immediately_available
3399         # is true
3400         #
3401         # if we decide to also delay fine handling until the item arrives home,
3402         # we will need to call lost fine handling code both when checking items
3403         # in and also when receiving transits
3404         $self->checkin_handle_lost($circ_lib);
3405     } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3406         $logger->info("circulator: not updating copy status on checkin because copy is missing");
3407     } else {
3408         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3409         $self->update_copy;
3410     }
3411
3412
3413     # see if there are any fines owed on this circ.  if not, close it
3414     ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3415     $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3416
3417     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3418
3419     return $self->bail_on_events($self->editor->event)
3420         unless $self->editor->update_action_circulation($circ);
3421
3422     return undef;
3423 }
3424
3425
3426 # ------------------------------------------------------------------
3427 # See if we need to void billings for lost checkin
3428 # ------------------------------------------------------------------
3429 sub checkin_handle_lost {
3430     my $self = shift;
3431     my $circ_lib = shift;
3432     my $circ = $self->circ;
3433
3434     my $max_return = $U->ou_ancestor_setting_value(
3435         $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3436
3437     if ($max_return) {
3438
3439         my $today = time();
3440         my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3441         $tm[5] -= 1 if $tm[5] > 0;
3442         my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3443
3444         my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3445         $logger->info("MAX OD: ".$max_return."  DUEDATE: ".$circ->due_date."  TODAY: ".$today."  DUE: ".$due."  LAST: ".$last_chance);
3446
3447         $max_return = 0 if $today < $last_chance;
3448     }
3449
3450     if (!$max_return){  # there's either no max time to accept returns defined or we're within that time
3451
3452         my $void_lost = $U->ou_ancestor_setting_value(
3453             $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3454         my $void_lost_fee = $U->ou_ancestor_setting_value(
3455             $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3456         my $restore_od = $U->ou_ancestor_setting_value(
3457             $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3458         $self->generate_lost_overdue(1) if $U->ou_ancestor_setting_value(
3459             $circ_lib, OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, $self->editor);
3460
3461         $self->checkin_handle_lost_now_found(3) if $void_lost;
3462         $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3463         $self->checkin_handle_lost_now_found_restore_od($circ_lib) if $restore_od && ! $self->void_overdues;
3464     }
3465
3466     if ($circ_lib != $self->circ_lib) {
3467         # if the item is not home, check to see if we want to retain the lost
3468         # status at this point in the process
3469         my $immediately_available = $U->ou_ancestor_setting_value($circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3470
3471         if ($immediately_available) {
3472             # lost item status does not need to be retained, so give it a
3473             # reshelving status as if it were a normal checkin
3474             $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3475             $self->update_copy;
3476         } else {
3477             $logger->info("circulator: not updating copy status on checkin because copy is lost");
3478         }
3479     } else {
3480         # lost item is home and processed, treat like a normal checkin from
3481         # this point on
3482         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3483         $self->update_copy;
3484     }
3485 }
3486
3487
3488 sub checkin_handle_backdate {
3489     my $self = shift;
3490
3491     # ------------------------------------------------------------------
3492     # clean up the backdate for date comparison
3493     # XXX We are currently taking the due-time from the original due-date,
3494     # not the input.  Do we need to do this?  This certainly interferes with
3495     # backdating of hourly checkouts, but that is likely a very rare case.
3496     # ------------------------------------------------------------------
3497     my $bd = cleanse_ISO8601($self->backdate);
3498     my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3499     my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3500     $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3501
3502     $self->backdate($bd);
3503     return undef;
3504 }
3505
3506
3507 sub check_checkin_copy_status {
3508     my $self = shift;
3509    my $copy = $self->copy;
3510
3511    my $status = $U->copy_status($copy->status)->id;
3512
3513    return undef
3514       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
3515             $status == OILS_COPY_STATUS_CHECKED_OUT ||
3516             $status == OILS_COPY_STATUS_IN_PROCESS  ||
3517             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
3518             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
3519             $status == OILS_COPY_STATUS_CATALOGING  ||
3520             $status == OILS_COPY_STATUS_ON_RESV_SHELF  ||
3521             $status == OILS_COPY_STATUS_RESHELVING );
3522
3523    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3524       if( $status == OILS_COPY_STATUS_LOST );
3525
3526    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3527       if( $status == OILS_COPY_STATUS_MISSING );
3528
3529    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3530 }
3531
3532
3533
3534 # --------------------------------------------------------------------------
3535 # On checkin, we need to return as many relevant objects as we can
3536 # --------------------------------------------------------------------------
3537 sub checkin_flesh_events {
3538     my $self = shift;
3539
3540     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
3541         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3542             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3543     }
3544
3545     my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3546
3547     my $hold;
3548     if($self->hold and !$self->hold->cancel_time) {
3549         $hold = $self->hold;
3550         $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3551     }
3552
3553     if($self->circ) {
3554         # if we checked in a circulation, flesh the billing summary data
3555         $self->circ->billable_transaction(
3556             $self->editor->retrieve_money_billable_transaction([
3557                 $self->circ->id,
3558                 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3559             ])
3560         );
3561     }
3562
3563     if($self->patron) {
3564         # flesh some patron fields before returning
3565         $self->patron(
3566             $self->editor->retrieve_actor_user([
3567                 $self->patron->id,
3568                 {
3569                     flesh => 1,
3570                     flesh_fields => {
3571                         au => ['card', 'billing_address', 'mailing_address']
3572                     }
3573                 }
3574             ])
3575         );
3576     }
3577
3578     for my $evt (@{$self->events}) {
3579
3580         my $payload         = {};
3581         $payload->{copy}    = $U->unflesh_copy($self->copy);
3582         $payload->{volume}  = $self->volume;
3583         $payload->{record}  = $record,
3584         $payload->{circ}    = $self->circ;
3585         $payload->{transit} = $self->transit;
3586         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3587         $payload->{hold}    = $hold;
3588         $payload->{patron}  = $self->patron;
3589         $payload->{reservation} = $self->reservation
3590             unless (not $self->reservation or $self->reservation->cancel_time);
3591
3592         $evt->{payload}     = $payload;
3593     }
3594 }
3595
3596 sub log_me {
3597     my( $self, $msg ) = @_;
3598     my $bc = ($self->copy) ? $self->copy->barcode :
3599         $self->barcode;
3600     $bc ||= "";
3601     my $usr = ($self->patron) ? $self->patron->id : "";
3602     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3603         ", recipient=$usr, copy=$bc");
3604 }
3605
3606
3607 sub do_renew {
3608     my $self = shift;
3609     $self->log_me("do_renew()");
3610
3611     # Make sure there is an open circ to renew that is not
3612     # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3613     my $usrid = $self->patron->id if $self->patron;
3614     my $circ = $self->editor->search_action_circulation({
3615         target_copy => $self->copy->id,
3616         xact_finish => undef,
3617         checkin_time => undef,
3618         ($usrid ? (usr => $usrid) : ()),
3619         '-or' => [
3620             {stop_fines => undef},
3621             {stop_fines => OILS_STOP_FINES_MAX_FINES}
3622         ]
3623     })->[0];
3624
3625     return $self->bail_on_events($self->editor->event) unless $circ;
3626
3627     # A user is not allowed to renew another user's items without permission
3628     unless( $circ->usr eq $self->editor->requestor->id ) {
3629         return $self->bail_on_events($self->editor->events)
3630             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3631     }   
3632
3633     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3634         if $circ->renewal_remaining < 1;
3635
3636     # -----------------------------------------------------------------
3637
3638     $self->parent_circ($circ->id);
3639     $self->renewal_remaining( $circ->renewal_remaining - 1 );
3640     $self->circ($circ);
3641
3642     # Opac renewal - re-use circ library from original circ (unless told not to)
3643     if($self->opac_renewal) {
3644         unless(defined($opac_renewal_use_circ_lib)) {
3645             my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3646             if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3647                 $opac_renewal_use_circ_lib = 1;
3648             }
3649             else {
3650                 $opac_renewal_use_circ_lib = 0;
3651             }
3652         }
3653         $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3654     }
3655
3656     # Run the fine generator against the old circ
3657     $self->generate_fines_start;
3658
3659     $self->run_renew_permit;
3660
3661     # Check the item in
3662     $self->do_checkin();
3663     return if $self->bail_out;
3664
3665     unless( $self->permit_override ) {
3666         $self->do_permit();
3667         return if $self->bail_out;
3668         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3669         $self->remove_event('ITEM_NOT_CATALOGED');
3670     }   
3671
3672     $self->override_events;
3673     return if $self->bail_out;
3674
3675     $self->events([]);
3676     $self->do_checkout();
3677 }
3678
3679
3680 sub remove_event {
3681     my( $self, $evt ) = @_;
3682     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3683     $logger->debug("circulator: removing event from list: $evt");
3684     my @events = @{$self->events};
3685     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3686 }
3687
3688
3689 sub have_event {
3690     my( $self, $evt ) = @_;
3691     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3692     return grep { $_->{textcode} eq $evt } @{$self->events};
3693 }
3694
3695
3696
3697 sub run_renew_permit {
3698     my $self = shift;
3699
3700     if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3701         my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3702             $self->editor, $self->copy, $self->editor->requestor, 1
3703         );
3704         $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3705     }
3706
3707     if(!$self->legacy_script_support) {
3708         my $results = $self->run_indb_circ_test;
3709         $self->push_events($self->matrix_test_result_events)
3710             unless $self->circ_test_success;
3711     } else {
3712
3713         my $runner = $self->script_runner;
3714
3715         $runner->load($self->circ_permit_renew);
3716         my $result = $runner->run or 
3717             throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3718         if ($result->{"events"}) {
3719             $self->push_events(
3720                 map { new OpenILS::Event($_) } @{$result->{"events"}}
3721             );
3722             $logger->activity(
3723                 "circulator: circ_permit_renew for user " .
3724                 $self->patron->id . " returned " .
3725                 scalar(@{$result->{"events"}}) . " event(s)"
3726             );
3727         }
3728
3729         $self->mk_script_runner;
3730     }
3731
3732     $logger->debug("circulator: re-creating script runner to be safe");
3733 }
3734
3735
3736 # XXX: The primary mechanism for storing circ history is now handled
3737 # by tracking real circulation objects instead of bibs in a bucket.
3738 # However, this code is disabled by default and could be useful 
3739 # some day, so may as well leave it for now.
3740 sub append_reading_list {
3741     my $self = shift;
3742
3743     return undef unless 
3744         $self->is_checkout and 
3745         $self->patron and 
3746         $self->copy and 
3747         !$self->is_noncat;
3748
3749
3750     # verify history is globally enabled and uses the bucket mechanism
3751     my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3752         apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3753
3754     return undef unless $htype and $htype eq 'bucket';
3755
3756     my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3757
3758     # verify the patron wants to retain the hisory
3759         my $setting = $e->search_actor_user_setting(
3760                 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3761     
3762     unless($setting and $setting->value) {
3763         $e->rollback;
3764         return undef;
3765     }
3766
3767     my $bkt = $e->search_container_copy_bucket(
3768         {owner => $self->patron->id, btype => 'circ_history'})->[0];
3769
3770     my $pos = 1;
3771
3772     if($bkt) {
3773         # find the next item position
3774         my $last_item = $e->search_container_copy_bucket_item(
3775             {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3776         $pos = $last_item->pos + 1 if $last_item;
3777
3778     } else {
3779         # create the history bucket if necessary
3780         $bkt = Fieldmapper::container::copy_bucket->new;
3781         $bkt->owner($self->patron->id);
3782         $bkt->name('');
3783         $bkt->btype('circ_history');
3784         $bkt->pub('f');
3785         $e->create_container_copy_bucket($bkt) or return $e->die_event;
3786     }
3787
3788     my $item = Fieldmapper::container::copy_bucket_item->new;
3789
3790     $item->bucket($bkt->id);
3791     $item->target_copy($self->copy->id);
3792     $item->pos($pos);
3793
3794     $e->create_container_copy_bucket_item($item) or return $e->die_event;
3795     $e->commit;
3796
3797     return undef;
3798 }
3799
3800
3801 sub make_trigger_events {
3802     my $self = shift;
3803     return unless $self->circ;
3804     $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3805     $U->create_events_for_hook('checkin',  $self->circ, $self->circ_lib) if $self->is_checkin;
3806     $U->create_events_for_hook('renewal',  $self->circ, $self->circ_lib) if $self->is_renewal;
3807 }
3808
3809
3810
3811 sub checkin_handle_lost_now_found {
3812     my ($self, $bill_type) = @_;
3813
3814     # ------------------------------------------------------------------
3815     # remove charge from patron's account if lost item is returned
3816     # ------------------------------------------------------------------
3817
3818     my $bills = $self->editor->search_money_billing(
3819         {
3820             xact => $self->circ->id,
3821             btype => $bill_type
3822         }
3823     );
3824
3825     $logger->debug("voiding lost item charge of  ".scalar(@$bills));
3826     for my $bill (@$bills) {
3827         if( !$U->is_true($bill->voided) ) {
3828             $logger->info("lost item returned - voiding bill ".$bill->id);
3829             $bill->voided('t');
3830             $bill->void_time('now');
3831             $bill->voider($self->editor->requestor->id);
3832             my $note = ($bill->note) ? $bill->note . "\n" : '';
3833             $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3834
3835             $self->bail_on_events($self->editor->event)
3836                 unless $self->editor->update_money_billing($bill);
3837         }
3838     }
3839 }
3840
3841 sub checkin_handle_lost_now_found_restore_od {
3842     my $self = shift;
3843     my $circ_lib = shift;
3844
3845     # ------------------------------------------------------------------
3846     # restore those overdue charges voided when item was set to lost
3847     # ------------------------------------------------------------------
3848
3849     my $ods = $self->editor->search_money_billing(
3850         {
3851                 xact => $self->circ->id,
3852                 btype => 1
3853         }
3854     );
3855
3856     $logger->debug("returning overdue charges pre-lost  ".scalar(@$ods));
3857     for my $bill (@$ods) {
3858         if( $U->is_true($bill->voided) ) {
3859                 $logger->info("lost item returned - restoring overdue ".$bill->id);
3860                 $bill->voided('f');
3861                 $bill->clear_void_time;
3862                 $bill->voider($self->editor->requestor->id);
3863                 my $note = ($bill->note) ? $bill->note . "\n" : '';
3864                 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3865
3866                 $self->bail_on_events($self->editor->event)
3867                         unless $self->editor->update_money_billing($bill);
3868         }
3869     }
3870 }
3871
3872 # ------------------------------------------------------------------
3873 # Lost-then-found item checked in.  This sub generates new overdue
3874 # fines, beyond the point of any existing and possibly voided 
3875 # overdue fines, up to the point of final checkin time (or max fine
3876 # amount).  
3877 # ------------------------------------------------------------------
3878 sub generate_lost_overdue_fines {
3879     my $self = shift;
3880     my $circ = $self->circ;
3881     my $e = $self->editor;
3882
3883     # Re-open the transaction so the fine generator can see it
3884     if($circ->xact_finish or $circ->stop_fines) {
3885         $e->xact_begin;
3886         $circ->clear_xact_finish;
3887         $circ->clear_stop_fines;
3888         $circ->clear_stop_fines_time;
3889         $e->update_action_circulation($circ) or return $e->die_event;
3890         $e->xact_commit;
3891     }
3892
3893     $e->xact_begin; # generate_fines expects an in-xact editor
3894     $self->generate_fines;
3895     $circ = $self->circ; # generate fines re-fetches the circ
3896     
3897     my $update = 0;
3898
3899     # Re-close the transaction if no money is owed
3900     my ($obt) = $U->fetch_mbts($circ->id, $e);
3901     if ($obt and $obt->balance_owed == 0) {
3902         $circ->xact_finish('now');
3903         $update = 1;
3904     }
3905
3906     # Set stop fines if the fine generator didn't have to
3907     unless($circ->stop_fines) {
3908         $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3909         $circ->stop_fines_time('now');
3910         $update = 1;
3911     }
3912
3913     # update the event data sent to the caller within the transaction
3914     $self->checkin_flesh_events;
3915
3916     if ($update) {
3917         $e->update_action_circulation($circ) or return $e->die_event;
3918         $e->commit;
3919     } else {
3920         $e->rollback;
3921     }
3922
3923     return undef;
3924 }
3925
3926 1;