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