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