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