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