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