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