189d062ee7e95d6dcb1c385f07f6fde62f1fcd67
[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 OpenSRF::Utils::Logger qw(:logger);
13
14 $Data::Dumper::Indent = 0;
15 my $apputils = "OpenILS::Application::AppUtils";
16 my $U = $apputils;
17 my $holdcode = "OpenILS::Application::Circ::Holds";
18
19 my %scripts;                    # - circulation script filenames
20 my $script_libs;                # - any additional script libraries
21 my %cache;                              # - db objects cache
22 my %contexts;                   # - Script runner contexts
23 my $cache_handle;               # - memcache handle
24
25 sub PRECAT_FINE_LEVEL { return 2; }
26 sub PRECAT_LOAN_DURATION { return 2; }
27
28 # for security, this is a process-defined and not
29 # a client-defined variable
30 my $__isrenewal = 0;
31
32 # ------------------------------------------------------------------------------
33 # Load the circ script from the config
34 # ------------------------------------------------------------------------------
35 sub initialize {
36
37         my $self = shift;
38         $cache_handle = OpenSRF::Utils::Cache->new('global');
39         my $conf = OpenSRF::Utils::SettingsClient->new;
40         my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
41         my @pfx = ( @pfx2, "scripts" );
42
43         my $p           = $conf->config_value(  @pfx, 'circ_permit_patron' );
44         my $c           = $conf->config_value(  @pfx, 'circ_permit_copy' );
45         my $d           = $conf->config_value(  @pfx, 'circ_duration' );
46         my $f           = $conf->config_value(  @pfx, 'circ_recurring_fines' );
47         my $m           = $conf->config_value(  @pfx, 'circ_max_fines' );
48         my $pr  = $conf->config_value(  @pfx, 'circ_permit_renew' );
49         my $ph  = $conf->config_value(  @pfx, 'circ_permit_hold' );
50         my $lb  = $conf->config_value(  @pfx2, 'script_path' );
51
52         $logger->error( "Missing circ script(s)" ) 
53                 unless( $p and $c and $d and $f and $m and $pr and $ph );
54
55         $scripts{circ_permit_patron}    = $p;
56         $scripts{circ_permit_copy}              = $c;
57         $scripts{circ_duration}                 = $d;
58         $scripts{circ_recurring_fines}= $f;
59         $scripts{circ_max_fines}                = $m;
60         $scripts{circ_permit_renew}     = $pr;
61         $scripts{hold_permit_permit}    = $ph;
62
63         $lb = [ $lb ] unless ref($lb);
64         $script_libs = $lb;
65
66         $logger->debug("Loaded rules scripts for circ: " .
67                 "circ permit patron: $p, circ permit copy: $c, ".
68                 "circ duration :$d , circ recurring fines : $f, " .
69                 "circ max fines : $m, circ renew permit : $pr, permit hold: $ph");
70 }
71
72
73 # ------------------------------------------------------------------------------
74 # Loads the necessary circ objects and pushes them into the script environment
75 # Returns ( $data, $evt ).  if $evt is defined, then an
76 # unexpedted event occurred and should be dealt with / returned to the caller
77 # ------------------------------------------------------------------------------
78 sub create_circ_ctx {
79         my %params = @_;
80         $U->logmark;
81
82         my $evt;
83         my $ctx = \%params;
84
85         $evt = _ctx_add_patron_objects($ctx, %params);
86         return (undef,$evt) if $evt;
87
88         if(!$params{noncat}) {
89                 if( $evt = _ctx_add_copy_objects($ctx, %params) ) {
90                         $ctx->{precat} = 1 if($evt->{textcode} eq 'COPY_NOT_FOUND')
91                 } else {
92                         $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
93                 }
94         }
95
96         _doctor_patron_object($ctx) if $ctx->{patron};
97         _doctor_copy_object($ctx) if $ctx->{copy};
98
99         if(!$ctx->{no_runner}) {
100                 _build_circ_script_runner($ctx);
101                 _add_script_runner_methods($ctx);
102         }
103
104         return $ctx;
105 }
106
107 sub _ctx_add_patron_objects {
108         my( $ctx, %params) = @_;
109         $U->logmark;
110
111         if(!defined($cache{patron_standings})) {
112                 $cache{patron_standings} = $U->fetch_patron_standings();
113                 $cache{group_tree} = $U->fetch_permission_group_tree();
114         }
115
116         $ctx->{patron_standings} = $cache{patron_standings};
117         $ctx->{group_tree} = $cache{group_tree};
118
119         $ctx->{patron_circ_summary} = 
120                 $U->fetch_patron_circ_summary($ctx->{patron}->id) 
121                 if $params{fetch_patron_circsummary};
122
123         return undef;
124 }
125
126
127 sub _find_copy_by_attr {
128         my %params = @_;
129         $U->logmark;
130         my $evt;
131
132         my $copy = $params{copy} || undef;
133
134         if(!$copy) {
135
136                 ( $copy, $evt ) = 
137                         $U->fetch_copy($params{copyid}) if $params{copyid};
138                 return (undef,$evt) if $evt;
139
140                 if(!$copy) {
141                         ( $copy, $evt ) = 
142                                 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
143                         return (undef,$evt) if $evt;
144                 }
145         }
146         return ( $copy, $evt );
147 }
148
149 sub _ctx_add_copy_objects {
150         my($ctx, %params)  = @_;
151         $U->logmark;
152         my $evt;
153         my $copy;
154
155         $cache{copy_statuses} = $U->fetch_copy_statuses 
156                 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
157
158         $cache{copy_locations} = $U->fetch_copy_locations 
159                 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
160
161         $ctx->{copy_statuses} = $cache{copy_statuses};
162         $ctx->{copy_locations} = $cache{copy_locations};
163
164         ($copy, $evt) = _find_copy_by_attr(%params);
165         return $evt if $evt;
166
167         if( $copy ) {
168                 $logger->debug("Copy status: " . $copy->status);
169                 ( $ctx->{title}, $evt ) = $U->fetch_record_by_copy( $copy->id );
170                 return $evt if $evt;
171                 $ctx->{copy} = $copy;
172         }
173
174         return undef;
175 }
176
177
178 # ------------------------------------------------------------------------------
179 # Fleshes parts of the patron object
180 # ------------------------------------------------------------------------------
181 sub _doctor_copy_object {
182         my $ctx = shift;
183         $U->logmark;
184         my $copy = $ctx->{copy} || return undef;
185
186         $logger->debug("Doctoring copy object...");
187
188         # set the copy status to a status name
189         $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
190
191         # set the copy location to the location object
192         $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
193
194         $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
195 }
196
197
198 # ------------------------------------------------------------------------------
199 # Fleshes parts of the patron object
200 # ------------------------------------------------------------------------------
201 sub _doctor_patron_object {
202         my $ctx = shift;
203         $U->logmark;
204         my $patron = $ctx->{patron} || return undef;
205
206         # push the standing object into the patron
207         if(ref($ctx->{patron_standings})) {
208                 for my $s (@{$ctx->{patron_standings}}) {
209                         if( $s->id eq $ctx->{patron}->standing ) {
210                                 $patron->standing($s);
211                                 $logger->debug("Set patron standing to ". $s->value);
212                         }
213                 }
214         }
215
216         # set the patron ptofile to the profile name
217         $patron->profile( _get_patron_profile( 
218                 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
219
220         # flesh the org unit
221         $patron->home_ou( 
222                 $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
223
224 }
225
226 # recurse and find the patron profile name from the tree
227 # another option would be to grab the groups for the patron
228 # and cycle through those until the "profile" group has been found
229 sub _get_patron_profile { 
230         my( $patron, $group_tree ) = @_;
231         return $group_tree if ($group_tree->id eq $patron->profile);
232         return undef unless ($group_tree->children);
233
234         for my $child (@{$group_tree->children}) {
235                 my $ret = _get_patron_profile( $patron, $child );
236                 return $ret if $ret;
237         }
238         return undef;
239 }
240
241 sub _get_copy_status {
242         my( $copy, $cstatus ) = @_;
243         $U->logmark;
244         my $s = undef;
245         for my $status (@$cstatus) {
246                 $s = $status if( $status->id eq $copy->status ) 
247         }
248         $logger->debug("Retrieving copy status: " . $s->name) if $s;
249         return $s;
250 }
251
252 sub _get_copy_location {
253         my( $copy, $locations ) = @_;
254         $U->logmark;
255         my $l = undef;
256         for my $loc (@$locations) {
257                 $l = $loc if $loc->id eq $copy->location;
258         }
259         $logger->debug("Retrieving copy location: " . $l->name ) if $l;
260         return $l;
261 }
262
263
264 # ------------------------------------------------------------------------------
265 # Constructs and shoves data into the script environment
266 # ------------------------------------------------------------------------------
267 sub _build_circ_script_runner {
268         my $ctx = shift;
269         $U->logmark;
270
271         $logger->debug("Loading script environment for circulation");
272
273         my $runner;
274         if( $runner = $contexts{$ctx->{type}} ) {
275                 $runner->refresh_context;
276         } else {
277                 $runner = OpenILS::Utils::ScriptRunner->new unless $runner;
278                 $contexts{type} = $runner;
279         }
280
281         for(@$script_libs) {
282                 $logger->debug("Loading circ script lib path $_");
283                 $runner->add_path( $_ );
284         }
285
286
287         $runner->insert( 'environment.patron',          $ctx->{patron}, 1);
288         $runner->insert( 'environment.title',           $ctx->{title}, 1);
289         $runner->insert( 'environment.copy',            $ctx->{copy}, 1);
290
291         # circ script result
292         $runner->insert( 'result', {} );
293         $runner->insert( 'result.event', 'SUCCESS' );
294
295         $runner->insert('environment.isRenewal', 1) if $__isrenewal;
296         $runner->insert('environment.isNonCat', 1) if $ctx->{noncat};
297         $runner->insert('environment.nonCatType', $ctx->{noncat_type}) if $ctx->{noncat};
298
299         if(ref($ctx->{patron_circ_summary})) {
300                 $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
301                 $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
302         }
303
304         $ctx->{runner} = $runner;
305         return $runner;
306 }
307
308
309 sub _add_script_runner_methods {
310         my $ctx = shift;
311         $U->logmark;
312         my $runner = $ctx->{runner};
313
314         if( $ctx->{copy} ) {
315                 
316                 # allows a script to fetch a hold that is currently targeting the
317                 # copy in question
318                 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
319                                 my $key = shift;
320                                 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
321                                 $hold = undef unless $hold;
322                                 $runner->insert( $key, $hold, 1 );
323                         }
324                 );
325         }
326 }
327
328 # ------------------------------------------------------------------------------
329
330 __PACKAGE__->register_method(
331         method  => "permit_circ",
332         api_name        => "open-ils.circ.checkout.permit",
333         notes           => q/
334                 Determines if the given checkout can occur
335                 @param authtoken The login session key
336                 @param params A trailing hash of named params including 
337                         barcode : The copy barcode, 
338                         patron : The patron the checkout is occurring for, 
339                         renew : true or false - whether or not this is a renewal
340                 @return The event that occurred during the permit check.  
341         /);
342
343 sub permit_circ {
344         my( $self, $client, $authtoken, $params ) = @_;
345         $U->logmark;
346
347         my ( $requestor, $patron, $ctx, $evt, $circ );
348
349         # check permisson of the requestor
350         ( $requestor, $patron, $evt ) = 
351                 $U->checkses_requestor( 
352                 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
353         return $evt if $evt;
354
355         # fetch and build the circulation environment
356         if( !( $ctx = $params->{_ctx}) ) {
357
358                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
359                         patron                                                  => $patron, 
360                         requestor                                               => $requestor, 
361                         type                                                            => 'circ',
362                         fetch_patron_circ_summary       => 1,
363                         fetch_copy_statuses                     => 1, 
364                         fetch_copy_locations                    => 1, 
365                         );
366                 return $evt if $evt;
367         }
368
369         ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id) 
370                 if ( !$__isrenewal and $ctx->{copy});
371
372         return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
373
374         return _run_permit_scripts($ctx);
375 }
376
377
378
379 # Runs the patron and copy permit scripts
380 # if this is a non-cat circulation, the copy permit script 
381 # is not run
382 sub _run_permit_scripts {
383         my $ctx                 = shift;
384         my $runner              = $ctx->{runner};
385         my $patronid    = $ctx->{patron}->id;
386         my $barcode             = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
387         $U->logmark;
388
389         $runner->load($scripts{circ_permit_patron});
390         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
391         my $evtname = $runner->retrieve('result.event');
392         $logger->activity("circ_permit_patron for user $patronid returned event: $evtname");
393
394         return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';
395
396         my $key = _cache_permit_key();
397
398         if( $ctx->{noncat} ) {
399                 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
400                 return OpenILS::Event->new('SUCCESS', payload => $key);
401         }
402
403         if($ctx->{precat}) {
404                 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
405                 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
406         }
407
408         $runner->load($scripts{circ_permit_copy});
409         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
410         $evtname = $runner->retrieve('result.event');
411         $logger->activity("circ_permit_copy for user $patronid ".
412                 "and copy $barcode returned event: $evtname");
413
414         return OpenILS::Event->new($evtname, payload => $key) if( $evtname eq 'SUCCESS' );
415         return OpenILS::Event->new($evtname);
416 }
417
418 # takes copyid, patronid, and requestor id
419 sub _cache_permit_key {
420         my $key = md5_hex( time() . rand() . "$$" );
421         $logger->debug("Setting circ permit key to $key");
422         $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
423         return $key;
424 }
425
426 sub _check_permit_key {
427         my $key = shift;
428         $logger->debug("Fetching circ permit key $key");
429         my $k = "oils_permit_key_$key";
430         my $one = $cache_handle->get_cache($k);
431         $cache_handle->delete_cache($k);
432         return ($one) ? 1 : 0;
433 }
434
435
436 # ------------------------------------------------------------------------------
437
438 __PACKAGE__->register_method(
439         method  => "checkout",
440         api_name        => "open-ils.circ.checkout",
441         notes => q/
442                 Checks out an item
443                 @param authtoken The login session key
444                 @param params A named hash of params including:
445                         copy                    The copy object
446                         barcode         If no copy is provided, the copy is retrieved via barcode
447                         copyid          If no copy or barcode is provide, the copy id will be use
448                         patron          The patron's id
449                         noncat          True if this is a circulation for a non-cataloted item
450                         noncat_type     The non-cataloged type id
451                         noncat_circ_lib The location for the noncat circ.  
452                         precat          The item has yet to be cataloged
453                         dummy_title The temporary title of the pre-cataloded item
454                         dummy_author The temporary authr of the pre-cataloded item
455                                 Default is the home org of the staff member
456                 @return The SUCCESS event on success, any other event depending on the error
457         /);
458
459 sub checkout {
460         my( $self, $client, $authtoken, $params ) = @_;
461         $U->logmark;
462
463         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
464         my $key = $params->{permit_key};
465
466         # if this is a renewal, then the requestor does not have to
467         # have checkout privelages
468         ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
469         ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
470
471         $logger->debug("REQUESTOR event: " . ref($requestor));
472
473         return $evt if $evt;
474         ( $patron, $evt ) = $U->fetch_user($params->{patron});
475         return $evt if $evt;
476
477
478         # set the circ lib to the home org of the requestor if not specified
479         my $circlib = (defined($params->{circ_lib})) ? 
480                 $params->{circ_lib} : $requestor->home_ou;
481
482         # if this is a non-cataloged item, check it out and return
483         return _checkout_noncat( 
484                 $key, $requestor, $patron, %$params ) if $params->{noncat};
485
486         # if this item has yet to be cataloged, make sure a dummy copy exists
487         ( $params->{copy}, $evt ) = _make_precat_copy(
488                 $requestor, $circlib, $params ) if $params->{precat};
489         return $evt if $evt;
490
491         my $session = $U->start_db_session();
492
493         # fetch and build the circulation environment
494         if( !( $ctx = $params->{_ctx}) ) {
495                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
496                         patron                                                  => $patron, 
497                         requestor                                               => $requestor, 
498                         session                                                 => $session, 
499                         type                                                            => 'circ',
500                         fetch_patron_circ_summary       => 1,
501                         fetch_copy_statuses                     => 1, 
502                         fetch_copy_locations                    => 1, 
503                         );
504                 return $evt if $evt;
505         }
506         $ctx->{session} = $session;
507
508         my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
509         return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY') 
510                 unless _check_permit_key($key);
511
512         $ctx->{circ_lib} = $circlib;
513
514         $evt = _run_checkout_scripts($ctx);
515         return $evt if $evt;
516
517         _build_checkout_circ_object($ctx);
518
519         $evt = _commit_checkout_circ_object($ctx);
520         return $evt if $evt;
521
522         _update_checkout_copy($ctx);
523
524         $evt = _handle_related_holds($ctx);
525         return $evt if $evt;
526
527
528         $U->commit_db_session($session);
529         my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
530
531         return OpenILS::Event->new('SUCCESS', 
532                 payload         => { 
533                         copy            => $ctx->{copy},
534                         circ            => $ctx->{circ},
535                         record  => $record,
536                 } );
537 }
538
539
540 sub _make_precat_copy {
541         my ( $requestor, $circlib, $params ) =  @_;
542         $U->logmark;
543         my( $copy, undef ) = _find_copy_by_attr(%$params);
544
545         if($copy) {
546                 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
547                 return ($copy, undef);
548         }
549
550         $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
551
552         my $evt = OpenILS::Event->new(
553                 'BAD_PARAMS', desc => "Dummy title or author not provided" ) 
554                 unless ( $params->{dummy_title} and $params->{dummy_author} );
555         return (undef, $evt) if $evt;
556
557         $copy = Fieldmapper::asset::copy->new;
558         $copy->circ_lib($circlib);
559         $copy->creator($requestor->id);
560         $copy->editor($requestor->id);
561         $copy->barcode($params->{barcode});
562         $copy->call_number(-1); #special CN for precat materials
563         $copy->loan_duration(&PRECAT_LOAN_DURATION);  # these two should come from constants
564         $copy->fine_level(&PRECAT_FINE_LEVEL);
565
566         $copy->dummy_title($params->{dummy_title});
567         $copy->dummy_author($params->{dummy_author});
568
569         my $id = $U->storagereq(
570                 'open-ils.storage.direct.asset.copy.create', $copy );
571         return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
572
573         $logger->debug("Pre-cataloged copy successfully created");
574         return $U->fetch_copy($id);
575 }
576
577
578 sub _run_checkout_scripts {
579         my $ctx = shift;
580         $U->logmark;
581         my $evt;
582         my $circ;
583
584         my $runner = $ctx->{runner};
585
586         $runner->insert('result.durationLevel');
587         $runner->insert('result.durationRule');
588         $runner->insert('result.recurringFinesRule');
589         $runner->insert('result.recurringFinesLevel');
590         $runner->insert('result.maxFine');
591
592         $runner->load($scripts{circ_duration});
593         $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
594         my $duration = $runner->retrieve('result.durationRule');
595         $logger->debug("Circ duration script yielded a duration rule of: $duration");
596
597         $runner->load($scripts{circ_recurring_fines});
598         $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
599         my $recurring = $runner->retrieve('result.recurringFinesRule');
600         $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
601
602         $runner->load($scripts{circ_max_fines});
603         $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
604         my $max_fine = $runner->retrieve('result.maxFine');
605         $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
606
607         ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
608         return $evt if $evt;
609         ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
610         return $evt if $evt;
611         ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
612         return $evt if $evt;
613
614         $ctx->{duration_level}                  = $runner->retrieve('result.durationLevel');
615         $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
616         $ctx->{duration_rule}                   = $duration;
617         $ctx->{recurring_fines_rule}    = $recurring;
618         $ctx->{max_fine_rule}                   = $max_fine;
619
620         return undef;
621 }
622
623 sub _build_checkout_circ_object {
624         my $ctx = shift;
625         $U->logmark;
626
627         my $circ                        = new Fieldmapper::action::circulation;
628         my $duration    = $ctx->{duration_rule};
629         my $max                 = $ctx->{max_fine_rule};
630         my $recurring   = $ctx->{recurring_fines_rule};
631         my $copy                        = $ctx->{copy};
632         my $patron              = $ctx->{patron};
633         my $dur_level   = $ctx->{duration_level};
634         my $rec_level   = $ctx->{recurring_fines_level};
635
636         $circ->duration( $duration->shrt ) if ($dur_level == 1);
637         $circ->duration( $duration->normal ) if ($dur_level == 2);
638         $circ->duration( $duration->extended ) if ($dur_level == 3);
639
640         $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
641         $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
642         $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
643
644         $circ->duration_rule( $duration->name );
645         $circ->recuring_fine_rule( $recurring->name );
646         $circ->max_fine_rule( $max->name );
647         $circ->max_fine( $max->amount );
648
649         $circ->fine_interval($recurring->recurance_interval);
650         $circ->renewal_remaining( $duration->max_renewals );
651         $circ->target_copy( $copy->id );
652         $circ->usr( $patron->id );
653         $circ->circ_lib( $ctx->{circ_lib} );
654
655         if( $__isrenewal ) {
656                 $logger->debug("Circ is a renewal.  Setting renewal_remaining to " . $ctx->{renewal_remaining} );
657                 $circ->opac_renewal(1); 
658                 $circ->renewal_remaining($ctx->{renewal_remaining});
659                 $circ->circ_staff($ctx->{requestor}->id);
660         } 
661
662         # if a patron is renewing, 'requestor' will be the patron
663         $circ->circ_staff( $ctx->{requestor}->id ); 
664         _set_circ_due_date($circ);
665         $ctx->{circ} = $circ;
666 }
667
668 sub _create_due_date {
669         my $duration = shift;
670         $U->logmark;
671
672         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = 
673                 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
674
675         $year += 1900; $mon += 1;
676         my $due_date = sprintf(
677         '%s-%0.2d-%0.2dT%s:%0.2d:%0.s2-00',
678         $year, $mon, $mday, $hour, $min, $sec);
679         return $due_date;
680 }
681
682 sub _set_circ_due_date {
683         my $circ = shift;
684         $U->logmark;
685         my $dd = _create_due_date($circ->duration);
686         $logger->debug("Checkout setting due date on circ to: $dd");
687         $circ->due_date($dd);
688 }
689
690 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
691 sub _update_checkout_copy {
692         my $ctx = shift;
693         $U->logmark;
694         my $copy = $ctx->{copy};
695
696         my $s;
697         for my $status (@{$cache{copy_statuses}}) { #XXX Abstractify me
698                 $s = $status if( $status->name eq 'Checked out' );
699         }
700
701         $copy->status( $s->id ) if $s;
702         $copy->editor( $ctx->{requestor}->id );
703         $copy->edit_date( 'now' );
704         $copy->location( $copy->location->id );
705         $copy->circ_lib( $copy->circ_lib->id );
706
707         $logger->debug("Updating editor info on copy in checkout: " . $copy->id );
708         $ctx->{session}->request( 
709                 'open-ils.storage.direct.asset.copy.update', $copy )->gather(1);
710 }
711
712 # commits the circ object to the db then fleshes the circ with rules objects
713 sub _commit_checkout_circ_object {
714
715         my $ctx = shift;
716         my $circ = $ctx->{circ};
717         $U->logmark;
718
719         $circ->clear_id;
720         my $r = $ctx->{session}->request(
721                 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
722
723         return $U->DB_UPDATE_FAILED($circ) unless $r;
724
725         $logger->debug("Created a new circ object in checkout: $r");
726
727         $circ->id($r);
728         $circ->duration_rule($ctx->{duration_rule});
729         $circ->max_fine_rule($ctx->{max_fine_rule});
730         $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
731
732         return undef;
733 }
734
735
736 # sees if there are any holds that this copy 
737 sub _handle_related_holds {
738
739         my $ctx         = shift;
740         my $copy                = $ctx->{copy};
741         my $patron      = $ctx->{patron};
742         my $holds       = $holdcode->fetch_related_holds($copy->id);
743         $U->logmark;
744
745         if(ref($holds) && @$holds) {
746
747                 # for now, just sort by id to get what should be the oldest hold
748                 $holds = [ sort { $a->id <=> $b->id } @$holds ];
749                 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
750
751                 if(@$holds) {
752                         my $hold = $holds->[0];
753
754                         $logger->debug("Related hold found in checkout: " . $hold->id );
755
756                         $hold->fulfillment_time('now');
757                         my $r = $ctx->{session}->request(
758                                 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
759                         return $U->DB_UPDATE_FAILED( $hold ) unless $r;
760                 }
761         }
762
763         return undef;
764 }
765
766
767 sub _checkout_noncat {
768         my ( $key, $requestor, $patron, %params ) = @_;
769         my( $circ, $circlib, $evt );
770         $U->logmark;
771
772         $circlib = $params{noncat_circ_lib} || $requestor->home_ou;
773
774         return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY') 
775                 unless _check_permit_key($key);
776
777         ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
778                         $requestor->id, $patron->id, $circlib, $params{noncat_type} );
779
780         return $evt if $evt;
781         return OpenILS::Event->new( 
782                 'SUCCESS', payload => { noncat_circ => $circ } );
783 }
784
785
786 # ------------------------------------------------------------------------------
787
788 __PACKAGE__->register_method(
789         method  => "checkin",
790         api_name        => "open-ils.circ.checkin",
791         notes           => <<"  NOTES");
792         PARAMS( authtoken, barcode => bc )
793         Checks in based on barcode
794         Returns an event object whose payload contains the record, circ, and copy
795         If the item needs to be routed, the event is a ROUTE_ITEM event
796         with an additional 'route_to' variable set on the event
797         NOTES
798
799 sub checkin {
800         my( $self, $client, $authtoken, $params ) = @_;
801         $U->logmark;
802
803         my( $ctx, $requestor, $evt, $patron, $circ, $copy );
804
805         ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
806         ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
807         return $evt if $evt;
808         ( $patron, $evt ) = $U->fetch_user($params->{patron});
809         return $evt if $evt;
810
811 #       if( !( $ctx = $params->{_ctx}) ) {
812 #               ( $ctx, $evt ) = create_circ_ctx( %$params, 
813 #                       patron                                                  => $patron, 
814 #                       requestor                                               => $requestor, 
815 #                       #session                                                        => $session, 
816 #                       type                                                            => 'circ',
817 #                       fetch_patron_circ_summary       => 1,
818 #                       fetch_copy_statuses                     => 1, 
819 #                       fetch_copy_locations                    => 1, 
820 #                       no_runner                                               => 1, 
821 #                       );
822 #               return $evt if $evt;
823 #       }
824
825         ($copy, $evt) = $U->fetch_copy_by_barcode($params->{barcode});
826         return $evt if $evt;
827
828 #       return OpenILS::Event->new('COPY_NOT_FOUND') unless $ctx->{copy};
829         ( $circ, $evt ) = $U->fetch_open_circulation($copy->id);
830         return $evt if $evt;
831
832         # XXX Use the old checkin for now XXX 
833         my $checkin = $self->method_lookup("open-ils.circ.checkin.barcode");
834         my ($status) = $checkin->run($authtoken, $copy->barcode, $__isrenewal);
835
836         return OpenILS::Event->new('PERM_FAILURE') 
837                 if ref($status) eq "Fieldmapper::perm_ex";
838         return OpenILS::Event->new('ROUTE_ITEM', payload => $status->{route_to} )
839                 if( $status->{status} eq '3' );
840         return OpenILS::Event->new('UNKNOWN', 
841                 payload => $status ) if ($status->{status} ne "0"); 
842         return OpenILS::Event->new('SUCCESS');
843 }
844
845 # ------------------------------------------------------------------------------
846
847 __PACKAGE__->register_method(
848         method  => "renew",
849         api_name        => "open-ils.circ.renew_",
850         notes           => <<"  NOTES");
851         PARAMS( authtoken, circ => circ_id );
852         open-ils.circ.renew(login_session, circ_object);
853         Renews the provided circulation.  login_session is the requestor of the
854         renewal and if the logged in user is not the same as circ->usr, then
855         the logged in user must have RENEW_CIRC permissions.
856         NOTES
857
858 sub renew {
859         my( $self, $client, $authtoken, $params ) = @_;
860         $U->logmark;
861
862         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
863         $__isrenewal = 1;
864
865         # if requesting a renewal for someone else, you must have
866         # renew privelages
867         ( $requestor, $patron, $evt ) = $U->checkses_requestor( 
868                 $authtoken, $params->{patron}, 'RENEW_CIRC' );
869         return $evt if $evt;
870
871
872         # fetch and build the circulation environment
873         ( $ctx, $evt ) = create_circ_ctx( %$params, 
874                 patron                                                  => $patron, 
875                 requestor                                               => $requestor, 
876                 type                                                            => 'circ',
877                 fetch_patron_circ_summary       => 1,
878                 fetch_copy_statuses                     => 1, 
879                 fetch_copy_locations                    => 1, 
880                 );
881         return $evt if $evt;
882         $params->{_ctx} = $ctx;
883
884         # make sure they have some renewals left and make sure the circulation exists
885         ($circ, $evt) = _check_renewal_remaining($ctx);
886         return $evt if $evt;
887         $ctx->{old_circ} = $circ;
888         my $renewals = $circ->renewal_remaining - 1;
889
890         # run the renew permit script
891         return $evt if( ($evt = _run_renew_scripts($ctx)) );
892
893         # checkin the cop
894         $evt = $self->checkin($client, $authtoken, 
895                 { barcode => $params->{barcode}, patron => $params->{patron}} );
896
897         return $evt unless $U->event_equals($evt, 'SUCCESS') 
898                 or $U->event_equals($evt, 'ROUTE_ITEM');
899
900         # re-fetch the context since objects have changed in the checkin
901         ( $ctx, $evt ) = create_circ_ctx( %$params, 
902                 patron                                                  => $patron, 
903                 requestor                                               => $requestor, 
904                 type                                                            => 'circ',
905                 fetch_patron_circ_summary       => 1,
906                 fetch_copy_statuses                     => 1, 
907                 fetch_copy_locations                    => 1, 
908                 );
909         return $evt if $evt;
910         $params->{_ctx} = $ctx;
911         $ctx->{renewal_remaining} = $renewals;
912
913         # run the circ permit scripts
914         $evt = $self->permit_circ( $client, $authtoken, $params );
915         if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
916                 $ctx->{precat} = 1;
917         } else {
918                 return $evt unless $U->event_equals($evt, 'SUCCESS');
919         }
920         $params->{permit_key} = $evt->{payload};
921
922
923         # checkout the item again
924         $evt = $self->checkout($client, $authtoken, $params );
925
926         $__isrenewal = 0;
927         return $evt;
928 }
929
930 sub _check_renewal_remaining {
931         my $ctx = shift;
932         $U->logmark;
933         my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
934         return (undef, $evt) if $evt;
935         $evt = OpenILS::Event->new(
936                 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
937         return ($circ, $evt);
938 }
939
940 sub _run_renew_scripts {
941         my $ctx = shift;
942         my $runner = $ctx->{runner};
943         $U->logmark;
944
945         $runner->load($scripts{circ_permit_renew});
946         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
947         my $evtname = $runner->retrieve('result.event');
948         $logger->activity("circ_permit_renew for user ".$ctx->{patron}." returned event: $evtname");
949
950         return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';
951         return undef;
952 }
953
954         
955
956
957 1;