]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
clear shelf_time when hold is reset
[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     # The holds queue is defined as the set of holds that share at 
790     # least one potential copy with the context hold
791     my $q_holds = $e->json_query({
792          select => { 
793             ahcm => ['hold'], 
794             # fetch request_time since it's in the order_by and we're asking for distinct values
795             ahr => ['request_time']
796         },
797         from => {ahcm => 'ahr'},
798         order_by => {ahr => ['request_time']},
799         distinct => 1,
800         where => {
801             target_copy => {
802                 in => {
803                     select => {ahcm => ['target_copy']},
804                     from => 'ahcm',
805                     where => {hold => $hold->id}
806                 } 
807             } 
808         }, 
809     });
810
811     my $qpos = 1;
812     for my $h (@$q_holds) {
813         last if $h->{hold} == $hold->id;
814         $qpos++;
815     }
816
817     # total count of potential copies
818     my $num_potentials = $e->json_query({
819         select => {ahcm => [{column => 'id', transform => 'count', alias => 'count'}]},
820         from => 'ahcm',
821         where => {hold => $hold->id}
822     })->[0];
823
824     my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
825     my $default_hold_interval = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
826     my $estimated_wait = $qpos * ($default_hold_interval / $num_potentials) if $default_hold_interval;
827
828     return {
829         total_holds => scalar(@$q_holds),
830         queue_position => $qpos,
831         potential_copies => $num_potentials,
832         status => _hold_status($e, $hold),
833         estimated_wait => int($estimated_wait)
834     };
835 }
836
837
838 sub fetch_open_hold_by_current_copy {
839         my $class = shift;
840         my $copyid = shift;
841         my $hold = $apputils->simplereq(
842                 'open-ils.cstore', 
843                 'open-ils.cstore.direct.action.hold_request.search.atomic',
844                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
845         return $hold->[0] if ref($hold);
846         return undef;
847 }
848
849 sub fetch_related_holds {
850         my $class = shift;
851         my $copyid = shift;
852         return $apputils->simplereq(
853                 'open-ils.cstore', 
854                 'open-ils.cstore.direct.action.hold_request.search.atomic',
855                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
856 }
857
858
859 __PACKAGE__->register_method (
860         method          => "hold_pull_list",
861         api_name                => "open-ils.circ.hold_pull_list.retrieve",
862         signature       => q/
863                 Returns a list of holds that need to be "pulled"
864                 by a given location
865         /
866 );
867
868 __PACKAGE__->register_method (
869         method          => "hold_pull_list",
870         api_name                => "open-ils.circ.hold_pull_list.id_list.retrieve",
871         signature       => q/
872                 Returns a list of hold ID's that need to be "pulled"
873                 by a given location
874         /
875 );
876
877
878 sub hold_pull_list {
879         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
880         my( $reqr, $evt ) = $U->checkses($authtoken);
881         return $evt if $evt;
882
883         my $org = $reqr->ws_ou || $reqr->home_ou;
884         # the perm locaiton shouldn't really matter here since holds
885         # will exist all over and VIEW_HOLDS should be universal
886         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
887         return $evt if $evt;
888
889         if( $self->api_name =~ /id_list/ ) {
890                 return $U->storagereq(
891                         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
892                         $org, $limit, $offset ); 
893         } else {
894                 return $U->storagereq(
895                         'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
896                         $org, $limit, $offset ); 
897         }
898 }
899
900 __PACKAGE__->register_method (
901         method          => 'fetch_hold_notify',
902         api_name                => 'open-ils.circ.hold_notification.retrieve_by_hold',
903     authoritative => 1,
904         signature       => q/ 
905                 Returns a list of hold notification objects based on hold id.
906                 @param authtoken The loggin session key
907                 @param holdid The id of the hold whose notifications we want to retrieve
908                 @return An array of hold notification objects, event on error.
909         /
910 );
911
912 sub fetch_hold_notify {
913         my( $self, $conn, $authtoken, $holdid ) = @_;
914         my( $requestor, $evt ) = $U->checkses($authtoken);
915         return $evt if $evt;
916         my ($hold, $patron);
917         ($hold, $evt) = $U->fetch_hold($holdid);
918         return $evt if $evt;
919         ($patron, $evt) = $U->fetch_user($hold->usr);
920         return $evt if $evt;
921
922         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
923         return $evt if $evt;
924
925         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
926         return $U->cstorereq(
927                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
928 }
929
930
931 __PACKAGE__->register_method (
932         method          => 'create_hold_notify',
933         api_name                => 'open-ils.circ.hold_notification.create',
934         signature       => q/
935                 Creates a new hold notification object
936                 @param authtoken The login session key
937                 @param notification The hold notification object to create
938                 @return ID of the new object on success, Event on error
939                 /
940 );
941
942 sub create_hold_notify {
943    my( $self, $conn, $auth, $note ) = @_;
944    my $e = new_editor(authtoken=>$auth, xact=>1);
945    return $e->die_event unless $e->checkauth;
946
947    my $hold = $e->retrieve_action_hold_request($note->hold)
948       or return $e->die_event;
949    my $patron = $e->retrieve_actor_user($hold->usr) 
950       or return $e->die_event;
951
952    return $e->die_event unless 
953       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
954
955         $note->notify_staff($e->requestor->id);
956    $e->create_action_hold_notification($note) or return $e->die_event;
957    $e->commit;
958    return $note->id;
959 }
960
961 __PACKAGE__->register_method (
962         method          => 'create_hold_note',
963         api_name                => 'open-ils.circ.hold_note.create',
964         signature       => q/
965                 Creates a new hold request note object
966                 @param authtoken The login session key
967                 @param note The hold note object to create
968                 @return ID of the new object on success, Event on error
969                 /
970 );
971
972 sub create_hold_note {
973    my( $self, $conn, $auth, $note ) = @_;
974    my $e = new_editor(authtoken=>$auth, xact=>1);
975    return $e->die_event unless $e->checkauth;
976
977    my $hold = $e->retrieve_action_hold_request($note->hold)
978       or return $e->die_event;
979    my $patron = $e->retrieve_actor_user($hold->usr) 
980       or return $e->die_event;
981
982    return $e->die_event unless 
983       $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn.  Probably want something more specific
984
985    $e->create_action_hold_request_note($note) or return $e->die_event;
986    $e->commit;
987    return $note->id;
988 }
989
990 __PACKAGE__->register_method(
991         method  => 'reset_hold',
992         api_name        => 'open-ils.circ.hold.reset',
993         signature       => q/
994                 Un-captures and un-targets a hold, essentially returning
995                 it to the state it was in directly after it was placed,
996                 then attempts to re-target the hold
997                 @param authtoken The login session key
998                 @param holdid The id of the hold
999         /
1000 );
1001
1002
1003 sub reset_hold {
1004         my( $self, $conn, $auth, $holdid ) = @_;
1005         my $reqr;
1006         my ($hold, $evt) = $U->fetch_hold($holdid);
1007         return $evt if $evt;
1008         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1009         return $evt if $evt;
1010         $evt = _reset_hold($self, $reqr, $hold);
1011         return $evt if $evt;
1012         return 1;
1013 }
1014
1015
1016 __PACKAGE__->register_method(
1017         method  => 'reset_hold_batch',
1018         api_name        => 'open-ils.circ.hold.reset.batch'
1019 );
1020
1021 sub reset_hold_batch {
1022     my($self, $conn, $auth, $hold_ids) = @_;
1023
1024     my $e = new_editor(authtoken => $auth);
1025     return $e->event unless $e->checkauth;
1026
1027     for my $hold_id ($hold_ids) {
1028
1029         my $hold = $e->retrieve_action_hold_request(
1030             [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}]) 
1031             or return $e->event;
1032
1033             next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1034         _reset_hold($self, $e->requestor, $hold);
1035     }
1036
1037     return 1;
1038 }
1039
1040
1041 sub _reset_hold {
1042         my ($self, $reqr, $hold) = @_;
1043
1044         my $e = new_editor(xact =>1, requestor => $reqr);
1045
1046         $logger->info("reseting hold ".$hold->id);
1047
1048         my $hid = $hold->id;
1049
1050         if( $hold->capture_time and $hold->current_copy ) {
1051
1052                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1053                         or return $e->event;
1054
1055                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1056                         $logger->info("setting copy to status 'reshelving' on hold retarget");
1057                         $copy->status(OILS_COPY_STATUS_RESHELVING);
1058                         $copy->editor($e->requestor->id);
1059                         $copy->edit_date('now');
1060                         $e->update_asset_copy($copy) or return $e->event;
1061
1062                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1063
1064                         # We don't want the copy to remain "in transit"
1065                         $copy->status(OILS_COPY_STATUS_RESHELVING);
1066                         $logger->warn("! reseting hold [$hid] that is in transit");
1067                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1068
1069                         if( $transid ) {
1070                                 my $trans = $e->retrieve_action_transit_copy($transid);
1071                                 if( $trans ) {
1072                                         $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1073                                         my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
1074                                         $logger->info("Transit abort completed with result $evt");
1075                                         return $evt unless "$evt" eq 1;
1076                                 }
1077                         }
1078                 }
1079         }
1080
1081         $hold->clear_capture_time;
1082         $hold->clear_current_copy;
1083         $hold->clear_shelf_time;
1084
1085         $e->update_action_hold_request($hold) or return $e->event;
1086         $e->commit;
1087
1088         $U->storagereq(
1089                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1090
1091         return undef;
1092 }
1093
1094
1095 __PACKAGE__->register_method(
1096         method => 'fetch_open_title_holds',
1097         api_name        => 'open-ils.circ.open_holds.retrieve',
1098         signature       => q/
1099                 Returns a list ids of un-fulfilled holds for a given title id
1100                 @param authtoken The login session key
1101                 @param id the id of the item whose holds we want to retrieve
1102                 @param type The hold type - M, T, V, C
1103         /
1104 );
1105
1106 sub fetch_open_title_holds {
1107         my( $self, $conn, $auth, $id, $type, $org ) = @_;
1108         my $e = new_editor( authtoken => $auth );
1109         return $e->event unless $e->checkauth;
1110
1111         $type ||= "T";
1112         $org ||= $e->requestor->ws_ou;
1113
1114 #       return $e->search_action_hold_request(
1115 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
1116
1117         # XXX make me return IDs in the future ^--
1118         my $holds = $e->search_action_hold_request(
1119                 { 
1120                         target                          => $id, 
1121                         cancel_time                     => undef, 
1122                         hold_type                       => $type, 
1123                         fulfillment_time        => undef 
1124                 }
1125         );
1126
1127         flesh_hold_transits($holds);
1128         return $holds;
1129 }
1130
1131
1132 sub flesh_hold_transits {
1133         my $holds = shift;
1134         for my $hold ( @$holds ) {
1135                 $hold->transit(
1136                         $apputils->simplereq(
1137                                 'open-ils.cstore',
1138                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
1139                                 { hold => $hold->id },
1140                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
1141                         )->[0]
1142                 );
1143         }
1144 }
1145
1146 sub flesh_hold_notices {
1147         my( $holds, $e ) = @_;
1148         $e ||= new_editor();
1149
1150         for my $hold (@$holds) {
1151                 my $notices = $e->search_action_hold_notification(
1152                         [
1153                                 { hold => $hold->id },
1154                                 { order_by => { anh => 'notify_time desc' } },
1155                         ],
1156                         {idlist=>1}
1157                 );
1158
1159                 $hold->notify_count(scalar(@$notices));
1160                 if( @$notices ) {
1161                         my $n = $e->retrieve_action_hold_notification($$notices[0])
1162                                 or return $e->event;
1163                         $hold->notify_time($n->notify_time);
1164                 }
1165         }
1166 }
1167
1168
1169 __PACKAGE__->register_method(
1170         method => 'fetch_captured_holds',
1171         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1172     stream => 1,
1173         signature       => q/
1174                 Returns a list of un-fulfilled holds for a given title id
1175                 @param authtoken The login session key
1176                 @param org The org id of the location in question
1177         /
1178 );
1179
1180 __PACKAGE__->register_method(
1181         method => 'fetch_captured_holds',
1182         api_name        => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1183     stream => 1,
1184         signature       => q/
1185                 Returns a list ids of un-fulfilled holds for a given title id
1186                 @param authtoken The login session key
1187                 @param org The org id of the location in question
1188         /
1189 );
1190
1191 sub fetch_captured_holds {
1192         my( $self, $conn, $auth, $org ) = @_;
1193
1194         my $e = new_editor(authtoken => $auth);
1195         return $e->event unless $e->checkauth;
1196         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1197
1198         $org ||= $e->requestor->ws_ou;
1199
1200     my $hold_ids = $e->json_query(
1201         { 
1202             select => { ahr => ['id'] },
1203             from => {
1204                 ahr => {
1205                     acp => {
1206                         field => 'id',
1207                         fkey => 'current_copy'
1208                     },
1209                 }
1210             }, 
1211             where => {
1212                 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1213                 '+ahr' => {
1214                     capture_time                => { "!=" => undef },
1215                     current_copy                => { "!=" => undef },
1216                     fulfillment_time    => undef,
1217                     pickup_lib                  => $org,
1218                     cancel_time                 => undef,
1219                 }
1220             }
1221         },
1222     );
1223
1224     for my $hold_id (@$hold_ids) {
1225         if($self->api_name =~ /id_list/) {
1226             $conn->respond($hold_id->{id});
1227             next;
1228         } else {
1229             $conn->respond(
1230                 $e->retrieve_action_hold_request([
1231                     $hold_id->{id},
1232                     {
1233                         flesh => 1,
1234                         flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
1235                         order_by => {anh => 'notify_time desc'}
1236                     }
1237                 ])
1238             );
1239         }
1240     }
1241
1242     return undef;
1243 }
1244 __PACKAGE__->register_method(
1245         method  => "check_title_hold",
1246         api_name        => "open-ils.circ.title_hold.is_possible",
1247         notes           => q/
1248                 Determines if a hold were to be placed by a given user,
1249                 whether or not said hold would have any potential copies
1250                 to fulfill it.
1251                 @param authtoken The login session key
1252                 @param params A hash of named params including:
1253                         patronid  - the id of the hold recipient
1254                         titleid (brn) - the id of the title to be held
1255                         depth   - the hold range depth (defaults to 0)
1256         /);
1257
1258 sub check_title_hold {
1259         my( $self, $client, $authtoken, $params ) = @_;
1260
1261         my %params              = %$params;
1262         my $titleid             = $params{titleid} ||"";
1263         my $volid               = $params{volume_id};
1264         my $copyid              = $params{copy_id};
1265         my $mrid                = $params{mrid} ||"";
1266         my $depth               = $params{depth} || 0;
1267         my $pickup_lib  = $params{pickup_lib};
1268         my $hold_type   = $params{hold_type} || 'T';
1269     my $selection_ou = $params{selection_ou} || $pickup_lib;
1270
1271         my $e = new_editor(authtoken=>$authtoken);
1272         return $e->event unless $e->checkauth;
1273         my $patron = $e->retrieve_actor_user($params{patronid})
1274                 or return $e->event;
1275
1276         if( $e->requestor->id ne $patron->id ) {
1277                 return $e->event unless 
1278                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1279         }
1280
1281         return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1282
1283         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1284                 or return $e->event;
1285
1286     my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1287     my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1288
1289     if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1290         # work up the tree and as soon as we find a potential copy, use that depth
1291         # also, make sure we don't go past the hard boundary if it exists
1292
1293         # our min boundary is the greater of user-specified boundary or hard boundary
1294         my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?  
1295             $hard_boundary : $$params{depth};
1296
1297         my $depth = $soft_boundary;
1298         while($depth >= $min_depth) {
1299             $logger->info("performing hold possibility check with soft boundary $depth");
1300             my @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1301             return {success => 1, depth => $depth, local_avail => $status[1]} if $status[0];
1302             $depth--;
1303         }
1304         return {success => 0};
1305
1306     } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1307         # there is no soft boundary, enforce the hard boundary if it exists
1308         $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1309         my @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
1310         if($status[0]) {
1311             return {success => 1, depth => $hard_boundary, local_avail => $status[1]}
1312         } else {
1313             return {success => 0};
1314         }
1315
1316     } else {
1317         # no boundaries defined, fall back to user specifed boundary or no boundary
1318         $logger->info("performing hold possibility check with no boundary");
1319         my @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
1320         if($status[0]) {
1321             return {success => 1, depth => $hard_boundary, local_avail => $status[1]};
1322         } else {
1323             return {success => 0};
1324         }
1325     }
1326 }
1327
1328 sub do_possibility_checks {
1329     my($e, $patron, $request_lib, $depth, %params) = @_;
1330
1331         my $titleid             = $params{titleid} ||"";
1332         my $volid               = $params{volume_id};
1333         my $copyid              = $params{copy_id};
1334         my $mrid                = $params{mrid} ||"";
1335         my $pickup_lib  = $params{pickup_lib};
1336         my $hold_type   = $params{hold_type} || 'T';
1337     my $selection_ou = $params{selection_ou} || $pickup_lib;
1338
1339
1340         my $copy;
1341         my $volume;
1342         my $title;
1343
1344         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1345
1346                 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1347                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1348                         or return $e->event;
1349                 $title = $e->retrieve_biblio_record_entry($volume->record)
1350                         or return $e->event;
1351                 return verify_copy_for_hold( 
1352                         $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1353
1354         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1355
1356                 $volume = $e->retrieve_asset_call_number($volid)
1357                         or return $e->event;
1358                 $title = $e->retrieve_biblio_record_entry($volume->record)
1359                         or return $e->event;
1360
1361                 return _check_volume_hold_is_possible(
1362                         $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1363
1364         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1365
1366                 return _check_title_hold_is_possible(
1367                         $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1368
1369         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1370
1371                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1372                 my @recs = map { $_->source } @$maps;
1373                 for my $rec (@recs) {
1374             my @status = _check_title_hold_is_possible(
1375                                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1376             return @status if $status[1];
1377                 }
1378                 return (0);     
1379         }
1380 }
1381
1382 my %prox_cache;
1383 sub create_ranged_org_filter {
1384     my($e, $selection_ou, $depth) = @_;
1385
1386     # find the orgs from which this hold may be fulfilled, 
1387     # based on the selection_ou and depth
1388
1389     my $top_org = $e->search_actor_org_unit([
1390         {parent_ou => undef}, 
1391         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1392     my %org_filter;
1393
1394     return () if $depth == $top_org->ou_type->depth;
1395
1396     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1397     %org_filter = (circ_lib => []);
1398     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1399
1400     $logger->info("hold org filter at depth $depth and selection_ou ".
1401         "$selection_ou created list of @{$org_filter{circ_lib}}");
1402
1403     return %org_filter;
1404 }
1405
1406
1407 sub _check_title_hold_is_possible {
1408         my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1409    
1410     my $e = new_editor();
1411     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1412
1413     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1414     my $copies = $e->json_query(
1415         { 
1416             select => { acp => ['id', 'circ_lib'] },
1417             from => {
1418                 acp => {
1419                     acn => {
1420                         field => 'id',
1421                         fkey => 'call_number',
1422                         'join' => {
1423                             bre => {
1424                                 field => 'id',
1425                                 filter => { id => $titleid },
1426                                 fkey => 'record'
1427                             }
1428                         }
1429                     },
1430                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1431                     ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1432                 }
1433             }, 
1434             where => {
1435                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1436             }
1437         }
1438     );
1439
1440    $logger->info("title possible found ".scalar(@$copies)." potential copies");
1441    return (0) unless @$copies;
1442
1443    # -----------------------------------------------------------------------
1444    # sort the copies into buckets based on their circ_lib proximity to 
1445    # the patron's home_ou.  
1446    # -----------------------------------------------------------------------
1447
1448    my $home_org = $patron->home_ou;
1449    my $req_org = $request_lib->id;
1450
1451     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1452
1453     $prox_cache{$home_org} = 
1454         $e->search_actor_org_unit_proximity({from_org => $home_org})
1455         unless $prox_cache{$home_org};
1456     my $home_prox = $prox_cache{$home_org};
1457
1458    my %buckets;
1459    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1460    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1461
1462    my @keys = sort { $a <=> $b } keys %buckets;
1463
1464
1465    if( $home_org ne $req_org ) {
1466       # -----------------------------------------------------------------------
1467       # shove the copies close to the request_lib into the primary buckets 
1468       # directly before the farthest away copies.  That way, they are not 
1469       # given priority, but they are checked before the farthest copies.
1470       # -----------------------------------------------------------------------
1471         $prox_cache{$req_org} = 
1472             $e->search_actor_org_unit_proximity({from_org => $req_org})
1473             unless $prox_cache{$req_org};
1474         my $req_prox = $prox_cache{$req_org};
1475
1476
1477       my %buckets2;
1478       my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1479       push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1480
1481       my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1482       my $new_key = $highest_key - 0.5; # right before the farthest prox
1483       my @keys2 = sort { $a <=> $b } keys %buckets2;
1484       for my $key (@keys2) {
1485          last if $key >= $highest_key;
1486          push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1487       }
1488    }
1489
1490    @keys = sort { $a <=> $b } keys %buckets;
1491
1492    my $title;
1493    my %seen;
1494    for my $key (@keys) {
1495       my @cps = @{$buckets{$key}};
1496
1497       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1498
1499       for my $copyid (@cps) {
1500
1501          next if $seen{$copyid};
1502          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1503          my $copy = $e->retrieve_asset_copy($copyid);
1504          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1505
1506          unless($title) { # grab the title if we don't already have it
1507             my $vol = $e->retrieve_asset_call_number(
1508                [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1509             $title = $vol->record;
1510          }
1511    
1512          my @status = verify_copy_for_hold( 
1513             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1514
1515         return @status if $status[0];
1516       }
1517    }
1518
1519    return (0);
1520 }
1521
1522
1523 sub _check_volume_hold_is_possible {
1524         my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1525     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1526         my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1527         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1528         for my $copy ( @$copies ) {
1529         my @status = verify_copy_for_hold( 
1530                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1531         return @status if $status[0];
1532         }
1533         return (0);
1534 }
1535
1536
1537
1538 sub verify_copy_for_hold {
1539         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1540         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1541     my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1542                 {       patron                          => $patron, 
1543                         requestor                       => $requestor, 
1544                         copy                            => $copy,
1545                         title                           => $title, 
1546                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1547                         pickup_lib                      => $pickup_lib,
1548                         request_lib                     => $request_lib,
1549             new_hold            => 1
1550                 } 
1551         );
1552
1553     return (
1554         $permitted,
1555         (
1556                 ($copy->circ_lib == $pickup_lib) and 
1557             ($copy->status == OILS_COPY_STATUS_AVAILABLE)
1558         )
1559     );
1560 }
1561
1562
1563
1564 sub find_nearest_permitted_hold {
1565
1566         my $class       = shift;
1567         my $editor      = shift; # CStoreEditor object
1568         my $copy                = shift; # copy to target
1569         my $user                = shift; # staff 
1570         my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1571         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1572
1573         my $bc = $copy->barcode;
1574
1575         # find any existing holds that already target this copy
1576         my $old_holds = $editor->search_action_hold_request(
1577                 {       current_copy => $copy->id, 
1578                         cancel_time => undef, 
1579                         capture_time => undef 
1580                 } 
1581         );
1582
1583         # hold->type "R" means we need this copy
1584         for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1585
1586
1587     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1588
1589         $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1590         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1591
1592         # search for what should be the best holds for this copy to fulfill
1593         my $best_holds = $U->storagereq(
1594                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1595                 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1596
1597         unless(@$best_holds) {
1598
1599                 if( my $hold = $$old_holds[0] ) {
1600                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1601                         return ($hold);
1602                 }
1603
1604                 $logger->info("circulator: no suitable holds found for copy $bc");
1605                 return (undef, $evt);
1606         }
1607
1608
1609         my $best_hold;
1610
1611         # for each potential hold, we have to run the permit script
1612         # to make sure the hold is actually permitted.
1613         for my $holdid (@$best_holds) {
1614                 next unless $holdid;
1615                 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1616
1617                 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1618                 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1619                 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1620
1621                 # see if this hold is permitted
1622                 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1623                         {       patron_id                       => $hold->usr,
1624                                 requestor                       => $reqr,
1625                                 copy                            => $copy,
1626                                 pickup_lib                      => $hold->pickup_lib,
1627                                 request_lib                     => $rlib,
1628                         } 
1629                 );
1630
1631                 if( $permitted ) {
1632                         $best_hold = $hold;
1633                         last;
1634                 }
1635         }
1636
1637
1638         unless( $best_hold ) { # no "good" permitted holds were found
1639                 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1640                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1641                         return ($hold);
1642                 }
1643
1644                 # we got nuthin
1645                 $logger->info("circulator: no suitable holds found for copy $bc");
1646                 return (undef, $evt);
1647         }
1648
1649         $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1650
1651         # indicate a permitted hold was found
1652         return $best_hold if $check_only;
1653
1654         # we've found a permitted hold.  we need to "grab" the copy 
1655         # to prevent re-targeted holds (next part) from re-grabbing the copy
1656         $best_hold->current_copy($copy->id);
1657         $editor->update_action_hold_request($best_hold) 
1658                 or return (undef, $editor->event);
1659
1660
1661     my @retarget;
1662
1663         # re-target any other holds that already target this copy
1664         for my $old_hold (@$old_holds) {
1665                 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1666                 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1667             $old_hold->id." after a better hold [".$best_hold->id."] was found");
1668         $old_hold->clear_current_copy;
1669         $old_hold->clear_prev_check_time;
1670         $editor->update_action_hold_request($old_hold) 
1671             or return (undef, $editor->event);
1672         push(@retarget, $old_hold->id);
1673         }
1674
1675         return ($best_hold, undef, (@retarget) ? \@retarget : undef);
1676 }
1677
1678
1679
1680
1681
1682
1683 __PACKAGE__->register_method(
1684         method => 'all_rec_holds',
1685         api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1686 );
1687
1688 sub all_rec_holds {
1689         my( $self, $conn, $auth, $title_id, $args ) = @_;
1690
1691         my $e = new_editor(authtoken=>$auth);
1692         $e->checkauth or return $e->event;
1693         $e->allowed('VIEW_HOLD') or return $e->event;
1694
1695         $args ||= {};
1696     $args->{fulfillment_time} = undef; #  we don't want to see old fulfilled holds
1697         $args->{cancel_time} = undef;
1698
1699         my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1700
1701     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1702     if($mr_map) {
1703         $resp->{metarecord_holds} = $e->search_action_hold_request(
1704             {   hold_type => OILS_HOLD_TYPE_METARECORD,
1705                 target => $mr_map->metarecord,
1706                 %$args 
1707             }, {idlist => 1}
1708         );
1709     }
1710
1711         $resp->{title_holds} = $e->search_action_hold_request(
1712                 { 
1713                         hold_type => OILS_HOLD_TYPE_TITLE, 
1714                         target => $title_id, 
1715                         %$args 
1716                 }, {idlist=>1} );
1717
1718         my $vols = $e->search_asset_call_number(
1719                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1720
1721         return $resp unless @$vols;
1722
1723         $resp->{volume_holds} = $e->search_action_hold_request(
1724                 { 
1725                         hold_type => OILS_HOLD_TYPE_VOLUME, 
1726                         target => $vols,
1727                         %$args }, 
1728                 {idlist=>1} );
1729
1730         my $copies = $e->search_asset_copy(
1731                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1732
1733         return $resp unless @$copies;
1734
1735         $resp->{copy_holds} = $e->search_action_hold_request(
1736                 { 
1737                         hold_type => OILS_HOLD_TYPE_COPY,
1738                         target => $copies,
1739                         %$args }, 
1740                 {idlist=>1} );
1741
1742         return $resp;
1743 }
1744
1745
1746
1747
1748
1749 __PACKAGE__->register_method(
1750         method => 'uber_hold',
1751     authoritative => 1,
1752         api_name => 'open-ils.circ.hold.details.retrieve'
1753 );
1754
1755 sub uber_hold {
1756         my($self, $client, $auth, $hold_id) = @_;
1757         my $e = new_editor(authtoken=>$auth);
1758         $e->checkauth or return $e->event;
1759         $e->allowed('VIEW_HOLD') or return $e->event;
1760
1761         my $resp = {};
1762
1763         my $hold = $e->retrieve_action_hold_request(
1764                 [
1765                         $hold_id,
1766                         {
1767                                 flesh => 1,
1768                                 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
1769                         }
1770                 ]
1771         ) or return $e->event;
1772
1773         my $user = $hold->usr;
1774         $hold->usr($user->id);
1775
1776         my $card = $e->retrieve_actor_card($user->card)
1777                 or return $e->event;
1778
1779         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1780
1781         flesh_hold_notices([$hold], $e);
1782         flesh_hold_transits([$hold]);
1783
1784     my $details = retrieve_hold_queue_status_impl($e, $hold);
1785
1786         return {
1787                 hold            => $hold,
1788                 copy            => $copy,
1789                 volume  => $volume,
1790                 mvr             => $mvr,
1791                 patron_first => $user->first_given_name,
1792                 patron_last  => $user->family_name,
1793                 patron_barcode => $card->barcode,
1794         %$details
1795         };
1796 }
1797
1798
1799
1800 # -----------------------------------------------------
1801 # Returns the MVR object that represents what the
1802 # hold is all about
1803 # -----------------------------------------------------
1804 sub find_hold_mvr {
1805         my( $e, $hold ) = @_;
1806
1807         my $tid;
1808         my $copy;
1809         my $volume;
1810
1811         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1812                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1813                         or return $e->event;
1814                 $tid = $mr->master_record;
1815
1816         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1817                 $tid = $hold->target;
1818
1819         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1820                 $volume = $e->retrieve_asset_call_number($hold->target)
1821                         or return $e->event;
1822                 $tid = $volume->record;
1823
1824         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1825                 $copy = $e->retrieve_asset_copy($hold->target)
1826                         or return $e->event;
1827                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1828                         or return $e->event;
1829                 $tid = $volume->record;
1830         }
1831
1832         if(!$copy and ref $hold->current_copy ) {
1833                 $copy = $hold->current_copy;
1834                 $hold->current_copy($copy->id);
1835         }
1836
1837         if(!$volume and $copy) {
1838                 $volume = $e->retrieve_asset_call_number($copy->call_number);
1839         }
1840
1841     # TODO return metarcord mvr for M holds
1842         my $title = $e->retrieve_biblio_record_entry($tid);
1843         return ( $U->record_to_mvr($title), $volume, $copy );
1844 }
1845
1846
1847
1848
1849 1;