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