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