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