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