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