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