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