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