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