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