]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
running hold-notify login in a transaction to fetch latest objects
[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                 $self->circ(
1278                         $self->editor->search_action_circulation(
1279                         { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1280         }
1281
1282         # if the circ is marked as 'claims returned', add the event to the list
1283         $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1284                 if ($self->circ and $self->circ->stop_fines 
1285                                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1286
1287         # handle the overridable events 
1288         $self->override_events unless $self->is_renewal;
1289         return if $self->bail_out;
1290         
1291         if( $self->copy ) {
1292                 $self->transit(
1293                         $self->editor->search_action_transit_copy(
1294                         { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);     
1295         }
1296
1297         if( $self->circ ) {
1298                 $self->checkin_handle_circ;
1299                 return if $self->bail_out;
1300                 $self->checkin_changed(1);
1301
1302         } elsif( $self->transit ) {
1303                 my $hold_transit = $self->process_received_transit;
1304                 $self->checkin_changed(1);
1305
1306                 if( $self->bail_out ) { 
1307                         $self->checkin_flesh_events;
1308                         return;
1309                 }
1310                 
1311                 if( my $e = $self->check_checkin_copy_status() ) {
1312                         # If the original copy status is special, alert the caller
1313                         my $ev = $self->events;
1314                         $self->events([$e]);
1315                         $self->override_events;
1316                         return if $self->bail_out;
1317                         $self->events($ev);
1318                 }
1319
1320                 if( $hold_transit or 
1321                                 $U->copy_status($self->copy->status)->id 
1322                                         == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1323
1324          my $hold;
1325          if( $hold_transit ) {
1326             $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1327          } else {
1328                                 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1329          }
1330
1331                         $self->hold($hold);
1332
1333                         if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1334
1335                                 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1336                                 $self->reshelve_copy(1);
1337                                 $self->cancelled_hold_transit(1);
1338                                 return if $self->bail_out;
1339
1340                         } else {
1341
1342                                 # hold transited to correct location
1343                                 $self->checkin_flesh_events;
1344                                 return;
1345                         }
1346                 } 
1347
1348         } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1349
1350                 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1351                         " that is in-transit, but there is no transit.. repairing");
1352                 $self->reshelve_copy(1);
1353                 return if $self->bail_out;
1354         }
1355
1356         if( $self->is_renewal ) {
1357                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1358                 return;
1359         }
1360
1361    # ------------------------------------------------------------------------------
1362    # Circulations and transits are now closed where necessary.  Now go on to see if
1363    # this copy can fulfill a hold or needs to be routed to a different location
1364    # ------------------------------------------------------------------------------
1365
1366         if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1367                 return if $self->bail_out;
1368
1369    } else { # not needed for a hold
1370
1371                 my $circ_lib = (ref $self->copy->circ_lib) ? 
1372                                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1373
1374                 if( $self->remote_hold ) {
1375                         $circ_lib = $self->remote_hold->pickup_lib;
1376                         $logger->warn("circulator: Copy ".$self->copy->barcode.
1377                                 " is on a remote hold's shelf, sending to $circ_lib");
1378                 }
1379
1380                 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1381
1382       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1383
1384                         $self->checkin_handle_precat();
1385                         return if $self->bail_out;
1386
1387       } else {
1388
1389                         my $bc = $self->copy->barcode;
1390                         $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1391                         $self->checkin_build_copy_transit($circ_lib);
1392                         return if $self->bail_out;
1393                         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1394       }
1395    }
1396
1397         $self->reshelve_copy;
1398         return if $self->bail_out;
1399
1400         unless($self->checkin_changed) {
1401
1402                 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1403                 my $stat = $U->copy_status($self->copy->status)->id;
1404
1405         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1406          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1407                 $self->bail_out(1); # no need to commit anything
1408
1409         } else {
1410
1411                 $self->push_events(OpenILS::Event->new('SUCCESS')) 
1412                         unless @{$self->events};
1413         }
1414
1415
1416    # ------------------------------------------------------------------------------
1417    # Update the patron penalty info in the DB
1418    # ------------------------------------------------------------------------------
1419    $U->update_patron_penalties(
1420       authtoken => $self->editor->authtoken,
1421       patron    => $self->patron,
1422       background  => 1 ) if $self->is_checkin;
1423
1424         $self->checkin_flesh_events;
1425         return;
1426 }
1427
1428 sub reshelve_copy {
1429    my $self    = shift;
1430    my $force   = $self->force || shift;
1431    my $copy    = $self->copy;
1432
1433    my $stat = $U->copy_status($copy->status)->id;
1434
1435    if($force || (
1436       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1437       $stat != OILS_COPY_STATUS_CATALOGING and
1438       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1439       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1440
1441         $copy->status( OILS_COPY_STATUS_RESHELVING );
1442                         $self->update_copy;
1443                         $self->checkin_changed(1);
1444         }
1445 }
1446
1447
1448 # Returns true if the item is at the current location
1449 # because it was transited there for a hold and the 
1450 # hold has not been fulfilled
1451 sub checkin_check_holds_shelf {
1452         my $self = shift;
1453         return 0 unless $self->copy;
1454
1455         return 0 unless 
1456                 $U->copy_status($self->copy->status)->id ==
1457                         OILS_COPY_STATUS_ON_HOLDS_SHELF;
1458
1459         # find the hold that put us on the holds shelf
1460         my $holds = $self->editor->search_action_hold_request(
1461                 { 
1462                         current_copy => $self->copy->id,
1463                         capture_time => { '!=' => undef },
1464                         fulfillment_time => undef,
1465                         cancel_time => undef,
1466                 }
1467         );
1468
1469         unless(@$holds) {
1470                 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1471                 $self->reshelve_copy(1);
1472                 return 0;
1473         }
1474
1475         my $hold = $$holds[0];
1476
1477         $logger->info("circulator: we found a captured, un-fulfilled hold [".
1478                 $hold->id. "] for copy ".$self->copy->barcode);
1479
1480         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1481                 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1482                 return 1;
1483         }
1484
1485         $logger->info("circulator: hold is not for here..");
1486         $self->remote_hold($hold);
1487         return 0;
1488 }
1489
1490
1491 sub checkin_handle_precat {
1492         my $self        = shift;
1493    my $copy    = $self->copy;
1494
1495    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1496       $copy->status(OILS_COPY_STATUS_CATALOGING);
1497                 $self->update_copy();
1498                 $self->checkin_changed(1);
1499                 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1500    }
1501 }
1502
1503
1504 sub checkin_build_copy_transit {
1505         my $self                        = shift;
1506         my $dest                        = shift;
1507         my $copy       = $self->copy;
1508    my $transit    = Fieldmapper::action::transit_copy->new;
1509
1510         #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1511         $logger->info("circulator: transiting copy to $dest");
1512
1513    $transit->source($self->editor->requestor->ws_ou);
1514    $transit->dest($dest);
1515    $transit->target_copy($copy->id);
1516    $transit->source_send_time('now');
1517    $transit->copy_status( $U->copy_status($copy->status)->id );
1518
1519         $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1520
1521         return $self->bail_on_events($self->editor->event)
1522                 unless $self->editor->create_action_transit_copy($transit);
1523
1524    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1525         $self->update_copy;
1526         $self->checkin_changed(1);
1527 }
1528
1529
1530 sub attempt_checkin_hold_capture {
1531         my $self = shift;
1532         my $copy = $self->copy;
1533
1534         # See if this copy can fulfill any holds
1535         my ($hold) = $holdcode->find_nearest_permitted_hold( 
1536                 $self->editor, $copy, $self->editor->requestor );
1537
1538         if(!$hold) {
1539                 $logger->debug("circulator: no potential permitted".
1540                         "holds found for copy ".$copy->barcode);
1541                 return undef;
1542         }
1543
1544
1545         $logger->info("circulator: found permitted hold ".
1546                 $hold->id . " for copy, capturing...");
1547
1548         $hold->current_copy($copy->id);
1549         $hold->capture_time('now');
1550
1551         # prevent DB errors caused by fetching 
1552         # holds from storage, and updating through cstore
1553         $hold->clear_fulfillment_time;
1554         $hold->clear_fulfillment_staff;
1555         $hold->clear_fulfillment_lib;
1556         $hold->clear_expire_time; 
1557         $hold->clear_cancel_time;
1558         $hold->clear_prev_check_time unless $hold->prev_check_time;
1559
1560         $self->bail_on_events($self->editor->event)
1561                 unless $self->editor->update_action_hold_request($hold);
1562         $self->hold($hold);
1563         $self->checkin_changed(1);
1564
1565         return 1 if $self->bail_out;
1566
1567         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1568
1569                 # This hold was captured in the correct location
1570         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1571                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1572
1573                 #$self->do_hold_notify($hold->id);
1574                 $self->notify_hold($hold->id);
1575
1576         } else {
1577         
1578                 # Hold needs to be picked up elsewhere.  Build a hold
1579                 # transit and route the item.
1580                 $self->checkin_build_hold_transit();
1581         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1582                 return 1 if $self->bail_out;
1583                 $self->push_events(
1584                         OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1585         }
1586
1587         # make sure we save the copy status
1588         $self->update_copy;
1589         return 1;
1590 }
1591
1592 sub do_hold_notify {
1593         my( $self, $holdid ) = @_;
1594
1595         $logger->info("circulator: running delayed hold notify process");
1596
1597 #       my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1598 #               hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1599
1600         my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1601                 hold_id => $holdid, requestor => $self->editor->requestor);
1602
1603         $logger->debug("circulator: built hold notifier");
1604
1605         if(!$notifier->event) {
1606
1607                 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1608
1609                 my $stat = $notifier->send_email_notify;
1610                 if( $stat == '1' ) {
1611                         $logger->info("ciculator: hold notify succeeded for hold $holdid");
1612                         return;
1613                 } 
1614
1615                 $logger->warn("ciculator:  * hold notify failed for hold $holdid");
1616
1617         } else {
1618                 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1619         }
1620 }
1621
1622
1623 sub checkin_build_hold_transit {
1624         my $self = shift;
1625
1626    my $copy = $self->copy;
1627    my $hold = $self->hold;
1628    my $trans = Fieldmapper::action::hold_transit_copy->new;
1629
1630         $logger->debug("circulator: building hold transit for ".$copy->barcode);
1631
1632    $trans->hold($hold->id);
1633    $trans->source($self->editor->requestor->ws_ou);
1634    $trans->dest($hold->pickup_lib);
1635    $trans->source_send_time("now");
1636    $trans->target_copy($copy->id);
1637
1638         # when the copy gets to its destination, it will recover
1639         # this status - put it onto the holds shelf
1640    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1641
1642         return $self->bail_on_events($self->editor->event)
1643                 unless $self->editor->create_action_hold_transit_copy($trans);
1644 }
1645
1646
1647
1648 sub process_received_transit {
1649         my $self = shift;
1650         my $copy = $self->copy;
1651    my $copyid = $self->copy->id;
1652
1653         my $status_name = $U->copy_status($copy->status)->name;
1654    $logger->debug("circulator: attempting transit receive on ".
1655                 "copy $copyid. Copy status is $status_name");
1656
1657         my $transit = $self->transit;
1658
1659    if( $transit->dest != $self->editor->requestor->ws_ou ) {
1660                 my $tid = $transit->id; 
1661       $logger->info("circulator: Fowarding transit on copy which is destined ".
1662          "for a different location. transit=$tid, copy=$copyid,current ".
1663          "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1664
1665                 return $self->bail_on_events(
1666                         OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1667    }
1668
1669    # The transit is received, set the receive time
1670    $transit->dest_recv_time('now');
1671         $self->bail_on_events($self->editor->event)
1672                 unless $self->editor->update_action_transit_copy($transit);
1673
1674         my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1675
1676    $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
1677    $copy->status( $transit->copy_status );
1678         $self->update_copy();
1679         return if $self->bail_out;
1680
1681         my $ishold = 0;
1682         if($hold_transit) {     
1683                 #$self->do_hold_notify($hold_transit->hold);
1684                 $self->notify_hold($hold_transit->hold);
1685                 $ishold = 1;
1686         }
1687
1688         $self->push_events( 
1689                 OpenILS::Event->new(
1690                 'SUCCESS', 
1691                 ishold => $ishold,
1692       payload => { transit => $transit, holdtransit => $hold_transit } ));
1693
1694         return $hold_transit;
1695 }
1696
1697
1698 sub checkin_handle_circ {
1699    my $self = shift;
1700         $U->logmark;
1701
1702    my $circ = $self->circ;
1703    my $copy = $self->copy;
1704    my $evt;
1705    my $obt;
1706
1707    # backdate the circ if necessary
1708    if($self->backdate) {
1709                 $self->checkin_handle_backdate;
1710                 return if $self->bail_out;
1711    }
1712
1713    if(!$circ->stop_fines) {
1714       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1715       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1716       $circ->stop_fines_time('now') unless $self->backdate;
1717       $circ->stop_fines_time($self->backdate) if $self->backdate;
1718    }
1719
1720    # see if there are any fines owed on this circ.  if not, close it
1721         ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1722    $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1723
1724         $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1725
1726    # Set the checkin vars since we have the item
1727         $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
1728
1729    $circ->checkin_staff($self->editor->requestor->id);
1730    $circ->checkin_lib($self->editor->requestor->ws_ou);
1731
1732         my $circ_lib = (ref $self->copy->circ_lib) ?  
1733                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1734         my $stat = $U->copy_status($self->copy->status)->id;
1735
1736         # If the item is lost/missing and it needs to be sent home, don't 
1737         # reshelve the copy, leave it lost/missing so the recipient will know
1738         if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1739                 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1740                 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1741
1742         } else {
1743                 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1744                 $self->update_copy;
1745         }
1746
1747         return $self->bail_on_events($self->editor->event)
1748                 unless $self->editor->update_action_circulation($circ);
1749 }
1750
1751
1752 sub checkin_handle_backdate {
1753         my $self = shift;
1754
1755         my $bd = $self->backdate;
1756
1757         # ------------------------------------------------------------------
1758         # clean up the backdate for date comparison
1759         # we want any bills created on or after the backdate
1760         # ------------------------------------------------------------------
1761         $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1762         #$bd = "${bd}T23:59:59";
1763
1764         my $bills = $self->editor->search_money_billing(
1765                 { 
1766                         billing_ts => { '>=' => $bd }, 
1767                         xact => $self->circ->id, 
1768                         billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1769                 }
1770         );
1771
1772         $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1773
1774         for my $bill (@$bills) {        
1775                 unless( $U->is_true($bill->voided) ) {
1776                         $logger->info("backdate voiding bill ".$bill->id);
1777                         $bill->voided('t');
1778                         $bill->void_time('now');
1779                         $bill->voider($self->editor->requestor->id);
1780                         my $n = $bill->note || "";
1781                         $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1782
1783                         $self->bail_on_events($self->editor->event)
1784                                 unless $self->editor->update_money_billing($bill);
1785                 }
1786         }
1787 }
1788
1789
1790
1791 =head
1792 # XXX Legacy version for Circ.pm support
1793 sub _checkin_handle_backdate {
1794    my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1795
1796         my $bd = $backdate;
1797         $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1798         $bd = "${bd}T23:59:59";
1799
1800    my $bills = $session->request(
1801       "open-ils.storage.direct.money.billing.search_where.atomic",
1802                 billing_ts => { '>=' => $bd }, 
1803                 xact => $circ->id,
1804                 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1805         )->gather(1);
1806
1807         $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1808
1809    if($bills) {
1810       for my $bill (@$bills) {
1811                         unless( $U->is_true($bill->voided) ) {
1812                                 $logger->debug("voiding bill ".$bill->id);
1813                                 $bill->voided('t');
1814                                 $bill->void_time('now');
1815                                 $bill->voider($requestor->id);
1816                                 my $n = $bill->note || "";
1817                                 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1818                                 my $s = $session->request(
1819                                         "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1820                                 return $U->DB_UPDATE_FAILED($bill) unless $s;
1821                         }
1822                 }
1823    }
1824
1825         return 100;
1826 }
1827 =cut
1828
1829
1830
1831
1832
1833
1834 sub find_patron_from_copy {
1835         my $self = shift;
1836         my $circs = $self->editor->search_action_circulation(
1837                 { target_copy => $self->copy->id, checkin_time => undef });
1838         my $circ = $circs->[0];
1839         return unless $circ;
1840         my $u = $self->editor->retrieve_actor_user($circ->usr)
1841                 or return $self->bail_on_events($self->editor->event);
1842         $self->patron($u);
1843 }
1844
1845 sub check_checkin_copy_status {
1846         my $self = shift;
1847    my $copy = $self->copy;
1848
1849    my $islost     = 0;
1850    my $ismissing  = 0;
1851    my $evt        = undef;
1852
1853    my $status = $U->copy_status($copy->status)->id;
1854
1855    return undef
1856       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
1857             $status == OILS_COPY_STATUS_CHECKED_OUT ||
1858             $status == OILS_COPY_STATUS_IN_PROCESS  ||
1859             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
1860             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
1861             $status == OILS_COPY_STATUS_CATALOGING  ||
1862             $status == OILS_COPY_STATUS_RESHELVING );
1863
1864    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1865       if( $status == OILS_COPY_STATUS_LOST );
1866
1867    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1868       if( $status == OILS_COPY_STATUS_MISSING );
1869
1870    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1871 }
1872
1873
1874
1875 # --------------------------------------------------------------------------
1876 # On checkin, we need to return as many relevant objects as we can
1877 # --------------------------------------------------------------------------
1878 sub checkin_flesh_events {
1879         my $self = shift;
1880
1881         if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
1882                 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1883                         $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1884         }
1885
1886
1887         for my $evt (@{$self->events}) {
1888
1889                 my $payload          = {};
1890                 $payload->{copy}     = $U->unflesh_copy($self->copy);
1891                 $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1892                 $payload->{circ}     = $self->circ;
1893                 $payload->{transit}  = $self->transit;
1894                 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
1895
1896                 # $self->hold may or may not have been replaced with a 
1897                 # valid hold after processing a cancelled hold
1898                 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
1899
1900                 $evt->{payload} = $payload;
1901         }
1902 }
1903
1904 sub log_me {
1905         my( $self, $msg ) = @_;
1906         my $bc = ($self->copy) ? $self->copy->barcode :
1907                 $self->barcode;
1908         $bc ||= "";
1909         my $usr = ($self->patron) ? $self->patron->id : "";
1910         $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1911                 ", recipient=$usr, copy=$bc");
1912 }
1913
1914
1915 sub do_renew {
1916         my $self = shift;
1917         $self->log_me("do_renew()");
1918         $self->is_renewal(1);
1919
1920         unless( $self->is_renewal ) {
1921                 return $self->bail_on_events($self->editor->events)
1922                         unless $self->editor->allowed('RENEW_CIRC');
1923         }       
1924
1925         # Make sure there is an open circ to renew that is not
1926         # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1927         my $circ = $self->editor->search_action_circulation(
1928                         { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1929
1930         if(!$circ) {
1931                 $circ = $self->editor->search_action_circulation(
1932                         { 
1933                                 target_copy => $self->copy->id, 
1934                                 stop_fines => OILS_STOP_FINES_MAX_FINES,
1935                                 checkin_time => undef
1936                         } 
1937                 )->[0];
1938         }
1939
1940         return $self->bail_on_events($self->editor->event) unless $circ;
1941
1942         $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1943                 if $circ->renewal_remaining < 1;
1944
1945         # -----------------------------------------------------------------
1946
1947         $self->renewal_remaining( $circ->renewal_remaining - 1 );
1948         $self->circ($circ);
1949
1950         $self->run_renew_permit;
1951
1952         # Check the item in
1953         $self->do_checkin();
1954         return if $self->bail_out;
1955
1956         unless( $self->permit_override ) {
1957                 $self->do_permit();
1958                 return if $self->bail_out;
1959                 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1960                 $self->remove_event('ITEM_NOT_CATALOGED');
1961         }       
1962
1963         $self->override_events;
1964         return if $self->bail_out;
1965
1966         $self->events([]);
1967         $self->do_checkout();
1968 }
1969
1970
1971 sub remove_event {
1972         my( $self, $evt ) = @_;
1973         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1974         $logger->debug("circulator: removing event from list: $evt");
1975         my @events = @{$self->events};
1976         $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1977 }
1978
1979
1980 sub have_event {
1981         my( $self, $evt ) = @_;
1982         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1983         return grep { $_->{textcode} eq $evt } @{$self->events};
1984 }
1985
1986
1987
1988 sub run_renew_permit {
1989         my $self = shift;
1990    my $runner = $self->script_runner;
1991
1992    $runner->load($self->circ_permit_renew);
1993    my $result = $runner->run or 
1994                 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1995    my $events = $result->{events};
1996
1997    $logger->activity("ciculator: circ_permit_renew for user ".
1998       $self->patron->id." returned events: @$events") if @$events;
1999
2000         $self->push_events(OpenILS::Event->new($_)) for @$events;
2001         
2002         $logger->debug("circulator: re-creating script runner to be safe");
2003         $self->mk_script_runner;
2004 }
2005
2006
2007
2008