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