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