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