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