implemented soft hold 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 and $$params{depth} < $soft_ceiling) {
1032         my $depth = $soft_ceiling;
1033         # work up the tree and as soon as we find a potential copy, use that depth
1034         while($depth >= $$params{depth}) {
1035             $logger->info("performing hold possibility check with soft ceiling $depth");
1036             return {success => 1, depth => $depth}
1037                 if do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1038             $depth--;
1039         }
1040         return {success => 0};
1041
1042     } else {
1043         $logger->info("performing hold possibility check with no ceiling");
1044         if(do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params)) {
1045             return {success => 1, depth => $hard_ceiling};
1046         } else {
1047             return {success => 0};
1048         }
1049     }
1050 }
1051
1052 sub do_possibility_checks {
1053     my($e, $patron, $request_lib, $depth, %params) = @_;
1054
1055         my $titleid             = $params{titleid} ||"";
1056         my $volid               = $params{volume_id};
1057         my $copyid              = $params{copy_id};
1058         my $mrid                = $params{mrid} ||"";
1059         my $pickup_lib  = $params{pickup_lib};
1060         my $hold_type   = $params{hold_type} || 'T';
1061     my $selection_ou = $params{selection_ou} || $pickup_lib;
1062
1063
1064         my $copy;
1065         my $volume;
1066         my $title;
1067
1068         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1069
1070                 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1071                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1072                         or return $e->event;
1073                 $title = $e->retrieve_biblio_record_entry($volume->record)
1074                         or return $e->event;
1075                 return verify_copy_for_hold( 
1076                         $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1077
1078         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1079
1080                 $volume = $e->retrieve_asset_call_number($volid)
1081                         or return $e->event;
1082                 $title = $e->retrieve_biblio_record_entry($volume->record)
1083                         or return $e->event;
1084
1085                 return _check_volume_hold_is_possible(
1086                         $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1087
1088         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1089
1090                 return _check_title_hold_is_possible(
1091                         $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1092
1093         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1094
1095                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1096                 my @recs = map { $_->source } @$maps;
1097                 for my $rec (@recs) {
1098                         return 1 if (_check_title_hold_is_possible(
1099                                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou));
1100                 }
1101                 return 0;       
1102         }
1103 }
1104
1105 my %prox_cache;
1106
1107 sub _check_metarecord_hold_is_possible {
1108         my( $mrid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1109    
1110    my $e = new_editor();
1111
1112     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given metarecord
1113     my $copies = $e->json_query(
1114         { 
1115             select => { acp => ['id', 'circ_lib'] },
1116             from => {
1117                 acp => {
1118                     acn => {
1119                         field => 'id',
1120                         fkey => 'call_number',
1121                         'join' => {
1122                             mmrsm => {
1123                                 field => 'source',
1124                                 fkey => 'record',
1125                                 filter => { metarecord => $mrid }
1126                             }
1127                         }
1128                     },
1129                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1130                     ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1131                 }
1132             }, 
1133             where => {
1134                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't' }
1135             }
1136         }
1137     );
1138
1139    return $e->event unless defined $copies;
1140    $logger->info("metarecord possible found ".scalar(@$copies)." potential copies");
1141    return 0 unless @$copies;
1142
1143    # -----------------------------------------------------------------------
1144    # sort the copies into buckets based on their circ_lib proximity to 
1145    # the patron's home_ou.  
1146    # -----------------------------------------------------------------------
1147
1148    my $home_org = $patron->home_ou;
1149    my $req_org = $request_lib->id;
1150
1151    my $home_prox = 
1152       ($prox_cache{$home_org}) ? 
1153          $prox_cache{$home_org} :
1154          $prox_cache{$home_org} = $e->search_actor_org_unit_proximity({from_org => $home_org});
1155
1156    my %buckets;
1157    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1158    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1159
1160    my @keys = sort { $a <=> $b } keys %buckets;
1161
1162
1163    if( $home_org ne $req_org ) {
1164       # -----------------------------------------------------------------------
1165       # shove the copies close to the request_lib into the primary buckets 
1166       # directly before the farthest away copies.  That way, they are not 
1167       # given priority, but they are checked before the farthest copies.
1168       # -----------------------------------------------------------------------
1169       my $req_prox = 
1170          ($prox_cache{$req_org}) ? 
1171             $prox_cache{$req_org} :
1172             $prox_cache{$req_org} = $e->search_actor_org_unit_proximity({from_org => $req_org});
1173
1174       my %buckets2;
1175       my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1176       push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1177
1178       my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1179       my $new_key = $highest_key - 0.5; # right before the farthest prox
1180       my @keys2 = sort { $a <=> $b } keys %buckets2;
1181       for my $key (@keys2) {
1182          last if $key >= $highest_key;
1183          push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1184       }
1185    }
1186
1187    @keys = sort { $a <=> $b } keys %buckets;
1188
1189    my %seen;
1190    for my $key (@keys) {
1191       my @cps = @{$buckets{$key}};
1192
1193       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1194
1195       for my $copyid (@cps) {
1196
1197          next if $seen{$copyid};
1198          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1199          my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1200          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1201
1202          my $vol = $e->retrieve_asset_call_number(
1203            [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1204
1205          return 1 if verify_copy_for_hold( 
1206             $patron, $requestor, $vol->record, $copy, $pickup_lib, $request_lib );
1207    
1208       }
1209    }
1210
1211    return 0;
1212 }
1213
1214 sub create_ranged_org_filter {
1215     my($e, $selection_ou, $depth) = @_;
1216
1217     # find the orgs from which this hold may be fulfilled, 
1218     # based on the selection_ou and depth
1219
1220     my $top_org = $e->search_actor_org_unit([
1221         {parent_ou => undef}, 
1222         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1223     my %org_filter;
1224
1225     return () if $depth == $top_org->ou_type->depth;
1226
1227     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1228     %org_filter = (circ_lib => []);
1229     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1230
1231     $logger->info("hold org filter at depth $depth and selection_ou ".
1232         "$selection_ou created list of @{$org_filter{circ_lib}}");
1233
1234     return %org_filter;
1235 }
1236
1237
1238 sub _check_title_hold_is_possible {
1239         my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1240    
1241     my $e = new_editor();
1242     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1243
1244     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1245     my $copies = $e->json_query(
1246         { 
1247             select => { acp => ['id', 'circ_lib'] },
1248             from => {
1249                 acp => {
1250                     acn => {
1251                         field => 'id',
1252                         fkey => 'call_number',
1253                         'join' => {
1254                             bre => {
1255                                 field => 'id',
1256                                 filter => { id => $titleid },
1257                                 fkey => 'record'
1258                             }
1259                         }
1260                     },
1261                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1262                     ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1263                 }
1264             }, 
1265             where => {
1266                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1267             }
1268         }
1269     );
1270
1271    return $e->event unless defined $copies;
1272    $logger->info("title possible found ".scalar(@$copies)." potential copies");
1273    return 0 unless @$copies;
1274
1275    # -----------------------------------------------------------------------
1276    # sort the copies into buckets based on their circ_lib proximity to 
1277    # the patron's home_ou.  
1278    # -----------------------------------------------------------------------
1279
1280    my $home_org = $patron->home_ou;
1281    my $req_org = $request_lib->id;
1282
1283    my $home_prox = 
1284       ($prox_cache{$home_org}) ? 
1285          $prox_cache{$home_org} :
1286          $prox_cache{$home_org} = $e->search_actor_org_unit_proximity({from_org => $home_org});
1287
1288    my %buckets;
1289    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1290    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1291
1292    my @keys = sort { $a <=> $b } keys %buckets;
1293
1294
1295    if( $home_org ne $req_org ) {
1296       # -----------------------------------------------------------------------
1297       # shove the copies close to the request_lib into the primary buckets 
1298       # directly before the farthest away copies.  That way, they are not 
1299       # given priority, but they are checked before the farthest copies.
1300       # -----------------------------------------------------------------------
1301       my $req_prox = 
1302          ($prox_cache{$req_org}) ? 
1303             $prox_cache{$req_org} :
1304             $prox_cache{$req_org} = $e->search_actor_org_unit_proximity({from_org => $req_org});
1305
1306       my %buckets2;
1307       my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1308       push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1309
1310       my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1311       my $new_key = $highest_key - 0.5; # right before the farthest prox
1312       my @keys2 = sort { $a <=> $b } keys %buckets2;
1313       for my $key (@keys2) {
1314          last if $key >= $highest_key;
1315          push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1316       }
1317    }
1318
1319    @keys = sort { $a <=> $b } keys %buckets;
1320
1321    my $title;
1322    my %seen;
1323    for my $key (@keys) {
1324       my @cps = @{$buckets{$key}};
1325
1326       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1327
1328       for my $copyid (@cps) {
1329
1330          next if $seen{$copyid};
1331          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1332          my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1333          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1334
1335          unless($title) { # grab the title if we don't already have it
1336             my $vol = $e->retrieve_asset_call_number(
1337                [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1338             $title = $vol->record;
1339          }
1340    
1341          return 1 if verify_copy_for_hold( 
1342             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1343    
1344       }
1345    }
1346
1347    return 0;
1348 }
1349
1350
1351 sub _check_volume_hold_is_possible {
1352         my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1353     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1354         my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1355         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1356         for my $copy ( @$copies ) {
1357                 return 1 if verify_copy_for_hold( 
1358                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1359         }
1360         return 0;
1361 }
1362
1363
1364
1365 sub verify_copy_for_hold {
1366         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1367         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1368         return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1369                 {       patron                          => $patron, 
1370                         requestor                       => $requestor, 
1371                         copy                            => $copy,
1372                         title                           => $title, 
1373                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1374                         pickup_lib                      => $pickup_lib,
1375                         request_lib                     => $request_lib,
1376             new_hold            => 1
1377                 } 
1378         );
1379         return 0;
1380 }
1381
1382
1383
1384 sub find_nearest_permitted_hold {
1385
1386         my $class       = shift;
1387         my $editor      = shift; # CStoreEditor object
1388         my $copy                = shift; # copy to target
1389         my $user                = shift; # staff 
1390         my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1391         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1392
1393         my $bc = $copy->barcode;
1394
1395         # find any existing holds that already target this copy
1396         my $old_holds = $editor->search_action_hold_request(
1397                 {       current_copy => $copy->id, 
1398                         cancel_time => undef, 
1399                         capture_time => undef 
1400                 } 
1401         );
1402
1403         # hold->type "R" means we need this copy
1404         for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1405
1406
1407     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1408
1409         $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1410         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1411
1412         # search for what should be the best holds for this copy to fulfill
1413         my $best_holds = $U->storagereq(
1414                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1415                 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1416
1417         unless(@$best_holds) {
1418
1419                 if( my $hold = $$old_holds[0] ) {
1420                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1421                         return ($hold);
1422                 }
1423
1424                 $logger->info("circulator: no suitable holds found for copy $bc");
1425                 return (undef, $evt);
1426         }
1427
1428
1429         my $best_hold;
1430
1431         # for each potential hold, we have to run the permit script
1432         # to make sure the hold is actually permitted.
1433         for my $holdid (@$best_holds) {
1434                 next unless $holdid;
1435                 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1436
1437                 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1438                 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1439                 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1440
1441                 # see if this hold is permitted
1442                 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1443                         {       patron_id                       => $hold->usr,
1444                                 requestor                       => $reqr,
1445                                 copy                            => $copy,
1446                                 pickup_lib                      => $hold->pickup_lib,
1447                                 request_lib                     => $rlib,
1448                         } 
1449                 );
1450
1451                 if( $permitted ) {
1452                         $best_hold = $hold;
1453                         last;
1454                 }
1455         }
1456
1457
1458         unless( $best_hold ) { # no "good" permitted holds were found
1459                 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1460                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1461                         return ($hold);
1462                 }
1463
1464                 # we got nuthin
1465                 $logger->info("circulator: no suitable holds found for copy $bc");
1466                 return (undef, $evt);
1467         }
1468
1469         $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1470
1471         # indicate a permitted hold was found
1472         return $best_hold if $check_only;
1473
1474         # we've found a permitted hold.  we need to "grab" the copy 
1475         # to prevent re-targeted holds (next part) from re-grabbing the copy
1476         $best_hold->current_copy($copy->id);
1477         $editor->update_action_hold_request($best_hold) 
1478                 or return (undef, $editor->event);
1479
1480
1481     my $retarget = 0;
1482
1483         # re-target any other holds that already target this copy
1484         for my $old_hold (@$old_holds) {
1485                 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1486                 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1487             $old_hold->id." after a better hold [".$best_hold->id."] was found");
1488         $old_hold->clear_current_copy;
1489         $old_hold->clear_prev_check_time;
1490         $editor->update_action_hold_request($old_hold) 
1491             or return (undef, $editor->event);
1492         $retarget = 1;
1493         }
1494
1495         return ($best_hold, undef, $retarget);
1496 }
1497
1498
1499
1500
1501
1502
1503 __PACKAGE__->register_method(
1504         method => 'all_rec_holds',
1505         api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1506 );
1507
1508 sub all_rec_holds {
1509         my( $self, $conn, $auth, $title_id, $args ) = @_;
1510
1511         my $e = new_editor(authtoken=>$auth);
1512         $e->checkauth or return $e->event;
1513         $e->allowed('VIEW_HOLD') or return $e->event;
1514
1515         $args ||= { fulfillment_time => undef };
1516         $args->{cancel_time} = undef;
1517
1518         my $resp = { volume_holds => [], copy_holds => [] };
1519
1520         $resp->{title_holds} = $e->search_action_hold_request(
1521                 { 
1522                         hold_type => OILS_HOLD_TYPE_TITLE, 
1523                         target => $title_id, 
1524                         %$args 
1525                 }, {idlist=>1} );
1526
1527         my $vols = $e->search_asset_call_number(
1528                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1529
1530         return $resp unless @$vols;
1531
1532         $resp->{volume_holds} = $e->search_action_hold_request(
1533                 { 
1534                         hold_type => OILS_HOLD_TYPE_VOLUME, 
1535                         target => $vols,
1536                         %$args }, 
1537                 {idlist=>1} );
1538
1539         my $copies = $e->search_asset_copy(
1540                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1541
1542         return $resp unless @$copies;
1543
1544         $resp->{copy_holds} = $e->search_action_hold_request(
1545                 { 
1546                         hold_type => OILS_HOLD_TYPE_COPY,
1547                         target => $copies,
1548                         %$args }, 
1549                 {idlist=>1} );
1550
1551         return $resp;
1552 }
1553
1554
1555
1556
1557
1558 __PACKAGE__->register_method(
1559         method => 'uber_hold',
1560     authoritative => 1,
1561         api_name => 'open-ils.circ.hold.details.retrieve'
1562 );
1563
1564 sub uber_hold {
1565         my($self, $client, $auth, $hold_id) = @_;
1566         my $e = new_editor(authtoken=>$auth);
1567         $e->checkauth or return $e->event;
1568         $e->allowed('VIEW_HOLD') or return $e->event;
1569
1570         my $resp = {};
1571
1572         my $hold = $e->retrieve_action_hold_request(
1573                 [
1574                         $hold_id,
1575                         {
1576                                 flesh => 1,
1577                                 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1578                         }
1579                 ]
1580         ) or return $e->event;
1581
1582         my $user = $hold->usr;
1583         $hold->usr($user->id);
1584
1585         my $card = $e->retrieve_actor_card($user->card)
1586                 or return $e->event;
1587
1588         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1589
1590         flesh_hold_notices([$hold], $e);
1591         flesh_hold_transits([$hold]);
1592
1593         return {
1594                 hold            => $hold,
1595                 copy            => $copy,
1596                 volume  => $volume,
1597                 mvr             => $mvr,
1598                 status  => _hold_status($e, $hold),
1599                 patron_first => $user->first_given_name,
1600                 patron_last  => $user->family_name,
1601                 patron_barcode => $card->barcode,
1602         };
1603 }
1604
1605
1606
1607 # -----------------------------------------------------
1608 # Returns the MVR object that represents what the
1609 # hold is all about
1610 # -----------------------------------------------------
1611 sub find_hold_mvr {
1612         my( $e, $hold ) = @_;
1613
1614         my $tid;
1615         my $copy;
1616         my $volume;
1617
1618         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1619                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1620                         or return $e->event;
1621                 $tid = $mr->master_record;
1622
1623         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1624                 $tid = $hold->target;
1625
1626         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1627                 $volume = $e->retrieve_asset_call_number($hold->target)
1628                         or return $e->event;
1629                 $tid = $volume->record;
1630
1631         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1632                 $copy = $e->retrieve_asset_copy($hold->target)
1633                         or return $e->event;
1634                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1635                         or return $e->event;
1636                 $tid = $volume->record;
1637         }
1638
1639         if(!$copy and ref $hold->current_copy ) {
1640                 $copy = $hold->current_copy;
1641                 $hold->current_copy($copy->id);
1642         }
1643
1644         if(!$volume and $copy) {
1645                 $volume = $e->retrieve_asset_call_number($copy->call_number);
1646         }
1647
1648         my $title = $e->retrieve_biblio_record_entry($tid);
1649         return ( $U->record_to_mvr($title), $volume, $copy );
1650 }
1651
1652
1653
1654
1655 1;