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