95227e982961d719ea0a37fbca76e7947f1ba394
[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
2210 sub do_checkin {
2211     my $self = shift;
2212     $self->log_me("do_checkin()");
2213
2214     return $self->bail_on_events(
2215         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
2216         unless $self->copy;
2217
2218     # the renew code and mk_env should have already found our circulation object
2219     unless( $self->circ ) {
2220
2221         my $circs = $self->editor->search_action_circulation(
2222             { target_copy => $self->copy->id, checkin_time => undef });
2223
2224         $self->circ($$circs[0]);
2225
2226         # for now, just warn if there are multiple open circs on a copy
2227         $logger->warn("circulator: we have ".scalar(@$circs).
2228             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2229     }
2230
2231     # run the fine generator against this circ, if this circ is there
2232     $self->generate_fines_start if $self->circ;
2233
2234     if( $self->checkin_check_holds_shelf() ) {
2235         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2236         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2237         $self->checkin_flesh_events;
2238         return;
2239     }
2240
2241     unless( $self->is_renewal ) {
2242         return $self->bail_on_events($self->editor->event)
2243             unless $self->editor->allowed('COPY_CHECKIN');
2244     }
2245
2246     $self->push_events($self->check_copy_alert());
2247     $self->push_events($self->check_checkin_copy_status());
2248
2249     # if the circ is marked as 'claims returned', add the event to the list
2250     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2251         if ($self->circ and $self->circ->stop_fines 
2252                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2253
2254     $self->check_circ_deposit();
2255
2256     # handle the overridable events 
2257     $self->override_events unless $self->is_renewal;
2258     return if $self->bail_out;
2259     
2260     if( $self->copy ) {
2261         $self->transit(
2262             $self->editor->search_action_transit_copy(
2263                 { target_copy => $self->copy->id, dest_recv_time => undef }
2264             )->[0]
2265         ); 
2266     }
2267
2268     if( $self->circ ) {
2269         $self->checkin_handle_circ;
2270         return if $self->bail_out;
2271         $self->checkin_changed(1);
2272
2273     } elsif( $self->transit ) {
2274         my $hold_transit = $self->process_received_transit;
2275         $self->checkin_changed(1);
2276
2277         if( $self->bail_out ) { 
2278             $self->checkin_flesh_events;
2279             return;
2280         }
2281         
2282         if( my $e = $self->check_checkin_copy_status() ) {
2283             # If the original copy status is special, alert the caller
2284             my $ev = $self->events;
2285             $self->events([$e]);
2286             $self->override_events;
2287             return if $self->bail_out;
2288             $self->events($ev);
2289         }
2290
2291         if( $hold_transit or 
2292                 $U->copy_status($self->copy->status)->id 
2293                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2294
2295             my $hold;
2296             if( $hold_transit ) {
2297                $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2298             } else {
2299                    ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2300             }
2301
2302             $self->hold($hold);
2303
2304             if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2305
2306                 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2307                 $self->reshelve_copy(1);
2308                 $self->cancelled_hold_transit(1);
2309                 $self->notify_hold(0); # don't notify for cancelled holds
2310                 return if $self->bail_out;
2311
2312             } else {
2313
2314                 # hold transited to correct location
2315                 $self->checkin_flesh_events;
2316                 return;
2317             }
2318         } 
2319
2320     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2321
2322         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2323             " that is in-transit, but there is no transit.. repairing");
2324         $self->reshelve_copy(1);
2325         return if $self->bail_out;
2326     }
2327
2328     if( $self->is_renewal ) {
2329         $self->finish_fines_and_voiding;
2330         return if $self->bail_out;
2331         $self->push_events(OpenILS::Event->new('SUCCESS'));
2332         return;
2333     }
2334
2335    # ------------------------------------------------------------------------------
2336    # Circulations and transits are now closed where necessary.  Now go on to see if
2337    # this copy can fulfill a hold or needs to be routed to a different location
2338    # ------------------------------------------------------------------------------
2339
2340     my $needed_for_something = 0; # formerly "needed_for_hold"
2341
2342     if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2343
2344         if (!$self->remote_hold) {
2345             if ($self->use_booking) {
2346                 my $potential_hold = $self->hold_capture_is_possible;
2347                 my $potential_reservation = $self->reservation_capture_is_possible;
2348
2349                 if ($potential_hold and $potential_reservation) {
2350                     $logger->info("circulator: item could fulfill either hold or reservation");
2351                     $self->push_events(new OpenILS::Event(
2352                         "HOLD_RESERVATION_CONFLICT",
2353                         "hold" => $potential_hold,
2354                         "reservation" => $potential_reservation
2355                     ));
2356                     return if $self->bail_out;
2357                 } elsif ($potential_hold) {
2358                     $needed_for_something =
2359                         $self->attempt_checkin_hold_capture;
2360                 } elsif ($potential_reservation) {
2361                     $needed_for_something =
2362                         $self->attempt_checkin_reservation_capture;
2363                 }
2364             } else {
2365                 $needed_for_something = $self->attempt_checkin_hold_capture;
2366             }
2367         }
2368         return if $self->bail_out;
2369     
2370         unless($needed_for_something) {
2371             my $circ_lib = (ref $self->copy->circ_lib) ? 
2372                     $self->copy->circ_lib->id : $self->copy->circ_lib;
2373     
2374             if( $self->remote_hold ) {
2375                 $circ_lib = $self->remote_hold->pickup_lib;
2376                 $logger->warn("circulator: Copy ".$self->copy->barcode.
2377                     " is on a remote hold's shelf, sending to $circ_lib");
2378             }
2379     
2380             $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2381     
2382             if( $circ_lib == $self->circ_lib) {
2383                 # copy is where it needs to be, either for hold or reshelving
2384     
2385                 $self->checkin_handle_precat();
2386                 return if $self->bail_out;
2387     
2388             } else {
2389                 # copy needs to transit "home", or stick here if it's a floating copy
2390     
2391                 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2392                     $self->checkin_changed(1);
2393                     $self->copy->circ_lib( $self->circ_lib );
2394                     $self->update_copy;
2395                 } else {
2396                     my $bc = $self->copy->barcode;
2397                     $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2398                     $self->checkin_build_copy_transit($circ_lib);
2399                     return if $self->bail_out;
2400                     $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2401                 }
2402             }
2403         }
2404     } else { # no-op checkin
2405         if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2406             $self->checkin_changed(1);
2407             $self->copy->circ_lib( $self->circ_lib );
2408             $self->update_copy;
2409         }
2410     }
2411
2412     if($self->claims_never_checked_out and 
2413             $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2414
2415         # the item was not supposed to be checked out to the user and should now be marked as missing
2416         $self->copy->status(OILS_COPY_STATUS_MISSING);
2417         $self->update_copy;
2418
2419     } else {
2420         $self->reshelve_copy unless $needed_for_something;
2421     }
2422
2423     return if $self->bail_out;
2424
2425     unless($self->checkin_changed) {
2426
2427         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2428         my $stat = $U->copy_status($self->copy->status)->id;
2429
2430         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2431          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2432         $self->bail_out(1); # no need to commit anything
2433
2434     } else {
2435
2436         $self->push_events(OpenILS::Event->new('SUCCESS')) 
2437             unless @{$self->events};
2438     }
2439
2440     $self->finish_fines_and_voiding;
2441
2442     OpenILS::Utils::Penalty->calculate_penalties(
2443         $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2444
2445     $self->checkin_flesh_events;
2446     return;
2447 }
2448
2449 sub finish_fines_and_voiding {
2450     my $self = shift;
2451     return unless $self->circ;
2452
2453     # gather any updates to the circ after fine generation, if there was a circ
2454     $self->generate_fines_finish;
2455
2456     return unless $self->backdate or $self->void_overdues;
2457
2458     # void overdues after fine generation to prevent concurrent DB access to overdue billings
2459     my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2460
2461     my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2462         $self->editor, $self->circ, $self->backdate, $note);
2463
2464     return $self->bail_on_events($evt) if $evt;
2465
2466     # make sure the circ isn't closed if we just voided some fines
2467     $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2468     return $self->bail_on_events($evt) if $evt;
2469
2470     return undef;
2471 }
2472
2473
2474 # if a deposit was payed for this item, push the event
2475 sub check_circ_deposit {
2476     my $self = shift;
2477     return unless $self->circ;
2478     my $deposit = $self->editor->search_money_billing(
2479         {   btype => 5, 
2480             xact => $self->circ->id, 
2481             voided => 'f'
2482         }, {idlist => 1})->[0];
2483
2484     $self->push_events(OpenILS::Event->new(
2485         'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2486 }
2487
2488 sub reshelve_copy {
2489    my $self    = shift;
2490    my $force   = $self->force || shift;
2491    my $copy    = $self->copy;
2492
2493    my $stat = $U->copy_status($copy->status)->id;
2494
2495    if($force || (
2496       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2497       $stat != OILS_COPY_STATUS_CATALOGING and
2498       $stat != OILS_COPY_STATUS_IN_TRANSIT and
2499       $stat != OILS_COPY_STATUS_RESHELVING  )) {
2500
2501         $copy->status( OILS_COPY_STATUS_RESHELVING );
2502             $self->update_copy;
2503             $self->checkin_changed(1);
2504     }
2505 }
2506
2507
2508 # Returns true if the item is at the current location
2509 # because it was transited there for a hold and the 
2510 # hold has not been fulfilled
2511 sub checkin_check_holds_shelf {
2512     my $self = shift;
2513     return 0 unless $self->copy;
2514
2515     return 0 unless 
2516         $U->copy_status($self->copy->status)->id ==
2517             OILS_COPY_STATUS_ON_HOLDS_SHELF;
2518
2519     # find the hold that put us on the holds shelf
2520     my $holds = $self->editor->search_action_hold_request(
2521         { 
2522             current_copy => $self->copy->id,
2523             capture_time => { '!=' => undef },
2524             fulfillment_time => undef,
2525             cancel_time => undef,
2526         }
2527     );
2528
2529     unless(@$holds) {
2530         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2531         $self->reshelve_copy(1);
2532         return 0;
2533     }
2534
2535     my $hold = $$holds[0];
2536
2537     $logger->info("circulator: we found a captured, un-fulfilled hold [".
2538         $hold->id. "] for copy ".$self->copy->barcode);
2539
2540     if( $hold->pickup_lib == $self->circ_lib ) {
2541         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2542         return 1;
2543     }
2544
2545     $logger->info("circulator: hold is not for here..");
2546     $self->remote_hold($hold);
2547     return 0;
2548 }
2549
2550
2551 sub checkin_handle_precat {
2552     my $self    = shift;
2553    my $copy    = $self->copy;
2554
2555    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2556         $copy->status(OILS_COPY_STATUS_CATALOGING);
2557         $self->update_copy();
2558         $self->checkin_changed(1);
2559         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2560    }
2561 }
2562
2563
2564 sub checkin_build_copy_transit {
2565     my $self            = shift;
2566     my $dest            = shift;
2567     my $copy       = $self->copy;
2568    my $transit    = Fieldmapper::action::transit_copy->new;
2569
2570     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2571     $logger->info("circulator: transiting copy to $dest");
2572
2573    $transit->source($self->circ_lib);
2574    $transit->dest($dest);
2575    $transit->target_copy($copy->id);
2576    $transit->source_send_time('now');
2577    $transit->copy_status( $U->copy_status($copy->status)->id );
2578
2579     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2580
2581     return $self->bail_on_events($self->editor->event)
2582         unless $self->editor->create_action_transit_copy($transit);
2583
2584    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2585     $self->update_copy;
2586     $self->checkin_changed(1);
2587 }
2588
2589
2590 sub hold_capture_is_possible {
2591     my $self = shift;
2592     my $copy = $self->copy;
2593
2594     # we've been explicitly told not to capture any holds
2595     return 0 if $self->capture eq 'nocapture';
2596
2597     # See if this copy can fulfill any holds
2598     my $hold = $holdcode->find_nearest_permitted_hold(
2599         $self->editor, $copy, $self->editor->requestor, 1 # check_only
2600     );
2601     return undef if ref $hold eq "HASH" and
2602         $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2603     return $hold;
2604 }
2605
2606 sub reservation_capture_is_possible {
2607     my $self = shift;
2608     my $copy = $self->copy;
2609
2610     # we've been explicitly told not to capture any holds
2611     return 0 if $self->capture eq 'nocapture';
2612
2613     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2614     my $resv = $booking_ses->request(
2615         "open-ils.booking.reservations.could_capture",
2616         $self->editor->authtoken, $copy->barcode
2617     )->gather(1);
2618     $booking_ses->disconnect;
2619     if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2620         $self->push_events($resv);
2621     } else {
2622         return $resv;
2623     }
2624 }
2625
2626 # returns true if the item was used (or may potentially be used 
2627 # in subsequent calls) to capture a hold.
2628 sub attempt_checkin_hold_capture {
2629     my $self = shift;
2630     my $copy = $self->copy;
2631
2632     # we've been explicitly told not to capture any holds
2633     return 0 if $self->capture eq 'nocapture';
2634
2635     # See if this copy can fulfill any holds
2636     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
2637         $self->editor, $copy, $self->editor->requestor );
2638
2639     if(!$hold) {
2640         $logger->debug("circulator: no potential permitted".
2641             "holds found for copy ".$copy->barcode);
2642         return 0;
2643     }
2644
2645     if($self->capture ne 'capture') {
2646         # see if this item is in a hold-capture-delay location
2647         my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2648         if($U->is_true($location->hold_verify)) {
2649             $self->bail_on_events(
2650                 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2651             return 1;
2652         }
2653     }
2654
2655     $self->retarget($retarget);
2656
2657     $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2658
2659     $hold->current_copy($copy->id);
2660     $hold->capture_time('now');
2661     $self->put_hold_on_shelf($hold) 
2662         if $hold->pickup_lib == $self->circ_lib;
2663
2664     # prevent DB errors caused by fetching 
2665     # holds from storage, and updating through cstore
2666     $hold->clear_fulfillment_time;
2667     $hold->clear_fulfillment_staff;
2668     $hold->clear_fulfillment_lib;
2669     $hold->clear_expire_time; 
2670     $hold->clear_cancel_time;
2671     $hold->clear_prev_check_time unless $hold->prev_check_time;
2672
2673     $self->bail_on_events($self->editor->event)
2674         unless $self->editor->update_action_hold_request($hold);
2675     $self->hold($hold);
2676     $self->checkin_changed(1);
2677
2678     return 0 if $self->bail_out;
2679
2680     if( $hold->pickup_lib == $self->circ_lib ) {
2681
2682         # This hold was captured in the correct location
2683         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2684         $self->push_events(OpenILS::Event->new('SUCCESS'));
2685
2686         #$self->do_hold_notify($hold->id);
2687         $self->notify_hold($hold->id);
2688
2689     } else {
2690     
2691         # Hold needs to be picked up elsewhere.  Build a hold
2692         # transit and route the item.
2693         $self->checkin_build_hold_transit();
2694         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2695         return 0 if $self->bail_out;
2696         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2697     }
2698
2699     # make sure we save the copy status
2700     $self->update_copy;
2701     return 1;
2702 }
2703
2704 sub attempt_checkin_reservation_capture {
2705     my $self = shift;
2706     my $copy = $self->copy;
2707
2708     # we've been explicitly told not to capture any holds
2709     return 0 if $self->capture eq 'nocapture';
2710
2711     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2712     my $evt = $booking_ses->request(
2713         "open-ils.booking.resources.capture_for_reservation",
2714         $self->editor->authtoken,
2715         $copy->barcode,
2716         1 # don't update copy - we probably have it locked
2717     )->gather(1);
2718     $booking_ses->disconnect;
2719
2720     if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2721         $logger->warn(
2722             "open-ils.booking.resources.capture_for_reservation " .
2723             "didn't return an event!"
2724         );
2725     } else {
2726         if (
2727             $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2728             $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2729         ) {
2730             # not-transferable is an error event we'll pass on the user
2731             $logger->warn("reservation capture attempted against non-transferable item");
2732             $self->push_events($evt);
2733             return 0;
2734         } elsif ($evt->{"textcode"} eq "SUCCESS") {
2735             # Re-retrieve copy as reservation capture may have changed
2736             # its status and whatnot.
2737             $logger->info(
2738                 "circulator: booking capture win on copy " . $self->copy->id
2739             );
2740             if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2741                 $logger->info(
2742                     "circulator: changing copy " . $self->copy->id .
2743                     "'s status from " . $self->copy->status . " to " .
2744                     $new_copy_status
2745                 );
2746                 $self->copy->status($new_copy_status);
2747                 $self->update_copy;
2748             }
2749             $self->reservation($evt->{"payload"}->{"reservation"});
2750
2751             if (exists $evt->{"payload"}->{"transit"}) {
2752                 $self->push_events(
2753                     new OpenILS::Event(
2754                         "ROUTE_ITEM",
2755                         "org" => $evt->{"payload"}->{"transit"}->dest
2756                     )
2757                 );
2758             }
2759             $self->checkin_changed(1);
2760             return 1;
2761         }
2762     }
2763     # other results are treated as "nothing to capture"
2764     return 0;
2765 }
2766
2767 sub do_hold_notify {
2768     my( $self, $holdid ) = @_;
2769
2770     my $e = new_editor(xact => 1);
2771     my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2772     $e->rollback;
2773     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2774     $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2775
2776     $logger->info("circulator: running delayed hold notify process");
2777
2778 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2779 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2780
2781     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2782         hold_id => $holdid, requestor => $self->editor->requestor);
2783
2784     $logger->debug("circulator: built hold notifier");
2785
2786     if(!$notifier->event) {
2787
2788         $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2789
2790         my $stat = $notifier->send_email_notify;
2791         if( $stat == '1' ) {
2792             $logger->info("circulator: hold notify succeeded for hold $holdid");
2793             return;
2794         } 
2795
2796         $logger->debug("circulator:  * hold notify cancelled or failed for hold $holdid");
2797
2798     } else {
2799         $logger->info("circulator: Not sending hold notification since the patron has no email address");
2800     }
2801 }
2802
2803 sub retarget_holds {
2804     my $self = shift;
2805     $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2806     my $ses = OpenSRF::AppSession->create('open-ils.storage');
2807     $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2808     # no reason to wait for the return value
2809     return;
2810 }
2811
2812 sub checkin_build_hold_transit {
2813     my $self = shift;
2814
2815    my $copy = $self->copy;
2816    my $hold = $self->hold;
2817    my $trans = Fieldmapper::action::hold_transit_copy->new;
2818
2819     $logger->debug("circulator: building hold transit for ".$copy->barcode);
2820
2821    $trans->hold($hold->id);
2822    $trans->source($self->circ_lib);
2823    $trans->dest($hold->pickup_lib);
2824    $trans->source_send_time("now");
2825    $trans->target_copy($copy->id);
2826
2827     # when the copy gets to its destination, it will recover
2828     # this status - put it onto the holds shelf
2829    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2830
2831     return $self->bail_on_events($self->editor->event)
2832         unless $self->editor->create_action_hold_transit_copy($trans);
2833 }
2834
2835
2836
2837 sub process_received_transit {
2838     my $self = shift;
2839     my $copy = $self->copy;
2840     my $copyid = $self->copy->id;
2841
2842     my $status_name = $U->copy_status($copy->status)->name;
2843     $logger->debug("circulator: attempting transit receive on ".
2844         "copy $copyid. Copy status is $status_name");
2845
2846     my $transit = $self->transit;
2847
2848     if( $transit->dest != $self->circ_lib ) {
2849         # - this item is in-transit to a different location
2850
2851         my $tid = $transit->id; 
2852         my $loc = $self->circ_lib;
2853         my $dest = $transit->dest;
2854
2855         $logger->info("circulator: Fowarding transit on copy which is destined ".
2856             "for a different location. transit=$tid, copy=$copyid, current ".
2857             "location=$loc, destination location=$dest");
2858
2859         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2860
2861         # grab the associated hold object if available
2862         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2863         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2864
2865         return $self->bail_on_events($evt);
2866     }
2867
2868     # The transit is received, set the receive time
2869     $transit->dest_recv_time('now');
2870     $self->bail_on_events($self->editor->event)
2871         unless $self->editor->update_action_transit_copy($transit);
2872
2873     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2874
2875     $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2876     $copy->status( $transit->copy_status );
2877     $self->update_copy();
2878     return if $self->bail_out;
2879
2880     my $ishold = 0;
2881     if($hold_transit) { 
2882         my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2883
2884         # hold has arrived at destination, set shelf time
2885         $self->put_hold_on_shelf($hold);
2886         $self->bail_on_events($self->editor->event)
2887             unless $self->editor->update_action_hold_request($hold);
2888         return if $self->bail_out;
2889
2890         $self->notify_hold($hold_transit->hold);
2891         $ishold = 1;
2892     }
2893
2894     $self->push_events( 
2895         OpenILS::Event->new(
2896         'SUCCESS', 
2897         ishold => $ishold,
2898       payload => { transit => $transit, holdtransit => $hold_transit } ));
2899
2900     return $hold_transit;
2901 }
2902
2903
2904 # ------------------------------------------------------------------
2905 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2906 # ------------------------------------------------------------------
2907 sub put_hold_on_shelf {
2908     my($self, $hold) = @_;
2909
2910     $hold->shelf_time('now');
2911
2912     my $shelf_expire = $U->ou_ancestor_setting_value(
2913         $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2914
2915     if($shelf_expire) {
2916         my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2917         my $expire_time = DateTime->now->add(seconds => $seconds);
2918         $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2919     }
2920
2921     return undef;
2922 }
2923
2924
2925
2926 sub generate_fines {
2927    my $self = shift;
2928    my $reservation = shift;
2929
2930    $self->generate_fines_start($reservation);
2931    $self->generate_fines_finish($reservation);
2932
2933    return undef;
2934 }
2935
2936 sub generate_fines_start {
2937    my $self = shift;
2938    my $reservation = shift;
2939    my $dt_parser = DateTime::Format::ISO8601->new;
2940
2941    my $obj = $reservation ? $self->reservation : $self->circ;
2942
2943    # If we have a grace period
2944    if($obj->can('grace_period')) {
2945       # Parse out the due date
2946       my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
2947       # Add the grace period to the due date
2948       $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
2949       # Don't generate fines on circs still in grace period
2950       return undef if ($due_date > DateTime->now);
2951    }
2952
2953    if (!exists($self->{_gen_fines_req})) {
2954       $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage') 
2955           ->request(
2956              'open-ils.storage.action.circulation.overdue.generate_fines',
2957              $obj->id
2958           );
2959    }
2960
2961    return undef;
2962 }
2963
2964 sub generate_fines_finish {
2965    my $self = shift;
2966    my $reservation = shift;
2967
2968    return undef unless $self->{_gen_fines_req};
2969
2970    my $id = $reservation ? $self->reservation->id : $self->circ->id;
2971
2972    $self->{_gen_fines_req}->wait_complete;
2973    delete($self->{_gen_fines_req});
2974
2975    # refresh the circ in case the fine generator set the stop_fines field
2976    $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
2977    $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
2978
2979    return undef;
2980 }
2981
2982 sub checkin_handle_circ {
2983    my $self = shift;
2984    my $circ = $self->circ;
2985    my $copy = $self->copy;
2986    my $evt;
2987    my $obt;
2988
2989    $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
2990
2991    # backdate the circ if necessary
2992    if($self->backdate) {
2993         my $evt = $self->checkin_handle_backdate;
2994         return $self->bail_on_events($evt) if $evt;
2995    }
2996
2997    if(!$circ->stop_fines) {
2998       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2999       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3000       $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3001       $circ->stop_fines_time('now');
3002       $circ->stop_fines_time($self->backdate) if $self->backdate;
3003    }
3004
3005     # Set the checkin vars since we have the item
3006     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3007
3008     # capture the true scan time for back-dated checkins
3009     $circ->checkin_scan_time('now');
3010
3011     $circ->checkin_staff($self->editor->requestor->id);
3012     $circ->checkin_lib($self->circ_lib);
3013     $circ->checkin_workstation($self->editor->requestor->wsid);
3014
3015     my $circ_lib = (ref $self->copy->circ_lib) ?  
3016         $self->copy->circ_lib->id : $self->copy->circ_lib;
3017     my $stat = $U->copy_status($self->copy->status)->id;
3018
3019     # immediately available keeps items lost or missing items from going home before being handled
3020     my $lost_immediately_available = $U->ou_ancestor_setting_value(
3021         $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3022
3023
3024     if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
3025
3026         if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
3027             $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
3028         } else {
3029             $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3030             $self->update_copy;
3031         }
3032
3033     } elsif ($stat == OILS_COPY_STATUS_LOST) {
3034
3035         $self->checkin_handle_lost($circ_lib);
3036
3037     } else {
3038
3039         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3040         $self->update_copy;
3041     }
3042
3043
3044     # see if there are any fines owed on this circ.  if not, close it
3045     ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3046     $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3047
3048     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3049
3050     return $self->bail_on_events($self->editor->event)
3051         unless $self->editor->update_action_circulation($circ);
3052
3053     return undef;
3054 }
3055
3056
3057 # ------------------------------------------------------------------
3058 # See if we need to void billings for lost checkin
3059 # ------------------------------------------------------------------
3060 sub checkin_handle_lost {
3061     my $self = shift;
3062     my $circ_lib = shift;
3063     my $circ = $self->circ;
3064
3065     my $max_return = $U->ou_ancestor_setting_value(
3066         $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3067
3068     if ($max_return) {
3069
3070         my $today = time();
3071         my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3072         $tm[5] -= 1 if $tm[5] > 0;
3073         my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3074
3075         my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3076         $logger->info("MAX OD: ".$max_return."  DUEDATE: ".$circ->due_date."  TODAY: ".$today."  DUE: ".$due."  LAST: ".$last_chance);
3077
3078         $max_return = 0 if $today < $last_chance;
3079     }
3080
3081     if (!$max_return){  # there's either no max time to accept returns defined or we're within that time
3082
3083         my $void_lost = $U->ou_ancestor_setting_value(
3084             $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3085         my $void_lost_fee = $U->ou_ancestor_setting_value(
3086             $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3087         my $restore_od = $U->ou_ancestor_setting_value(
3088             $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3089
3090         $self->checkin_handle_lost_now_found(3) if $void_lost;
3091         $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3092         $self->checkin_handle_lost_now_found_restore_od() if $restore_od && ! $self->void_overdues;
3093     }
3094
3095     $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3096     $self->update_copy;
3097 }
3098
3099
3100 sub checkin_handle_backdate {
3101     my $self = shift;
3102
3103     # ------------------------------------------------------------------
3104     # clean up the backdate for date comparison
3105     # XXX We are currently taking the due-time from the original due-date,
3106     # not the input.  Do we need to do this?  This certainly interferes with
3107     # backdating of hourly checkouts, but that is likely a very rare case.
3108     # ------------------------------------------------------------------
3109     my $bd = cleanse_ISO8601($self->backdate);
3110     my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3111     my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3112     $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3113
3114     $self->backdate($bd);
3115     return undef;
3116 }
3117
3118
3119 sub check_checkin_copy_status {
3120     my $self = shift;
3121    my $copy = $self->copy;
3122
3123    my $status = $U->copy_status($copy->status)->id;
3124
3125    return undef
3126       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
3127             $status == OILS_COPY_STATUS_CHECKED_OUT ||
3128             $status == OILS_COPY_STATUS_IN_PROCESS  ||
3129             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
3130             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
3131             $status == OILS_COPY_STATUS_CATALOGING  ||
3132             $status == OILS_COPY_STATUS_ON_RESV_SHELF  ||
3133             $status == OILS_COPY_STATUS_RESHELVING );
3134
3135    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3136       if( $status == OILS_COPY_STATUS_LOST );
3137
3138    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3139       if( $status == OILS_COPY_STATUS_MISSING );
3140
3141    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3142 }
3143
3144
3145
3146 # --------------------------------------------------------------------------
3147 # On checkin, we need to return as many relevant objects as we can
3148 # --------------------------------------------------------------------------
3149 sub checkin_flesh_events {
3150     my $self = shift;
3151
3152     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
3153         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3154             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3155     }
3156
3157     my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3158
3159     my $hold;
3160     if($self->hold and !$self->hold->cancel_time) {
3161         $hold = $self->hold;
3162         $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3163     }
3164
3165     if($self->circ) {
3166         # if we checked in a circulation, flesh the billing summary data
3167         $self->circ->billable_transaction(
3168             $self->editor->retrieve_money_billable_transaction([
3169                 $self->circ->id,
3170                 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3171             ])
3172         );
3173     }
3174
3175     if($self->patron) {
3176         # flesh some patron fields before returning
3177         $self->patron(
3178             $self->editor->retrieve_actor_user([
3179                 $self->patron->id,
3180                 {
3181                     flesh => 1,
3182                     flesh_fields => {
3183                         au => ['card', 'billing_address', 'mailing_address']
3184                     }
3185                 }
3186             ])
3187         );
3188     }
3189
3190     for my $evt (@{$self->events}) {
3191
3192         my $payload         = {};
3193         $payload->{copy}    = $U->unflesh_copy($self->copy);
3194         $payload->{volume}  = $self->volume;
3195         $payload->{record}  = $record,
3196         $payload->{circ}    = $self->circ;
3197         $payload->{transit} = $self->transit;
3198         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3199         $payload->{hold}    = $hold;
3200         $payload->{patron}  = $self->patron;
3201         $payload->{reservation} = $self->reservation
3202             unless (not $self->reservation or $self->reservation->cancel_time);
3203
3204         $evt->{payload}     = $payload;
3205     }
3206 }
3207
3208 sub log_me {
3209     my( $self, $msg ) = @_;
3210     my $bc = ($self->copy) ? $self->copy->barcode :
3211         $self->barcode;
3212     $bc ||= "";
3213     my $usr = ($self->patron) ? $self->patron->id : "";
3214     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3215         ", recipient=$usr, copy=$bc");
3216 }
3217
3218
3219 sub do_renew {
3220     my $self = shift;
3221     $self->log_me("do_renew()");
3222
3223     # Make sure there is an open circ to renew that is not
3224     # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3225     my $usrid = $self->patron->id if $self->patron;
3226     my $circ = $self->editor->search_action_circulation({
3227         target_copy => $self->copy->id,
3228         xact_finish => undef,
3229         checkin_time => undef,
3230         ($usrid ? (usr => $usrid) : ()),
3231         '-or' => [
3232             {stop_fines => undef},
3233             {stop_fines => OILS_STOP_FINES_MAX_FINES}
3234         ]
3235     })->[0];
3236
3237     return $self->bail_on_events($self->editor->event) unless $circ;
3238
3239     # A user is not allowed to renew another user's items without permission
3240     unless( $circ->usr eq $self->editor->requestor->id ) {
3241         return $self->bail_on_events($self->editor->events)
3242             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3243     }   
3244
3245     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3246         if $circ->renewal_remaining < 1;
3247
3248     # -----------------------------------------------------------------
3249
3250     $self->parent_circ($circ->id);
3251     $self->renewal_remaining( $circ->renewal_remaining - 1 );
3252     $self->circ($circ);
3253
3254     # Run the fine generator against the old circ
3255     $self->generate_fines_start;
3256
3257     $self->run_renew_permit;
3258
3259     # Check the item in
3260     $self->do_checkin();
3261     return if $self->bail_out;
3262
3263     unless( $self->permit_override ) {
3264         $self->do_permit();
3265         return if $self->bail_out;
3266         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3267         $self->remove_event('ITEM_NOT_CATALOGED');
3268     }   
3269
3270     $self->override_events;
3271     return if $self->bail_out;
3272
3273     $self->events([]);
3274     $self->do_checkout();
3275 }
3276
3277
3278 sub remove_event {
3279     my( $self, $evt ) = @_;
3280     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3281     $logger->debug("circulator: removing event from list: $evt");
3282     my @events = @{$self->events};
3283     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3284 }
3285
3286
3287 sub have_event {
3288     my( $self, $evt ) = @_;
3289     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3290     return grep { $_->{textcode} eq $evt } @{$self->events};
3291 }
3292
3293
3294
3295 sub run_renew_permit {
3296     my $self = shift;
3297
3298     if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3299         my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3300             $self->editor, $self->copy, $self->editor->requestor, 1
3301         );
3302         $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3303     }
3304
3305     if(!$self->legacy_script_support) {
3306         my $results = $self->run_indb_circ_test;
3307         $self->push_events($self->matrix_test_result_events)
3308             unless $self->circ_test_success;
3309     } else {
3310
3311         my $runner = $self->script_runner;
3312
3313         $runner->load($self->circ_permit_renew);
3314         my $result = $runner->run or 
3315             throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3316         if ($result->{"events"}) {
3317             $self->push_events(
3318                 map { new OpenILS::Event($_) } @{$result->{"events"}}
3319             );
3320             $logger->activity(
3321                 "circulator: circ_permit_renew for user " .
3322                 $self->patron->id . " returned " .
3323                 scalar(@{$result->{"events"}}) . " event(s)"
3324             );
3325         }
3326
3327         $self->mk_script_runner;
3328     }
3329
3330     $logger->debug("circulator: re-creating script runner to be safe");
3331 }
3332
3333
3334 # XXX: The primary mechanism for storing circ history is now handled
3335 # by tracking real circulation objects instead of bibs in a bucket.
3336 # However, this code is disabled by default and could be useful 
3337 # some day, so may as well leave it for now.
3338 sub append_reading_list {
3339     my $self = shift;
3340
3341     return undef unless 
3342         $self->is_checkout and 
3343         $self->patron and 
3344         $self->copy and 
3345         !$self->is_noncat;
3346
3347
3348     # verify history is globally enabled and uses the bucket mechanism
3349     my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3350         apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3351
3352     return undef unless $htype and $htype eq 'bucket';
3353
3354     my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3355
3356     # verify the patron wants to retain the hisory
3357         my $setting = $e->search_actor_user_setting(
3358                 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3359     
3360     unless($setting and $setting->value) {
3361         $e->rollback;
3362         return undef;
3363     }
3364
3365     my $bkt = $e->search_container_copy_bucket(
3366         {owner => $self->patron->id, btype => 'circ_history'})->[0];
3367
3368     my $pos = 1;
3369
3370     if($bkt) {
3371         # find the next item position
3372         my $last_item = $e->search_container_copy_bucket_item(
3373             {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3374         $pos = $last_item->pos + 1 if $last_item;
3375
3376     } else {
3377         # create the history bucket if necessary
3378         $bkt = Fieldmapper::container::copy_bucket->new;
3379         $bkt->owner($self->patron->id);
3380         $bkt->name('');
3381         $bkt->btype('circ_history');
3382         $bkt->pub('f');
3383         $e->create_container_copy_bucket($bkt) or return $e->die_event;
3384     }
3385
3386     my $item = Fieldmapper::container::copy_bucket_item->new;
3387
3388     $item->bucket($bkt->id);
3389     $item->target_copy($self->copy->id);
3390     $item->pos($pos);
3391
3392     $e->create_container_copy_bucket_item($item) or return $e->die_event;
3393     $e->commit;
3394
3395     return undef;
3396 }
3397
3398
3399 sub make_trigger_events {
3400     my $self = shift;
3401     return unless $self->circ;
3402     $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3403     $U->create_events_for_hook('checkin',  $self->circ, $self->circ_lib) if $self->is_checkin;
3404     $U->create_events_for_hook('renewal',  $self->circ, $self->circ_lib) if $self->is_renewal;
3405 }
3406
3407
3408
3409 sub checkin_handle_lost_now_found {
3410     my ($self, $bill_type) = @_;
3411
3412     # ------------------------------------------------------------------
3413     # remove charge from patron's account if lost item is returned
3414     # ------------------------------------------------------------------
3415
3416     my $bills = $self->editor->search_money_billing(
3417         {
3418             xact => $self->circ->id,
3419             btype => $bill_type
3420         }
3421     );
3422
3423     $logger->debug("voiding lost item charge of  ".scalar(@$bills));
3424     for my $bill (@$bills) {
3425         if( !$U->is_true($bill->voided) ) {
3426             $logger->info("lost item returned - voiding bill ".$bill->id);
3427             $bill->voided('t');
3428             $bill->void_time('now');
3429             $bill->voider($self->editor->requestor->id);
3430             my $note = ($bill->note) ? $bill->note . "\n" : '';
3431             $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3432
3433             $self->bail_on_events($self->editor->event)
3434                 unless $self->editor->update_money_billing($bill);
3435         }
3436     }
3437 }
3438
3439 sub checkin_handle_lost_now_found_restore_od {
3440     my $self = shift;
3441
3442     # ------------------------------------------------------------------
3443     # restore those overdue charges voided when item was set to lost
3444     # ------------------------------------------------------------------
3445
3446     my $ods = $self->editor->search_money_billing(
3447         {
3448                 xact => $self->circ->id,
3449                 btype => 1
3450         }
3451     );
3452
3453     $logger->debug("returning overdue charges pre-lost  ".scalar(@$ods));
3454     for my $bill (@$ods) {
3455         if( $U->is_true($bill->voided) ) {
3456                 $logger->info("lost item returned - restoring overdue ".$bill->id);
3457                 $bill->voided('f');
3458                 $bill->clear_void_time;
3459                 $bill->voider($self->editor->requestor->id);
3460                 my $note = ($bill->note) ? $bill->note . "\n" : '';
3461                 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3462
3463                 $self->bail_on_events($self->editor->event)
3464                         unless $self->editor->update_money_billing($bill);
3465         }
3466     }
3467 }
3468
3469 1;