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