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