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