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