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