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