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