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