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