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