]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
new method for un-canceling holds. request_time and expire_time are reset if appropr...
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Circ / Holds.pm
1 # ---------------------------------------------------------------
2 # Copyright (C) 2005  Georgia Public Library Service 
3 # Bill Erickson <highfalutin@gmail.com>
4
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 # ---------------------------------------------------------------
15
16
17 package OpenILS::Application::Circ::Holds;
18 use base qw/OpenILS::Application/;
19 use strict; use warnings;
20 use OpenILS::Application::AppUtils;
21 use DateTime;
22 use Data::Dumper;
23 use OpenSRF::EX qw(:try);
24 use OpenILS::Perm;
25 use OpenILS::Event;
26 use OpenSRF::Utils;
27 use OpenSRF::Utils::Logger qw(:logger);
28 use OpenILS::Utils::CStoreEditor q/:funcs/;
29 use OpenILS::Utils::PermitHold;
30 use OpenSRF::Utils::SettingsClient;
31 use OpenILS::Const qw/:const/;
32 use OpenILS::Application::Circ::Transit;
33 use OpenILS::Application::Actor::Friends;
34
35 my $apputils = "OpenILS::Application::AppUtils";
36 my $U = $apputils;
37
38
39
40
41 __PACKAGE__->register_method(
42         method  => "create_hold",
43         api_name        => "open-ils.circ.holds.create",
44         notes           => <<NOTE);
45 Create a new hold for an item.  From a permissions perspective, 
46 the login session is used as the 'requestor' of the hold.  
47 The hold recipient is determined by the 'usr' setting within
48 the hold object.
49
50 First we verify the requestion has holds request permissions.
51 Then we verify that the recipient is allowed to make the given hold.
52 If not, we see if the requestor has "override" capabilities.  If not,
53 a permission exception is returned.  If permissions allow, we cycle
54 through the set of holds objects and create.
55
56 If the recipient does not have permission to place multiple holds
57 on a single title and said operation is attempted, a permission
58 exception is returned
59 NOTE
60
61
62 __PACKAGE__->register_method(
63         method  => "create_hold",
64         api_name        => "open-ils.circ.holds.create.override",
65         signature       => q/
66                 If the recipient is not allowed to receive the requested hold,
67                 call this method to attempt the override
68                 @see open-ils.circ.holds.create
69         /
70 );
71
72 sub create_hold {
73         my( $self, $conn, $auth, @holds ) = @_;
74         my $e = new_editor(authtoken=>$auth, xact=>1);
75         return $e->event unless $e->checkauth;
76
77         my $override = 1 if $self->api_name =~ /override/;
78
79         my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
80
81 #       my @copyholds;
82
83         for my $hold (@$holds) {
84
85                 next unless $hold;
86                 my @events;
87
88                 my $requestor = $e->requestor;
89                 my $recipient = $requestor;
90
91
92                 if( $requestor->id ne $hold->usr ) {
93                         # Make sure the requestor is allowed to place holds for 
94                         # the recipient if they are not the same people
95                         $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
96                         $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
97                 }
98
99                 # Now make sure the recipient is allowed to receive the specified hold
100                 my $pevt;
101                 my $porg                = $recipient->home_ou;
102                 my $rid         = $e->requestor->id;
103                 my $t                   = $hold->hold_type;
104
105                 # See if a duplicate hold already exists
106                 my $sargs = {
107                         usr                     => $recipient->id, 
108                         hold_type       => $t, 
109                         fulfillment_time => undef, 
110                         target          => $hold->target,
111                         cancel_time     => undef,
112                 };
113
114                 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
115                         
116                 my $existing = $e->search_action_hold_request($sargs); 
117                 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
118
119                 if( $t eq OILS_HOLD_TYPE_METARECORD ) 
120                         { $pevt = $e->event unless $e->allowed('MR_HOLDS', $porg); }
121
122                 if( $t eq OILS_HOLD_TYPE_TITLE ) 
123                         { $pevt = $e->event unless $e->allowed('TITLE_HOLDS', $porg);  }
124
125                 if( $t eq OILS_HOLD_TYPE_VOLUME ) 
126                         { $pevt = $e->event unless $e->allowed('VOLUME_HOLDS', $porg); }
127
128                 if( $t eq OILS_HOLD_TYPE_COPY ) 
129                         { $pevt = $e->event unless $e->allowed('COPY_HOLDS', $porg); }
130
131                 return $pevt if $pevt;
132
133                 if( @events ) {
134                         if( $override ) {
135                                 for my $evt (@events) {
136                                         next unless $evt;
137                                         my $name = $evt->{textcode};
138                                         return $e->event unless $e->allowed("$name.override", $porg);
139                                 }
140                         } else {
141                                 return \@events;
142                         }
143                 }
144
145         # set the configured expire time
146         unless($hold->expire_time) {
147             my $interval = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_HOLD_EXPIRE);
148             if($interval) {
149                 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
150                 $hold->expire_time($U->epoch2ISO8601($date->epoch));
151             }
152         }
153
154                 $hold->requestor($e->requestor->id); 
155                 $hold->request_lib($e->requestor->ws_ou);
156                 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
157                 $hold = $e->create_action_hold_request($hold) or return $e->event;
158         }
159
160         $e->commit;
161
162         $conn->respond_complete(1);
163
164     for(@holds) {
165         next if $U->is_true($_->frozen);
166             $U->storagereq(
167                     'open-ils.storage.action.hold_request.copy_targeter', 
168                     undef, $_->id );
169     }
170
171         return undef;
172 }
173
174 sub __create_hold {
175         my( $self, $client, $login_session, @holds) = @_;
176
177         if(!@holds){return 0;}
178         my( $user, $evt ) = $apputils->checkses($login_session);
179         return $evt if $evt;
180
181         my $holds;
182         if(ref($holds[0]) eq 'ARRAY') {
183                 $holds = $holds[0];
184         } else { $holds = [ @holds ]; }
185
186         $logger->debug("Iterating over holds requests...");
187
188         for my $hold (@$holds) {
189
190                 if(!$hold){next};
191                 my $type = $hold->hold_type;
192
193                 $logger->activity("User " . $user->id . 
194                         " creating new hold of type $type for user " . $hold->usr);
195
196                 my $recipient;
197                 if($user->id ne $hold->usr) {
198                         ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
199                         return $evt if $evt;
200
201                 } else {
202                         $recipient = $user;
203                 }
204
205
206                 my $perm = undef;
207
208                 # am I allowed to place holds for this user?
209                 if($hold->requestor ne $hold->usr) {
210                         $perm = _check_request_holds_perm($user->id, $user->home_ou);
211                         if($perm) { return $perm; }
212                 }
213
214                 # is this user allowed to have holds of this type?
215                 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
216         return $perm if $perm;
217
218                 #enforce the fact that the login is the one requesting the hold
219                 $hold->requestor($user->id); 
220                 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
221
222                 my $resp = $apputils->simplereq(
223                         'open-ils.storage',
224                         'open-ils.storage.direct.action.hold_request.create', $hold );
225
226                 if(!$resp) { 
227                         return OpenSRF::EX::ERROR ("Error creating hold"); 
228                 }
229         }
230
231         return 1;
232 }
233
234 # makes sure that a user has permission to place the type of requested hold
235 # returns the Perm exception if not allowed, returns undef if all is well
236 sub _check_holds_perm {
237         my($type, $user_id, $org_id) = @_;
238
239         my $evt;
240         if($type eq "M") {
241                 if($evt = $apputils->check_perms(
242                         $user_id, $org_id, "MR_HOLDS")) {
243                         return $evt;
244                 } 
245
246         } elsif ($type eq "T") {
247                 if($evt = $apputils->check_perms(
248                         $user_id, $org_id, "TITLE_HOLDS")) {
249                         return $evt;
250                 }
251
252         } elsif($type eq "V") {
253                 if($evt = $apputils->check_perms(
254                         $user_id, $org_id, "VOLUME_HOLDS")) {
255                         return $evt;
256                 }
257
258         } elsif($type eq "C") {
259                 if($evt = $apputils->check_perms(
260                         $user_id, $org_id, "COPY_HOLDS")) {
261                         return $evt;
262                 }
263         }
264
265         return undef;
266 }
267
268 # tests if the given user is allowed to place holds on another's behalf
269 sub _check_request_holds_perm {
270         my $user_id = shift;
271         my $org_id = shift;
272         if(my $evt = $apputils->check_perms(
273                 $user_id, $org_id, "REQUEST_HOLDS")) {
274                 return $evt;
275         }
276 }
277
278 __PACKAGE__->register_method(
279         method  => "retrieve_holds_by_id",
280         api_name        => "open-ils.circ.holds.retrieve_by_id",
281         notes           => <<NOTE);
282 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
283 different from the user, then the requestor must have VIEW_HOLD permissions.
284 NOTE
285
286
287 sub retrieve_holds_by_id {
288         my($self, $client, $auth, $hold_id) = @_;
289         my $e = new_editor(authtoken=>$auth);
290         $e->checkauth or return $e->event;
291         $e->allowed('VIEW_HOLD') or return $e->event;
292
293         my $holds = $e->search_action_hold_request(
294                 [
295                         { id =>  $hold_id , fulfillment_time => undef }, 
296                         { order_by => { ahr => "request_time" } }
297                 ]
298         );
299
300         flesh_hold_transits($holds);
301         flesh_hold_notices($holds, $e);
302         return $holds;
303 }
304
305
306 __PACKAGE__->register_method(
307         method  => "retrieve_holds",
308         api_name        => "open-ils.circ.holds.retrieve",
309         notes           => <<NOTE);
310 Retrieves all the holds, with hold transits attached, for the specified
311 user id.  The login session is the requestor and if the requestor is
312 different from the user, then the requestor must have VIEW_HOLD permissions.
313 NOTE
314
315 __PACKAGE__->register_method(
316         method  => "retrieve_holds",
317     authoritative => 1,
318         api_name        => "open-ils.circ.holds.id_list.retrieve",
319         notes           => <<NOTE);
320 Retrieves all the hold ids for the specified
321 user id.  The login session is the requestor and if the requestor is
322 different from the user, then the requestor must have VIEW_HOLD permissions.
323 NOTE
324
325 sub retrieve_holds {
326         my($self, $client, $auth, $user_id, $options) = @_;
327
328     my $e = new_editor(authtoken=>$auth);
329     return $e->event unless $e->checkauth;
330     $user_id = $e->requestor->id unless defined $user_id;
331     $options ||= {};
332
333     unless($user_id == $e->requestor->id) {
334         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
335         unless($e->allowed('VIEW_HOLD', $user->home_ou)) {
336             my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
337                 $e, $user_id, $e->requestor->id, 'hold.view');
338             return $e->event unless $allowed;
339         }
340     }
341
342     my $holds = $e->search_action_hold_request([
343                 {   usr =>  $user_id , 
344                         fulfillment_time => undef,
345                         cancel_time => undef,
346                 }, 
347                 {order_by => {ahr => "request_time"}}
348         ]);
349
350     if($$options{canceled}) {
351         my $count = $$options{cancel_count} || 
352             $U->ou_ancestor_setting_value($e->requestor->ws_ou, 
353                 'circ.canceled_hold_display_count', $e) || 5;
354
355         my $canceled = $e->search_action_hold_request([
356                     {   usr =>  $user_id , 
357                             fulfillment_time => undef,
358                             cancel_time => {'!=' => undef},
359                     }, 
360                     {order_by => {ahr => "cancel_time desc"}, limit => $count}
361             ]);
362         push(@$holds, @$canceled);
363     }
364         
365         if( ! $self->api_name =~ /id_list/ ) {
366                 for my $hold ( @$holds ) {
367                         $hold->transit(
368                 $e->search_action_hold_transit_copy([
369                                         {hold => $hold->id},
370                                         {order_by => {ahtc => 'id desc'}, limit => 1}])->[0]
371                         );
372                 }
373         }
374
375         if( $self->api_name =~ /id_list/ ) {
376                 return [ map { $_->id } @$holds ];
377         } else {
378                 return $holds;
379         }
380 }
381
382
383 __PACKAGE__->register_method(
384    method => 'user_hold_count',
385    api_name => 'open-ils.circ.hold.user.count');
386
387 sub user_hold_count {
388    my( $self, $conn, $auth, $userid ) = @_;
389    my $e = new_editor(authtoken=>$auth);
390    return $e->event unless $e->checkauth;
391    my $patron = $e->retrieve_actor_user($userid)
392       or return $e->event;
393    return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
394    return __user_hold_count($self, $e, $userid);
395 }
396
397 sub __user_hold_count {
398    my( $self, $e, $userid ) = @_;
399    my $holds = $e->search_action_hold_request(
400       {  usr =>  $userid , 
401          fulfillment_time => undef,
402          cancel_time => undef,
403       }, 
404       {idlist => 1}
405    );
406
407    return scalar(@$holds);
408 }
409
410
411 __PACKAGE__->register_method(
412         method  => "retrieve_holds_by_pickup_lib",
413         api_name        => "open-ils.circ.holds.retrieve_by_pickup_lib",
414         notes           => <<NOTE);
415 Retrieves all the holds, with hold transits attached, for the specified
416 pickup_ou id. 
417 NOTE
418
419 __PACKAGE__->register_method(
420         method  => "retrieve_holds_by_pickup_lib",
421         api_name        => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
422         notes           => <<NOTE);
423 Retrieves all the hold ids for the specified
424 pickup_ou id. 
425 NOTE
426
427 sub retrieve_holds_by_pickup_lib {
428         my($self, $client, $login_session, $ou_id) = @_;
429
430         #FIXME -- put an appropriate permission check here
431         #my( $user, $target, $evt ) = $apputils->checkses_requestor(
432         #       $login_session, $user_id, 'VIEW_HOLD' );
433         #return $evt if $evt;
434
435         my $holds = $apputils->simplereq(
436                 'open-ils.cstore',
437                 "open-ils.cstore.direct.action.hold_request.search.atomic",
438                 { 
439                         pickup_lib =>  $ou_id , 
440                         fulfillment_time => undef,
441                         cancel_time => undef
442                 }, 
443                 { order_by => { ahr => "request_time" } });
444
445
446         if( ! $self->api_name =~ /id_list/ ) {
447                 flesh_hold_transits($holds);
448         }
449
450         if( $self->api_name =~ /id_list/ ) {
451                 return [ map { $_->id } @$holds ];
452         } else {
453                 return $holds;
454         }
455 }
456
457
458 __PACKAGE__->register_method(
459         method  => "uncancel_hold",
460         api_name        => "open-ils.circ.hold.uncancel"
461 );
462
463 sub uncancel_hold {
464         my($self, $client, $auth, $hold_id) = @_;
465         my $e = new_editor(authtoken=>$auth, xact=>1);
466         return $e->event unless $e->checkauth;
467
468         my $hold = $e->retrieve_action_hold_request($hold_id)
469                 or return $e->die_event;
470     return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
471
472     return 0 if $hold->fulfillment_time;
473     return 1 unless $hold->cancel_time;
474
475     # if configured to reset the request time, also reset the expire time
476     if($U->ou_ancestor_setting_value(
477         $hold->request_lib, 'circ.hold_reset_request_time_on_uncancel', $e)) {
478
479         $hold->request_time('now');
480         my $interval = $U->ou_ancestor_setting_value($hold->request_lib, OILS_SETTING_HOLD_EXPIRE);
481         if($interval) {
482             my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
483             $hold->expire_time($U->epoch2ISO8601($date->epoch));
484         }
485     }
486
487     $hold->clear_cancel_time;
488     $e->update_action_hold_request($hold) or return $e->die_event;
489     $e->commit;
490
491     $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $hold_id);
492
493     return 1;
494 }
495
496
497 __PACKAGE__->register_method(
498         method  => "cancel_hold",
499         api_name        => "open-ils.circ.hold.cancel",
500         notes           => <<"  NOTE");
501         Cancels the specified hold.  The login session
502         is the requestor and if the requestor is different from the usr field
503         on the hold, the requestor must have CANCEL_HOLDS permissions.
504         the hold may be either the hold object or the hold id
505         NOTE
506
507 sub cancel_hold {
508         my($self, $client, $auth, $holdid, $cause, $note) = @_;
509
510         my $e = new_editor(authtoken=>$auth, xact=>1);
511         return $e->event unless $e->checkauth;
512
513         my $hold = $e->retrieve_action_hold_request($holdid)
514                 or return $e->event;
515
516         if( $e->requestor->id ne $hold->usr ) {
517                 return $e->event unless $e->allowed('CANCEL_HOLDS');
518         }
519
520         return 1 if $hold->cancel_time;
521
522         # If the hold is captured, reset the copy status
523         if( $hold->capture_time and $hold->current_copy ) {
524
525                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
526                         or return $e->event;
527
528                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
529          $logger->info("canceling hold $holdid whose item is on the holds shelf");
530 #                       $logger->info("setting copy to status 'reshelving' on hold cancel");
531 #                       $copy->status(OILS_COPY_STATUS_RESHELVING);
532 #                       $copy->editor($e->requestor->id);
533 #                       $copy->edit_date('now');
534 #                       $e->update_asset_copy($copy) or return $e->event;
535
536                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
537
538                         my $hid = $hold->id;
539                         $logger->warn("! canceling hold [$hid] that is in transit");
540                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
541
542                         if( $transid ) {
543                                 my $trans = $e->retrieve_action_transit_copy($transid);
544                                 # Leave the transit alive, but  set the copy status to 
545                                 # reshelving so it will be properly reshelved when it gets back home
546                                 if( $trans ) {
547                                         $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
548                                         $e->update_action_transit_copy($trans) or return $e->die_event;
549                                 }
550                         }
551                 }
552         }
553
554         $hold->cancel_time('now');
555     $hold->cancel_cause($cause);
556     $hold->cancel_note($note);
557         $e->update_action_hold_request($hold)
558                 or return $e->event;
559
560         delete_hold_copy_maps($self, $e, $hold->id);
561
562         $e->commit;
563         return 1;
564 }
565
566 sub delete_hold_copy_maps {
567         my $class = shift;
568         my $editor = shift;
569         my $holdid = shift;
570
571         my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
572         for(@$maps) {
573                 $editor->delete_action_hold_copy_map($_) 
574                         or return $editor->event;
575         }
576         return undef;
577 }
578
579
580 __PACKAGE__->register_method(
581         method  => "update_hold",
582         api_name        => "open-ils.circ.hold.update",
583         notes           => <<"  NOTE");
584         Updates the specified hold.  The login session
585         is the requestor and if the requestor is different from the usr field
586         on the hold, the requestor must have UPDATE_HOLDS permissions.
587         NOTE
588
589 sub update_hold {
590         my($self, $client, $auth, $hold) = @_;
591
592     my $e = new_editor(authtoken=>$auth, xact=>1);
593     return $e->die_event unless $e->checkauth;
594
595     my $orig_hold = $e->retrieve_action_hold_request($hold->id)
596         or return $e->die_event;
597
598     # don't allow the user to be changed
599     return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
600
601     if($hold->usr ne $e->requestor->id) {
602         # if the hold is for a different user, make sure the 
603         # requestor has the appropriate permissions
604         my $usr = $e->retrieve_actor_user($hold->usr)
605             or return $e->die_event;
606         return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
607     }
608
609     # --------------------------------------------------------------
610     # if the hold is on the holds shelf and the pickup lib changes, 
611     # we need to create a new transit
612     # --------------------------------------------------------------
613     if( ($orig_hold->pickup_lib ne $hold->pickup_lib) and (_hold_status($e, $hold) == 4)) {
614         return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
615         return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
616         my $evt = transit_hold($e, $orig_hold, $hold, 
617             $e->retrieve_asset_copy($hold->current_copy));
618         return $evt if $evt;
619     }
620
621     update_hold_if_frozen($self, $e, $hold, $orig_hold);
622     $e->update_action_hold_request($hold) or return $e->die_event;
623     $e->commit;
624     return $hold->id;
625 }
626
627 sub transit_hold {
628     my($e, $orig_hold, $hold, $copy) = @_;
629     my $src = $orig_hold->pickup_lib;
630     my $dest = $hold->pickup_lib;
631
632     $logger->info("putting hold into transit on pickup_lib update");
633
634     my $transit = Fieldmapper::action::transit_copy->new;
635     $transit->source($src);
636     $transit->dest($dest);
637     $transit->target_copy($copy->id);
638     $transit->source_send_time('now');
639     $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
640
641     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
642     $copy->editor($e->requestor->id);
643     $copy->edit_date('now');
644
645     $e->create_action_transit_copy($transit) or return $e->die_event;
646     $e->update_asset_copy($copy) or return $e->die_event;
647     return undef;
648 }
649
650 # if the hold is frozen, this method ensures that the hold is not "targeted", 
651 # that is, it clears the current_copy and prev_check_time to essentiallly 
652 # reset the hold.  If it is being activated, it runs the targeter in the background
653 sub update_hold_if_frozen {
654     my($self, $e, $hold, $orig_hold) = @_;
655     return if $hold->capture_time;
656
657     if($U->is_true($hold->frozen)) {
658         $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
659         $hold->clear_current_copy;
660         $hold->clear_prev_check_time;
661
662     } else {
663         if($U->is_true($orig_hold->frozen)) {
664             $logger->info("Running targeter on activated hold ".$hold->id);
665                 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
666         }
667     }
668 }
669
670
671 __PACKAGE__->register_method(
672         method  => "retrieve_hold_status",
673         api_name        => "open-ils.circ.hold.status.retrieve",
674         notes           => <<"  NOTE");
675         Calculates the current status of the hold.
676         the requestor must have VIEW_HOLD permissions if the hold is for a user
677         other than the requestor.
678         Returns -1  on error (for now)
679         Returns 1 for 'waiting for copy to become available'
680         Returns 2 for 'waiting for copy capture'
681         Returns 3 for 'in transit'
682         Returns 4 for 'arrived'
683         NOTE
684
685 sub retrieve_hold_status {
686         my($self, $client, $auth, $hold_id) = @_;
687
688         my $e = new_editor(authtoken => $auth);
689         return $e->event unless $e->checkauth;
690         my $hold = $e->retrieve_action_hold_request($hold_id)
691                 or return $e->event;
692
693         if( $e->requestor->id != $hold->usr ) {
694                 return $e->event unless $e->allowed('VIEW_HOLD');
695         }
696
697         return _hold_status($e, $hold);
698
699 }
700
701 sub _hold_status {
702         my($e, $hold) = @_;
703         return 1 unless $hold->current_copy;
704         return 2 unless $hold->capture_time;
705
706         my $copy = $hold->current_copy;
707         unless( ref $copy ) {
708                 $copy = $e->retrieve_asset_copy($hold->current_copy)
709                         or return $e->event;
710         }
711
712         return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
713         return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
714
715         return -1;
716 }
717
718
719
720 __PACKAGE__->register_method(
721         method  => "retrieve_hold_queue_stats",
722         api_name        => "open-ils.circ.hold.queue_stats.retrieve",
723     signature => {
724         desc => q/
725             Returns object with total_holds count, queue_position, potential_copies count, and status code
726         /
727     }
728 );
729
730 sub retrieve_hold_queue_stats {
731     my($self, $conn, $auth, $hold_id) = @_;
732         my $e = new_editor(authtoken => $auth);
733         return $e->event unless $e->checkauth;
734         my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
735         if($e->requestor->id != $hold->usr) {
736                 return $e->event unless $e->allowed('VIEW_HOLD');
737         }
738     return retrieve_hold_queue_status_impl($e, $hold);
739 }
740
741 sub retrieve_hold_queue_status_impl {
742     my $e = shift;
743     my $hold = shift;
744
745     my $hold_ids = $e->search_action_hold_request(
746         [
747             {   target => $hold->target, 
748                 hold_type => $hold->hold_type,
749                 cancel_time => undef,
750                 capture_time => undef
751             },
752             {order_by => {ahr => 'request_time asc'}}
753         ], 
754         {idlist => 1} 
755     );
756
757     my $qpos = 0;
758     for my $hid (@$hold_ids) {
759         $qpos++;
760         last if $hid == $hold->id;
761     }
762
763     my $potentials = $e->search_action_hold_copy_map({hold => $hold->id}, {idlist => 1});
764     my $num_potentials = scalar(@$potentials);
765
766     my $user_org = $e->json_query({select => {au => 'home_ou'}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
767     my $default_hold_interval = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
768     my $estimated_wait = $qpos * ($default_hold_interval / $num_potentials) if $default_hold_interval;
769
770     return {
771         total_holds => scalar(@$hold_ids),
772         queue_position => $qpos,
773         potential_copies => $num_potentials,
774         status => _hold_status($e, $hold),
775         estimated_wait => int($estimated_wait)
776     };
777 }
778
779
780 sub fetch_open_hold_by_current_copy {
781         my $class = shift;
782         my $copyid = shift;
783         my $hold = $apputils->simplereq(
784                 'open-ils.cstore', 
785                 'open-ils.cstore.direct.action.hold_request.search.atomic',
786                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
787         return $hold->[0] if ref($hold);
788         return undef;
789 }
790
791 sub fetch_related_holds {
792         my $class = shift;
793         my $copyid = shift;
794         return $apputils->simplereq(
795                 'open-ils.cstore', 
796                 'open-ils.cstore.direct.action.hold_request.search.atomic',
797                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
798 }
799
800
801 __PACKAGE__->register_method (
802         method          => "hold_pull_list",
803         api_name                => "open-ils.circ.hold_pull_list.retrieve",
804         signature       => q/
805                 Returns a list of holds that need to be "pulled"
806                 by a given location
807         /
808 );
809
810 __PACKAGE__->register_method (
811         method          => "hold_pull_list",
812         api_name                => "open-ils.circ.hold_pull_list.id_list.retrieve",
813         signature       => q/
814                 Returns a list of hold ID's that need to be "pulled"
815                 by a given location
816         /
817 );
818
819
820 sub hold_pull_list {
821         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
822         my( $reqr, $evt ) = $U->checkses($authtoken);
823         return $evt if $evt;
824
825         my $org = $reqr->ws_ou || $reqr->home_ou;
826         # the perm locaiton shouldn't really matter here since holds
827         # will exist all over and VIEW_HOLDS should be universal
828         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
829         return $evt if $evt;
830
831         if( $self->api_name =~ /id_list/ ) {
832                 return $U->storagereq(
833                         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
834                         $org, $limit, $offset ); 
835         } else {
836                 return $U->storagereq(
837                         'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
838                         $org, $limit, $offset ); 
839         }
840 }
841
842 __PACKAGE__->register_method (
843         method          => 'fetch_hold_notify',
844         api_name                => 'open-ils.circ.hold_notification.retrieve_by_hold',
845         signature       => q/ 
846                 Returns a list of hold notification objects based on hold id.
847                 @param authtoken The loggin session key
848                 @param holdid The id of the hold whose notifications we want to retrieve
849                 @return An array of hold notification objects, event on error.
850         /
851 );
852
853 sub fetch_hold_notify {
854         my( $self, $conn, $authtoken, $holdid ) = @_;
855         my( $requestor, $evt ) = $U->checkses($authtoken);
856         return $evt if $evt;
857         my ($hold, $patron);
858         ($hold, $evt) = $U->fetch_hold($holdid);
859         return $evt if $evt;
860         ($patron, $evt) = $U->fetch_user($hold->usr);
861         return $evt if $evt;
862
863         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
864         return $evt if $evt;
865
866         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
867         return $U->cstorereq(
868                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
869 }
870
871
872 __PACKAGE__->register_method (
873         method          => 'create_hold_notify',
874         api_name                => 'open-ils.circ.hold_notification.create',
875         signature       => q/
876                 Creates a new hold notification object
877                 @param authtoken The login session key
878                 @param notification The hold notification object to create
879                 @return ID of the new object on success, Event on error
880                 /
881 );
882
883 sub create_hold_notify {
884    my( $self, $conn, $auth, $note ) = @_;
885    my $e = new_editor(authtoken=>$auth, xact=>1);
886    return $e->die_event unless $e->checkauth;
887
888    my $hold = $e->retrieve_action_hold_request($note->hold)
889       or return $e->die_event;
890    my $patron = $e->retrieve_actor_user($hold->usr) 
891       or return $e->die_event;
892
893    return $e->die_event unless 
894       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
895
896         $note->notify_staff($e->requestor->id);
897    $e->create_action_hold_notification($note) or return $e->die_event;
898    $e->commit;
899    return $note->id;
900 }
901
902
903 __PACKAGE__->register_method(
904         method  => 'reset_hold',
905         api_name        => 'open-ils.circ.hold.reset',
906         signature       => q/
907                 Un-captures and un-targets a hold, essentially returning
908                 it to the state it was in directly after it was placed,
909                 then attempts to re-target the hold
910                 @param authtoken The login session key
911                 @param holdid The id of the hold
912         /
913 );
914
915
916 sub reset_hold {
917         my( $self, $conn, $auth, $holdid ) = @_;
918         my $reqr;
919         my ($hold, $evt) = $U->fetch_hold($holdid);
920         return $evt if $evt;
921         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
922         return $evt if $evt;
923         $evt = _reset_hold($self, $reqr, $hold);
924         return $evt if $evt;
925         return 1;
926 }
927
928 sub _reset_hold {
929         my ($self, $reqr, $hold) = @_;
930
931         my $e = new_editor(xact =>1, requestor => $reqr);
932
933         $logger->info("reseting hold ".$hold->id);
934
935         my $hid = $hold->id;
936
937         if( $hold->capture_time and $hold->current_copy ) {
938
939                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
940                         or return $e->event;
941
942                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
943                         $logger->info("setting copy to status 'reshelving' on hold retarget");
944                         $copy->status(OILS_COPY_STATUS_RESHELVING);
945                         $copy->editor($e->requestor->id);
946                         $copy->edit_date('now');
947                         $e->update_asset_copy($copy) or return $e->event;
948
949                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
950
951                         # We don't want the copy to remain "in transit"
952                         $copy->status(OILS_COPY_STATUS_RESHELVING);
953                         $logger->warn("! reseting hold [$hid] that is in transit");
954                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
955
956                         if( $transid ) {
957                                 my $trans = $e->retrieve_action_transit_copy($transid);
958                                 if( $trans ) {
959                                         $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
960                                         my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
961                                         $logger->info("Transit abort completed with result $evt");
962                                         return $evt unless "$evt" eq 1;
963                                 }
964                         }
965                 }
966         }
967
968         $hold->clear_capture_time;
969         $hold->clear_current_copy;
970
971         $e->update_action_hold_request($hold) or return $e->event;
972         $e->commit;
973
974         $U->storagereq(
975                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
976
977         return undef;
978 }
979
980
981 __PACKAGE__->register_method(
982         method => 'fetch_open_title_holds',
983         api_name        => 'open-ils.circ.open_holds.retrieve',
984         signature       => q/
985                 Returns a list ids of un-fulfilled holds for a given title id
986                 @param authtoken The login session key
987                 @param id the id of the item whose holds we want to retrieve
988                 @param type The hold type - M, T, V, C
989         /
990 );
991
992 sub fetch_open_title_holds {
993         my( $self, $conn, $auth, $id, $type, $org ) = @_;
994         my $e = new_editor( authtoken => $auth );
995         return $e->event unless $e->checkauth;
996
997         $type ||= "T";
998         $org ||= $e->requestor->ws_ou;
999
1000 #       return $e->search_action_hold_request(
1001 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
1002
1003         # XXX make me return IDs in the future ^--
1004         my $holds = $e->search_action_hold_request(
1005                 { 
1006                         target                          => $id, 
1007                         cancel_time                     => undef, 
1008                         hold_type                       => $type, 
1009                         fulfillment_time        => undef 
1010                 }
1011         );
1012
1013         flesh_hold_transits($holds);
1014         return $holds;
1015 }
1016
1017
1018 sub flesh_hold_transits {
1019         my $holds = shift;
1020         for my $hold ( @$holds ) {
1021                 $hold->transit(
1022                         $apputils->simplereq(
1023                                 'open-ils.cstore',
1024                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
1025                                 { hold => $hold->id },
1026                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
1027                         )->[0]
1028                 );
1029         }
1030 }
1031
1032 sub flesh_hold_notices {
1033         my( $holds, $e ) = @_;
1034         $e ||= new_editor();
1035
1036         for my $hold (@$holds) {
1037                 my $notices = $e->search_action_hold_notification(
1038                         [
1039                                 { hold => $hold->id },
1040                                 { order_by => { anh => 'notify_time desc' } },
1041                         ],
1042                         {idlist=>1}
1043                 );
1044
1045                 $hold->notify_count(scalar(@$notices));
1046                 if( @$notices ) {
1047                         my $n = $e->retrieve_action_hold_notification($$notices[0])
1048                                 or return $e->event;
1049                         $hold->notify_time($n->notify_time);
1050                 }
1051         }
1052 }
1053
1054
1055 __PACKAGE__->register_method(
1056         method => 'fetch_captured_holds',
1057         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1058     stream => 1,
1059         signature       => q/
1060                 Returns a list of un-fulfilled holds for a given title id
1061                 @param authtoken The login session key
1062                 @param org The org id of the location in question
1063         /
1064 );
1065
1066 __PACKAGE__->register_method(
1067         method => 'fetch_captured_holds',
1068         api_name        => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1069     stream => 1,
1070         signature       => q/
1071                 Returns a list ids of un-fulfilled holds for a given title id
1072                 @param authtoken The login session key
1073                 @param org The org id of the location in question
1074         /
1075 );
1076
1077 sub fetch_captured_holds {
1078         my( $self, $conn, $auth, $org ) = @_;
1079
1080         my $e = new_editor(authtoken => $auth);
1081         return $e->event unless $e->checkauth;
1082         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1083
1084         $org ||= $e->requestor->ws_ou;
1085
1086     my $hold_ids = $e->json_query(
1087         { 
1088             select => { ahr => ['id'] },
1089             from => {
1090                 ahr => {
1091                     acp => {
1092                         field => 'id',
1093                         fkey => 'current_copy'
1094                     },
1095                 }
1096             }, 
1097             where => {
1098                 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1099                 '+ahr' => {
1100                     capture_time                => { "!=" => undef },
1101                     current_copy                => { "!=" => undef },
1102                     fulfillment_time    => undef,
1103                     pickup_lib                  => $org,
1104                     cancel_time                 => undef,
1105                 }
1106             }
1107         },
1108     );
1109
1110     for my $hold_id (@$hold_ids) {
1111         if($self->api_name =~ /id_list/) {
1112             $conn->respond($hold_id->{id});
1113             next;
1114         } else {
1115             $conn->respond(
1116                 $e->retrieve_action_hold_request([
1117                     $hold_id->{id},
1118                     {
1119                         flesh => 1,
1120                         flesh_fields => {ahr => ['notifications', 'transit']},
1121                         order_by => {anh => 'notify_time desc'}
1122                     }
1123                 ])
1124             );
1125         }
1126     }
1127
1128     return undef;
1129 }
1130 __PACKAGE__->register_method(
1131         method  => "check_title_hold",
1132         api_name        => "open-ils.circ.title_hold.is_possible",
1133         notes           => q/
1134                 Determines if a hold were to be placed by a given user,
1135                 whether or not said hold would have any potential copies
1136                 to fulfill it.
1137                 @param authtoken The login session key
1138                 @param params A hash of named params including:
1139                         patronid  - the id of the hold recipient
1140                         titleid (brn) - the id of the title to be held
1141                         depth   - the hold range depth (defaults to 0)
1142         /);
1143
1144 sub check_title_hold {
1145         my( $self, $client, $authtoken, $params ) = @_;
1146
1147         my %params              = %$params;
1148         my $titleid             = $params{titleid} ||"";
1149         my $volid               = $params{volume_id};
1150         my $copyid              = $params{copy_id};
1151         my $mrid                = $params{mrid} ||"";
1152         my $depth               = $params{depth} || 0;
1153         my $pickup_lib  = $params{pickup_lib};
1154         my $hold_type   = $params{hold_type} || 'T';
1155     my $selection_ou = $params{selection_ou} || $pickup_lib;
1156
1157         my $e = new_editor(authtoken=>$authtoken);
1158         return $e->event unless $e->checkauth;
1159         my $patron = $e->retrieve_actor_user($params{patronid})
1160                 or return $e->event;
1161
1162         if( $e->requestor->id ne $patron->id ) {
1163                 return $e->event unless 
1164                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1165         }
1166
1167         return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1168
1169         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1170                 or return $e->event;
1171
1172     my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1173     my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1174
1175     if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1176         # work up the tree and as soon as we find a potential copy, use that depth
1177         # also, make sure we don't go past the hard boundary if it exists
1178
1179         # our min boundary is the greater of user-specified boundary or hard boundary
1180         my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?  
1181             $hard_boundary : $$params{depth};
1182
1183         my $depth = $soft_boundary;
1184         while($depth >= $min_depth) {
1185             $logger->info("performing hold possibility check with soft boundary $depth");
1186             return {success => 1, depth => $depth}
1187                 if do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1188             $depth--;
1189         }
1190         return {success => 0};
1191
1192     } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1193         # there is no soft boundary, enforce the hard boundary if it exists
1194         $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1195         if(do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params)) {
1196             return {success => 1, depth => $hard_boundary}
1197         } else {
1198             return {success => 0};
1199         }
1200
1201     } else {
1202         # no boundaries defined, fall back to user specifed boundary or no boundary
1203         $logger->info("performing hold possibility check with no boundary");
1204         if(do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params)) {
1205             return {success => 1, depth => $hard_boundary};
1206         } else {
1207             return {success => 0};
1208         }
1209     }
1210 }
1211
1212 sub do_possibility_checks {
1213     my($e, $patron, $request_lib, $depth, %params) = @_;
1214
1215         my $titleid             = $params{titleid} ||"";
1216         my $volid               = $params{volume_id};
1217         my $copyid              = $params{copy_id};
1218         my $mrid                = $params{mrid} ||"";
1219         my $pickup_lib  = $params{pickup_lib};
1220         my $hold_type   = $params{hold_type} || 'T';
1221     my $selection_ou = $params{selection_ou} || $pickup_lib;
1222
1223
1224         my $copy;
1225         my $volume;
1226         my $title;
1227
1228         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1229
1230                 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1231                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1232                         or return $e->event;
1233                 $title = $e->retrieve_biblio_record_entry($volume->record)
1234                         or return $e->event;
1235                 return verify_copy_for_hold( 
1236                         $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1237
1238         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1239
1240                 $volume = $e->retrieve_asset_call_number($volid)
1241                         or return $e->event;
1242                 $title = $e->retrieve_biblio_record_entry($volume->record)
1243                         or return $e->event;
1244
1245                 return _check_volume_hold_is_possible(
1246                         $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1247
1248         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1249
1250                 return _check_title_hold_is_possible(
1251                         $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1252
1253         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1254
1255                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1256                 my @recs = map { $_->source } @$maps;
1257                 for my $rec (@recs) {
1258                         return 1 if (_check_title_hold_is_possible(
1259                                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou));
1260                 }
1261                 return 0;       
1262         }
1263 }
1264
1265 my %prox_cache;
1266
1267 sub _check_metarecord_hold_is_possible {
1268         my( $mrid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1269    
1270    my $e = new_editor();
1271
1272     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given metarecord
1273     my $copies = $e->json_query(
1274         { 
1275             select => { acp => ['id', 'circ_lib'] },
1276             from => {
1277                 acp => {
1278                     acn => {
1279                         field => 'id',
1280                         fkey => 'call_number',
1281                         'join' => {
1282                             mmrsm => {
1283                                 field => 'source',
1284                                 fkey => 'record',
1285                                 filter => { metarecord => $mrid }
1286                             }
1287                         }
1288                     },
1289                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1290                     ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1291                 }
1292             }, 
1293             where => {
1294                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't' }
1295             }
1296         }
1297     );
1298
1299    return $e->event unless defined $copies;
1300    $logger->info("metarecord possible found ".scalar(@$copies)." potential copies");
1301    return 0 unless @$copies;
1302
1303    # -----------------------------------------------------------------------
1304    # sort the copies into buckets based on their circ_lib proximity to 
1305    # the patron's home_ou.  
1306    # -----------------------------------------------------------------------
1307
1308    my $home_org = $patron->home_ou;
1309    my $req_org = $request_lib->id;
1310
1311     $prox_cache{$home_org} = 
1312         $e->search_actor_org_unit_proximity({from_org => $home_org})
1313         unless $prox_cache{$home_org};
1314     my $home_prox = $prox_cache{$home_org};
1315
1316    my %buckets;
1317    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1318    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1319
1320    my @keys = sort { $a <=> $b } keys %buckets;
1321
1322
1323    if( $home_org ne $req_org ) {
1324       # -----------------------------------------------------------------------
1325       # shove the copies close to the request_lib into the primary buckets 
1326       # directly before the farthest away copies.  That way, they are not 
1327       # given priority, but they are checked before the farthest copies.
1328       # -----------------------------------------------------------------------
1329
1330         $prox_cache{$req_org} = 
1331             $e->search_actor_org_unit_proximity({from_org => $req_org})
1332             unless $prox_cache{$req_org};
1333         my $req_prox = $prox_cache{$req_org};
1334
1335       my %buckets2;
1336       my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1337       push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1338
1339       my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1340       my $new_key = $highest_key - 0.5; # right before the farthest prox
1341       my @keys2 = sort { $a <=> $b } keys %buckets2;
1342       for my $key (@keys2) {
1343          last if $key >= $highest_key;
1344          push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1345       }
1346    }
1347
1348    @keys = sort { $a <=> $b } keys %buckets;
1349
1350    my %seen;
1351    for my $key (@keys) {
1352       my @cps = @{$buckets{$key}};
1353
1354       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1355
1356       for my $copyid (@cps) {
1357
1358          next if $seen{$copyid};
1359          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1360          my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1361          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1362
1363          my $vol = $e->retrieve_asset_call_number(
1364            [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1365
1366          return 1 if verify_copy_for_hold( 
1367             $patron, $requestor, $vol->record, $copy, $pickup_lib, $request_lib );
1368    
1369       }
1370    }
1371
1372    return 0;
1373 }
1374
1375 sub create_ranged_org_filter {
1376     my($e, $selection_ou, $depth) = @_;
1377
1378     # find the orgs from which this hold may be fulfilled, 
1379     # based on the selection_ou and depth
1380
1381     my $top_org = $e->search_actor_org_unit([
1382         {parent_ou => undef}, 
1383         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1384     my %org_filter;
1385
1386     return () if $depth == $top_org->ou_type->depth;
1387
1388     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1389     %org_filter = (circ_lib => []);
1390     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1391
1392     $logger->info("hold org filter at depth $depth and selection_ou ".
1393         "$selection_ou created list of @{$org_filter{circ_lib}}");
1394
1395     return %org_filter;
1396 }
1397
1398
1399 sub _check_title_hold_is_possible {
1400         my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1401    
1402     my $e = new_editor();
1403     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1404
1405     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1406     my $copies = $e->json_query(
1407         { 
1408             select => { acp => ['id', 'circ_lib'] },
1409             from => {
1410                 acp => {
1411                     acn => {
1412                         field => 'id',
1413                         fkey => 'call_number',
1414                         'join' => {
1415                             bre => {
1416                                 field => 'id',
1417                                 filter => { id => $titleid },
1418                                 fkey => 'record'
1419                             }
1420                         }
1421                     },
1422                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1423                     ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1424                 }
1425             }, 
1426             where => {
1427                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1428             }
1429         }
1430     );
1431
1432    return $e->event unless defined $copies;
1433    $logger->info("title possible found ".scalar(@$copies)." potential copies");
1434    return 0 unless @$copies;
1435
1436    # -----------------------------------------------------------------------
1437    # sort the copies into buckets based on their circ_lib proximity to 
1438    # the patron's home_ou.  
1439    # -----------------------------------------------------------------------
1440
1441    my $home_org = $patron->home_ou;
1442    my $req_org = $request_lib->id;
1443
1444     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1445
1446     $prox_cache{$home_org} = 
1447         $e->search_actor_org_unit_proximity({from_org => $home_org})
1448         unless $prox_cache{$home_org};
1449     my $home_prox = $prox_cache{$home_org};
1450
1451    my %buckets;
1452    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1453    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1454
1455    my @keys = sort { $a <=> $b } keys %buckets;
1456
1457
1458    if( $home_org ne $req_org ) {
1459       # -----------------------------------------------------------------------
1460       # shove the copies close to the request_lib into the primary buckets 
1461       # directly before the farthest away copies.  That way, they are not 
1462       # given priority, but they are checked before the farthest copies.
1463       # -----------------------------------------------------------------------
1464         $prox_cache{$req_org} = 
1465             $e->search_actor_org_unit_proximity({from_org => $req_org})
1466             unless $prox_cache{$req_org};
1467         my $req_prox = $prox_cache{$req_org};
1468
1469
1470       my %buckets2;
1471       my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1472       push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1473
1474       my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1475       my $new_key = $highest_key - 0.5; # right before the farthest prox
1476       my @keys2 = sort { $a <=> $b } keys %buckets2;
1477       for my $key (@keys2) {
1478          last if $key >= $highest_key;
1479          push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1480       }
1481    }
1482
1483    @keys = sort { $a <=> $b } keys %buckets;
1484
1485    my $title;
1486    my %seen;
1487    for my $key (@keys) {
1488       my @cps = @{$buckets{$key}};
1489
1490       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1491
1492       for my $copyid (@cps) {
1493
1494          next if $seen{$copyid};
1495          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1496          my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1497          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1498
1499          unless($title) { # grab the title if we don't already have it
1500             my $vol = $e->retrieve_asset_call_number(
1501                [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1502             $title = $vol->record;
1503          }
1504    
1505          return 1 if verify_copy_for_hold( 
1506             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1507    
1508       }
1509    }
1510
1511    return 0;
1512 }
1513
1514
1515 sub _check_volume_hold_is_possible {
1516         my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1517     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1518         my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1519         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1520         for my $copy ( @$copies ) {
1521                 return 1 if verify_copy_for_hold( 
1522                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1523         }
1524         return 0;
1525 }
1526
1527
1528
1529 sub verify_copy_for_hold {
1530         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1531         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1532         return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1533                 {       patron                          => $patron, 
1534                         requestor                       => $requestor, 
1535                         copy                            => $copy,
1536                         title                           => $title, 
1537                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1538                         pickup_lib                      => $pickup_lib,
1539                         request_lib                     => $request_lib,
1540             new_hold            => 1
1541                 } 
1542         );
1543         return 0;
1544 }
1545
1546
1547
1548 sub find_nearest_permitted_hold {
1549
1550         my $class       = shift;
1551         my $editor      = shift; # CStoreEditor object
1552         my $copy                = shift; # copy to target
1553         my $user                = shift; # staff 
1554         my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1555         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1556
1557         my $bc = $copy->barcode;
1558
1559         # find any existing holds that already target this copy
1560         my $old_holds = $editor->search_action_hold_request(
1561                 {       current_copy => $copy->id, 
1562                         cancel_time => undef, 
1563                         capture_time => undef 
1564                 } 
1565         );
1566
1567         # hold->type "R" means we need this copy
1568         for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1569
1570
1571     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1572
1573         $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1574         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1575
1576         # search for what should be the best holds for this copy to fulfill
1577         my $best_holds = $U->storagereq(
1578                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1579                 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1580
1581         unless(@$best_holds) {
1582
1583                 if( my $hold = $$old_holds[0] ) {
1584                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1585                         return ($hold);
1586                 }
1587
1588                 $logger->info("circulator: no suitable holds found for copy $bc");
1589                 return (undef, $evt);
1590         }
1591
1592
1593         my $best_hold;
1594
1595         # for each potential hold, we have to run the permit script
1596         # to make sure the hold is actually permitted.
1597         for my $holdid (@$best_holds) {
1598                 next unless $holdid;
1599                 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1600
1601                 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1602                 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1603                 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1604
1605                 # see if this hold is permitted
1606                 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1607                         {       patron_id                       => $hold->usr,
1608                                 requestor                       => $reqr,
1609                                 copy                            => $copy,
1610                                 pickup_lib                      => $hold->pickup_lib,
1611                                 request_lib                     => $rlib,
1612                         } 
1613                 );
1614
1615                 if( $permitted ) {
1616                         $best_hold = $hold;
1617                         last;
1618                 }
1619         }
1620
1621
1622         unless( $best_hold ) { # no "good" permitted holds were found
1623                 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1624                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1625                         return ($hold);
1626                 }
1627
1628                 # we got nuthin
1629                 $logger->info("circulator: no suitable holds found for copy $bc");
1630                 return (undef, $evt);
1631         }
1632
1633         $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1634
1635         # indicate a permitted hold was found
1636         return $best_hold if $check_only;
1637
1638         # we've found a permitted hold.  we need to "grab" the copy 
1639         # to prevent re-targeted holds (next part) from re-grabbing the copy
1640         $best_hold->current_copy($copy->id);
1641         $editor->update_action_hold_request($best_hold) 
1642                 or return (undef, $editor->event);
1643
1644
1645     my $retarget = 0;
1646
1647         # re-target any other holds that already target this copy
1648         for my $old_hold (@$old_holds) {
1649                 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1650                 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1651             $old_hold->id." after a better hold [".$best_hold->id."] was found");
1652         $old_hold->clear_current_copy;
1653         $old_hold->clear_prev_check_time;
1654         $editor->update_action_hold_request($old_hold) 
1655             or return (undef, $editor->event);
1656         $retarget = 1;
1657         }
1658
1659         return ($best_hold, undef, $retarget);
1660 }
1661
1662
1663
1664
1665
1666
1667 __PACKAGE__->register_method(
1668         method => 'all_rec_holds',
1669         api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1670 );
1671
1672 sub all_rec_holds {
1673         my( $self, $conn, $auth, $title_id, $args ) = @_;
1674
1675         my $e = new_editor(authtoken=>$auth);
1676         $e->checkauth or return $e->event;
1677         $e->allowed('VIEW_HOLD') or return $e->event;
1678
1679         $args ||= { fulfillment_time => undef };
1680         $args->{cancel_time} = undef;
1681
1682         my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1683
1684     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1685     if($mr_map) {
1686         $resp->{metarecord_holds} = $e->search_action_hold_request(
1687             {   hold_type => OILS_HOLD_TYPE_METARECORD,
1688                 target => $mr_map->metarecord,
1689                 %$args 
1690             }, {idlist => 1}
1691         );
1692     }
1693
1694         $resp->{title_holds} = $e->search_action_hold_request(
1695                 { 
1696                         hold_type => OILS_HOLD_TYPE_TITLE, 
1697                         target => $title_id, 
1698                         %$args 
1699                 }, {idlist=>1} );
1700
1701         my $vols = $e->search_asset_call_number(
1702                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1703
1704         return $resp unless @$vols;
1705
1706         $resp->{volume_holds} = $e->search_action_hold_request(
1707                 { 
1708                         hold_type => OILS_HOLD_TYPE_VOLUME, 
1709                         target => $vols,
1710                         %$args }, 
1711                 {idlist=>1} );
1712
1713         my $copies = $e->search_asset_copy(
1714                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1715
1716         return $resp unless @$copies;
1717
1718         $resp->{copy_holds} = $e->search_action_hold_request(
1719                 { 
1720                         hold_type => OILS_HOLD_TYPE_COPY,
1721                         target => $copies,
1722                         %$args }, 
1723                 {idlist=>1} );
1724
1725         return $resp;
1726 }
1727
1728
1729
1730
1731
1732 __PACKAGE__->register_method(
1733         method => 'uber_hold',
1734     authoritative => 1,
1735         api_name => 'open-ils.circ.hold.details.retrieve'
1736 );
1737
1738 sub uber_hold {
1739         my($self, $client, $auth, $hold_id) = @_;
1740         my $e = new_editor(authtoken=>$auth);
1741         $e->checkauth or return $e->event;
1742         $e->allowed('VIEW_HOLD') or return $e->event;
1743
1744         my $resp = {};
1745
1746         my $hold = $e->retrieve_action_hold_request(
1747                 [
1748                         $hold_id,
1749                         {
1750                                 flesh => 1,
1751                                 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1752                         }
1753                 ]
1754         ) or return $e->event;
1755
1756         my $user = $hold->usr;
1757         $hold->usr($user->id);
1758
1759         my $card = $e->retrieve_actor_card($user->card)
1760                 or return $e->event;
1761
1762         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1763
1764         flesh_hold_notices([$hold], $e);
1765         flesh_hold_transits([$hold]);
1766
1767         return {
1768                 hold            => $hold,
1769                 copy            => $copy,
1770                 volume  => $volume,
1771                 mvr             => $mvr,
1772                 status  => _hold_status($e, $hold),
1773                 patron_first => $user->first_given_name,
1774                 patron_last  => $user->family_name,
1775                 patron_barcode => $card->barcode,
1776         };
1777 }
1778
1779
1780
1781 # -----------------------------------------------------
1782 # Returns the MVR object that represents what the
1783 # hold is all about
1784 # -----------------------------------------------------
1785 sub find_hold_mvr {
1786         my( $e, $hold ) = @_;
1787
1788         my $tid;
1789         my $copy;
1790         my $volume;
1791
1792         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1793                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1794                         or return $e->event;
1795                 $tid = $mr->master_record;
1796
1797         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1798                 $tid = $hold->target;
1799
1800         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1801                 $volume = $e->retrieve_asset_call_number($hold->target)
1802                         or return $e->event;
1803                 $tid = $volume->record;
1804
1805         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1806                 $copy = $e->retrieve_asset_copy($hold->target)
1807                         or return $e->event;
1808                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1809                         or return $e->event;
1810                 $tid = $volume->record;
1811         }
1812
1813         if(!$copy and ref $hold->current_copy ) {
1814                 $copy = $hold->current_copy;
1815                 $hold->current_copy($copy->id);
1816         }
1817
1818         if(!$volume and $copy) {
1819                 $volume = $e->retrieve_asset_call_number($copy->call_number);
1820         }
1821
1822         my $title = $e->retrieve_biblio_record_entry($tid);
1823         return ( $U->record_to_mvr($title), $volume, $copy );
1824 }
1825
1826
1827
1828
1829 1;