]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Merge branch 'opac-tt-poc' of ssh://senator@yeti.esilibrary.com/home/evergreen/evergr...
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Circ / Circulate.pm
1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenILS::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::AppSession;
6 use OpenSRF::Utils::SettingsClient;
7 use OpenSRF::Utils::Logger qw(:logger);
8 use OpenILS::Const qw/:const/;
9 use OpenILS::Application::AppUtils;
10 use DateTime;
11 my $U = "OpenILS::Application::AppUtils";
12
13 my %scripts;
14 my $script_libs;
15 my $legacy_script_support = 0;
16 my $booking_status;
17
18 sub determine_booking_status {
19     unless (defined $booking_status) {
20         my $ses = create OpenSRF::AppSession("router");
21         $booking_status = grep {$_ eq "open-ils.booking"} @{
22             $ses->request("opensrf.router.info.class.list")->gather(1)
23         };
24         $ses->disconnect;
25         $logger->info("booking status: " . ($booking_status ? "on" : "off"));
26     }
27
28     return $booking_status;
29 }
30
31
32 my $MK_ENV_FLESH = { 
33     flesh => 2, 
34     flesh_fields => {acp => ['call_number'], acn => ['record']} 
35 };
36
37 sub initialize {
38
39     my $self = shift;
40     my $conf = OpenSRF::Utils::SettingsClient->new;
41     my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
42
43     $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
44     $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
45
46     my $lb  = $conf->config_value(  @pfx2, 'script_path' );
47     $lb = [ $lb ] unless ref($lb);
48     $script_libs = $lb;
49
50     return unless $legacy_script_support;
51
52     my @pfx = ( @pfx2, "scripts" );
53     my $p   = $conf->config_value(  @pfx, 'circ_permit_patron' );
54     my $c   = $conf->config_value(  @pfx, 'circ_permit_copy' );
55     my $d   = $conf->config_value(  @pfx, 'circ_duration' );
56     my $f   = $conf->config_value(  @pfx, 'circ_recurring_fines' );
57     my $m   = $conf->config_value(  @pfx, 'circ_max_fines' );
58     my $pr  = $conf->config_value(  @pfx, 'circ_permit_renew' );
59
60     $logger->error( "Missing circ script(s)" ) 
61         unless( $p and $c and $d and $f and $m and $pr );
62
63     $scripts{circ_permit_patron}   = $p;
64     $scripts{circ_permit_copy}     = $c;
65     $scripts{circ_duration}        = $d;
66     $scripts{circ_recurring_fines} = $f;
67     $scripts{circ_max_fines}       = $m;
68     $scripts{circ_permit_renew}    = $pr;
69
70     $logger->debug(
71         "circulator: Loaded rules scripts for circ: " .
72         "circ permit patron = $p, ".
73         "circ permit copy = $c, ".
74         "circ duration = $d, ".
75         "circ recurring fines = $f, " .
76         "circ max fines = $m, ".
77         "circ renew permit = $pr.  ".
78         "lib paths = @$lb. ".
79         "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
80         );
81 }
82
83 __PACKAGE__->register_method(
84     method  => "run_method",
85     api_name    => "open-ils.circ.checkout.permit",
86     notes       => q/
87         Determines if the given checkout can occur
88         @param authtoken The login session key
89         @param params A trailing hash of named params including 
90             barcode : The copy barcode, 
91             patron : The patron the checkout is occurring for, 
92             renew : true or false - whether or not this is a renewal
93         @return The event that occurred during the permit check.  
94     /);
95
96
97 __PACKAGE__->register_method (
98     method      => 'run_method',
99     api_name        => 'open-ils.circ.checkout.permit.override',
100     signature   => q/@see open-ils.circ.checkout.permit/,
101 );
102
103
104 __PACKAGE__->register_method(
105     method  => "run_method",
106     api_name    => "open-ils.circ.checkout",
107     notes => q/
108         Checks out an item
109         @param authtoken The login session key
110         @param params A named hash of params including:
111             copy            The copy object
112             barcode     If no copy is provided, the copy is retrieved via barcode
113             copyid      If no copy or barcode is provide, the copy id will be use
114             patron      The patron's id
115             noncat      True if this is a circulation for a non-cataloted item
116             noncat_type The non-cataloged type id
117             noncat_circ_lib The location for the noncat circ.  
118             precat      The item has yet to be cataloged
119             dummy_title The temporary title of the pre-cataloded item
120             dummy_author The temporary authr of the pre-cataloded item
121                 Default is the home org of the staff member
122         @return The SUCCESS event on success, any other event depending on the error
123     /);
124
125 __PACKAGE__->register_method(
126     method  => "run_method",
127     api_name    => "open-ils.circ.checkin",
128     argc        => 2,
129     signature   => q/
130         Generic super-method for handling all copies
131         @param authtoken The login session key
132         @param params Hash of named parameters including:
133             barcode - The copy barcode
134             force   - If true, copies in bad statuses will be checked in and give good statuses
135             noop    - don't capture holds or put items into transit
136             void_overdues - void all overdues for the circulation (aka amnesty)
137             ...
138     /
139 );
140
141 __PACKAGE__->register_method(
142     method    => "run_method",
143     api_name  => "open-ils.circ.checkin.override",
144     signature => q/@see open-ils.circ.checkin/
145 );
146
147 __PACKAGE__->register_method(
148     method    => "run_method",
149     api_name  => "open-ils.circ.renew.override",
150     signature => q/@see open-ils.circ.renew/,
151 );
152
153
154 __PACKAGE__->register_method(
155     method  => "run_method",
156     api_name    => "open-ils.circ.renew",
157     notes       => <<"    NOTES");
158     PARAMS( authtoken, circ => circ_id );
159     open-ils.circ.renew(login_session, circ_object);
160     Renews the provided circulation.  login_session is the requestor of the
161     renewal and if the logged in user is not the same as circ->usr, then
162     the logged in user must have RENEW_CIRC permissions.
163     NOTES
164
165 __PACKAGE__->register_method(
166     method   => "run_method",
167     api_name => "open-ils.circ.checkout.full"
168 );
169 __PACKAGE__->register_method(
170     method   => "run_method",
171     api_name => "open-ils.circ.checkout.full.override"
172 );
173 __PACKAGE__->register_method(
174     method   => "run_method",
175     api_name => "open-ils.circ.reservation.pickup"
176 );
177 __PACKAGE__->register_method(
178     method   => "run_method",
179     api_name => "open-ils.circ.reservation.return"
180 );
181 __PACKAGE__->register_method(
182     method   => "run_method",
183     api_name => "open-ils.circ.checkout.inspect",
184     desc     => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
185 );
186
187
188 sub run_method {
189     my( $self, $conn, $auth, $args ) = @_;
190     translate_legacy_args($args);
191     my $api = $self->api_name;
192
193     my $circulator = 
194         OpenILS::Application::Circ::Circulator->new($auth, %$args);
195
196     return circ_events($circulator) if $circulator->bail_out;
197
198     $circulator->use_booking(determine_booking_status());
199
200     # --------------------------------------------------------------------------
201     # First, check for a booking transit, as the barcode may not be a copy
202     # barcode, but a resource barcode, and nothing else in here will work
203     # --------------------------------------------------------------------------
204
205     if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
206         my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
207         if (@$resources) { # yes!
208
209             my $res_id_list = [ map { $_->id } @$resources ];
210             my $transit = $circulator->editor->search_action_reservation_transit_copy(
211                 [
212                     { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
213                     { order_by => { artc => 'source_send_time' }, limit => 1 }
214                 ]
215             )->[0]; # Any transit for this barcode?
216
217             if ($transit) { # yes! unwrap it.
218
219                 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
220                 my $res_type    = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
221
222                 my $success_event = new OpenILS::Event(
223                     "SUCCESS", "payload" => {"reservation" => $reservation}
224                 );
225                 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
226                     if (my $copy = $circulator->editor->search_asset_copy([
227                         { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
228                     ])->[0]) { # got a copy
229                         $copy->status( $transit->copy_status );
230                         $copy->editor($circulator->editor->requestor->id);
231                         $copy->edit_date('now');
232                         $circulator->editor->update_asset_copy($copy);
233                         $success_event->{"payload"}->{"record"} =
234                             $U->record_to_mvr($copy->call_number->record);
235                         $copy->call_number($copy->call_number->id);
236                         $success_event->{"payload"}->{"copy"} = $copy;
237                     }
238                 }
239
240                 $transit->dest_recv_time('now');
241                 $circulator->editor->update_action_reservation_transit_copy( $transit );
242
243                 $circulator->editor->commit;
244                 # Formerly this branch just stopped here. Argh!
245                 $conn->respond_complete($success_event);
246                 return;
247             }
248         }
249     }
250             
251     
252
253     # --------------------------------------------------------------------------
254     # Go ahead and load the script runner to make sure we have all 
255     # of the objects we need
256     # --------------------------------------------------------------------------
257
258     if ($circulator->use_booking) {
259         $circulator->is_res_checkin($circulator->is_checkin(1))
260             if $api =~ /reservation.return/ or (
261                 $api =~ /checkin/ and $circulator->seems_like_reservation()
262             );
263
264         $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
265     }
266
267     $circulator->is_renewal(1) if $api =~ /renew/;
268     $circulator->is_checkin(1) if $api =~ /checkin/;
269
270     $circulator->mk_env();
271     $circulator->noop(1) if $circulator->claims_never_checked_out;
272
273     if($legacy_script_support and not $circulator->is_checkin) {
274         $circulator->mk_script_runner();
275         $circulator->legacy_script_support(1);
276         $circulator->circ_permit_patron($scripts{circ_permit_patron});
277         $circulator->circ_permit_copy($scripts{circ_permit_copy});      
278         $circulator->circ_duration($scripts{circ_duration});             
279         $circulator->circ_permit_renew($scripts{circ_permit_renew});
280     }
281     return circ_events($circulator) if $circulator->bail_out;
282
283     
284     $circulator->override(1) if $api =~ /override/o;
285
286     if( $api =~ /checkout\.permit/ ) {
287         $circulator->do_permit();
288
289     } elsif( $api =~ /checkout.full/ ) {
290
291         # requesting a precat checkout implies that any required
292         # overrides have been performed.  Go ahead and re-override.
293         $circulator->skip_permit_key(1);
294         $circulator->override(1) if $circulator->request_precat;
295         $circulator->do_permit();
296         $circulator->is_checkout(1);
297         unless( $circulator->bail_out ) {
298             $circulator->events([]);
299             $circulator->do_checkout();
300         }
301
302     } elsif( $circulator->is_res_checkout ) {
303         $circulator->do_reservation_pickup();
304
305     } elsif( $api =~ /inspect/ ) {
306         my $data = $circulator->do_inspect();
307         $circulator->editor->rollback;
308         return $data;
309
310     } elsif( $api =~ /checkout/ ) {
311         $circulator->is_checkout(1);
312         $circulator->do_checkout();
313
314     } elsif( $circulator->is_res_checkin ) {
315         $circulator->do_reservation_return();
316         $circulator->do_checkin() if ($circulator->copy());
317     } elsif( $api =~ /checkin/ ) {
318         $circulator->do_checkin();
319
320     } elsif( $api =~ /renew/ ) {
321         $circulator->is_renewal(1);
322         $circulator->do_renew();
323     }
324
325     if( $circulator->bail_out ) {
326
327         my @ee;
328         # make sure no success event accidentally slip in
329         $circulator->events(
330             [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
331
332         # Log the events
333         my @e = @{$circulator->events};
334         push( @ee, $_->{textcode} ) for @e;
335         $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
336
337         $circulator->editor->rollback;
338
339     } else {
340         $circulator->editor->commit;
341     }
342
343     $circulator->script_runner->cleanup if $circulator->script_runner;
344     
345     $conn->respond_complete(circ_events($circulator));
346
347     unless($circulator->bail_out) {
348         $circulator->do_hold_notify($circulator->notify_hold)
349             if $circulator->notify_hold;
350         $circulator->retarget_holds if $circulator->retarget;
351         $circulator->append_reading_list;
352         $circulator->make_trigger_events;
353     }
354 }
355
356 sub circ_events {
357     my $circ = shift;
358     my @e = @{$circ->events};
359     # if we have multiple events, SUCCESS should not be one of them;
360     @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
361     return (@e == 1) ? $e[0] : \@e;
362 }
363
364
365 sub translate_legacy_args {
366     my $args = shift;
367
368     if( $$args{barcode} ) {
369         $$args{copy_barcode} = $$args{barcode};
370         delete $$args{barcode};
371     }
372
373     if( $$args{copyid} ) {
374         $$args{copy_id} = $$args{copyid};
375         delete $$args{copyid};
376     }
377
378     if( $$args{patronid} ) {
379         $$args{patron_id} = $$args{patronid};
380         delete $$args{patronid};
381     }
382
383     if( $$args{patron} and !ref($$args{patron}) ) {
384         $$args{patron_id} = $$args{patron};
385         delete $$args{patron};
386     }
387
388
389     if( $$args{noncat} ) {
390         $$args{is_noncat} = $$args{noncat};
391         delete $$args{noncat};
392     }
393
394     if( $$args{precat} ) {
395         $$args{is_precat} = $$args{request_precat} = $$args{precat};
396         delete $$args{precat};
397     }
398 }
399
400
401
402 # --------------------------------------------------------------------------
403 # This package actually manages all of the circulation logic
404 # --------------------------------------------------------------------------
405 package OpenILS::Application::Circ::Circulator;
406 use strict; use warnings;
407 use vars q/$AUTOLOAD/;
408 use DateTime;
409 use OpenILS::Utils::Fieldmapper;
410 use OpenSRF::Utils::Cache;
411 use Digest::MD5 qw(md5_hex);
412 use DateTime::Format::ISO8601;
413 use OpenILS::Utils::PermitHold;
414 use OpenSRF::Utils qw/:datetime/;
415 use OpenSRF::Utils::SettingsClient;
416 use OpenILS::Application::Circ::Holds;
417 use OpenILS::Application::Circ::Transit;
418 use OpenSRF::Utils::Logger qw(:logger);
419 use OpenILS::Utils::CStoreEditor qw/:funcs/;
420 use OpenILS::Application::Circ::ScriptBuilder;
421 use OpenILS::Const qw/:const/;
422 use OpenILS::Utils::Penalty;
423 use OpenILS::Application::Circ::CircCommon;
424 use Time::Local;
425
426 my $holdcode    = "OpenILS::Application::Circ::Holds";
427 my $transcode   = "OpenILS::Application::Circ::Transit";
428 my %user_groups;
429
430 sub DESTROY { }
431
432
433 # --------------------------------------------------------------------------
434 # Add a pile of automagic getter/setter methods
435 # --------------------------------------------------------------------------
436 my @AUTOLOAD_FIELDS = qw/
437     notify_hold
438     remote_hold
439     backdate
440     reservation
441     copy
442     copy_id
443     copy_barcode
444     patron
445     patron_id
446     patron_barcode
447     script_runner
448     volume
449     title
450     is_renewal
451     is_checkout
452     is_res_checkout
453     is_precat
454     is_noncat
455     request_precat
456     is_checkin
457     is_res_checkin
458     noncat_type
459     editor
460     events
461     cache_handle
462     override
463     circ_permit_patron
464     circ_permit_copy
465     circ_duration
466     circ_recurring_fines
467     circ_max_fines
468     circ_permit_renew
469     circ
470     transit
471     hold
472     permit_key
473     noncat_circ_lib
474     noncat_count
475     checkout_time
476     dummy_title
477     dummy_author
478     dummy_isbn
479     circ_modifier
480     circ_lib
481     barcode
482     duration_level
483     recurring_fines_level
484     duration_rule
485     recurring_fines_rule
486     max_fine_rule
487     renewal_remaining
488     hard_due_date
489     due_date
490     fulfilled_holds
491     transit
492     checkin_changed
493     force
494     permit_override
495     pending_checkouts
496     cancelled_hold_transit
497     opac_renewal
498     phone_renewal
499     desk_renewal
500     sip_renewal
501     retarget
502     matrix_test_result
503     circ_matrix_matchpoint
504     circ_test_success
505     legacy_script_support
506     is_deposit
507     is_rental
508     deposit_billing
509     rental_billing
510     capture
511     noop
512     void_overdues
513     parent_circ
514     return_patron
515     claims_never_checked_out
516     skip_permit_key
517     skip_deposit_fee
518     skip_rental_fee
519     use_booking
520 /;
521
522
523 sub AUTOLOAD {
524     my $self = shift;
525     my $type = ref($self) or die "$self is not an object";
526     my $data = shift;
527     my $name = $AUTOLOAD;
528     $name =~ s/.*://o;   
529
530     unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
531         $logger->error("circulator: $type: invalid autoload field: $name");
532         die "$type: invalid autoload field: $name\n" 
533     }
534
535     {
536         no strict 'refs';
537         *{"${type}::${name}"} = sub {
538             my $s = shift;
539             my $v = shift;
540             $s->{$name} = $v if defined $v;
541             return $s->{$name};
542         }
543     }
544     return $self->$name($data);
545 }
546
547
548 sub new {
549     my( $class, $auth, %args ) = @_;
550     $class = ref($class) || $class;
551     my $self = bless( {}, $class );
552
553     $self->events([]);
554     $self->editor(new_editor(xact => 1, authtoken => $auth));
555
556     unless( $self->editor->checkauth ) {
557         $self->bail_on_events($self->editor->event);
558         return $self;
559     }
560
561     $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
562
563     $self->$_($args{$_}) for keys %args;
564
565     $self->circ_lib(
566         ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
567
568     # if this is a renewal, default to desk_renewal
569     $self->desk_renewal(1) unless 
570         $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
571
572     $self->capture('') unless $self->capture;
573
574     unless(%user_groups) {
575         my $gps = $self->editor->retrieve_all_permission_grp_tree;
576         %user_groups = map { $_->id => $_ } @$gps;
577     }
578
579     return $self;
580 }
581
582
583 # --------------------------------------------------------------------------
584 # True if we should discontinue processing
585 # --------------------------------------------------------------------------
586 sub bail_out {
587     my( $self, $bool ) = @_;
588     if( defined $bool ) {
589         $logger->info("circulator: BAILING OUT") if $bool;
590         $self->{bail_out} = $bool;
591     }
592     return $self->{bail_out};
593 }
594
595
596 sub push_events {
597     my( $self, @evts ) = @_;
598     for my $e (@evts) {
599         next unless $e;
600         $e->{payload} = $self->copy if 
601               ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
602
603         $logger->info("circulator: pushing event ".$e->{textcode});
604         push( @{$self->events}, $e ) unless
605             grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
606     }
607 }
608
609 sub mk_permit_key {
610     my $self = shift;
611     return '' if $self->skip_permit_key;
612     my $key = md5_hex( time() . rand() . "$$" );
613     $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
614     return $self->permit_key($key);
615 }
616
617 sub check_permit_key {
618     my $self = shift;
619     return 1 if $self->skip_permit_key;
620     my $key = $self->permit_key;
621     return 0 unless $key;
622     my $k = "oils_permit_key_$key";
623     my $one = $self->cache_handle->get_cache($k);
624     $self->cache_handle->delete_cache($k);
625     return ($one) ? 1 : 0;
626 }
627
628 sub seems_like_reservation {
629     my $self = shift;
630
631     # Some words about the following method:
632     # 1) It requires the VIEW_USER permission, but that's not an
633     # issue, right, since all staff should have that?
634     # 2) It returns only one reservation at a time, even if an item can be
635     # and is currently overbooked.  Hmmm....
636     my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
637     my $result = $booking_ses->request(
638         "open-ils.booking.reservations.by_returnable_resource_barcode",
639         $self->editor->authtoken,
640         $self->copy_barcode
641     )->gather(1);
642     $booking_ses->disconnect;
643
644     return $self->bail_on_events($result) if defined $U->event_code($result);
645
646     if (@$result > 0) {
647         $self->reservation(shift @$result);
648         return 1;
649     } else {
650         return 0;
651     }
652
653 }
654
655 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
656 sub save_trimmed_copy {
657     my ($self, $copy) = @_;
658
659     $self->copy($copy);
660     $self->volume($copy->call_number);
661     $self->title($self->volume->record);
662     $self->copy->call_number($self->volume->id);
663     $self->volume->record($self->title->id);
664     $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
665     if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
666         $self->is_deposit(1) if $U->is_true($self->copy->deposit);
667         $self->is_rental(1) unless $U->is_true($self->copy->deposit);
668     }
669 }
670
671 sub mk_env {
672     my $self = shift;
673     my $e = $self->editor;
674
675     # --------------------------------------------------------------------------
676     # Grab the fleshed copy
677     # --------------------------------------------------------------------------
678     unless($self->is_noncat) {
679         my $copy;
680             if($self->copy_id) {
681                     $copy = $e->retrieve_asset_copy(
682                             [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
683     
684             } elsif( $self->copy_barcode ) {
685     
686                     $copy = $e->search_asset_copy(
687                             [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
688             } elsif( $self->reservation ) {
689             my $res = $e->json_query(
690                 {
691                     "select" => {"acp" => ["id"]},
692                     "from" => {
693                         "acp" => {
694                             "brsrc" => {
695                                 "fkey" => "barcode",
696                                 "field" => "barcode",
697                                 "join" => {
698                                     "bresv" => {
699                                         "fkey" => "id",
700                                         "field" => "current_resource"
701                                     }
702                                 }
703                             }
704                         }
705                     },
706                     "where" => {
707                         "+bresv" => {
708                             "id" => (ref $self->reservation) ?
709                                 $self->reservation->id : $self->reservation
710                         }
711                     }
712                 }
713             );
714             if (ref $res eq "ARRAY" and scalar @$res) {
715                 $logger->info("circulator: mapped reservation " .
716                     $self->reservation . " to copy " . $res->[0]->{"id"});
717                 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
718             }
719         }
720     
721         if($copy) {
722             $self->save_trimmed_copy($copy);
723         } else {
724             # We can't renew if there is no copy
725             return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
726                 if $self->is_renewal;
727             $self->is_precat(1);
728         }
729     }
730
731     # --------------------------------------------------------------------------
732     # Grab the patron
733     # --------------------------------------------------------------------------
734     my $patron;
735         my $flesh = {
736                 flesh => 1,
737                 flesh_fields => {au => [ qw/ card / ]}
738         };
739
740         if( $self->patron_id ) {
741                 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
742                         or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
743
744         } elsif( $self->patron_barcode ) {
745
746                 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
747                 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0] 
748                         or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
749
750                 $patron = $e->search_actor_user([{card => $card->id}, $flesh])->[0]
751                         or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
752
753     } else {
754         if( my $copy = $self->copy ) {
755
756             $flesh->{flesh} = 2;
757             $flesh->{flesh_fields}->{circ} = ['usr'];
758
759             my $circ = $e->search_action_circulation([
760                 {target_copy => $copy->id, checkin_time => undef}, $flesh
761             ])->[0];
762
763             if($circ) {
764                 $patron = $circ->usr;
765                 $circ->usr($patron->id); # de-flesh for consistency
766                 $self->circ($circ); 
767             }
768         }
769     }
770
771     return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
772         unless $self->patron($patron) or $self->is_checkin;
773
774     unless($self->is_checkin) {
775
776         # Check for inactivity and patron reg. expiration
777
778         $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
779                         unless $U->is_true($patron->active);
780         
781                 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
782                         unless $U->is_true($patron->card->active);
783         
784                 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
785                         cleanse_ISO8601($patron->expire_date));
786         
787                 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
788                         if( CORE::time > $expire->epoch ) ;
789     }
790 }
791
792 # --------------------------------------------------------------------------
793 # This builds the script runner environment and fetches most of the
794 # objects we need
795 # --------------------------------------------------------------------------
796 sub mk_script_runner {
797     my $self = shift;
798     my $args = {};
799
800
801     my @fields = 
802         qw/copy copy_barcode copy_id patron 
803             patron_id patron_barcode volume title editor/;
804
805     # Translate our objects into the ScriptBuilder args hash
806     $$args{$_} = $self->$_() for @fields;
807
808     $args->{ignore_user_status} = 1 if $self->is_checkin;
809     $$args{fetch_patron_by_circ_copy} = 1;
810     $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
811
812     if( my $pco = $self->pending_checkouts ) {
813         $logger->info("circulator: we were given a pending checkouts number of $pco");
814         $$args{patronItemsOut} = $pco;
815     }
816
817     # This fetches most of the objects we need
818     $self->script_runner(
819         OpenILS::Application::Circ::ScriptBuilder->build($args));
820
821     # Now we translate the ScriptBuilder objects back into self
822     $self->$_($$args{$_}) for @fields;
823
824     my @evts = @{$args->{_events}} if $args->{_events};
825
826     $logger->debug("circulator: script builder returned events: @evts") if @evts;
827
828
829     if(@evts) {
830         # Anything besides ASSET_COPY_NOT_FOUND will stop processing
831         if(!$self->is_noncat and 
832             @evts == 1 and 
833             $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
834                 $self->is_precat(1);
835
836         } else {
837             my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
838             return $self->bail_on_events(@e);
839         }
840     }
841
842     if($self->copy) {
843         $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
844         if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
845             $self->is_deposit(1) if $U->is_true($self->copy->deposit);
846             $self->is_rental(1) unless $U->is_true($self->copy->deposit);
847         }
848     }
849
850     # We can't renew if there is no copy
851     return $self->bail_on_events(@evts) if 
852         $self->is_renewal and !$self->copy;
853
854     # Set some circ-specific flags in the script environment
855     my $evt = "environment";
856     $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
857
858     if( $self->is_noncat ) {
859       $self->script_runner->insert("$evt.isNonCat", 1);
860       $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
861     }
862
863     if( $self->is_precat ) {
864         $self->script_runner->insert("environment.isPrecat", 1, 1);
865     }
866
867     $self->script_runner->add_path( $_ ) for @$script_libs;
868
869     return 1;
870 }
871
872 # --------------------------------------------------------------------------
873 # Does the circ permit work
874 # --------------------------------------------------------------------------
875 sub do_permit {
876     my $self = shift;
877
878     $self->log_me("do_permit()");
879
880     unless( $self->editor->requestor->id == $self->patron->id ) {
881         return $self->bail_on_events($self->editor->event)
882             unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
883     }
884
885     $self->check_captured_holds();
886     $self->do_copy_checks();
887     return if $self->bail_out;
888     $self->run_patron_permit_scripts();
889     $self->run_copy_permit_scripts() 
890         unless $self->is_precat or $self->is_noncat;
891     $self->check_item_deposit_events();
892     $self->override_events();
893     return if $self->bail_out;
894
895     if($self->is_precat and not $self->request_precat) {
896         $self->push_events(
897             OpenILS::Event->new(
898                 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
899         return $self->bail_out(1) unless $self->is_renewal;
900     }
901
902     $self->push_events(
903         OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
904 }
905
906 sub check_item_deposit_events {
907     my $self = shift;
908     $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy)) 
909         if $self->is_deposit and not $self->is_deposit_exempt;
910     $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy)) 
911         if $self->is_rental and not $self->is_rental_exempt;
912 }
913
914 # returns true if the user is not required to pay deposits
915 sub is_deposit_exempt {
916     my $self = shift;
917     my $pid = (ref $self->patron->profile) ?
918         $self->patron->profile->id : $self->patron->profile;
919     my $groups = $U->ou_ancestor_setting_value(
920         $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
921     for my $grp (@$groups) {
922         return 1 if $self->is_group_descendant($grp, $pid);
923     }
924     return 0;
925 }
926
927 # returns true if the user is not required to pay rental fees
928 sub is_rental_exempt {
929     my $self = shift;
930     my $pid = (ref $self->patron->profile) ?
931         $self->patron->profile->id : $self->patron->profile;
932     my $groups = $U->ou_ancestor_setting_value(
933         $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
934     for my $grp (@$groups) {
935         return 1 if $self->is_group_descendant($grp, $pid);
936     }
937     return 0;
938 }
939
940 sub is_group_descendant {
941     my($self, $p_id, $c_id) = @_;
942     return 0 unless defined $p_id and defined $c_id;
943     return 1 if $c_id == $p_id;
944     while(my $grp = $user_groups{$c_id}) {
945         $c_id = $grp->parent;
946         return 0 unless defined $c_id;
947         return 1 if $c_id == $p_id;
948     }
949     return 0;
950 }
951
952 sub check_captured_holds {
953    my $self    = shift;
954    my $copy    = $self->copy;
955    my $patron  = $self->patron;
956
957     return undef unless $copy;
958
959     my $s = $U->copy_status($copy->status)->id;
960     return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
961     $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
962
963     # Item is on the holds shelf, make sure it's going to the right person
964     my $holds   = $self->editor->search_action_hold_request(
965         [
966             { 
967                 current_copy        => $copy->id , 
968                 capture_time        => { '!=' => undef },
969                 cancel_time         => undef, 
970                 fulfillment_time    => undef 
971             },
972             { limit => 1 }
973         ]
974     );
975
976     if( $holds and $$holds[0] ) {
977         return undef if $$holds[0]->usr == $patron->id;
978     }
979
980     $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
981
982     $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
983 }
984
985
986 sub do_copy_checks {
987     my $self = shift;
988     my $copy = $self->copy;
989     return unless $copy;
990
991     my $stat = $U->copy_status($copy->status)->id;
992
993     # We cannot check out a copy if it is in-transit
994     if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
995         return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
996     }
997
998     $self->handle_claims_returned();
999     return if $self->bail_out;
1000
1001     # no claims returned circ was found, check if there is any open circ
1002     unless( $self->is_renewal ) {
1003
1004         my $circs = $self->editor->search_action_circulation(
1005             { target_copy => $copy->id, checkin_time => undef }
1006         );
1007
1008         if(my $old_circ = $circs->[0]) { # an open circ was found
1009
1010             my $payload = {copy => $copy};
1011
1012             if($old_circ->usr == $self->patron->id) {
1013                 
1014                 $payload->{old_circ} = $old_circ;
1015
1016                 # If there is an open circulation on the checkout item and an auto-renew 
1017                 # interval is defined, inform the caller that they should go 
1018                 # ahead and renew the item instead of warning about open circulations.
1019     
1020                 my $auto_renew_intvl = $U->ou_ancestor_setting_value(        
1021                     $self->circ_lib,
1022                     'circ.checkout_auto_renew_age', 
1023                     $self->editor
1024                 );
1025
1026                 if($auto_renew_intvl) {
1027                     my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1028                     my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1029
1030                     if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1031                         $payload->{auto_renew} = 1;
1032                     }
1033                 }
1034             }
1035
1036             return $self->bail_on_events(
1037                 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1038             );
1039         }
1040     }
1041 }
1042
1043 my $LEGACY_CIRC_EVENT_MAP = {
1044     'no_item' => 'ITEM_NOT_CATALOGED',
1045     'actor.usr.barred' => 'PATRON_BARRED',
1046     'asset.copy.circulate' =>  'COPY_CIRC_NOT_ALLOWED',
1047     'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1048     'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1049     'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1050     'config.circ_matrix_test.max_items_out' =>  'PATRON_EXCEEDS_CHECKOUT_COUNT',
1051     'config.circ_matrix_test.max_overdue' =>  'PATRON_EXCEEDS_OVERDUE_COUNT',
1052     'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1053     'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1054 };
1055
1056
1057 # ---------------------------------------------------------------------
1058 # This pushes any patron-related events into the list but does not
1059 # set bail_out for any events
1060 # ---------------------------------------------------------------------
1061 sub run_patron_permit_scripts {
1062     my $self        = shift;
1063     my $runner      = $self->script_runner;
1064     my $patronid    = $self->patron->id;
1065
1066     my @allevents; 
1067
1068     if(!$self->legacy_script_support) {
1069
1070         my $results = $self->run_indb_circ_test;
1071         unless($self->circ_test_success) {
1072             # no_item result is OK during noncat checkout
1073             unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
1074                 push @allevents, $self->matrix_test_result_events;
1075             }
1076         }
1077
1078     } else {
1079
1080         # --------------------------------------------------------------------- 
1081         # # Now run the patron permit script 
1082         # ---------------------------------------------------------------------
1083         $runner->load($self->circ_permit_patron);
1084         my $result = $runner->run or 
1085             throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1086
1087         my $patron_events = $result->{events};
1088
1089         OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1090         my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1091         my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1092         $penalties = $penalties->{fatal_penalties};
1093
1094         for my $pen (@$penalties) {
1095             my $event = OpenILS::Event->new($pen->name);
1096             $event->{desc} = $pen->label;
1097             push(@allevents, $event);
1098         }
1099
1100         push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1101     }
1102
1103     for (@allevents) {
1104        $_->{payload} = $self->copy if 
1105              ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1106     }
1107
1108     $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1109
1110     $self->push_events(@allevents);
1111 }
1112
1113 sub matrix_test_result_codes {
1114     my $self = shift;
1115     map { $_->{"fail_part"} } @{$self->matrix_test_result};
1116 }
1117
1118 sub matrix_test_result_events {
1119     my $self = shift;
1120     map {
1121         my $event = new OpenILS::Event(
1122             $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1123         );
1124         $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1125         $event;
1126     } (@{$self->matrix_test_result});
1127 }
1128
1129 sub run_indb_circ_test {
1130     my $self = shift;
1131     return $self->matrix_test_result if $self->matrix_test_result;
1132
1133     my $dbfunc = ($self->is_renewal) ? 
1134         'action.item_user_renew_test' : 'action.item_user_circ_test';
1135
1136     if( $self->is_precat && $self->request_precat) {
1137         $self->make_precat_copy;
1138         return if $self->bail_out;
1139     }
1140
1141     my $results = $self->editor->json_query(
1142         {   from => [
1143                 $dbfunc,
1144                 $self->circ_lib,
1145                 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id, 
1146                 $self->patron->id,
1147             ]
1148         }
1149     );
1150
1151     $self->circ_test_success($U->is_true($results->[0]->{success}));
1152
1153     if(my $mp = $results->[0]->{matchpoint}) {
1154         $logger->info("circulator: circ policy test found matchpoint $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
2224     if( $self->checkin_check_holds_shelf() ) {
2225         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2226         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2227         $self->checkin_flesh_events;
2228         return;
2229     }
2230
2231     unless( $self->is_renewal ) {
2232         return $self->bail_on_events($self->editor->event)
2233             unless $self->editor->allowed('COPY_CHECKIN');
2234     }
2235
2236     $self->push_events($self->check_copy_alert());
2237     $self->push_events($self->check_checkin_copy_status());
2238
2239     # if the circ is marked as 'claims returned', add the event to the list
2240     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2241         if ($self->circ and $self->circ->stop_fines 
2242                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2243
2244     $self->check_circ_deposit();
2245
2246     # handle the overridable events 
2247     $self->override_events unless $self->is_renewal;
2248     return if $self->bail_out;
2249     
2250     if( $self->copy ) {
2251         $self->transit(
2252             $self->editor->search_action_transit_copy(
2253                 { target_copy => $self->copy->id, dest_recv_time => undef }
2254             )->[0]
2255         ); 
2256     }
2257
2258     if( $self->circ ) {
2259         $self->checkin_handle_circ;
2260         return if $self->bail_out;
2261         $self->checkin_changed(1);
2262
2263     } elsif( $self->transit ) {
2264         my $hold_transit = $self->process_received_transit;
2265         $self->checkin_changed(1);
2266
2267         if( $self->bail_out ) { 
2268             $self->checkin_flesh_events;
2269             return;
2270         }
2271         
2272         if( my $e = $self->check_checkin_copy_status() ) {
2273             # If the original copy status is special, alert the caller
2274             my $ev = $self->events;
2275             $self->events([$e]);
2276             $self->override_events;
2277             return if $self->bail_out;
2278             $self->events($ev);
2279         }
2280
2281         if( $hold_transit or 
2282                 $U->copy_status($self->copy->status)->id 
2283                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2284
2285             my $hold;
2286             if( $hold_transit ) {
2287                $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2288             } else {
2289                    ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2290             }
2291
2292             $self->hold($hold);
2293
2294             if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2295
2296                 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2297                 $self->reshelve_copy(1);
2298                 $self->cancelled_hold_transit(1);
2299                 $self->notify_hold(0); # don't notify for cancelled holds
2300                 return if $self->bail_out;
2301
2302             } else {
2303
2304                 # hold transited to correct location
2305                 $self->checkin_flesh_events;
2306                 return;
2307             }
2308         } 
2309
2310     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2311
2312         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2313             " that is in-transit, but there is no transit.. repairing");
2314         $self->reshelve_copy(1);
2315         return if $self->bail_out;
2316     }
2317
2318     if( $self->is_renewal ) {
2319         $self->push_events(OpenILS::Event->new('SUCCESS'));
2320         return;
2321     }
2322
2323    # ------------------------------------------------------------------------------
2324    # Circulations and transits are now closed where necessary.  Now go on to see if
2325    # this copy can fulfill a hold or needs to be routed to a different location
2326    # ------------------------------------------------------------------------------
2327
2328     my $needed_for_something = 0; # formerly "needed_for_hold"
2329
2330     if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2331
2332         if (!$self->remote_hold) {
2333             if ($self->use_booking) {
2334                 my $potential_hold = $self->hold_capture_is_possible;
2335                 my $potential_reservation = $self->reservation_capture_is_possible;
2336
2337                 if ($potential_hold and $potential_reservation) {
2338                     $logger->info("circulator: item could fulfill either hold or reservation");
2339                     $self->push_events(new OpenILS::Event(
2340                         "HOLD_RESERVATION_CONFLICT",
2341                         "hold" => $potential_hold,
2342                         "reservation" => $potential_reservation
2343                     ));
2344                     return if $self->bail_out;
2345                 } elsif ($potential_hold) {
2346                     $needed_for_something =
2347                         $self->attempt_checkin_hold_capture;
2348                 } elsif ($potential_reservation) {
2349                     $needed_for_something =
2350                         $self->attempt_checkin_reservation_capture;
2351                 }
2352             } else {
2353                 $needed_for_something = $self->attempt_checkin_hold_capture;
2354             }
2355         }
2356         return if $self->bail_out;
2357     
2358         unless($needed_for_something) {
2359             my $circ_lib = (ref $self->copy->circ_lib) ? 
2360                     $self->copy->circ_lib->id : $self->copy->circ_lib;
2361     
2362             if( $self->remote_hold ) {
2363                 $circ_lib = $self->remote_hold->pickup_lib;
2364                 $logger->warn("circulator: Copy ".$self->copy->barcode.
2365                     " is on a remote hold's shelf, sending to $circ_lib");
2366             }
2367     
2368             $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2369     
2370             if( $circ_lib == $self->circ_lib) {
2371                 # copy is where it needs to be, either for hold or reshelving
2372     
2373                 $self->checkin_handle_precat();
2374                 return if $self->bail_out;
2375     
2376             } else {
2377                 # copy needs to transit "home", or stick here if it's a floating copy
2378     
2379                 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2380                     $self->checkin_changed(1);
2381                     $self->copy->circ_lib( $self->circ_lib );
2382                     $self->update_copy;
2383                 } else {
2384                     my $bc = $self->copy->barcode;
2385                     $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2386                     $self->checkin_build_copy_transit($circ_lib);
2387                     return if $self->bail_out;
2388                     $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2389                 }
2390             }
2391         }
2392     } else { # no-op checkin
2393         if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2394             $self->checkin_changed(1);
2395             $self->copy->circ_lib( $self->circ_lib );
2396             $self->update_copy;
2397         }
2398     }
2399
2400     if($self->claims_never_checked_out and 
2401             $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2402
2403         # the item was not supposed to be checked out to the user and should now be marked as missing
2404         $self->copy->status(OILS_COPY_STATUS_MISSING);
2405         $self->update_copy;
2406
2407     } else {
2408         $self->reshelve_copy unless $needed_for_something;
2409     }
2410
2411     return if $self->bail_out;
2412
2413     unless($self->checkin_changed) {
2414
2415         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2416         my $stat = $U->copy_status($self->copy->status)->id;
2417
2418         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2419          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2420         $self->bail_out(1); # no need to commit anything
2421
2422     } else {
2423
2424         $self->push_events(OpenILS::Event->new('SUCCESS')) 
2425             unless @{$self->events};
2426     }
2427
2428     # gather any updates to the circ after fine generation, if there was a circ
2429     $self->generate_fines_finish if ($self->circ);
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 # if a deposit was payed for this item, push the event
2439 sub check_circ_deposit {
2440     my $self = shift;
2441     return unless $self->circ;
2442     my $deposit = $self->editor->search_money_billing(
2443         {   btype => 5, 
2444             xact => $self->circ->id, 
2445             voided => 'f'
2446         }, {idlist => 1})->[0];
2447
2448     $self->push_events(OpenILS::Event->new(
2449         'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2450 }
2451
2452 sub reshelve_copy {
2453    my $self    = shift;
2454    my $force   = $self->force || shift;
2455    my $copy    = $self->copy;
2456
2457    my $stat = $U->copy_status($copy->status)->id;
2458
2459    if($force || (
2460       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2461       $stat != OILS_COPY_STATUS_CATALOGING and
2462       $stat != OILS_COPY_STATUS_IN_TRANSIT and
2463       $stat != OILS_COPY_STATUS_RESHELVING  )) {
2464
2465         $copy->status( OILS_COPY_STATUS_RESHELVING );
2466             $self->update_copy;
2467             $self->checkin_changed(1);
2468     }
2469 }
2470
2471
2472 # Returns true if the item is at the current location
2473 # because it was transited there for a hold and the 
2474 # hold has not been fulfilled
2475 sub checkin_check_holds_shelf {
2476     my $self = shift;
2477     return 0 unless $self->copy;
2478
2479     return 0 unless 
2480         $U->copy_status($self->copy->status)->id ==
2481             OILS_COPY_STATUS_ON_HOLDS_SHELF;
2482
2483     # find the hold that put us on the holds shelf
2484     my $holds = $self->editor->search_action_hold_request(
2485         { 
2486             current_copy => $self->copy->id,
2487             capture_time => { '!=' => undef },
2488             fulfillment_time => undef,
2489             cancel_time => undef,
2490         }
2491     );
2492
2493     unless(@$holds) {
2494         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2495         $self->reshelve_copy(1);
2496         return 0;
2497     }
2498
2499     my $hold = $$holds[0];
2500
2501     $logger->info("circulator: we found a captured, un-fulfilled hold [".
2502         $hold->id. "] for copy ".$self->copy->barcode);
2503
2504     if( $hold->pickup_lib == $self->circ_lib ) {
2505         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2506         return 1;
2507     }
2508
2509     $logger->info("circulator: hold is not for here..");
2510     $self->remote_hold($hold);
2511     return 0;
2512 }
2513
2514
2515 sub checkin_handle_precat {
2516     my $self    = shift;
2517    my $copy    = $self->copy;
2518
2519    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2520         $copy->status(OILS_COPY_STATUS_CATALOGING);
2521         $self->update_copy();
2522         $self->checkin_changed(1);
2523         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2524    }
2525 }
2526
2527
2528 sub checkin_build_copy_transit {
2529     my $self            = shift;
2530     my $dest            = shift;
2531     my $copy       = $self->copy;
2532    my $transit    = Fieldmapper::action::transit_copy->new;
2533
2534     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2535     $logger->info("circulator: transiting copy to $dest");
2536
2537    $transit->source($self->circ_lib);
2538    $transit->dest($dest);
2539    $transit->target_copy($copy->id);
2540    $transit->source_send_time('now');
2541    $transit->copy_status( $U->copy_status($copy->status)->id );
2542
2543     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2544
2545     return $self->bail_on_events($self->editor->event)
2546         unless $self->editor->create_action_transit_copy($transit);
2547
2548    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2549     $self->update_copy;
2550     $self->checkin_changed(1);
2551 }
2552
2553
2554 sub hold_capture_is_possible {
2555     my $self = shift;
2556     my $copy = $self->copy;
2557
2558     # we've been explicitly told not to capture any holds
2559     return 0 if $self->capture eq 'nocapture';
2560
2561     # See if this copy can fulfill any holds
2562     my $hold = $holdcode->find_nearest_permitted_hold(
2563         $self->editor, $copy, $self->editor->requestor, 1 # check_only
2564     );
2565     return undef if ref $hold eq "HASH" and
2566         $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2567     return $hold;
2568 }
2569
2570 sub reservation_capture_is_possible {
2571     my $self = shift;
2572     my $copy = $self->copy;
2573
2574     # we've been explicitly told not to capture any holds
2575     return 0 if $self->capture eq 'nocapture';
2576
2577     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2578     my $resv = $booking_ses->request(
2579         "open-ils.booking.reservations.could_capture",
2580         $self->editor->authtoken, $copy->barcode
2581     )->gather(1);
2582     $booking_ses->disconnect;
2583     if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2584         $self->push_events($resv);
2585     } else {
2586         return $resv;
2587     }
2588 }
2589
2590 # returns true if the item was used (or may potentially be used 
2591 # in subsequent calls) to capture a hold.
2592 sub attempt_checkin_hold_capture {
2593     my $self = shift;
2594     my $copy = $self->copy;
2595
2596     # we've been explicitly told not to capture any holds
2597     return 0 if $self->capture eq 'nocapture';
2598
2599     # See if this copy can fulfill any holds
2600     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
2601         $self->editor, $copy, $self->editor->requestor );
2602
2603     if(!$hold) {
2604         $logger->debug("circulator: no potential permitted".
2605             "holds found for copy ".$copy->barcode);
2606         return 0;
2607     }
2608
2609     if($self->capture ne 'capture') {
2610         # see if this item is in a hold-capture-delay location
2611         my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2612         if($U->is_true($location->hold_verify)) {
2613             $self->bail_on_events(
2614                 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2615             return 1;
2616         }
2617     }
2618
2619     $self->retarget($retarget);
2620
2621     $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2622
2623     $hold->current_copy($copy->id);
2624     $hold->capture_time('now');
2625     $self->put_hold_on_shelf($hold) 
2626         if $hold->pickup_lib == $self->circ_lib;
2627
2628     # prevent DB errors caused by fetching 
2629     # holds from storage, and updating through cstore
2630     $hold->clear_fulfillment_time;
2631     $hold->clear_fulfillment_staff;
2632     $hold->clear_fulfillment_lib;
2633     $hold->clear_expire_time; 
2634     $hold->clear_cancel_time;
2635     $hold->clear_prev_check_time unless $hold->prev_check_time;
2636
2637     $self->bail_on_events($self->editor->event)
2638         unless $self->editor->update_action_hold_request($hold);
2639     $self->hold($hold);
2640     $self->checkin_changed(1);
2641
2642     return 0 if $self->bail_out;
2643
2644     if( $hold->pickup_lib == $self->circ_lib ) {
2645
2646         # This hold was captured in the correct location
2647         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2648         $self->push_events(OpenILS::Event->new('SUCCESS'));
2649
2650         #$self->do_hold_notify($hold->id);
2651         $self->notify_hold($hold->id);
2652
2653     } else {
2654     
2655         # Hold needs to be picked up elsewhere.  Build a hold
2656         # transit and route the item.
2657         $self->checkin_build_hold_transit();
2658         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2659         return 0 if $self->bail_out;
2660         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2661     }
2662
2663     # make sure we save the copy status
2664     $self->update_copy;
2665     return 1;
2666 }
2667
2668 sub attempt_checkin_reservation_capture {
2669     my $self = shift;
2670     my $copy = $self->copy;
2671
2672     # we've been explicitly told not to capture any holds
2673     return 0 if $self->capture eq 'nocapture';
2674
2675     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2676     my $evt = $booking_ses->request(
2677         "open-ils.booking.resources.capture_for_reservation",
2678         $self->editor->authtoken,
2679         $copy->barcode,
2680         1 # don't update copy - we probably have it locked
2681     )->gather(1);
2682     $booking_ses->disconnect;
2683
2684     if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2685         $logger->warn(
2686             "open-ils.booking.resources.capture_for_reservation " .
2687             "didn't return an event!"
2688         );
2689     } else {
2690         if (
2691             $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2692             $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2693         ) {
2694             # not-transferable is an error event we'll pass on the user
2695             $logger->warn("reservation capture attempted against non-transferable item");
2696             $self->push_events($evt);
2697             return 0;
2698         } elsif ($evt->{"textcode"} eq "SUCCESS") {
2699             # Re-retrieve copy as reservation capture may have changed
2700             # its status and whatnot.
2701             $logger->info(
2702                 "circulator: booking capture win on copy " . $self->copy->id
2703             );
2704             if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2705                 $logger->info(
2706                     "circulator: changing copy " . $self->copy->id .
2707                     "'s status from " . $self->copy->status . " to " .
2708                     $new_copy_status
2709                 );
2710                 $self->copy->status($new_copy_status);
2711                 $self->update_copy;
2712             }
2713             $self->reservation($evt->{"payload"}->{"reservation"});
2714
2715             if (exists $evt->{"payload"}->{"transit"}) {
2716                 $self->push_events(
2717                     new OpenILS::Event(
2718                         "ROUTE_ITEM",
2719                         "org" => $evt->{"payload"}->{"transit"}->dest
2720                     )
2721                 );
2722             }
2723             $self->checkin_changed(1);
2724             return 1;
2725         }
2726     }
2727     # other results are treated as "nothing to capture"
2728     return 0;
2729 }
2730
2731 sub do_hold_notify {
2732     my( $self, $holdid ) = @_;
2733
2734     my $e = new_editor(xact => 1);
2735     my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2736     $e->rollback;
2737     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2738     $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2739
2740     $logger->info("circulator: running delayed hold notify process");
2741
2742 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2743 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2744
2745     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2746         hold_id => $holdid, requestor => $self->editor->requestor);
2747
2748     $logger->debug("circulator: built hold notifier");
2749
2750     if(!$notifier->event) {
2751
2752         $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2753
2754         my $stat = $notifier->send_email_notify;
2755         if( $stat == '1' ) {
2756             $logger->info("circulator: hold notify succeeded for hold $holdid");
2757             return;
2758         } 
2759
2760         $logger->debug("circulator:  * hold notify cancelled or failed for hold $holdid");
2761
2762     } else {
2763         $logger->info("circulator: Not sending hold notification since the patron has no email address");
2764     }
2765 }
2766
2767 sub retarget_holds {
2768     my $self = shift;
2769     $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2770     my $ses = OpenSRF::AppSession->create('open-ils.storage');
2771     $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2772     # no reason to wait for the return value
2773     return;
2774 }
2775
2776 sub checkin_build_hold_transit {
2777     my $self = shift;
2778
2779    my $copy = $self->copy;
2780    my $hold = $self->hold;
2781    my $trans = Fieldmapper::action::hold_transit_copy->new;
2782
2783     $logger->debug("circulator: building hold transit for ".$copy->barcode);
2784
2785    $trans->hold($hold->id);
2786    $trans->source($self->circ_lib);
2787    $trans->dest($hold->pickup_lib);
2788    $trans->source_send_time("now");
2789    $trans->target_copy($copy->id);
2790
2791     # when the copy gets to its destination, it will recover
2792     # this status - put it onto the holds shelf
2793    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2794
2795     return $self->bail_on_events($self->editor->event)
2796         unless $self->editor->create_action_hold_transit_copy($trans);
2797 }
2798
2799
2800
2801 sub process_received_transit {
2802     my $self = shift;
2803     my $copy = $self->copy;
2804     my $copyid = $self->copy->id;
2805
2806     my $status_name = $U->copy_status($copy->status)->name;
2807     $logger->debug("circulator: attempting transit receive on ".
2808         "copy $copyid. Copy status is $status_name");
2809
2810     my $transit = $self->transit;
2811
2812     if( $transit->dest != $self->circ_lib ) {
2813         # - this item is in-transit to a different location
2814
2815         my $tid = $transit->id; 
2816         my $loc = $self->circ_lib;
2817         my $dest = $transit->dest;
2818
2819         $logger->info("circulator: Fowarding transit on copy which is destined ".
2820             "for a different location. transit=$tid, copy=$copyid, current ".
2821             "location=$loc, destination location=$dest");
2822
2823         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2824
2825         # grab the associated hold object if available
2826         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2827         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2828
2829         return $self->bail_on_events($evt);
2830     }
2831
2832     # The transit is received, set the receive time
2833     $transit->dest_recv_time('now');
2834     $self->bail_on_events($self->editor->event)
2835         unless $self->editor->update_action_transit_copy($transit);
2836
2837     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2838
2839     $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2840     $copy->status( $transit->copy_status );
2841     $self->update_copy();
2842     return if $self->bail_out;
2843
2844     my $ishold = 0;
2845     if($hold_transit) { 
2846         my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2847
2848         # hold has arrived at destination, set shelf time
2849         $self->put_hold_on_shelf($hold);
2850         $self->bail_on_events($self->editor->event)
2851             unless $self->editor->update_action_hold_request($hold);
2852         return if $self->bail_out;
2853
2854         $self->notify_hold($hold_transit->hold);
2855         $ishold = 1;
2856     }
2857
2858     $self->push_events( 
2859         OpenILS::Event->new(
2860         'SUCCESS', 
2861         ishold => $ishold,
2862       payload => { transit => $transit, holdtransit => $hold_transit } ));
2863
2864     return $hold_transit;
2865 }
2866
2867
2868 # ------------------------------------------------------------------
2869 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2870 # ------------------------------------------------------------------
2871 sub put_hold_on_shelf {
2872     my($self, $hold) = @_;
2873
2874     $hold->shelf_time('now');
2875
2876     my $shelf_expire = $U->ou_ancestor_setting_value(
2877         $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2878
2879     if($shelf_expire) {
2880         my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2881         my $expire_time = DateTime->now->add(seconds => $seconds);
2882         $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2883     }
2884
2885     return undef;
2886 }
2887
2888
2889
2890 sub generate_fines {
2891    my $self = shift;
2892    my $reservation = shift;
2893
2894    $self->generate_fines_start($reservation);
2895    $self->generate_fines_finish($reservation);
2896
2897    return undef;
2898 }
2899
2900 sub generate_fines_start {
2901    my $self = shift;
2902    my $reservation = shift;
2903
2904    my $id = $reservation ? $self->reservation->id : $self->circ->id;
2905
2906    if (!exists($self->{_gen_fines_req})) {
2907       $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage') 
2908           ->request(
2909              'open-ils.storage.action.circulation.overdue.generate_fines',
2910              undef,
2911              $id
2912           );
2913    }
2914
2915    return undef;
2916 }
2917
2918 sub generate_fines_finish {
2919    my $self = shift;
2920    my $reservation = shift;
2921
2922    my $id = $reservation ? $self->reservation->id : $self->circ->id;
2923
2924    $self->{_gen_fines_req}->wait_complete;
2925    delete($self->{_gen_fines_req});
2926
2927    # refresh the circ in case the fine generator set the stop_fines field
2928    $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
2929    $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
2930
2931    return undef;
2932 }
2933
2934 sub checkin_handle_circ {
2935    my $self = shift;
2936    my $circ = $self->circ;
2937    my $copy = $self->copy;
2938    my $evt;
2939    my $obt;
2940
2941    $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
2942
2943    # backdate the circ if necessary
2944    if($self->backdate) {
2945         my $evt = $self->checkin_handle_backdate;
2946         return $self->bail_on_events($evt) if $evt;
2947    }
2948
2949    if($self->void_overdues) {
2950         my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2951             $self->editor, $circ, undef, 'System: Amnesty Checkin'); # TODO i18n for system-generated notes
2952         return $self->bail_on_events($evt) if $evt;
2953    }
2954
2955    if(!$circ->stop_fines) {
2956       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2957       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2958       $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
2959       $circ->stop_fines_time('now');
2960       $circ->stop_fines_time($self->backdate) if $self->backdate;
2961    }
2962
2963     # Set the checkin vars since we have the item
2964     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2965
2966     # capture the true scan time for back-dated checkins
2967     $circ->checkin_scan_time('now');
2968
2969     $circ->checkin_staff($self->editor->requestor->id);
2970     $circ->checkin_lib($self->circ_lib);
2971     $circ->checkin_workstation($self->editor->requestor->wsid);
2972
2973     my $circ_lib = (ref $self->copy->circ_lib) ?  
2974         $self->copy->circ_lib->id : $self->copy->circ_lib;
2975     my $stat = $U->copy_status($self->copy->status)->id;
2976
2977     # immediately available keeps items lost or missing items from going home before being handled
2978     my $lost_immediately_available = $U->ou_ancestor_setting_value(
2979         $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
2980
2981
2982     if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
2983
2984         if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
2985             $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2986         } else {
2987             $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2988             $self->update_copy;
2989         }
2990
2991     } elsif ($stat == OILS_COPY_STATUS_LOST) {
2992
2993         $self->checkin_handle_lost($circ_lib);
2994
2995     } else {
2996
2997         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2998         $self->update_copy;
2999     }
3000
3001
3002     # see if there are any fines owed on this circ.  if not, close it
3003     ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3004     $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3005
3006     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3007
3008     return $self->bail_on_events($self->editor->event)
3009         unless $self->editor->update_action_circulation($circ);
3010
3011     # make sure the circ isn't closed if we just voided some fines
3012     $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $circ->id);
3013     return $self->bail_on_events($evt) if $evt;
3014
3015     return undef;
3016 }
3017
3018
3019 # ------------------------------------------------------------------
3020 # See if we need to void billings for lost checkin
3021 # ------------------------------------------------------------------
3022 sub checkin_handle_lost {
3023     my $self = shift;
3024     my $circ_lib = shift;
3025     my $circ = $self->circ;
3026
3027     my $max_return = $U->ou_ancestor_setting_value(
3028         $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3029
3030     if ($max_return) {
3031
3032         my $today = time();
3033         my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3034         $tm[5] -= 1 if $tm[5] > 0;
3035         my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3036
3037         my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3038         $logger->info("MAX OD: ".$max_return."  DUEDATE: ".$circ->due_date."  TODAY: ".$today."  DUE: ".$due."  LAST: ".$last_chance);
3039
3040         $max_return = 0 if $today < $last_chance;
3041     }
3042
3043     if (!$max_return){  # there's either no max time to accept returns defined or we're within that time
3044
3045         my $void_lost = $U->ou_ancestor_setting_value(
3046             $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3047         my $void_lost_fee = $U->ou_ancestor_setting_value(
3048             $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3049         my $restore_od = $U->ou_ancestor_setting_value(
3050             $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3051
3052         $self->checkin_handle_lost_now_found(3) if $void_lost;
3053         $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3054         $self->checkin_handle_lost_now_found_restore_od() if $restore_od && ! $self->void_overdues;
3055     }
3056
3057     $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3058     $self->update_copy;
3059 }
3060
3061
3062 sub checkin_handle_backdate {
3063     my $self = shift;
3064
3065     # ------------------------------------------------------------------
3066     # clean up the backdate for date comparison
3067     # XXX We are currently taking the due-time from the original due-date,
3068     # not the input.  Do we need to do this?  This certainly interferes with
3069     # backdating of hourly checkouts, but that is likely a very rare case.
3070     # ------------------------------------------------------------------
3071     my $bd = cleanse_ISO8601($self->backdate);
3072     my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3073     my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3074     $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3075
3076     $self->backdate($bd);
3077
3078     my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($self->editor, $self->circ, $bd);
3079     return $evt if $evt;
3080
3081     return undef;
3082 }
3083
3084
3085 sub check_checkin_copy_status {
3086     my $self = shift;
3087    my $copy = $self->copy;
3088
3089    my $status = $U->copy_status($copy->status)->id;
3090
3091    return undef
3092       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
3093             $status == OILS_COPY_STATUS_CHECKED_OUT ||
3094             $status == OILS_COPY_STATUS_IN_PROCESS  ||
3095             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
3096             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
3097             $status == OILS_COPY_STATUS_CATALOGING  ||
3098             $status == OILS_COPY_STATUS_ON_RESV_SHELF  ||
3099             $status == OILS_COPY_STATUS_RESHELVING );
3100
3101    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3102       if( $status == OILS_COPY_STATUS_LOST );
3103
3104    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3105       if( $status == OILS_COPY_STATUS_MISSING );
3106
3107    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3108 }
3109
3110
3111
3112 # --------------------------------------------------------------------------
3113 # On checkin, we need to return as many relevant objects as we can
3114 # --------------------------------------------------------------------------
3115 sub checkin_flesh_events {
3116     my $self = shift;
3117
3118     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
3119         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3120             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3121     }
3122
3123     my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3124
3125     my $hold;
3126     if($self->hold and !$self->hold->cancel_time) {
3127         $hold = $self->hold;
3128         $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3129     }
3130
3131     if($self->circ) {
3132         # if we checked in a circulation, flesh the billing summary data
3133         $self->circ->billable_transaction(
3134             $self->editor->retrieve_money_billable_transaction([
3135                 $self->circ->id,
3136                 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3137             ])
3138         );
3139     }
3140
3141     if($self->patron) {
3142         # flesh some patron fields before returning
3143         $self->patron(
3144             $self->editor->retrieve_actor_user([
3145                 $self->patron->id,
3146                 {
3147                     flesh => 1,
3148                     flesh_fields => {
3149                         au => ['card', 'billing_address', 'mailing_address']
3150                     }
3151                 }
3152             ])
3153         );
3154     }
3155
3156     for my $evt (@{$self->events}) {
3157
3158         my $payload         = {};
3159         $payload->{copy}    = $U->unflesh_copy($self->copy);
3160         $payload->{record}  = $record,
3161         $payload->{circ}    = $self->circ;
3162         $payload->{transit} = $self->transit;
3163         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3164         $payload->{hold}    = $hold;
3165         $payload->{patron}  = $self->patron;
3166         $payload->{reservation} = $self->reservation
3167             unless (not $self->reservation or $self->reservation->cancel_time);
3168
3169         $evt->{payload}     = $payload;
3170     }
3171 }
3172
3173 sub log_me {
3174     my( $self, $msg ) = @_;
3175     my $bc = ($self->copy) ? $self->copy->barcode :
3176         $self->barcode;
3177     $bc ||= "";
3178     my $usr = ($self->patron) ? $self->patron->id : "";
3179     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3180         ", recipient=$usr, copy=$bc");
3181 }
3182
3183
3184 sub do_renew {
3185     my $self = shift;
3186     $self->log_me("do_renew()");
3187
3188     # Make sure there is an open circ to renew that is not
3189     # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3190     my $usrid = $self->patron->id if $self->patron;
3191     my $circ = $self->editor->search_action_circulation({
3192         target_copy => $self->copy->id,
3193         xact_finish => undef,
3194         ($usrid ? (usr => $usrid) : ()),
3195         '-or' => [
3196             {stop_fines => undef},
3197             {stop_fines => OILS_STOP_FINES_MAX_FINES}
3198         ]
3199     })->[0];
3200
3201     return $self->bail_on_events($self->editor->event) unless $circ;
3202
3203     # A user is not allowed to renew another user's items without permission
3204     unless( $circ->usr eq $self->editor->requestor->id ) {
3205         return $self->bail_on_events($self->editor->events)
3206             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3207     }   
3208
3209     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3210         if $circ->renewal_remaining < 1;
3211
3212     # -----------------------------------------------------------------
3213
3214     $self->parent_circ($circ->id);
3215     $self->renewal_remaining( $circ->renewal_remaining - 1 );
3216     $self->circ($circ);
3217
3218     # Run the fine generator against the old circ
3219     $self->generate_fines_start;
3220
3221     $self->run_renew_permit;
3222
3223     # Check the item in
3224     $self->do_checkin();
3225     return if $self->bail_out;
3226
3227     unless( $self->permit_override ) {
3228         $self->do_permit();
3229         return if $self->bail_out;
3230         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3231         $self->remove_event('ITEM_NOT_CATALOGED');
3232     }   
3233
3234     $self->override_events;
3235     return if $self->bail_out;
3236
3237     $self->events([]);
3238     $self->do_checkout();
3239 }
3240
3241
3242 sub remove_event {
3243     my( $self, $evt ) = @_;
3244     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3245     $logger->debug("circulator: removing event from list: $evt");
3246     my @events = @{$self->events};
3247     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3248 }
3249
3250
3251 sub have_event {
3252     my( $self, $evt ) = @_;
3253     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3254     return grep { $_->{textcode} eq $evt } @{$self->events};
3255 }
3256
3257
3258
3259 sub run_renew_permit {
3260     my $self = shift;
3261
3262     if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3263         my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3264             $self->editor, $self->copy, $self->editor->requestor, 1
3265         );
3266         $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3267     }
3268
3269     if(!$self->legacy_script_support) {
3270         my $results = $self->run_indb_circ_test;
3271         $self->push_events($self->matrix_test_result_events)
3272             unless $self->circ_test_success;
3273     } else {
3274
3275         my $runner = $self->script_runner;
3276
3277         $runner->load($self->circ_permit_renew);
3278         my $result = $runner->run or 
3279             throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3280         if ($result->{"events"}) {
3281             $self->push_events(
3282                 map { new OpenILS::Event($_) } @{$result->{"events"}}
3283             );
3284             $logger->activity(
3285                 "circulator: circ_permit_renew for user " .
3286                 $self->patron->id . " returned " .
3287                 scalar(@{$result->{"events"}}) . " event(s)"
3288             );
3289         }
3290
3291         $self->mk_script_runner;
3292     }
3293
3294     $logger->debug("circulator: re-creating script runner to be safe");
3295 }
3296
3297
3298 # XXX: The primary mechanism for storing circ history is now handled
3299 # by tracking real circulation objects instead of bibs in a bucket.
3300 # However, this code is disabled by default and could be useful 
3301 # some day, so may as well leave it for now.
3302 sub append_reading_list {
3303     my $self = shift;
3304
3305     return undef unless 
3306         $self->is_checkout and 
3307         $self->patron and 
3308         $self->copy and 
3309         !$self->is_noncat;
3310
3311
3312     # verify history is globally enabled and uses the bucket mechanism
3313     my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3314         apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3315
3316     return undef unless $htype and $htype eq 'bucket';
3317
3318     my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3319
3320     # verify the patron wants to retain the hisory
3321         my $setting = $e->search_actor_user_setting(
3322                 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3323     
3324     unless($setting and $setting->value) {
3325         $e->rollback;
3326         return undef;
3327     }
3328
3329     my $bkt = $e->search_container_copy_bucket(
3330         {owner => $self->patron->id, btype => 'circ_history'})->[0];
3331
3332     my $pos = 1;
3333
3334     if($bkt) {
3335         # find the next item position
3336         my $last_item = $e->search_container_copy_bucket_item(
3337             {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3338         $pos = $last_item->pos + 1 if $last_item;
3339
3340     } else {
3341         # create the history bucket if necessary
3342         $bkt = Fieldmapper::container::copy_bucket->new;
3343         $bkt->owner($self->patron->id);
3344         $bkt->name('');
3345         $bkt->btype('circ_history');
3346         $bkt->pub('f');
3347         $e->create_container_copy_bucket($bkt) or return $e->die_event;
3348     }
3349
3350     my $item = Fieldmapper::container::copy_bucket_item->new;
3351
3352     $item->bucket($bkt->id);
3353     $item->target_copy($self->copy->id);
3354     $item->pos($pos);
3355
3356     $e->create_container_copy_bucket_item($item) or return $e->die_event;
3357     $e->commit;
3358
3359     return undef;
3360 }
3361
3362
3363 sub make_trigger_events {
3364     my $self = shift;
3365     return unless $self->circ;
3366     $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3367     $U->create_events_for_hook('checkin',  $self->circ, $self->circ_lib) if $self->is_checkin;
3368     $U->create_events_for_hook('renewal',  $self->circ, $self->circ_lib) if $self->is_renewal;
3369 }
3370
3371
3372
3373 sub checkin_handle_lost_now_found {
3374     my ($self, $bill_type) = @_;
3375
3376     # ------------------------------------------------------------------
3377     # remove charge from patron's account if lost item is returned
3378     # ------------------------------------------------------------------
3379
3380     my $bills = $self->editor->search_money_billing(
3381         {
3382             xact => $self->circ->id,
3383             btype => $bill_type
3384         }
3385     );
3386
3387     $logger->debug("voiding lost item charge of  ".scalar(@$bills));
3388     for my $bill (@$bills) {
3389         if( !$U->is_true($bill->voided) ) {
3390             $logger->info("lost item returned - voiding bill ".$bill->id);
3391             $bill->voided('t');
3392             $bill->void_time('now');
3393             $bill->voider($self->editor->requestor->id);
3394             my $note = ($bill->note) ? $bill->note . "\n" : '';
3395             $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3396
3397             $self->bail_on_events($self->editor->event)
3398                 unless $self->editor->update_money_billing($bill);
3399         }
3400     }
3401 }
3402
3403 sub checkin_handle_lost_now_found_restore_od {
3404     my $self = shift;
3405
3406     # ------------------------------------------------------------------
3407     # restore those overdue charges voided when item was set to lost
3408     # ------------------------------------------------------------------
3409
3410     my $ods = $self->editor->search_money_billing(
3411         {
3412                 xact => $self->circ->id,
3413                 btype => 1
3414         }
3415     );
3416
3417     $logger->debug("returning overdue charges pre-lost  ".scalar(@$ods));
3418     for my $bill (@$ods) {
3419         if( $U->is_true($bill->voided) ) {
3420                 $logger->info("lost item returned - restoring overdue ".$bill->id);
3421                 $bill->voided('f');
3422                 $bill->clear_void_time;
3423                 $bill->voider($self->editor->requestor->id);
3424                 my $note = ($bill->note) ? $bill->note . "\n" : '';
3425                 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3426
3427                 $self->bail_on_events($self->editor->event)
3428                         unless $self->editor->update_money_billing($bill);
3429         }
3430     }
3431 }
3432
3433 1;