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