]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
update to match autogenerated not-found events. more to come
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Circ / Circulate.pm
1 package OpenILS::Application::Circ::Circulate;
2 use base 'OpenSRF::Application';
3 use strict; use warnings;
4 use OpenSRF::EX qw(:try);
5 use Data::Dumper;
6 use OpenSRF::Utils::Cache;
7 use OpenSRF::AppSession;
8 use Digest::MD5 qw(md5_hex);
9 use OpenILS::Utils::ScriptRunner;
10 use OpenILS::Application::AppUtils;
11 use OpenILS::Application::Circ::Holds;
12 use OpenILS::Application::Circ::Transit;
13 use OpenILS::Utils::PermitHold;
14 use OpenSRF::Utils::Logger qw(:logger);
15 use DateTime;
16 use DateTime::Format::ISO8601;
17 use OpenSRF::Utils qw/:datetime/;
18
19 $Data::Dumper::Indent = 0;
20 my $apputils    = "OpenILS::Application::AppUtils";
21 my $U                           = $apputils;
22 my $holdcode    = "OpenILS::Application::Circ::Holds";
23 my $transcode   = "OpenILS::Application::Circ::Transit";
24
25 my %scripts;                    # - circulation script filenames
26 my $script_libs;                # - any additional script libraries
27 my %cache;                              # - db objects cache
28 my %contexts;                   # - Script runner contexts
29 my $cache_handle;               # - memcache handle
30
31 sub PRECAT_FINE_LEVEL { return 2; }
32 sub PRECAT_LOAN_DURATION { return 2; }
33
34 my %RECORD_FROM_COPY_CACHE;
35
36
37 # for security, this is a process-defined and not
38 # a client-defined variable
39 my $__isrenewal = 0;
40
41 # ------------------------------------------------------------------------------
42 # Load the circ script from the config
43 # ------------------------------------------------------------------------------
44 sub initialize {
45
46         my $self = shift;
47         $cache_handle = OpenSRF::Utils::Cache->new('global');
48         my $conf = OpenSRF::Utils::SettingsClient->new;
49         my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
50         my @pfx = ( @pfx2, "scripts" );
51
52         my $p           = $conf->config_value(  @pfx, 'circ_permit_patron' );
53         my $c           = $conf->config_value(  @pfx, 'circ_permit_copy' );
54         my $d           = $conf->config_value(  @pfx, 'circ_duration' );
55         my $f           = $conf->config_value(  @pfx, 'circ_recurring_fines' );
56         my $m           = $conf->config_value(  @pfx, 'circ_max_fines' );
57         my $pr  = $conf->config_value(  @pfx, 'circ_permit_renew' );
58         my $lb  = $conf->config_value(  @pfx2, 'script_path' );
59
60         $logger->error( "Missing circ script(s)" ) 
61                 unless( $p and $c and $d and $f and $m and $pr );
62
63         $scripts{circ_permit_patron}    = $p;
64         $scripts{circ_permit_copy}              = $c;
65         $scripts{circ_duration}                 = $d;
66         $scripts{circ_recurring_fines}= $f;
67         $scripts{circ_max_fines}                = $m;
68         $scripts{circ_permit_renew}     = $pr;
69
70         $lb = [ $lb ] unless ref($lb);
71         $script_libs = $lb;
72
73         $logger->debug("Loaded rules scripts for circ: " .
74                 "circ permit patron: $p, circ permit copy: $c, ".
75                 "circ duration :$d , circ recurring fines : $f, " .
76                 "circ max fines : $m, circ renew permit : $pr");
77 }
78
79
80 # ------------------------------------------------------------------------------
81 # Loads the necessary circ objects and pushes them into the script environment
82 # Returns ( $data, $evt ).  if $evt is defined, then an
83 # unexpedted event occurred and should be dealt with / returned to the caller
84 # ------------------------------------------------------------------------------
85 sub create_circ_ctx {
86         my %params = @_;
87         $U->logmark;
88
89         my $evt;
90         my $ctx = \%params;
91
92         $evt = _ctx_add_patron_objects($ctx, %params);
93         return (undef,$evt) if $evt;
94
95         if(!$params{noncat}) {
96                 if( $evt = _ctx_add_copy_objects($ctx, %params) ) {
97                         $ctx->{precat} = 1 if($evt->{textcode} eq 'ASSET_COPY_NOT_FOUND')
98                 } else {
99                         $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
100                 }
101         }
102
103         _doctor_patron_object($ctx) if $ctx->{patron};
104         _doctor_copy_object($ctx) if $ctx->{copy};
105
106         if(!$ctx->{no_runner}) {
107                 _build_circ_script_runner($ctx);
108                 _add_script_runner_methods($ctx);
109         }
110
111         return $ctx;
112 }
113
114 sub _ctx_add_patron_objects {
115         my( $ctx, %params) = @_;
116         $U->logmark;
117
118         # - patron standings are now handled in the penalty server...
119
120         #if(!defined($cache{patron_standings})) {
121         #       $cache{patron_standings} = $U->fetch_patron_standings();
122         #}
123         #$ctx->{patron_standings} = $cache{patron_standings};
124
125         $cache{group_tree} = $U->fetch_permission_group_tree() unless $cache{group_tree};
126         $ctx->{group_tree} = $cache{group_tree};
127
128         $ctx->{patron_circ_summary} = 
129                 $U->fetch_patron_circ_summary($ctx->{patron}->id) 
130                 if $params{fetch_patron_circsummary};
131
132         return undef;
133 }
134
135
136 sub _find_copy_by_attr {
137         my %params = @_;
138         $U->logmark;
139         my $evt;
140
141         my $copy = $params{copy} || undef;
142
143         if(!$copy) {
144
145                 ( $copy, $evt ) = 
146                         $U->fetch_copy($params{copyid}) if $params{copyid};
147                 return (undef,$evt) if $evt;
148
149                 if(!$copy) {
150                         ( $copy, $evt ) = 
151                                 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
152                         return (undef,$evt) if $evt;
153                 }
154         }
155         return ( $copy, $evt );
156 }
157
158 sub _ctx_add_copy_objects {
159         my($ctx, %params)  = @_;
160         $U->logmark;
161         my $evt;
162         my $copy;
163
164         $cache{copy_statuses} = $U->fetch_copy_statuses 
165                 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
166
167         $cache{copy_locations} = $U->fetch_copy_locations 
168                 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
169
170         $ctx->{copy_statuses} = $cache{copy_statuses};
171         $ctx->{copy_locations} = $cache{copy_locations};
172
173         ($copy, $evt) = _find_copy_by_attr(%params);
174         return $evt if $evt;
175
176         if( $copy and !$ctx->{title} ) {
177                 $logger->debug("Copy status: " . $copy->status);
178
179                 my $r = $RECORD_FROM_COPY_CACHE{$copy->id};
180                 ($r, $evt) = $U->fetch_record_by_copy( $copy->id ) unless $r;
181                 return $evt if $evt;
182                 $RECORD_FROM_COPY_CACHE{$copy->id} = $r;
183
184                 $ctx->{title} = $r;
185                 $ctx->{copy} = $copy;
186         }
187
188         return undef;
189 }
190
191
192 # ------------------------------------------------------------------------------
193 # Fleshes parts of the patron object
194 # ------------------------------------------------------------------------------
195 sub _doctor_copy_object {
196         my $ctx = shift;
197         $U->logmark;
198         my $copy = $ctx->{copy} || return undef;
199
200         $logger->debug("Doctoring copy object...");
201
202         # set the copy status to a status name
203         $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
204
205         # set the copy location to the location object
206         $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
207
208         $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
209 }
210
211
212 # ------------------------------------------------------------------------------
213 # Fleshes parts of the patron object
214 # ------------------------------------------------------------------------------
215 sub _doctor_patron_object {
216         my $ctx = shift;
217         $U->logmark;
218         my $patron = $ctx->{patron} || return undef;
219
220         # push the standing object into the patron
221 #       if(ref($ctx->{patron_standings})) {
222 #               for my $s (@{$ctx->{patron_standings}}) {
223 #                       if( $s->id eq $ctx->{patron}->standing ) {
224 #                               $patron->standing($s);
225 #                               $logger->debug("Set patron standing to ". $s->value);
226 #                       }
227 #               }
228 #       }
229
230         # set the patron ptofile to the profile name
231         $patron->profile( _get_patron_profile( 
232                 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
233
234         # flesh the org unit
235         $patron->home_ou( 
236                 $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
237
238 }
239
240 # recurse and find the patron profile name from the tree
241 # another option would be to grab the groups for the patron
242 # and cycle through those until the "profile" group has been found
243 sub _get_patron_profile { 
244         my( $patron, $group_tree ) = @_;
245         return $group_tree if ($group_tree->id eq $patron->profile);
246         return undef unless ($group_tree->children);
247
248         for my $child (@{$group_tree->children}) {
249                 my $ret = _get_patron_profile( $patron, $child );
250                 return $ret if $ret;
251         }
252         return undef;
253 }
254
255 sub _get_copy_status {
256         my( $copy, $cstatus ) = @_;
257         $U->logmark;
258         my $s = undef;
259         for my $status (@$cstatus) {
260                 $s = $status if( $status->id eq $copy->status ) 
261         }
262         $logger->debug("Retrieving copy status: " . $s->name) if $s;
263         return $s;
264 }
265
266 sub _get_copy_location {
267         my( $copy, $locations ) = @_;
268         $U->logmark;
269         my $l = undef;
270         for my $loc (@$locations) {
271                 $l = $loc if $loc->id eq $copy->location;
272         }
273         $logger->debug("Retrieving copy location: " . $l->name ) if $l;
274         return $l;
275 }
276
277
278 # ------------------------------------------------------------------------------
279 # Constructs and shoves data into the script environment
280 # ------------------------------------------------------------------------------
281 sub _build_circ_script_runner {
282         my $ctx = shift;
283         $U->logmark;
284
285         $logger->debug("Loading script environment for circulation");
286
287         my $runner;
288         if( $runner = $contexts{$ctx->{type}} ) {
289                 $runner->refresh_context;
290         } else {
291                 $runner = OpenILS::Utils::ScriptRunner->new;
292                 $contexts{type} = $runner;
293         }
294
295         for(@$script_libs) {
296                 $logger->debug("Loading circ script lib path $_");
297                 $runner->add_path( $_ );
298         }
299
300         # Note: inserting the number 0 into the script turns into the
301         # string "0", and thus evaluates to true in JS land
302         # inserting undef will insert "", which evaluates to false
303
304         $runner->insert( 'environment.patron',  $ctx->{patron}, 1);
305         $runner->insert( 'environment.title',   $ctx->{title}, 1);
306         $runner->insert( 'environment.copy',    $ctx->{copy}, 1);
307
308         # circ script result
309         $runner->insert( 'result', {} );
310         #$runner->insert( 'result.event', 'SUCCESS' );
311         $runner->insert( 'result.events', [] );
312
313         if($__isrenewal) {
314                 $runner->insert('environment.isRenewal', 1);
315         } else {
316                 $runner->insert('environment.isRenewal', undef);
317         }
318
319         if($ctx->{ishold} ) { 
320                 $runner->insert('environment.isHold', 1); 
321         } else{ 
322                 $runner->insert('environment.isHold', undef) 
323         }
324
325         if( $ctx->{noncat} ) {
326                 $runner->insert('environment.isNonCat', 1);
327                 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
328         } else {
329                 $runner->insert('environment.isNonCat', undef);
330         }
331
332 #       if(ref($ctx->{patron_circ_summary})) {
333 #               $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
334 #               $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
335 #       }
336
337         $ctx->{runner} = $runner;
338         return $runner;
339 }
340
341
342 sub _add_script_runner_methods {
343         my $ctx = shift;
344         $U->logmark;
345         my $runner = $ctx->{runner};
346
347         if( $ctx->{copy} ) {
348                 
349                 # allows a script to fetch a hold that is currently targeting the
350                 # copy in question
351                 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
352                                 my $key = shift;
353                                 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
354                                 $hold = undef unless $hold;
355                                 $runner->insert( $key, $hold, 1 );
356                         }
357                 );
358         }
359 }
360
361 # ------------------------------------------------------------------------------
362
363 __PACKAGE__->register_method(
364         method  => "permit_circ",
365         api_name        => "open-ils.circ.checkout.permit",
366         notes           => q/
367                 Determines if the given checkout can occur
368                 @param authtoken The login session key
369                 @param params A trailing hash of named params including 
370                         barcode : The copy barcode, 
371                         patron : The patron the checkout is occurring for, 
372                         renew : true or false - whether or not this is a renewal
373                 @return The event that occurred during the permit check.  
374         /);
375
376 __PACKAGE__->register_method (
377         method          => 'permit_circ',
378         api_name                => 'open-ils.circ.checkout.permit.override',
379         signature       => q/@see open-ils.circ.checkout.permit/,
380 );
381
382 sub permit_circ {
383         my( $self, $client, $authtoken, $params ) = @_;
384         $U->logmark;
385
386         my $override = $params->{override} = 1 if $self->api_name =~ /override/o;
387
388         my ( $requestor, $patron, $ctx, $evt, $circ );
389
390         # check permisson of the requestor
391         ( $requestor, $patron, $evt ) = 
392                 $U->checkses_requestor( 
393                 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
394         return $evt if $evt;
395
396         # fetch and build the circulation environment
397         if( !( $ctx = $params->{_ctx}) ) {
398
399                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
400                         patron                                                  => $patron, 
401                         requestor                                               => $requestor, 
402                         type                                                            => 'circ',
403                         #fetch_patron_circ_summary      => 1,
404                         fetch_copy_statuses                     => 1, 
405                         fetch_copy_locations                    => 1, 
406                         );
407                 return $evt if $evt;
408         }
409
410         $ctx->{authtoken} = $authtoken;
411
412         $evt = undef;
413         if( $ctx->{copy} and ($evt = _handle_claims_returned($ctx)) ) {
414                 return $evt unless $U->event_equals($evt, 'SUCCESS');
415         }
416
417         if($evt) { 
418                 $evt = undef;
419
420         } else { 
421
422                 # no claims returned circ was found, check if there is any open circ
423                 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
424                         ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
425                         return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
426                 }
427         }
428
429
430         $ctx->{permit_key} = _cache_permit_key();
431         my $events = _run_permit_scripts($ctx);
432
433         if( $override ) {
434                 $evt = override_events($requestor, $requestor->ws_ou, $events);
435                 return $evt if $evt;
436                 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
437         }
438
439         return $events;
440 }
441
442 sub override_events {
443
444         my( $requestor, $org, $events ) = @_;
445         $events = [ $events ] unless ref($events) eq 'ARRAY';
446         my @failed;
447
448         for my $e (@$events) {
449                 my $tc = $e->{textcode};
450                 next if $tc eq 'SUCCESS';
451                 my $ov = "$tc.override";
452                 $logger->info("attempting to override event $ov");
453                 my $evt = $U->check_perms( $requestor->id, $org, $ov );
454                 return $evt if $evt;
455         }
456
457         return undef;
458 }
459
460
461 __PACKAGE__->register_method(
462         method  => "check_title_hold",
463         api_name        => "open-ils.circ.title_hold.is_possible",
464         notes           => q/
465                 Determines if a hold were to be placed by a given user,
466                 whether or not said hold would have any potential copies
467                 to fulfill it.
468                 @param authtoken The login session key
469                 @param params A hash of named params including:
470                         patronid  - the id of the hold recipient
471                         titleid (brn) - the id of the title to be held
472                         depth   - the hold range depth (defaults to 0)
473         /);
474
475 # XXX add pickup lib to the call to test for perms
476
477 sub check_title_hold {
478         my( $self, $client, $authtoken, $params ) = @_;
479         my %params = %$params;
480         my $titleid = $params{titleid};
481
482         my ( $requestor, $patron, $evt ) = $U->checkses_requestor( 
483                 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
484         return $evt if $evt;
485
486         my $rangelib    = $patron->home_ou;
487         my $depth               = $params{depth} || 0;
488
489         $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
490
491         my $org = $U->simplereq(
492                 'open-ils.actor', 
493                 'open-ils.actor.org_unit.retrieve', 
494                 $authtoken, $requestor->home_ou );
495
496         my $limit       = 10;
497         my $offset      = 0;
498         my $title;
499
500         while( $title = $U->storagereq(
501                                 'open-ils.storage.biblio.record_entry.ranged_tree', 
502                                 $titleid, $rangelib, $depth, $limit, $offset ) ) {
503
504                 last unless ref($title);
505
506                 for my $cn (@{$title->call_numbers}) {
507         
508                         $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
509         
510                         for my $copy (@{$cn->copies}) {
511         
512                                 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
513         
514                                 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
515                                         {       patron                          => $patron, 
516                                                 requestor                       => $requestor, 
517                                                 copy                                    => $copy,
518                                                 title                                   => $title, 
519                                                 title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
520                                                 request_lib                     => $org } );
521         
522                                 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
523                         }
524                 }
525
526                 $offset += $limit;
527         }
528
529         return 0;
530 }
531
532
533 # Runs the patron and copy permit scripts
534 # if this is a non-cat circulation, the copy permit script 
535 # is not run
536 sub _run_permit_scripts {
537
538         my $ctx                 = shift;
539         my $runner              = $ctx->{runner};
540         my $patronid    = $ctx->{patron}->id;
541         my $barcode             = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
542         my $key                 = $ctx->{permit_key};
543
544         my $penalties = $U->update_patron_penalties( 
545                 authtoken => $ctx->{authtoken}, 
546                 patron    => $ctx->{patron} 
547         );
548
549         $penalties = $penalties->{fatal_penalties};
550
551         $logger->info("circ patron penalties user $patronid: @$penalties");
552
553         if( $ctx->{noncat} ) {
554                 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
555                 return OpenILS::Event->new('SUCCESS', payload => $key);
556         }
557
558         if($ctx->{precat}) {
559                 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
560                 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
561         }
562
563         if($ctx->{ishold}) {
564                 $logger->debug("Exiting circ permit early because request is for hold patron permit");
565                 return OpenILS::Event->new('SUCCESS');
566         }
567
568         $runner->load($scripts{circ_permit_copy});
569         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
570
571         # ---------------------------------------------------------------------
572         # Capture all of the copy permit events
573         # ---------------------------------------------------------------------
574         my $copy_events = $runner->retrieve('result.events');
575         $copy_events = [ split(/,/, $copy_events) ]; 
576         $ctx->{circ_permit_copy_events} = $copy_events;
577         $logger->activity("circ_permit_copy for copy ".
578                 "$barcode returned events: @$copy_events") if @$copy_events;
579
580         my @allevents;
581         push( @allevents, OpenILS::Event->new($_)) for @$penalties;
582         push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
583
584         my $ae = _check_copy_alert($ctx->{copy});
585         push( @allevents, $ae ) if $ae;
586
587         return OpenILS::Event->new('SUCCESS', payload => $key) unless (@allevents);
588
589         # uniquify the events
590         my %hash = map { ($_->{ilsevent} => $_) } @allevents;
591         @allevents = values %hash;
592
593         for (@allevents) {
594                 $_->{payload} = $ctx->{copy}->status->id
595                         if ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
596         }
597
598         return \@allevents;
599 }
600
601 sub _check_copy_alert {
602         my $copy = shift;
603         return OpenILS::Event->new('COPY_ALERT_MESSAGE', 
604                 payload => $copy->alert_message) if $copy->alert_message;
605         return undef;
606 }
607
608 # takes copyid, patronid, and requestor id
609 sub _cache_permit_key {
610         my $key = md5_hex( time() . rand() . "$$" );
611         $logger->debug("Setting circ permit key to $key");
612         $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
613         return $key;
614 }
615
616 sub _check_permit_key {
617         my $key = shift;
618         $logger->debug("Fetching circ permit key $key");
619         my $k = "oils_permit_key_$key";
620         my $one = $cache_handle->get_cache($k);
621         $cache_handle->delete_cache($k);
622         return ($one) ? 1 : 0;
623 }
624
625
626 # ------------------------------------------------------------------------------
627
628 __PACKAGE__->register_method(
629         method  => "checkout",
630         api_name        => "open-ils.circ.checkout",
631         notes => q/
632                 Checks out an item
633                 @param authtoken The login session key
634                 @param params A named hash of params including:
635                         copy                    The copy object
636                         barcode         If no copy is provided, the copy is retrieved via barcode
637                         copyid          If no copy or barcode is provide, the copy id will be use
638                         patron          The patron's id
639                         noncat          True if this is a circulation for a non-cataloted item
640                         noncat_type     The non-cataloged type id
641                         noncat_circ_lib The location for the noncat circ.  
642                         precat          The item has yet to be cataloged
643                         dummy_title The temporary title of the pre-cataloded item
644                         dummy_author The temporary authr of the pre-cataloded item
645                                 Default is the home org of the staff member
646                 @return The SUCCESS event on success, any other event depending on the error
647         /);
648
649 sub checkout {
650         my( $self, $client, $authtoken, $params ) = @_;
651         $U->logmark;
652
653         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
654         my $key = $params->{permit_key};
655
656         # if this is a renewal, then the requestor does not have to
657         # have checkout privelages
658         ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
659         ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
660         return $evt if $evt;
661
662         if( $params->{patron} ) {
663                 ( $patron, $evt ) = $U->fetch_user($params->{patron});
664                 return $evt if $evt;
665         } else {
666                 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
667                 return $evt if $evt;
668         }
669
670         # set the circ lib to the home org of the requestor if not specified
671         my $circlib = (defined($params->{circ_lib})) ? 
672                 $params->{circ_lib} : $requestor->ws_ou;
673
674
675         # Make sure the caller has a valid permit key or is 
676         # overriding the permit can
677         if( $params->{permit_override} ) {
678                 $evt = $U->check_perms(
679                         $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
680                 return $evt if $evt;
681
682         } else {
683                 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY') 
684                         unless _check_permit_key($key);
685         }
686
687         # if this is a non-cataloged item, check it out and return
688         return _checkout_noncat( 
689                 $key, $requestor, $patron, %$params ) if $params->{noncat};
690
691         # if this item has yet to be cataloged, make sure a dummy copy exists
692         ( $params->{copy}, $evt ) = _make_precat_copy(
693                 $requestor, $circlib, $params ) if $params->{precat};
694         return $evt if $evt;
695
696
697         # fetch and build the circulation environment
698         if( !( $ctx = $params->{_ctx}) ) {
699                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
700                         patron                                                  => $patron, 
701                         requestor                                               => $requestor, 
702                         session                                                 => $U->start_db_session(),
703                         type                                                            => 'circ',
704                         #fetch_patron_circ_summary      => 1,
705                         fetch_copy_statuses                     => 1, 
706                         fetch_copy_locations                    => 1, 
707                         );
708                 return $evt if $evt;
709         }
710         $ctx->{session} = $U->start_db_session() unless $ctx->{session};
711
712         # if the call doesn't know it's not cataloged..
713         if(!$params->{precat}) {
714                 if( $ctx->{copy}->call_number eq '-1' ) {
715                         return OpenILS::Event->new('ITEM_NOT_CATALOGED');
716                 }
717         }
718
719         # this happens in permit.. but we need to check here for 'offline' requests
720         ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
721         return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
722
723         my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
724
725
726         $ctx->{circ_lib} = $circlib;
727
728         $evt = _run_checkout_scripts($ctx);
729         return $evt if $evt;
730
731
732         _build_checkout_circ_object($ctx);
733
734         $evt = _apply_modified_due_date($ctx);
735         return $evt if $evt;
736
737         $evt = _commit_checkout_circ_object($ctx);
738         return $evt if $evt;
739
740         $evt = _update_checkout_copy($ctx);
741         return $evt if $evt;
742
743         my $holds;
744         ($holds, $evt) = _handle_related_holds($ctx);
745         return $evt if $evt;
746
747
748         $logger->debug("Checkout committing objects with session thread trace: ".$ctx->{session}->session_id);
749         $U->commit_db_session($ctx->{session});
750         my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
751
752         $logger->activity("user ".$requestor->id." successfully checked out item ".
753                 $ctx->{copy}->barcode." to user ".$ctx->{patron}->id );
754
755
756         # ------------------------------------------------------------------------------
757         # Update the patron penalty info in the DB
758         # ------------------------------------------------------------------------------
759         $U->update_patron_penalties( 
760                 authtoken => $authtoken, 
761                 patron    => $ctx->{patron} ,
762                 background      => 1,
763         );
764
765         return OpenILS::Event->new('SUCCESS', 
766                 payload => { 
767                         copy                                    => $U->unflesh_copy($ctx->{copy}),
768                         circ                                    => $ctx->{circ},
769                         record                          => $record,
770                         holds_fulfilled => $holds,
771                 } 
772         )
773 }
774
775
776 sub _make_precat_copy {
777         my ( $requestor, $circlib, $params ) =  @_;
778         $U->logmark;
779         my( $copy, undef ) = _find_copy_by_attr(%$params);
780
781         if($copy) {
782                 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
783
784                 $copy->editor($requestor->id);
785                 $copy->edit_date('now');
786                 $copy->dummy_title($params->{dummy_title});
787                 $copy->dummy_author($params->{dummy_author});
788
789                 my $stat = $U->storagereq(
790                         'open-ils.storage.direct.asset.copy.update', $copy );
791
792                 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $stat;
793                 return ($copy);
794         }
795
796         $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
797
798         my $evt = OpenILS::Event->new(
799                 'BAD_PARAMS', desc => "Dummy title or author not provided" ) 
800                 unless ( $params->{dummy_title} and $params->{dummy_author} );
801         return (undef, $evt) if $evt;
802
803         $copy = Fieldmapper::asset::copy->new;
804         $copy->circ_lib($circlib);
805         $copy->creator($requestor->id);
806         $copy->editor($requestor->id);
807         $copy->barcode($params->{barcode});
808         $copy->call_number(-1); #special CN for precat materials
809         $copy->loan_duration(&PRECAT_LOAN_DURATION); 
810         $copy->fine_level(&PRECAT_FINE_LEVEL);
811
812         $copy->dummy_title($params->{dummy_title});
813         $copy->dummy_author($params->{dummy_author});
814
815         my $id = $U->storagereq(
816                 'open-ils.storage.direct.asset.copy.create', $copy );
817         return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
818
819         $logger->debug("Pre-cataloged copy successfully created");
820         return ($U->fetch_copy($id));
821 }
822
823
824 sub _run_checkout_scripts {
825         my $ctx = shift;
826         $U->logmark;
827         my $evt;
828         my $circ;
829
830         my $runner = $ctx->{runner};
831
832         $runner->insert('result.durationLevel');
833         $runner->insert('result.durationRule');
834         $runner->insert('result.recurringFinesRule');
835         $runner->insert('result.recurringFinesLevel');
836         $runner->insert('result.maxFine');
837
838         $runner->load($scripts{circ_duration});
839         $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
840         my $duration = $runner->retrieve('result.durationRule');
841         $logger->debug("Circ duration script yielded a duration rule of: $duration");
842
843         $runner->load($scripts{circ_recurring_fines});
844         $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
845         my $recurring = $runner->retrieve('result.recurringFinesRule');
846         $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
847
848         $runner->load($scripts{circ_max_fines});
849         $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
850         my $max_fine = $runner->retrieve('result.maxFine');
851         $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
852
853         ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
854         return $evt if $evt;
855         ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
856         return $evt if $evt;
857         ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
858         return $evt if $evt;
859
860         $ctx->{duration_level}                  = $runner->retrieve('result.durationLevel');
861         $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
862         $ctx->{duration_rule}                   = $duration;
863         $ctx->{recurring_fines_rule}    = $recurring;
864         $ctx->{max_fine_rule}                   = $max_fine;
865
866         return undef;
867 }
868
869 sub _build_checkout_circ_object {
870         my $ctx = shift;
871         $U->logmark;
872
873         my $circ                        = new Fieldmapper::action::circulation;
874         my $duration    = $ctx->{duration_rule};
875         my $max                 = $ctx->{max_fine_rule};
876         my $recurring   = $ctx->{recurring_fines_rule};
877         my $copy                        = $ctx->{copy};
878         my $patron              = $ctx->{patron};
879         my $dur_level   = $ctx->{duration_level};
880         my $rec_level   = $ctx->{recurring_fines_level};
881
882         $circ->duration( $duration->shrt ) if ($dur_level == 1);
883         $circ->duration( $duration->normal ) if ($dur_level == 2);
884         $circ->duration( $duration->extended ) if ($dur_level == 3);
885
886         $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
887         $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
888         $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
889
890         $circ->duration_rule( $duration->name );
891         $circ->recuring_fine_rule( $recurring->name );
892         $circ->max_fine_rule( $max->name );
893         $circ->max_fine( $max->amount );
894
895         $circ->fine_interval($recurring->recurance_interval);
896         $circ->renewal_remaining( $duration->max_renewals );
897         $circ->target_copy( $copy->id );
898         $circ->usr( $patron->id );
899         $circ->circ_lib( $ctx->{circ_lib} );
900
901         if( $__isrenewal ) {
902                 $logger->debug("Circ is a renewal.  Setting renewal_remaining to " . $ctx->{renewal_remaining} );
903                 $circ->opac_renewal(1); 
904                 $circ->renewal_remaining($ctx->{renewal_remaining});
905                 $circ->circ_staff($ctx->{requestor}->id);
906         } 
907
908
909         # if the user provided an overiding checkout time, 
910         # (e.g. the checkout really happened several hours ago), then
911         # we apply that here.  Does this need a perm??
912         if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
913                 $logger->debug("circ setting checkout_time to $ds");
914                 $circ->xact_start($ds);
915         }
916
917         # if a patron is renewing, 'requestor' will be the patron
918         $circ->circ_staff($ctx->{requestor}->id ); 
919         _set_circ_due_date($circ);
920         $ctx->{circ} = $circ;
921 }
922
923 sub _apply_modified_due_date {
924         my $ctx = shift;
925         my $circ = $ctx->{circ};
926
927         if( $ctx->{due_date} ) {
928
929                 my $evt = $U->check_perms(
930                         $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
931                 return $evt if $evt;
932
933                 my $ds = _create_date_stamp($ctx->{due_date});
934                 $logger->debug("circ modifying  due_date to $ds");
935                 $circ->due_date($ds);
936
937         }
938         return undef;
939 }
940
941 sub _create_date_stamp {
942         my $datestring = shift;
943         return undef unless $datestring;
944         $datestring = clense_ISO8601($datestring);
945         $logger->debug("circ created date stamp => $datestring");
946         return $datestring;
947 }
948
949 sub _create_due_date {
950         my $duration = shift;
951         $U->logmark;
952         my ($sec,$min,$hour,$mday,$mon,$year) = 
953                 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
954         $year += 1900; $mon += 1;
955         my $due_date = sprintf(
956         '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
957         $year, $mon, $mday, $hour, $min, $sec);
958         return $due_date;
959 }
960
961 sub _set_circ_due_date {
962         my $circ = shift;
963         $U->logmark;
964         my $dd = _create_due_date($circ->duration);
965         $logger->debug("Checkout setting due date on circ to: $dd");
966         $circ->due_date($dd);
967 }
968
969 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
970 sub _update_checkout_copy {
971         my $ctx = shift;
972         $U->logmark;
973         my $copy = $ctx->{copy};
974
975         my $s = $U->copy_status_from_name('checked out');
976         $copy->status( $s->id ) if $s;
977
978         my $evt = $U->update_copy( session => $ctx->{session}, 
979                 copy => $copy, editor => $ctx->{requestor}->id );
980         return (undef,$evt) if $evt;
981
982         return undef;
983 }
984
985 # commits the circ object to the db then fleshes the circ with rules objects
986 sub _commit_checkout_circ_object {
987
988         my $ctx = shift;
989         my $circ = $ctx->{circ};
990         $U->logmark;
991
992         $circ->clear_id;
993         my $r = $ctx->{session}->request(
994                 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
995
996         return $U->DB_UPDATE_FAILED($circ) unless $r;
997
998         $logger->debug("Created a new circ object in checkout: $r");
999
1000         $circ->id($r);
1001         $circ->duration_rule($ctx->{duration_rule});
1002         $circ->max_fine_rule($ctx->{max_fine_rule});
1003         $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
1004
1005         return undef;
1006 }
1007
1008
1009 # sees if there are any holds that this copy 
1010 sub _handle_related_holds {
1011
1012         my $ctx         = shift;
1013         my $copy                = $ctx->{copy};
1014         my $patron      = $ctx->{patron};
1015         my $holds       = $holdcode->fetch_related_holds($copy->id);
1016         $U->logmark;
1017         my @fulfilled;
1018
1019         # XXX We should only fulfill one hold here...
1020         # XXX If a hold was transited to the user who is checking out
1021         # the item, we need to make sure that hold is what's grabbed
1022         if(ref($holds) && @$holds) {
1023
1024                 # for now, just sort by id to get what should be the oldest hold
1025                 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1026                 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
1027
1028                 if(@$holds) {
1029                         my $hold = $holds->[0];
1030
1031                         $logger->debug("Related hold found in checkout: " . $hold->id );
1032
1033                         $hold->current_copy($copy->id); # just make sure it's set
1034                         # if the hold was never officially captured, capture it.
1035                         $hold->capture_time('now') unless $hold->capture_time;
1036                         $hold->fulfillment_time('now');
1037                         my $r = $ctx->{session}->request(
1038                                 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1039                         return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1040                         push( @fulfilled, $hold->id );
1041                 }
1042         }
1043
1044         return (\@fulfilled, undef);
1045 }
1046
1047 sub _checkout_noncat {
1048         my ( $key, $requestor, $patron, %params ) = @_;
1049         my( $circ, $circlib, $evt );
1050         $U->logmark;
1051
1052         $circlib = $params{noncat_circ_lib} || $requestor->ws_ou;
1053
1054         my $count = $params{noncat_count} || 1;
1055         my $cotime = _create_date_stamp($params{checkout_time}) || "";
1056         $logger->info("circ creating $count noncat circs with checkout time $cotime");
1057         for(1..$count) {
1058                 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1059                         $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1060                 return $evt if $evt;
1061         }
1062
1063         return OpenILS::Event->new( 
1064                 'SUCCESS', payload => { noncat_circ => $circ } );
1065 }
1066
1067
1068 __PACKAGE__->register_method(
1069         method  => "generic_receive",
1070         api_name        => "open-ils.circ.checkin",
1071         argc            => 2,
1072         signature       => q/
1073                 Generic super-method for handling all copies
1074                 @param authtoken The login session key
1075                 @param params Hash of named parameters including:
1076                         barcode - The copy barcode
1077                         force           - If true, copies in bad statuses will be checked in and give good statuses
1078                         ...
1079         /
1080 );
1081
1082 __PACKAGE__->register_method(
1083         method  => "generic_receive",
1084         api_name        => "open-ils.circ.checkin.override",
1085         signature       => q/@see open-ils.circ.checkin/
1086 );
1087
1088 sub generic_receive {
1089         my( $self, $conn, $authtoken, $params ) = @_;
1090         my( $ctx, $requestor, $evt );
1091
1092         ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1093         ( $requestor, $evt ) = $U->checksesperm( 
1094                 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1095         return $evt if $evt;
1096
1097         # load up the circ objects
1098         if( !( $ctx = $params->{_ctx}) ) {
1099                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
1100                         requestor                                               => $requestor, 
1101                         session                                                 => $U->start_db_session(),
1102                         type                                                            => 'circ',
1103                         fetch_copy_statuses                     => 1, 
1104                         fetch_copy_locations                    => 1, 
1105                         no_runner                                               => 1,  
1106                         );
1107                 return $evt if $evt;
1108         }
1109         $ctx->{override} = 1 if $self->api_name =~ /override/o;
1110         $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1111         $ctx->{authtoken} = $authtoken;
1112         my $session = $ctx->{session};
1113
1114         my $copy = $ctx->{copy};
1115         $U->unflesh_copy($copy);
1116         return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $copy;
1117
1118         $logger->info("Checkin copy called by user ".
1119                 $requestor->id." for copy ".$copy->id);
1120
1121         # ------------------------------------------------------------------------------
1122         # Update the patron penalty info in the DB
1123         # ------------------------------------------------------------------------------
1124         $U->update_patron_penalties( 
1125                 authtoken => $authtoken, 
1126                 patron    => $ctx->{patron},
1127                 background => 1
1128         );
1129
1130         return $self->checkin_do_receive($conn, $ctx);
1131 }
1132
1133 sub checkin_do_receive {
1134
1135         my( $self, $connection, $ctx ) = @_;
1136
1137         my $evt;
1138         my $copy                        = $ctx->{copy};
1139         my $session             = $ctx->{session};
1140         my $requestor   = $ctx->{requestor};
1141         my $change              = 0; # did we actually do anything?
1142         my $circ;
1143
1144         my @eventlist;
1145
1146         # does the copy have an attached alert message?
1147         my $ae = _check_copy_alert($copy);
1148         push(@eventlist, $ae) if $ae;
1149
1150         # is the copy is an a status we can't automatically resolve?
1151         $evt = _checkin_check_copy_status($ctx);
1152         push( @eventlist, $evt ) if $evt;
1153
1154
1155         # - see if the copy has an open circ attached
1156         ($ctx->{circ}, $evt)    = $U->fetch_open_circulation($copy->id);
1157         return $evt if ($evt and $__isrenewal); # renewals require a circulation
1158         $evt = undef;
1159         $circ = $ctx->{circ};
1160
1161         # if the circ is marked as 'claims returned', add the event to the list
1162         push( @eventlist, 'CIRC_CLAIMS_RETURNED' ) 
1163                 if ($circ and $circ->stop_fines eq 'CLAIMSRETURNED');
1164
1165         # override or die
1166         if(@eventlist) {
1167                 if($ctx->{override}) {
1168                         $evt = override_events($requestor, $requestor->ws_ou, \@eventlist );
1169                         return $evt if $evt;
1170                 } else {
1171                         return \@eventlist;
1172                 }
1173         }
1174
1175         ($ctx->{transit})       = $U->fetch_open_transit_by_copy($copy->id);
1176
1177         if( $ctx->{circ} ) {
1178
1179                 # There is an open circ on this item, close it out.
1180                 $change = 1;
1181                 $evt            = _checkin_handle_circ($ctx);
1182                 return $evt if $evt;
1183
1184         } elsif( $ctx->{transit} ) {
1185
1186                 # is this item currently in transit?
1187                 $change                 = 1;
1188                 $evt                            = $transcode->transit_receive( $copy, $requestor, $session );
1189                 my $holdtrans   = $evt->{holdtransit};
1190                 ($ctx->{hold})  = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1191
1192                 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1193
1194                         # either an error occurred or a ROUTE_ITEM was generated and the 
1195                         # item must be forwarded on to its destination.
1196                         return _checkin_flesh_event($ctx, $evt);
1197
1198                 } else {
1199
1200                         # Transit has been closed, now let's see if the copy's original
1201                         # status is something the staff should be warned of
1202                         my $e = _checkin_check_copy_status($ctx);
1203                         $evt = $e if $e;
1204
1205                         if($holdtrans) {
1206
1207                                 # copy was received as a hold transit.  Copy is at target lib
1208                                 # and hold transit is complete.  We're done here...
1209                                 $U->commit_db_session($session);
1210                                 return _checkin_flesh_event($ctx, $evt);
1211                         }
1212                         $evt = undef;
1213                 }
1214         }
1215
1216         # ------------------------------------------------------------------------------
1217         # Circulations and transits are now closed where necessary.  Now go on to see if
1218         # this copy can fulfill a hold or needs to be routed to a different location
1219         # ------------------------------------------------------------------------------
1220
1221
1222         # If it's a renewal, we're done
1223         if($__isrenewal) {
1224                 $U->commit_db_session($session);
1225                 return OpenILS::Event->new('SUCCESS');
1226         }
1227
1228         # Now, let's see if this copy is needed for a hold
1229         my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor ); 
1230
1231         if($hold) {
1232
1233                 $ctx->{hold}    = $hold;
1234                 $change                 = 1;
1235                 
1236                 # Capture the hold with this copy
1237                 return $evt if ($evt = _checkin_capture_hold($ctx));
1238
1239                 if( $hold->pickup_lib == $requestor->ws_ou ) {
1240
1241                         # This hold was captured in the correct location
1242                         $evt = OpenILS::Event->new('SUCCESS');
1243
1244                 } else {
1245
1246                         # Hold needs to be picked up elsewhere.  Build a hold 
1247                         # transit and route the item.
1248                         return $evt if ($evt =_checkin_build_hold_transit($ctx));
1249                         $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1250                 }
1251
1252         } else { # not needed for a hold
1253
1254                 if( $copy->circ_lib == $requestor->ws_ou ) {
1255
1256                         # Copy is in the right place.
1257                         $evt = OpenILS::Event->new('SUCCESS');
1258
1259                         # if the item happens to be a pre-cataloged item, send it
1260                         # to cataloging and return the event
1261                         my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1262                         return $err if $err;
1263                         $change         = 1 if $c;
1264                         $evt                    = $e if $e;
1265
1266                 } else {
1267
1268                         # Copy wants to go home. Transit it there.
1269                         return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1270                         $evt                    = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1271                         $change         = 1;
1272                 }
1273         }
1274
1275
1276         # ------------------------------------------------------------------
1277         # if the copy is not in a state that should persist,
1278         # set the copy to reshelving if it's not already there
1279         # ------------------------------------------------------------------
1280         my ($c, $e) = _reshelve_copy($ctx);
1281         return $e if $e;
1282         $change = $c unless $change;
1283
1284         if(!$change) {
1285
1286                 $evt = OpenILS::Event->new('NO_CHANGE');
1287                 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id) 
1288                         if( $copy->status == $U->copy_status_from_name('on holds shelf')->id );
1289
1290         } else {
1291
1292                 $U->commit_db_session($session);
1293         }
1294
1295         $logger->activity("checkin by user ".$requestor->id." on item ".
1296                 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1297
1298         return _checkin_flesh_event($ctx, $evt);
1299 }
1300
1301 sub _reshelve_copy {
1302
1303         my $ctx = shift;
1304         my $copy                = $ctx->{copy};
1305         my $reqr                = $ctx->{requestor};
1306         my $session     = $ctx->{session};
1307
1308         my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1309
1310         if($stat != $U->copy_status_from_name('on holds shelf')->id and 
1311                 $stat != $U->copy_status_from_name('available')->id and 
1312                 $stat != $U->copy_status_from_name('cataloging')->id and 
1313                 $stat != $U->copy_status_from_name('in transit')->id and 
1314                 $stat != $U->copy_status_from_name('reshelving')->id ) {
1315
1316                 $copy->status( $U->copy_status_from_name('reshelving')->id );
1317
1318                 my $evt = $U->update_copy( 
1319                         copy            => $copy,
1320                         editor  => $reqr->id,
1321                         session => $session,
1322                         );
1323
1324                 return( 1, $evt );
1325         }
1326         return undef;
1327 }
1328
1329
1330
1331
1332 # returns undef if there are no 'open' claims-returned circs attached
1333 # to the given copy.  if there is an open claims-returned circ, 
1334 # then we check for override mode.  if in override, mark the claims-returned
1335 # circ as checked in.  if not, return event.
1336 sub _handle_claims_returned {
1337         my $ctx = shift;
1338         my $copy = $ctx->{copy};
1339
1340         my $CR  = _fetch_open_claims_returned($copy->id);
1341         return undef unless $CR;
1342
1343         # - If the caller has set the override flag, we will check the item in
1344         if($ctx->{override}) {
1345
1346                 $CR->checkin_time('now');       
1347                 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1348                 $CR->checkin_staff($ctx->{requestor}->id);
1349
1350                 my $stat = $U->storagereq(
1351                         'open-ils.storage.direct.action.circulation.update', $CR);
1352                 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1353                 return OpenILS::Event->new('SUCCESS');
1354
1355         } else {
1356                 # - if not in override mode, return the CR event
1357                 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1358         }
1359 }
1360
1361
1362 sub _fetch_open_claims_returned {
1363         my $copyid = shift;
1364         my $trans = $U->storagereq(
1365                 'open-ils.storage.direct.action.circulation.search_where',
1366                 {       
1367                         target_copy             => $copyid, 
1368                         stop_fines              => 'CLAIMSRETURNED',
1369                         checkin_time    => undef,
1370                 }
1371         );
1372         return $$trans[0] if $trans && $$trans[0];
1373         return undef;
1374 }
1375
1376 # - if the copy is has the 'in process' status, set it to reshelving
1377 #sub _check_in_process {
1378         #my $ctx = shift;
1379 #
1380         #my $copy = $ctx->{copy};
1381         #my $reqr       = $ctx->{requestor};
1382         #my $ses        = $ctx->{session};
1383 ##
1384         #my $stat = $U->copy_status_from_name('in process');
1385         #my $rstat = $U->copy_status_from_name('reshelving');
1386 #
1387         #if( $stat->id == $copy->status->id ) {
1388                 #$logger->info("marking 'in-process' copy ".$copy->id." as 'reshelving'");
1389                 #$copy->status( $rstat->id );
1390                 #my $evt = $U->update_copy( 
1391                         #copy           => $copy,
1392                         #editor => $reqr->id,
1393                         #session        => $ses
1394                         #);
1395                 #return $evt if $evt;
1396 #
1397                 #$copy->status( $rstat ); # - reflesh the copy status
1398         #}
1399         #return undef;
1400 #}
1401
1402
1403 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1404 sub _checkin_handle_precat {
1405
1406         my $ctx         = shift;
1407         my $copy                = $ctx->{copy};
1408         my $evt         = undef;
1409         my $errevt      = undef;
1410         my $change      = 0;
1411
1412         my $catstat = $U->copy_status_from_name('cataloging');
1413
1414         if( $ctx->{precat} ) {
1415
1416                 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1417
1418                 if( $copy->status != $catstat->id ) {
1419                         $copy->status($catstat->id);
1420
1421                         return (undef, 0, $errevt) if (
1422                                 $errevt = $U->update_copy(
1423                                         copy            => $copy, 
1424                                         editor  => $ctx->{requestor}->id, 
1425                                         session => $ctx->{session} ));
1426                         $change = 1;
1427
1428                 }
1429         }
1430
1431         return ($evt, $change, undef);
1432 }
1433
1434
1435 # returns the appropriate event for the given copy status
1436 # if the copy is not in a 'special' status, undef is returned
1437 sub _checkin_check_copy_status {
1438         my $ctx = shift;
1439         my $copy = $ctx->{copy};
1440         my $reqr        = $ctx->{requestor};
1441         my $ses = $ctx->{session};
1442
1443         my $islost              = 0;
1444         my $ismissing   = 0;
1445         my $evt                 = undef;
1446
1447         my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1448
1449         return undef 
1450                 if(     $status == $U->copy_status_from_name('available')->id           ||
1451                                 $status == $U->copy_status_from_name('checked out')->id ||
1452                                 $status == $U->copy_status_from_name('in process')->id  ||
1453                                 $status == $U->copy_status_from_name('in transit')->id  ||
1454                                 $status == $U->copy_status_from_name('reshelving')->id );
1455
1456         return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy ) 
1457                 if( $status == $U->copy_status_from_name('lost')->id );
1458
1459         return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy ) 
1460                 if( $status == $U->copy_status_from_name('missing')->id );
1461
1462         return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1463
1464
1465
1466 #       my $rstat = $U->copy_status_from_name('reshelving');
1467 #       my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1468 #
1469 #       if( $stat == $U->copy_status_from_name('lost')->id ) {
1470 #               $islost = 1;
1471 #               $evt = OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy );
1472 #
1473 #       } elsif( $stat == $U->copy_status_from_name('missing')->id) {
1474 #               $ismissing = 1;
1475 #               $evt = OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy );
1476 #       }
1477 #
1478 #       return (undef,$evt) if(!$ctx->{override});
1479 #
1480 #       # we're are now going to attempt to override the failure 
1481 #       # and set the copy to reshelving
1482 #       my $e;
1483 #       my $copyid = $copy->id;
1484 #       my $userid = $reqr->id;
1485 #       if( $islost ) {
1486 #
1487 #               # - make sure we have permission
1488 #               $e = $U->check_perms( $reqr->id, 
1489 #                       $copy->circ_lib, 'COPY_STATUS_LOST.override');
1490 #               return (undef,$e) if $e;
1491 #               $copy->status( $rstat->id );
1492 #
1493 #               # XXX if no fines are owed in the circ, close it out - will this happen later anyway?
1494 #               #my $circ = $U->storagereq(
1495 #               #       'open-ils.storage.direct.action.circulation
1496 #
1497 #               $logger->activity("user $userid overriding 'lost' copy status for copy $copyid");
1498 #
1499 #       } elsif( $ismissing ) {
1500 #
1501 #               # - make sure we have permission
1502 #               $e = $U->check_perms( $reqr->id, 
1503 #                       $copy->circ_lib, 'COPY_STATUS_MISSING.override');
1504 #               return (undef,$e) if $e;
1505 #               $copy->status( $rstat->id );
1506 #               $logger->activity("user $userid overriding 'missing' copy status for copy $copyid");
1507 #       }
1508 #
1509 #       if( $islost or $ismissing ) {
1510 #
1511 #               # - update the copy with the new status
1512 #               $evt = $U->update_copy(
1513 #                       copy            => $copy,
1514 #                       editor  => $reqr->id,
1515 #                       session => $ses
1516 #               );
1517 #               return (undef,$evt) if $evt;
1518 #               $copy->status( $rstat );
1519 #       }
1520 #
1521 #       return (1);
1522
1523
1524 }
1525
1526 # Just gets the copy back home.  Returns undef on success, event on error
1527 sub _checkin_build_generic_copy_transit {
1528
1529         my $ctx                 = shift;
1530         my $requestor   = $ctx->{requestor};
1531         my $copy                        = $ctx->{copy};
1532         my $transit             = Fieldmapper::action::transit_copy->new;
1533         my $session             = $ctx->{session};
1534
1535         $logger->activity("User ". $requestor->id ." creating a ".
1536                 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1537
1538         $transit->source($requestor->ws_ou);
1539         $transit->dest($copy->circ_lib);
1540         $transit->target_copy($copy->id);
1541         $transit->source_send_time('now');
1542         $transit->copy_status($copy->status);
1543         
1544         $logger->debug("Creating new copy_transit in DB");
1545
1546         my $s = $session->request(
1547                 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1548         return $U->DB_UPDATE_FAILED($transit) unless $s;
1549
1550         $logger->info("Checkin copy successfully created new transit: $s");
1551
1552         $copy->status($U->copy_status_from_name('in transit')->id );
1553
1554         return $U->update_copy( copy => $copy, 
1555                         editor => $requestor->id, session => $session );
1556         
1557 }
1558
1559
1560 # returns event on error, undef on success
1561 sub _checkin_build_hold_transit {
1562         my $ctx = shift;
1563
1564         my $copy = $ctx->{copy};
1565         my $hold = $ctx->{hold};
1566         my $trans = Fieldmapper::action::hold_transit_copy->new;
1567
1568         $trans->hold($hold->id);
1569         $trans->source($ctx->{requestor}->ws_ou);
1570         $trans->dest($hold->pickup_lib);
1571         $trans->source_send_time("now");
1572         $trans->target_copy($copy->id);
1573         $trans->copy_status($copy->status);
1574
1575         my $id = $ctx->{session}->request(
1576                 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1577         return $U->DB_UPDATE_FAILED($trans) unless $id;
1578
1579         $logger->info("Checkin copy successfully created hold transit: $id");
1580
1581         $copy->status($U->copy_status_from_name('in transit')->id );
1582         return $U->update_copy( copy => $copy, 
1583                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1584 }
1585
1586 # Returns event on error, undef on success
1587 sub _checkin_capture_hold {
1588         my $ctx = shift;
1589         my $copy = $ctx->{copy};
1590         my $hold = $ctx->{hold}; 
1591
1592         $logger->debug("Checkin copy capturing hold ".$hold->id);
1593
1594         $hold->current_copy($copy->id);
1595         $hold->capture_time('now'); 
1596
1597         my $stat = $ctx->{session}->request(
1598                 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1599         return $U->DB_UPDATE_FAILED($hold) unless $stat;
1600
1601         $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1602
1603         return $U->update_copy( copy => $copy, 
1604                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1605 }
1606
1607 # fleshes an event with the relevant objects from the context
1608 sub _checkin_flesh_event {
1609         my $ctx = shift;
1610         my $evt = shift;
1611
1612         my $payload                             = {};
1613         $payload->{copy}                = $U->unflesh_copy($ctx->{copy});
1614         $payload->{record}      = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1615         $payload->{circ}                = $ctx->{circ} if $ctx->{circ};
1616         $payload->{transit}     = $ctx->{transit} if $ctx->{transit};
1617         $payload->{hold}                = $ctx->{hold} if $ctx->{hold};
1618
1619         $evt->{payload} = $payload;
1620         return $evt;
1621 }
1622
1623
1624 # Closes out the circulation, puts the copy into reshelving.
1625 # Voids any bills attached to this circ after the backdate time 
1626 # if a backdate is provided
1627 sub _checkin_handle_circ { 
1628
1629         my $ctx = shift;
1630
1631         my $circ = $ctx->{circ};
1632         my $copy = $ctx->{copy};
1633         my $requestor   = $ctx->{requestor};
1634         my $session             = $ctx->{session};
1635         my $evt;
1636         my $obt;
1637
1638         $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1639
1640         #$ctx->{longoverdue}            = 1 if ($circ->stop_fines =~ /longoverdue/io);
1641         #$ctx->{claimsreturned} = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1642
1643         # backdate the circ if necessary
1644         if(my $backdate = $ctx->{backdate}) {
1645                 return $evt if ($evt = 
1646                         _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1647         }
1648
1649
1650         if(!$circ->stop_fines) {
1651                 $circ->stop_fines('CHECKIN');
1652                 $circ->stop_fines('RENEW') if $__isrenewal;
1653                 $circ->stop_fines_time('now');
1654         }
1655
1656         # see if there are any fines owed on this circ.  if not, close it
1657         ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1658         return $evt if $evt;
1659         $circ->xact_finish('now') if( $obt->balance_owed <= 0 );
1660
1661         # Set the checkin vars since we have the item
1662         $circ->checkin_time('now');
1663         $circ->checkin_staff($requestor->id);
1664         $circ->checkin_lib($requestor->ws_ou);
1665
1666
1667 #       $copy->status($U->copy_status_from_name('reshelving')->id);
1668 #       $evt = $U->update_copy( session => $session, 
1669 #               copy => $copy, editor => $requestor->id );
1670 #       return $evt if $evt;
1671
1672         $ctx->{session}->request(
1673                 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1674
1675         return undef;
1676 }
1677
1678 sub _set_copy_reshelving {
1679         my( $copy, $reqr, $session ) = @_;
1680
1681         $logger->info("Setting copy ".$copy->id." to reshelving");
1682         $copy->status($U->copy_status_from_name('reshelving')->id);
1683
1684         my $evt = $U->update_copy( 
1685                 session => $session, 
1686                 copy            => $copy, 
1687                 editor  => $reqr
1688                 );
1689         return $evt if $evt;
1690 }
1691
1692 # returns event on error, undef on success
1693 # This voids all bills attached to the given circulation that occurred
1694 # after the backdate 
1695 # THIS DOES NOT CLOSE THE CIRC if there are no more fines on the item
1696 sub _checkin_handle_backdate {
1697         my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1698
1699         $logger->activity("User ".$requestor->id.
1700                 " backdating circ [".$circ->target_copy."] to date: $backdate");
1701
1702         my $bills = $session->request( # XXX Verify this call is correct
1703                 "open-ils.storage.direct.money.billing.search_where.atomic",
1704                 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1705
1706         if($bills) {
1707                 for my $bill (@$bills) {
1708                         $bill->voided('t');
1709                         my $s = $session->request(
1710                                 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1711                         return $U->DB_UPDATE_FAILED($bill) unless $s;
1712                 }
1713         }
1714
1715         # if the caller elects to attempt to close the circulation
1716         # transaction, then it will be closed if there are not further
1717         # charges on the transaction
1718         #if( $closecirc ) {
1719                 #my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1720            #return $evt if $evt;
1721                 #$circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1722         #}
1723
1724         return undef;
1725 }
1726
1727
1728 sub _find_patron_from_params {
1729         my $params = shift;
1730
1731         my $patron;
1732         my $copy;
1733         my $circ;
1734         my $evt;
1735
1736         if(my $barcode = $params->{barcode}) {
1737                 $logger->debug("circ finding user from params with barcode $barcode");
1738                 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1739                 return (undef, undef, $evt) if $evt;
1740                 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1741                 return (undef, undef, $evt) if $evt;
1742                 ($patron, $evt) = $U->fetch_user($circ->usr);
1743                 return (undef, undef, $evt) if $evt;
1744         }
1745         return ($patron, $copy);
1746 }
1747
1748
1749 # ------------------------------------------------------------------------------
1750
1751 __PACKAGE__->register_method(
1752         method  => "renew",
1753         api_name        => "open-ils.circ.renew.override",
1754         signature       => q/@see open-ils.circ.renew/,
1755 );
1756
1757
1758 __PACKAGE__->register_method(
1759         method  => "renew",
1760         api_name        => "open-ils.circ.renew",
1761         notes           => <<"  NOTES");
1762         PARAMS( authtoken, circ => circ_id );
1763         open-ils.circ.renew(login_session, circ_object);
1764         Renews the provided circulation.  login_session is the requestor of the
1765         renewal and if the logged in user is not the same as circ->usr, then
1766         the logged in user must have RENEW_CIRC permissions.
1767         NOTES
1768
1769 sub renew {
1770         my( $self, $client, $authtoken, $params ) = @_;
1771         $U->logmark;
1772
1773         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1774         $__isrenewal = 1;
1775
1776         $params->{override} = 1 if $self->api_name =~ /override/o;
1777
1778         # fetch the patron object one way or another
1779         if( $params->{patron} ) {
1780                 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1781                 if($evt) { $__isrenewal = 0; return $evt; }
1782
1783         } elsif( $params->{patron_barcode} ) {
1784                 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1785                 if($evt) { $__isrenewal = 0; return $evt; }
1786
1787         } else {
1788                 ($patron, $copy, $evt) = _find_patron_from_params($params);
1789                 return $evt if $evt;
1790                 $params->{copy} = $copy;
1791         }
1792
1793         # verify our login session
1794         ($requestor, $evt) = $U->checkses($authtoken);
1795         if($evt) { $__isrenewal = 0; return $evt; }
1796
1797         # make sure we have permission to perform a renewal
1798         if( $requestor->id ne $patron->id ) {
1799                 $evt = $U->check_perms($requestor->id, $requestor->ws_ou, 'RENEW_CIRC');
1800                 if($evt) { $__isrenewal = 0; return $evt; }
1801         }
1802
1803
1804         # fetch and build the circulation environment
1805         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1806                 patron                                                  => $patron, 
1807                 requestor                                               => $requestor, 
1808                 patron                                                  => $patron, 
1809                 type                                                            => 'circ',
1810                 #fetch_patron_circ_summary      => 1,
1811                 fetch_copy_statuses                     => 1, 
1812                 fetch_copy_locations                    => 1, 
1813                 );
1814         if($evt) { $__isrenewal = 0; return $evt; }
1815         $params->{_ctx} = $ctx;
1816
1817         # make sure they have some renewals left and make sure the circulation exists
1818         ($circ, $evt) = _check_renewal_remaining($ctx);
1819         if($evt) { $__isrenewal = 0; return $evt; }
1820         $ctx->{old_circ} = $circ;
1821         my $renewals = $circ->renewal_remaining - 1;
1822
1823         # run the renew permit script
1824         $evt = _run_renew_scripts($ctx);
1825         if($evt) { $__isrenewal = 0; return $evt; }
1826
1827         # checkin the cop
1828         #$ctx->{patron} = $ctx->{patron}->id;
1829         $evt = $self->generic_receive($client, $authtoken, $ctx );
1830                 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1831
1832         if( !$U->event_equals($evt, 'SUCCESS') ) {
1833                 $__isrenewal = 0; return $evt; 
1834         }
1835
1836         # re-fetch the context since objects have changed in the checkin
1837         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1838                 patron                                                  => $patron, 
1839                 requestor                                               => $requestor, 
1840                 patron                                                  => $patron, 
1841                 type                                                            => 'circ',
1842                 #fetch_patron_circ_summary      => 1,
1843                 fetch_copy_statuses                     => 1, 
1844                 fetch_copy_locations                    => 1, 
1845                 );
1846         if($evt) { $__isrenewal = 0; return $evt; }
1847         $params->{_ctx} = $ctx;
1848         $ctx->{renewal_remaining} = $renewals;
1849
1850         # run the circ permit scripts
1851         if( $ctx->{permit_override} ) {
1852                 $evt = $U->check_perms(
1853                         $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1854                 if($evt) { $__isrenewal = 0; return $evt; }
1855
1856         } else {
1857                 $evt = $self->permit_circ( $client, $authtoken, $params );
1858                 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1859                         #$ctx->{precat} = 1;
1860                         $params->{precat} = 1;
1861
1862                 } else {
1863                         if(!$U->event_equals($evt, 'SUCCESS')) {
1864                                 if($evt) { $__isrenewal = 0; return $evt; }
1865                         }
1866                 }
1867                 $params->{permit_key} = $evt->{payload};
1868         }
1869
1870
1871         # checkout the item again
1872         $params->{patron} = $ctx->{patron}->id;
1873         $evt = $self->checkout($client, $authtoken, $params );
1874
1875         $logger->activity("user ".$requestor->id." renewl of item ".
1876                 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1877
1878         $__isrenewal = 0;
1879         return $evt;
1880 }
1881
1882 sub _check_renewal_remaining {
1883         my $ctx = shift;
1884         $U->logmark;
1885         my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1886         return (undef, $evt) if $evt;
1887         $evt = OpenILS::Event->new(
1888                 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1889         return ($circ, $evt);
1890 }
1891
1892 sub _run_renew_scripts {
1893         my $ctx = shift;
1894         my $runner = $ctx->{runner};
1895         $U->logmark;
1896
1897         $runner->load($scripts{circ_permit_renew});
1898         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1899
1900         my $events = $runner->retrieve('result.events');
1901         $events = [ split(/,/, $events) ]; 
1902         $logger->activity("circ_permit_renew for user ".
1903                 $ctx->{patron}->id." returned events: @$events") if @$events;
1904
1905         my @allevents;
1906         push( @allevents, OpenILS::Event->new($_)) for @$events;
1907         return \@allevents if  @allevents;
1908
1909         return undef;
1910 }
1911
1912         
1913
1914
1915 1;
1916