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