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