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