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