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