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