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