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