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