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