]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
typo on stored proc name
[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                             or return $e->event;
508             }
509     
510         if($copy) {
511             $self->copy($copy);
512             $self->volume($copy->call_number);
513             $self->title($self->volume->record);
514             $self->copy->call_number($self->volume->id);
515             $self->volume->record($self->title->id);
516             $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
517         } else {
518             # We can't renew if there is no copy
519             return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
520                 if $self->is_renewal;
521             $self->is_precat(1);
522         }
523     }
524
525
526     # --------------------------------------------------------------------------
527     # Grab the patron
528     # --------------------------------------------------------------------------
529     my $patron;
530     my $p_evt;
531         if( $self->patron_id ) {
532                 $patron = $e->retrieve_actor_user($self->patron_id) or $p_evt = $e->event;
533
534         } elsif( $self->patron_barcode ) {
535
536                 my $card = $e->search_actor_card( 
537                         {barcode => $self->patron_barcode})->[0] or return $e->event;
538
539                 $patron = $e->search_actor_user( 
540                         {card => $card->id})->[0] or return $e->event;
541
542         } else {
543                 if( my $copy = $self->copy ) {
544                         my $circs = $e->search_action_circulation(
545                                 {target_copy => $copy->id, checkin_time => undef});
546
547                         if( my $circ = $circs->[0] ) {
548                                 $patron = $e->retrieve_actor_user($circ->usr)
549                                         or return $e->event;
550                         }
551                 }
552         }
553
554     return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
555         unless $self->patron($patron);
556 }
557
558 # --------------------------------------------------------------------------
559 # This builds the script runner environment and fetches most of the
560 # objects we need
561 # --------------------------------------------------------------------------
562 sub mk_script_runner {
563     my $self = shift;
564     my $args = {};
565
566
567     my @fields = 
568         qw/copy copy_barcode copy_id patron 
569             patron_id patron_barcode volume title editor/;
570
571     # Translate our objects into the ScriptBuilder args hash
572     $$args{$_} = $self->$_() for @fields;
573
574     $args->{ignore_user_status} = 1 if $self->is_checkin;
575     $$args{fetch_patron_by_circ_copy} = 1;
576     $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
577
578     if( my $pco = $self->pending_checkouts ) {
579         $logger->info("circulator: we were given a pending checkouts number of $pco");
580         $$args{patronItemsOut} = $pco;
581     }
582
583     # This fetches most of the objects we need
584     $self->script_runner(
585         OpenILS::Application::Circ::ScriptBuilder->build($args));
586
587     # Now we translate the ScriptBuilder objects back into self
588     $self->$_($$args{$_}) for @fields;
589
590     my @evts = @{$args->{_events}} if $args->{_events};
591
592     $logger->debug("circulator: script builder returned events: @evts") if @evts;
593
594
595     if(@evts) {
596         # Anything besides ASSET_COPY_NOT_FOUND will stop processing
597         if(!$self->is_noncat and 
598             @evts == 1 and 
599             $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
600                 $self->is_precat(1);
601
602         } else {
603             my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
604             return $self->bail_on_events(@e);
605         }
606     }
607
608     $self->is_precat(1) if $self->copy 
609         and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
610
611     # We can't renew if there is no copy
612     return $self->bail_on_events(@evts) if 
613         $self->is_renewal and !$self->copy;
614
615     # Set some circ-specific flags in the script environment
616     my $evt = "environment";
617     $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
618
619     if( $self->is_noncat ) {
620       $self->script_runner->insert("$evt.isNonCat", 1);
621       $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
622     }
623
624     if( $self->is_precat ) {
625         $self->script_runner->insert("environment.isPrecat", 1, 1);
626     }
627
628     $self->script_runner->add_path( $_ ) for @$script_libs;
629
630     return 1;
631 }
632
633 # --------------------------------------------------------------------------
634 # Does the circ permit work
635 # --------------------------------------------------------------------------
636 sub do_permit {
637     my $self = shift;
638
639     $self->log_me("do_permit()");
640
641     unless( $self->editor->requestor->id == $self->patron->id ) {
642         return $self->bail_on_events($self->editor->event)
643             unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
644     }
645
646     $self->check_captured_holds();
647     $self->do_copy_checks();
648     return if $self->bail_out;
649     $self->run_patron_permit_scripts();
650     $self->run_copy_permit_scripts() 
651         unless $self->is_precat or $self->is_noncat;
652     $self->override_events() unless 
653         $self->is_renewal and not $self->check_penalty_on_renew;
654     return if $self->bail_out;
655
656     if( $self->is_precat ) {
657         $self->push_events(
658             OpenILS::Event->new(
659                 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
660         return $self->bail_out(1) unless $self->is_renewal;
661     }
662
663     $self->push_events(
664       OpenILS::Event->new(
665             'SUCCESS', 
666             payload => $self->mk_permit_key));
667 }
668
669
670 sub check_captured_holds {
671    my $self    = shift;
672    my $copy    = $self->copy;
673    my $patron  = $self->patron;
674
675     return undef unless $copy;
676
677     my $s = $U->copy_status($copy->status)->id;
678     return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
679     $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
680
681     # Item is on the holds shelf, make sure it's going to the right person
682     my $holds   = $self->editor->search_action_hold_request(
683         [
684             { 
685                 current_copy        => $copy->id , 
686                 capture_time        => { '!=' => undef },
687                 cancel_time         => undef, 
688                 fulfillment_time    => undef 
689             },
690             { limit => 1 }
691         ]
692     );
693
694     if( $holds and $$holds[0] ) {
695         return undef if $$holds[0]->usr == $patron->id;
696     }
697
698     $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
699
700     $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
701 }
702
703
704 sub do_copy_checks {
705     my $self = shift;
706     my $copy = $self->copy;
707     return unless $copy;
708
709     my $stat = $U->copy_status($copy->status)->id;
710
711     # We cannot check out a copy if it is in-transit
712     if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
713         return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
714     }
715
716     $self->handle_claims_returned();
717     return if $self->bail_out;
718
719     # no claims returned circ was found, check if there is any open circ
720     unless( $self->is_renewal ) {
721         my $circs = $self->editor->search_action_circulation(
722             { target_copy => $copy->id, checkin_time => undef }
723         );
724
725         return $self->bail_on_events(
726             OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
727     }
728 }
729
730
731 sub send_penalty_request {
732     my $self = shift;
733     my $ses = OpenSRF::AppSession->create('open-ils.penalty');
734     $self->penalty_request(
735         $ses->request(  
736             'open-ils.penalty.patron_penalty.calculate', 
737             {   update => 1, 
738                 authtoken => $self->editor->authtoken,
739                 patron => $self->patron } ) );
740 }
741
742 sub gather_penalty_request {
743     my $self = shift;
744     return [] unless $self->penalty_request;
745     my $data = $self->penalty_request->recv;
746     if( ref $data ) {
747         throw $data if UNIVERSAL::isa($data,'Error');
748         $data = $data->content;
749         return $data->{fatal_penalties};
750     }
751     $logger->error("circulator: penalty request returned no data");
752     return [];
753 }
754
755 my $LEGACY_CIRC_EVENT_MAP = {
756     'actor.usr.barred' => 'PATRON_BARRED',
757     'asset.copy.circulate' =>  'COPY_CIRC_NOT_ALLOWED',
758     'asset.copy.status' => 'COPY_NOT_AVAILABLE',
759     'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
760     'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
761     'config.circ_matrix_test.max_items_out' =>  'PATRON_EXCEEDS_CHECKOUT_COUNT',
762     'config.circ_matrix_test.max_overdue' =>  'PATRON_EXCEEDS_OVERDUE_COUNT',
763     'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
764     'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
765 };
766
767
768 # ---------------------------------------------------------------------
769 # This pushes any patron-related events into the list but does not
770 # set bail_out for any events
771 # ---------------------------------------------------------------------
772 sub run_patron_permit_scripts {
773     my $self        = shift;
774     my $runner      = $self->script_runner;
775     my $patronid    = $self->patron->id;
776
777     my @allevents; 
778
779     if(!$self->legacy_script_support) {
780
781         my $results = $self->run_indb_circ_test;
782         unless($self->circ_test_success) {
783             push(@allevents, OpenILS::Event->new(
784                 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
785         }
786
787     } else {
788
789         $self->send_penalty_request() unless
790             $self->is_renewal and not $self->check_penalty_on_renew;
791
792
793         # --------------------------------------------------------------------- # Now run the patron permit script 
794         # ---------------------------------------------------------------------
795         $runner->load($self->circ_permit_patron);
796         my $result = $runner->run or 
797             throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
798
799         my $patron_events = $result->{events};
800
801         my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ? 
802             [] : $self->gather_penalty_request();
803
804         push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
805     }
806
807     $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
808
809     $self->push_events(@allevents);
810 }
811
812 sub run_indb_circ_test {
813     my $self = shift;
814     return $self->matrix_test_result if $self->matrix_test_result;
815
816     my $dbfunc = ($self->is_renewal) ? 
817         'action.item_user_renew_test' : 'action.item_user_circ_test';
818
819     my $results = ($self->matrix_test_result) ? 
820         $self->matrix_test_result : 
821             $self->editor->json_query(
822                 {   from => [
823                         $dbfunc,
824                         $self->editor->requestor->ws_ou,
825                         $self->copy->id, 
826                         $self->patron->id,
827                     ]
828                 }
829             );
830
831     $self->circ_test_success($U->is_true($results->[0]->{success}));
832     if($self->circ_test_success) {
833         $self->circ_matrix_test(
834             $self->editor->retrieve_config_circ_matrix_test(
835                 $results->[0]->{matchpoint}
836             )
837         );
838     }
839
840     if($self->circ_test_success) {
841         $self->circ_matrix_ruleset(
842             $self->editor->retrieve_config_circ_matrix_ruleset([
843                 $results->[0]->{matchpoint},
844                 {   flesh => 1,
845                     flesh_fields => {
846                         'ccmrs' => ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']
847                     }
848                 }
849                 ]
850             )
851         );
852     }
853
854     return $self->matrix_test_result($results);
855 }
856
857 sub do_inspect {
858     my $self = shift;
859     $self->run_indb_circ_test;
860     return {
861         circ_test_success => $self->circ_test_success,
862         matrix_test_result => $self->matrix_test_result,
863         circ_matrix_test => $self->circ_matrix_test,
864         circ_matrix_ruleset => $self->circ_matrix_ruleset
865     };
866 }
867
868
869 sub run_copy_permit_scripts {
870     my $self = shift;
871     my $copy = $self->copy || return;
872     my $runner = $self->script_runner;
873
874     my @allevents;
875
876     if(!$self->legacy_script_support) {
877         my $results = $self->run_indb_circ_test;
878         unless($self->circ_test_success) {
879             push(@allevents, OpenILS::Event->new(
880                 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
881         }
882     } else {
883     
884        # ---------------------------------------------------------------------
885        # Capture all of the copy permit events
886        # ---------------------------------------------------------------------
887        $runner->load($self->circ_permit_copy);
888        my $result = $runner->run or 
889             throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
890        my $copy_events = $result->{events};
891
892        # ---------------------------------------------------------------------
893        # Now collect all of the events together
894        # ---------------------------------------------------------------------
895        push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
896     }
897
898     # See if this copy has an alert message
899     my $ae = $self->check_copy_alert();
900     push( @allevents, $ae ) if $ae;
901
902    # uniquify the events
903    my %hash = map { ($_->{ilsevent} => $_) } @allevents;
904    @allevents = values %hash;
905
906    for (@allevents) {
907       $_->{payload} = $copy if 
908             ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
909    }
910
911     $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
912
913     $self->push_events(@allevents);
914 }
915
916
917 sub check_copy_alert {
918     my $self = shift;
919     return undef if $self->is_renewal;
920     return OpenILS::Event->new(
921         'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
922         if $self->copy and $self->copy->alert_message;
923     return undef;
924 }
925
926
927
928 # --------------------------------------------------------------------------
929 # If the call is overriding and has permissions to override every collected
930 # event, the are cleared.  Any event that the caller does not have
931 # permission to override, will be left in the event list and bail_out will
932 # be set
933 # XXX We need code in here to cancel any holds/transits on copies 
934 # that are being force-checked out
935 # --------------------------------------------------------------------------
936 sub override_events {
937     my $self = shift;
938     my @events = @{$self->events};
939     return unless @events;
940
941     if(!$self->override) {
942         return $self->bail_out(1) 
943             if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
944     }   
945
946     $self->events([]);
947     
948    for my $e (@events) {
949       my $tc = $e->{textcode};
950       next if $tc eq 'SUCCESS';
951       my $ov = "$tc.override";
952       $logger->info("circulator: attempting to override event: $ov");
953
954         return $self->bail_on_events($self->editor->event)
955             unless( $self->editor->allowed($ov) );
956    }
957 }
958     
959
960 # --------------------------------------------------------------------------
961 # If there is an open claimsreturn circ on the requested copy, close the 
962 # circ if overriding, otherwise bail out
963 # --------------------------------------------------------------------------
964 sub handle_claims_returned {
965     my $self = shift;
966     my $copy = $self->copy;
967
968     my $CR = $self->editor->search_action_circulation(
969         {   
970             target_copy     => $copy->id,
971             stop_fines      => OILS_STOP_FINES_CLAIMSRETURNED,
972             checkin_time    => undef,
973         }
974     );
975
976     return unless ($CR = $CR->[0]); 
977
978     my $evt;
979
980     # - If the caller has set the override flag, we will check the item in
981     if($self->override) {
982
983         $CR->checkin_time('now');   
984         $CR->checkin_lib($self->editor->requestor->ws_ou);
985         $CR->checkin_staff($self->editor->requestor->id);
986
987         $evt = $self->editor->event 
988             unless $self->editor->update_action_circulation($CR);
989
990     } else {
991         $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
992     }
993
994     $self->bail_on_events($evt) if $evt;
995     return;
996 }
997
998
999 # --------------------------------------------------------------------------
1000 # This performs the checkout
1001 # --------------------------------------------------------------------------
1002 sub do_checkout {
1003     my $self = shift;
1004
1005     $self->log_me("do_checkout()");
1006
1007     # make sure perms are good if this isn't a renewal
1008     unless( $self->is_renewal ) {
1009         return $self->bail_on_events($self->editor->event)
1010             unless( $self->editor->allowed('COPY_CHECKOUT') );
1011     }
1012
1013     # verify the permit key
1014     unless( $self->check_permit_key ) {
1015         if( $self->permit_override ) {
1016             return $self->bail_on_events($self->editor->event)
1017                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1018         } else {
1019             return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1020         }   
1021     }
1022
1023     # if this is a non-cataloged circ, build the circ and finish
1024     if( $self->is_noncat ) {
1025         $self->checkout_noncat;
1026         $self->push_events(
1027             OpenILS::Event->new('SUCCESS', 
1028             payload => { noncat_circ => $self->circ }));
1029         return;
1030     }
1031
1032     if( $self->is_precat ) {
1033         #$self->script_runner->insert("environment.isPrecat", 1, 1)
1034         $self->make_precat_copy;
1035         return if $self->bail_out;
1036
1037     } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1038         return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1039     }
1040
1041     $self->do_copy_checks;
1042     return if $self->bail_out;
1043
1044     $self->run_checkout_scripts();
1045     return if $self->bail_out;
1046
1047     $self->build_checkout_circ_object();
1048     return if $self->bail_out;
1049
1050     $self->apply_modified_due_date();
1051     return if $self->bail_out;
1052
1053     return $self->bail_on_events($self->editor->event)
1054         unless $self->editor->create_action_circulation($self->circ);
1055
1056     # refresh the circ to force local time zone for now
1057     $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1058
1059     $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1060     $self->update_copy;
1061     return if $self->bail_out;
1062
1063     $self->handle_checkout_holds();
1064     return if $self->bail_out;
1065
1066    # ------------------------------------------------------------------------------
1067    # Update the patron penalty info in the DB.  Run it for permit-overrides or
1068     # renewals since both of those cases do not require the penalty server to
1069     # run during the permit phase of the checkout
1070    # ------------------------------------------------------------------------------
1071     if( $self->permit_override or $self->is_renewal ) {
1072         $U->update_patron_penalties(
1073             authtoken => $self->editor->authtoken,
1074             patron    => $self->patron,
1075             background  => 1,
1076         );
1077     }
1078
1079     my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1080     $self->push_events(
1081         OpenILS::Event->new('SUCCESS',
1082             payload  => {
1083                 copy              => $U->unflesh_copy($self->copy),
1084                 circ              => $self->circ,
1085                 record            => $record,
1086                 holds_fulfilled   => $self->fulfilled_holds,
1087             }
1088         )
1089     );
1090 }
1091
1092 sub update_copy {
1093     my $self = shift;
1094     my $copy = $self->copy;
1095
1096     my $stat = $copy->status if ref $copy->status;
1097     my $loc = $copy->location if ref $copy->location;
1098     my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1099
1100     $copy->status($stat->id) if $stat;
1101     $copy->location($loc->id) if $loc;
1102     $copy->circ_lib($circ_lib->id) if $circ_lib;
1103     $copy->editor($self->editor->requestor->id);
1104     $copy->edit_date('now');
1105     $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1106
1107     return $self->bail_on_events($self->editor->event)
1108         unless $self->editor->update_asset_copy($self->copy);
1109
1110     $copy->status($U->copy_status($copy->status));
1111     $copy->location($loc) if $loc;
1112     $copy->circ_lib($circ_lib) if $circ_lib;
1113 }
1114
1115
1116 sub bail_on_events {
1117     my( $self, @evts ) = @_;
1118     $self->push_events(@evts);
1119     $self->bail_out(1);
1120 }
1121
1122 sub handle_checkout_holds {
1123    my $self    = shift;
1124
1125    my $copy    = $self->copy;
1126    my $patron  = $self->patron;
1127
1128     my $holds   = $self->editor->search_action_hold_request(
1129         { 
1130             current_copy        => $copy->id , 
1131             cancel_time         => undef, 
1132             fulfillment_time    => undef 
1133         }
1134     );
1135
1136    my @fulfilled;
1137
1138    # XXX We should only fulfill one hold here...
1139    # XXX If a hold was transited to the user who is checking out
1140    # the item, we need to make sure that hold is what's grabbed
1141    if(@$holds) {
1142
1143       # for now, just sort by id to get what should be the oldest hold
1144       $holds = [ sort { $a->id <=> $b->id } @$holds ];
1145       my @myholds = grep { $_->usr eq $patron->id } @$holds;
1146       my @altholds   = grep { $_->usr ne $patron->id } @$holds;
1147
1148       if(@myholds) {
1149          my $hold = $myholds[0];
1150
1151          $logger->debug("circulator: related hold found in checkout: " . $hold->id );
1152
1153          # if the hold was never officially captured, capture it.
1154          $hold->capture_time('now') unless $hold->capture_time;
1155
1156             # just make sure it's set correctly
1157          $hold->current_copy($copy->id); 
1158
1159          $hold->fulfillment_time('now');
1160             $hold->fulfillment_staff($self->editor->requestor->id);
1161             $hold->fulfillment_lib($self->editor->requestor->ws_ou);
1162
1163             return $self->bail_on_events($self->editor->event)
1164                 unless $self->editor->update_action_hold_request($hold);
1165
1166             $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
1167
1168          push( @fulfilled, $hold->id );
1169       }
1170
1171       # If there are any holds placed for other users that point to this copy,
1172       # then we need to un-target those holds so the targeter can pick a new copy
1173       for(@altholds) {
1174
1175          $logger->info("circulator: un-targeting hold ".$_->id.
1176             " because copy ".$copy->id." is getting checked out");
1177
1178             # - make the targeter process this hold at next run
1179          $_->clear_prev_check_time; 
1180
1181             # - clear out the targetted copy
1182          $_->clear_current_copy;
1183          $_->clear_capture_time;
1184
1185             return $self->bail_on_event($self->editor->event)
1186                 unless $self->editor->update_action_hold_request($_);
1187       }
1188    }
1189
1190     $self->fulfilled_holds(\@fulfilled);
1191 }
1192
1193
1194
1195 sub run_checkout_scripts {
1196     my $self = shift;
1197
1198     my $evt;
1199     my $runner = $self->script_runner;
1200
1201     my $duration;
1202     my $recurring;
1203     my $max_fine;
1204     my $duration_name;
1205     my $recurring_name;
1206     my $max_fine_name;
1207
1208     if(!$self->legacy_script_support) {
1209         $self->run_indb_circ_test();
1210         $duration = $self->circ_matrix_ruleset->duration_rule;
1211         $recurring = $self->circ_matrix_ruleset->recurring_fine_rule;
1212         $max_fine = $self->circ_matrix_ruleset->max_fine_rule;
1213
1214     } else {
1215
1216        $runner->load($self->circ_duration);
1217
1218        my $result = $runner->run or 
1219             throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1220
1221        $duration_name   = $result->{durationRule};
1222        $recurring_name  = $result->{recurringFinesRule};
1223        $max_fine_name   = $result->{maxFine};
1224     }
1225
1226     $duration_name = $duration->name if $duration;
1227     if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1228
1229         unless($duration) {
1230             ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1231             return $self->bail_on_events($evt) if $evt;
1232         
1233             ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1234             return $self->bail_on_events($evt) if $evt;
1235         
1236             ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1237             return $self->bail_on_events($evt) if $evt;
1238         }
1239
1240     } else {
1241
1242         # The item circulates with an unlimited duration
1243         $duration   = undef;
1244         $recurring  = undef;
1245         $max_fine   = undef;
1246     }
1247
1248    $self->duration_rule($duration);
1249    $self->recurring_fines_rule($recurring);
1250    $self->max_fine_rule($max_fine);
1251 }
1252
1253
1254 sub build_checkout_circ_object {
1255     my $self = shift;
1256
1257    my $circ       = Fieldmapper::action::circulation->new;
1258    my $duration   = $self->duration_rule;
1259    my $max        = $self->max_fine_rule;
1260    my $recurring  = $self->recurring_fines_rule;
1261    my $copy       = $self->copy;
1262    my $patron     = $self->patron;
1263
1264     if( $duration ) {
1265
1266         my $dname = $duration->name;
1267         my $mname = $max->name;
1268         my $rname = $recurring->name;
1269     
1270         my $max_amount = $max->amount;
1271
1272         # if is_percent is true then the max->amount is
1273         # use as a percentage of the copy price
1274         if ($U->is_true($max->is_percent)) {
1275
1276             my $cn = $self->editor->retrieve_asset_call_number($copy->call_number);
1277
1278             my $default_price = $U->ou_ancestor_setting_value(
1279                 $cn->owning_lib, OILS_SETTING_DEF_ITEM_PRICE, $self->editor) || 0;
1280             my $charge_on_0 = $U->ou_ancestor_setting_value(
1281                 $cn->owning_lib, OILS_SETTING_CHARGE_LOST_ON_ZERO, $self->editor) || 0;
1282
1283             # Find the most appropriate "price" -- same definition as the
1284             # LOST price.  See OpenILS::Circ::new_set_circ_lost
1285             $max_amount = $copy->price;
1286             $max_amount = $default_price unless defined $max_amount;
1287             $max_amount = 0 if $max_amount < 0;
1288             $max_amount = $default_price if $max_amount == 0 and $charge_on_0;
1289
1290             $max_amount *= $max->amount / 100;
1291
1292         }
1293
1294         $logger->debug("circulator: building circulation ".
1295             "with duration=$dname, maxfine=$mname, recurring=$rname");
1296     
1297         $circ->duration( $duration->shrt ) 
1298             if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1299         $circ->duration( $duration->normal ) 
1300             if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1301         $circ->duration( $duration->extended ) 
1302             if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1303     
1304         $circ->recuring_fine( $recurring->low ) 
1305             if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1306         $circ->recuring_fine( $recurring->normal ) 
1307             if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1308         $circ->recuring_fine( $recurring->high ) 
1309             if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1310
1311         $circ->duration_rule( $duration->name );
1312         $circ->recuring_fine_rule( $recurring->name );
1313         $circ->max_fine_rule( $max->name );
1314
1315         $circ->max_fine( $max_amount );
1316
1317         $circ->fine_interval($recurring->recurance_interval);
1318         $circ->renewal_remaining( $duration->max_renewals );
1319
1320     } else {
1321
1322         $logger->info("circulator: copy found with an unlimited circ duration");
1323         $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1324         $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1325         $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1326         $circ->renewal_remaining(0);
1327     }
1328
1329    $circ->target_copy( $copy->id );
1330    $circ->usr( $patron->id );
1331    $circ->circ_lib( $self->circ_lib );
1332
1333    if( $self->is_renewal ) {
1334       $circ->opac_renewal('t') if $self->opac_renewal;
1335       $circ->phone_renewal('t') if $self->phone_renewal;
1336       $circ->desk_renewal('t') if $self->desk_renewal;
1337       $circ->renewal_remaining($self->renewal_remaining);
1338       $circ->circ_staff($self->editor->requestor->id);
1339    }
1340
1341
1342    # if the user provided an overiding checkout time,
1343    # (e.g. the checkout really happened several hours ago), then
1344    # we apply that here.  Does this need a perm??
1345     $circ->xact_start(clense_ISO8601($self->checkout_time))
1346         if $self->checkout_time;
1347
1348    # if a patron is renewing, 'requestor' will be the patron
1349    $circ->circ_staff($self->editor->requestor->id);
1350     $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1351
1352     $self->circ($circ);
1353 }
1354
1355
1356 sub apply_modified_due_date {
1357     my $self = shift;
1358     my $circ = $self->circ;
1359     my $copy = $self->copy;
1360
1361    if( $self->due_date ) {
1362
1363         return $self->bail_on_events($self->editor->event)
1364             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1365
1366       $circ->due_date(clense_ISO8601($self->due_date));
1367
1368    } else {
1369
1370       # if the due_date lands on a day when the location is closed
1371       return unless $copy and $circ->due_date;
1372
1373         #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1374
1375         # due-date overlap should be determined by the location the item
1376         # is checked out from, not the owning or circ lib of the item
1377         my $org = $self->editor->requestor->ws_ou;
1378
1379       $logger->info("circulator: circ searching for closed date overlap on lib $org".
1380             " with an item due date of ".$circ->due_date );
1381
1382       my $dateinfo = $U->storagereq(
1383          'open-ils.storage.actor.org_unit.closed_date.overlap', 
1384             $org, $circ->due_date );
1385
1386       if($dateinfo) {
1387          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1388             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1389
1390             # XXX make the behavior more dynamic
1391             # for now, we just push the due date to after the close date
1392             $circ->due_date($dateinfo->{end});
1393       }
1394    }
1395 }
1396
1397
1398
1399 sub create_due_date {
1400     my( $self, $duration ) = @_;
1401     # if there is a raw time component (e.g. from postgres), 
1402     # turn it into an interval that interval_to_seconds can parse
1403     $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1404    my ($sec,$min,$hour,$mday,$mon,$year) =
1405       gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1406    $year += 1900; $mon += 1;
1407    my $due_date = sprintf(
1408       '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1409       $year, $mon, $mday, $hour, $min, $sec);
1410    return $due_date;
1411 }
1412
1413
1414
1415 sub make_precat_copy {
1416     my $self = shift;
1417     my $copy = $self->copy;
1418
1419    if($copy) {
1420       $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1421
1422       $copy->editor($self->editor->requestor->id);
1423       $copy->edit_date('now');
1424       $copy->dummy_title($self->dummy_title);
1425       $copy->dummy_author($self->dummy_author);
1426
1427         $self->update_copy();
1428         return;
1429    }
1430
1431    $logger->info("circulator: Creating a new precataloged ".
1432         "copy in checkout with barcode " . $self->copy_barcode);
1433
1434    $copy = Fieldmapper::asset::copy->new;
1435    $copy->circ_lib($self->circ_lib);
1436    $copy->creator($self->editor->requestor->id);
1437    $copy->editor($self->editor->requestor->id);
1438    $copy->barcode($self->copy_barcode);
1439    $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
1440    $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1441    $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1442
1443    $copy->dummy_title($self->dummy_title || "");
1444    $copy->dummy_author($self->dummy_author || "");
1445
1446     unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1447         $self->bail_out(1);
1448         $self->push_events($self->editor->event);
1449         return;
1450     }   
1451
1452     # this is a little bit of a hack, but we need to 
1453     # get the copy into the script runner
1454     $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1455 }
1456
1457
1458 sub checkout_noncat {
1459     my $self = shift;
1460
1461     my $circ;
1462     my $evt;
1463
1464    my $lib      = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1465    my $count    = $self->noncat_count || 1;
1466    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1467
1468    $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1469
1470    for(1..$count) {
1471
1472       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1473          $self->editor->requestor->id, 
1474             $self->patron->id, 
1475             $lib, 
1476             $self->noncat_type, 
1477             $cotime,
1478             $self->editor );
1479
1480         if( $evt ) {
1481             $self->push_events($evt);
1482             $self->bail_out(1);
1483             return; 
1484         }
1485         $self->circ($circ);
1486    }
1487 }
1488
1489
1490 sub do_checkin {
1491     my $self = shift;
1492     $self->log_me("do_checkin()");
1493
1494
1495     return $self->bail_on_events(
1496         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
1497         unless $self->copy;
1498
1499     if( $self->checkin_check_holds_shelf() ) {
1500         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1501         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1502         $self->checkin_flesh_events;
1503         return;
1504     }
1505
1506     unless( $self->is_renewal ) {
1507         return $self->bail_on_events($self->editor->event)
1508             unless $self->editor->allowed('COPY_CHECKIN');
1509     }
1510
1511     $self->push_events($self->check_copy_alert());
1512     $self->push_events($self->check_checkin_copy_status());
1513
1514     # the renew code will have already found our circulation object
1515     unless( $self->is_renewal and $self->circ ) {
1516         my $circs = $self->editor->search_action_circulation(
1517             { target_copy => $self->copy->id, checkin_time => undef });
1518         $self->circ($$circs[0]);
1519
1520         # for now, just warn if there are multiple open circs on a copy
1521         $logger->warn("circulator: we have ".scalar(@$circs).
1522             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1523     }
1524
1525     # if the circ is marked as 'claims returned', add the event to the list
1526     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1527         if ($self->circ and $self->circ->stop_fines 
1528                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1529
1530     # handle the overridable events 
1531     $self->override_events unless $self->is_renewal;
1532     return if $self->bail_out;
1533     
1534     if( $self->copy ) {
1535         $self->transit(
1536             $self->editor->search_action_transit_copy(
1537             { target_copy => $self->copy->id, dest_recv_time => undef })->[0]); 
1538     }
1539
1540     if( $self->circ ) {
1541         $self->checkin_handle_circ;
1542         return if $self->bail_out;
1543         $self->checkin_changed(1);
1544
1545     } elsif( $self->transit ) {
1546         my $hold_transit = $self->process_received_transit;
1547         $self->checkin_changed(1);
1548
1549         if( $self->bail_out ) { 
1550             $self->checkin_flesh_events;
1551             return;
1552         }
1553         
1554         if( my $e = $self->check_checkin_copy_status() ) {
1555             # If the original copy status is special, alert the caller
1556             my $ev = $self->events;
1557             $self->events([$e]);
1558             $self->override_events;
1559             return if $self->bail_out;
1560             $self->events($ev);
1561         }
1562
1563         if( $hold_transit or 
1564                 $U->copy_status($self->copy->status)->id 
1565                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1566
1567          my $hold;
1568          if( $hold_transit ) {
1569             $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1570          } else {
1571                 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1572          }
1573
1574             $self->hold($hold);
1575
1576             if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1577
1578                 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1579                 $self->reshelve_copy(1);
1580                 $self->cancelled_hold_transit(1);
1581                 $self->notify_hold(0); # don't notify for cancelled holds
1582                 return if $self->bail_out;
1583
1584             } else {
1585
1586                 # hold transited to correct location
1587                 $self->checkin_flesh_events;
1588                 return;
1589             }
1590         } 
1591
1592     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1593
1594         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1595             " that is in-transit, but there is no transit.. repairing");
1596         $self->reshelve_copy(1);
1597         return if $self->bail_out;
1598     }
1599
1600     if( $self->is_renewal ) {
1601         $self->push_events(OpenILS::Event->new('SUCCESS'));
1602         return;
1603     }
1604
1605    # ------------------------------------------------------------------------------
1606    # Circulations and transits are now closed where necessary.  Now go on to see if
1607    # this copy can fulfill a hold or needs to be routed to a different location
1608    # ------------------------------------------------------------------------------
1609
1610     if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1611         return if $self->bail_out;
1612
1613    } else { # not needed for a hold
1614
1615         my $circ_lib = (ref $self->copy->circ_lib) ? 
1616                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1617
1618         if( $self->remote_hold ) {
1619             $circ_lib = $self->remote_hold->pickup_lib;
1620             $logger->warn("circulator: Copy ".$self->copy->barcode.
1621                 " is on a remote hold's shelf, sending to $circ_lib");
1622         }
1623
1624         $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1625
1626       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1627
1628             $self->checkin_handle_precat();
1629             return if $self->bail_out;
1630
1631       } else {
1632
1633             my $bc = $self->copy->barcode;
1634             $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1635             $self->checkin_build_copy_transit($circ_lib);
1636             return if $self->bail_out;
1637             $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1638       }
1639    }
1640
1641     $self->reshelve_copy;
1642     return if $self->bail_out;
1643
1644     unless($self->checkin_changed) {
1645
1646         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1647         my $stat = $U->copy_status($self->copy->status)->id;
1648
1649         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1650          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1651         $self->bail_out(1); # no need to commit anything
1652
1653     } else {
1654
1655         $self->push_events(OpenILS::Event->new('SUCCESS')) 
1656             unless @{$self->events};
1657     }
1658
1659
1660    # ------------------------------------------------------------------------------
1661    # Update the patron penalty info in the DB
1662    # ------------------------------------------------------------------------------
1663    $U->update_patron_penalties(
1664       authtoken => $self->editor->authtoken,
1665       patron    => $self->patron,
1666       background  => 1 ) if $self->is_checkin;
1667
1668     $self->checkin_flesh_events;
1669     return;
1670 }
1671
1672 sub reshelve_copy {
1673    my $self    = shift;
1674    my $force   = $self->force || shift;
1675    my $copy    = $self->copy;
1676
1677    my $stat = $U->copy_status($copy->status)->id;
1678
1679    if($force || (
1680       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1681       $stat != OILS_COPY_STATUS_CATALOGING and
1682       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1683       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1684
1685         $copy->status( OILS_COPY_STATUS_RESHELVING );
1686             $self->update_copy;
1687             $self->checkin_changed(1);
1688     }
1689 }
1690
1691
1692 # Returns true if the item is at the current location
1693 # because it was transited there for a hold and the 
1694 # hold has not been fulfilled
1695 sub checkin_check_holds_shelf {
1696     my $self = shift;
1697     return 0 unless $self->copy;
1698
1699     return 0 unless 
1700         $U->copy_status($self->copy->status)->id ==
1701             OILS_COPY_STATUS_ON_HOLDS_SHELF;
1702
1703     # find the hold that put us on the holds shelf
1704     my $holds = $self->editor->search_action_hold_request(
1705         { 
1706             current_copy => $self->copy->id,
1707             capture_time => { '!=' => undef },
1708             fulfillment_time => undef,
1709             cancel_time => undef,
1710         }
1711     );
1712
1713     unless(@$holds) {
1714         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1715         $self->reshelve_copy(1);
1716         return 0;
1717     }
1718
1719     my $hold = $$holds[0];
1720
1721     $logger->info("circulator: we found a captured, un-fulfilled hold [".
1722         $hold->id. "] for copy ".$self->copy->barcode);
1723
1724     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1725         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1726         return 1;
1727     }
1728
1729     $logger->info("circulator: hold is not for here..");
1730     $self->remote_hold($hold);
1731     return 0;
1732 }
1733
1734
1735 sub checkin_handle_precat {
1736     my $self    = shift;
1737    my $copy    = $self->copy;
1738
1739    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1740       $copy->status(OILS_COPY_STATUS_CATALOGING);
1741         $self->update_copy();
1742         $self->checkin_changed(1);
1743         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1744    }
1745 }
1746
1747
1748 sub checkin_build_copy_transit {
1749     my $self            = shift;
1750     my $dest            = shift;
1751     my $copy       = $self->copy;
1752    my $transit    = Fieldmapper::action::transit_copy->new;
1753
1754     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1755     $logger->info("circulator: transiting copy to $dest");
1756
1757    $transit->source($self->editor->requestor->ws_ou);
1758    $transit->dest($dest);
1759    $transit->target_copy($copy->id);
1760    $transit->source_send_time('now');
1761    $transit->copy_status( $U->copy_status($copy->status)->id );
1762
1763     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1764
1765     return $self->bail_on_events($self->editor->event)
1766         unless $self->editor->create_action_transit_copy($transit);
1767
1768    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1769     $self->update_copy;
1770     $self->checkin_changed(1);
1771 }
1772
1773
1774 sub attempt_checkin_hold_capture {
1775     my $self = shift;
1776     my $copy = $self->copy;
1777
1778     # See if this copy can fulfill any holds
1779     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
1780         $self->editor, $copy, $self->editor->requestor );
1781
1782     if(!$hold) {
1783         $logger->debug("circulator: no potential permitted".
1784             "holds found for copy ".$copy->barcode);
1785         return undef;
1786     }
1787
1788     $self->retarget($retarget);
1789
1790     $logger->info("circulator: found permitted hold ".
1791         $hold->id . " for copy, capturing...");
1792
1793     $hold->current_copy($copy->id);
1794     $hold->capture_time('now');
1795
1796     # prevent DB errors caused by fetching 
1797     # holds from storage, and updating through cstore
1798     $hold->clear_fulfillment_time;
1799     $hold->clear_fulfillment_staff;
1800     $hold->clear_fulfillment_lib;
1801     $hold->clear_expire_time; 
1802     $hold->clear_cancel_time;
1803     $hold->clear_prev_check_time unless $hold->prev_check_time;
1804
1805     $self->bail_on_events($self->editor->event)
1806         unless $self->editor->update_action_hold_request($hold);
1807     $self->hold($hold);
1808     $self->checkin_changed(1);
1809
1810     return 1 if $self->bail_out;
1811
1812     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1813
1814         # This hold was captured in the correct location
1815     $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1816         $self->push_events(OpenILS::Event->new('SUCCESS'));
1817
1818         #$self->do_hold_notify($hold->id);
1819         $self->notify_hold($hold->id);
1820
1821     } else {
1822     
1823         # Hold needs to be picked up elsewhere.  Build a hold
1824         # transit and route the item.
1825         $self->checkin_build_hold_transit();
1826     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1827         return 1 if $self->bail_out;
1828         $self->push_events(
1829             OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1830     }
1831
1832     # make sure we save the copy status
1833     $self->update_copy;
1834     return 1;
1835 }
1836
1837 sub do_hold_notify {
1838     my( $self, $holdid ) = @_;
1839
1840     $logger->info("circulator: running delayed hold notify process");
1841
1842 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1843 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1844
1845     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1846         hold_id => $holdid, requestor => $self->editor->requestor);
1847
1848     $logger->debug("circulator: built hold notifier");
1849
1850     if(!$notifier->event) {
1851
1852         $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1853
1854         my $stat = $notifier->send_email_notify;
1855         if( $stat == '1' ) {
1856             $logger->info("ciculator: hold notify succeeded for hold $holdid");
1857             return;
1858         } 
1859
1860         $logger->warn("ciculator:  * hold notify failed for hold $holdid");
1861
1862     } else {
1863         $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1864     }
1865 }
1866
1867 sub retarget_holds {
1868     $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1869     my $ses = OpenSRF::AppSession->create('open-ils.storage');
1870     $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1871     # no reason to wait for the return value
1872     return;
1873 }
1874
1875 sub checkin_build_hold_transit {
1876     my $self = shift;
1877
1878    my $copy = $self->copy;
1879    my $hold = $self->hold;
1880    my $trans = Fieldmapper::action::hold_transit_copy->new;
1881
1882     $logger->debug("circulator: building hold transit for ".$copy->barcode);
1883
1884    $trans->hold($hold->id);
1885    $trans->source($self->editor->requestor->ws_ou);
1886    $trans->dest($hold->pickup_lib);
1887    $trans->source_send_time("now");
1888    $trans->target_copy($copy->id);
1889
1890     # when the copy gets to its destination, it will recover
1891     # this status - put it onto the holds shelf
1892    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1893
1894     return $self->bail_on_events($self->editor->event)
1895         unless $self->editor->create_action_hold_transit_copy($trans);
1896 }
1897
1898
1899
1900 sub process_received_transit {
1901     my $self = shift;
1902     my $copy = $self->copy;
1903     my $copyid = $self->copy->id;
1904
1905     my $status_name = $U->copy_status($copy->status)->name;
1906     $logger->debug("circulator: attempting transit receive on ".
1907         "copy $copyid. Copy status is $status_name");
1908
1909     my $transit = $self->transit;
1910
1911     if( $transit->dest != $self->editor->requestor->ws_ou ) {
1912         # - this item is in-transit to a different location
1913
1914         my $tid = $transit->id; 
1915         my $loc = $self->editor->requestor->ws_ou;
1916         my $dest = $transit->dest;
1917
1918         $logger->info("circulator: Fowarding transit on copy which is destined ".
1919             "for a different location. transit=$tid, copy=$copyid, current ".
1920             "location=$loc, destination location=$dest");
1921
1922         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
1923
1924         # grab the associated hold object if available
1925         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
1926         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
1927
1928         return $self->bail_on_events($evt);
1929     }
1930
1931    # The transit is received, set the receive time
1932    $transit->dest_recv_time('now');
1933     $self->bail_on_events($self->editor->event)
1934         unless $self->editor->update_action_transit_copy($transit);
1935
1936     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1937
1938    $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
1939    $copy->status( $transit->copy_status );
1940     $self->update_copy();
1941     return if $self->bail_out;
1942
1943     my $ishold = 0;
1944     if($hold_transit) { 
1945         #$self->do_hold_notify($hold_transit->hold);
1946         $self->notify_hold($hold_transit->hold);
1947         $ishold = 1;
1948     }
1949
1950     $self->push_events( 
1951         OpenILS::Event->new(
1952         'SUCCESS', 
1953         ishold => $ishold,
1954       payload => { transit => $transit, holdtransit => $hold_transit } ));
1955
1956     return $hold_transit;
1957 }
1958
1959
1960 sub checkin_handle_circ {
1961    my $self = shift;
1962     $U->logmark;
1963
1964    my $circ = $self->circ;
1965    my $copy = $self->copy;
1966    my $evt;
1967    my $obt;
1968
1969    # backdate the circ if necessary
1970    if($self->backdate) {
1971         $self->checkin_handle_backdate;
1972         return if $self->bail_out;
1973    }
1974
1975    if(!$circ->stop_fines) {
1976       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1977       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1978       $circ->stop_fines_time('now') unless $self->backdate;
1979       $circ->stop_fines_time($self->backdate) if $self->backdate;
1980    }
1981
1982    # see if there are any fines owed on this circ.  if not, close it
1983     ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1984    $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1985
1986     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1987
1988    # Set the checkin vars since we have the item
1989     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
1990
1991    $circ->checkin_staff($self->editor->requestor->id);
1992    $circ->checkin_lib($self->editor->requestor->ws_ou);
1993
1994     my $circ_lib = (ref $self->copy->circ_lib) ?  
1995         $self->copy->circ_lib->id : $self->copy->circ_lib;
1996     my $stat = $U->copy_status($self->copy->status)->id;
1997
1998     # If the item is lost/missing and it needs to be sent home, don't 
1999     # reshelve the copy, leave it lost/missing so the recipient will know
2000     if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
2001         and ($circ_lib != $self->editor->requestor->ws_ou) ) {
2002         $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2003
2004     } else {
2005         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2006         $self->update_copy;
2007     }
2008
2009     return $self->bail_on_events($self->editor->event)
2010         unless $self->editor->update_action_circulation($circ);
2011 }
2012
2013
2014 sub checkin_handle_backdate {
2015     my $self = shift;
2016
2017     my $bd = $self->backdate;
2018
2019     # ------------------------------------------------------------------
2020     # clean up the backdate for date comparison
2021     # we want any bills created on or after the backdate
2022     # ------------------------------------------------------------------
2023     $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2024     #$bd = "${bd}T23:59:59";
2025
2026     my $bills = $self->editor->search_money_billing(
2027         { 
2028             billing_ts => { '>=' => $bd }, 
2029             xact => $self->circ->id, 
2030             billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2031         }
2032     );
2033
2034     $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2035
2036     for my $bill (@$bills) {    
2037         unless( $U->is_true($bill->voided) ) {
2038             $logger->info("backdate voiding bill ".$bill->id);
2039             $bill->voided('t');
2040             $bill->void_time('now');
2041             $bill->voider($self->editor->requestor->id);
2042             my $n = $bill->note || "";
2043             $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2044
2045             $self->bail_on_events($self->editor->event)
2046                 unless $self->editor->update_money_billing($bill);
2047         }
2048     }
2049 }
2050
2051
2052
2053 =head
2054 # XXX Legacy version for Circ.pm support
2055 sub _checkin_handle_backdate {
2056    my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
2057
2058     my $bd = $backdate;
2059     $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2060     $bd = "${bd}T23:59:59";
2061
2062    my $bills = $session->request(
2063       "open-ils.storage.direct.money.billing.search_where.atomic",
2064         billing_ts => { '>=' => $bd }, 
2065         xact => $circ->id,
2066         billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2067     )->gather(1);
2068
2069     $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2070
2071    if($bills) {
2072       for my $bill (@$bills) {
2073             unless( $U->is_true($bill->voided) ) {
2074                 $logger->debug("voiding bill ".$bill->id);
2075                 $bill->voided('t');
2076                 $bill->void_time('now');
2077                 $bill->voider($requestor->id);
2078                 my $n = $bill->note || "";
2079                 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
2080                 my $s = $session->request(
2081                     "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
2082                 return $U->DB_UPDATE_FAILED($bill) unless $s;
2083             }
2084         }
2085    }
2086
2087     return 100;
2088 }
2089 =cut
2090
2091
2092
2093
2094
2095
2096 sub find_patron_from_copy {
2097     my $self = shift;
2098     my $circs = $self->editor->search_action_circulation(
2099         { target_copy => $self->copy->id, checkin_time => undef });
2100     my $circ = $circs->[0];
2101     return unless $circ;
2102     my $u = $self->editor->retrieve_actor_user($circ->usr)
2103         or return $self->bail_on_events($self->editor->event);
2104     $self->patron($u);
2105 }
2106
2107 sub check_checkin_copy_status {
2108     my $self = shift;
2109    my $copy = $self->copy;
2110
2111    my $islost     = 0;
2112    my $ismissing  = 0;
2113    my $evt        = undef;
2114
2115    my $status = $U->copy_status($copy->status)->id;
2116
2117    return undef
2118       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
2119             $status == OILS_COPY_STATUS_CHECKED_OUT ||
2120             $status == OILS_COPY_STATUS_IN_PROCESS  ||
2121             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
2122             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
2123             $status == OILS_COPY_STATUS_CATALOGING  ||
2124             $status == OILS_COPY_STATUS_RESHELVING );
2125
2126    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2127       if( $status == OILS_COPY_STATUS_LOST );
2128
2129    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2130       if( $status == OILS_COPY_STATUS_MISSING );
2131
2132    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2133 }
2134
2135
2136
2137 # --------------------------------------------------------------------------
2138 # On checkin, we need to return as many relevant objects as we can
2139 # --------------------------------------------------------------------------
2140 sub checkin_flesh_events {
2141     my $self = shift;
2142
2143     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
2144         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2145             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2146     }
2147
2148
2149     for my $evt (@{$self->events}) {
2150
2151         my $payload          = {};
2152         $payload->{copy}     = $U->unflesh_copy($self->copy);
2153         $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2154         $payload->{circ}     = $self->circ;
2155         $payload->{transit}  = $self->transit;
2156         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2157
2158         # $self->hold may or may not have been replaced with a 
2159         # valid hold after processing a cancelled hold
2160         $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
2161
2162         $evt->{payload} = $payload;
2163     }
2164 }
2165
2166 sub log_me {
2167     my( $self, $msg ) = @_;
2168     my $bc = ($self->copy) ? $self->copy->barcode :
2169         $self->barcode;
2170     $bc ||= "";
2171     my $usr = ($self->patron) ? $self->patron->id : "";
2172     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2173         ", recipient=$usr, copy=$bc");
2174 }
2175
2176
2177 sub do_renew {
2178     my $self = shift;
2179     $self->log_me("do_renew()");
2180     $self->is_renewal(1);
2181
2182     # Make sure there is an open circ to renew that is not
2183     # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2184     my $circ = $self->editor->search_action_circulation(
2185             { target_copy => $self->copy->id, stop_fines => undef } )->[0];
2186
2187     if(!$circ) {
2188         $circ = $self->editor->search_action_circulation(
2189             { 
2190                 target_copy => $self->copy->id, 
2191                 stop_fines => OILS_STOP_FINES_MAX_FINES,
2192                 checkin_time => undef
2193             } 
2194         )->[0];
2195     }
2196
2197     return $self->bail_on_events($self->editor->event) unless $circ;
2198
2199     # A user is not allowed to renew another user's items without permission
2200     unless( $circ->usr eq $self->editor->requestor->id ) {
2201         return $self->bail_on_events($self->editor->events)
2202             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2203     }   
2204
2205     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2206         if $circ->renewal_remaining < 1;
2207
2208     # -----------------------------------------------------------------
2209
2210     $self->renewal_remaining( $circ->renewal_remaining - 1 );
2211     $self->circ($circ);
2212
2213     $self->run_renew_permit;
2214
2215     # Check the item in
2216     $self->do_checkin();
2217     return if $self->bail_out;
2218
2219     unless( $self->permit_override ) {
2220         $self->do_permit();
2221         return if $self->bail_out;
2222         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2223         $self->remove_event('ITEM_NOT_CATALOGED');
2224     }   
2225
2226     $self->override_events;
2227     return if $self->bail_out;
2228
2229     $self->events([]);
2230     $self->do_checkout();
2231 }
2232
2233
2234 sub remove_event {
2235     my( $self, $evt ) = @_;
2236     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2237     $logger->debug("circulator: removing event from list: $evt");
2238     my @events = @{$self->events};
2239     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2240 }
2241
2242
2243 sub have_event {
2244     my( $self, $evt ) = @_;
2245     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2246     return grep { $_->{textcode} eq $evt } @{$self->events};
2247 }
2248
2249
2250
2251 sub run_renew_permit {
2252     my $self = shift;
2253
2254     my $events = [];
2255
2256     if(!$self->legacy_script_support) {
2257         my $results = $self->run_indb_circ_test;
2258         unless($self->circ_test_success) {
2259             push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) for @$results;
2260         }
2261
2262     } else {
2263
2264         my $runner = $self->script_runner;
2265
2266         $runner->load($self->circ_permit_renew);
2267         my $result = $runner->run or 
2268             throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2269         my $events = $result->{events};
2270     }
2271
2272     $logger->activity("ciculator: circ_permit_renew for user ".
2273       $self->patron->id." returned events: @$events") if @$events;
2274
2275     $self->push_events(OpenILS::Event->new($_)) for @$events;
2276
2277     $logger->debug("circulator: re-creating script runner to be safe");
2278     #$self->mk_script_runner;
2279 }
2280
2281
2282
2283