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