]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
turning raw time durations into h/m/s notation so interval_to_seconds can parse it
[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         $logger->debug("circulator: building circulation ".
1064             "with duration=$dname, maxfine=$mname, recurring=$rname");
1065     
1066         $circ->duration( $duration->shrt ) 
1067             if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1068         $circ->duration( $duration->normal ) 
1069             if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1070         $circ->duration( $duration->extended ) 
1071             if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1072     
1073         $circ->recuring_fine( $recurring->low ) 
1074             if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1075         $circ->recuring_fine( $recurring->normal ) 
1076             if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1077         $circ->recuring_fine( $recurring->high ) 
1078             if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1079
1080         $circ->duration_rule( $duration->name );
1081         $circ->recuring_fine_rule( $recurring->name );
1082         $circ->max_fine_rule( $max->name );
1083         $circ->max_fine( $max->amount );
1084
1085         $circ->fine_interval($recurring->recurance_interval);
1086         $circ->renewal_remaining( $duration->max_renewals );
1087
1088     } else {
1089
1090         $logger->info("circulator: copy found with an unlimited circ duration");
1091         $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1092         $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1093         $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1094         $circ->renewal_remaining(0);
1095     }
1096
1097    $circ->target_copy( $copy->id );
1098    $circ->usr( $patron->id );
1099    $circ->circ_lib( $self->circ_lib );
1100
1101    if( $self->is_renewal ) {
1102       $circ->opac_renewal('t') if $self->opac_renewal;
1103       $circ->phone_renewal('t') if $self->phone_renewal;
1104       $circ->desk_renewal('t') if $self->desk_renewal;
1105       $circ->renewal_remaining($self->renewal_remaining);
1106       $circ->circ_staff($self->editor->requestor->id);
1107    }
1108
1109
1110    # if the user provided an overiding checkout time,
1111    # (e.g. the checkout really happened several hours ago), then
1112    # we apply that here.  Does this need a perm??
1113     $circ->xact_start(clense_ISO8601($self->checkout_time))
1114         if $self->checkout_time;
1115
1116    # if a patron is renewing, 'requestor' will be the patron
1117    $circ->circ_staff($self->editor->requestor->id);
1118     $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1119
1120     $self->circ($circ);
1121 }
1122
1123
1124 sub apply_modified_due_date {
1125     my $self = shift;
1126     my $circ = $self->circ;
1127     my $copy = $self->copy;
1128
1129    if( $self->due_date ) {
1130
1131         return $self->bail_on_events($self->editor->event)
1132             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1133
1134       $circ->due_date(clense_ISO8601($self->due_date));
1135
1136    } else {
1137
1138       # if the due_date lands on a day when the location is closed
1139       return unless $copy and $circ->due_date;
1140
1141         #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1142
1143         # due-date overlap should be determined by the location the item
1144         # is checked out from, not the owning or circ lib of the item
1145         my $org = $self->editor->requestor->ws_ou;
1146
1147       $logger->info("circulator: circ searching for closed date overlap on lib $org".
1148             " with an item due date of ".$circ->due_date );
1149
1150       my $dateinfo = $U->storagereq(
1151          'open-ils.storage.actor.org_unit.closed_date.overlap', 
1152             $org, $circ->due_date );
1153
1154       if($dateinfo) {
1155          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1156             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1157
1158             # XXX make the behavior more dynamic
1159             # for now, we just push the due date to after the close date
1160             $circ->due_date($dateinfo->{end});
1161       }
1162    }
1163 }
1164
1165
1166
1167 sub create_due_date {
1168     my( $self, $duration ) = @_;
1169     # if there is a raw time component (e.g. from postgres), 
1170     # turn it into an interval that interval_to_seconds can parse
1171     $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1172    my ($sec,$min,$hour,$mday,$mon,$year) =
1173       gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1174    $year += 1900; $mon += 1;
1175    my $due_date = sprintf(
1176       '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1177       $year, $mon, $mday, $hour, $min, $sec);
1178    return $due_date;
1179 }
1180
1181
1182
1183 sub make_precat_copy {
1184     my $self = shift;
1185     my $copy = $self->copy;
1186
1187    if($copy) {
1188       $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1189
1190       $copy->editor($self->editor->requestor->id);
1191       $copy->edit_date('now');
1192       $copy->dummy_title($self->dummy_title);
1193       $copy->dummy_author($self->dummy_author);
1194
1195         $self->update_copy();
1196         return;
1197    }
1198
1199    $logger->info("circulator: Creating a new precataloged ".
1200         "copy in checkout with barcode " . $self->copy_barcode);
1201
1202    $copy = Fieldmapper::asset::copy->new;
1203    $copy->circ_lib($self->circ_lib);
1204    $copy->creator($self->editor->requestor->id);
1205    $copy->editor($self->editor->requestor->id);
1206    $copy->barcode($self->copy_barcode);
1207    $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
1208    $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1209    $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1210
1211    $copy->dummy_title($self->dummy_title || "");
1212    $copy->dummy_author($self->dummy_author || "");
1213
1214     unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1215         $self->bail_out(1);
1216         $self->push_events($self->editor->event);
1217         return;
1218     }   
1219
1220     # this is a little bit of a hack, but we need to 
1221     # get the copy into the script runner
1222     $self->script_runner->insert("environment.copy", $copy, 1);
1223 }
1224
1225
1226 sub checkout_noncat {
1227     my $self = shift;
1228
1229     my $circ;
1230     my $evt;
1231
1232    my $lib      = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1233    my $count    = $self->noncat_count || 1;
1234    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1235
1236    $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1237
1238    for(1..$count) {
1239
1240       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1241          $self->editor->requestor->id, 
1242             $self->patron->id, 
1243             $lib, 
1244             $self->noncat_type, 
1245             $cotime,
1246             $self->editor );
1247
1248         if( $evt ) {
1249             $self->push_events($evt);
1250             $self->bail_out(1);
1251             return; 
1252         }
1253         $self->circ($circ);
1254    }
1255 }
1256
1257
1258 sub do_checkin {
1259     my $self = shift;
1260     $self->log_me("do_checkin()");
1261
1262
1263     return $self->bail_on_events(
1264         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
1265         unless $self->copy;
1266
1267     if( $self->checkin_check_holds_shelf() ) {
1268         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1269         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1270         $self->checkin_flesh_events;
1271         return;
1272     }
1273
1274     unless( $self->is_renewal ) {
1275         return $self->bail_on_events($self->editor->event)
1276             unless $self->editor->allowed('COPY_CHECKIN');
1277     }
1278
1279     $self->push_events($self->check_copy_alert());
1280     $self->push_events($self->check_checkin_copy_status());
1281
1282     # the renew code will have already found our circulation object
1283     unless( $self->is_renewal and $self->circ ) {
1284         my $circs = $self->editor->search_action_circulation(
1285             { target_copy => $self->copy->id, checkin_time => undef });
1286         $self->circ($$circs[0]);
1287
1288         # for now, just warn if there are multiple open circs on a copy
1289         $logger->warn("circulator: we have ".scalar(@$circs).
1290             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1291     }
1292
1293     # if the circ is marked as 'claims returned', add the event to the list
1294     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1295         if ($self->circ and $self->circ->stop_fines 
1296                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1297
1298     # handle the overridable events 
1299     $self->override_events unless $self->is_renewal;
1300     return if $self->bail_out;
1301     
1302     if( $self->copy ) {
1303         $self->transit(
1304             $self->editor->search_action_transit_copy(
1305             { target_copy => $self->copy->id, dest_recv_time => undef })->[0]); 
1306     }
1307
1308     if( $self->circ ) {
1309         $self->checkin_handle_circ;
1310         return if $self->bail_out;
1311         $self->checkin_changed(1);
1312
1313     } elsif( $self->transit ) {
1314         my $hold_transit = $self->process_received_transit;
1315         $self->checkin_changed(1);
1316
1317         if( $self->bail_out ) { 
1318             $self->checkin_flesh_events;
1319             return;
1320         }
1321         
1322         if( my $e = $self->check_checkin_copy_status() ) {
1323             # If the original copy status is special, alert the caller
1324             my $ev = $self->events;
1325             $self->events([$e]);
1326             $self->override_events;
1327             return if $self->bail_out;
1328             $self->events($ev);
1329         }
1330
1331         if( $hold_transit or 
1332                 $U->copy_status($self->copy->status)->id 
1333                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1334
1335          my $hold;
1336          if( $hold_transit ) {
1337             $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1338          } else {
1339                 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1340          }
1341
1342             $self->hold($hold);
1343
1344             if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1345
1346                 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1347                 $self->reshelve_copy(1);
1348                 $self->cancelled_hold_transit(1);
1349                 $self->notify_hold(0); # don't notify for cancelled holds
1350                 return if $self->bail_out;
1351
1352             } else {
1353
1354                 # hold transited to correct location
1355                 $self->checkin_flesh_events;
1356                 return;
1357             }
1358         } 
1359
1360     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1361
1362         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1363             " that is in-transit, but there is no transit.. repairing");
1364         $self->reshelve_copy(1);
1365         return if $self->bail_out;
1366     }
1367
1368     if( $self->is_renewal ) {
1369         $self->push_events(OpenILS::Event->new('SUCCESS'));
1370         return;
1371     }
1372
1373    # ------------------------------------------------------------------------------
1374    # Circulations and transits are now closed where necessary.  Now go on to see if
1375    # this copy can fulfill a hold or needs to be routed to a different location
1376    # ------------------------------------------------------------------------------
1377
1378     if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1379         return if $self->bail_out;
1380
1381    } else { # not needed for a hold
1382
1383         my $circ_lib = (ref $self->copy->circ_lib) ? 
1384                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1385
1386         if( $self->remote_hold ) {
1387             $circ_lib = $self->remote_hold->pickup_lib;
1388             $logger->warn("circulator: Copy ".$self->copy->barcode.
1389                 " is on a remote hold's shelf, sending to $circ_lib");
1390         }
1391
1392         $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1393
1394       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1395
1396             $self->checkin_handle_precat();
1397             return if $self->bail_out;
1398
1399       } else {
1400
1401             my $bc = $self->copy->barcode;
1402             $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1403             $self->checkin_build_copy_transit($circ_lib);
1404             return if $self->bail_out;
1405             $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1406       }
1407    }
1408
1409     $self->reshelve_copy;
1410     return if $self->bail_out;
1411
1412     unless($self->checkin_changed) {
1413
1414         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1415         my $stat = $U->copy_status($self->copy->status)->id;
1416
1417         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1418          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1419         $self->bail_out(1); # no need to commit anything
1420
1421     } else {
1422
1423         $self->push_events(OpenILS::Event->new('SUCCESS')) 
1424             unless @{$self->events};
1425     }
1426
1427
1428    # ------------------------------------------------------------------------------
1429    # Update the patron penalty info in the DB
1430    # ------------------------------------------------------------------------------
1431    $U->update_patron_penalties(
1432       authtoken => $self->editor->authtoken,
1433       patron    => $self->patron,
1434       background  => 1 ) if $self->is_checkin;
1435
1436     $self->checkin_flesh_events;
1437     return;
1438 }
1439
1440 sub reshelve_copy {
1441    my $self    = shift;
1442    my $force   = $self->force || shift;
1443    my $copy    = $self->copy;
1444
1445    my $stat = $U->copy_status($copy->status)->id;
1446
1447    if($force || (
1448       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1449       $stat != OILS_COPY_STATUS_CATALOGING and
1450       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1451       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1452
1453         $copy->status( OILS_COPY_STATUS_RESHELVING );
1454             $self->update_copy;
1455             $self->checkin_changed(1);
1456     }
1457 }
1458
1459
1460 # Returns true if the item is at the current location
1461 # because it was transited there for a hold and the 
1462 # hold has not been fulfilled
1463 sub checkin_check_holds_shelf {
1464     my $self = shift;
1465     return 0 unless $self->copy;
1466
1467     return 0 unless 
1468         $U->copy_status($self->copy->status)->id ==
1469             OILS_COPY_STATUS_ON_HOLDS_SHELF;
1470
1471     # find the hold that put us on the holds shelf
1472     my $holds = $self->editor->search_action_hold_request(
1473         { 
1474             current_copy => $self->copy->id,
1475             capture_time => { '!=' => undef },
1476             fulfillment_time => undef,
1477             cancel_time => undef,
1478         }
1479     );
1480
1481     unless(@$holds) {
1482         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1483         $self->reshelve_copy(1);
1484         return 0;
1485     }
1486
1487     my $hold = $$holds[0];
1488
1489     $logger->info("circulator: we found a captured, un-fulfilled hold [".
1490         $hold->id. "] for copy ".$self->copy->barcode);
1491
1492     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1493         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1494         return 1;
1495     }
1496
1497     $logger->info("circulator: hold is not for here..");
1498     $self->remote_hold($hold);
1499     return 0;
1500 }
1501
1502
1503 sub checkin_handle_precat {
1504     my $self    = shift;
1505    my $copy    = $self->copy;
1506
1507    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1508       $copy->status(OILS_COPY_STATUS_CATALOGING);
1509         $self->update_copy();
1510         $self->checkin_changed(1);
1511         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1512    }
1513 }
1514
1515
1516 sub checkin_build_copy_transit {
1517     my $self            = shift;
1518     my $dest            = shift;
1519     my $copy       = $self->copy;
1520    my $transit    = Fieldmapper::action::transit_copy->new;
1521
1522     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1523     $logger->info("circulator: transiting copy to $dest");
1524
1525    $transit->source($self->editor->requestor->ws_ou);
1526    $transit->dest($dest);
1527    $transit->target_copy($copy->id);
1528    $transit->source_send_time('now');
1529    $transit->copy_status( $U->copy_status($copy->status)->id );
1530
1531     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1532
1533     return $self->bail_on_events($self->editor->event)
1534         unless $self->editor->create_action_transit_copy($transit);
1535
1536    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1537     $self->update_copy;
1538     $self->checkin_changed(1);
1539 }
1540
1541
1542 sub attempt_checkin_hold_capture {
1543     my $self = shift;
1544     my $copy = $self->copy;
1545
1546     # See if this copy can fulfill any holds
1547     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
1548         $self->editor, $copy, $self->editor->requestor );
1549
1550     if(!$hold) {
1551         $logger->debug("circulator: no potential permitted".
1552             "holds found for copy ".$copy->barcode);
1553         return undef;
1554     }
1555
1556     $self->retarget($retarget);
1557
1558     $logger->info("circulator: found permitted hold ".
1559         $hold->id . " for copy, capturing...");
1560
1561     $hold->current_copy($copy->id);
1562     $hold->capture_time('now');
1563
1564     # prevent DB errors caused by fetching 
1565     # holds from storage, and updating through cstore
1566     $hold->clear_fulfillment_time;
1567     $hold->clear_fulfillment_staff;
1568     $hold->clear_fulfillment_lib;
1569     $hold->clear_expire_time; 
1570     $hold->clear_cancel_time;
1571     $hold->clear_prev_check_time unless $hold->prev_check_time;
1572
1573     $self->bail_on_events($self->editor->event)
1574         unless $self->editor->update_action_hold_request($hold);
1575     $self->hold($hold);
1576     $self->checkin_changed(1);
1577
1578     return 1 if $self->bail_out;
1579
1580     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1581
1582         # This hold was captured in the correct location
1583     $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1584         $self->push_events(OpenILS::Event->new('SUCCESS'));
1585
1586         #$self->do_hold_notify($hold->id);
1587         $self->notify_hold($hold->id);
1588
1589     } else {
1590     
1591         # Hold needs to be picked up elsewhere.  Build a hold
1592         # transit and route the item.
1593         $self->checkin_build_hold_transit();
1594     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1595         return 1 if $self->bail_out;
1596         $self->push_events(
1597             OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1598     }
1599
1600     # make sure we save the copy status
1601     $self->update_copy;
1602     return 1;
1603 }
1604
1605 sub do_hold_notify {
1606     my( $self, $holdid ) = @_;
1607
1608     $logger->info("circulator: running delayed hold notify process");
1609
1610 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1611 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1612
1613     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1614         hold_id => $holdid, requestor => $self->editor->requestor);
1615
1616     $logger->debug("circulator: built hold notifier");
1617
1618     if(!$notifier->event) {
1619
1620         $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1621
1622         my $stat = $notifier->send_email_notify;
1623         if( $stat == '1' ) {
1624             $logger->info("ciculator: hold notify succeeded for hold $holdid");
1625             return;
1626         } 
1627
1628         $logger->warn("ciculator:  * hold notify failed for hold $holdid");
1629
1630     } else {
1631         $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1632     }
1633 }
1634
1635 sub retarget_holds {
1636     $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1637     my $ses = OpenSRF::AppSession->create('open-ils.storage');
1638     $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1639     # no reason to wait for the return value
1640     return;
1641 }
1642
1643 sub checkin_build_hold_transit {
1644     my $self = shift;
1645
1646    my $copy = $self->copy;
1647    my $hold = $self->hold;
1648    my $trans = Fieldmapper::action::hold_transit_copy->new;
1649
1650     $logger->debug("circulator: building hold transit for ".$copy->barcode);
1651
1652    $trans->hold($hold->id);
1653    $trans->source($self->editor->requestor->ws_ou);
1654    $trans->dest($hold->pickup_lib);
1655    $trans->source_send_time("now");
1656    $trans->target_copy($copy->id);
1657
1658     # when the copy gets to its destination, it will recover
1659     # this status - put it onto the holds shelf
1660    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1661
1662     return $self->bail_on_events($self->editor->event)
1663         unless $self->editor->create_action_hold_transit_copy($trans);
1664 }
1665
1666
1667
1668 sub process_received_transit {
1669     my $self = shift;
1670     my $copy = $self->copy;
1671     my $copyid = $self->copy->id;
1672
1673     my $status_name = $U->copy_status($copy->status)->name;
1674     $logger->debug("circulator: attempting transit receive on ".
1675         "copy $copyid. Copy status is $status_name");
1676
1677     my $transit = $self->transit;
1678
1679     if( $transit->dest != $self->editor->requestor->ws_ou ) {
1680         # - this item is in-transit to a different location
1681
1682         my $tid = $transit->id; 
1683         my $loc = $self->editor->requestor->ws_ou;
1684         my $dest = $transit->dest;
1685
1686         $logger->info("circulator: Fowarding transit on copy which is destined ".
1687             "for a different location. transit=$tid, copy=$copyid, current ".
1688             "location=$loc, destination location=$dest");
1689
1690         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
1691
1692         # grab the associated hold object if available
1693         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
1694         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
1695
1696         return $self->bail_on_events($evt);
1697     }
1698
1699    # The transit is received, set the receive time
1700    $transit->dest_recv_time('now');
1701     $self->bail_on_events($self->editor->event)
1702         unless $self->editor->update_action_transit_copy($transit);
1703
1704     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1705
1706    $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
1707    $copy->status( $transit->copy_status );
1708     $self->update_copy();
1709     return if $self->bail_out;
1710
1711     my $ishold = 0;
1712     if($hold_transit) { 
1713         #$self->do_hold_notify($hold_transit->hold);
1714         $self->notify_hold($hold_transit->hold);
1715         $ishold = 1;
1716     }
1717
1718     $self->push_events( 
1719         OpenILS::Event->new(
1720         'SUCCESS', 
1721         ishold => $ishold,
1722       payload => { transit => $transit, holdtransit => $hold_transit } ));
1723
1724     return $hold_transit;
1725 }
1726
1727
1728 sub checkin_handle_circ {
1729    my $self = shift;
1730     $U->logmark;
1731
1732    my $circ = $self->circ;
1733    my $copy = $self->copy;
1734    my $evt;
1735    my $obt;
1736
1737    # backdate the circ if necessary
1738    if($self->backdate) {
1739         $self->checkin_handle_backdate;
1740         return if $self->bail_out;
1741    }
1742
1743    if(!$circ->stop_fines) {
1744       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1745       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1746       $circ->stop_fines_time('now') unless $self->backdate;
1747       $circ->stop_fines_time($self->backdate) if $self->backdate;
1748    }
1749
1750    # see if there are any fines owed on this circ.  if not, close it
1751     ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1752    $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1753
1754     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1755
1756    # Set the checkin vars since we have the item
1757     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
1758
1759    $circ->checkin_staff($self->editor->requestor->id);
1760    $circ->checkin_lib($self->editor->requestor->ws_ou);
1761
1762     my $circ_lib = (ref $self->copy->circ_lib) ?  
1763         $self->copy->circ_lib->id : $self->copy->circ_lib;
1764     my $stat = $U->copy_status($self->copy->status)->id;
1765
1766     # If the item is lost/missing and it needs to be sent home, don't 
1767     # reshelve the copy, leave it lost/missing so the recipient will know
1768     if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1769         and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1770         $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1771
1772     } else {
1773         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1774         $self->update_copy;
1775     }
1776
1777     return $self->bail_on_events($self->editor->event)
1778         unless $self->editor->update_action_circulation($circ);
1779 }
1780
1781
1782 sub checkin_handle_backdate {
1783     my $self = shift;
1784
1785     my $bd = $self->backdate;
1786
1787     # ------------------------------------------------------------------
1788     # clean up the backdate for date comparison
1789     # we want any bills created on or after the backdate
1790     # ------------------------------------------------------------------
1791     $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1792     #$bd = "${bd}T23:59:59";
1793
1794     my $bills = $self->editor->search_money_billing(
1795         { 
1796             billing_ts => { '>=' => $bd }, 
1797             xact => $self->circ->id, 
1798             billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1799         }
1800     );
1801
1802     $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1803
1804     for my $bill (@$bills) {    
1805         unless( $U->is_true($bill->voided) ) {
1806             $logger->info("backdate voiding bill ".$bill->id);
1807             $bill->voided('t');
1808             $bill->void_time('now');
1809             $bill->voider($self->editor->requestor->id);
1810             my $n = $bill->note || "";
1811             $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1812
1813             $self->bail_on_events($self->editor->event)
1814                 unless $self->editor->update_money_billing($bill);
1815         }
1816     }
1817 }
1818
1819
1820
1821 =head
1822 # XXX Legacy version for Circ.pm support
1823 sub _checkin_handle_backdate {
1824    my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1825
1826     my $bd = $backdate;
1827     $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1828     $bd = "${bd}T23:59:59";
1829
1830    my $bills = $session->request(
1831       "open-ils.storage.direct.money.billing.search_where.atomic",
1832         billing_ts => { '>=' => $bd }, 
1833         xact => $circ->id,
1834         billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1835     )->gather(1);
1836
1837     $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1838
1839    if($bills) {
1840       for my $bill (@$bills) {
1841             unless( $U->is_true($bill->voided) ) {
1842                 $logger->debug("voiding bill ".$bill->id);
1843                 $bill->voided('t');
1844                 $bill->void_time('now');
1845                 $bill->voider($requestor->id);
1846                 my $n = $bill->note || "";
1847                 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1848                 my $s = $session->request(
1849                     "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1850                 return $U->DB_UPDATE_FAILED($bill) unless $s;
1851             }
1852         }
1853    }
1854
1855     return 100;
1856 }
1857 =cut
1858
1859
1860
1861
1862
1863
1864 sub find_patron_from_copy {
1865     my $self = shift;
1866     my $circs = $self->editor->search_action_circulation(
1867         { target_copy => $self->copy->id, checkin_time => undef });
1868     my $circ = $circs->[0];
1869     return unless $circ;
1870     my $u = $self->editor->retrieve_actor_user($circ->usr)
1871         or return $self->bail_on_events($self->editor->event);
1872     $self->patron($u);
1873 }
1874
1875 sub check_checkin_copy_status {
1876     my $self = shift;
1877    my $copy = $self->copy;
1878
1879    my $islost     = 0;
1880    my $ismissing  = 0;
1881    my $evt        = undef;
1882
1883    my $status = $U->copy_status($copy->status)->id;
1884
1885    return undef
1886       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
1887             $status == OILS_COPY_STATUS_CHECKED_OUT ||
1888             $status == OILS_COPY_STATUS_IN_PROCESS  ||
1889             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
1890             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
1891             $status == OILS_COPY_STATUS_CATALOGING  ||
1892             $status == OILS_COPY_STATUS_RESHELVING );
1893
1894    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1895       if( $status == OILS_COPY_STATUS_LOST );
1896
1897    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1898       if( $status == OILS_COPY_STATUS_MISSING );
1899
1900    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1901 }
1902
1903
1904
1905 # --------------------------------------------------------------------------
1906 # On checkin, we need to return as many relevant objects as we can
1907 # --------------------------------------------------------------------------
1908 sub checkin_flesh_events {
1909     my $self = shift;
1910
1911     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
1912         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1913             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1914     }
1915
1916
1917     for my $evt (@{$self->events}) {
1918
1919         my $payload          = {};
1920         $payload->{copy}     = $U->unflesh_copy($self->copy);
1921         $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1922         $payload->{circ}     = $self->circ;
1923         $payload->{transit}  = $self->transit;
1924         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
1925
1926         # $self->hold may or may not have been replaced with a 
1927         # valid hold after processing a cancelled hold
1928         $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
1929
1930         $evt->{payload} = $payload;
1931     }
1932 }
1933
1934 sub log_me {
1935     my( $self, $msg ) = @_;
1936     my $bc = ($self->copy) ? $self->copy->barcode :
1937         $self->barcode;
1938     $bc ||= "";
1939     my $usr = ($self->patron) ? $self->patron->id : "";
1940     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1941         ", recipient=$usr, copy=$bc");
1942 }
1943
1944
1945 sub do_renew {
1946     my $self = shift;
1947     $self->log_me("do_renew()");
1948     $self->is_renewal(1);
1949
1950     # Make sure there is an open circ to renew that is not
1951     # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1952     my $circ = $self->editor->search_action_circulation(
1953             { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1954
1955     if(!$circ) {
1956         $circ = $self->editor->search_action_circulation(
1957             { 
1958                 target_copy => $self->copy->id, 
1959                 stop_fines => OILS_STOP_FINES_MAX_FINES,
1960                 checkin_time => undef
1961             } 
1962         )->[0];
1963     }
1964
1965     return $self->bail_on_events($self->editor->event) unless $circ;
1966
1967     # A user is not allowed to renew another user's items without permission
1968     unless( $circ->usr eq $self->editor->requestor->id ) {
1969         return $self->bail_on_events($self->editor->events)
1970             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
1971     }   
1972
1973     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1974         if $circ->renewal_remaining < 1;
1975
1976     # -----------------------------------------------------------------
1977
1978     $self->renewal_remaining( $circ->renewal_remaining - 1 );
1979     $self->circ($circ);
1980
1981     $self->run_renew_permit;
1982
1983     # Check the item in
1984     $self->do_checkin();
1985     return if $self->bail_out;
1986
1987     unless( $self->permit_override ) {
1988         $self->do_permit();
1989         return if $self->bail_out;
1990         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1991         $self->remove_event('ITEM_NOT_CATALOGED');
1992     }   
1993
1994     $self->override_events;
1995     return if $self->bail_out;
1996
1997     $self->events([]);
1998     $self->do_checkout();
1999 }
2000
2001
2002 sub remove_event {
2003     my( $self, $evt ) = @_;
2004     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2005     $logger->debug("circulator: removing event from list: $evt");
2006     my @events = @{$self->events};
2007     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2008 }
2009
2010
2011 sub have_event {
2012     my( $self, $evt ) = @_;
2013     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2014     return grep { $_->{textcode} eq $evt } @{$self->events};
2015 }
2016
2017
2018
2019 sub run_renew_permit {
2020     my $self = shift;
2021    my $runner = $self->script_runner;
2022
2023    $runner->load($self->circ_permit_renew);
2024    my $result = $runner->run or 
2025         throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2026    my $events = $result->{events};
2027
2028    $logger->activity("ciculator: circ_permit_renew for user ".
2029       $self->patron->id." returned events: @$events") if @$events;
2030
2031     $self->push_events(OpenILS::Event->new($_)) for @$events;
2032     
2033     $logger->debug("circulator: re-creating script runner to be safe");
2034     $self->mk_script_runner;
2035 }
2036
2037
2038
2039