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