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