]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
added backdate support for claims_returned
[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 Digest::MD5 qw(md5_hex);
8 use OpenILS::Utils::ScriptRunner;
9 use OpenILS::Application::AppUtils;
10 use OpenILS::Application::Circ::Holds;
11 use OpenILS::Application::Circ::Transit;
12 use OpenILS::Utils::PermitHold;
13 use OpenSRF::Utils::Logger qw(:logger);
14 use DateTime;
15 use DateTime::Format::ISO8601;
16 use OpenSRF::Utils qw/:datetime/;
17
18 $Data::Dumper::Indent = 0;
19 my $apputils    = "OpenILS::Application::AppUtils";
20 my $U                           = $apputils;
21 my $holdcode    = "OpenILS::Application::Circ::Holds";
22 my $transcode   = "OpenILS::Application::Circ::Transit";
23
24 my %scripts;                    # - circulation script filenames
25 my $script_libs;                # - any additional script libraries
26 my %cache;                              # - db objects cache
27 my %contexts;                   # - Script runner contexts
28 my $cache_handle;               # - memcache handle
29
30 sub PRECAT_FINE_LEVEL { return 2; }
31 sub PRECAT_LOAN_DURATION { return 2; }
32
33 my %RECORD_FROM_COPY_CACHE;
34
35
36 # for security, this is a process-defined and not
37 # a client-defined variable
38 my $__isrenewal = 0;
39 my $__islost            = 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 '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         if(!defined($cache{patron_standings})) {
119                 $cache{patron_standings} = $U->fetch_patron_standings();
120                 $cache{group_tree} = $U->fetch_permission_group_tree();
121         }
122
123         $ctx->{patron_standings} = $cache{patron_standings};
124         $ctx->{group_tree} = $cache{group_tree};
125
126         $ctx->{patron_circ_summary} = 
127                 $U->fetch_patron_circ_summary($ctx->{patron}->id) 
128                 if $params{fetch_patron_circsummary};
129
130         return undef;
131 }
132
133
134 sub _find_copy_by_attr {
135         my %params = @_;
136         $U->logmark;
137         my $evt;
138
139         my $copy = $params{copy} || undef;
140
141         if(!$copy) {
142
143                 ( $copy, $evt ) = 
144                         $U->fetch_copy($params{copyid}) if $params{copyid};
145                 return (undef,$evt) if $evt;
146
147                 if(!$copy) {
148                         ( $copy, $evt ) = 
149                                 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
150                         return (undef,$evt) if $evt;
151                 }
152         }
153         return ( $copy, $evt );
154 }
155
156 sub _ctx_add_copy_objects {
157         my($ctx, %params)  = @_;
158         $U->logmark;
159         my $evt;
160         my $copy;
161
162         $cache{copy_statuses} = $U->fetch_copy_statuses 
163                 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
164
165         $cache{copy_locations} = $U->fetch_copy_locations 
166                 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
167
168         $ctx->{copy_statuses} = $cache{copy_statuses};
169         $ctx->{copy_locations} = $cache{copy_locations};
170
171         ($copy, $evt) = _find_copy_by_attr(%params);
172         return $evt if $evt;
173
174         if( $copy and !$ctx->{title} ) {
175                 $logger->debug("Copy status: " . $copy->status);
176
177                 my $r = $RECORD_FROM_COPY_CACHE{$copy->id};
178                 ($r, $evt) = $U->fetch_record_by_copy( $copy->id ) unless $r;
179                 return $evt if $evt;
180                 $RECORD_FROM_COPY_CACHE{$copy->id} = $r;
181
182                 $ctx->{title} = $r;
183                 $ctx->{copy} = $copy;
184         }
185
186         return undef;
187 }
188
189
190 # ------------------------------------------------------------------------------
191 # Fleshes parts of the patron object
192 # ------------------------------------------------------------------------------
193 sub _doctor_copy_object {
194         my $ctx = shift;
195         $U->logmark;
196         my $copy = $ctx->{copy} || return undef;
197
198         $logger->debug("Doctoring copy object...");
199
200         # set the copy status to a status name
201         $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
202
203         # set the copy location to the location object
204         $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
205
206         $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
207 }
208
209
210 # ------------------------------------------------------------------------------
211 # Fleshes parts of the patron object
212 # ------------------------------------------------------------------------------
213 sub _doctor_patron_object {
214         my $ctx = shift;
215         $U->logmark;
216         my $patron = $ctx->{patron} || return undef;
217
218         # push the standing object into the patron
219         if(ref($ctx->{patron_standings})) {
220                 for my $s (@{$ctx->{patron_standings}}) {
221                         if( $s->id eq $ctx->{patron}->standing ) {
222                                 $patron->standing($s);
223                                 $logger->debug("Set patron standing to ". $s->value);
224                         }
225                 }
226         }
227
228         # set the patron ptofile to the profile name
229         $patron->profile( _get_patron_profile( 
230                 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
231
232         # flesh the org unit
233         $patron->home_ou( 
234                 $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
235
236 }
237
238 # recurse and find the patron profile name from the tree
239 # another option would be to grab the groups for the patron
240 # and cycle through those until the "profile" group has been found
241 sub _get_patron_profile { 
242         my( $patron, $group_tree ) = @_;
243         return $group_tree if ($group_tree->id eq $patron->profile);
244         return undef unless ($group_tree->children);
245
246         for my $child (@{$group_tree->children}) {
247                 my $ret = _get_patron_profile( $patron, $child );
248                 return $ret if $ret;
249         }
250         return undef;
251 }
252
253 sub _get_copy_status {
254         my( $copy, $cstatus ) = @_;
255         $U->logmark;
256         my $s = undef;
257         for my $status (@$cstatus) {
258                 $s = $status if( $status->id eq $copy->status ) 
259         }
260         $logger->debug("Retrieving copy status: " . $s->name) if $s;
261         return $s;
262 }
263
264 sub _get_copy_location {
265         my( $copy, $locations ) = @_;
266         $U->logmark;
267         my $l = undef;
268         for my $loc (@$locations) {
269                 $l = $loc if $loc->id eq $copy->location;
270         }
271         $logger->debug("Retrieving copy location: " . $l->name ) if $l;
272         return $l;
273 }
274
275
276 # ------------------------------------------------------------------------------
277 # Constructs and shoves data into the script environment
278 # ------------------------------------------------------------------------------
279 sub _build_circ_script_runner {
280         my $ctx = shift;
281         $U->logmark;
282
283         $logger->debug("Loading script environment for circulation");
284
285         my $runner;
286         if( $runner = $contexts{$ctx->{type}} ) {
287                 $runner->refresh_context;
288         } else {
289                 $runner = OpenILS::Utils::ScriptRunner->new;
290                 $contexts{type} = $runner;
291         }
292
293         for(@$script_libs) {
294                 $logger->debug("Loading circ script lib path $_");
295                 $runner->add_path( $_ );
296         }
297
298         # Note: inserting the number 0 into the script turns into the
299         # string "0", and thus evaluates to true in JS land
300         # inserting undef will insert "", which evaluates to false
301
302         $runner->insert( 'environment.patron',  $ctx->{patron}, 1);
303         $runner->insert( 'environment.title',   $ctx->{title}, 1);
304         $runner->insert( 'environment.copy',    $ctx->{copy}, 1);
305
306         # circ script result
307         $runner->insert( 'result', {} );
308         #$runner->insert( 'result.event', 'SUCCESS' );
309         $runner->insert( 'result.events', [] );
310
311         if($__isrenewal) {
312                 $runner->insert('environment.isRenewal', 1);
313         } else {
314                 $runner->insert('environment.isRenewal', undef);
315         }
316
317         if($ctx->{ishold} ) { 
318                 $runner->insert('environment.isHold', 1); 
319         } else{ 
320                 $runner->insert('environment.isHold', undef) 
321         }
322
323         if( $ctx->{noncat} ) {
324                 $runner->insert('environment.isNonCat', 1);
325                 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
326         } else {
327                 $runner->insert('environment.isNonCat', undef);
328         }
329
330         if(ref($ctx->{patron_circ_summary})) {
331                 $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
332                 $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
333         }
334
335         $ctx->{runner} = $runner;
336         return $runner;
337 }
338
339
340 sub _add_script_runner_methods {
341         my $ctx = shift;
342         $U->logmark;
343         my $runner = $ctx->{runner};
344
345         if( $ctx->{copy} ) {
346                 
347                 # allows a script to fetch a hold that is currently targeting the
348                 # copy in question
349                 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
350                                 my $key = shift;
351                                 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
352                                 $hold = undef unless $hold;
353                                 $runner->insert( $key, $hold, 1 );
354                         }
355                 );
356         }
357 }
358
359 # ------------------------------------------------------------------------------
360
361 __PACKAGE__->register_method(
362         method  => "permit_circ",
363         api_name        => "open-ils.circ.checkout.permit",
364         notes           => q/
365                 Determines if the given checkout can occur
366                 @param authtoken The login session key
367                 @param params A trailing hash of named params including 
368                         barcode : The copy barcode, 
369                         patron : The patron the checkout is occurring for, 
370                         renew : true or false - whether or not this is a renewal
371                 @return The event that occurred during the permit check.  
372         /);
373
374 sub permit_circ {
375         my( $self, $client, $authtoken, $params ) = @_;
376         $U->logmark;
377
378         my ( $requestor, $patron, $ctx, $evt, $circ );
379
380         # check permisson of the requestor
381         ( $requestor, $patron, $evt ) = 
382                 $U->checkses_requestor( 
383                 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
384         return $evt if $evt;
385
386         # fetch and build the circulation environment
387         if( !( $ctx = $params->{_ctx}) ) {
388
389                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
390                         patron                                                  => $patron, 
391                         requestor                                               => $requestor, 
392                         type                                                            => 'circ',
393                         fetch_patron_circ_summary       => 1,
394                         fetch_copy_statuses                     => 1, 
395                         fetch_copy_locations                    => 1, 
396                         );
397                 return $evt if $evt;
398         }
399
400         if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
401                 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
402                 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
403         }
404
405         return _run_permit_scripts($ctx);
406 }
407
408
409 __PACKAGE__->register_method(
410         method  => "check_title_hold",
411         api_name        => "open-ils.circ.title_hold.is_possible",
412         notes           => q/
413                 Determines if a hold were to be placed by a given user,
414                 whether or not said hold would have any potential copies
415                 to fulfill it.
416                 @param authtoken The login session key
417                 @param params A hash of named params including:
418                         patronid  - the id of the hold recipient
419                         titleid (brn) - the id of the title to be held
420                         depth   - the hold range depth (defaults to 0)
421         /);
422
423 sub check_title_hold {
424         my( $self, $client, $authtoken, $params ) = @_;
425         my %params = %$params;
426         my $titleid = $params{titleid};
427
428         my ( $requestor, $patron, $evt ) = $U->checkses_requestor( 
429                 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
430         return $evt if $evt;
431
432         my $rangelib    = $patron->home_ou;
433         my $depth               = $params{depth} || 0;
434
435         $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
436
437         my $org = $U->simplereq(
438                 'open-ils.actor', 
439                 'open-ils.actor.org_unit.retrieve', 
440                 $authtoken, $requestor->home_ou );
441
442         my $limit       = 10;
443         my $offset      = 0;
444         my $title;
445
446         while( $title = $U->storagereq(
447                                 'open-ils.storage.biblio.record_entry.ranged_tree', 
448                                 $titleid, $rangelib, $depth, $limit, $offset ) ) {
449
450                 last unless ref($title);
451
452                 for my $cn (@{$title->call_numbers}) {
453         
454                         $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
455         
456                         for my $copy (@{$cn->copies}) {
457         
458                                 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
459         
460                                 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
461                                         {       patron                          => $patron, 
462                                                 requestor                       => $requestor, 
463                                                 copy                                    => $copy,
464                                                 title                                   => $title, 
465                                                 title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
466                                                 request_lib                     => $org } );
467         
468                                 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
469                         }
470                 }
471
472                 $offset += $limit;
473         }
474
475         return 0;
476 }
477
478
479
480 # Runs the patron and copy permit scripts
481 # if this is a non-cat circulation, the copy permit script 
482 # is not run
483 sub _run_permit_scripts {
484         my $ctx                 = shift;
485         my $runner              = $ctx->{runner};
486         my $patronid    = $ctx->{patron}->id;
487         my $barcode             = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
488         $U->logmark;
489
490         $runner->load($scripts{circ_permit_patron});
491         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
492
493         #my $evtname = $runner->retrieve('result.event');
494
495         # ---------------------------------------------------------------------
496         # Capture all of the patron permit events
497         # ---------------------------------------------------------------------
498         my $patron_events = $runner->retrieve('result.events');
499         $patron_events = [ split(/,/, $patron_events) ]; 
500         #$ctx->{circ_permit_patron_events} = $patron_events;
501
502         #$logger->activity("circ_permit_patron for user $patronid returned event: $evtname");
503         $logger->activity("circ_permit_patron for user $patronid returned events: @$patron_events");
504
505         #return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';
506
507         my $key = _cache_permit_key();
508
509         if( $ctx->{noncat} ) {
510                 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
511                 return OpenILS::Event->new('SUCCESS', payload => $key);
512         }
513
514         if($ctx->{precat}) {
515                 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
516                 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
517         }
518
519         if($ctx->{ishold}) {
520                 $logger->debug("Exiting circ permit early because request is for hold patron permit");
521                 return OpenILS::Event->new('SUCCESS');
522         }
523
524         $runner->load($scripts{circ_permit_copy});
525         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
526         #$evtname = $runner->retrieve('result.event');
527         #$logger->activity("circ_permit_copy for user $patronid ".
528                 #"and copy $barcode returned event: $evtname");
529
530         # ---------------------------------------------------------------------
531         # Capture all of the copy permit events
532         # ---------------------------------------------------------------------
533         my $copy_events = $runner->retrieve('result.events');
534         $copy_events = [ split(/,/, $copy_events) ]; 
535         $ctx->{circ_permit_copy_events} = $copy_events;
536         $logger->activity("circ_permit_copy for copy $barcode returned events: @$copy_events");
537
538         #return OpenILS::Event->new($evtname, payload => $key) if( $evtname eq 'SUCCESS' );
539         my @allevents;
540         push( @allevents, OpenILS::Event->new($_)) for @$patron_events;
541         push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
542
543         return OpenILS::Event->new('SUCCESS', payload => $key) 
544                 unless (@$copy_events or @$patron_events);
545
546         # uniquify the events
547         my %hash = map { ($_->{ilsevent} => $_) } @allevents;
548         @allevents = values %hash;
549         return \@allevents;
550 }
551
552 # takes copyid, patronid, and requestor id
553 sub _cache_permit_key {
554         my $key = md5_hex( time() . rand() . "$$" );
555         $logger->debug("Setting circ permit key to $key");
556         $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
557         return $key;
558 }
559
560 sub _check_permit_key {
561         my $key = shift;
562         $logger->debug("Fetching circ permit key $key");
563         my $k = "oils_permit_key_$key";
564         my $one = $cache_handle->get_cache($k);
565         $cache_handle->delete_cache($k);
566         return ($one) ? 1 : 0;
567 }
568
569
570 # ------------------------------------------------------------------------------
571
572 __PACKAGE__->register_method(
573         method  => "checkout",
574         api_name        => "open-ils.circ.checkout",
575         notes => q/
576                 Checks out an item
577                 @param authtoken The login session key
578                 @param params A named hash of params including:
579                         copy                    The copy object
580                         barcode         If no copy is provided, the copy is retrieved via barcode
581                         copyid          If no copy or barcode is provide, the copy id will be use
582                         patron          The patron's id
583                         noncat          True if this is a circulation for a non-cataloted item
584                         noncat_type     The non-cataloged type id
585                         noncat_circ_lib The location for the noncat circ.  
586                         precat          The item has yet to be cataloged
587                         dummy_title The temporary title of the pre-cataloded item
588                         dummy_author The temporary authr of the pre-cataloded item
589                                 Default is the home org of the staff member
590                 @return The SUCCESS event on success, any other event depending on the error
591         /);
592
593 sub checkout {
594         my( $self, $client, $authtoken, $params ) = @_;
595         $U->logmark;
596
597         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
598         my $key = $params->{permit_key};
599
600         # if this is a renewal, then the requestor does not have to
601         # have checkout privelages
602         ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
603         ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
604         return $evt if $evt;
605
606         if( $params->{patron} ) {
607                 ( $patron, $evt ) = $U->fetch_user($params->{patron});
608                 return $evt if $evt;
609         } else {
610                 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
611                 return $evt if $evt;
612         }
613
614         # set the circ lib to the home org of the requestor if not specified
615         my $circlib = (defined($params->{circ_lib})) ? 
616                 $params->{circ_lib} : $requestor->home_ou;
617
618
619         # Make sure the caller has a valid permit key or is 
620         # overriding the permit can
621         if( $params->{permit_override} ) {
622                 $evt = $U->check_perms(
623                         $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
624                 return $evt if $evt;
625
626         } else {
627                 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY') 
628                         unless _check_permit_key($key);
629         }
630
631         # if this is a non-cataloged item, check it out and return
632         return _checkout_noncat( 
633                 $key, $requestor, $patron, %$params ) if $params->{noncat};
634
635         # if this item has yet to be cataloged, make sure a dummy copy exists
636         ( $params->{copy}, $evt ) = _make_precat_copy(
637                 $requestor, $circlib, $params ) if $params->{precat};
638         return $evt if $evt;
639
640
641         # fetch and build the circulation environment
642         if( !( $ctx = $params->{_ctx}) ) {
643                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
644                         patron                                                  => $patron, 
645                         requestor                                               => $requestor, 
646                         session                                                 => $U->start_db_session(),
647                         type                                                            => 'circ',
648                         fetch_patron_circ_summary       => 1,
649                         fetch_copy_statuses                     => 1, 
650                         fetch_copy_locations                    => 1, 
651                         );
652                 return $evt if $evt;
653         }
654         $ctx->{session} = $U->start_db_session() unless $ctx->{session};
655
656         # if the call doesn't know it's not cataloged..
657         if(!$params->{precat}) {
658                 if( $ctx->{copy}->call_number eq '-1' ) {
659                         return OpenILS::Event->new('ITEM_NOT_CATALOGED');
660                 }
661         }
662
663         # this happens in permit.. but we need to check here for 'offline' requests
664         ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
665         return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
666
667         my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
668
669
670         $ctx->{circ_lib} = $circlib;
671
672         $evt = _run_checkout_scripts($ctx);
673         return $evt if $evt;
674
675
676         _build_checkout_circ_object($ctx);
677
678         $evt = _apply_modified_due_date($ctx);
679         return $evt if $evt;
680
681         $evt = _commit_checkout_circ_object($ctx);
682         return $evt if $evt;
683
684         $evt = _update_checkout_copy($ctx);
685         return $evt if $evt;
686
687         my $holds;
688         ($holds, $evt) = _handle_related_holds($ctx);
689         return $evt if $evt;
690
691
692         $logger->debug("Checkin committing objects with session thread trace: ".$ctx->{session}->session_id);
693         $U->commit_db_session($ctx->{session});
694         my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
695
696         return OpenILS::Event->new('SUCCESS', 
697                 payload => { 
698                         copy                                    => $U->unflesh_copy($ctx->{copy}),
699                         circ                                    => $ctx->{circ},
700                         record                          => $record,
701                         holds_fulfilled => $holds,
702                 } );
703 }
704
705
706 sub _make_precat_copy {
707         my ( $requestor, $circlib, $params ) =  @_;
708         $U->logmark;
709         my( $copy, undef ) = _find_copy_by_attr(%$params);
710
711         if($copy) {
712                 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
713                 return ($copy, undef);
714         }
715
716         $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
717
718         my $evt = OpenILS::Event->new(
719                 'BAD_PARAMS', desc => "Dummy title or author not provided" ) 
720                 unless ( $params->{dummy_title} and $params->{dummy_author} );
721         return (undef, $evt) if $evt;
722
723         $copy = Fieldmapper::asset::copy->new;
724         $copy->circ_lib($circlib);
725         $copy->creator($requestor->id);
726         $copy->editor($requestor->id);
727         $copy->barcode($params->{barcode});
728         $copy->call_number(-1); #special CN for precat materials
729         $copy->loan_duration(&PRECAT_LOAN_DURATION); 
730         $copy->fine_level(&PRECAT_FINE_LEVEL);
731
732         $copy->dummy_title($params->{dummy_title});
733         $copy->dummy_author($params->{dummy_author});
734
735         my $id = $U->storagereq(
736                 'open-ils.storage.direct.asset.copy.create', $copy );
737         return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
738
739         $logger->debug("Pre-cataloged copy successfully created");
740         return $U->fetch_copy($id);
741 }
742
743
744 sub _run_checkout_scripts {
745         my $ctx = shift;
746         $U->logmark;
747         my $evt;
748         my $circ;
749
750         my $runner = $ctx->{runner};
751
752         $runner->insert('result.durationLevel');
753         $runner->insert('result.durationRule');
754         $runner->insert('result.recurringFinesRule');
755         $runner->insert('result.recurringFinesLevel');
756         $runner->insert('result.maxFine');
757
758         $runner->load($scripts{circ_duration});
759         $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
760         my $duration = $runner->retrieve('result.durationRule');
761         $logger->debug("Circ duration script yielded a duration rule of: $duration");
762
763         $runner->load($scripts{circ_recurring_fines});
764         $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
765         my $recurring = $runner->retrieve('result.recurringFinesRule');
766         $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
767
768         $runner->load($scripts{circ_max_fines});
769         $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
770         my $max_fine = $runner->retrieve('result.maxFine');
771         $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
772
773         ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
774         return $evt if $evt;
775         ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
776         return $evt if $evt;
777         ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
778         return $evt if $evt;
779
780         $ctx->{duration_level}                  = $runner->retrieve('result.durationLevel');
781         $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
782         $ctx->{duration_rule}                   = $duration;
783         $ctx->{recurring_fines_rule}    = $recurring;
784         $ctx->{max_fine_rule}                   = $max_fine;
785
786         return undef;
787 }
788
789 sub _build_checkout_circ_object {
790         my $ctx = shift;
791         $U->logmark;
792
793         my $circ                        = new Fieldmapper::action::circulation;
794         my $duration    = $ctx->{duration_rule};
795         my $max                 = $ctx->{max_fine_rule};
796         my $recurring   = $ctx->{recurring_fines_rule};
797         my $copy                        = $ctx->{copy};
798         my $patron              = $ctx->{patron};
799         my $dur_level   = $ctx->{duration_level};
800         my $rec_level   = $ctx->{recurring_fines_level};
801
802         $circ->duration( $duration->shrt ) if ($dur_level == 1);
803         $circ->duration( $duration->normal ) if ($dur_level == 2);
804         $circ->duration( $duration->extended ) if ($dur_level == 3);
805
806         $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
807         $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
808         $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
809
810         $circ->duration_rule( $duration->name );
811         $circ->recuring_fine_rule( $recurring->name );
812         $circ->max_fine_rule( $max->name );
813         $circ->max_fine( $max->amount );
814
815         $circ->fine_interval($recurring->recurance_interval);
816         $circ->renewal_remaining( $duration->max_renewals );
817         $circ->target_copy( $copy->id );
818         $circ->usr( $patron->id );
819         $circ->circ_lib( $ctx->{circ_lib} );
820
821         if( $__isrenewal ) {
822                 $logger->debug("Circ is a renewal.  Setting renewal_remaining to " . $ctx->{renewal_remaining} );
823                 $circ->opac_renewal(1); 
824                 $circ->renewal_remaining($ctx->{renewal_remaining});
825                 $circ->circ_staff($ctx->{requestor}->id);
826         } 
827
828
829         # if the user provided an overiding checkout time, 
830         # (e.g. the checkout really happened several hours ago), then
831         # we apply that here.  Does this need a perm??
832         if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
833                 $logger->debug("circ setting checkout_time to $ds");
834                 $circ->xact_start($ds);
835         }
836
837         # if a patron is renewing, 'requestor' will be the patron
838         $circ->circ_staff($ctx->{requestor}->id ); 
839         _set_circ_due_date($circ);
840         $ctx->{circ} = $circ;
841 }
842
843 sub _apply_modified_due_date {
844         my $ctx = shift;
845         my $circ = $ctx->{circ};
846
847         if( $ctx->{due_date} ) {
848
849                 my $evt = $U->check_perms(
850                         $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
851                 return $evt if $evt;
852
853                 my $ds = _create_date_stamp($ctx->{due_date});
854                 $logger->debug("circ modifying  due_date to $ds");
855                 $circ->due_date($ds);
856
857         }
858         return undef;
859 }
860
861 sub _create_date_stamp {
862         my $datestring = shift;
863         return undef unless $datestring;
864         $datestring = clense_ISO8601($datestring);
865         $logger->debug("circ created date stamp => $datestring");
866         return $datestring;
867 }
868
869 sub _create_due_date {
870         my $duration = shift;
871         $U->logmark;
872         my ($sec,$min,$hour,$mday,$mon,$year) = 
873                 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
874         $year += 1900; $mon += 1;
875         my $due_date = sprintf(
876         '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
877         $year, $mon, $mday, $hour, $min, $sec);
878         return $due_date;
879 }
880
881 sub _set_circ_due_date {
882         my $circ = shift;
883         $U->logmark;
884         my $dd = _create_due_date($circ->duration);
885         $logger->debug("Checkout setting due date on circ to: $dd");
886         $circ->due_date($dd);
887 }
888
889 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
890 sub _update_checkout_copy {
891         my $ctx = shift;
892         $U->logmark;
893         my $copy = $ctx->{copy};
894
895         my $s = $U->copy_status_from_name('checked out');
896         $copy->status( $s->id ) if $s;
897
898         my $evt = $U->update_copy( session => $ctx->{session}, 
899                 copy => $copy, editor => $ctx->{requestor}->id );
900         return (undef,$evt) if $evt;
901
902         return undef;
903 }
904
905 # commits the circ object to the db then fleshes the circ with rules objects
906 sub _commit_checkout_circ_object {
907
908         my $ctx = shift;
909         my $circ = $ctx->{circ};
910         $U->logmark;
911
912         $circ->clear_id;
913         my $r = $ctx->{session}->request(
914                 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
915
916         return $U->DB_UPDATE_FAILED($circ) unless $r;
917
918         $logger->debug("Created a new circ object in checkout: $r");
919
920         $circ->id($r);
921         $circ->duration_rule($ctx->{duration_rule});
922         $circ->max_fine_rule($ctx->{max_fine_rule});
923         $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
924
925         return undef;
926 }
927
928
929 # sees if there are any holds that this copy 
930 sub _handle_related_holds {
931
932         my $ctx         = shift;
933         my $copy                = $ctx->{copy};
934         my $patron      = $ctx->{patron};
935         my $holds       = $holdcode->fetch_related_holds($copy->id);
936         $U->logmark;
937         my @fulfilled;
938
939         # XXX We should only fulfill one hold here...
940         # XXX If a hold was transited to the user who is checking out
941         # the item, we need to make sure that hold is what's grabbed
942         if(ref($holds) && @$holds) {
943
944                 # for now, just sort by id to get what should be the oldest hold
945                 $holds = [ sort { $a->id <=> $b->id } @$holds ];
946                 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
947
948                 if(@$holds) {
949                         my $hold = $holds->[0];
950
951                         $logger->debug("Related hold found in checkout: " . $hold->id );
952
953                         $hold->current_copy($copy->id); # just make sure it's set
954                         # if the hold was never officially captured, capture it.
955                         $hold->capture_time('now') unless $hold->capture_time;
956                         $hold->fulfillment_time('now');
957                         my $r = $ctx->{session}->request(
958                                 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
959                         return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
960                         push( @fulfilled, $hold->id );
961                 }
962         }
963
964         return (\@fulfilled, undef);
965 }
966
967 sub _checkout_noncat {
968         my ( $key, $requestor, $patron, %params ) = @_;
969         my( $circ, $circlib, $evt );
970         $U->logmark;
971
972         $circlib = $params{noncat_circ_lib} || $requestor->home_ou;
973
974         my $count = $params{noncat_count} || 1;
975         my $cotime = _create_date_stamp($params{checkout_time}) || "";
976         $logger->info("circ creating $count noncat circs with checkout time $cotime");
977         for(1..$count) {
978                 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
979                         $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
980                 return $evt if $evt;
981         }
982
983         return OpenILS::Event->new( 
984                 'SUCCESS', payload => { noncat_circ => $circ } );
985 }
986
987
988 __PACKAGE__->register_method(
989         method  => "generic_receive",
990         api_name        => "open-ils.circ.checkin",
991         argc            => 2,
992         signature       => q/
993                 Generic super-method for handling all copies
994                 @param authtoken The login session key
995                 @param params Hash of named parameters including:
996                         barcode - The copy barcode
997                         force           - If true, copies in bad statuses will be checked in and give good statuses
998                         ...
999         /
1000 );
1001
1002 sub generic_receive {
1003         my( $self, $connection, $authtoken, $params ) = @_;
1004         my( $ctx, $requestor, $evt );
1005
1006         ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1007         ( $requestor, $evt ) = $U->checksesperm( 
1008                 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1009         return $evt if $evt;
1010
1011         # load up the circ objects
1012         if( !( $ctx = $params->{_ctx}) ) {
1013                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
1014                         requestor                                               => $requestor, 
1015                         session                                                 => $U->start_db_session(),
1016                         type                                                            => 'circ',
1017                         fetch_copy_statuses                     => 1, 
1018                         fetch_copy_locations                    => 1, 
1019                         no_runner                                               => 1,  
1020                         );
1021                 return $evt if $evt;
1022         }
1023         $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1024         $ctx->{authtoken} = $authtoken;
1025         my $session = $ctx->{session};
1026
1027         my $copy = $ctx->{copy};
1028         $U->unflesh_copy($copy);
1029         return OpenILS::Event->new('COPY_NOT_FOUND') unless $copy;
1030
1031         $logger->info("Checkin copy called by user ".
1032                 $requestor->id." for copy ".$copy->id);
1033
1034         return $self->checkin_do_receive($connection, $ctx);
1035 }
1036
1037 sub checkin_do_receive {
1038
1039         my( $self, $connection, $ctx ) = @_;
1040
1041         my $evt;
1042         my $copy                        = $ctx->{copy};
1043         my $session             = $ctx->{session};
1044         my $requestor   = $ctx->{requestor};
1045         my $change              = 0; # did we actually do anything?
1046
1047         if(!$ctx->{force}) {
1048                 return $evt if ($evt = _checkin_check_copy_status($copy));
1049         }
1050
1051         ($ctx->{circ}, $evt)    = $U->fetch_open_circulation($copy->id);
1052         return $evt if ($evt and $__isrenewal); # renewals require a circulation
1053         $evt = undef;
1054         ($ctx->{transit})               = $U->fetch_open_transit_by_copy($copy->id);
1055
1056         if( $ctx->{circ} ) {
1057
1058                 # There is an open circ on this item, close it out.
1059                 $change = 1;
1060                 $evt            = _checkin_handle_circ($ctx);
1061                 return $evt if $evt;
1062
1063         } elsif( $ctx->{transit} ) {
1064
1065                 # is this item currently in transit?
1066                 $change                 = 1;
1067                 $evt                            = $transcode->transit_receive( $copy, $requestor, $session );
1068                 my $holdtrans   = $evt->{holdtransit};
1069                 ($ctx->{hold})  = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1070
1071                 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1072
1073                         # either an error occurred or a ROUTE_ITEM was generated and the 
1074                         # item must be forwarded on to its destination.
1075                         return _checkin_flesh_event($ctx, $evt);
1076
1077                 } else {
1078
1079                         if($holdtrans) {
1080
1081                                 # copy was received as a hold transit.  Copy is at target lib
1082                                 # and hold transit is complete.  We're done here...
1083                                 $U->commit_db_session($session);
1084                                 return _checkin_flesh_event($ctx, $evt);
1085                         }
1086                         $evt = undef;
1087                 }
1088         }
1089
1090         # ------------------------------------------------------------------------------
1091         # Circulations and transits are now closed where necessary.  Now go on to see if
1092         # this copy can fulfill a hold or needs to be routed to a different location
1093         # ------------------------------------------------------------------------------
1094
1095
1096         # If it's a renewal, we're done
1097         if($__isrenewal) {
1098                 $U->commit_db_session($session);
1099                 return OpenILS::Event->new('SUCCESS');
1100         }
1101
1102
1103         # Now, let's see if this copy is needed for a hold
1104         my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor ); 
1105
1106         if($hold) {
1107
1108                 $ctx->{hold}    = $hold;
1109                 $change                 = 1;
1110                 
1111                 # Capture the hold with this copy
1112                 return $evt if ($evt = _checkin_capture_hold($ctx));
1113
1114                 if( $hold->pickup_lib == $requestor->home_ou ) {
1115
1116                         # This hold was captured in the correct location
1117                         $evt = OpenILS::Event->new('SUCCESS');
1118
1119                 } else {
1120
1121                         # Hold needs to be picked up elsewhere.  Build a hold 
1122                         # transit and route the item.
1123                         return $evt if ($evt =_checkin_build_hold_transit($ctx));
1124                         $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1125                 }
1126
1127         } else { # not needed for a hold
1128
1129                 if( $copy->circ_lib == $requestor->home_ou ) {
1130
1131                         # Copy is in the right place.
1132                         $evt = OpenILS::Event->new('SUCCESS');
1133
1134                         # if the item happens to be a pre-cataloged item, send it
1135                         # to cataloging and return the event
1136                         my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1137                         return $err if $err;
1138                         $change         = 1 if $c;
1139                         $evt                    = $e if $e;
1140
1141                 } else {
1142
1143                         # Copy wants to go home. Transit it there.
1144                         return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1145                         $evt                    = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1146                         $change         = 1;
1147                 }
1148         }
1149
1150         $logger->info("Copy checkin finished with event: ".$evt->{textcode});
1151
1152         if(!$change) {
1153
1154                 $evt = OpenILS::Event->new('NO_CHANGE');
1155                 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id) 
1156                         if ($copy->status == $U->copy_status_from_name('on holds shelf')->id);
1157
1158         } else {
1159
1160                 $U->commit_db_session($session);
1161         }
1162
1163         return _checkin_flesh_event($ctx, $evt);
1164 }
1165
1166 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1167 sub _checkin_handle_precat {
1168
1169         my $ctx         = shift;
1170         my $copy                = $ctx->{copy};
1171         my $evt         = undef;
1172         my $errevt      = undef;
1173         my $change      = 0;
1174
1175         my $catstat = $U->copy_status_from_name('cataloging');
1176
1177         if( $ctx->{precat} ) {
1178
1179                 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1180
1181                 if( $copy->status != $catstat->id ) {
1182                         $copy->status($catstat->id);
1183
1184                         return (undef, 0, $errevt) if (
1185                                 $errevt = $U->update_copy(
1186                                         copy            => $copy, 
1187                                         editor  => $ctx->{requestor}->id, 
1188                                         session => $ctx->{session} ));
1189                         $change = 1;
1190
1191                 }
1192         }
1193
1194         return ($evt, $change, undef);
1195 }
1196
1197
1198 sub _checkin_check_copy_status {
1199         my $copy = shift;
1200         my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1201         my $evt = OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1202         return $evt if ($stat == $U->copy_status_from_name('lost')->id);
1203         return $evt if ($stat == $U->copy_status_from_name('missing')->id);
1204         return undef;
1205 }
1206
1207 # Just gets the copy back home.  Returns undef on success, event on error
1208 sub _checkin_build_generic_copy_transit {
1209
1210         my $ctx                 = shift;
1211         my $requestor   = $ctx->{requestor};
1212         my $copy                        = $ctx->{copy};
1213         my $transit             = Fieldmapper::action::transit_copy->new;
1214         my $session             = $ctx->{session};
1215
1216         $logger->activity("User ". $requestor->id ." creating a ".
1217                 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1218
1219         $transit->source($requestor->home_ou);
1220         $transit->dest($copy->circ_lib);
1221         $transit->target_copy($copy->id);
1222         $transit->source_send_time('now');
1223         $transit->copy_status($copy->status);
1224         
1225         $logger->debug("Creating new copy_transit in DB");
1226
1227         my $s = $session->request(
1228                 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1229         return $U->DB_UPDATE_FAILED($transit) unless $s;
1230
1231         $logger->info("Checkin copy successfully created new transit: $s");
1232
1233         $copy->status($U->copy_status_from_name('in transit')->id );
1234
1235         return $U->update_copy( copy => $copy, 
1236                         editor => $requestor->id, session => $session );
1237         
1238 }
1239
1240
1241 # returns event on error, undef on success
1242 sub _checkin_build_hold_transit {
1243         my $ctx = shift;
1244
1245         my $copy = $ctx->{copy};
1246         my $hold = $ctx->{hold};
1247         my $trans = Fieldmapper::action::hold_transit_copy->new;
1248
1249         $trans->hold($hold->id);
1250         $trans->source($ctx->{requestor}->home_ou);
1251         $trans->dest($hold->pickup_lib);
1252         $trans->source_send_time("now");
1253         $trans->target_copy($copy->id);
1254         $trans->copy_status($copy->status);
1255
1256         my $id = $ctx->{session}->request(
1257                 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1258         return $U->DB_UPDATE_FAILED($trans) unless $id;
1259
1260         $logger->info("Checkin copy successfully created hold transit: $id");
1261
1262         $copy->status($U->copy_status_from_name('in transit')->id );
1263         return $U->update_copy( copy => $copy, 
1264                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1265 }
1266
1267 # Returns event on error, undef on success
1268 sub _checkin_capture_hold {
1269         my $ctx = shift;
1270         my $copy = $ctx->{copy};
1271         my $hold = $ctx->{hold}; 
1272
1273         $logger->debug("Checkin copy capturing hold ".$hold->id);
1274
1275         $hold->current_copy($copy->id);
1276         $hold->capture_time('now'); 
1277
1278         my $stat = $ctx->{session}->request(
1279                 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1280         return $U->DB_UPDATE_FAILED($hold) unless $stat;
1281
1282         $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1283
1284         return $U->update_copy( copy => $copy, 
1285                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1286 }
1287
1288 # fleshes an event with the relevant objects from the context
1289 sub _checkin_flesh_event {
1290         my $ctx = shift;
1291         my $evt = shift;
1292
1293         my $payload                             = {};
1294         $payload->{copy}                = $U->unflesh_copy($ctx->{copy});
1295         $payload->{record}      = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1296         $payload->{circ}                = $ctx->{circ} if $ctx->{circ};
1297         $payload->{transit}     = $ctx->{transit} if $ctx->{transit};
1298         $payload->{hold}                = $ctx->{hold} if $ctx->{hold};
1299
1300         $evt->{payload} = $payload;
1301         return $evt;
1302 }
1303
1304
1305 # Closes out the circulation, puts the copy into reshelving.
1306 # Voids any bills attached to this circ after the backdate time 
1307 # if a backdate is provided
1308 sub _checkin_handle_circ { 
1309
1310         my $ctx = shift;
1311
1312         my $circ = $ctx->{circ};
1313         my $copy = $ctx->{copy};
1314         my $requestor = $ctx->{requestor};
1315         my $session = $ctx->{session};
1316
1317         $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1318
1319         $ctx->{longoverdue}             = 1 if ($circ->stop_fines =~ /longoverdue/io);
1320         $ctx->{claimsreturned}  = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1321
1322         my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1323         return $evt if $evt;
1324
1325         $circ->stop_fines('CHECKIN');
1326         $circ->stop_fines('RENEW') if $__isrenewal;
1327         $circ->stop_fines('LOST') if($__islost);
1328         $circ->xact_finish('now') if($obt->balance_owed <= 0 and !$__islost);
1329         $circ->stop_fines_time('now');
1330         $circ->checkin_time('now');
1331         $circ->checkin_staff($requestor->id);
1332
1333         if(my $backdate = $ctx->{backdate}) {
1334                 return $evt if ($evt = 
1335                         _checkin_handle_backdate($backdate, $circ, $requestor, $session));
1336         }
1337
1338         $logger->info("Checkin copy setting status to 'reshelving' and committing...");
1339         $copy->status($U->copy_status_from_name('reshelving')->id);
1340         $evt = $U->update_copy( session => $session, 
1341                 copy => $copy, editor => $requestor->id );
1342         return $evt if $evt;
1343
1344         $ctx->{session}->request(
1345                 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1346
1347         return undef;
1348 }
1349
1350 # returns event on error, undef on success
1351 # This voids all bills attached to the given circulation that occurred
1352 # after the backdate 
1353 sub _checkin_handle_backdate {
1354         my( $backdate, $circ, $requestor, $session ) = @_;
1355
1356         $logger->activity("User ".$requestor->id.
1357                 " backdating circ [".$circ->target_copy."] to date: $backdate");
1358
1359         $circ->xact_finish($backdate); 
1360
1361         my $bills = $session->request( # XXX Verify this call is correct
1362                 "open-ils.storage.direct.money.billing.search_where.atomic",
1363                 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1364
1365         if($bills) {
1366                 for my $bill (@$bills) {
1367                         $bill->voided('t');
1368                         my $s = $session->request(
1369                                 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1370                         return $U->DB_UPDATE_FAILED($bill) unless $s;
1371                 }
1372         }
1373
1374         return undef;
1375 }
1376
1377
1378
1379 # ------------------------------------------------------------------------------
1380
1381 __PACKAGE__->register_method(
1382         method  => "renew",
1383         api_name        => "open-ils.circ.renew",
1384         notes           => <<"  NOTES");
1385         PARAMS( authtoken, circ => circ_id );
1386         open-ils.circ.renew(login_session, circ_object);
1387         Renews the provided circulation.  login_session is the requestor of the
1388         renewal and if the logged in user is not the same as circ->usr, then
1389         the logged in user must have RENEW_CIRC permissions.
1390         NOTES
1391
1392 sub renew {
1393         my( $self, $client, $authtoken, $params ) = @_;
1394         $U->logmark;
1395
1396         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1397         $__isrenewal = 1;
1398
1399         # fetch the patron object one way or another
1400         if( $params->{patron} ) {
1401                 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1402                 if($evt) { $__isrenewal = 0; return $evt; }
1403         } else {
1404                 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1405                 if($evt) { $__isrenewal = 0; return $evt; }
1406         }
1407
1408         # verify our login session
1409         ($requestor, $evt) = $U->checkses($authtoken);
1410         if($evt) { $__isrenewal = 0; return $evt; }
1411
1412         # make sure we have permission to perform a renewal
1413         if( $requestor->id ne $patron->id ) {
1414                 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'RENEW_CIRC');
1415                 if($evt) { $__isrenewal = 0; return $evt; }
1416         }
1417
1418
1419         # fetch and build the circulation environment
1420         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1421                 patron                                                  => $patron, 
1422                 requestor                                               => $requestor, 
1423                 patron                                                  => $patron, 
1424                 type                                                            => 'circ',
1425                 fetch_patron_circ_summary       => 1,
1426                 fetch_copy_statuses                     => 1, 
1427                 fetch_copy_locations                    => 1, 
1428                 );
1429         if($evt) { $__isrenewal = 0; return $evt; }
1430         $params->{_ctx} = $ctx;
1431
1432         # make sure they have some renewals left and make sure the circulation exists
1433         ($circ, $evt) = _check_renewal_remaining($ctx);
1434         if($evt) { $__isrenewal = 0; return $evt; }
1435         $ctx->{old_circ} = $circ;
1436         my $renewals = $circ->renewal_remaining - 1;
1437
1438         # run the renew permit script
1439         $evt = _run_renew_scripts($ctx);
1440         if($evt) { $__isrenewal = 0; return $evt; }
1441
1442         # checkin the cop
1443         #$ctx->{patron} = $ctx->{patron}->id;
1444         $evt = $self->generic_receive($client, $authtoken, $ctx );
1445                 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1446
1447         if( !$U->event_equals($evt, 'SUCCESS') ) {
1448                 $__isrenewal = 0; return $evt; 
1449         }
1450
1451         # re-fetch the context since objects have changed in the checkin
1452         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1453                 patron                                                  => $patron, 
1454                 requestor                                               => $requestor, 
1455                 patron                                                  => $patron, 
1456                 type                                                            => 'circ',
1457                 fetch_patron_circ_summary       => 1,
1458                 fetch_copy_statuses                     => 1, 
1459                 fetch_copy_locations                    => 1, 
1460                 );
1461         if($evt) { $__isrenewal = 0; return $evt; }
1462         $params->{_ctx} = $ctx;
1463         $ctx->{renewal_remaining} = $renewals;
1464
1465         # run the circ permit scripts
1466         if( $ctx->{permit_override} ) {
1467                 $evt = $U->check_perms(
1468                         $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1469                 if($evt) { $__isrenewal = 0; return $evt; }
1470
1471         } else {
1472                 $evt = $self->permit_circ( $client, $authtoken, $params );
1473                 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1474                         $ctx->{precat} = 1;
1475
1476                 } else {
1477                         if(!$U->event_equals($evt, 'SUCCESS')) {
1478                                 if($evt) { $__isrenewal = 0; return $evt; }
1479                         }
1480                 }
1481                 $params->{permit_key} = $evt->{payload};
1482         }
1483
1484
1485         # checkout the item again
1486         $evt = $self->checkout($client, $authtoken, $params );
1487
1488         $__isrenewal = 0;
1489         return $evt;
1490 }
1491
1492 sub _check_renewal_remaining {
1493         my $ctx = shift;
1494         $U->logmark;
1495         my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1496         return (undef, $evt) if $evt;
1497         $evt = OpenILS::Event->new(
1498                 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1499         return ($circ, $evt);
1500 }
1501
1502 sub _run_renew_scripts {
1503         my $ctx = shift;
1504         my $runner = $ctx->{runner};
1505         $U->logmark;
1506
1507         $runner->load($scripts{circ_permit_renew});
1508         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1509         #my $evtname = $runner->retrieve('result.event');
1510         #$logger->activity("circ_permit_renew for user ".$ctx->{patron}->id." returned event: $evtname");
1511
1512         my $events = $runner->retrieve('result.events');
1513         $events = [ split(/,/, $events) ]; 
1514         $logger->activity("circ_permit_renew for user ".$ctx->{patron}->id." returned events: @$events");
1515
1516         my @allevents;
1517         push( @allevents, OpenILS::Event->new($_)) for @$events;
1518         return \@allevents if  @allevents;
1519
1520         #return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';
1521         return undef;
1522 }
1523
1524         
1525
1526
1527 1;
1528