]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
returning copy location info on hold delay event
[working/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 'OpenILS::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 use OpenILS::Application::AppUtils;
9 my $U = "OpenILS::Application::AppUtils";
10
11 my %scripts;
12 my $script_libs;
13 my $legacy_script_support = 0;
14
15 sub initialize {
16
17     my $self = shift;
18     my $conf = OpenSRF::Utils::SettingsClient->new;
19     my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
20     my @pfx = ( @pfx2, "scripts" );
21
22     my $p       = $conf->config_value(  @pfx, 'circ_permit_patron' );
23     my $c       = $conf->config_value(  @pfx, 'circ_permit_copy' );
24     my $d       = $conf->config_value(  @pfx, 'circ_duration' );
25     my $f       = $conf->config_value(  @pfx, 'circ_recurring_fines' );
26     my $m       = $conf->config_value(  @pfx, 'circ_max_fines' );
27     my $pr  = $conf->config_value(  @pfx, 'circ_permit_renew' );
28     my $lb  = $conf->config_value(  @pfx2, 'script_path' );
29
30     $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
31     $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
32
33     $logger->error( "Missing circ script(s)" ) 
34         unless( $p and $c and $d and $f and $m and $pr );
35
36     $scripts{circ_permit_patron}    = $p;
37     $scripts{circ_permit_copy}      = $c;
38     $scripts{circ_duration}         = $d;
39     $scripts{circ_recurring_fines}= $f;
40     $scripts{circ_max_fines}        = $m;
41     $scripts{circ_permit_renew} = $pr;
42
43     $lb = [ $lb ] unless ref($lb);
44     $script_libs = $lb;
45
46     $logger->debug(
47         "circulator: Loaded rules scripts for circ: " .
48         "circ permit patron = $p, ".
49         "circ permit copy = $c, ".
50         "circ duration = $d, ".
51         "circ recurring fines = $f, " .
52         "circ max fines = $m, ".
53         "circ renew permit = $pr.  ".
54         "lib paths = @$lb. ".
55         "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
56         );
57 }
58
59
60 __PACKAGE__->register_method(
61     method  => "run_method",
62     api_name    => "open-ils.circ.checkout.permit",
63     notes       => q/
64         Determines if the given checkout can occur
65         @param authtoken The login session key
66         @param params A trailing hash of named params including 
67             barcode : The copy barcode, 
68             patron : The patron the checkout is occurring for, 
69             renew : true or false - whether or not this is a renewal
70         @return The event that occurred during the permit check.  
71     /);
72
73
74 __PACKAGE__->register_method (
75     method      => 'run_method',
76     api_name        => 'open-ils.circ.checkout.permit.override',
77     signature   => q/@see open-ils.circ.checkout.permit/,
78 );
79
80
81 __PACKAGE__->register_method(
82     method  => "run_method",
83     api_name    => "open-ils.circ.checkout",
84     notes => q/
85         Checks out an item
86         @param authtoken The login session key
87         @param params A named hash of params including:
88             copy            The copy object
89             barcode     If no copy is provided, the copy is retrieved via barcode
90             copyid      If no copy or barcode is provide, the copy id will be use
91             patron      The patron's id
92             noncat      True if this is a circulation for a non-cataloted item
93             noncat_type The non-cataloged type id
94             noncat_circ_lib The location for the noncat circ.  
95             precat      The item has yet to be cataloged
96             dummy_title The temporary title of the pre-cataloded item
97             dummy_author The temporary authr of the pre-cataloded item
98                 Default is the home org of the staff member
99         @return The SUCCESS event on success, any other event depending on the error
100     /);
101
102 __PACKAGE__->register_method(
103     method  => "run_method",
104     api_name    => "open-ils.circ.checkin",
105     argc        => 2,
106     signature   => q/
107         Generic super-method for handling all copies
108         @param authtoken The login session key
109         @param params Hash of named parameters including:
110             barcode - The copy barcode
111             force       - If true, copies in bad statuses will be checked in and give good statuses
112             ...
113     /
114 );
115
116 __PACKAGE__->register_method(
117     method  => "run_method",
118     api_name    => "open-ils.circ.checkin.override",
119     signature   => q/@see open-ils.circ.checkin/
120 );
121
122 __PACKAGE__->register_method(
123     method  => "run_method",
124     api_name    => "open-ils.circ.renew.override",
125     signature   => q/@see open-ils.circ.renew/,
126 );
127
128
129 __PACKAGE__->register_method(
130     method  => "run_method",
131     api_name    => "open-ils.circ.renew",
132     notes       => <<"    NOTES");
133     PARAMS( authtoken, circ => circ_id );
134     open-ils.circ.renew(login_session, circ_object);
135     Renews the provided circulation.  login_session is the requestor of the
136     renewal and if the logged in user is not the same as circ->usr, then
137     the logged in user must have RENEW_CIRC permissions.
138     NOTES
139
140 __PACKAGE__->register_method(
141     method  => "run_method",
142     api_name    => "open-ils.circ.checkout.full");
143 __PACKAGE__->register_method(
144     method  => "run_method",
145     api_name    => "open-ils.circ.checkout.full.override");
146
147 __PACKAGE__->register_method(
148     method  => "run_method",
149     api_name    => "open-ils.circ.checkout.inspect",
150     desc => q/
151         Returns the circ matrix test result and, on success, the rule set and matrix test object
152     /
153 );
154
155
156
157 sub run_method {
158     my( $self, $conn, $auth, $args ) = @_;
159     translate_legacy_args($args);
160     my $api = $self->api_name;
161
162     my $circulator = 
163         OpenILS::Application::Circ::Circulator->new($auth, %$args);
164
165     return circ_events($circulator) if $circulator->bail_out;
166
167     # --------------------------------------------------------------------------
168     # Go ahead and load the script runner to make sure we have all 
169     # of the objects we need
170     # --------------------------------------------------------------------------
171     $circulator->is_renewal(1) if $api =~ /renew/;
172     $circulator->is_checkin(1) if $api =~ /checkin/;
173     $circulator->check_penalty_on_renew(1) if
174         $circulator->is_renewal and $U->ou_ancestor_setting_value(
175             $circulator->circ_lib, 'circ.renew.check_penalty', $circulator->editor);
176
177     if($legacy_script_support and not $circulator->is_checkin) {
178         $circulator->mk_script_runner();
179         $circulator->legacy_script_support(1);
180         $circulator->circ_permit_patron($scripts{circ_permit_patron});
181         $circulator->circ_permit_copy($scripts{circ_permit_copy});      
182         $circulator->circ_duration($scripts{circ_duration});             
183         $circulator->circ_permit_renew($scripts{circ_permit_renew});
184     } else {
185         $circulator->mk_env();
186     }
187     return circ_events($circulator) if $circulator->bail_out;
188
189     
190     $circulator->override(1) if $api =~ /override/o;
191
192     if( $api =~ /checkout\.permit/ ) {
193         $circulator->do_permit();
194
195     } elsif( $api =~ /checkout.full/ ) {
196
197         # requesting a precat checkout implies that any required
198         # overrides have been performed.  Go ahead and re-override.
199         $circulator->override(1) if $circulator->request_precat;
200         $circulator->do_permit();
201         unless( $circulator->bail_out ) {
202             $circulator->events([]);
203             $circulator->do_checkout();
204         }
205
206     } elsif( $api =~ /inspect/ ) {
207         my $data = $circulator->do_inspect();
208         $circulator->editor->rollback;
209         return $data;
210
211     } elsif( $api =~ /checkout/ ) {
212         $circulator->do_checkout();
213
214     } elsif( $api =~ /checkin/ ) {
215         $circulator->do_checkin();
216
217     } elsif( $api =~ /renew/ ) {
218         $circulator->is_renewal(1);
219         $circulator->do_renew();
220     }
221
222     if( $circulator->bail_out ) {
223
224         my @ee;
225         # make sure no success event accidentally slip in
226         $circulator->events(
227             [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
228
229         # Log the events
230         my @e = @{$circulator->events};
231         push( @ee, $_->{textcode} ) for @e;
232         $logger->info("circulator: bailing out with events: @ee");
233
234         $circulator->editor->rollback;
235
236     } else {
237         $circulator->editor->commit;
238     }
239
240     $circulator->script_runner->cleanup if $circulator->script_runner;
241     
242     $conn->respond_complete(circ_events($circulator));
243
244     unless($circulator->bail_out) {
245         $circulator->do_hold_notify($circulator->notify_hold)
246             if $circulator->notify_hold;
247         $circulator->retarget_holds if $circulator->retarget;
248     }
249 }
250
251 sub circ_events {
252     my $circ = shift;
253     my @e = @{$circ->events};
254     # if we have multiple events, SUCCESS should not be one of them;
255     @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
256     return (@e == 1) ? $e[0] : \@e;
257 }
258
259
260 sub translate_legacy_args {
261     my $args = shift;
262
263     if( $$args{barcode} ) {
264         $$args{copy_barcode} = $$args{barcode};
265         delete $$args{barcode};
266     }
267
268     if( $$args{copyid} ) {
269         $$args{copy_id} = $$args{copyid};
270         delete $$args{copyid};
271     }
272
273     if( $$args{patronid} ) {
274         $$args{patron_id} = $$args{patronid};
275         delete $$args{patronid};
276     }
277
278     if( $$args{patron} and !ref($$args{patron}) ) {
279         $$args{patron_id} = $$args{patron};
280         delete $$args{patron};
281     }
282
283
284     if( $$args{noncat} ) {
285         $$args{is_noncat} = $$args{noncat};
286         delete $$args{noncat};
287     }
288
289     if( $$args{precat} ) {
290         $$args{is_precat} = $$args{request_precat} = $$args{precat};
291         delete $$args{precat};
292     }
293 }
294
295
296
297 # --------------------------------------------------------------------------
298 # This package actually manages all of the circulation logic
299 # --------------------------------------------------------------------------
300 package OpenILS::Application::Circ::Circulator;
301 use strict; use warnings;
302 use vars q/$AUTOLOAD/;
303 use DateTime;
304 use OpenILS::Utils::Fieldmapper;
305 use OpenSRF::Utils::Cache;
306 use Digest::MD5 qw(md5_hex);
307 use DateTime::Format::ISO8601;
308 use OpenILS::Utils::PermitHold;
309 use OpenSRF::Utils qw/:datetime/;
310 use OpenSRF::Utils::SettingsClient;
311 use OpenILS::Application::Circ::Holds;
312 use OpenILS::Application::Circ::Transit;
313 use OpenSRF::Utils::Logger qw(:logger);
314 use OpenILS::Utils::CStoreEditor qw/:funcs/;
315 use OpenILS::Application::Circ::ScriptBuilder;
316 use OpenILS::Const qw/:const/;
317
318 my $holdcode    = "OpenILS::Application::Circ::Holds";
319 my $transcode   = "OpenILS::Application::Circ::Transit";
320
321 sub DESTROY { }
322
323
324 # --------------------------------------------------------------------------
325 # Add a pile of automagic getter/setter methods
326 # --------------------------------------------------------------------------
327 my @AUTOLOAD_FIELDS = qw/
328     notify_hold
329     penalty_request
330     remote_hold
331     backdate
332     copy
333     copy_id
334     copy_barcode
335     patron
336     patron_id
337     patron_barcode
338     script_runner
339     volume
340     title
341     is_renewal
342     check_penalty_on_renew
343     is_noncat
344     is_precat
345     request_precat
346     is_checkin
347     noncat_type
348     editor
349     events
350     cache_handle
351     override
352     circ_permit_patron
353     circ_permit_copy
354     circ_duration
355     circ_recurring_fines
356     circ_max_fines
357     circ_permit_renew
358     circ
359     transit
360     hold
361     permit_key
362     noncat_circ_lib
363     noncat_count
364     checkout_time
365     dummy_title
366     dummy_author
367     circ_lib
368     barcode
369     duration_level
370     recurring_fines_level
371     duration_rule
372     recurring_fines_rule
373     max_fine_rule
374     renewal_remaining
375     due_date
376     fulfilled_holds
377     transit
378     checkin_changed
379     force
380     old_circ
381     permit_override
382     pending_checkouts
383     cancelled_hold_transit
384     opac_renewal
385     phone_renewal
386     desk_renewal
387     retarget
388     circ_test_success
389     matrix_test_result
390     circ_matrix_test
391     circ_matrix_ruleset
392     legacy_script_support
393     is_deposit
394     is_rental
395     deposit_billing
396     rental_billing
397     capture
398     noop
399 /;
400
401
402 sub AUTOLOAD {
403     my $self = shift;
404     my $type = ref($self) or die "$self is not an object";
405     my $data = shift;
406     my $name = $AUTOLOAD;
407     $name =~ s/.*://o;   
408
409     unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
410         $logger->error("circulator: $type: invalid autoload field: $name");
411         die "$type: invalid autoload field: $name\n" 
412     }
413
414     {
415         no strict 'refs';
416         *{"${type}::${name}"} = sub {
417             my $s = shift;
418             my $v = shift;
419             $s->{$name} = $v if defined $v;
420             return $s->{$name};
421         }
422     }
423     return $self->$name($data);
424 }
425
426
427 sub new {
428     my( $class, $auth, %args ) = @_;
429     $class = ref($class) || $class;
430     my $self = bless( {}, $class );
431
432     $self->events([]);
433     $self->editor(new_editor(xact => 1, authtoken => $auth));
434
435     unless( $self->editor->checkauth ) {
436         $self->bail_on_events($self->editor->event);
437         return $self;
438     }
439
440     $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
441
442     $self->$_($args{$_}) for keys %args;
443
444     $self->circ_lib(
445         ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
446
447     # if this is a renewal, default to desk_renewal
448     $self->desk_renewal(1) unless 
449         $self->opac_renewal or $self->phone_renewal;
450
451     $self->capture('') unless $self->capture;
452
453     return $self;
454 }
455
456
457 # --------------------------------------------------------------------------
458 # True if we should discontinue processing
459 # --------------------------------------------------------------------------
460 sub bail_out {
461     my( $self, $bool ) = @_;
462     if( defined $bool ) {
463         $logger->info("circulator: BAILING OUT") if $bool;
464         $self->{bail_out} = $bool;
465     }
466     return $self->{bail_out};
467 }
468
469
470 sub push_events {
471     my( $self, @evts ) = @_;
472     for my $e (@evts) {
473         next unless $e;
474         $logger->info("circulator: pushing event ".$e->{textcode});
475         push( @{$self->events}, $e ) unless
476             grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
477     }
478 }
479
480 sub mk_permit_key {
481     my $self = shift;
482     my $key = md5_hex( time() . rand() . "$$" );
483     $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
484     return $self->permit_key($key);
485 }
486
487 sub check_permit_key {
488     my $self = shift;
489     my $key = $self->permit_key;
490     return 0 unless $key;
491     my $k = "oils_permit_key_$key";
492     my $one = $self->cache_handle->get_cache($k);
493     $self->cache_handle->delete_cache($k);
494     return ($one) ? 1 : 0;
495 }
496
497 sub mk_env {
498     my $self = shift;
499     my $e = $self->editor;
500
501     # --------------------------------------------------------------------------
502     # Grab the fleshed copy
503     # --------------------------------------------------------------------------
504     unless($self->is_noncat) {
505         my $copy;
506             my $flesh = { 
507                     flesh => 2, 
508                     flesh_fields => {acp => ['call_number'], acn => ['record']} 
509             };
510             if($self->copy_id) {
511                     $copy = $e->retrieve_asset_copy(
512                             [$self->copy_id, $flesh ]) or return $e->event;
513     
514             } elsif( $self->copy_barcode ) {
515     
516                     $copy = $e->search_asset_copy(
517                             [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
518             }
519     
520         if($copy) {
521             $self->copy($copy);
522             $self->volume($copy->call_number);
523             $self->title($self->volume->record);
524             $self->copy->call_number($self->volume->id);
525             $self->volume->record($self->title->id);
526             $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
527             if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
528                 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
529                 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
530             }
531         } else {
532             # We can't renew if there is no copy
533             return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
534                 if $self->is_renewal;
535             $self->is_precat(1);
536         }
537     }
538
539     return undef if $self->is_checkin;
540
541     # --------------------------------------------------------------------------
542     # Grab the patron
543     # --------------------------------------------------------------------------
544     my $patron;
545         if( $self->patron_id ) {
546                 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
547
548         } elsif( $self->patron_barcode ) {
549
550                 my $card = $e->search_actor_card( 
551                         {barcode => $self->patron_barcode})->[0] or return $e->event;
552
553                 $patron = $e->search_actor_user( 
554                         {card => $card->id})->[0] or return $e->event;
555
556         } else {
557                 if( my $copy = $self->copy ) {
558                         my $circs = $e->search_action_circulation(
559                                 {target_copy => $copy->id, checkin_time => undef});
560
561                         if( my $circ = $circs->[0] ) {
562                                 $patron = $e->retrieve_actor_user($circ->usr)
563                                         or return $e->event;
564                         }
565                 }
566         }
567
568     return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
569         unless $self->patron($patron);
570 }
571
572 # --------------------------------------------------------------------------
573 # This builds the script runner environment and fetches most of the
574 # objects we need
575 # --------------------------------------------------------------------------
576 sub mk_script_runner {
577     my $self = shift;
578     my $args = {};
579
580
581     my @fields = 
582         qw/copy copy_barcode copy_id patron 
583             patron_id patron_barcode volume title editor/;
584
585     # Translate our objects into the ScriptBuilder args hash
586     $$args{$_} = $self->$_() for @fields;
587
588     $args->{ignore_user_status} = 1 if $self->is_checkin;
589     $$args{fetch_patron_by_circ_copy} = 1;
590     $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
591
592     if( my $pco = $self->pending_checkouts ) {
593         $logger->info("circulator: we were given a pending checkouts number of $pco");
594         $$args{patronItemsOut} = $pco;
595     }
596
597     # This fetches most of the objects we need
598     $self->script_runner(
599         OpenILS::Application::Circ::ScriptBuilder->build($args));
600
601     # Now we translate the ScriptBuilder objects back into self
602     $self->$_($$args{$_}) for @fields;
603
604     my @evts = @{$args->{_events}} if $args->{_events};
605
606     $logger->debug("circulator: script builder returned events: @evts") if @evts;
607
608
609     if(@evts) {
610         # Anything besides ASSET_COPY_NOT_FOUND will stop processing
611         if(!$self->is_noncat and 
612             @evts == 1 and 
613             $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
614                 $self->is_precat(1);
615
616         } else {
617             my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
618             return $self->bail_on_events(@e);
619         }
620     }
621
622     if($self->copy) {
623         $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
624         if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
625             $self->is_deposit(1) if $U->is_true($self->copy->deposit);
626             $self->is_rental(1) unless $U->is_true($self->copy->deposit);
627         }
628     }
629
630     # We can't renew if there is no copy
631     return $self->bail_on_events(@evts) if 
632         $self->is_renewal and !$self->copy;
633
634     # Set some circ-specific flags in the script environment
635     my $evt = "environment";
636     $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
637
638     if( $self->is_noncat ) {
639       $self->script_runner->insert("$evt.isNonCat", 1);
640       $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
641     }
642
643     if( $self->is_precat ) {
644         $self->script_runner->insert("environment.isPrecat", 1, 1);
645     }
646
647     $self->script_runner->add_path( $_ ) for @$script_libs;
648
649     return 1;
650 }
651
652 # --------------------------------------------------------------------------
653 # Does the circ permit work
654 # --------------------------------------------------------------------------
655 sub do_permit {
656     my $self = shift;
657
658     $self->log_me("do_permit()");
659
660     unless( $self->editor->requestor->id == $self->patron->id ) {
661         return $self->bail_on_events($self->editor->event)
662             unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
663     }
664
665     $self->check_captured_holds();
666     $self->do_copy_checks();
667     return if $self->bail_out;
668     $self->run_patron_permit_scripts();
669     $self->run_copy_permit_scripts() 
670         unless $self->is_precat or $self->is_noncat;
671     $self->check_item_deposit_events();
672     $self->override_events() unless 
673         $self->is_renewal and not $self->check_penalty_on_renew;
674     return if $self->bail_out;
675
676     if($self->is_precat and not $self->request_precat) {
677         $self->push_events(
678             OpenILS::Event->new(
679                 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
680         return $self->bail_out(1) unless $self->is_renewal;
681     }
682
683     $self->push_events(
684       OpenILS::Event->new(
685             'SUCCESS', 
686             payload => $self->mk_permit_key));
687 }
688
689 sub check_item_deposit_events {
690     my $self = shift;
691     $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED')) 
692         if $self->is_deposit and not $self->is_deposit_exempt;
693     $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED')) 
694         if $self->is_rental and not $self->is_rental_exempt;
695 }
696
697 # returns true if the user is not required to pay deposits
698 sub is_deposit_exempt {
699     my $self = shift;
700     my $pid = (ref $self->patron->profile) ?
701         $self->patron->profile->id : $self->patron->profile;
702     my $groups = $U->ou_ancestor_setting_value(
703         $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
704     return 1 if $groups and grep {$_ == $pid} @$groups;
705     return 0;
706 }
707
708 # returns true if the user is not required to pay rental fees
709 sub is_rental_exempt {
710     my $self = shift;
711     my $pid = (ref $self->patron->profile) ?
712         $self->patron->profile->id : $self->patron->profile;
713     my $groups = $U->ou_ancestor_setting_value(
714         $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
715     return 1 if $groups and grep {$_ == $pid} @$groups;
716     return 0;
717 }
718
719 sub check_captured_holds {
720    my $self    = shift;
721    my $copy    = $self->copy;
722    my $patron  = $self->patron;
723
724     return undef unless $copy;
725
726     my $s = $U->copy_status($copy->status)->id;
727     return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
728     $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
729
730     # Item is on the holds shelf, make sure it's going to the right person
731     my $holds   = $self->editor->search_action_hold_request(
732         [
733             { 
734                 current_copy        => $copy->id , 
735                 capture_time        => { '!=' => undef },
736                 cancel_time         => undef, 
737                 fulfillment_time    => undef 
738             },
739             { limit => 1 }
740         ]
741     );
742
743     if( $holds and $$holds[0] ) {
744         return undef if $$holds[0]->usr == $patron->id;
745     }
746
747     $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
748
749     $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
750 }
751
752
753 sub do_copy_checks {
754     my $self = shift;
755     my $copy = $self->copy;
756     return unless $copy;
757
758     my $stat = $U->copy_status($copy->status)->id;
759
760     # We cannot check out a copy if it is in-transit
761     if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
762         return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
763     }
764
765     $self->handle_claims_returned();
766     return if $self->bail_out;
767
768     # no claims returned circ was found, check if there is any open circ
769     unless( $self->is_renewal ) {
770         my $circs = $self->editor->search_action_circulation(
771             { target_copy => $copy->id, checkin_time => undef }
772         );
773
774         return $self->bail_on_events(
775             OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
776     }
777 }
778
779
780 sub send_penalty_request {
781     my $self = shift;
782     my $ses = OpenSRF::AppSession->create('open-ils.penalty');
783     $self->penalty_request(
784         $ses->request(  
785             'open-ils.penalty.patron_penalty.calculate', 
786             {   update => 1, 
787                 authtoken => $self->editor->authtoken,
788                 patron => $self->patron } ) );
789 }
790
791 sub gather_penalty_request {
792     my $self = shift;
793     return [] unless $self->penalty_request;
794     my $data = $self->penalty_request->recv;
795     if( ref $data ) {
796         throw $data if UNIVERSAL::isa($data,'Error');
797         $data = $data->content;
798         return $data->{fatal_penalties};
799     }
800     $logger->error("circulator: penalty request returned no data");
801     return [];
802 }
803
804 my $LEGACY_CIRC_EVENT_MAP = {
805     'actor.usr.barred' => 'PATRON_BARRED',
806     'asset.copy.circulate' =>  'COPY_CIRC_NOT_ALLOWED',
807     'asset.copy.status' => 'COPY_NOT_AVAILABLE',
808     'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
809     'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
810     'config.circ_matrix_test.max_items_out' =>  'PATRON_EXCEEDS_CHECKOUT_COUNT',
811     'config.circ_matrix_test.max_overdue' =>  'PATRON_EXCEEDS_OVERDUE_COUNT',
812     'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
813     'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
814 };
815
816
817 # ---------------------------------------------------------------------
818 # This pushes any patron-related events into the list but does not
819 # set bail_out for any events
820 # ---------------------------------------------------------------------
821 sub run_patron_permit_scripts {
822     my $self        = shift;
823     my $runner      = $self->script_runner;
824     my $patronid    = $self->patron->id;
825
826     my @allevents; 
827
828     if(!$self->legacy_script_support) {
829
830         my $results = $self->run_indb_circ_test;
831         unless($self->circ_test_success) {
832             push(@allevents, OpenILS::Event->new(
833                 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
834         }
835
836     } else {
837
838         $self->send_penalty_request() unless
839             $self->is_renewal and not $self->check_penalty_on_renew;
840
841
842         # --------------------------------------------------------------------- # Now run the patron permit script 
843         # ---------------------------------------------------------------------
844         $runner->load($self->circ_permit_patron);
845         my $result = $runner->run or 
846             throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
847
848         my $patron_events = $result->{events};
849
850         my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ? 
851             [] : $self->gather_penalty_request();
852
853         push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
854     }
855
856     $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
857
858     $self->push_events(@allevents);
859 }
860
861 sub run_indb_circ_test {
862     my $self = shift;
863     return $self->matrix_test_result if $self->matrix_test_result;
864
865     my $dbfunc = ($self->is_renewal) ? 
866         'action.item_user_renew_test' : 'action.item_user_circ_test';
867
868     my $results = ($self->matrix_test_result) ? 
869         $self->matrix_test_result : 
870             $self->editor->json_query(
871                 {   from => [
872                         $dbfunc,
873                         $self->editor->requestor->ws_ou,
874                         ($self->is_precat) ? undef : $self->copy->id, 
875                         $self->patron->id,
876                     ]
877                 }
878             );
879
880     $self->circ_test_success($U->is_true($results->[0]->{success}));
881     if($self->circ_test_success) {
882         $self->circ_matrix_test(
883             $self->editor->retrieve_config_circ_matrix_test(
884                 $results->[0]->{matchpoint}
885             )
886         );
887     }
888
889     if($self->circ_test_success) {
890         $self->circ_matrix_ruleset(
891             $self->editor->retrieve_config_circ_matrix_ruleset([
892                 $results->[0]->{matchpoint},
893                 {   flesh => 1,
894                     flesh_fields => {
895                         'ccmrs' => ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']
896                     }
897                 }
898                 ]
899             )
900         );
901     }
902
903     return $self->matrix_test_result($results);
904 }
905
906 sub do_inspect {
907     my $self = shift;
908     $self->run_indb_circ_test;
909
910     my $results = {
911         circ_test_success => $self->circ_test_success,
912         failure_events => [],
913         failure_codes => [],
914     };
915
916     unless($self->circ_test_success) {
917         push(@{$results->{failure_codes}}, 
918             $_->{fail_part}) for @{$self->matrix_test_result};
919         push(@{$results->{failure_events}}, 
920             $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) 
921                 for @{$self->matrix_test_result};
922         return $results;
923     }
924
925     my $duration_rule = $self->circ_matrix_ruleset->duration_rule;
926     my $recurring_fine_rule = $self->circ_matrix_ruleset->recurring_fine_rule;
927     my $max_fine_rule = $self->circ_matrix_ruleset->max_fine_rule;
928
929     my $policy = $self->get_circ_policy(
930         $duration_rule, $recurring_fine_rule, $max_fine_rule);
931
932     $$results{$_} = $$policy{$_} for keys %$policy;
933     return $results;
934 }
935
936 # ---------------------------------------------------------------------
937 # Loads the circ policy info for duration, recurring fine, and max
938 # fine based on the current copy
939 # ---------------------------------------------------------------------
940 sub get_circ_policy {
941     my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
942
943     my $policy = {
944         duration_rule => $duration_rule->name,
945         recurring_fine_rule => $recurring_fine_rule->name,
946         max_fine_rule => $max_fine_rule->name,
947         max_fine => $self->get_max_fine_amount($max_fine_rule),
948         fine_interval => $recurring_fine_rule->recurance_interval,
949         renewal_remaining => $duration_rule->max_renewals
950     };
951
952     $policy->{duration} = $duration_rule->shrt
953         if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
954     $policy->{duration} = $duration_rule->normal
955         if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
956     $policy->{duration} = $duration_rule->extended
957         if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
958
959     $policy->{recurring_fine} = $recurring_fine_rule->low
960         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
961     $policy->{recurring_fine} = $recurring_fine_rule->normal
962         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
963     $policy->{recurring_fine} = $recurring_fine_rule->high
964         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
965
966     return $policy;
967 }
968
969 sub get_max_fine_amount {
970     my $self = shift;
971     my $max_fine_rule = shift;
972     my $max_amount = $max_fine_rule->amount;
973
974     # if is_percent is true then the max->amount is
975     # use as a percentage of the copy price
976     if ($U->is_true($max_fine_rule->is_percent)) {
977         my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
978         $max_amount = $price * $max_fine_rule->amount / 100;
979     }  
980
981     return $max_amount;
982 }
983
984
985
986 sub run_copy_permit_scripts {
987     my $self = shift;
988     my $copy = $self->copy || return;
989     my $runner = $self->script_runner;
990
991     my @allevents;
992
993     if(!$self->legacy_script_support) {
994         my $results = $self->run_indb_circ_test;
995         unless($self->circ_test_success) {
996             push(@allevents, OpenILS::Event->new(
997                 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
998         }
999     } else {
1000     
1001        # ---------------------------------------------------------------------
1002        # Capture all of the copy permit events
1003        # ---------------------------------------------------------------------
1004        $runner->load($self->circ_permit_copy);
1005        my $result = $runner->run or 
1006             throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1007        my $copy_events = $result->{events};
1008
1009        # ---------------------------------------------------------------------
1010        # Now collect all of the events together
1011        # ---------------------------------------------------------------------
1012        push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1013     }
1014
1015     # See if this copy has an alert message
1016     my $ae = $self->check_copy_alert();
1017     push( @allevents, $ae ) if $ae;
1018
1019    # uniquify the events
1020    my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1021    @allevents = values %hash;
1022
1023    for (@allevents) {
1024       $_->{payload} = $copy if 
1025             ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1026    }
1027
1028     $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1029
1030     $self->push_events(@allevents);
1031 }
1032
1033
1034 sub check_copy_alert {
1035     my $self = shift;
1036     return undef if $self->is_renewal;
1037     return OpenILS::Event->new(
1038         'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1039         if $self->copy and $self->copy->alert_message;
1040     return undef;
1041 }
1042
1043
1044
1045 # --------------------------------------------------------------------------
1046 # If the call is overriding and has permissions to override every collected
1047 # event, the are cleared.  Any event that the caller does not have
1048 # permission to override, will be left in the event list and bail_out will
1049 # be set
1050 # XXX We need code in here to cancel any holds/transits on copies 
1051 # that are being force-checked out
1052 # --------------------------------------------------------------------------
1053 sub override_events {
1054     my $self = shift;
1055     my @events = @{$self->events};
1056     return unless @events;
1057
1058     if(!$self->override) {
1059         return $self->bail_out(1) 
1060             if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1061     }   
1062
1063     $self->events([]);
1064     
1065    for my $e (@events) {
1066       my $tc = $e->{textcode};
1067       next if $tc eq 'SUCCESS';
1068       my $ov = "$tc.override";
1069       $logger->info("circulator: attempting to override event: $ov");
1070
1071         return $self->bail_on_events($self->editor->event)
1072             unless( $self->editor->allowed($ov) );
1073    }
1074 }
1075     
1076
1077 # --------------------------------------------------------------------------
1078 # If there is an open claimsreturn circ on the requested copy, close the 
1079 # circ if overriding, otherwise bail out
1080 # --------------------------------------------------------------------------
1081 sub handle_claims_returned {
1082     my $self = shift;
1083     my $copy = $self->copy;
1084
1085     my $CR = $self->editor->search_action_circulation(
1086         {   
1087             target_copy     => $copy->id,
1088             stop_fines      => OILS_STOP_FINES_CLAIMSRETURNED,
1089             checkin_time    => undef,
1090         }
1091     );
1092
1093     return unless ($CR = $CR->[0]); 
1094
1095     my $evt;
1096
1097     # - If the caller has set the override flag, we will check the item in
1098     if($self->override) {
1099
1100         $CR->checkin_time('now');   
1101         $CR->checkin_lib($self->editor->requestor->ws_ou);
1102         $CR->checkin_staff($self->editor->requestor->id);
1103
1104         $evt = $self->editor->event 
1105             unless $self->editor->update_action_circulation($CR);
1106
1107     } else {
1108         $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1109     }
1110
1111     $self->bail_on_events($evt) if $evt;
1112     return;
1113 }
1114
1115
1116 # --------------------------------------------------------------------------
1117 # This performs the checkout
1118 # --------------------------------------------------------------------------
1119 sub do_checkout {
1120     my $self = shift;
1121
1122     $self->log_me("do_checkout()");
1123
1124     # make sure perms are good if this isn't a renewal
1125     unless( $self->is_renewal ) {
1126         return $self->bail_on_events($self->editor->event)
1127             unless( $self->editor->allowed('COPY_CHECKOUT') );
1128     }
1129
1130     # verify the permit key
1131     unless( $self->check_permit_key ) {
1132         if( $self->permit_override ) {
1133             return $self->bail_on_events($self->editor->event)
1134                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1135         } else {
1136             return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1137         }   
1138     }
1139
1140     # if this is a non-cataloged circ, build the circ and finish
1141     if( $self->is_noncat ) {
1142         $self->checkout_noncat;
1143         $self->push_events(
1144             OpenILS::Event->new('SUCCESS', 
1145             payload => { noncat_circ => $self->circ }));
1146         return;
1147     }
1148
1149     if( $self->is_precat ) {
1150         #$self->script_runner->insert("environment.isPrecat", 1, 1)
1151         $self->make_precat_copy;
1152         return if $self->bail_out;
1153
1154     } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1155         return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1156     }
1157
1158     $self->do_copy_checks;
1159     return if $self->bail_out;
1160
1161     $self->run_checkout_scripts();
1162     return if $self->bail_out;
1163
1164     $self->build_checkout_circ_object();
1165     return if $self->bail_out;
1166
1167     $self->apply_modified_due_date();
1168     return if $self->bail_out;
1169
1170     return $self->bail_on_events($self->editor->event)
1171         unless $self->editor->create_action_circulation($self->circ);
1172
1173     # refresh the circ to force local time zone for now
1174     $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1175
1176     $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1177     $self->update_copy;
1178     return if $self->bail_out;
1179
1180     $self->apply_deposit_fee();
1181     return if $self->bail_out;
1182
1183     $self->handle_checkout_holds();
1184     return if $self->bail_out;
1185
1186    # ------------------------------------------------------------------------------
1187    # Update the patron penalty info in the DB.  Run it for permit-overrides or
1188     # renewals since both of those cases do not require the penalty server to
1189     # run during the permit phase of the checkout
1190    # ------------------------------------------------------------------------------
1191     if( $self->permit_override or $self->is_renewal ) {
1192         $U->update_patron_penalties(
1193             authtoken => $self->editor->authtoken,
1194             patron    => $self->patron,
1195             background  => 1,
1196         );
1197     }
1198
1199     my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1200     $self->push_events(
1201         OpenILS::Event->new('SUCCESS',
1202             payload  => {
1203                 copy              => $U->unflesh_copy($self->copy),
1204                 circ              => $self->circ,
1205                 record            => $record,
1206                 holds_fulfilled   => $self->fulfilled_holds,
1207                 deposit_billing      => $self->deposit_billing,
1208                 rental_billing       => $self->rental_billing
1209             }
1210         )
1211     );
1212 }
1213
1214 sub apply_deposit_fee {
1215     my $self = shift;
1216     my $copy = $self->copy;
1217     return unless 
1218         ($self->is_deposit and not $self->is_deposit_exempt) or 
1219         ($self->is_rental and not $self->is_rental_exempt);
1220
1221         my $bill = Fieldmapper::money::billing->new;
1222     my $amount = $copy->deposit_amount;
1223     my $billing_type;
1224
1225     if($self->is_deposit) {
1226         $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1227         $self->deposit_billing($bill);
1228     } else {
1229         $billing_type = OILS_BILLING_TYPE_RENTAL;
1230         $self->rental_billing($bill);
1231     }
1232
1233         $bill->xact($self->circ->id);
1234         $bill->amount($amount);
1235         $bill->note(OILS_BILLING_NOTE_SYSTEM);
1236         $bill->billing_type($billing_type);
1237     $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1238
1239         $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1240 }
1241
1242 sub update_copy {
1243     my $self = shift;
1244     my $copy = $self->copy;
1245
1246     my $stat = $copy->status if ref $copy->status;
1247     my $loc = $copy->location if ref $copy->location;
1248     my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1249
1250     $copy->status($stat->id) if $stat;
1251     $copy->location($loc->id) if $loc;
1252     $copy->circ_lib($circ_lib->id) if $circ_lib;
1253     $copy->editor($self->editor->requestor->id);
1254     $copy->edit_date('now');
1255     $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1256
1257     return $self->bail_on_events($self->editor->event)
1258         unless $self->editor->update_asset_copy($self->copy);
1259
1260     $copy->status($U->copy_status($copy->status));
1261     $copy->location($loc) if $loc;
1262     $copy->circ_lib($circ_lib) if $circ_lib;
1263 }
1264
1265
1266 sub bail_on_events {
1267     my( $self, @evts ) = @_;
1268     $self->push_events(@evts);
1269     $self->bail_out(1);
1270 }
1271
1272 sub handle_checkout_holds {
1273    my $self    = shift;
1274
1275    my $copy    = $self->copy;
1276    my $patron  = $self->patron;
1277
1278     my $holds   = $self->editor->search_action_hold_request(
1279         { 
1280             current_copy        => $copy->id , 
1281             cancel_time         => undef, 
1282             fulfillment_time    => undef 
1283         }
1284     );
1285
1286    my @fulfilled;
1287
1288    # XXX We should only fulfill one hold here...
1289    # XXX If a hold was transited to the user who is checking out
1290    # the item, we need to make sure that hold is what's grabbed
1291    if(@$holds) {
1292
1293       # for now, just sort by id to get what should be the oldest hold
1294       $holds = [ sort { $a->id <=> $b->id } @$holds ];
1295       my @myholds = grep { $_->usr eq $patron->id } @$holds;
1296       my @altholds   = grep { $_->usr ne $patron->id } @$holds;
1297
1298       if(@myholds) {
1299          my $hold = $myholds[0];
1300
1301          $logger->debug("circulator: related hold found in checkout: " . $hold->id );
1302
1303          # if the hold was never officially captured, capture it.
1304          $hold->capture_time('now') unless $hold->capture_time;
1305
1306             # just make sure it's set correctly
1307          $hold->current_copy($copy->id); 
1308
1309          $hold->fulfillment_time('now');
1310             $hold->fulfillment_staff($self->editor->requestor->id);
1311             $hold->fulfillment_lib($self->editor->requestor->ws_ou);
1312
1313             return $self->bail_on_events($self->editor->event)
1314                 unless $self->editor->update_action_hold_request($hold);
1315
1316             $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
1317
1318          push( @fulfilled, $hold->id );
1319       }
1320
1321       # If there are any holds placed for other users that point to this copy,
1322       # then we need to un-target those holds so the targeter can pick a new copy
1323       for(@altholds) {
1324
1325          $logger->info("circulator: un-targeting hold ".$_->id.
1326             " because copy ".$copy->id." is getting checked out");
1327
1328             # - make the targeter process this hold at next run
1329          $_->clear_prev_check_time; 
1330
1331             # - clear out the targetted copy
1332          $_->clear_current_copy;
1333          $_->clear_capture_time;
1334
1335             return $self->bail_on_event($self->editor->event)
1336                 unless $self->editor->update_action_hold_request($_);
1337       }
1338    }
1339
1340     $self->fulfilled_holds(\@fulfilled);
1341 }
1342
1343
1344
1345 sub run_checkout_scripts {
1346     my $self = shift;
1347
1348     my $evt;
1349     my $runner = $self->script_runner;
1350
1351     my $duration;
1352     my $recurring;
1353     my $max_fine;
1354     my $duration_name;
1355     my $recurring_name;
1356     my $max_fine_name;
1357
1358     if(!$self->legacy_script_support) {
1359         $self->run_indb_circ_test();
1360         $duration = $self->circ_matrix_ruleset->duration_rule;
1361         $recurring = $self->circ_matrix_ruleset->recurring_fine_rule;
1362         $max_fine = $self->circ_matrix_ruleset->max_fine_rule;
1363
1364     } else {
1365
1366        $runner->load($self->circ_duration);
1367
1368        my $result = $runner->run or 
1369             throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1370
1371        $duration_name   = $result->{durationRule};
1372        $recurring_name  = $result->{recurringFinesRule};
1373        $max_fine_name   = $result->{maxFine};
1374     }
1375
1376     $duration_name = $duration->name if $duration;
1377     if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1378
1379         unless($duration) {
1380             ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1381             return $self->bail_on_events($evt) if $evt;
1382         
1383             ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1384             return $self->bail_on_events($evt) if $evt;
1385         
1386             ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1387             return $self->bail_on_events($evt) if $evt;
1388         }
1389
1390     } else {
1391
1392         # The item circulates with an unlimited duration
1393         $duration   = undef;
1394         $recurring  = undef;
1395         $max_fine   = undef;
1396     }
1397
1398    $self->duration_rule($duration);
1399    $self->recurring_fines_rule($recurring);
1400    $self->max_fine_rule($max_fine);
1401 }
1402
1403
1404 sub build_checkout_circ_object {
1405     my $self = shift;
1406
1407    my $circ       = Fieldmapper::action::circulation->new;
1408    my $duration   = $self->duration_rule;
1409    my $max        = $self->max_fine_rule;
1410    my $recurring  = $self->recurring_fines_rule;
1411    my $copy       = $self->copy;
1412    my $patron     = $self->patron;
1413
1414     if( $duration ) {
1415
1416         my $policy = $self->get_circ_policy($duration, $recurring, $max);
1417
1418         my $dname = $duration->name;
1419         my $mname = $max->name;
1420         my $rname = $recurring->name;
1421
1422         $logger->debug("circulator: building circulation ".
1423             "with duration=$dname, maxfine=$mname, recurring=$rname");
1424     
1425         $circ->duration($policy->{duration});
1426         $circ->recuring_fine($policy->{recurring_fine});
1427         $circ->duration_rule($duration->name);
1428         $circ->recuring_fine_rule($recurring->name);
1429         $circ->max_fine_rule($max->name);
1430         $circ->max_fine($policy->{max_fine});
1431         $circ->fine_interval($recurring->recurance_interval);
1432         $circ->renewal_remaining($duration->max_renewals);
1433
1434     } else {
1435
1436         $logger->info("circulator: copy found with an unlimited circ duration");
1437         $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1438         $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1439         $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1440         $circ->renewal_remaining(0);
1441     }
1442
1443    $circ->target_copy( $copy->id );
1444    $circ->usr( $patron->id );
1445    $circ->circ_lib( $self->circ_lib );
1446
1447    if( $self->is_renewal ) {
1448       $circ->opac_renewal('t') if $self->opac_renewal;
1449       $circ->phone_renewal('t') if $self->phone_renewal;
1450       $circ->desk_renewal('t') if $self->desk_renewal;
1451       $circ->renewal_remaining($self->renewal_remaining);
1452       $circ->circ_staff($self->editor->requestor->id);
1453    }
1454
1455
1456    # if the user provided an overiding checkout time,
1457    # (e.g. the checkout really happened several hours ago), then
1458    # we apply that here.  Does this need a perm??
1459     $circ->xact_start(clense_ISO8601($self->checkout_time))
1460         if $self->checkout_time;
1461
1462    # if a patron is renewing, 'requestor' will be the patron
1463    $circ->circ_staff($self->editor->requestor->id);
1464     $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1465
1466     $self->circ($circ);
1467 }
1468
1469
1470 sub apply_modified_due_date {
1471     my $self = shift;
1472     my $circ = $self->circ;
1473     my $copy = $self->copy;
1474
1475    if( $self->due_date ) {
1476
1477         return $self->bail_on_events($self->editor->event)
1478             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1479
1480       $circ->due_date(clense_ISO8601($self->due_date));
1481
1482    } else {
1483
1484       # if the due_date lands on a day when the location is closed
1485       return unless $copy and $circ->due_date;
1486
1487         #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1488
1489         # due-date overlap should be determined by the location the item
1490         # is checked out from, not the owning or circ lib of the item
1491         my $org = $self->editor->requestor->ws_ou;
1492
1493       $logger->info("circulator: circ searching for closed date overlap on lib $org".
1494             " with an item due date of ".$circ->due_date );
1495
1496       my $dateinfo = $U->storagereq(
1497          'open-ils.storage.actor.org_unit.closed_date.overlap', 
1498             $org, $circ->due_date );
1499
1500       if($dateinfo) {
1501          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1502             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1503
1504             # XXX make the behavior more dynamic
1505             # for now, we just push the due date to after the close date
1506             $circ->due_date($dateinfo->{end});
1507       }
1508    }
1509 }
1510
1511
1512
1513 sub create_due_date {
1514     my( $self, $duration ) = @_;
1515     # if there is a raw time component (e.g. from postgres), 
1516     # turn it into an interval that interval_to_seconds can parse
1517     $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1518    my ($sec,$min,$hour,$mday,$mon,$year) =
1519       gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1520    $year += 1900; $mon += 1;
1521    my $due_date = sprintf(
1522       '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1523       $year, $mon, $mday, $hour, $min, $sec);
1524    return $due_date;
1525 }
1526
1527
1528
1529 sub make_precat_copy {
1530     my $self = shift;
1531     my $copy = $self->copy;
1532
1533    if($copy) {
1534       $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1535
1536       $copy->editor($self->editor->requestor->id);
1537       $copy->edit_date('now');
1538       $copy->dummy_title($self->dummy_title);
1539       $copy->dummy_author($self->dummy_author);
1540
1541         $self->update_copy();
1542         return;
1543    }
1544
1545    $logger->info("circulator: Creating a new precataloged ".
1546         "copy in checkout with barcode " . $self->copy_barcode);
1547
1548    $copy = Fieldmapper::asset::copy->new;
1549    $copy->circ_lib($self->circ_lib);
1550    $copy->creator($self->editor->requestor->id);
1551    $copy->editor($self->editor->requestor->id);
1552    $copy->barcode($self->copy_barcode);
1553    $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
1554    $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1555    $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1556
1557    $copy->dummy_title($self->dummy_title || "");
1558    $copy->dummy_author($self->dummy_author || "");
1559
1560     unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1561         $self->bail_out(1);
1562         $self->push_events($self->editor->event);
1563         return;
1564     }   
1565
1566     # this is a little bit of a hack, but we need to 
1567     # get the copy into the script runner
1568     $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1569 }
1570
1571
1572 sub checkout_noncat {
1573     my $self = shift;
1574
1575     my $circ;
1576     my $evt;
1577
1578    my $lib      = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1579    my $count    = $self->noncat_count || 1;
1580    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1581
1582    $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1583
1584    for(1..$count) {
1585
1586       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1587          $self->editor->requestor->id, 
1588             $self->patron->id, 
1589             $lib, 
1590             $self->noncat_type, 
1591             $cotime,
1592             $self->editor );
1593
1594         if( $evt ) {
1595             $self->push_events($evt);
1596             $self->bail_out(1);
1597             return; 
1598         }
1599         $self->circ($circ);
1600    }
1601 }
1602
1603
1604 sub do_checkin {
1605     my $self = shift;
1606     $self->log_me("do_checkin()");
1607
1608
1609     return $self->bail_on_events(
1610         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
1611         unless $self->copy;
1612
1613     if( $self->checkin_check_holds_shelf() ) {
1614         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1615         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1616         $self->checkin_flesh_events;
1617         return;
1618     }
1619
1620     unless( $self->is_renewal ) {
1621         return $self->bail_on_events($self->editor->event)
1622             unless $self->editor->allowed('COPY_CHECKIN');
1623     }
1624
1625     $self->push_events($self->check_copy_alert());
1626     $self->push_events($self->check_checkin_copy_status());
1627
1628     # the renew code will have already found our circulation object
1629     unless( $self->is_renewal and $self->circ ) {
1630         my $circs = $self->editor->search_action_circulation(
1631             { target_copy => $self->copy->id, checkin_time => undef });
1632         $self->circ($$circs[0]);
1633
1634         # for now, just warn if there are multiple open circs on a copy
1635         $logger->warn("circulator: we have ".scalar(@$circs).
1636             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1637     }
1638
1639     # if the circ is marked as 'claims returned', add the event to the list
1640     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1641         if ($self->circ and $self->circ->stop_fines 
1642                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1643
1644     $self->check_circ_deposit();
1645
1646     # handle the overridable events 
1647     $self->override_events unless $self->is_renewal;
1648     return if $self->bail_out;
1649     
1650     if( $self->copy ) {
1651         $self->transit(
1652             $self->editor->search_action_transit_copy(
1653             { target_copy => $self->copy->id, dest_recv_time => undef })->[0]); 
1654     }
1655
1656     if( $self->circ ) {
1657         $self->checkin_handle_circ;
1658         return if $self->bail_out;
1659         $self->checkin_changed(1);
1660
1661     } elsif( $self->transit ) {
1662         my $hold_transit = $self->process_received_transit;
1663         $self->checkin_changed(1);
1664
1665         if( $self->bail_out ) { 
1666             $self->checkin_flesh_events;
1667             return;
1668         }
1669         
1670         if( my $e = $self->check_checkin_copy_status() ) {
1671             # If the original copy status is special, alert the caller
1672             my $ev = $self->events;
1673             $self->events([$e]);
1674             $self->override_events;
1675             return if $self->bail_out;
1676             $self->events($ev);
1677         }
1678
1679         if( $hold_transit or 
1680                 $U->copy_status($self->copy->status)->id 
1681                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1682
1683          my $hold;
1684          if( $hold_transit ) {
1685             $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1686          } else {
1687                 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1688          }
1689
1690             $self->hold($hold);
1691
1692             if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1693
1694                 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1695                 $self->reshelve_copy(1);
1696                 $self->cancelled_hold_transit(1);
1697                 $self->notify_hold(0); # don't notify for cancelled holds
1698                 return if $self->bail_out;
1699
1700             } else {
1701
1702                 # hold transited to correct location
1703                 $self->checkin_flesh_events;
1704                 return;
1705             }
1706         } 
1707
1708     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1709
1710         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1711             " that is in-transit, but there is no transit.. repairing");
1712         $self->reshelve_copy(1);
1713         return if $self->bail_out;
1714     }
1715
1716     if( $self->is_renewal ) {
1717         $self->push_events(OpenILS::Event->new('SUCCESS'));
1718         return;
1719     }
1720
1721    # ------------------------------------------------------------------------------
1722    # Circulations and transits are now closed where necessary.  Now go on to see if
1723    # this copy can fulfill a hold or needs to be routed to a different location
1724    # ------------------------------------------------------------------------------
1725
1726     unless($self->noop) { # no-op checkins to not capture holds or put items into transit
1727
1728         my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
1729         return if $self->bail_out;
1730     
1731         unless($needed_for_hold) {
1732             my $circ_lib = (ref $self->copy->circ_lib) ? 
1733                     $self->copy->circ_lib->id : $self->copy->circ_lib;
1734     
1735             if( $self->remote_hold ) {
1736                 $circ_lib = $self->remote_hold->pickup_lib;
1737                 $logger->warn("circulator: Copy ".$self->copy->barcode.
1738                     " is on a remote hold's shelf, sending to $circ_lib");
1739             }
1740     
1741             $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1742     
1743             if( $circ_lib == $self->editor->requestor->ws_ou ) {
1744     
1745                 $self->checkin_handle_precat();
1746                 return if $self->bail_out;
1747     
1748             } else {
1749     
1750                 my $bc = $self->copy->barcode;
1751                 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1752                 $self->checkin_build_copy_transit($circ_lib);
1753                 return if $self->bail_out;
1754                 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1755             }
1756         }
1757     }
1758
1759     $self->reshelve_copy;
1760     return if $self->bail_out;
1761
1762     unless($self->checkin_changed) {
1763
1764         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1765         my $stat = $U->copy_status($self->copy->status)->id;
1766
1767         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1768          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1769         $self->bail_out(1); # no need to commit anything
1770
1771     } else {
1772
1773         $self->push_events(OpenILS::Event->new('SUCCESS')) 
1774             unless @{$self->events};
1775     }
1776
1777
1778    # ------------------------------------------------------------------------------
1779    # Update the patron penalty info in the DB
1780    # ------------------------------------------------------------------------------
1781    $U->update_patron_penalties(
1782       authtoken => $self->editor->authtoken,
1783       patron    => $self->patron,
1784       background  => 1 ) if $self->is_checkin;
1785
1786     $self->checkin_flesh_events;
1787     return;
1788 }
1789
1790 # if a deposit was payed for this item, push the event
1791 sub check_circ_deposit {
1792     my $self = shift;
1793     return unless $self->circ;
1794     my $deposit = $self->editor->search_money_billing(
1795         {   billing_type => OILS_BILLING_TYPE_DEPOSIT, 
1796             xact => $self->circ->id, 
1797             voided => 'f'
1798         }, {idlist => 1})->[0];
1799
1800     $self->push_events(OpenILS::Event->new(
1801         'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1802 }
1803
1804 sub reshelve_copy {
1805    my $self    = shift;
1806    my $force   = $self->force || shift;
1807    my $copy    = $self->copy;
1808
1809    my $stat = $U->copy_status($copy->status)->id;
1810
1811    if($force || (
1812       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1813       $stat != OILS_COPY_STATUS_CATALOGING and
1814       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1815       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1816
1817         $copy->status( OILS_COPY_STATUS_RESHELVING );
1818             $self->update_copy;
1819             $self->checkin_changed(1);
1820     }
1821 }
1822
1823
1824 # Returns true if the item is at the current location
1825 # because it was transited there for a hold and the 
1826 # hold has not been fulfilled
1827 sub checkin_check_holds_shelf {
1828     my $self = shift;
1829     return 0 unless $self->copy;
1830
1831     return 0 unless 
1832         $U->copy_status($self->copy->status)->id ==
1833             OILS_COPY_STATUS_ON_HOLDS_SHELF;
1834
1835     # find the hold that put us on the holds shelf
1836     my $holds = $self->editor->search_action_hold_request(
1837         { 
1838             current_copy => $self->copy->id,
1839             capture_time => { '!=' => undef },
1840             fulfillment_time => undef,
1841             cancel_time => undef,
1842         }
1843     );
1844
1845     unless(@$holds) {
1846         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1847         $self->reshelve_copy(1);
1848         return 0;
1849     }
1850
1851     my $hold = $$holds[0];
1852
1853     $logger->info("circulator: we found a captured, un-fulfilled hold [".
1854         $hold->id. "] for copy ".$self->copy->barcode);
1855
1856     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1857         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1858         return 1;
1859     }
1860
1861     $logger->info("circulator: hold is not for here..");
1862     $self->remote_hold($hold);
1863     return 0;
1864 }
1865
1866
1867 sub checkin_handle_precat {
1868     my $self    = shift;
1869    my $copy    = $self->copy;
1870
1871    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1872       $copy->status(OILS_COPY_STATUS_CATALOGING);
1873         $self->update_copy();
1874         $self->checkin_changed(1);
1875         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1876    }
1877 }
1878
1879
1880 sub checkin_build_copy_transit {
1881     my $self            = shift;
1882     my $dest            = shift;
1883     my $copy       = $self->copy;
1884    my $transit    = Fieldmapper::action::transit_copy->new;
1885
1886     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1887     $logger->info("circulator: transiting copy to $dest");
1888
1889    $transit->source($self->editor->requestor->ws_ou);
1890    $transit->dest($dest);
1891    $transit->target_copy($copy->id);
1892    $transit->source_send_time('now');
1893    $transit->copy_status( $U->copy_status($copy->status)->id );
1894
1895     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1896
1897     return $self->bail_on_events($self->editor->event)
1898         unless $self->editor->create_action_transit_copy($transit);
1899
1900    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1901     $self->update_copy;
1902     $self->checkin_changed(1);
1903 }
1904
1905
1906 # returns true if the item was used (or may potentially be used 
1907 # in subsequent calls) to capture a hold.
1908 sub attempt_checkin_hold_capture {
1909     my $self = shift;
1910     my $copy = $self->copy;
1911
1912     # we've been explicitly told not to capture any holds
1913     return 0 if $self->capture eq 'nocapture';
1914
1915     # See if this copy can fulfill any holds
1916     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
1917         $self->editor, $copy, $self->editor->requestor );
1918
1919     if(!$hold) {
1920         $logger->debug("circulator: no potential permitted".
1921             "holds found for copy ".$copy->barcode);
1922         return 0;
1923     }
1924
1925     if($self->capture ne 'capture') {
1926         # see if this item is in a hold-capture-delay location
1927         my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
1928         if($U->is_true($location->hold_verify)) {
1929             $self->bail_on_events(
1930                 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
1931             return 1;
1932         }
1933     }
1934
1935     $self->retarget($retarget);
1936
1937     $logger->info("circulator: found permitted hold ".
1938         $hold->id . " for copy, capturing...");
1939
1940     $hold->current_copy($copy->id);
1941     $hold->capture_time('now');
1942
1943     # prevent DB errors caused by fetching 
1944     # holds from storage, and updating through cstore
1945     $hold->clear_fulfillment_time;
1946     $hold->clear_fulfillment_staff;
1947     $hold->clear_fulfillment_lib;
1948     $hold->clear_expire_time; 
1949     $hold->clear_cancel_time;
1950     $hold->clear_prev_check_time unless $hold->prev_check_time;
1951
1952     $self->bail_on_events($self->editor->event)
1953         unless $self->editor->update_action_hold_request($hold);
1954     $self->hold($hold);
1955     $self->checkin_changed(1);
1956
1957     return 0 if $self->bail_out;
1958
1959     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1960
1961         # This hold was captured in the correct location
1962         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1963         $self->push_events(OpenILS::Event->new('SUCCESS'));
1964
1965         #$self->do_hold_notify($hold->id);
1966         $self->notify_hold($hold->id);
1967
1968     } else {
1969     
1970         # Hold needs to be picked up elsewhere.  Build a hold
1971         # transit and route the item.
1972         $self->checkin_build_hold_transit();
1973         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1974         return 0 if $self->bail_out;
1975         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1976     }
1977
1978     # make sure we save the copy status
1979     $self->update_copy;
1980     return 1;
1981 }
1982
1983 sub do_hold_notify {
1984     my( $self, $holdid ) = @_;
1985
1986     $logger->info("circulator: running delayed hold notify process");
1987
1988 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1989 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1990
1991     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1992         hold_id => $holdid, requestor => $self->editor->requestor);
1993
1994     $logger->debug("circulator: built hold notifier");
1995
1996     if(!$notifier->event) {
1997
1998         $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1999
2000         my $stat = $notifier->send_email_notify;
2001         if( $stat == '1' ) {
2002             $logger->info("ciculator: hold notify succeeded for hold $holdid");
2003             return;
2004         } 
2005
2006         $logger->warn("ciculator:  * hold notify failed for hold $holdid");
2007
2008     } else {
2009         $logger->info("ciculator: Not sending hold notification since the patron has no email address");
2010     }
2011 }
2012
2013 sub retarget_holds {
2014     $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
2015     my $ses = OpenSRF::AppSession->create('open-ils.storage');
2016     $ses->request('open-ils.storage.action.hold_request.copy_targeter');
2017     # no reason to wait for the return value
2018     return;
2019 }
2020
2021 sub checkin_build_hold_transit {
2022     my $self = shift;
2023
2024    my $copy = $self->copy;
2025    my $hold = $self->hold;
2026    my $trans = Fieldmapper::action::hold_transit_copy->new;
2027
2028     $logger->debug("circulator: building hold transit for ".$copy->barcode);
2029
2030    $trans->hold($hold->id);
2031    $trans->source($self->editor->requestor->ws_ou);
2032    $trans->dest($hold->pickup_lib);
2033    $trans->source_send_time("now");
2034    $trans->target_copy($copy->id);
2035
2036     # when the copy gets to its destination, it will recover
2037     # this status - put it onto the holds shelf
2038    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2039
2040     return $self->bail_on_events($self->editor->event)
2041         unless $self->editor->create_action_hold_transit_copy($trans);
2042 }
2043
2044
2045
2046 sub process_received_transit {
2047     my $self = shift;
2048     my $copy = $self->copy;
2049     my $copyid = $self->copy->id;
2050
2051     my $status_name = $U->copy_status($copy->status)->name;
2052     $logger->debug("circulator: attempting transit receive on ".
2053         "copy $copyid. Copy status is $status_name");
2054
2055     my $transit = $self->transit;
2056
2057     if( $transit->dest != $self->editor->requestor->ws_ou ) {
2058         # - this item is in-transit to a different location
2059
2060         my $tid = $transit->id; 
2061         my $loc = $self->editor->requestor->ws_ou;
2062         my $dest = $transit->dest;
2063
2064         $logger->info("circulator: Fowarding transit on copy which is destined ".
2065             "for a different location. transit=$tid, copy=$copyid, current ".
2066             "location=$loc, destination location=$dest");
2067
2068         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2069
2070         # grab the associated hold object if available
2071         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2072         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2073
2074         return $self->bail_on_events($evt);
2075     }
2076
2077    # The transit is received, set the receive time
2078    $transit->dest_recv_time('now');
2079     $self->bail_on_events($self->editor->event)
2080         unless $self->editor->update_action_transit_copy($transit);
2081
2082     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2083
2084    $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2085    $copy->status( $transit->copy_status );
2086     $self->update_copy();
2087     return if $self->bail_out;
2088
2089     my $ishold = 0;
2090     if($hold_transit) { 
2091         #$self->do_hold_notify($hold_transit->hold);
2092         $self->notify_hold($hold_transit->hold);
2093         $ishold = 1;
2094     }
2095
2096     $self->push_events( 
2097         OpenILS::Event->new(
2098         'SUCCESS', 
2099         ishold => $ishold,
2100       payload => { transit => $transit, holdtransit => $hold_transit } ));
2101
2102     return $hold_transit;
2103 }
2104
2105
2106 sub checkin_handle_circ {
2107    my $self = shift;
2108     $U->logmark;
2109
2110    my $circ = $self->circ;
2111    my $copy = $self->copy;
2112    my $evt;
2113    my $obt;
2114
2115    # backdate the circ if necessary
2116    if($self->backdate) {
2117         $self->checkin_handle_backdate;
2118         return if $self->bail_out;
2119    }
2120
2121    if(!$circ->stop_fines) {
2122       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2123       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2124       $circ->stop_fines_time('now') unless $self->backdate;
2125       $circ->stop_fines_time($self->backdate) if $self->backdate;
2126    }
2127
2128    # see if there are any fines owed on this circ.  if not, close it
2129     ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2130    $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2131
2132     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2133
2134    # Set the checkin vars since we have the item
2135     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2136
2137    $circ->checkin_staff($self->editor->requestor->id);
2138    $circ->checkin_lib($self->editor->requestor->ws_ou);
2139
2140     my $circ_lib = (ref $self->copy->circ_lib) ?  
2141         $self->copy->circ_lib->id : $self->copy->circ_lib;
2142     my $stat = $U->copy_status($self->copy->status)->id;
2143
2144     # If the item is lost/missing and it needs to be sent home, don't 
2145     # reshelve the copy, leave it lost/missing so the recipient will know
2146     if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
2147         and ($circ_lib != $self->editor->requestor->ws_ou) ) {
2148         $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2149
2150     } else {
2151         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2152         $self->update_copy;
2153     }
2154
2155     return $self->bail_on_events($self->editor->event)
2156         unless $self->editor->update_action_circulation($circ);
2157 }
2158
2159
2160 sub checkin_handle_backdate {
2161     my $self = shift;
2162
2163     my $bd = $self->backdate;
2164
2165     # ------------------------------------------------------------------
2166     # clean up the backdate for date comparison
2167     # we want any bills created on or after the backdate
2168     # ------------------------------------------------------------------
2169     $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2170     #$bd = "${bd}T23:59:59";
2171
2172     my $bills = $self->editor->search_money_billing(
2173         { 
2174             billing_ts => { '>=' => $bd }, 
2175             xact => $self->circ->id, 
2176             billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2177         }
2178     );
2179
2180     $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2181
2182     for my $bill (@$bills) {    
2183         unless( $U->is_true($bill->voided) ) {
2184             $logger->info("backdate voiding bill ".$bill->id);
2185             $bill->voided('t');
2186             $bill->void_time('now');
2187             $bill->voider($self->editor->requestor->id);
2188             my $n = $bill->note || "";
2189             $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2190
2191             $self->bail_on_events($self->editor->event)
2192                 unless $self->editor->update_money_billing($bill);
2193         }
2194     }
2195 }
2196
2197
2198
2199
2200 sub find_patron_from_copy {
2201     my $self = shift;
2202     my $circs = $self->editor->search_action_circulation(
2203         { target_copy => $self->copy->id, checkin_time => undef });
2204     my $circ = $circs->[0];
2205     return unless $circ;
2206     my $u = $self->editor->retrieve_actor_user($circ->usr)
2207         or return $self->bail_on_events($self->editor->event);
2208     $self->patron($u);
2209 }
2210
2211 sub check_checkin_copy_status {
2212     my $self = shift;
2213    my $copy = $self->copy;
2214
2215    my $islost     = 0;
2216    my $ismissing  = 0;
2217    my $evt        = undef;
2218
2219    my $status = $U->copy_status($copy->status)->id;
2220
2221    return undef
2222       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
2223             $status == OILS_COPY_STATUS_CHECKED_OUT ||
2224             $status == OILS_COPY_STATUS_IN_PROCESS  ||
2225             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
2226             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
2227             $status == OILS_COPY_STATUS_CATALOGING  ||
2228             $status == OILS_COPY_STATUS_RESHELVING );
2229
2230    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2231       if( $status == OILS_COPY_STATUS_LOST );
2232
2233    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2234       if( $status == OILS_COPY_STATUS_MISSING );
2235
2236    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2237 }
2238
2239
2240
2241 # --------------------------------------------------------------------------
2242 # On checkin, we need to return as many relevant objects as we can
2243 # --------------------------------------------------------------------------
2244 sub checkin_flesh_events {
2245     my $self = shift;
2246
2247     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
2248         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2249             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2250     }
2251
2252
2253     for my $evt (@{$self->events}) {
2254
2255         my $payload          = {};
2256         $payload->{copy}     = $U->unflesh_copy($self->copy);
2257         $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2258         $payload->{circ}     = $self->circ;
2259         $payload->{transit}  = $self->transit;
2260         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2261
2262         # $self->hold may or may not have been replaced with a 
2263         # valid hold after processing a cancelled hold
2264         $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
2265
2266         $evt->{payload} = $payload;
2267     }
2268 }
2269
2270 sub log_me {
2271     my( $self, $msg ) = @_;
2272     my $bc = ($self->copy) ? $self->copy->barcode :
2273         $self->barcode;
2274     $bc ||= "";
2275     my $usr = ($self->patron) ? $self->patron->id : "";
2276     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2277         ", recipient=$usr, copy=$bc");
2278 }
2279
2280
2281 sub do_renew {
2282     my $self = shift;
2283     $self->log_me("do_renew()");
2284     $self->is_renewal(1);
2285
2286     # Make sure there is an open circ to renew that is not
2287     # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2288     my $circ = $self->editor->search_action_circulation(
2289             { target_copy => $self->copy->id, stop_fines => undef } )->[0];
2290
2291     if(!$circ) {
2292         $circ = $self->editor->search_action_circulation(
2293             { 
2294                 target_copy => $self->copy->id, 
2295                 stop_fines => OILS_STOP_FINES_MAX_FINES,
2296                 checkin_time => undef
2297             } 
2298         )->[0];
2299     }
2300
2301     return $self->bail_on_events($self->editor->event) unless $circ;
2302
2303     # A user is not allowed to renew another user's items without permission
2304     unless( $circ->usr eq $self->editor->requestor->id ) {
2305         return $self->bail_on_events($self->editor->events)
2306             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2307     }   
2308
2309     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2310         if $circ->renewal_remaining < 1;
2311
2312     # -----------------------------------------------------------------
2313
2314     $self->renewal_remaining( $circ->renewal_remaining - 1 );
2315     $self->circ($circ);
2316
2317     $self->run_renew_permit;
2318
2319     # Check the item in
2320     $self->do_checkin();
2321     return if $self->bail_out;
2322
2323     unless( $self->permit_override ) {
2324         $self->do_permit();
2325         return if $self->bail_out;
2326         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2327         $self->remove_event('ITEM_NOT_CATALOGED');
2328     }   
2329
2330     $self->override_events;
2331     return if $self->bail_out;
2332
2333     $self->events([]);
2334     $self->do_checkout();
2335 }
2336
2337
2338 sub remove_event {
2339     my( $self, $evt ) = @_;
2340     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2341     $logger->debug("circulator: removing event from list: $evt");
2342     my @events = @{$self->events};
2343     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2344 }
2345
2346
2347 sub have_event {
2348     my( $self, $evt ) = @_;
2349     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2350     return grep { $_->{textcode} eq $evt } @{$self->events};
2351 }
2352
2353
2354
2355 sub run_renew_permit {
2356     my $self = shift;
2357
2358     my $events = [];
2359
2360     if(!$self->legacy_script_support) {
2361         my $results = $self->run_indb_circ_test;
2362         unless($self->circ_test_success) {
2363             push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) for @$results;
2364         }
2365
2366     } else {
2367
2368         my $runner = $self->script_runner;
2369
2370         $runner->load($self->circ_permit_renew);
2371         my $result = $runner->run or 
2372             throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2373         my $events = $result->{events};
2374     }
2375
2376     $logger->activity("ciculator: circ_permit_renew for user ".
2377       $self->patron->id." returned events: @$events") if @$events;
2378
2379     $self->push_events(OpenILS::Event->new($_)) for @$events;
2380
2381     $logger->debug("circulator: re-creating script runner to be safe");
2382     #$self->mk_script_runner;
2383 }
2384
2385
2386
2387