]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
needed an extra bit of logic to tell checkout.full not to treat a precat as a stop...
[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
948         my $ol = ($self->is_precat) ? 
949             $self->editor->requestor->ws_ou : $self->volume->owning_lib;
950
951         my $default_price = $U->ou_ancestor_setting_value(
952             $ol, OILS_SETTING_DEF_ITEM_PRICE, $self->editor) || 0;
953         my $charge_on_0 = $U->ou_ancestor_setting_value(
954             $ol, OILS_SETTING_CHARGE_LOST_ON_ZERO, $self->editor) || 0;
955
956         # Find the most appropriate "price" -- same definition as the
957         # LOST price.  See OpenILS::Circ::new_set_circ_lost
958         $max_amount = $self->copy->price;
959         $max_amount = $default_price unless defined $max_amount;
960         $max_amount = 0 if $max_amount < 0;
961         $max_amount = $default_price if $max_amount == 0 and $charge_on_0;
962
963         $max_amount *= $max_fine_rule->amount / 100;
964     }  
965
966     return $max_amount;
967 }
968
969
970
971 sub run_copy_permit_scripts {
972     my $self = shift;
973     my $copy = $self->copy || return;
974     my $runner = $self->script_runner;
975
976     my @allevents;
977
978     if(!$self->legacy_script_support) {
979         my $results = $self->run_indb_circ_test;
980         unless($self->circ_test_success) {
981             push(@allevents, OpenILS::Event->new(
982                 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
983         }
984     } else {
985     
986        # ---------------------------------------------------------------------
987        # Capture all of the copy permit events
988        # ---------------------------------------------------------------------
989        $runner->load($self->circ_permit_copy);
990        my $result = $runner->run or 
991             throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
992        my $copy_events = $result->{events};
993
994        # ---------------------------------------------------------------------
995        # Now collect all of the events together
996        # ---------------------------------------------------------------------
997        push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
998     }
999
1000     # See if this copy has an alert message
1001     my $ae = $self->check_copy_alert();
1002     push( @allevents, $ae ) if $ae;
1003
1004    # uniquify the events
1005    my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1006    @allevents = values %hash;
1007
1008    for (@allevents) {
1009       $_->{payload} = $copy if 
1010             ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1011    }
1012
1013     $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1014
1015     $self->push_events(@allevents);
1016 }
1017
1018
1019 sub check_copy_alert {
1020     my $self = shift;
1021     return undef if $self->is_renewal;
1022     return OpenILS::Event->new(
1023         'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1024         if $self->copy and $self->copy->alert_message;
1025     return undef;
1026 }
1027
1028
1029
1030 # --------------------------------------------------------------------------
1031 # If the call is overriding and has permissions to override every collected
1032 # event, the are cleared.  Any event that the caller does not have
1033 # permission to override, will be left in the event list and bail_out will
1034 # be set
1035 # XXX We need code in here to cancel any holds/transits on copies 
1036 # that are being force-checked out
1037 # --------------------------------------------------------------------------
1038 sub override_events {
1039     my $self = shift;
1040     my @events = @{$self->events};
1041     return unless @events;
1042
1043     if(!$self->override) {
1044         return $self->bail_out(1) 
1045             if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1046     }   
1047
1048     $self->events([]);
1049     
1050    for my $e (@events) {
1051       my $tc = $e->{textcode};
1052       next if $tc eq 'SUCCESS';
1053       my $ov = "$tc.override";
1054       $logger->info("circulator: attempting to override event: $ov");
1055
1056         return $self->bail_on_events($self->editor->event)
1057             unless( $self->editor->allowed($ov) );
1058    }
1059 }
1060     
1061
1062 # --------------------------------------------------------------------------
1063 # If there is an open claimsreturn circ on the requested copy, close the 
1064 # circ if overriding, otherwise bail out
1065 # --------------------------------------------------------------------------
1066 sub handle_claims_returned {
1067     my $self = shift;
1068     my $copy = $self->copy;
1069
1070     my $CR = $self->editor->search_action_circulation(
1071         {   
1072             target_copy     => $copy->id,
1073             stop_fines      => OILS_STOP_FINES_CLAIMSRETURNED,
1074             checkin_time    => undef,
1075         }
1076     );
1077
1078     return unless ($CR = $CR->[0]); 
1079
1080     my $evt;
1081
1082     # - If the caller has set the override flag, we will check the item in
1083     if($self->override) {
1084
1085         $CR->checkin_time('now');   
1086         $CR->checkin_lib($self->editor->requestor->ws_ou);
1087         $CR->checkin_staff($self->editor->requestor->id);
1088
1089         $evt = $self->editor->event 
1090             unless $self->editor->update_action_circulation($CR);
1091
1092     } else {
1093         $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1094     }
1095
1096     $self->bail_on_events($evt) if $evt;
1097     return;
1098 }
1099
1100
1101 # --------------------------------------------------------------------------
1102 # This performs the checkout
1103 # --------------------------------------------------------------------------
1104 sub do_checkout {
1105     my $self = shift;
1106
1107     $self->log_me("do_checkout()");
1108
1109     # make sure perms are good if this isn't a renewal
1110     unless( $self->is_renewal ) {
1111         return $self->bail_on_events($self->editor->event)
1112             unless( $self->editor->allowed('COPY_CHECKOUT') );
1113     }
1114
1115     # verify the permit key
1116     unless( $self->check_permit_key ) {
1117         if( $self->permit_override ) {
1118             return $self->bail_on_events($self->editor->event)
1119                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1120         } else {
1121             return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1122         }   
1123     }
1124
1125     # if this is a non-cataloged circ, build the circ and finish
1126     if( $self->is_noncat ) {
1127         $self->checkout_noncat;
1128         $self->push_events(
1129             OpenILS::Event->new('SUCCESS', 
1130             payload => { noncat_circ => $self->circ }));
1131         return;
1132     }
1133
1134     if( $self->is_precat ) {
1135         #$self->script_runner->insert("environment.isPrecat", 1, 1)
1136         $self->make_precat_copy;
1137         return if $self->bail_out;
1138
1139     } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1140         return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1141     }
1142
1143     $self->do_copy_checks;
1144     return if $self->bail_out;
1145
1146     $self->run_checkout_scripts();
1147     return if $self->bail_out;
1148
1149     $self->build_checkout_circ_object();
1150     return if $self->bail_out;
1151
1152     $self->apply_modified_due_date();
1153     return if $self->bail_out;
1154
1155     return $self->bail_on_events($self->editor->event)
1156         unless $self->editor->create_action_circulation($self->circ);
1157
1158     # refresh the circ to force local time zone for now
1159     $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1160
1161     $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1162     $self->update_copy;
1163     return if $self->bail_out;
1164
1165     $self->apply_deposit_fee();
1166     return if $self->bail_out;
1167
1168     $self->handle_checkout_holds();
1169     return if $self->bail_out;
1170
1171    # ------------------------------------------------------------------------------
1172    # Update the patron penalty info in the DB.  Run it for permit-overrides or
1173     # renewals since both of those cases do not require the penalty server to
1174     # run during the permit phase of the checkout
1175    # ------------------------------------------------------------------------------
1176     if( $self->permit_override or $self->is_renewal ) {
1177         $U->update_patron_penalties(
1178             authtoken => $self->editor->authtoken,
1179             patron    => $self->patron,
1180             background  => 1,
1181         );
1182     }
1183
1184     my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1185     $self->push_events(
1186         OpenILS::Event->new('SUCCESS',
1187             payload  => {
1188                 copy              => $U->unflesh_copy($self->copy),
1189                 circ              => $self->circ,
1190                 record            => $record,
1191                 holds_fulfilled   => $self->fulfilled_holds,
1192                 deposit_billing      => $self->deposit_billing,
1193                 rental_billing       => $self->rental_billing
1194             }
1195         )
1196     );
1197 }
1198
1199 sub apply_deposit_fee {
1200     my $self = shift;
1201     my $copy = $self->copy;
1202     return unless $self->is_deposit or $self->is_rental;
1203
1204         my $bill = Fieldmapper::money::billing->new;
1205     my $amount = $copy->deposit_amount;
1206     my $billing_type;
1207
1208     if($self->is_deposit) {
1209         $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1210         $self->deposit_billing($bill);
1211     } else {
1212         $billing_type = OILS_BILLING_TYPE_RENTAL;
1213         $self->rental_billing($bill);
1214     }
1215
1216         $bill->xact($self->circ->id);
1217         $bill->amount($amount);
1218         $bill->note(OILS_BILLING_NOTE_SYSTEM);
1219         $bill->billing_type($billing_type);
1220     $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1221
1222         $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1223 }
1224
1225 sub update_copy {
1226     my $self = shift;
1227     my $copy = $self->copy;
1228
1229     my $stat = $copy->status if ref $copy->status;
1230     my $loc = $copy->location if ref $copy->location;
1231     my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1232
1233     $copy->status($stat->id) if $stat;
1234     $copy->location($loc->id) if $loc;
1235     $copy->circ_lib($circ_lib->id) if $circ_lib;
1236     $copy->editor($self->editor->requestor->id);
1237     $copy->edit_date('now');
1238     $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1239
1240     return $self->bail_on_events($self->editor->event)
1241         unless $self->editor->update_asset_copy($self->copy);
1242
1243     $copy->status($U->copy_status($copy->status));
1244     $copy->location($loc) if $loc;
1245     $copy->circ_lib($circ_lib) if $circ_lib;
1246 }
1247
1248
1249 sub bail_on_events {
1250     my( $self, @evts ) = @_;
1251     $self->push_events(@evts);
1252     $self->bail_out(1);
1253 }
1254
1255 sub handle_checkout_holds {
1256    my $self    = shift;
1257
1258    my $copy    = $self->copy;
1259    my $patron  = $self->patron;
1260
1261     my $holds   = $self->editor->search_action_hold_request(
1262         { 
1263             current_copy        => $copy->id , 
1264             cancel_time         => undef, 
1265             fulfillment_time    => undef 
1266         }
1267     );
1268
1269    my @fulfilled;
1270
1271    # XXX We should only fulfill one hold here...
1272    # XXX If a hold was transited to the user who is checking out
1273    # the item, we need to make sure that hold is what's grabbed
1274    if(@$holds) {
1275
1276       # for now, just sort by id to get what should be the oldest hold
1277       $holds = [ sort { $a->id <=> $b->id } @$holds ];
1278       my @myholds = grep { $_->usr eq $patron->id } @$holds;
1279       my @altholds   = grep { $_->usr ne $patron->id } @$holds;
1280
1281       if(@myholds) {
1282          my $hold = $myholds[0];
1283
1284          $logger->debug("circulator: related hold found in checkout: " . $hold->id );
1285
1286          # if the hold was never officially captured, capture it.
1287          $hold->capture_time('now') unless $hold->capture_time;
1288
1289             # just make sure it's set correctly
1290          $hold->current_copy($copy->id); 
1291
1292          $hold->fulfillment_time('now');
1293             $hold->fulfillment_staff($self->editor->requestor->id);
1294             $hold->fulfillment_lib($self->editor->requestor->ws_ou);
1295
1296             return $self->bail_on_events($self->editor->event)
1297                 unless $self->editor->update_action_hold_request($hold);
1298
1299             $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
1300
1301          push( @fulfilled, $hold->id );
1302       }
1303
1304       # If there are any holds placed for other users that point to this copy,
1305       # then we need to un-target those holds so the targeter can pick a new copy
1306       for(@altholds) {
1307
1308          $logger->info("circulator: un-targeting hold ".$_->id.
1309             " because copy ".$copy->id." is getting checked out");
1310
1311             # - make the targeter process this hold at next run
1312          $_->clear_prev_check_time; 
1313
1314             # - clear out the targetted copy
1315          $_->clear_current_copy;
1316          $_->clear_capture_time;
1317
1318             return $self->bail_on_event($self->editor->event)
1319                 unless $self->editor->update_action_hold_request($_);
1320       }
1321    }
1322
1323     $self->fulfilled_holds(\@fulfilled);
1324 }
1325
1326
1327
1328 sub run_checkout_scripts {
1329     my $self = shift;
1330
1331     my $evt;
1332     my $runner = $self->script_runner;
1333
1334     my $duration;
1335     my $recurring;
1336     my $max_fine;
1337     my $duration_name;
1338     my $recurring_name;
1339     my $max_fine_name;
1340
1341     if(!$self->legacy_script_support) {
1342         $self->run_indb_circ_test();
1343         $duration = $self->circ_matrix_ruleset->duration_rule;
1344         $recurring = $self->circ_matrix_ruleset->recurring_fine_rule;
1345         $max_fine = $self->circ_matrix_ruleset->max_fine_rule;
1346
1347     } else {
1348
1349        $runner->load($self->circ_duration);
1350
1351        my $result = $runner->run or 
1352             throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1353
1354        $duration_name   = $result->{durationRule};
1355        $recurring_name  = $result->{recurringFinesRule};
1356        $max_fine_name   = $result->{maxFine};
1357     }
1358
1359     $duration_name = $duration->name if $duration;
1360     if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1361
1362         unless($duration) {
1363             ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1364             return $self->bail_on_events($evt) if $evt;
1365         
1366             ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1367             return $self->bail_on_events($evt) if $evt;
1368         
1369             ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1370             return $self->bail_on_events($evt) if $evt;
1371         }
1372
1373     } else {
1374
1375         # The item circulates with an unlimited duration
1376         $duration   = undef;
1377         $recurring  = undef;
1378         $max_fine   = undef;
1379     }
1380
1381    $self->duration_rule($duration);
1382    $self->recurring_fines_rule($recurring);
1383    $self->max_fine_rule($max_fine);
1384 }
1385
1386
1387 sub build_checkout_circ_object {
1388     my $self = shift;
1389
1390    my $circ       = Fieldmapper::action::circulation->new;
1391    my $duration   = $self->duration_rule;
1392    my $max        = $self->max_fine_rule;
1393    my $recurring  = $self->recurring_fines_rule;
1394    my $copy       = $self->copy;
1395    my $patron     = $self->patron;
1396
1397     if( $duration ) {
1398
1399         my $policy = $self->get_circ_policy($duration, $recurring, $max);
1400
1401         my $dname = $duration->name;
1402         my $mname = $max->name;
1403         my $rname = $recurring->name;
1404
1405         $logger->debug("circulator: building circulation ".
1406             "with duration=$dname, maxfine=$mname, recurring=$rname");
1407     
1408         $circ->duration($policy->{duration});
1409         $circ->recuring_fine($policy->{recurring_fine});
1410         $circ->duration_rule($duration->name);
1411         $circ->recuring_fine_rule($recurring->name);
1412         $circ->max_fine_rule($max->name);
1413         $circ->max_fine($policy->{max_fine});
1414         $circ->fine_interval($recurring->recurance_interval);
1415         $circ->renewal_remaining($duration->max_renewals);
1416
1417     } else {
1418
1419         $logger->info("circulator: copy found with an unlimited circ duration");
1420         $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1421         $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1422         $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1423         $circ->renewal_remaining(0);
1424     }
1425
1426    $circ->target_copy( $copy->id );
1427    $circ->usr( $patron->id );
1428    $circ->circ_lib( $self->circ_lib );
1429
1430    if( $self->is_renewal ) {
1431       $circ->opac_renewal('t') if $self->opac_renewal;
1432       $circ->phone_renewal('t') if $self->phone_renewal;
1433       $circ->desk_renewal('t') if $self->desk_renewal;
1434       $circ->renewal_remaining($self->renewal_remaining);
1435       $circ->circ_staff($self->editor->requestor->id);
1436    }
1437
1438
1439    # if the user provided an overiding checkout time,
1440    # (e.g. the checkout really happened several hours ago), then
1441    # we apply that here.  Does this need a perm??
1442     $circ->xact_start(clense_ISO8601($self->checkout_time))
1443         if $self->checkout_time;
1444
1445    # if a patron is renewing, 'requestor' will be the patron
1446    $circ->circ_staff($self->editor->requestor->id);
1447     $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1448
1449     $self->circ($circ);
1450 }
1451
1452
1453 sub apply_modified_due_date {
1454     my $self = shift;
1455     my $circ = $self->circ;
1456     my $copy = $self->copy;
1457
1458    if( $self->due_date ) {
1459
1460         return $self->bail_on_events($self->editor->event)
1461             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1462
1463       $circ->due_date(clense_ISO8601($self->due_date));
1464
1465    } else {
1466
1467       # if the due_date lands on a day when the location is closed
1468       return unless $copy and $circ->due_date;
1469
1470         #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1471
1472         # due-date overlap should be determined by the location the item
1473         # is checked out from, not the owning or circ lib of the item
1474         my $org = $self->editor->requestor->ws_ou;
1475
1476       $logger->info("circulator: circ searching for closed date overlap on lib $org".
1477             " with an item due date of ".$circ->due_date );
1478
1479       my $dateinfo = $U->storagereq(
1480          'open-ils.storage.actor.org_unit.closed_date.overlap', 
1481             $org, $circ->due_date );
1482
1483       if($dateinfo) {
1484          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1485             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1486
1487             # XXX make the behavior more dynamic
1488             # for now, we just push the due date to after the close date
1489             $circ->due_date($dateinfo->{end});
1490       }
1491    }
1492 }
1493
1494
1495
1496 sub create_due_date {
1497     my( $self, $duration ) = @_;
1498     # if there is a raw time component (e.g. from postgres), 
1499     # turn it into an interval that interval_to_seconds can parse
1500     $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1501    my ($sec,$min,$hour,$mday,$mon,$year) =
1502       gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1503    $year += 1900; $mon += 1;
1504    my $due_date = sprintf(
1505       '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1506       $year, $mon, $mday, $hour, $min, $sec);
1507    return $due_date;
1508 }
1509
1510
1511
1512 sub make_precat_copy {
1513     my $self = shift;
1514     my $copy = $self->copy;
1515
1516    if($copy) {
1517       $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1518
1519       $copy->editor($self->editor->requestor->id);
1520       $copy->edit_date('now');
1521       $copy->dummy_title($self->dummy_title);
1522       $copy->dummy_author($self->dummy_author);
1523
1524         $self->update_copy();
1525         return;
1526    }
1527
1528    $logger->info("circulator: Creating a new precataloged ".
1529         "copy in checkout with barcode " . $self->copy_barcode);
1530
1531    $copy = Fieldmapper::asset::copy->new;
1532    $copy->circ_lib($self->circ_lib);
1533    $copy->creator($self->editor->requestor->id);
1534    $copy->editor($self->editor->requestor->id);
1535    $copy->barcode($self->copy_barcode);
1536    $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
1537    $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1538    $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1539
1540    $copy->dummy_title($self->dummy_title || "");
1541    $copy->dummy_author($self->dummy_author || "");
1542
1543     unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1544         $self->bail_out(1);
1545         $self->push_events($self->editor->event);
1546         return;
1547     }   
1548
1549     # this is a little bit of a hack, but we need to 
1550     # get the copy into the script runner
1551     $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1552 }
1553
1554
1555 sub checkout_noncat {
1556     my $self = shift;
1557
1558     my $circ;
1559     my $evt;
1560
1561    my $lib      = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1562    my $count    = $self->noncat_count || 1;
1563    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1564
1565    $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1566
1567    for(1..$count) {
1568
1569       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1570          $self->editor->requestor->id, 
1571             $self->patron->id, 
1572             $lib, 
1573             $self->noncat_type, 
1574             $cotime,
1575             $self->editor );
1576
1577         if( $evt ) {
1578             $self->push_events($evt);
1579             $self->bail_out(1);
1580             return; 
1581         }
1582         $self->circ($circ);
1583    }
1584 }
1585
1586
1587 sub do_checkin {
1588     my $self = shift;
1589     $self->log_me("do_checkin()");
1590
1591
1592     return $self->bail_on_events(
1593         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
1594         unless $self->copy;
1595
1596     if( $self->checkin_check_holds_shelf() ) {
1597         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1598         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1599         $self->checkin_flesh_events;
1600         return;
1601     }
1602
1603     unless( $self->is_renewal ) {
1604         return $self->bail_on_events($self->editor->event)
1605             unless $self->editor->allowed('COPY_CHECKIN');
1606     }
1607
1608     $self->push_events($self->check_copy_alert());
1609     $self->push_events($self->check_checkin_copy_status());
1610
1611     # the renew code will have already found our circulation object
1612     unless( $self->is_renewal and $self->circ ) {
1613         my $circs = $self->editor->search_action_circulation(
1614             { target_copy => $self->copy->id, checkin_time => undef });
1615         $self->circ($$circs[0]);
1616
1617         # for now, just warn if there are multiple open circs on a copy
1618         $logger->warn("circulator: we have ".scalar(@$circs).
1619             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1620     }
1621
1622     # if the circ is marked as 'claims returned', add the event to the list
1623     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1624         if ($self->circ and $self->circ->stop_fines 
1625                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1626
1627     $self->check_circ_deposit();
1628
1629     # handle the overridable events 
1630     $self->override_events unless $self->is_renewal;
1631     return if $self->bail_out;
1632     
1633     if( $self->copy ) {
1634         $self->transit(
1635             $self->editor->search_action_transit_copy(
1636             { target_copy => $self->copy->id, dest_recv_time => undef })->[0]); 
1637     }
1638
1639     if( $self->circ ) {
1640         $self->checkin_handle_circ;
1641         return if $self->bail_out;
1642         $self->checkin_changed(1);
1643
1644     } elsif( $self->transit ) {
1645         my $hold_transit = $self->process_received_transit;
1646         $self->checkin_changed(1);
1647
1648         if( $self->bail_out ) { 
1649             $self->checkin_flesh_events;
1650             return;
1651         }
1652         
1653         if( my $e = $self->check_checkin_copy_status() ) {
1654             # If the original copy status is special, alert the caller
1655             my $ev = $self->events;
1656             $self->events([$e]);
1657             $self->override_events;
1658             return if $self->bail_out;
1659             $self->events($ev);
1660         }
1661
1662         if( $hold_transit or 
1663                 $U->copy_status($self->copy->status)->id 
1664                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1665
1666          my $hold;
1667          if( $hold_transit ) {
1668             $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1669          } else {
1670                 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1671          }
1672
1673             $self->hold($hold);
1674
1675             if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1676
1677                 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1678                 $self->reshelve_copy(1);
1679                 $self->cancelled_hold_transit(1);
1680                 $self->notify_hold(0); # don't notify for cancelled holds
1681                 return if $self->bail_out;
1682
1683             } else {
1684
1685                 # hold transited to correct location
1686                 $self->checkin_flesh_events;
1687                 return;
1688             }
1689         } 
1690
1691     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1692
1693         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1694             " that is in-transit, but there is no transit.. repairing");
1695         $self->reshelve_copy(1);
1696         return if $self->bail_out;
1697     }
1698
1699     if( $self->is_renewal ) {
1700         $self->push_events(OpenILS::Event->new('SUCCESS'));
1701         return;
1702     }
1703
1704    # ------------------------------------------------------------------------------
1705    # Circulations and transits are now closed where necessary.  Now go on to see if
1706    # this copy can fulfill a hold or needs to be routed to a different location
1707    # ------------------------------------------------------------------------------
1708
1709     if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1710         return if $self->bail_out;
1711
1712    } else { # not needed for a hold
1713
1714         my $circ_lib = (ref $self->copy->circ_lib) ? 
1715                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1716
1717         if( $self->remote_hold ) {
1718             $circ_lib = $self->remote_hold->pickup_lib;
1719             $logger->warn("circulator: Copy ".$self->copy->barcode.
1720                 " is on a remote hold's shelf, sending to $circ_lib");
1721         }
1722
1723         $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1724
1725       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1726
1727             $self->checkin_handle_precat();
1728             return if $self->bail_out;
1729
1730       } else {
1731
1732             my $bc = $self->copy->barcode;
1733             $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1734             $self->checkin_build_copy_transit($circ_lib);
1735             return if $self->bail_out;
1736             $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1737       }
1738    }
1739
1740     $self->reshelve_copy;
1741     return if $self->bail_out;
1742
1743     unless($self->checkin_changed) {
1744
1745         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1746         my $stat = $U->copy_status($self->copy->status)->id;
1747
1748         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1749          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1750         $self->bail_out(1); # no need to commit anything
1751
1752     } else {
1753
1754         $self->push_events(OpenILS::Event->new('SUCCESS')) 
1755             unless @{$self->events};
1756     }
1757
1758
1759    # ------------------------------------------------------------------------------
1760    # Update the patron penalty info in the DB
1761    # ------------------------------------------------------------------------------
1762    $U->update_patron_penalties(
1763       authtoken => $self->editor->authtoken,
1764       patron    => $self->patron,
1765       background  => 1 ) if $self->is_checkin;
1766
1767     $self->checkin_flesh_events;
1768     return;
1769 }
1770
1771 # if a deposit was payed for this item, push the event
1772 sub check_circ_deposit {
1773     my $self = shift;
1774     return unless $self->circ;
1775     my $deposit = $self->editor->search_money_billing(
1776         {   billing_type => OILS_BILLING_TYPE_DEPOSIT, 
1777             xact => $self->circ->id, 
1778             voided => 'f'
1779         }, {idlist => 1})->[0];
1780
1781     $self->push_events(OpenILS::Event->new(
1782         'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1783 }
1784
1785 sub reshelve_copy {
1786    my $self    = shift;
1787    my $force   = $self->force || shift;
1788    my $copy    = $self->copy;
1789
1790    my $stat = $U->copy_status($copy->status)->id;
1791
1792    if($force || (
1793       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1794       $stat != OILS_COPY_STATUS_CATALOGING and
1795       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1796       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1797
1798         $copy->status( OILS_COPY_STATUS_RESHELVING );
1799             $self->update_copy;
1800             $self->checkin_changed(1);
1801     }
1802 }
1803
1804
1805 # Returns true if the item is at the current location
1806 # because it was transited there for a hold and the 
1807 # hold has not been fulfilled
1808 sub checkin_check_holds_shelf {
1809     my $self = shift;
1810     return 0 unless $self->copy;
1811
1812     return 0 unless 
1813         $U->copy_status($self->copy->status)->id ==
1814             OILS_COPY_STATUS_ON_HOLDS_SHELF;
1815
1816     # find the hold that put us on the holds shelf
1817     my $holds = $self->editor->search_action_hold_request(
1818         { 
1819             current_copy => $self->copy->id,
1820             capture_time => { '!=' => undef },
1821             fulfillment_time => undef,
1822             cancel_time => undef,
1823         }
1824     );
1825
1826     unless(@$holds) {
1827         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1828         $self->reshelve_copy(1);
1829         return 0;
1830     }
1831
1832     my $hold = $$holds[0];
1833
1834     $logger->info("circulator: we found a captured, un-fulfilled hold [".
1835         $hold->id. "] for copy ".$self->copy->barcode);
1836
1837     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1838         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1839         return 1;
1840     }
1841
1842     $logger->info("circulator: hold is not for here..");
1843     $self->remote_hold($hold);
1844     return 0;
1845 }
1846
1847
1848 sub checkin_handle_precat {
1849     my $self    = shift;
1850    my $copy    = $self->copy;
1851
1852    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1853       $copy->status(OILS_COPY_STATUS_CATALOGING);
1854         $self->update_copy();
1855         $self->checkin_changed(1);
1856         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1857    }
1858 }
1859
1860
1861 sub checkin_build_copy_transit {
1862     my $self            = shift;
1863     my $dest            = shift;
1864     my $copy       = $self->copy;
1865    my $transit    = Fieldmapper::action::transit_copy->new;
1866
1867     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1868     $logger->info("circulator: transiting copy to $dest");
1869
1870    $transit->source($self->editor->requestor->ws_ou);
1871    $transit->dest($dest);
1872    $transit->target_copy($copy->id);
1873    $transit->source_send_time('now');
1874    $transit->copy_status( $U->copy_status($copy->status)->id );
1875
1876     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1877
1878     return $self->bail_on_events($self->editor->event)
1879         unless $self->editor->create_action_transit_copy($transit);
1880
1881    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1882     $self->update_copy;
1883     $self->checkin_changed(1);
1884 }
1885
1886
1887 sub attempt_checkin_hold_capture {
1888     my $self = shift;
1889     my $copy = $self->copy;
1890
1891     # See if this copy can fulfill any holds
1892     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
1893         $self->editor, $copy, $self->editor->requestor );
1894
1895     if(!$hold) {
1896         $logger->debug("circulator: no potential permitted".
1897             "holds found for copy ".$copy->barcode);
1898         return undef;
1899     }
1900
1901     $self->retarget($retarget);
1902
1903     $logger->info("circulator: found permitted hold ".
1904         $hold->id . " for copy, capturing...");
1905
1906     $hold->current_copy($copy->id);
1907     $hold->capture_time('now');
1908
1909     # prevent DB errors caused by fetching 
1910     # holds from storage, and updating through cstore
1911     $hold->clear_fulfillment_time;
1912     $hold->clear_fulfillment_staff;
1913     $hold->clear_fulfillment_lib;
1914     $hold->clear_expire_time; 
1915     $hold->clear_cancel_time;
1916     $hold->clear_prev_check_time unless $hold->prev_check_time;
1917
1918     $self->bail_on_events($self->editor->event)
1919         unless $self->editor->update_action_hold_request($hold);
1920     $self->hold($hold);
1921     $self->checkin_changed(1);
1922
1923     return 1 if $self->bail_out;
1924
1925     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1926
1927         # This hold was captured in the correct location
1928     $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1929         $self->push_events(OpenILS::Event->new('SUCCESS'));
1930
1931         #$self->do_hold_notify($hold->id);
1932         $self->notify_hold($hold->id);
1933
1934     } else {
1935     
1936         # Hold needs to be picked up elsewhere.  Build a hold
1937         # transit and route the item.
1938         $self->checkin_build_hold_transit();
1939     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1940         return 1 if $self->bail_out;
1941         $self->push_events(
1942             OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1943     }
1944
1945     # make sure we save the copy status
1946     $self->update_copy;
1947     return 1;
1948 }
1949
1950 sub do_hold_notify {
1951     my( $self, $holdid ) = @_;
1952
1953     $logger->info("circulator: running delayed hold notify process");
1954
1955 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1956 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1957
1958     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1959         hold_id => $holdid, requestor => $self->editor->requestor);
1960
1961     $logger->debug("circulator: built hold notifier");
1962
1963     if(!$notifier->event) {
1964
1965         $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1966
1967         my $stat = $notifier->send_email_notify;
1968         if( $stat == '1' ) {
1969             $logger->info("ciculator: hold notify succeeded for hold $holdid");
1970             return;
1971         } 
1972
1973         $logger->warn("ciculator:  * hold notify failed for hold $holdid");
1974
1975     } else {
1976         $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1977     }
1978 }
1979
1980 sub retarget_holds {
1981     $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1982     my $ses = OpenSRF::AppSession->create('open-ils.storage');
1983     $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1984     # no reason to wait for the return value
1985     return;
1986 }
1987
1988 sub checkin_build_hold_transit {
1989     my $self = shift;
1990
1991    my $copy = $self->copy;
1992    my $hold = $self->hold;
1993    my $trans = Fieldmapper::action::hold_transit_copy->new;
1994
1995     $logger->debug("circulator: building hold transit for ".$copy->barcode);
1996
1997    $trans->hold($hold->id);
1998    $trans->source($self->editor->requestor->ws_ou);
1999    $trans->dest($hold->pickup_lib);
2000    $trans->source_send_time("now");
2001    $trans->target_copy($copy->id);
2002
2003     # when the copy gets to its destination, it will recover
2004     # this status - put it onto the holds shelf
2005    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2006
2007     return $self->bail_on_events($self->editor->event)
2008         unless $self->editor->create_action_hold_transit_copy($trans);
2009 }
2010
2011
2012
2013 sub process_received_transit {
2014     my $self = shift;
2015     my $copy = $self->copy;
2016     my $copyid = $self->copy->id;
2017
2018     my $status_name = $U->copy_status($copy->status)->name;
2019     $logger->debug("circulator: attempting transit receive on ".
2020         "copy $copyid. Copy status is $status_name");
2021
2022     my $transit = $self->transit;
2023
2024     if( $transit->dest != $self->editor->requestor->ws_ou ) {
2025         # - this item is in-transit to a different location
2026
2027         my $tid = $transit->id; 
2028         my $loc = $self->editor->requestor->ws_ou;
2029         my $dest = $transit->dest;
2030
2031         $logger->info("circulator: Fowarding transit on copy which is destined ".
2032             "for a different location. transit=$tid, copy=$copyid, current ".
2033             "location=$loc, destination location=$dest");
2034
2035         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2036
2037         # grab the associated hold object if available
2038         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2039         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2040
2041         return $self->bail_on_events($evt);
2042     }
2043
2044    # The transit is received, set the receive time
2045    $transit->dest_recv_time('now');
2046     $self->bail_on_events($self->editor->event)
2047         unless $self->editor->update_action_transit_copy($transit);
2048
2049     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2050
2051    $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2052    $copy->status( $transit->copy_status );
2053     $self->update_copy();
2054     return if $self->bail_out;
2055
2056     my $ishold = 0;
2057     if($hold_transit) { 
2058         #$self->do_hold_notify($hold_transit->hold);
2059         $self->notify_hold($hold_transit->hold);
2060         $ishold = 1;
2061     }
2062
2063     $self->push_events( 
2064         OpenILS::Event->new(
2065         'SUCCESS', 
2066         ishold => $ishold,
2067       payload => { transit => $transit, holdtransit => $hold_transit } ));
2068
2069     return $hold_transit;
2070 }
2071
2072
2073 sub checkin_handle_circ {
2074    my $self = shift;
2075     $U->logmark;
2076
2077    my $circ = $self->circ;
2078    my $copy = $self->copy;
2079    my $evt;
2080    my $obt;
2081
2082    # backdate the circ if necessary
2083    if($self->backdate) {
2084         $self->checkin_handle_backdate;
2085         return if $self->bail_out;
2086    }
2087
2088    if(!$circ->stop_fines) {
2089       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2090       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2091       $circ->stop_fines_time('now') unless $self->backdate;
2092       $circ->stop_fines_time($self->backdate) if $self->backdate;
2093    }
2094
2095    # see if there are any fines owed on this circ.  if not, close it
2096     ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2097    $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2098
2099     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2100
2101    # Set the checkin vars since we have the item
2102     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2103
2104    $circ->checkin_staff($self->editor->requestor->id);
2105    $circ->checkin_lib($self->editor->requestor->ws_ou);
2106
2107     my $circ_lib = (ref $self->copy->circ_lib) ?  
2108         $self->copy->circ_lib->id : $self->copy->circ_lib;
2109     my $stat = $U->copy_status($self->copy->status)->id;
2110
2111     # If the item is lost/missing and it needs to be sent home, don't 
2112     # reshelve the copy, leave it lost/missing so the recipient will know
2113     if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
2114         and ($circ_lib != $self->editor->requestor->ws_ou) ) {
2115         $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2116
2117     } else {
2118         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2119         $self->update_copy;
2120     }
2121
2122     return $self->bail_on_events($self->editor->event)
2123         unless $self->editor->update_action_circulation($circ);
2124 }
2125
2126
2127 sub checkin_handle_backdate {
2128     my $self = shift;
2129
2130     my $bd = $self->backdate;
2131
2132     # ------------------------------------------------------------------
2133     # clean up the backdate for date comparison
2134     # we want any bills created on or after the backdate
2135     # ------------------------------------------------------------------
2136     $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2137     #$bd = "${bd}T23:59:59";
2138
2139     my $bills = $self->editor->search_money_billing(
2140         { 
2141             billing_ts => { '>=' => $bd }, 
2142             xact => $self->circ->id, 
2143             billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2144         }
2145     );
2146
2147     $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2148
2149     for my $bill (@$bills) {    
2150         unless( $U->is_true($bill->voided) ) {
2151             $logger->info("backdate voiding bill ".$bill->id);
2152             $bill->voided('t');
2153             $bill->void_time('now');
2154             $bill->voider($self->editor->requestor->id);
2155             my $n = $bill->note || "";
2156             $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2157
2158             $self->bail_on_events($self->editor->event)
2159                 unless $self->editor->update_money_billing($bill);
2160         }
2161     }
2162 }
2163
2164
2165
2166
2167 sub find_patron_from_copy {
2168     my $self = shift;
2169     my $circs = $self->editor->search_action_circulation(
2170         { target_copy => $self->copy->id, checkin_time => undef });
2171     my $circ = $circs->[0];
2172     return unless $circ;
2173     my $u = $self->editor->retrieve_actor_user($circ->usr)
2174         or return $self->bail_on_events($self->editor->event);
2175     $self->patron($u);
2176 }
2177
2178 sub check_checkin_copy_status {
2179     my $self = shift;
2180    my $copy = $self->copy;
2181
2182    my $islost     = 0;
2183    my $ismissing  = 0;
2184    my $evt        = undef;
2185
2186    my $status = $U->copy_status($copy->status)->id;
2187
2188    return undef
2189       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
2190             $status == OILS_COPY_STATUS_CHECKED_OUT ||
2191             $status == OILS_COPY_STATUS_IN_PROCESS  ||
2192             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
2193             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
2194             $status == OILS_COPY_STATUS_CATALOGING  ||
2195             $status == OILS_COPY_STATUS_RESHELVING );
2196
2197    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2198       if( $status == OILS_COPY_STATUS_LOST );
2199
2200    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2201       if( $status == OILS_COPY_STATUS_MISSING );
2202
2203    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2204 }
2205
2206
2207
2208 # --------------------------------------------------------------------------
2209 # On checkin, we need to return as many relevant objects as we can
2210 # --------------------------------------------------------------------------
2211 sub checkin_flesh_events {
2212     my $self = shift;
2213
2214     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
2215         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2216             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2217     }
2218
2219
2220     for my $evt (@{$self->events}) {
2221
2222         my $payload          = {};
2223         $payload->{copy}     = $U->unflesh_copy($self->copy);
2224         $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2225         $payload->{circ}     = $self->circ;
2226         $payload->{transit}  = $self->transit;
2227         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2228
2229         # $self->hold may or may not have been replaced with a 
2230         # valid hold after processing a cancelled hold
2231         $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
2232
2233         $evt->{payload} = $payload;
2234     }
2235 }
2236
2237 sub log_me {
2238     my( $self, $msg ) = @_;
2239     my $bc = ($self->copy) ? $self->copy->barcode :
2240         $self->barcode;
2241     $bc ||= "";
2242     my $usr = ($self->patron) ? $self->patron->id : "";
2243     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2244         ", recipient=$usr, copy=$bc");
2245 }
2246
2247
2248 sub do_renew {
2249     my $self = shift;
2250     $self->log_me("do_renew()");
2251     $self->is_renewal(1);
2252
2253     # Make sure there is an open circ to renew that is not
2254     # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2255     my $circ = $self->editor->search_action_circulation(
2256             { target_copy => $self->copy->id, stop_fines => undef } )->[0];
2257
2258     if(!$circ) {
2259         $circ = $self->editor->search_action_circulation(
2260             { 
2261                 target_copy => $self->copy->id, 
2262                 stop_fines => OILS_STOP_FINES_MAX_FINES,
2263                 checkin_time => undef
2264             } 
2265         )->[0];
2266     }
2267
2268     return $self->bail_on_events($self->editor->event) unless $circ;
2269
2270     # A user is not allowed to renew another user's items without permission
2271     unless( $circ->usr eq $self->editor->requestor->id ) {
2272         return $self->bail_on_events($self->editor->events)
2273             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2274     }   
2275
2276     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2277         if $circ->renewal_remaining < 1;
2278
2279     # -----------------------------------------------------------------
2280
2281     $self->renewal_remaining( $circ->renewal_remaining - 1 );
2282     $self->circ($circ);
2283
2284     $self->run_renew_permit;
2285
2286     # Check the item in
2287     $self->do_checkin();
2288     return if $self->bail_out;
2289
2290     unless( $self->permit_override ) {
2291         $self->do_permit();
2292         return if $self->bail_out;
2293         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2294         $self->remove_event('ITEM_NOT_CATALOGED');
2295     }   
2296
2297     $self->override_events;
2298     return if $self->bail_out;
2299
2300     $self->events([]);
2301     $self->do_checkout();
2302 }
2303
2304
2305 sub remove_event {
2306     my( $self, $evt ) = @_;
2307     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2308     $logger->debug("circulator: removing event from list: $evt");
2309     my @events = @{$self->events};
2310     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2311 }
2312
2313
2314 sub have_event {
2315     my( $self, $evt ) = @_;
2316     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2317     return grep { $_->{textcode} eq $evt } @{$self->events};
2318 }
2319
2320
2321
2322 sub run_renew_permit {
2323     my $self = shift;
2324
2325     my $events = [];
2326
2327     if(!$self->legacy_script_support) {
2328         my $results = $self->run_indb_circ_test;
2329         unless($self->circ_test_success) {
2330             push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) for @$results;
2331         }
2332
2333     } else {
2334
2335         my $runner = $self->script_runner;
2336
2337         $runner->load($self->circ_permit_renew);
2338         my $result = $runner->run or 
2339             throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2340         my $events = $result->{events};
2341     }
2342
2343     $logger->activity("ciculator: circ_permit_renew for user ".
2344       $self->patron->id." returned events: @$events") if @$events;
2345
2346     $self->push_events(OpenILS::Event->new($_)) for @$events;
2347
2348     $logger->debug("circulator: re-creating script runner to be safe");
2349     #$self->mk_script_runner;
2350 }
2351
2352
2353
2354