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