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