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