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