]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Merge branch 'master' of git+ssh://yeti.esilibrary.com/home/evergreen/evergreen-equin...
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / 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::AppSession;
6 use OpenSRF::Utils::SettingsClient;
7 use OpenSRF::Utils::Logger qw(:logger);
8 use OpenILS::Const qw/:const/;
9 use OpenILS::Application::AppUtils;
10 use DateTime;
11 my $U = "OpenILS::Application::AppUtils";
12
13 my %scripts;
14 my $script_libs;
15 my $legacy_script_support = 0;
16 my $booking_status;
17
18 sub determine_booking_status {
19     unless (defined $booking_status) {
20         my $ses = create OpenSRF::AppSession("router");
21         $booking_status = grep {$_ eq "open-ils.booking"} @{
22             $ses->request("opensrf.router.info.class.list")->gather(1)
23         };
24         $ses->disconnect;
25         $logger->info("booking status: " . ($booking_status ? "on" : "off"));
26     }
27
28     return $booking_status;
29 }
30
31
32 my $MK_ENV_FLESH = { 
33     flesh => 2, 
34     flesh_fields => {acp => ['call_number'], acn => ['record']} 
35 };
36
37 sub initialize {
38
39     my $self = shift;
40     my $conf = OpenSRF::Utils::SettingsClient->new;
41     my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
42
43     $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
44     $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
45
46     my $lb  = $conf->config_value(  @pfx2, 'script_path' );
47     $lb = [ $lb ] unless ref($lb);
48     $script_libs = $lb;
49
50     return unless $legacy_script_support;
51
52     my @pfx = ( @pfx2, "scripts" );
53     my $p   = $conf->config_value(  @pfx, 'circ_permit_patron' );
54     my $c   = $conf->config_value(  @pfx, 'circ_permit_copy' );
55     my $d   = $conf->config_value(  @pfx, 'circ_duration' );
56     my $f   = $conf->config_value(  @pfx, 'circ_recurring_fines' );
57     my $m   = $conf->config_value(  @pfx, 'circ_max_fines' );
58     my $pr  = $conf->config_value(  @pfx, 'circ_permit_renew' );
59
60     $logger->error( "Missing circ script(s)" ) 
61         unless( $p and $c and $d and $f and $m and $pr );
62
63     $scripts{circ_permit_patron}   = $p;
64     $scripts{circ_permit_copy}     = $c;
65     $scripts{circ_duration}        = $d;
66     $scripts{circ_recurring_fines} = $f;
67     $scripts{circ_max_fines}       = $m;
68     $scripts{circ_permit_renew}    = $pr;
69
70     $logger->debug(
71         "circulator: Loaded rules scripts for circ: " .
72         "circ permit patron = $p, ".
73         "circ permit copy = $c, ".
74         "circ duration = $d, ".
75         "circ recurring fines = $f, " .
76         "circ max fines = $m, ".
77         "circ renew permit = $pr.  ".
78         "lib paths = @$lb. ".
79         "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
80         );
81 }
82
83 __PACKAGE__->register_method(
84     method  => "run_method",
85     api_name    => "open-ils.circ.checkout.permit",
86     notes       => q/
87         Determines if the given checkout can occur
88         @param authtoken The login session key
89         @param params A trailing hash of named params including 
90             barcode : The copy barcode, 
91             patron : The patron the checkout is occurring for, 
92             renew : true or false - whether or not this is a renewal
93         @return The event that occurred during the permit check.  
94     /);
95
96
97 __PACKAGE__->register_method (
98     method      => 'run_method',
99     api_name        => 'open-ils.circ.checkout.permit.override',
100     signature   => q/@see open-ils.circ.checkout.permit/,
101 );
102
103
104 __PACKAGE__->register_method(
105     method  => "run_method",
106     api_name    => "open-ils.circ.checkout",
107     notes => q/
108         Checks out an item
109         @param authtoken The login session key
110         @param params A named hash of params including:
111             copy            The copy object
112             barcode     If no copy is provided, the copy is retrieved via barcode
113             copyid      If no copy or barcode is provide, the copy id will be use
114             patron      The patron's id
115             noncat      True if this is a circulation for a non-cataloted item
116             noncat_type The non-cataloged type id
117             noncat_circ_lib The location for the noncat circ.  
118             precat      The item has yet to be cataloged
119             dummy_title The temporary title of the pre-cataloded item
120             dummy_author The temporary authr of the pre-cataloded item
121                 Default is the home org of the staff member
122         @return The SUCCESS event on success, any other event depending on the error
123     /);
124
125 __PACKAGE__->register_method(
126     method  => "run_method",
127     api_name    => "open-ils.circ.checkin",
128     argc        => 2,
129     signature   => q/
130         Generic super-method for handling all copies
131         @param authtoken The login session key
132         @param params Hash of named parameters including:
133             barcode - The copy barcode
134             force   - If true, copies in bad statuses will be checked in and give good statuses
135             noop    - don't capture holds or put items into transit
136             void_overdues - void all overdues for the circulation (aka amnesty)
137             ...
138     /
139 );
140
141 __PACKAGE__->register_method(
142     method    => "run_method",
143     api_name  => "open-ils.circ.checkin.override",
144     signature => q/@see open-ils.circ.checkin/
145 );
146
147 __PACKAGE__->register_method(
148     method    => "run_method",
149     api_name  => "open-ils.circ.renew.override",
150     signature => q/@see open-ils.circ.renew/,
151 );
152
153
154 __PACKAGE__->register_method(
155     method  => "run_method",
156     api_name    => "open-ils.circ.renew",
157     notes       => <<"    NOTES");
158     PARAMS( authtoken, circ => circ_id );
159     open-ils.circ.renew(login_session, circ_object);
160     Renews the provided circulation.  login_session is the requestor of the
161     renewal and if the logged in user is not the same as circ->usr, then
162     the logged in user must have RENEW_CIRC permissions.
163     NOTES
164
165 __PACKAGE__->register_method(
166     method   => "run_method",
167     api_name => "open-ils.circ.checkout.full"
168 );
169 __PACKAGE__->register_method(
170     method   => "run_method",
171     api_name => "open-ils.circ.checkout.full.override"
172 );
173 __PACKAGE__->register_method(
174     method   => "run_method",
175     api_name => "open-ils.circ.reservation.pickup"
176 );
177 __PACKAGE__->register_method(
178     method   => "run_method",
179     api_name => "open-ils.circ.reservation.return"
180 );
181 __PACKAGE__->register_method(
182     method   => "run_method",
183     api_name => "open-ils.circ.checkout.inspect",
184     desc     => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
185 );
186
187
188 sub run_method {
189     my( $self, $conn, $auth, $args ) = @_;
190     translate_legacy_args($args);
191     my $api = $self->api_name;
192
193     my $circulator = 
194         OpenILS::Application::Circ::Circulator->new($auth, %$args);
195
196     return circ_events($circulator) if $circulator->bail_out;
197
198     $circulator->use_booking(determine_booking_status());
199
200     # --------------------------------------------------------------------------
201     # First, check for a booking transit, as the barcode may not be a copy
202     # barcode, but a resource barcode, and nothing else in here will work
203     # --------------------------------------------------------------------------
204
205     if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
206         my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
207         if (@$resources) { # yes!
208
209             my $res_id_list = [ map { $_->id } @$resources ];
210             my $transit = $circulator->editor->search_action_reservation_transit_copy(
211                 [
212                     { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
213                     { order_by => { artc => 'source_send_time' }, limit => 1 }
214                 ]
215             )->[0]; # Any transit for this barcode?
216
217             if ($transit) { # yes! unwrap it.
218
219                 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
220                 my $res_type    = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
221
222                 my $success_event = new OpenILS::Event(
223                     "SUCCESS", "payload" => {"reservation" => $reservation}
224                 );
225                 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
226                     if (my $copy = $circulator->editor->search_asset_copy([
227                         { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
228                     ])->[0]) { # got a copy
229                         $copy->status( $transit->copy_status );
230                         $copy->editor($circulator->editor->requestor->id);
231                         $copy->edit_date('now');
232                         $circulator->editor->update_asset_copy($copy);
233                         $success_event->{"payload"}->{"record"} =
234                             $U->record_to_mvr($copy->call_number->record);
235                         $copy->call_number($copy->call_number->id);
236                         $success_event->{"payload"}->{"copy"} = $copy;
237                     }
238                 }
239
240                 $transit->dest_recv_time('now');
241                 $circulator->editor->update_action_reservation_transit_copy( $transit );
242
243                 $circulator->editor->commit;
244                 # Formerly this branch just stopped here. Argh!
245                 $conn->respond_complete($success_event);
246                 return;
247             }
248         }
249     }
250             
251     
252
253     # --------------------------------------------------------------------------
254     # Go ahead and load the script runner to make sure we have all 
255     # of the objects we need
256     # --------------------------------------------------------------------------
257
258     if ($circulator->use_booking) {
259         $circulator->is_res_checkin($circulator->is_checkin(1))
260             if $api =~ /reservation.return/ or (
261                 $api =~ /checkin/ and $circulator->seems_like_reservation()
262             );
263
264         $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
265     }
266
267     $circulator->is_renewal(1) if $api =~ /renew/;
268     $circulator->is_checkin(1) if $api =~ /checkin/;
269
270     $circulator->mk_env();
271     $circulator->noop(1) if $circulator->claims_never_checked_out;
272
273     if($legacy_script_support and not $circulator->is_checkin) {
274         $circulator->mk_script_runner();
275         $circulator->legacy_script_support(1);
276         $circulator->circ_permit_patron($scripts{circ_permit_patron});
277         $circulator->circ_permit_copy($scripts{circ_permit_copy});      
278         $circulator->circ_duration($scripts{circ_duration});             
279         $circulator->circ_permit_renew($scripts{circ_permit_renew});
280     }
281     return circ_events($circulator) if $circulator->bail_out;
282
283     
284     $circulator->override(1) if $api =~ /override/o;
285
286     if( $api =~ /checkout\.permit/ ) {
287         $circulator->do_permit();
288
289     } elsif( $api =~ /checkout.full/ ) {
290
291         # requesting a precat checkout implies that any required
292         # overrides have been performed.  Go ahead and re-override.
293         $circulator->skip_permit_key(1);
294         $circulator->override(1) if $circulator->request_precat;
295         $circulator->do_permit();
296         $circulator->is_checkout(1);
297         unless( $circulator->bail_out ) {
298             $circulator->events([]);
299             $circulator->do_checkout();
300         }
301
302     } elsif( $circulator->is_res_checkout ) {
303         $circulator->do_reservation_pickup();
304
305     } elsif( $api =~ /inspect/ ) {
306         my $data = $circulator->do_inspect();
307         $circulator->editor->rollback;
308         return $data;
309
310     } elsif( $api =~ /checkout/ ) {
311         $circulator->is_checkout(1);
312         $circulator->do_checkout();
313
314     } elsif( $circulator->is_res_checkin ) {
315         $circulator->do_reservation_return();
316         $circulator->do_checkin() if ($circulator->copy());
317     } elsif( $api =~ /checkin/ ) {
318         $circulator->do_checkin();
319
320     } elsif( $api =~ /renew/ ) {
321         $circulator->is_renewal(1);
322         $circulator->do_renew();
323     }
324
325     if( $circulator->bail_out ) {
326
327         my @ee;
328         # make sure no success event accidentally slip in
329         $circulator->events(
330             [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
331
332         # Log the events
333         my @e = @{$circulator->events};
334         push( @ee, $_->{textcode} ) for @e;
335         $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
336
337         $circulator->editor->rollback;
338
339     } else {
340         $circulator->editor->commit;
341     }
342
343     $circulator->script_runner->cleanup if $circulator->script_runner;
344     
345     $conn->respond_complete(circ_events($circulator));
346
347     unless($circulator->bail_out) {
348         $circulator->do_hold_notify($circulator->notify_hold)
349             if $circulator->notify_hold;
350         $circulator->retarget_holds if $circulator->retarget;
351         $circulator->append_reading_list;
352         $circulator->make_trigger_events;
353     }
354 }
355
356 sub circ_events {
357     my $circ = shift;
358     my @e = @{$circ->events};
359     # if we have multiple events, SUCCESS should not be one of them;
360     @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
361     return (@e == 1) ? $e[0] : \@e;
362 }
363
364
365 sub translate_legacy_args {
366     my $args = shift;
367
368     if( $$args{barcode} ) {
369         $$args{copy_barcode} = $$args{barcode};
370         delete $$args{barcode};
371     }
372
373     if( $$args{copyid} ) {
374         $$args{copy_id} = $$args{copyid};
375         delete $$args{copyid};
376     }
377
378     if( $$args{patronid} ) {
379         $$args{patron_id} = $$args{patronid};
380         delete $$args{patronid};
381     }
382
383     if( $$args{patron} and !ref($$args{patron}) ) {
384         $$args{patron_id} = $$args{patron};
385         delete $$args{patron};
386     }
387
388
389     if( $$args{noncat} ) {
390         $$args{is_noncat} = $$args{noncat};
391         delete $$args{noncat};
392     }
393
394     if( $$args{precat} ) {
395         $$args{is_precat} = $$args{request_precat} = $$args{precat};
396         delete $$args{precat};
397     }
398 }
399
400
401
402 # --------------------------------------------------------------------------
403 # This package actually manages all of the circulation logic
404 # --------------------------------------------------------------------------
405 package OpenILS::Application::Circ::Circulator;
406 use strict; use warnings;
407 use vars q/$AUTOLOAD/;
408 use DateTime;
409 use OpenILS::Utils::Fieldmapper;
410 use OpenSRF::Utils::Cache;
411 use Digest::MD5 qw(md5_hex);
412 use DateTime::Format::ISO8601;
413 use OpenILS::Utils::PermitHold;
414 use OpenSRF::Utils qw/:datetime/;
415 use OpenSRF::Utils::SettingsClient;
416 use OpenILS::Application::Circ::Holds;
417 use OpenILS::Application::Circ::Transit;
418 use OpenSRF::Utils::Logger qw(:logger);
419 use OpenILS::Utils::CStoreEditor qw/:funcs/;
420 use OpenILS::Application::Circ::ScriptBuilder;
421 use OpenILS::Const qw/:const/;
422 use OpenILS::Utils::Penalty;
423 use OpenILS::Application::Circ::CircCommon;
424 use Time::Local;
425
426 my $holdcode    = "OpenILS::Application::Circ::Holds";
427 my $transcode   = "OpenILS::Application::Circ::Transit";
428 my %user_groups;
429
430 sub DESTROY { }
431
432
433 # --------------------------------------------------------------------------
434 # Add a pile of automagic getter/setter methods
435 # --------------------------------------------------------------------------
436 my @AUTOLOAD_FIELDS = qw/
437     notify_hold
438     remote_hold
439     backdate
440     reservation
441     copy
442     copy_id
443     copy_barcode
444     patron
445     patron_id
446     patron_barcode
447     script_runner
448     volume
449     title
450     is_renewal
451     is_checkout
452     is_res_checkout
453     is_precat
454     is_noncat
455     request_precat
456     is_checkin
457     is_res_checkin
458     noncat_type
459     editor
460     events
461     cache_handle
462     override
463     circ_permit_patron
464     circ_permit_copy
465     circ_duration
466     circ_recurring_fines
467     circ_max_fines
468     circ_permit_renew
469     circ
470     transit
471     hold
472     permit_key
473     noncat_circ_lib
474     noncat_count
475     checkout_time
476     dummy_title
477     dummy_author
478     dummy_isbn
479     circ_modifier
480     circ_lib
481     barcode
482     duration_level
483     recurring_fines_level
484     duration_rule
485     recurring_fines_rule
486     max_fine_rule
487     renewal_remaining
488     hard_due_date
489     due_date
490     fulfilled_holds
491     transit
492     checkin_changed
493     force
494     permit_override
495     pending_checkouts
496     cancelled_hold_transit
497     opac_renewal
498     phone_renewal
499     desk_renewal
500     sip_renewal
501     retarget
502     matrix_test_result
503     circ_matrix_matchpoint
504     circ_test_success
505     legacy_script_support
506     is_deposit
507     is_rental
508     deposit_billing
509     rental_billing
510     capture
511     noop
512     void_overdues
513     parent_circ
514     return_patron
515     claims_never_checked_out
516     skip_permit_key
517     skip_deposit_fee
518     skip_rental_fee
519     use_booking
520 /;
521
522
523 sub AUTOLOAD {
524     my $self = shift;
525     my $type = ref($self) or die "$self is not an object";
526     my $data = shift;
527     my $name = $AUTOLOAD;
528     $name =~ s/.*://o;   
529
530     unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
531         $logger->error("circulator: $type: invalid autoload field: $name");
532         die "$type: invalid autoload field: $name\n" 
533     }
534
535     {
536         no strict 'refs';
537         *{"${type}::${name}"} = sub {
538             my $s = shift;
539             my $v = shift;
540             $s->{$name} = $v if defined $v;
541             return $s->{$name};
542         }
543     }
544     return $self->$name($data);
545 }
546
547
548 sub new {
549     my( $class, $auth, %args ) = @_;
550     $class = ref($class) || $class;
551     my $self = bless( {}, $class );
552
553     $self->events([]);
554     $self->editor(new_editor(xact => 1, authtoken => $auth));
555
556     unless( $self->editor->checkauth ) {
557         $self->bail_on_events($self->editor->event);
558         return $self;
559     }
560
561     $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
562
563     $self->$_($args{$_}) for keys %args;
564
565     $self->circ_lib(
566         ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
567
568     # if this is a renewal, default to desk_renewal
569     $self->desk_renewal(1) unless 
570         $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
571
572     $self->capture('') unless $self->capture;
573
574     unless(%user_groups) {
575         my $gps = $self->editor->retrieve_all_permission_grp_tree;
576         %user_groups = map { $_->id => $_ } @$gps;
577     }
578
579     return $self;
580 }
581
582
583 # --------------------------------------------------------------------------
584 # True if we should discontinue processing
585 # --------------------------------------------------------------------------
586 sub bail_out {
587     my( $self, $bool ) = @_;
588     if( defined $bool ) {
589         $logger->info("circulator: BAILING OUT") if $bool;
590         $self->{bail_out} = $bool;
591     }
592     return $self->{bail_out};
593 }
594
595
596 sub push_events {
597     my( $self, @evts ) = @_;
598     for my $e (@evts) {
599         next unless $e;
600         $e->{payload} = $self->copy if 
601               ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
602
603         $logger->info("circulator: pushing event ".$e->{textcode});
604         push( @{$self->events}, $e ) unless
605             grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
606     }
607 }
608
609 sub mk_permit_key {
610     my $self = shift;
611     return '' if $self->skip_permit_key;
612     my $key = md5_hex( time() . rand() . "$$" );
613     $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
614     return $self->permit_key($key);
615 }
616
617 sub check_permit_key {
618     my $self = shift;
619     return 1 if $self->skip_permit_key;
620     my $key = $self->permit_key;
621     return 0 unless $key;
622     my $k = "oils_permit_key_$key";
623     my $one = $self->cache_handle->get_cache($k);
624     $self->cache_handle->delete_cache($k);
625     return ($one) ? 1 : 0;
626 }
627
628 sub seems_like_reservation {
629     my $self = shift;
630
631     # Some words about the following method:
632     # 1) It requires the VIEW_USER permission, but that's not an
633     # issue, right, since all staff should have that?
634     # 2) It returns only one reservation at a time, even if an item can be
635     # and is currently overbooked.  Hmmm....
636     my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
637     my $result = $booking_ses->request(
638         "open-ils.booking.reservations.by_returnable_resource_barcode",
639         $self->editor->authtoken,
640         $self->copy_barcode
641     )->gather(1);
642     $booking_ses->disconnect;
643
644     return $self->bail_on_events($result) if defined $U->event_code($result);
645
646     if (@$result > 0) {
647         $self->reservation(shift @$result);
648         return 1;
649     } else {
650         return 0;
651     }
652
653 }
654
655 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
656 sub save_trimmed_copy {
657     my ($self, $copy) = @_;
658
659     $self->copy($copy);
660     $self->volume($copy->call_number);
661     $self->title($self->volume->record);
662     $self->copy->call_number($self->volume->id);
663     $self->volume->record($self->title->id);
664     $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
665     if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
666         $self->is_deposit(1) if $U->is_true($self->copy->deposit);
667         $self->is_rental(1) unless $U->is_true($self->copy->deposit);
668     }
669 }
670
671 sub mk_env {
672     my $self = shift;
673     my $e = $self->editor;
674
675     # --------------------------------------------------------------------------
676     # Grab the fleshed copy
677     # --------------------------------------------------------------------------
678     unless($self->is_noncat) {
679         my $copy;
680             if($self->copy_id) {
681                     $copy = $e->retrieve_asset_copy(
682                             [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
683     
684             } elsif( $self->copy_barcode ) {
685     
686                     $copy = $e->search_asset_copy(
687                             [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
688             } elsif( $self->reservation ) {
689             my $res = $e->json_query(
690                 {
691                     "select" => {"acp" => ["id"]},
692                     "from" => {
693                         "acp" => {
694                             "brsrc" => {
695                                 "fkey" => "barcode",
696                                 "field" => "barcode",
697                                 "join" => {
698                                     "bresv" => {
699                                         "fkey" => "id",
700                                         "field" => "current_resource"
701                                     }
702                                 }
703                             }
704                         }
705                     },
706                     "where" => {
707                         "+bresv" => {
708                             "id" => (ref $self->reservation) ?
709                                 $self->reservation->id : $self->reservation
710                         }
711                     }
712                 }
713             );
714             if (ref $res eq "ARRAY" and scalar @$res) {
715                 $logger->info("circulator: mapped reservation " .
716                     $self->reservation . " to copy " . $res->[0]->{"id"});
717                 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
718             }
719         }
720     
721         if($copy) {
722             $self->save_trimmed_copy($copy);
723         } else {
724             # We can't renew if there is no copy
725             return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
726                 if $self->is_renewal;
727             $self->is_precat(1);
728         }
729     }
730
731     # --------------------------------------------------------------------------
732     # Grab the patron
733     # --------------------------------------------------------------------------
734     my $patron;
735         my $flesh = {
736                 flesh => 1,
737                 flesh_fields => {au => [ qw/ card / ]}
738         };
739
740         if( $self->patron_id ) {
741                 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
742                         or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
743
744         } elsif( $self->patron_barcode ) {
745
746                 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
747                 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0] 
748                         or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
749
750                 $patron = $e->search_actor_user([{card => $card->id}, $flesh])->[0]
751                         or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
752
753     } else {
754         if( my $copy = $self->copy ) {
755
756             $flesh->{flesh} = 2;
757             $flesh->{flesh_fields}->{circ} = ['usr'];
758
759             my $circ = $e->search_action_circulation([
760                 {target_copy => $copy->id, checkin_time => undef}, $flesh
761             ])->[0];
762
763             if($circ) {
764                 $patron = $circ->usr;
765                 $circ->usr($patron->id); # de-flesh for consistency
766                 $self->circ($circ); 
767             }
768         }
769     }
770
771     return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
772         unless $self->patron($patron) or $self->is_checkin;
773
774     unless($self->is_checkin) {
775
776         # Check for inactivity and patron reg. expiration
777
778         $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
779                         unless $U->is_true($patron->active);
780         
781                 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
782                         unless $U->is_true($patron->card->active);
783         
784                 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
785                         cleanse_ISO8601($patron->expire_date));
786         
787                 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
788                         if( CORE::time > $expire->epoch ) ;
789     }
790 }
791
792 # --------------------------------------------------------------------------
793 # This builds the script runner environment and fetches most of the
794 # objects we need
795 # --------------------------------------------------------------------------
796 sub mk_script_runner {
797     my $self = shift;
798     my $args = {};
799
800
801     my @fields = 
802         qw/copy copy_barcode copy_id patron 
803             patron_id patron_barcode volume title editor/;
804
805     # Translate our objects into the ScriptBuilder args hash
806     $$args{$_} = $self->$_() for @fields;
807
808     $args->{ignore_user_status} = 1 if $self->is_checkin;
809     $$args{fetch_patron_by_circ_copy} = 1;
810     $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
811
812     if( my $pco = $self->pending_checkouts ) {
813         $logger->info("circulator: we were given a pending checkouts number of $pco");
814         $$args{patronItemsOut} = $pco;
815     }
816
817     # This fetches most of the objects we need
818     $self->script_runner(
819         OpenILS::Application::Circ::ScriptBuilder->build($args));
820
821     # Now we translate the ScriptBuilder objects back into self
822     $self->$_($$args{$_}) for @fields;
823
824     my @evts = @{$args->{_events}} if $args->{_events};
825
826     $logger->debug("circulator: script builder returned events: @evts") if @evts;
827
828
829     if(@evts) {
830         # Anything besides ASSET_COPY_NOT_FOUND will stop processing
831         if(!$self->is_noncat and 
832             @evts == 1 and 
833             $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
834                 $self->is_precat(1);
835
836         } else {
837             my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
838             return $self->bail_on_events(@e);
839         }
840     }
841
842     if($self->copy) {
843         $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
844         if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
845             $self->is_deposit(1) if $U->is_true($self->copy->deposit);
846             $self->is_rental(1) unless $U->is_true($self->copy->deposit);
847         }
848     }
849
850     # We can't renew if there is no copy
851     return $self->bail_on_events(@evts) if 
852         $self->is_renewal and !$self->copy;
853
854     # Set some circ-specific flags in the script environment
855     my $evt = "environment";
856     $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
857
858     if( $self->is_noncat ) {
859       $self->script_runner->insert("$evt.isNonCat", 1);
860       $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
861     }
862
863     if( $self->is_precat ) {
864         $self->script_runner->insert("environment.isPrecat", 1, 1);
865     }
866
867     $self->script_runner->add_path( $_ ) for @$script_libs;
868
869     return 1;
870 }
871
872 # --------------------------------------------------------------------------
873 # Does the circ permit work
874 # --------------------------------------------------------------------------
875 sub do_permit {
876     my $self = shift;
877
878     $self->log_me("do_permit()");
879
880     unless( $self->editor->requestor->id == $self->patron->id ) {
881         return $self->bail_on_events($self->editor->event)
882             unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
883     }
884
885     $self->check_captured_holds();
886     $self->do_copy_checks();
887     return if $self->bail_out;
888     $self->run_patron_permit_scripts();
889     $self->run_copy_permit_scripts() 
890         unless $self->is_precat or $self->is_noncat;
891     $self->check_item_deposit_events();
892     $self->override_events();
893     return if $self->bail_out;
894
895     if($self->is_precat and not $self->request_precat) {
896         $self->push_events(
897             OpenILS::Event->new(
898                 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
899         return $self->bail_out(1) unless $self->is_renewal;
900     }
901
902     $self->push_events(
903         OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
904 }
905
906 sub check_item_deposit_events {
907     my $self = shift;
908     $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy)) 
909         if $self->is_deposit and not $self->is_deposit_exempt;
910     $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy)) 
911         if $self->is_rental and not $self->is_rental_exempt;
912 }
913
914 # returns true if the user is not required to pay deposits
915 sub is_deposit_exempt {
916     my $self = shift;
917     my $pid = (ref $self->patron->profile) ?
918         $self->patron->profile->id : $self->patron->profile;
919     my $groups = $U->ou_ancestor_setting_value(
920         $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
921     for my $grp (@$groups) {
922         return 1 if $self->is_group_descendant($grp, $pid);
923     }
924     return 0;
925 }
926
927 # returns true if the user is not required to pay rental fees
928 sub is_rental_exempt {
929     my $self = shift;
930     my $pid = (ref $self->patron->profile) ?
931         $self->patron->profile->id : $self->patron->profile;
932     my $groups = $U->ou_ancestor_setting_value(
933         $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
934     for my $grp (@$groups) {
935         return 1 if $self->is_group_descendant($grp, $pid);
936     }
937     return 0;
938 }
939
940 sub is_group_descendant {
941     my($self, $p_id, $c_id) = @_;
942     return 0 unless defined $p_id and defined $c_id;
943     return 1 if $c_id == $p_id;
944     while(my $grp = $user_groups{$c_id}) {
945         $c_id = $grp->parent;
946         return 0 unless defined $c_id;
947         return 1 if $c_id == $p_id;
948     }
949     return 0;
950 }
951
952 sub check_captured_holds {
953    my $self    = shift;
954    my $copy    = $self->copy;
955    my $patron  = $self->patron;
956
957     return undef unless $copy;
958
959     my $s = $U->copy_status($copy->status)->id;
960     return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
961     $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
962
963     # Item is on the holds shelf, make sure it's going to the right person
964     my $holds   = $self->editor->search_action_hold_request(
965         [
966             { 
967                 current_copy        => $copy->id , 
968                 capture_time        => { '!=' => undef },
969                 cancel_time         => undef, 
970                 fulfillment_time    => undef 
971             },
972             { limit => 1 }
973         ]
974     );
975
976     if( $holds and $$holds[0] ) {
977         return undef if $$holds[0]->usr == $patron->id;
978     }
979
980     $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
981
982     $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
983 }
984
985
986 sub do_copy_checks {
987     my $self = shift;
988     my $copy = $self->copy;
989     return unless $copy;
990
991     my $stat = $U->copy_status($copy->status)->id;
992
993     # We cannot check out a copy if it is in-transit
994     if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
995         return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
996     }
997
998     $self->handle_claims_returned();
999     return if $self->bail_out;
1000
1001     # no claims returned circ was found, check if there is any open circ
1002     unless( $self->is_renewal ) {
1003
1004         my $circs = $self->editor->search_action_circulation(
1005             { target_copy => $copy->id, checkin_time => undef }
1006         );
1007
1008         if(my $old_circ = $circs->[0]) { # an open circ was found
1009
1010             my $payload = {copy => $copy};
1011
1012             if($old_circ->usr == $self->patron->id) {
1013                 
1014                 $payload->{old_circ} = $old_circ;
1015
1016                 # If there is an open circulation on the checkout item and an auto-renew 
1017                 # interval is defined, inform the caller that they should go 
1018                 # ahead and renew the item instead of warning about open circulations.
1019     
1020                 my $auto_renew_intvl = $U->ou_ancestor_setting_value(        
1021                     $self->circ_lib,
1022                     'circ.checkout_auto_renew_age', 
1023                     $self->editor
1024                 );
1025
1026                 if($auto_renew_intvl) {
1027                     my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1028                     my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1029
1030                     if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1031                         $payload->{auto_renew} = 1;
1032                     }
1033                 }
1034             }
1035
1036             return $self->bail_on_events(
1037                 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1038             );
1039         }
1040     }
1041 }
1042
1043 my $LEGACY_CIRC_EVENT_MAP = {
1044     'no_item' => 'ITEM_NOT_CATALOGED',
1045     'actor.usr.barred' => 'PATRON_BARRED',
1046     'asset.copy.circulate' =>  'COPY_CIRC_NOT_ALLOWED',
1047     'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1048     'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1049     'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1050     'config.circ_matrix_test.max_items_out' =>  'PATRON_EXCEEDS_CHECKOUT_COUNT',
1051     'config.circ_matrix_test.max_overdue' =>  'PATRON_EXCEEDS_OVERDUE_COUNT',
1052     'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1053     'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1054 };
1055
1056
1057 # ---------------------------------------------------------------------
1058 # This pushes any patron-related events into the list but does not
1059 # set bail_out for any events
1060 # ---------------------------------------------------------------------
1061 sub run_patron_permit_scripts {
1062     my $self        = shift;
1063     my $runner      = $self->script_runner;
1064     my $patronid    = $self->patron->id;
1065
1066     my @allevents; 
1067
1068     if(!$self->legacy_script_support) {
1069
1070         my $results = $self->run_indb_circ_test;
1071         unless($self->circ_test_success) {
1072             # no_item result is OK during noncat checkout
1073             unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
1074                 push @allevents, $self->matrix_test_result_events;
1075             }
1076         }
1077
1078     } else {
1079
1080         # --------------------------------------------------------------------- 
1081         # # Now run the patron permit script 
1082         # ---------------------------------------------------------------------
1083         $runner->load($self->circ_permit_patron);
1084         my $result = $runner->run or 
1085             throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1086
1087         my $patron_events = $result->{events};
1088
1089         OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1090         my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1091         my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1092         $penalties = $penalties->{fatal_penalties};
1093
1094         for my $pen (@$penalties) {
1095             my $event = OpenILS::Event->new($pen->name);
1096             $event->{desc} = $pen->label;
1097             push(@allevents, $event);
1098         }
1099
1100         push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1101     }
1102
1103     for (@allevents) {
1104        $_->{payload} = $self->copy if 
1105              ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1106     }
1107
1108     $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1109
1110     $self->push_events(@allevents);
1111 }
1112
1113 sub matrix_test_result_codes {
1114     my $self = shift;
1115     map { $_->{"fail_part"} } @{$self->matrix_test_result};
1116 }
1117
1118 sub matrix_test_result_events {
1119     my $self = shift;
1120     map {
1121         my $event = new OpenILS::Event(
1122             $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1123         );
1124         $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1125         $event;
1126     } (@{$self->matrix_test_result});
1127 }
1128
1129 sub run_indb_circ_test {
1130     my $self = shift;
1131     return $self->matrix_test_result if $self->matrix_test_result;
1132
1133     my $dbfunc = ($self->is_renewal) ? 
1134         'action.item_user_renew_test' : 'action.item_user_circ_test';
1135
1136     if( $self->is_precat && $self->request_precat) {
1137         $self->make_precat_copy;
1138         return if $self->bail_out;
1139     }
1140
1141     my $results = $self->editor->json_query(
1142         {   from => [
1143                 $dbfunc,
1144                 $self->circ_lib,
1145                 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id, 
1146                 $self->patron->id,
1147             ]
1148         }
1149     );
1150
1151     $self->circ_test_success($U->is_true($results->[0]->{success}));
1152
1153     if(my $mp = $results->[0]->{matchpoint}) {
1154         $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1155         $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1156         $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1157         if($results->[0]->{renewals}) {
1158             $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1159         }
1160         $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1161         $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1162         $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1163     }
1164
1165     return $self->matrix_test_result($results);
1166 }
1167
1168 # ---------------------------------------------------------------------
1169 # given a use and copy, this will calculate the circulation policy
1170 # parameters.  Only works with in-db circ.
1171 # ---------------------------------------------------------------------
1172 sub do_inspect {
1173     my $self = shift;
1174
1175     return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1176
1177     $self->run_indb_circ_test;
1178
1179     my $results = {
1180         circ_test_success => $self->circ_test_success,
1181         failure_events => [],
1182         failure_codes => [],
1183         matchpoint => $self->circ_matrix_matchpoint
1184     };
1185
1186     unless($self->circ_test_success) {
1187         $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1188         $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1189     }
1190
1191     if($self->circ_matrix_matchpoint) {
1192         my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1193         my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1194         my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1195         my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1196     
1197         my $policy = $self->get_circ_policy(
1198             $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1199     
1200         $$results{$_} = $$policy{$_} for keys %$policy;
1201     }
1202
1203     return $results;
1204 }
1205
1206 # ---------------------------------------------------------------------
1207 # Loads the circ policy info for duration, recurring fine, and max
1208 # fine based on the current copy
1209 # ---------------------------------------------------------------------
1210 sub get_circ_policy {
1211     my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1212
1213     my $policy = {
1214         duration_rule => $duration_rule->name,
1215         recurring_fine_rule => $recurring_fine_rule->name,
1216         max_fine_rule => $max_fine_rule->name,
1217         max_fine => $self->get_max_fine_amount($max_fine_rule),
1218         fine_interval => $recurring_fine_rule->recurrence_interval,
1219         renewal_remaining => $duration_rule->max_renewals
1220     };
1221
1222     if($hard_due_date) {
1223         $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1224         $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1225     }
1226     else {
1227         $policy->{duration_date_ceiling} = undef;
1228         $policy->{duration_date_ceiling_force} = undef;
1229     }
1230
1231     $policy->{duration} = $duration_rule->shrt
1232         if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1233     $policy->{duration} = $duration_rule->normal
1234         if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1235     $policy->{duration} = $duration_rule->extended
1236         if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1237
1238     $policy->{recurring_fine} = $recurring_fine_rule->low
1239         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1240     $policy->{recurring_fine} = $recurring_fine_rule->normal
1241         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1242     $policy->{recurring_fine} = $recurring_fine_rule->high
1243         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1244
1245     return $policy;
1246 }
1247
1248 sub get_max_fine_amount {
1249     my $self = shift;
1250     my $max_fine_rule = shift;
1251     my $max_amount = $max_fine_rule->amount;
1252
1253     # if is_percent is true then the max->amount is
1254     # use as a percentage of the copy price
1255     if ($U->is_true($max_fine_rule->is_percent)) {
1256         my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1257         $max_amount = $price * $max_fine_rule->amount / 100;
1258     } elsif (
1259         $U->ou_ancestor_setting_value(
1260             $self->circ_lib,
1261             'circ.max_fine.cap_at_price',
1262             $self->editor
1263         )
1264     ) {
1265         my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1266         $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1267     }
1268
1269     return $max_amount;
1270 }
1271
1272
1273
1274 sub run_copy_permit_scripts {
1275     my $self = shift;
1276     my $copy = $self->copy || return;
1277     my $runner = $self->script_runner;
1278
1279     my @allevents;
1280
1281     if(!$self->legacy_script_support) {
1282         my $results = $self->run_indb_circ_test;
1283         push @allevents, $self->matrix_test_result_events
1284             unless $self->circ_test_success;
1285     } else {
1286     
1287        # ---------------------------------------------------------------------
1288        # Capture all of the copy permit events
1289        # ---------------------------------------------------------------------
1290        $runner->load($self->circ_permit_copy);
1291        my $result = $runner->run or 
1292             throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1293        my $copy_events = $result->{events};
1294
1295        # ---------------------------------------------------------------------
1296        # Now collect all of the events together
1297        # ---------------------------------------------------------------------
1298        push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1299     }
1300
1301     # See if this copy has an alert message
1302     my $ae = $self->check_copy_alert();
1303     push( @allevents, $ae ) if $ae;
1304
1305    # uniquify the events
1306    my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1307    @allevents = values %hash;
1308
1309     $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1310
1311     $self->push_events(@allevents);
1312 }
1313
1314
1315 sub check_copy_alert {
1316     my $self = shift;
1317     return undef if $self->is_renewal;
1318     return OpenILS::Event->new(
1319         'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1320         if $self->copy and $self->copy->alert_message;
1321     return undef;
1322 }
1323
1324
1325
1326 # --------------------------------------------------------------------------
1327 # If the call is overriding and has permissions to override every collected
1328 # event, the are cleared.  Any event that the caller does not have
1329 # permission to override, will be left in the event list and bail_out will
1330 # be set
1331 # XXX We need code in here to cancel any holds/transits on copies 
1332 # that are being force-checked out
1333 # --------------------------------------------------------------------------
1334 sub override_events {
1335     my $self = shift;
1336     my @events = @{$self->events};
1337     return unless @events;
1338
1339     if(!$self->override) {
1340         return $self->bail_out(1) 
1341             if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1342     }   
1343
1344     $self->events([]);
1345     
1346    for my $e (@events) {
1347       my $tc = $e->{textcode};
1348       next if $tc eq 'SUCCESS';
1349       my $ov = "$tc.override";
1350       $logger->info("circulator: attempting to override event: $ov");
1351
1352         return $self->bail_on_events($self->editor->event)
1353             unless( $self->editor->allowed($ov) );
1354    }
1355 }
1356     
1357
1358 # --------------------------------------------------------------------------
1359 # If there is an open claimsreturn circ on the requested copy, close the 
1360 # circ if overriding, otherwise bail out
1361 # --------------------------------------------------------------------------
1362 sub handle_claims_returned {
1363     my $self = shift;
1364     my $copy = $self->copy;
1365
1366     my $CR = $self->editor->search_action_circulation(
1367         {   
1368             target_copy     => $copy->id,
1369             stop_fines      => OILS_STOP_FINES_CLAIMSRETURNED,
1370             checkin_time    => undef,
1371         }
1372     );
1373
1374     return unless ($CR = $CR->[0]); 
1375
1376     my $evt;
1377
1378     # - If the caller has set the override flag, we will check the item in
1379     if($self->override) {
1380
1381         $CR->checkin_time('now');   
1382         $CR->checkin_scan_time('now');   
1383         $CR->checkin_lib($self->circ_lib);
1384         $CR->checkin_workstation($self->editor->requestor->wsid);
1385         $CR->checkin_staff($self->editor->requestor->id);
1386
1387         $evt = $self->editor->event 
1388             unless $self->editor->update_action_circulation($CR);
1389
1390     } else {
1391         $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1392     }
1393
1394     $self->bail_on_events($evt) if $evt;
1395     return;
1396 }
1397
1398
1399 # --------------------------------------------------------------------------
1400 # This performs the checkout
1401 # --------------------------------------------------------------------------
1402 sub do_checkout {
1403     my $self = shift;
1404
1405     $self->log_me("do_checkout()");
1406
1407     # make sure perms are good if this isn't a renewal
1408     unless( $self->is_renewal ) {
1409         return $self->bail_on_events($self->editor->event)
1410             unless( $self->editor->allowed('COPY_CHECKOUT') );
1411     }
1412
1413     # verify the permit key
1414     unless( $self->check_permit_key ) {
1415         if( $self->permit_override ) {
1416             return $self->bail_on_events($self->editor->event)
1417                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1418         } else {
1419             return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1420         }   
1421     }
1422
1423     # if this is a non-cataloged circ, build the circ and finish
1424     if( $self->is_noncat ) {
1425         $self->checkout_noncat;
1426         $self->push_events(
1427             OpenILS::Event->new('SUCCESS', 
1428             payload => { noncat_circ => $self->circ }));
1429         return;
1430     }
1431
1432     if( $self->is_precat ) {
1433         $self->make_precat_copy;
1434         return if $self->bail_out;
1435
1436     } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1437         return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1438     }
1439
1440     $self->do_copy_checks;
1441     return if $self->bail_out;
1442
1443     $self->run_checkout_scripts();
1444     return if $self->bail_out;
1445
1446     $self->build_checkout_circ_object();
1447     return if $self->bail_out;
1448
1449     my $modify_to_start = $self->booking_adjusted_due_date();
1450     return if $self->bail_out;
1451
1452     $self->apply_modified_due_date($modify_to_start);
1453     return if $self->bail_out;
1454
1455     return $self->bail_on_events($self->editor->event)
1456         unless $self->editor->create_action_circulation($self->circ);
1457
1458     # refresh the circ to force local time zone for now
1459     $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1460
1461     $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1462     $self->update_copy;
1463     return if $self->bail_out;
1464
1465     $self->apply_deposit_fee();
1466     return if $self->bail_out;
1467
1468     $self->handle_checkout_holds();
1469     return if $self->bail_out;
1470
1471     # ------------------------------------------------------------------------------
1472     # Update the patron penalty info in the DB.  Run it for permit-overrides 
1473     # since the penalties are not updated during the permit phase
1474     # ------------------------------------------------------------------------------
1475     OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1476
1477     my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1478     
1479     my $pcirc;
1480     if($self->is_renewal) {
1481         # flesh the billing summary for the checked-in circ
1482         $pcirc = $self->editor->retrieve_action_circulation([
1483             $self->parent_circ,
1484             {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1485         ]);
1486     }
1487
1488     $self->push_events(
1489         OpenILS::Event->new('SUCCESS',
1490             payload  => {
1491                 copy             => $U->unflesh_copy($self->copy),
1492                 circ             => $self->circ,
1493                 record           => $record,
1494                 holds_fulfilled  => $self->fulfilled_holds,
1495                 deposit_billing  => $self->deposit_billing,
1496                 rental_billing   => $self->rental_billing,
1497                 parent_circ      => $pcirc,
1498                 patron           => ($self->return_patron) ? $self->patron : undef,
1499                 patron_money     => $self->editor->retrieve_money_user_summary($self->patron->id)
1500             }
1501         )
1502     );
1503 }
1504
1505 sub apply_deposit_fee {
1506     my $self = shift;
1507     my $copy = $self->copy;
1508     return unless 
1509         ($self->is_deposit and not $self->is_deposit_exempt) or 
1510         ($self->is_rental and not $self->is_rental_exempt);
1511
1512     return if $self->is_deposit and $self->skip_deposit_fee;
1513     return if $self->is_rental and $self->skip_rental_fee;
1514
1515         my $bill = Fieldmapper::money::billing->new;
1516     my $amount = $copy->deposit_amount;
1517     my $billing_type;
1518     my $btype;
1519
1520     if($self->is_deposit) {
1521         $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1522         $btype = 5;
1523         $self->deposit_billing($bill);
1524     } else {
1525         $billing_type = OILS_BILLING_TYPE_RENTAL;
1526         $btype = 6;
1527         $self->rental_billing($bill);
1528     }
1529
1530         $bill->xact($self->circ->id);
1531         $bill->amount($amount);
1532         $bill->note(OILS_BILLING_NOTE_SYSTEM);
1533         $bill->billing_type($billing_type);
1534         $bill->btype($btype);
1535     $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1536
1537         $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1538 }
1539
1540 sub update_copy {
1541     my $self = shift;
1542     my $copy = $self->copy;
1543
1544     my $stat = $copy->status if ref $copy->status;
1545     my $loc = $copy->location if ref $copy->location;
1546     my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1547
1548     $copy->status($stat->id) if $stat;
1549     $copy->location($loc->id) if $loc;
1550     $copy->circ_lib($circ_lib->id) if $circ_lib;
1551     $copy->editor($self->editor->requestor->id);
1552     $copy->edit_date('now');
1553     $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1554
1555     return $self->bail_on_events($self->editor->event)
1556         unless $self->editor->update_asset_copy($self->copy);
1557
1558     $copy->status($U->copy_status($copy->status));
1559     $copy->location($loc) if $loc;
1560     $copy->circ_lib($circ_lib) if $circ_lib;
1561 }
1562
1563 sub update_reservation {
1564     my $self = shift;
1565     my $reservation = $self->reservation;
1566
1567     my $usr = $reservation->usr;
1568     my $target_rt = $reservation->target_resource_type;
1569     my $target_r = $reservation->target_resource;
1570     my $current_r = $reservation->current_resource;
1571
1572     $reservation->usr($usr->id) if ref $usr;
1573     $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1574     $reservation->target_resource($target_r->id) if ref $target_r;
1575     $reservation->current_resource($current_r->id) if ref $current_r;
1576
1577     return $self->bail_on_events($self->editor->event)
1578         unless $self->editor->update_booking_reservation($self->reservation);
1579
1580     my $evt;
1581     ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1582     $self->reservation($reservation);
1583 }
1584
1585
1586 sub bail_on_events {
1587     my( $self, @evts ) = @_;
1588     $self->push_events(@evts);
1589     $self->bail_out(1);
1590 }
1591
1592
1593 # ------------------------------------------------------------------------------
1594 # When an item is checked out, see if we can fulfill a hold for this patron
1595 # ------------------------------------------------------------------------------
1596 sub handle_checkout_holds {
1597    my $self    = shift;
1598    my $copy    = $self->copy;
1599    my $patron  = $self->patron;
1600
1601    my $e = $self->editor;
1602    $self->fulfilled_holds([]);
1603
1604    # pre/non-cats can't fulfill a hold
1605    return if $self->is_precat or $self->is_noncat;
1606
1607     my $hold = $e->search_action_hold_request({   
1608         current_copy        => $copy->id , 
1609         cancel_time         => undef, 
1610         fulfillment_time    => undef,
1611         '-or' => [
1612             {expire_time => undef},
1613             {expire_time => {'>' => 'now'}}
1614         ]
1615     })->[0];
1616
1617     if($hold and $hold->usr != $patron->id) {
1618         # reset the hold since the copy is now checked out
1619     
1620         $logger->info("circulator: un-targeting hold ".$hold->id.
1621             " because copy ".$copy->id." is getting checked out");
1622
1623         $hold->clear_prev_check_time; 
1624         $hold->clear_current_copy;
1625         $hold->clear_capture_time;
1626
1627         return $self->bail_on_event($e->event)
1628             unless $e->update_action_hold_request($hold);
1629
1630         $hold = undef;
1631     }
1632
1633     unless($hold) {
1634         $hold = $self->find_related_user_hold($copy, $patron) or return;
1635         $logger->info("circulator: found related hold to fulfill in checkout");
1636     }
1637
1638     $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1639
1640     # if the hold was never officially captured, capture it.
1641     $hold->current_copy($copy->id);
1642     $hold->capture_time('now') unless $hold->capture_time;
1643     $hold->fulfillment_time('now');
1644     $hold->fulfillment_staff($e->requestor->id);
1645     $hold->fulfillment_lib($self->circ_lib);
1646
1647     return $self->bail_on_events($e->event)
1648         unless $e->update_action_hold_request($hold);
1649
1650     $holdcode->delete_hold_copy_maps($e, $hold->id);
1651     return $self->fulfilled_holds([$hold->id]);
1652 }
1653
1654
1655 # ------------------------------------------------------------------------------
1656 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1657 # the patron directly targets the checked out item, see if there is another hold 
1658 # (with hold_type T or V) for the patron that could be fulfilled by the checked 
1659 # out item.  Fulfill the oldest hold and only fulfill 1 of them.
1660 # ------------------------------------------------------------------------------
1661 sub find_related_user_hold {
1662     my($self, $copy, $patron) = @_;
1663     my $e = $self->editor;
1664
1665     return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER; 
1666
1667     return undef unless $U->ou_ancestor_setting_value(        
1668         $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1669
1670     # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1671     my $args = {
1672         select => {ahr => ['id']}, 
1673         from => {
1674             ahr => {
1675                 acp => {
1676                     field => 'id', 
1677                     fkey => 'current_copy',
1678                     type => 'left' # there may be no current_copy
1679                 }
1680             }
1681         }, 
1682         where => {
1683             '+ahr' => {
1684                 usr => $patron->id,
1685                 fulfillment_time => undef,
1686                 cancel_time => undef,
1687                '-or' => [
1688                     {expire_time => undef},
1689                     {expire_time => {'>' => 'now'}}
1690                 ]
1691             },
1692             '-or' => [
1693                 {
1694                     '+ahr' => { 
1695                         hold_type => 'V',
1696                         target => $self->volume->id
1697                     }
1698                 },
1699                 { 
1700                     '+ahr' => { 
1701                         hold_type => 'T',
1702                         target => $self->title->id
1703                     }
1704                 },
1705             ],
1706             '+acp' => {
1707                 '-or' => [
1708                     {id => undef}, # left-join copy may be nonexistent
1709                     {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1710                 ]
1711             }
1712         },
1713         order_by => {ahr => {request_time => {direction => 'asc'}}},
1714         limit => 1
1715     };
1716
1717     my $hold_info = $e->json_query($args)->[0];
1718     return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1719     return undef;
1720 }
1721
1722
1723 sub run_checkout_scripts {
1724     my $self = shift;
1725     my $nobail = shift;
1726
1727     my $evt;
1728     my $runner = $self->script_runner;
1729
1730     my $duration;
1731     my $recurring;
1732     my $max_fine;
1733     my $hard_due_date;
1734     my $duration_name;
1735     my $recurring_name;
1736     my $max_fine_name;
1737     my $hard_due_date_name;
1738
1739     if(!$self->legacy_script_support) {
1740         $self->run_indb_circ_test();
1741         $duration = $self->circ_matrix_matchpoint->duration_rule;
1742         $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1743         $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1744         $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1745
1746     } else {
1747
1748        $runner->load($self->circ_duration);
1749
1750        my $result = $runner->run or 
1751             throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1752
1753        $duration_name   = $result->{durationRule};
1754        $recurring_name  = $result->{recurringFinesRule};
1755        $max_fine_name   = $result->{maxFine};
1756        $hard_due_date_name  = $result->{hardDueDate};
1757     }
1758
1759     $duration_name = $duration->name if $duration;
1760     if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1761
1762         unless($duration) {
1763             ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1764             return $self->bail_on_events($evt) if ($evt && !$nobail);
1765         
1766             ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1767             return $self->bail_on_events($evt) if ($evt && !$nobail);
1768         
1769             ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1770             return $self->bail_on_events($evt) if ($evt && !$nobail);
1771
1772             if($hard_due_date_name) {
1773                 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1774                 return $self->bail_on_events($evt) if ($evt && !$nobail);
1775             }
1776         }
1777
1778     } else {
1779
1780         # The item circulates with an unlimited duration
1781         $duration   = undef;
1782         $recurring  = undef;
1783         $max_fine   = undef;
1784         $hard_due_date = undef;
1785     }
1786
1787    $self->duration_rule($duration);
1788    $self->recurring_fines_rule($recurring);
1789    $self->max_fine_rule($max_fine);
1790    $self->hard_due_date($hard_due_date);
1791 }
1792
1793
1794 sub build_checkout_circ_object {
1795     my $self = shift;
1796
1797    my $circ       = Fieldmapper::action::circulation->new;
1798    my $duration   = $self->duration_rule;
1799    my $max        = $self->max_fine_rule;
1800    my $recurring  = $self->recurring_fines_rule;
1801    my $hard_due_date    = $self->hard_due_date;
1802    my $copy       = $self->copy;
1803    my $patron     = $self->patron;
1804    my $duration_date_ceiling;
1805    my $duration_date_ceiling_force;
1806
1807     if( $duration ) {
1808
1809         my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1810         $duration_date_ceiling = $policy->{duration_date_ceiling};
1811         $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1812
1813         my $dname = $duration->name;
1814         my $mname = $max->name;
1815         my $rname = $recurring->name;
1816         my $hdname = ''; 
1817         if($hard_due_date) {
1818             $hdname = $hard_due_date->name;
1819         }
1820
1821         $logger->debug("circulator: building circulation ".
1822             "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1823     
1824         $circ->duration($policy->{duration});
1825         $circ->recurring_fine($policy->{recurring_fine});
1826         $circ->duration_rule($duration->name);
1827         $circ->recurring_fine_rule($recurring->name);
1828         $circ->max_fine_rule($max->name);
1829         $circ->max_fine($policy->{max_fine});
1830         $circ->fine_interval($recurring->recurrence_interval);
1831         $circ->renewal_remaining($duration->max_renewals);
1832
1833     } else {
1834
1835         $logger->info("circulator: copy found with an unlimited circ duration");
1836         $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1837         $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1838         $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1839         $circ->renewal_remaining(0);
1840     }
1841
1842    $circ->target_copy( $copy->id );
1843    $circ->usr( $patron->id );
1844    $circ->circ_lib( $self->circ_lib );
1845    $circ->workstation($self->editor->requestor->wsid) 
1846     if defined $self->editor->requestor->wsid;
1847
1848     # renewals maintain a link to the parent circulation
1849     $circ->parent_circ($self->parent_circ);
1850
1851    if( $self->is_renewal ) {
1852       $circ->opac_renewal('t') if $self->opac_renewal;
1853       $circ->phone_renewal('t') if $self->phone_renewal;
1854       $circ->desk_renewal('t') if $self->desk_renewal;
1855       $circ->renewal_remaining($self->renewal_remaining);
1856       $circ->circ_staff($self->editor->requestor->id);
1857    }
1858
1859
1860     # if the user provided an overiding checkout time,
1861     # (e.g. the checkout really happened several hours ago), then
1862     # we apply that here.  Does this need a perm??
1863     $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1864         if $self->checkout_time;
1865
1866     # if a patron is renewing, 'requestor' will be the patron
1867     $circ->circ_staff($self->editor->requestor->id);
1868     $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
1869
1870     $self->circ($circ);
1871 }
1872
1873 sub do_reservation_pickup {
1874     my $self = shift;
1875
1876     $self->log_me("do_reservation_pickup()");
1877
1878     $self->reservation->pickup_time('now');
1879
1880     if (
1881         $self->reservation->current_resource &&
1882         $U->is_true($self->reservation->target_resource_type->catalog_item)
1883     ) {
1884         # We used to try to set $self->copy and $self->patron here,
1885         # but that should already be done.
1886
1887         $self->run_checkout_scripts(1);
1888
1889         my $duration   = $self->duration_rule;
1890         my $max        = $self->max_fine_rule;
1891         my $recurring  = $self->recurring_fines_rule;
1892
1893         if ($duration && $max && $recurring) {
1894             my $policy = $self->get_circ_policy($duration, $recurring, $max);
1895
1896             my $dname = $duration->name;
1897             my $mname = $max->name;
1898             my $rname = $recurring->name;
1899
1900             $logger->debug("circulator: updating reservation ".
1901                 "with duration=$dname, maxfine=$mname, recurring=$rname");
1902
1903             $self->reservation->fine_amount($policy->{recurring_fine});
1904             $self->reservation->max_fine($policy->{max_fine});
1905             $self->reservation->fine_interval($recurring->recurrence_interval);
1906         }
1907
1908         $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1909         $self->update_copy();
1910
1911     } else {
1912         $self->reservation->fine_amount(
1913             $self->reservation->target_resource_type->fine_amount
1914         );
1915         $self->reservation->max_fine(
1916             $self->reservation->target_resource_type->max_fine
1917         );
1918         $self->reservation->fine_interval(
1919             $self->reservation->target_resource_type->fine_interval
1920         );
1921     }
1922
1923     $self->update_reservation();
1924 }
1925
1926 sub do_reservation_return {
1927     my $self = shift;
1928     my $request = shift;
1929
1930     $self->log_me("do_reservation_return()");
1931
1932     if (not ref $self->reservation) {
1933         my ($reservation, $evt) =
1934             $U->fetch_booking_reservation($self->reservation);
1935         return $self->bail_on_events($evt) if $evt;
1936         $self->reservation($reservation);
1937     }
1938
1939     $self->generate_fines(1);
1940     $self->reservation->return_time('now');
1941     $self->update_reservation();
1942     $self->reshelve_copy if $self->copy;
1943
1944     if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1945         $self->copy( $self->reservation->current_resource->catalog_item );
1946     }
1947 }
1948
1949 sub booking_adjusted_due_date {
1950     my $self = shift;
1951     my $circ = $self->circ;
1952     my $copy = $self->copy;
1953
1954     return undef unless $self->use_booking;
1955
1956     my $changed;
1957
1958     if( $self->due_date ) {
1959
1960         return $self->bail_on_events($self->editor->event)
1961             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1962
1963        $circ->due_date(cleanse_ISO8601($self->due_date));
1964
1965     } else {
1966
1967         return unless $copy and $circ->due_date;
1968     }
1969
1970     my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1971     if (@$booking_items) {
1972         my $booking_item = $booking_items->[0];
1973         my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1974
1975         my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1976         my $shorten_circ_setting = $resource_type->elbow_room ||
1977             $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1978             '0 seconds';
1979
1980         my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1981         my $bookings = $booking_ses->request(
1982             'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
1983             { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef }}
1984         )->gather(1);
1985         $booking_ses->disconnect;
1986         
1987         my $dt_parser = DateTime::Format::ISO8601->new;
1988         my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
1989
1990         for my $bid (@$bookings) {
1991
1992             my $booking = $self->editor->retrieve_booking_reservation( $bid );
1993
1994             my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
1995             my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
1996
1997             return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
1998                 if ($booking_start < DateTime->now);
1999
2000
2001             if ($U->is_true($stop_circ_setting)) {
2002                 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ); 
2003             } else {
2004                 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2005                 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now); 
2006             }
2007             
2008             # We set the circ duration here only to affect the logic that will
2009             # later (in a DB trigger) mangle the time part of the due date to
2010             # 11:59pm. Having any circ duration that is not a whole number of
2011             # days is enough to prevent the "correction."
2012             my $new_circ_duration = $due_date->epoch - time;
2013             $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2014             $circ->duration("$new_circ_duration seconds");
2015
2016             $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2017             $changed = 1;
2018         }
2019
2020         return $self->bail_on_events($self->editor->event)
2021             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2022     }
2023
2024     return $changed;
2025 }
2026
2027 sub apply_modified_due_date {
2028     my $self = shift;
2029     my $shift_earlier = shift;
2030     my $circ = $self->circ;
2031     my $copy = $self->copy;
2032
2033    if( $self->due_date ) {
2034
2035         return $self->bail_on_events($self->editor->event)
2036             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2037
2038       $circ->due_date(cleanse_ISO8601($self->due_date));
2039
2040    } else {
2041
2042       # if the due_date lands on a day when the location is closed
2043       return unless $copy and $circ->due_date;
2044
2045         #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2046
2047         # due-date overlap should be determined by the location the item
2048         # is checked out from, not the owning or circ lib of the item
2049         my $org = $self->circ_lib;
2050
2051       $logger->info("circulator: circ searching for closed date overlap on lib $org".
2052             " with an item due date of ".$circ->due_date );
2053
2054       my $dateinfo = $U->storagereq(
2055          'open-ils.storage.actor.org_unit.closed_date.overlap', 
2056             $org, $circ->due_date );
2057
2058       if($dateinfo) {
2059          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2060             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2061
2062             # XXX make the behavior more dynamic
2063             # for now, we just push the due date to after the close date
2064             if ($shift_earlier) {
2065                 $circ->due_date($dateinfo->{start});
2066             } else {
2067                 $circ->due_date($dateinfo->{end});
2068             }
2069       }
2070    }
2071 }
2072
2073
2074
2075 sub create_due_date {
2076     my( $self, $duration, $date_ceiling, $force_date ) = @_;
2077
2078     # if there is a raw time component (e.g. from postgres), 
2079     # turn it into an interval that interval_to_seconds can parse
2080     $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2081
2082     # for now, use the server timezone.  TODO: use workstation org timezone
2083     my $due_date = DateTime->now(time_zone => 'local');
2084
2085     # add the circ duration
2086     $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2087
2088     if($date_ceiling) {
2089         my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2090         if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2091             $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2092             $due_date = $cdate;
2093         }
2094     }
2095
2096     # return ISO8601 time with timezone
2097     return $due_date->strftime('%FT%T%z');
2098 }
2099
2100
2101
2102 sub make_precat_copy {
2103     my $self = shift;
2104     my $copy = $self->copy;
2105
2106    if($copy) {
2107         $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2108
2109         $copy->editor($self->editor->requestor->id);
2110         $copy->edit_date('now');
2111         $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2112         $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2113         $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2114         $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2115         $self->update_copy();
2116         return;
2117    }
2118
2119     $logger->info("circulator: Creating a new precataloged ".
2120         "copy in checkout with barcode " . $self->copy_barcode);
2121
2122     $copy = Fieldmapper::asset::copy->new;
2123     $copy->circ_lib($self->circ_lib);
2124     $copy->creator($self->editor->requestor->id);
2125     $copy->editor($self->editor->requestor->id);
2126     $copy->barcode($self->copy_barcode);
2127     $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
2128     $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2129     $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2130
2131     $copy->dummy_title($self->dummy_title || "");
2132     $copy->dummy_author($self->dummy_author || "");
2133     $copy->dummy_isbn($self->dummy_isbn || "");
2134     $copy->circ_modifier($self->circ_modifier);
2135
2136
2137     # See if we need to override the circ_lib for the copy with a configured circ_lib
2138     # Setting is shortname of the org unit
2139     my $precat_circ_lib = $U->ou_ancestor_setting_value(
2140         $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2141
2142     if($precat_circ_lib) {
2143         my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2144
2145         if(!$org) {
2146             $self->bail_on_events($self->editor->event);
2147             return;
2148         }
2149
2150         $copy->circ_lib($org->id);
2151     }
2152
2153
2154     unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2155         $self->bail_out(1);
2156         $self->push_events($self->editor->event);
2157         return;
2158     }   
2159
2160     # this is a little bit of a hack, but we need to 
2161     # get the copy into the script runner
2162     $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2163 }
2164
2165
2166 sub checkout_noncat {
2167     my $self = shift;
2168
2169     my $circ;
2170     my $evt;
2171
2172    my $lib      = $self->noncat_circ_lib || $self->circ_lib;
2173    my $count    = $self->noncat_count || 1;
2174    my $cotime   = cleanse_ISO8601($self->checkout_time) || "";
2175
2176    $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2177
2178    for(1..$count) {
2179
2180       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2181          $self->editor->requestor->id, 
2182             $self->patron->id, 
2183             $lib, 
2184             $self->noncat_type, 
2185             $cotime,
2186             $self->editor );
2187
2188         if( $evt ) {
2189             $self->push_events($evt);
2190             $self->bail_out(1);
2191             return; 
2192         }
2193         $self->circ($circ);
2194    }
2195 }
2196
2197
2198 sub do_checkin {
2199     my $self = shift;
2200     $self->log_me("do_checkin()");
2201
2202     return $self->bail_on_events(
2203         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
2204         unless $self->copy;
2205
2206     # the renew code and mk_env should have already found our circulation object
2207     unless( $self->circ ) {
2208
2209         my $circs = $self->editor->search_action_circulation(
2210             { target_copy => $self->copy->id, checkin_time => undef });
2211
2212         $self->circ($$circs[0]);
2213
2214         # for now, just warn if there are multiple open circs on a copy
2215         $logger->warn("circulator: we have ".scalar(@$circs).
2216             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2217     }
2218
2219     # run the fine generator against this circ, if this circ is there
2220     $self->generate_fines_start if $self->circ;
2221
2222     if( $self->checkin_check_holds_shelf() ) {
2223         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2224         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2225         $self->checkin_flesh_events;
2226         return;
2227     }
2228
2229     unless( $self->is_renewal ) {
2230         return $self->bail_on_events($self->editor->event)
2231             unless $self->editor->allowed('COPY_CHECKIN');
2232     }
2233
2234     $self->push_events($self->check_copy_alert());
2235     $self->push_events($self->check_checkin_copy_status());
2236
2237     # if the circ is marked as 'claims returned', add the event to the list
2238     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2239         if ($self->circ and $self->circ->stop_fines 
2240                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2241
2242     $self->check_circ_deposit();
2243
2244     # handle the overridable events 
2245     $self->override_events unless $self->is_renewal;
2246     return if $self->bail_out;
2247     
2248     if( $self->copy ) {
2249         $self->transit(
2250             $self->editor->search_action_transit_copy(
2251                 { target_copy => $self->copy->id, dest_recv_time => undef }
2252             )->[0]
2253         ); 
2254     }
2255
2256     if( $self->circ ) {
2257         $self->checkin_handle_circ;
2258         return if $self->bail_out;
2259         $self->checkin_changed(1);
2260
2261     } elsif( $self->transit ) {
2262         my $hold_transit = $self->process_received_transit;
2263         $self->checkin_changed(1);
2264
2265         if( $self->bail_out ) { 
2266             $self->checkin_flesh_events;
2267             return;
2268         }
2269         
2270         if( my $e = $self->check_checkin_copy_status() ) {
2271             # If the original copy status is special, alert the caller
2272             my $ev = $self->events;
2273             $self->events([$e]);
2274             $self->override_events;
2275             return if $self->bail_out;
2276             $self->events($ev);
2277         }
2278
2279         if( $hold_transit or 
2280                 $U->copy_status($self->copy->status)->id 
2281                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2282
2283             my $hold;
2284             if( $hold_transit ) {
2285                $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2286             } else {
2287                    ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2288             }
2289
2290             $self->hold($hold);
2291
2292             if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2293
2294                 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2295                 $self->reshelve_copy(1);
2296                 $self->cancelled_hold_transit(1);
2297                 $self->notify_hold(0); # don't notify for cancelled holds
2298                 return if $self->bail_out;
2299
2300             } else {
2301
2302                 # hold transited to correct location
2303                 $self->checkin_flesh_events;
2304                 return;
2305             }
2306         } 
2307
2308     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2309
2310         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2311             " that is in-transit, but there is no transit.. repairing");
2312         $self->reshelve_copy(1);
2313         return if $self->bail_out;
2314     }
2315
2316     if( $self->is_renewal ) {
2317         $self->finish_fines_and_voiding;
2318         return if $self->bail_out;
2319         $self->push_events(OpenILS::Event->new('SUCCESS'));
2320         return;
2321     }
2322
2323    # ------------------------------------------------------------------------------
2324    # Circulations and transits are now closed where necessary.  Now go on to see if
2325    # this copy can fulfill a hold or needs to be routed to a different location
2326    # ------------------------------------------------------------------------------
2327
2328     my $needed_for_something = 0; # formerly "needed_for_hold"
2329
2330     if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2331
2332         if (!$self->remote_hold) {
2333             if ($self->use_booking) {
2334                 my $potential_hold = $self->hold_capture_is_possible;
2335                 my $potential_reservation = $self->reservation_capture_is_possible;
2336
2337                 if ($potential_hold and $potential_reservation) {
2338                     $logger->info("circulator: item could fulfill either hold or reservation");
2339                     $self->push_events(new OpenILS::Event(
2340                         "HOLD_RESERVATION_CONFLICT",
2341                         "hold" => $potential_hold,
2342                         "reservation" => $potential_reservation
2343                     ));
2344                     return if $self->bail_out;
2345                 } elsif ($potential_hold) {
2346                     $needed_for_something =
2347                         $self->attempt_checkin_hold_capture;
2348                 } elsif ($potential_reservation) {
2349                     $needed_for_something =
2350                         $self->attempt_checkin_reservation_capture;
2351                 }
2352             } else {
2353                 $needed_for_something = $self->attempt_checkin_hold_capture;
2354             }
2355         }
2356         return if $self->bail_out;
2357     
2358         unless($needed_for_something) {
2359             my $circ_lib = (ref $self->copy->circ_lib) ? 
2360                     $self->copy->circ_lib->id : $self->copy->circ_lib;
2361     
2362             if( $self->remote_hold ) {
2363                 $circ_lib = $self->remote_hold->pickup_lib;
2364                 $logger->warn("circulator: Copy ".$self->copy->barcode.
2365                     " is on a remote hold's shelf, sending to $circ_lib");
2366             }
2367     
2368             $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2369     
2370             if( $circ_lib == $self->circ_lib) {
2371                 # copy is where it needs to be, either for hold or reshelving
2372     
2373                 $self->checkin_handle_precat();
2374                 return if $self->bail_out;
2375     
2376             } else {
2377                 # copy needs to transit "home", or stick here if it's a floating copy
2378     
2379                 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2380                     $self->checkin_changed(1);
2381                     $self->copy->circ_lib( $self->circ_lib );
2382                     $self->update_copy;
2383                 } else {
2384                     my $bc = $self->copy->barcode;
2385                     $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2386                     $self->checkin_build_copy_transit($circ_lib);
2387                     return if $self->bail_out;
2388                     $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2389                 }
2390             }
2391         }
2392     } else { # no-op checkin
2393         if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2394             $self->checkin_changed(1);
2395             $self->copy->circ_lib( $self->circ_lib );
2396             $self->update_copy;
2397         }
2398     }
2399
2400     if($self->claims_never_checked_out and 
2401             $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2402
2403         # the item was not supposed to be checked out to the user and should now be marked as missing
2404         $self->copy->status(OILS_COPY_STATUS_MISSING);
2405         $self->update_copy;
2406
2407     } else {
2408         $self->reshelve_copy unless $needed_for_something;
2409     }
2410
2411     return if $self->bail_out;
2412
2413     unless($self->checkin_changed) {
2414
2415         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2416         my $stat = $U->copy_status($self->copy->status)->id;
2417
2418         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2419          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2420         $self->bail_out(1); # no need to commit anything
2421
2422     } else {
2423
2424         $self->push_events(OpenILS::Event->new('SUCCESS')) 
2425             unless @{$self->events};
2426     }
2427
2428     $self->finish_fines_and_voiding;
2429
2430     OpenILS::Utils::Penalty->calculate_penalties(
2431         $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2432
2433     $self->checkin_flesh_events;
2434     return;
2435 }
2436
2437 sub finish_fines_and_voiding {
2438     my $self = shift;
2439     return unless $self->circ;
2440
2441     # gather any updates to the circ after fine generation, if there was a circ
2442     $self->generate_fines_finish;
2443
2444     return unless $self->backdate or $self->void_overdues;
2445
2446     # void overdues after fine generation to prevent concurrent DB access to overdue billings
2447     my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2448
2449     my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2450         $self->editor, $self->circ, $self->backdate, $note);
2451
2452     return $self->bail_on_events($evt) if $evt;
2453
2454     # make sure the circ isn't closed if we just voided some fines
2455     $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2456     return $self->bail_on_events($evt) if $evt;
2457
2458     return undef;
2459 }
2460
2461
2462 # if a deposit was payed for this item, push the event
2463 sub check_circ_deposit {
2464     my $self = shift;
2465     return unless $self->circ;
2466     my $deposit = $self->editor->search_money_billing(
2467         {   btype => 5, 
2468             xact => $self->circ->id, 
2469             voided => 'f'
2470         }, {idlist => 1})->[0];
2471
2472     $self->push_events(OpenILS::Event->new(
2473         'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2474 }
2475
2476 sub reshelve_copy {
2477    my $self    = shift;
2478    my $force   = $self->force || shift;
2479    my $copy    = $self->copy;
2480
2481    my $stat = $U->copy_status($copy->status)->id;
2482
2483    if($force || (
2484       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2485       $stat != OILS_COPY_STATUS_CATALOGING and
2486       $stat != OILS_COPY_STATUS_IN_TRANSIT and
2487       $stat != OILS_COPY_STATUS_RESHELVING  )) {
2488
2489         $copy->status( OILS_COPY_STATUS_RESHELVING );
2490             $self->update_copy;
2491             $self->checkin_changed(1);
2492     }
2493 }
2494
2495
2496 # Returns true if the item is at the current location
2497 # because it was transited there for a hold and the 
2498 # hold has not been fulfilled
2499 sub checkin_check_holds_shelf {
2500     my $self = shift;
2501     return 0 unless $self->copy;
2502
2503     return 0 unless 
2504         $U->copy_status($self->copy->status)->id ==
2505             OILS_COPY_STATUS_ON_HOLDS_SHELF;
2506
2507     # find the hold that put us on the holds shelf
2508     my $holds = $self->editor->search_action_hold_request(
2509         { 
2510             current_copy => $self->copy->id,
2511             capture_time => { '!=' => undef },
2512             fulfillment_time => undef,
2513             cancel_time => undef,
2514         }
2515     );
2516
2517     unless(@$holds) {
2518         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2519         $self->reshelve_copy(1);
2520         return 0;
2521     }
2522
2523     my $hold = $$holds[0];
2524
2525     $logger->info("circulator: we found a captured, un-fulfilled hold [".
2526         $hold->id. "] for copy ".$self->copy->barcode);
2527
2528     if( $hold->pickup_lib == $self->circ_lib ) {
2529         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2530         return 1;
2531     }
2532
2533     $logger->info("circulator: hold is not for here..");
2534     $self->remote_hold($hold);
2535     return 0;
2536 }
2537
2538
2539 sub checkin_handle_precat {
2540     my $self    = shift;
2541    my $copy    = $self->copy;
2542
2543    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2544         $copy->status(OILS_COPY_STATUS_CATALOGING);
2545         $self->update_copy();
2546         $self->checkin_changed(1);
2547         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2548    }
2549 }
2550
2551
2552 sub checkin_build_copy_transit {
2553     my $self            = shift;
2554     my $dest            = shift;
2555     my $copy       = $self->copy;
2556    my $transit    = Fieldmapper::action::transit_copy->new;
2557
2558     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2559     $logger->info("circulator: transiting copy to $dest");
2560
2561    $transit->source($self->circ_lib);
2562    $transit->dest($dest);
2563    $transit->target_copy($copy->id);
2564    $transit->source_send_time('now');
2565    $transit->copy_status( $U->copy_status($copy->status)->id );
2566
2567     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2568
2569     return $self->bail_on_events($self->editor->event)
2570         unless $self->editor->create_action_transit_copy($transit);
2571
2572    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2573     $self->update_copy;
2574     $self->checkin_changed(1);
2575 }
2576
2577
2578 sub hold_capture_is_possible {
2579     my $self = shift;
2580     my $copy = $self->copy;
2581
2582     # we've been explicitly told not to capture any holds
2583     return 0 if $self->capture eq 'nocapture';
2584
2585     # See if this copy can fulfill any holds
2586     my $hold = $holdcode->find_nearest_permitted_hold(
2587         $self->editor, $copy, $self->editor->requestor, 1 # check_only
2588     );
2589     return undef if ref $hold eq "HASH" and
2590         $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2591     return $hold;
2592 }
2593
2594 sub reservation_capture_is_possible {
2595     my $self = shift;
2596     my $copy = $self->copy;
2597
2598     # we've been explicitly told not to capture any holds
2599     return 0 if $self->capture eq 'nocapture';
2600
2601     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2602     my $resv = $booking_ses->request(
2603         "open-ils.booking.reservations.could_capture",
2604         $self->editor->authtoken, $copy->barcode
2605     )->gather(1);
2606     $booking_ses->disconnect;
2607     if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2608         $self->push_events($resv);
2609     } else {
2610         return $resv;
2611     }
2612 }
2613
2614 # returns true if the item was used (or may potentially be used 
2615 # in subsequent calls) to capture a hold.
2616 sub attempt_checkin_hold_capture {
2617     my $self = shift;
2618     my $copy = $self->copy;
2619
2620     # we've been explicitly told not to capture any holds
2621     return 0 if $self->capture eq 'nocapture';
2622
2623     # See if this copy can fulfill any holds
2624     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
2625         $self->editor, $copy, $self->editor->requestor );
2626
2627     if(!$hold) {
2628         $logger->debug("circulator: no potential permitted".
2629             "holds found for copy ".$copy->barcode);
2630         return 0;
2631     }
2632
2633     if($self->capture ne 'capture') {
2634         # see if this item is in a hold-capture-delay location
2635         my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2636         if($U->is_true($location->hold_verify)) {
2637             $self->bail_on_events(
2638                 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2639             return 1;
2640         }
2641     }
2642
2643     $self->retarget($retarget);
2644
2645     $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2646
2647     $hold->current_copy($copy->id);
2648     $hold->capture_time('now');
2649     $self->put_hold_on_shelf($hold) 
2650         if $hold->pickup_lib == $self->circ_lib;
2651
2652     # prevent DB errors caused by fetching 
2653     # holds from storage, and updating through cstore
2654     $hold->clear_fulfillment_time;
2655     $hold->clear_fulfillment_staff;
2656     $hold->clear_fulfillment_lib;
2657     $hold->clear_expire_time; 
2658     $hold->clear_cancel_time;
2659     $hold->clear_prev_check_time unless $hold->prev_check_time;
2660
2661     $self->bail_on_events($self->editor->event)
2662         unless $self->editor->update_action_hold_request($hold);
2663     $self->hold($hold);
2664     $self->checkin_changed(1);
2665
2666     return 0 if $self->bail_out;
2667
2668     if( $hold->pickup_lib == $self->circ_lib ) {
2669
2670         # This hold was captured in the correct location
2671         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2672         $self->push_events(OpenILS::Event->new('SUCCESS'));
2673
2674         #$self->do_hold_notify($hold->id);
2675         $self->notify_hold($hold->id);
2676
2677     } else {
2678     
2679         # Hold needs to be picked up elsewhere.  Build a hold
2680         # transit and route the item.
2681         $self->checkin_build_hold_transit();
2682         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2683         return 0 if $self->bail_out;
2684         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2685     }
2686
2687     # make sure we save the copy status
2688     $self->update_copy;
2689     return 1;
2690 }
2691
2692 sub attempt_checkin_reservation_capture {
2693     my $self = shift;
2694     my $copy = $self->copy;
2695
2696     # we've been explicitly told not to capture any holds
2697     return 0 if $self->capture eq 'nocapture';
2698
2699     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2700     my $evt = $booking_ses->request(
2701         "open-ils.booking.resources.capture_for_reservation",
2702         $self->editor->authtoken,
2703         $copy->barcode,
2704         1 # don't update copy - we probably have it locked
2705     )->gather(1);
2706     $booking_ses->disconnect;
2707
2708     if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2709         $logger->warn(
2710             "open-ils.booking.resources.capture_for_reservation " .
2711             "didn't return an event!"
2712         );
2713     } else {
2714         if (
2715             $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2716             $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2717         ) {
2718             # not-transferable is an error event we'll pass on the user
2719             $logger->warn("reservation capture attempted against non-transferable item");
2720             $self->push_events($evt);
2721             return 0;
2722         } elsif ($evt->{"textcode"} eq "SUCCESS") {
2723             # Re-retrieve copy as reservation capture may have changed
2724             # its status and whatnot.
2725             $logger->info(
2726                 "circulator: booking capture win on copy " . $self->copy->id
2727             );
2728             if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2729                 $logger->info(
2730                     "circulator: changing copy " . $self->copy->id .
2731                     "'s status from " . $self->copy->status . " to " .
2732                     $new_copy_status
2733                 );
2734                 $self->copy->status($new_copy_status);
2735                 $self->update_copy;
2736             }
2737             $self->reservation($evt->{"payload"}->{"reservation"});
2738
2739             if (exists $evt->{"payload"}->{"transit"}) {
2740                 $self->push_events(
2741                     new OpenILS::Event(
2742                         "ROUTE_ITEM",
2743                         "org" => $evt->{"payload"}->{"transit"}->dest
2744                     )
2745                 );
2746             }
2747             $self->checkin_changed(1);
2748             return 1;
2749         }
2750     }
2751     # other results are treated as "nothing to capture"
2752     return 0;
2753 }
2754
2755 sub do_hold_notify {
2756     my( $self, $holdid ) = @_;
2757
2758     my $e = new_editor(xact => 1);
2759     my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2760     $e->rollback;
2761     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2762     $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2763
2764     $logger->info("circulator: running delayed hold notify process");
2765
2766 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2767 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2768
2769     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2770         hold_id => $holdid, requestor => $self->editor->requestor);
2771
2772     $logger->debug("circulator: built hold notifier");
2773
2774     if(!$notifier->event) {
2775
2776         $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2777
2778         my $stat = $notifier->send_email_notify;
2779         if( $stat == '1' ) {
2780             $logger->info("circulator: hold notify succeeded for hold $holdid");
2781             return;
2782         } 
2783
2784         $logger->debug("circulator:  * hold notify cancelled or failed for hold $holdid");
2785
2786     } else {
2787         $logger->info("circulator: Not sending hold notification since the patron has no email address");
2788     }
2789 }
2790
2791 sub retarget_holds {
2792     my $self = shift;
2793     $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2794     my $ses = OpenSRF::AppSession->create('open-ils.storage');
2795     $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2796     # no reason to wait for the return value
2797     return;
2798 }
2799
2800 sub checkin_build_hold_transit {
2801     my $self = shift;
2802
2803    my $copy = $self->copy;
2804    my $hold = $self->hold;
2805    my $trans = Fieldmapper::action::hold_transit_copy->new;
2806
2807     $logger->debug("circulator: building hold transit for ".$copy->barcode);
2808
2809    $trans->hold($hold->id);
2810    $trans->source($self->circ_lib);
2811    $trans->dest($hold->pickup_lib);
2812    $trans->source_send_time("now");
2813    $trans->target_copy($copy->id);
2814
2815     # when the copy gets to its destination, it will recover
2816     # this status - put it onto the holds shelf
2817    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2818
2819     return $self->bail_on_events($self->editor->event)
2820         unless $self->editor->create_action_hold_transit_copy($trans);
2821 }
2822
2823
2824
2825 sub process_received_transit {
2826     my $self = shift;
2827     my $copy = $self->copy;
2828     my $copyid = $self->copy->id;
2829
2830     my $status_name = $U->copy_status($copy->status)->name;
2831     $logger->debug("circulator: attempting transit receive on ".
2832         "copy $copyid. Copy status is $status_name");
2833
2834     my $transit = $self->transit;
2835
2836     if( $transit->dest != $self->circ_lib ) {
2837         # - this item is in-transit to a different location
2838
2839         my $tid = $transit->id; 
2840         my $loc = $self->circ_lib;
2841         my $dest = $transit->dest;
2842
2843         $logger->info("circulator: Fowarding transit on copy which is destined ".
2844             "for a different location. transit=$tid, copy=$copyid, current ".
2845             "location=$loc, destination location=$dest");
2846
2847         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2848
2849         # grab the associated hold object if available
2850         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2851         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2852
2853         return $self->bail_on_events($evt);
2854     }
2855
2856     # The transit is received, set the receive time
2857     $transit->dest_recv_time('now');
2858     $self->bail_on_events($self->editor->event)
2859         unless $self->editor->update_action_transit_copy($transit);
2860
2861     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2862
2863     $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2864     $copy->status( $transit->copy_status );
2865     $self->update_copy();
2866     return if $self->bail_out;
2867
2868     my $ishold = 0;
2869     if($hold_transit) { 
2870         my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2871
2872         # hold has arrived at destination, set shelf time
2873         $self->put_hold_on_shelf($hold);
2874         $self->bail_on_events($self->editor->event)
2875             unless $self->editor->update_action_hold_request($hold);
2876         return if $self->bail_out;
2877
2878         $self->notify_hold($hold_transit->hold);
2879         $ishold = 1;
2880     }
2881
2882     $self->push_events( 
2883         OpenILS::Event->new(
2884         'SUCCESS', 
2885         ishold => $ishold,
2886       payload => { transit => $transit, holdtransit => $hold_transit } ));
2887
2888     return $hold_transit;
2889 }
2890
2891
2892 # ------------------------------------------------------------------
2893 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2894 # ------------------------------------------------------------------
2895 sub put_hold_on_shelf {
2896     my($self, $hold) = @_;
2897
2898     $hold->shelf_time('now');
2899
2900     my $shelf_expire = $U->ou_ancestor_setting_value(
2901         $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2902
2903     if($shelf_expire) {
2904         my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2905         my $expire_time = DateTime->now->add(seconds => $seconds);
2906         $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2907     }
2908
2909     return undef;
2910 }
2911
2912
2913
2914 sub generate_fines {
2915    my $self = shift;
2916    my $reservation = shift;
2917
2918    $self->generate_fines_start($reservation);
2919    $self->generate_fines_finish($reservation);
2920
2921    return undef;
2922 }
2923
2924 sub generate_fines_start {
2925    my $self = shift;
2926    my $reservation = shift;
2927
2928    my $id = $reservation ? $self->reservation->id : $self->circ->id;
2929
2930    if (!exists($self->{_gen_fines_req})) {
2931       $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage') 
2932           ->request(
2933              'open-ils.storage.action.circulation.overdue.generate_fines',
2934              undef,
2935              $id
2936           );
2937    }
2938
2939    return undef;
2940 }
2941
2942 sub generate_fines_finish {
2943    my $self = shift;
2944    my $reservation = shift;
2945
2946    my $id = $reservation ? $self->reservation->id : $self->circ->id;
2947
2948    $self->{_gen_fines_req}->wait_complete;
2949    delete($self->{_gen_fines_req});
2950
2951    # refresh the circ in case the fine generator set the stop_fines field
2952    $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
2953    $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
2954
2955    return undef;
2956 }
2957
2958 sub checkin_handle_circ {
2959    my $self = shift;
2960    my $circ = $self->circ;
2961    my $copy = $self->copy;
2962    my $evt;
2963    my $obt;
2964
2965    $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
2966
2967    # backdate the circ if necessary
2968    if($self->backdate) {
2969         my $evt = $self->checkin_handle_backdate;
2970         return $self->bail_on_events($evt) if $evt;
2971    }
2972
2973    if(!$circ->stop_fines) {
2974       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2975       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2976       $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
2977       $circ->stop_fines_time('now');
2978       $circ->stop_fines_time($self->backdate) if $self->backdate;
2979    }
2980
2981     # Set the checkin vars since we have the item
2982     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2983
2984     # capture the true scan time for back-dated checkins
2985     $circ->checkin_scan_time('now');
2986
2987     $circ->checkin_staff($self->editor->requestor->id);
2988     $circ->checkin_lib($self->circ_lib);
2989     $circ->checkin_workstation($self->editor->requestor->wsid);
2990
2991     my $circ_lib = (ref $self->copy->circ_lib) ?  
2992         $self->copy->circ_lib->id : $self->copy->circ_lib;
2993     my $stat = $U->copy_status($self->copy->status)->id;
2994
2995     # immediately available keeps items lost or missing items from going home before being handled
2996     my $lost_immediately_available = $U->ou_ancestor_setting_value(
2997         $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
2998
2999
3000     if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
3001
3002         if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
3003             $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
3004         } else {
3005             $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3006             $self->update_copy;
3007         }
3008
3009     } elsif ($stat == OILS_COPY_STATUS_LOST) {
3010
3011         $self->checkin_handle_lost($circ_lib);
3012
3013     } else {
3014
3015         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3016         $self->update_copy;
3017     }
3018
3019
3020     # see if there are any fines owed on this circ.  if not, close it
3021     ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3022     $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3023
3024     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3025
3026     return $self->bail_on_events($self->editor->event)
3027         unless $self->editor->update_action_circulation($circ);
3028
3029     return undef;
3030 }
3031
3032
3033 # ------------------------------------------------------------------
3034 # See if we need to void billings for lost checkin
3035 # ------------------------------------------------------------------
3036 sub checkin_handle_lost {
3037     my $self = shift;
3038     my $circ_lib = shift;
3039     my $circ = $self->circ;
3040
3041     my $max_return = $U->ou_ancestor_setting_value(
3042         $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3043
3044     if ($max_return) {
3045
3046         my $today = time();
3047         my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3048         $tm[5] -= 1 if $tm[5] > 0;
3049         my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3050
3051         my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3052         $logger->info("MAX OD: ".$max_return."  DUEDATE: ".$circ->due_date."  TODAY: ".$today."  DUE: ".$due."  LAST: ".$last_chance);
3053
3054         $max_return = 0 if $today < $last_chance;
3055     }
3056
3057     if (!$max_return){  # there's either no max time to accept returns defined or we're within that time
3058
3059         my $void_lost = $U->ou_ancestor_setting_value(
3060             $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3061         my $void_lost_fee = $U->ou_ancestor_setting_value(
3062             $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3063         my $restore_od = $U->ou_ancestor_setting_value(
3064             $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3065
3066         $self->checkin_handle_lost_now_found(3) if $void_lost;
3067         $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3068         $self->checkin_handle_lost_now_found_restore_od() if $restore_od && ! $self->void_overdues;
3069     }
3070
3071     $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3072     $self->update_copy;
3073 }
3074
3075
3076 sub checkin_handle_backdate {
3077     my $self = shift;
3078
3079     # ------------------------------------------------------------------
3080     # clean up the backdate for date comparison
3081     # XXX We are currently taking the due-time from the original due-date,
3082     # not the input.  Do we need to do this?  This certainly interferes with
3083     # backdating of hourly checkouts, but that is likely a very rare case.
3084     # ------------------------------------------------------------------
3085     my $bd = cleanse_ISO8601($self->backdate);
3086     my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3087     my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3088     $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3089
3090     $self->backdate($bd);
3091     return undef;
3092 }
3093
3094
3095 sub check_checkin_copy_status {
3096     my $self = shift;
3097    my $copy = $self->copy;
3098
3099    my $status = $U->copy_status($copy->status)->id;
3100
3101    return undef
3102       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
3103             $status == OILS_COPY_STATUS_CHECKED_OUT ||
3104             $status == OILS_COPY_STATUS_IN_PROCESS  ||
3105             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
3106             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
3107             $status == OILS_COPY_STATUS_CATALOGING  ||
3108             $status == OILS_COPY_STATUS_ON_RESV_SHELF  ||
3109             $status == OILS_COPY_STATUS_RESHELVING );
3110
3111    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3112       if( $status == OILS_COPY_STATUS_LOST );
3113
3114    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3115       if( $status == OILS_COPY_STATUS_MISSING );
3116
3117    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3118 }
3119
3120
3121
3122 # --------------------------------------------------------------------------
3123 # On checkin, we need to return as many relevant objects as we can
3124 # --------------------------------------------------------------------------
3125 sub checkin_flesh_events {
3126     my $self = shift;
3127
3128     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
3129         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3130             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3131     }
3132
3133     my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3134
3135     my $hold;
3136     if($self->hold and !$self->hold->cancel_time) {
3137         $hold = $self->hold;
3138         $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3139     }
3140
3141     if($self->circ) {
3142         # if we checked in a circulation, flesh the billing summary data
3143         $self->circ->billable_transaction(
3144             $self->editor->retrieve_money_billable_transaction([
3145                 $self->circ->id,
3146                 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3147             ])
3148         );
3149     }
3150
3151     if($self->patron) {
3152         # flesh some patron fields before returning
3153         $self->patron(
3154             $self->editor->retrieve_actor_user([
3155                 $self->patron->id,
3156                 {
3157                     flesh => 1,
3158                     flesh_fields => {
3159                         au => ['card', 'billing_address', 'mailing_address']
3160                     }
3161                 }
3162             ])
3163         );
3164     }
3165
3166     for my $evt (@{$self->events}) {
3167
3168         my $payload         = {};
3169         $payload->{copy}    = $U->unflesh_copy($self->copy);
3170         $payload->{record}  = $record,
3171         $payload->{circ}    = $self->circ;
3172         $payload->{transit} = $self->transit;
3173         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3174         $payload->{hold}    = $hold;
3175         $payload->{patron}  = $self->patron;
3176         $payload->{reservation} = $self->reservation
3177             unless (not $self->reservation or $self->reservation->cancel_time);
3178
3179         $evt->{payload}     = $payload;
3180     }
3181 }
3182
3183 sub log_me {
3184     my( $self, $msg ) = @_;
3185     my $bc = ($self->copy) ? $self->copy->barcode :
3186         $self->barcode;
3187     $bc ||= "";
3188     my $usr = ($self->patron) ? $self->patron->id : "";
3189     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3190         ", recipient=$usr, copy=$bc");
3191 }
3192
3193
3194 sub do_renew {
3195     my $self = shift;
3196     $self->log_me("do_renew()");
3197
3198     # Make sure there is an open circ to renew that is not
3199     # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3200     my $usrid = $self->patron->id if $self->patron;
3201     my $circ = $self->editor->search_action_circulation({
3202         target_copy => $self->copy->id,
3203         xact_finish => undef,
3204         ($usrid ? (usr => $usrid) : ()),
3205         '-or' => [
3206             {stop_fines => undef},
3207             {stop_fines => OILS_STOP_FINES_MAX_FINES}
3208         ]
3209     })->[0];
3210
3211     return $self->bail_on_events($self->editor->event) unless $circ;
3212
3213     # A user is not allowed to renew another user's items without permission
3214     unless( $circ->usr eq $self->editor->requestor->id ) {
3215         return $self->bail_on_events($self->editor->events)
3216             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3217     }   
3218
3219     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3220         if $circ->renewal_remaining < 1;
3221
3222     # -----------------------------------------------------------------
3223
3224     $self->parent_circ($circ->id);
3225     $self->renewal_remaining( $circ->renewal_remaining - 1 );
3226     $self->circ($circ);
3227
3228     # Run the fine generator against the old circ
3229     $self->generate_fines_start;
3230
3231     $self->run_renew_permit;
3232
3233     # Check the item in
3234     $self->do_checkin();
3235     return if $self->bail_out;
3236
3237     unless( $self->permit_override ) {
3238         $self->do_permit();
3239         return if $self->bail_out;
3240         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3241         $self->remove_event('ITEM_NOT_CATALOGED');
3242     }   
3243
3244     $self->override_events;
3245     return if $self->bail_out;
3246
3247     $self->events([]);
3248     $self->do_checkout();
3249 }
3250
3251
3252 sub remove_event {
3253     my( $self, $evt ) = @_;
3254     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3255     $logger->debug("circulator: removing event from list: $evt");
3256     my @events = @{$self->events};
3257     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3258 }
3259
3260
3261 sub have_event {
3262     my( $self, $evt ) = @_;
3263     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3264     return grep { $_->{textcode} eq $evt } @{$self->events};
3265 }
3266
3267
3268
3269 sub run_renew_permit {
3270     my $self = shift;
3271
3272     if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3273         my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3274             $self->editor, $self->copy, $self->editor->requestor, 1
3275         );
3276         $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3277     }
3278
3279     if(!$self->legacy_script_support) {
3280         my $results = $self->run_indb_circ_test;
3281         $self->push_events($self->matrix_test_result_events)
3282             unless $self->circ_test_success;
3283     } else {
3284
3285         my $runner = $self->script_runner;
3286
3287         $runner->load($self->circ_permit_renew);
3288         my $result = $runner->run or 
3289             throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3290         if ($result->{"events"}) {
3291             $self->push_events(
3292                 map { new OpenILS::Event($_) } @{$result->{"events"}}
3293             );
3294             $logger->activity(
3295                 "circulator: circ_permit_renew for user " .
3296                 $self->patron->id . " returned " .
3297                 scalar(@{$result->{"events"}}) . " event(s)"
3298             );
3299         }
3300
3301         $self->mk_script_runner;
3302     }
3303
3304     $logger->debug("circulator: re-creating script runner to be safe");
3305 }
3306
3307
3308 # XXX: The primary mechanism for storing circ history is now handled
3309 # by tracking real circulation objects instead of bibs in a bucket.
3310 # However, this code is disabled by default and could be useful 
3311 # some day, so may as well leave it for now.
3312 sub append_reading_list {
3313     my $self = shift;
3314
3315     return undef unless 
3316         $self->is_checkout and 
3317         $self->patron and 
3318         $self->copy and 
3319         !$self->is_noncat;
3320
3321
3322     # verify history is globally enabled and uses the bucket mechanism
3323     my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3324         apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3325
3326     return undef unless $htype and $htype eq 'bucket';
3327
3328     my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3329
3330     # verify the patron wants to retain the hisory
3331         my $setting = $e->search_actor_user_setting(
3332                 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3333     
3334     unless($setting and $setting->value) {
3335         $e->rollback;
3336         return undef;
3337     }
3338
3339     my $bkt = $e->search_container_copy_bucket(
3340         {owner => $self->patron->id, btype => 'circ_history'})->[0];
3341
3342     my $pos = 1;
3343
3344     if($bkt) {
3345         # find the next item position
3346         my $last_item = $e->search_container_copy_bucket_item(
3347             {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3348         $pos = $last_item->pos + 1 if $last_item;
3349
3350     } else {
3351         # create the history bucket if necessary
3352         $bkt = Fieldmapper::container::copy_bucket->new;
3353         $bkt->owner($self->patron->id);
3354         $bkt->name('');
3355         $bkt->btype('circ_history');
3356         $bkt->pub('f');
3357         $e->create_container_copy_bucket($bkt) or return $e->die_event;
3358     }
3359
3360     my $item = Fieldmapper::container::copy_bucket_item->new;
3361
3362     $item->bucket($bkt->id);
3363     $item->target_copy($self->copy->id);
3364     $item->pos($pos);
3365
3366     $e->create_container_copy_bucket_item($item) or return $e->die_event;
3367     $e->commit;
3368
3369     return undef;
3370 }
3371
3372
3373 sub make_trigger_events {
3374     my $self = shift;
3375     return unless $self->circ;
3376     $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3377     $U->create_events_for_hook('checkin',  $self->circ, $self->circ_lib) if $self->is_checkin;
3378     $U->create_events_for_hook('renewal',  $self->circ, $self->circ_lib) if $self->is_renewal;
3379 }
3380
3381
3382
3383 sub checkin_handle_lost_now_found {
3384     my ($self, $bill_type) = @_;
3385
3386     # ------------------------------------------------------------------
3387     # remove charge from patron's account if lost item is returned
3388     # ------------------------------------------------------------------
3389
3390     my $bills = $self->editor->search_money_billing(
3391         {
3392             xact => $self->circ->id,
3393             btype => $bill_type
3394         }
3395     );
3396
3397     $logger->debug("voiding lost item charge of  ".scalar(@$bills));
3398     for my $bill (@$bills) {
3399         if( !$U->is_true($bill->voided) ) {
3400             $logger->info("lost item returned - voiding bill ".$bill->id);
3401             $bill->voided('t');
3402             $bill->void_time('now');
3403             $bill->voider($self->editor->requestor->id);
3404             my $note = ($bill->note) ? $bill->note . "\n" : '';
3405             $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3406
3407             $self->bail_on_events($self->editor->event)
3408                 unless $self->editor->update_money_billing($bill);
3409         }
3410     }
3411 }
3412
3413 sub checkin_handle_lost_now_found_restore_od {
3414     my $self = shift;
3415
3416     # ------------------------------------------------------------------
3417     # restore those overdue charges voided when item was set to lost
3418     # ------------------------------------------------------------------
3419
3420     my $ods = $self->editor->search_money_billing(
3421         {
3422                 xact => $self->circ->id,
3423                 btype => 1
3424         }
3425     );
3426
3427     $logger->debug("returning overdue charges pre-lost  ".scalar(@$ods));
3428     for my $bill (@$ods) {
3429         if( $U->is_true($bill->voided) ) {
3430                 $logger->info("lost item returned - restoring overdue ".$bill->id);
3431                 $bill->voided('f');
3432                 $bill->clear_void_time;
3433                 $bill->voider($self->editor->requestor->id);
3434                 my $note = ($bill->note) ? $bill->note . "\n" : '';
3435                 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3436
3437                 $self->bail_on_events($self->editor->event)
3438                         unless $self->editor->update_money_billing($bill);
3439         }
3440     }
3441 }
3442
3443 1;