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