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