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