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