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