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