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