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