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