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