]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
Method for moving title holds from their current bibs to a new bib.
[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     my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
115     push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
116
117     if( $t eq OILS_HOLD_TYPE_METARECORD ) 
118         { $pevt = $e->event unless $e->allowed('MR_HOLDS', $porg); }
119
120     if( $t eq OILS_HOLD_TYPE_TITLE ) 
121         { $pevt = $e->event unless $e->allowed('TITLE_HOLDS', $porg);  }
122
123     if( $t eq OILS_HOLD_TYPE_VOLUME ) 
124         { $pevt = $e->event unless $e->allowed('VOLUME_HOLDS', $porg); }
125
126     if( $t eq OILS_HOLD_TYPE_COPY ) 
127         { $pevt = $e->event unless $e->allowed('COPY_HOLDS', $porg); }
128
129     return $pevt if $pevt;
130
131     if( @events ) {
132         if( $override ) {
133             for my $evt (@events) {
134                 next unless $evt;
135                 my $name = $evt->{textcode};
136                 return $e->event unless $e->allowed("$name.override", $porg);
137             }
138         } else {
139             return \@events;
140         }
141     }
142
143     # set the configured expire time
144     unless($hold->expire_time) {
145         my $interval = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_HOLD_EXPIRE);
146         if($interval) {
147             my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
148             $hold->expire_time($U->epoch2ISO8601($date->epoch));
149         }
150     }
151
152     $hold->requestor($e->requestor->id); 
153     $hold->request_lib($e->requestor->ws_ou);
154     $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
155     $hold = $e->create_action_hold_request($hold) or return $e->event;
156
157         $e->commit;
158
159         $conn->respond_complete($hold->id);
160
161     $U->storagereq(
162         'open-ils.storage.action.hold_request.copy_targeter', 
163         undef, $hold->id ) unless $U->is_true($hold->frozen);
164
165         return undef;
166 }
167
168 sub __create_hold {
169         my( $self, $client, $login_session, @holds) = @_;
170
171         if(!@holds){return 0;}
172         my( $user, $evt ) = $apputils->checkses($login_session);
173         return $evt if $evt;
174
175         my $holds;
176         if(ref($holds[0]) eq 'ARRAY') {
177                 $holds = $holds[0];
178         } else { $holds = [ @holds ]; }
179
180         $logger->debug("Iterating over holds requests...");
181
182         for my $hold (@$holds) {
183
184                 if(!$hold){next};
185                 my $type = $hold->hold_type;
186
187                 $logger->activity("User " . $user->id . 
188                         " creating new hold of type $type for user " . $hold->usr);
189
190                 my $recipient;
191                 if($user->id ne $hold->usr) {
192                         ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
193                         return $evt if $evt;
194
195                 } else {
196                         $recipient = $user;
197                 }
198
199
200                 my $perm = undef;
201
202                 # am I allowed to place holds for this user?
203                 if($hold->requestor ne $hold->usr) {
204                         $perm = _check_request_holds_perm($user->id, $user->home_ou);
205                         if($perm) { return $perm; }
206                 }
207
208                 # is this user allowed to have holds of this type?
209                 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
210         return $perm if $perm;
211
212                 #enforce the fact that the login is the one requesting the hold
213                 $hold->requestor($user->id); 
214                 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
215
216                 my $resp = $apputils->simplereq(
217                         'open-ils.storage',
218                         'open-ils.storage.direct.action.hold_request.create', $hold );
219
220                 if(!$resp) { 
221                         return OpenSRF::EX::ERROR ("Error creating hold"); 
222                 }
223         }
224
225         return 1;
226 }
227
228 # makes sure that a user has permission to place the type of requested hold
229 # returns the Perm exception if not allowed, returns undef if all is well
230 sub _check_holds_perm {
231         my($type, $user_id, $org_id) = @_;
232
233         my $evt;
234         if($type eq "M") {
235                 if($evt = $apputils->check_perms(
236                         $user_id, $org_id, "MR_HOLDS")) {
237                         return $evt;
238                 } 
239
240         } elsif ($type eq "T") {
241                 if($evt = $apputils->check_perms(
242                         $user_id, $org_id, "TITLE_HOLDS")) {
243                         return $evt;
244                 }
245
246         } elsif($type eq "V") {
247                 if($evt = $apputils->check_perms(
248                         $user_id, $org_id, "VOLUME_HOLDS")) {
249                         return $evt;
250                 }
251
252         } elsif($type eq "C") {
253                 if($evt = $apputils->check_perms(
254                         $user_id, $org_id, "COPY_HOLDS")) {
255                         return $evt;
256                 }
257         }
258
259         return undef;
260 }
261
262 # tests if the given user is allowed to place holds on another's behalf
263 sub _check_request_holds_perm {
264         my $user_id = shift;
265         my $org_id = shift;
266         if(my $evt = $apputils->check_perms(
267                 $user_id, $org_id, "REQUEST_HOLDS")) {
268                 return $evt;
269         }
270 }
271
272 __PACKAGE__->register_method(
273         method  => "retrieve_holds_by_id",
274         api_name        => "open-ils.circ.holds.retrieve_by_id",
275         notes           => <<NOTE);
276 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
277 different from the user, then the requestor must have VIEW_HOLD permissions.
278 NOTE
279
280
281 sub retrieve_holds_by_id {
282         my($self, $client, $auth, $hold_id) = @_;
283         my $e = new_editor(authtoken=>$auth);
284         $e->checkauth or return $e->event;
285         $e->allowed('VIEW_HOLD') or return $e->event;
286
287         my $holds = $e->search_action_hold_request(
288                 [
289                         { id =>  $hold_id , fulfillment_time => undef }, 
290                         { 
291                 order_by => { ahr => "request_time" },
292                 flesh => 1,
293                 flesh_fields => {ahr => ['notes']}
294             }
295                 ]
296         );
297
298         flesh_hold_transits($holds);
299         flesh_hold_notices($holds, $e);
300         return $holds;
301 }
302
303
304 __PACKAGE__->register_method(
305         method  => "retrieve_holds",
306         api_name        => "open-ils.circ.holds.retrieve",
307         notes           => <<NOTE);
308 Retrieves all the holds, with hold transits attached, for the specified
309 user id.  The login session is the requestor and if the requestor is
310 different from the user, then the requestor must have VIEW_HOLD permissions.
311 NOTE
312
313 __PACKAGE__->register_method(
314         method  => "retrieve_holds",
315     authoritative => 1,
316         api_name        => "open-ils.circ.holds.id_list.retrieve",
317         notes           => <<NOTE);
318 Retrieves all the hold ids for the specified
319 user id.  The login session is the requestor and if the requestor is
320 different from the user, then the requestor must have VIEW_HOLD permissions.
321 NOTE
322
323
324 __PACKAGE__->register_method(
325         method  => "retrieve_holds",
326     authoritative => 1,
327         api_name        => "open-ils.circ.holds.canceled.retrieve",
328 );
329
330 __PACKAGE__->register_method(
331         method  => "retrieve_holds",
332     authoritative => 1,
333         api_name        => "open-ils.circ.holds.canceled.id_list.retrieve",
334 );
335
336
337 sub retrieve_holds {
338         my($self, $client, $auth, $user_id, $options) = @_;
339
340     my $e = new_editor(authtoken=>$auth);
341     return $e->event unless $e->checkauth;
342     $user_id = $e->requestor->id unless defined $user_id;
343     $options ||= {};
344
345     unless($user_id == $e->requestor->id) {
346         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
347         unless($e->allowed('VIEW_HOLD', $user->home_ou)) {
348             my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
349                 $e, $user_id, $e->requestor->id, 'hold.view');
350             return $e->event unless $allowed;
351         }
352     }
353
354     my $holds;
355
356     if($self->api_name !~ /canceled/) {
357
358         # Fetch the active holds
359
360         $holds = $e->search_action_hold_request([
361             {   usr =>  $user_id , 
362                 fulfillment_time => undef,
363                 cancel_time => undef,
364             }, 
365             {order_by => {ahr => "request_time"}}
366         ]);
367
368     } else {
369
370         # Fetch the canceled holds
371
372         my $cancel_age;
373         my $cancel_count = 
374             $U->ou_ancestor_setting_value(
375                 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
376
377         unless($cancel_count) {
378             $cancel_age = $U->ou_ancestor_setting_value(
379                 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
380         }
381
382         if($cancel_count) { # limit by count
383
384             # find at most cancel_count canceled holds
385             $holds = $e->search_action_hold_request([
386                 {   usr =>  $user_id , 
387                     fulfillment_time => undef,
388                     cancel_time => {'!=' => undef},
389                 }, 
390                 {order_by => {ahr => "cancel_time desc"}, limit => $cancel_count}
391             ]);
392
393         } elsif($cancel_age) { # limit by age
394
395             # find all of the canceled holds that were canceled within the configured time frame
396             my $date = DateTime->now->subtract(seconds => OpenSRF::Utils::interval_to_seconds($cancel_age));
397             $date = $U->epoch2ISO8601($date->epoch);
398
399             $holds = $e->search_action_hold_request([
400                 {   usr =>  $user_id , 
401                     fulfillment_time => undef,
402                     cancel_time => {'>=' => $date},
403                 }, 
404                 {order_by => {ahr => "cancel_time desc"}}
405             ]);
406         }
407     }
408         
409         if( ! $self->api_name =~ /id_list/ ) {
410                 for my $hold ( @$holds ) {
411                         $hold->transit(
412                 $e->search_action_hold_transit_copy([
413                                         {hold => $hold->id},
414                                         {order_by => {ahtc => 'id desc'}, limit => 1}])->[0]
415                         );
416                 }
417         }
418
419         if( $self->api_name =~ /id_list/ ) {
420                 return [ map { $_->id } @$holds ];
421         } else {
422                 return $holds;
423         }
424 }
425
426
427 __PACKAGE__->register_method(
428    method => 'user_hold_count',
429    api_name => 'open-ils.circ.hold.user.count');
430
431 sub user_hold_count {
432    my( $self, $conn, $auth, $userid ) = @_;
433    my $e = new_editor(authtoken=>$auth);
434    return $e->event unless $e->checkauth;
435    my $patron = $e->retrieve_actor_user($userid)
436       or return $e->event;
437    return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
438    return __user_hold_count($self, $e, $userid);
439 }
440
441 sub __user_hold_count {
442    my( $self, $e, $userid ) = @_;
443    my $holds = $e->search_action_hold_request(
444       {  usr =>  $userid , 
445          fulfillment_time => undef,
446          cancel_time => undef,
447       }, 
448       {idlist => 1}
449    );
450
451    return scalar(@$holds);
452 }
453
454
455 __PACKAGE__->register_method(
456         method  => "retrieve_holds_by_pickup_lib",
457         api_name        => "open-ils.circ.holds.retrieve_by_pickup_lib",
458         notes           => <<NOTE);
459 Retrieves all the holds, with hold transits attached, for the specified
460 pickup_ou id. 
461 NOTE
462
463 __PACKAGE__->register_method(
464         method  => "retrieve_holds_by_pickup_lib",
465         api_name        => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
466         notes           => <<NOTE);
467 Retrieves all the hold ids for the specified
468 pickup_ou id. 
469 NOTE
470
471 sub retrieve_holds_by_pickup_lib {
472         my($self, $client, $login_session, $ou_id) = @_;
473
474         #FIXME -- put an appropriate permission check here
475         #my( $user, $target, $evt ) = $apputils->checkses_requestor(
476         #       $login_session, $user_id, 'VIEW_HOLD' );
477         #return $evt if $evt;
478
479         my $holds = $apputils->simplereq(
480                 'open-ils.cstore',
481                 "open-ils.cstore.direct.action.hold_request.search.atomic",
482                 { 
483                         pickup_lib =>  $ou_id , 
484                         fulfillment_time => undef,
485                         cancel_time => undef
486                 }, 
487                 { order_by => { ahr => "request_time" } });
488
489
490         if( ! $self->api_name =~ /id_list/ ) {
491                 flesh_hold_transits($holds);
492         }
493
494         if( $self->api_name =~ /id_list/ ) {
495                 return [ map { $_->id } @$holds ];
496         } else {
497                 return $holds;
498         }
499 }
500
501
502 __PACKAGE__->register_method(
503         method  => "uncancel_hold",
504         api_name        => "open-ils.circ.hold.uncancel"
505 );
506
507 sub uncancel_hold {
508         my($self, $client, $auth, $hold_id) = @_;
509         my $e = new_editor(authtoken=>$auth, xact=>1);
510         return $e->event unless $e->checkauth;
511
512         my $hold = $e->retrieve_action_hold_request($hold_id)
513                 or return $e->die_event;
514     return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
515
516     return 0 if $hold->fulfillment_time;
517     return 1 unless $hold->cancel_time;
518
519     # if configured to reset the request time, also reset the expire time
520     if($U->ou_ancestor_setting_value(
521         $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
522
523         $hold->request_time('now');
524         my $interval = $U->ou_ancestor_setting_value($hold->request_lib, OILS_SETTING_HOLD_EXPIRE);
525         if($interval) {
526             my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
527             $hold->expire_time($U->epoch2ISO8601($date->epoch));
528         }
529     }
530
531     $hold->clear_cancel_time;
532     $hold->clear_cancel_cause;
533     $hold->clear_cancel_note;
534     $e->update_action_hold_request($hold) or return $e->die_event;
535     $e->commit;
536
537     $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $hold_id);
538
539     return 1;
540 }
541
542
543 __PACKAGE__->register_method(
544         method  => "cancel_hold",
545         api_name        => "open-ils.circ.hold.cancel",
546         notes           => <<"  NOTE");
547         Cancels the specified hold.  The login session
548         is the requestor and if the requestor is different from the usr field
549         on the hold, the requestor must have CANCEL_HOLDS permissions.
550         the hold may be either the hold object or the hold id
551         NOTE
552
553 sub cancel_hold {
554         my($self, $client, $auth, $holdid, $cause, $note) = @_;
555
556         my $e = new_editor(authtoken=>$auth, xact=>1);
557         return $e->event unless $e->checkauth;
558
559         my $hold = $e->retrieve_action_hold_request($holdid)
560                 or return $e->event;
561
562         if( $e->requestor->id ne $hold->usr ) {
563                 return $e->event unless $e->allowed('CANCEL_HOLDS');
564         }
565
566         return 1 if $hold->cancel_time;
567
568         # If the hold is captured, reset the copy status
569         if( $hold->capture_time and $hold->current_copy ) {
570
571                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
572                         or return $e->event;
573
574                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
575          $logger->info("canceling hold $holdid whose item is on the holds shelf");
576 #                       $logger->info("setting copy to status 'reshelving' on hold cancel");
577 #                       $copy->status(OILS_COPY_STATUS_RESHELVING);
578 #                       $copy->editor($e->requestor->id);
579 #                       $copy->edit_date('now');
580 #                       $e->update_asset_copy($copy) or return $e->event;
581
582                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
583
584                         my $hid = $hold->id;
585                         $logger->warn("! canceling hold [$hid] that is in transit");
586                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
587
588                         if( $transid ) {
589                                 my $trans = $e->retrieve_action_transit_copy($transid);
590                                 # Leave the transit alive, but  set the copy status to 
591                                 # reshelving so it will be properly reshelved when it gets back home
592                                 if( $trans ) {
593                                         $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
594                                         $e->update_action_transit_copy($trans) or return $e->die_event;
595                                 }
596                         }
597                 }
598         }
599
600         $hold->cancel_time('now');
601     $hold->cancel_cause($cause);
602     $hold->cancel_note($note);
603         $e->update_action_hold_request($hold)
604                 or return $e->event;
605
606         delete_hold_copy_maps($self, $e, $hold->id);
607
608         $e->commit;
609         return 1;
610 }
611
612 sub delete_hold_copy_maps {
613         my $class = shift;
614         my $editor = shift;
615         my $holdid = shift;
616
617         my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
618         for(@$maps) {
619                 $editor->delete_action_hold_copy_map($_) 
620                         or return $editor->event;
621         }
622         return undef;
623 }
624
625
626 __PACKAGE__->register_method(
627         method  => "update_hold",
628         api_name        => "open-ils.circ.hold.update",
629         notes           => <<"  NOTE");
630         Updates the specified hold.  The login session
631         is the requestor and if the requestor is different from the usr field
632         on the hold, the requestor must have UPDATE_HOLDS permissions.
633         NOTE
634
635 __PACKAGE__->register_method(
636         method  => "batch_update_hold",
637         api_name        => "open-ils.circ.hold.update.batch",
638     stream => 1,
639         notes           => <<"  NOTE");
640         Updates the specified hold.  The login session
641         is the requestor and if the requestor is different from the usr field
642         on the hold, the requestor must have UPDATE_HOLDS permissions.
643         NOTE
644
645 sub update_hold {
646         my($self, $client, $auth, $hold, $values) = @_;
647     my $e = new_editor(authtoken=>$auth, xact=>1);
648     return $e->die_event unless $e->checkauth;
649     my $resp = update_hold_impl($self, $e, $hold, $values);
650     return $resp if $U->event_code($resp);
651     $e->commit;
652     return $resp;
653 }
654
655 sub batch_update_hold {
656         my($self, $client, $auth, $hold_list, $values_list) = @_;
657     my $e = new_editor(authtoken=>$auth);
658     return $e->die_event unless $e->checkauth;
659
660     my $count = ($hold_list) ? scalar(@$hold_list) : scalar(@$values_list);
661     $hold_list ||= [];
662     $values_list ||= [];
663
664     for my $idx (0..$count-1) {
665         $e->xact_begin;
666         my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
667         $e->xact_commit unless $U->event_code($resp);
668         $client->respond($resp);
669     }
670
671     $e->disconnect;
672     return undef;
673 }
674
675 sub update_hold_impl {
676     my($self, $e, $hold, $values) = @_;
677
678     unless($hold) {
679         $hold = $e->retrieve_action_hold_request($values->{id})
680             or return $e->die_event;
681         $hold->$_($values->{$_}) for keys %$values;
682     }
683
684     my $orig_hold = $e->retrieve_action_hold_request($hold->id)
685         or return $e->die_event;
686
687     # don't allow the user to be changed
688     return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
689
690     if($hold->usr ne $e->requestor->id) {
691         # if the hold is for a different user, make sure the 
692         # requestor has the appropriate permissions
693         my $usr = $e->retrieve_actor_user($hold->usr)
694             or return $e->die_event;
695         return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
696     }
697
698
699     # --------------------------------------------------------------
700     # Changing the request time is like playing God
701     # --------------------------------------------------------------
702     if($hold->request_time ne $orig_hold->request_time) {
703         return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
704         return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
705     }
706
707     # --------------------------------------------------------------
708     # if the hold is on the holds shelf or in transit and the pickup 
709     # lib changes we need to create a new transit.
710     # --------------------------------------------------------------
711     if($orig_hold->pickup_lib ne $hold->pickup_lib) {
712
713         my $status = _hold_status($e, $hold);
714
715         if($status == 3) { # in transit
716
717             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
718             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
719
720             $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
721
722             # update the transit to reflect the new pickup location
723                         my $transit = $e->search_action_hold_transit_copy(
724                 {hold=>$hold->id, dest_recv_time => undef})->[0] 
725                 or return $e->die_event;
726
727             $transit->prev_dest($transit->dest); # mark the previous destination on the transit
728             $transit->dest($hold->pickup_lib);
729             $e->update_action_hold_transit_copy($transit) or return $e->die_event;
730
731         } elsif($status == 4) { # on holds shelf
732
733             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
734             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
735
736             $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
737
738             # create the new transit
739             my $evt = transit_hold($e, $orig_hold, $hold, $e->retrieve_asset_copy($hold->current_copy));
740             return $evt if $evt;
741         }
742     } 
743
744     update_hold_if_frozen($self, $e, $hold, $orig_hold);
745     $e->update_action_hold_request($hold) or return $e->die_event;
746     $e->commit;
747
748     # a change to mint-condition changes the set of potential copies, so retarget the hold;
749     if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
750         _reset_hold($self, $e->requestor, $hold) 
751     }
752
753     return $hold->id;
754 }
755
756 sub transit_hold {
757     my($e, $orig_hold, $hold, $copy) = @_;
758     my $src = $orig_hold->pickup_lib;
759     my $dest = $hold->pickup_lib;
760
761     $logger->info("putting hold into transit on pickup_lib update");
762
763     my $transit = Fieldmapper::action::hold_transit_copy->new;
764     $transit->hold($hold->id);
765     $transit->source($src);
766     $transit->dest($dest);
767     $transit->target_copy($copy->id);
768     $transit->source_send_time('now');
769     $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
770
771     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
772     $copy->editor($e->requestor->id);
773     $copy->edit_date('now');
774
775     $e->create_action_hold_transit_copy($transit) or return $e->die_event;
776     $e->update_asset_copy($copy) or return $e->die_event;
777     return undef;
778 }
779
780 # if the hold is frozen, this method ensures that the hold is not "targeted", 
781 # that is, it clears the current_copy and prev_check_time to essentiallly 
782 # reset the hold.  If it is being activated, it runs the targeter in the background
783 sub update_hold_if_frozen {
784     my($self, $e, $hold, $orig_hold) = @_;
785     return if $hold->capture_time;
786
787     if($U->is_true($hold->frozen)) {
788         $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
789         $hold->clear_current_copy;
790         $hold->clear_prev_check_time;
791
792     } else {
793         if($U->is_true($orig_hold->frozen)) {
794             $logger->info("Running targeter on activated hold ".$hold->id);
795                 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
796         }
797     }
798 }
799 __PACKAGE__->register_method(
800         method  => "hold_note_CUD",
801         api_name        => "open-ils.circ.hold_request.note.cud");
802
803 sub hold_note_CUD {
804         my($self, $conn, $auth, $note) = @_;
805
806     my $e = new_editor(authtoken => $auth, xact => 1);
807     return $e->die_event unless $e->checkauth;
808
809     my $hold = $e->retrieve_action_hold_request($note->hold)
810         or return $e->die_event;
811
812     if($hold->usr ne $e->requestor->id) {
813         my $usr = $e->retrieve_actor_user($hold->usr);
814         return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
815         $note->staff('t') if $note->isnew;
816     }
817
818     if($note->isnew) {
819         $e->create_action_hold_request_note($note) or return $e->die_event;
820     } elsif($note->ischanged) {
821         $e->update_action_hold_request_note($note) or return $e->die_event;
822     } elsif($note->isdeleted) {
823         $e->delete_action_hold_request_note($note) or return $e->die_event;
824     }
825
826     $e->commit;
827     return $note->id;
828 }
829
830
831
832 __PACKAGE__->register_method(
833         method  => "retrieve_hold_status",
834         api_name        => "open-ils.circ.hold.status.retrieve",
835         notes           => <<"  NOTE");
836         Calculates the current status of the hold.
837         the requestor must have VIEW_HOLD permissions if the hold is for a user
838         other than the requestor.
839         Returns -1  on error (for now)
840         Returns 1 for 'waiting for copy to become available'
841         Returns 2 for 'waiting for copy capture'
842         Returns 3 for 'in transit'
843         Returns 4 for 'arrived'
844         Returns 5 for 'hold-shelf-delay'
845         NOTE
846
847 sub retrieve_hold_status {
848         my($self, $client, $auth, $hold_id) = @_;
849
850         my $e = new_editor(authtoken => $auth);
851         return $e->event unless $e->checkauth;
852         my $hold = $e->retrieve_action_hold_request($hold_id)
853                 or return $e->event;
854
855         if( $e->requestor->id != $hold->usr ) {
856                 return $e->event unless $e->allowed('VIEW_HOLD');
857         }
858
859         return _hold_status($e, $hold);
860
861 }
862
863 sub _hold_status {
864         my($e, $hold) = @_;
865         return 1 unless $hold->current_copy;
866         return 2 unless $hold->capture_time;
867
868         my $copy = $hold->current_copy;
869         unless( ref $copy ) {
870                 $copy = $e->retrieve_asset_copy($hold->current_copy)
871                         or return $e->event;
872         }
873
874         return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
875
876         if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
877
878         my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
879         return 4 unless $hs_wait_interval;
880
881         # if a hold_shelf_status_delay interval is defined and start_time plus 
882         # the interval is greater than now, consider the hold to be in the virtual 
883         # "on its way to the holds shelf" status. Return 5.
884
885         my $transit = $e->search_action_hold_transit_copy({hold => $hold->id})->[0];
886         my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
887         $start_time = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time));
888         my $end_time = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
889
890         return 5 if $end_time > DateTime->now;
891         return 4;
892     }
893
894         return -1;
895 }
896
897
898
899 __PACKAGE__->register_method(
900         method  => "retrieve_hold_queue_stats",
901         api_name        => "open-ils.circ.hold.queue_stats.retrieve",
902     signature => {
903         desc => q/
904             Returns object with total_holds count, queue_position, potential_copies count, and status code
905         /
906     }
907 );
908
909 sub retrieve_hold_queue_stats {
910     my($self, $conn, $auth, $hold_id) = @_;
911         my $e = new_editor(authtoken => $auth);
912         return $e->event unless $e->checkauth;
913         my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
914         if($e->requestor->id != $hold->usr) {
915                 return $e->event unless $e->allowed('VIEW_HOLD');
916         }
917     return retrieve_hold_queue_status_impl($e, $hold);
918 }
919
920 sub retrieve_hold_queue_status_impl {
921     my $e = shift;
922     my $hold = shift;
923
924     # The holds queue is defined as the distinct set of holds that share at 
925     # least one potential copy with the context hold, plus any holds that
926     # share the same hold type and target.  The latter part exists to
927     # accomodate holds that currently have no potential copies
928     my $q_holds = $e->json_query({
929
930         # fetch request_time since it's in the order_by and we're asking for distinct values
931         select => {ahr => ['id', 'request_time']},
932
933         from => {
934             ahr => {
935                 ahcm => {type => 'left'} # there may be no copy maps 
936             }
937         },
938         order_by => {ahr => ['request_time']},
939         distinct => 1,
940         where => {
941             '-or' => [
942                 {
943                     '+ahcm' => {
944                         target_copy => {
945                             in => {
946                                 select => {ahcm => ['target_copy']},
947                                 from => 'ahcm',
948                                 where => {hold => $hold->id}
949                             } 
950                         } 
951                     }
952                 },
953                 {
954                     '+ahr' => {
955                         hold_type => $hold->hold_type,
956                         target => $hold->target
957                     }
958                 }
959             ]
960         }, 
961     });
962
963     my $qpos = 1;
964     for my $h (@$q_holds) {
965         last if $h->{id} == $hold->id;
966         $qpos++;
967     }
968
969     # total count of potential copies
970     my $num_potentials = $e->json_query({
971         select => {ahcm => [{column => 'id', transform => 'count', alias => 'count'}]},
972         from => 'ahcm',
973         where => {hold => $hold->id}
974     })->[0];
975
976     my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
977     my $default_hold_interval = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
978     my $estimated_wait = $qpos * ($default_hold_interval / $num_potentials) if $default_hold_interval;
979
980     return {
981         total_holds => scalar(@$q_holds),
982         queue_position => $qpos,
983         potential_copies => $num_potentials->{count},
984         status => _hold_status($e, $hold),
985         estimated_wait => int($estimated_wait)
986     };
987 }
988
989
990 sub fetch_open_hold_by_current_copy {
991         my $class = shift;
992         my $copyid = shift;
993         my $hold = $apputils->simplereq(
994                 'open-ils.cstore', 
995                 'open-ils.cstore.direct.action.hold_request.search.atomic',
996                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
997         return $hold->[0] if ref($hold);
998         return undef;
999 }
1000
1001 sub fetch_related_holds {
1002         my $class = shift;
1003         my $copyid = shift;
1004         return $apputils->simplereq(
1005                 'open-ils.cstore', 
1006                 'open-ils.cstore.direct.action.hold_request.search.atomic',
1007                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
1008 }
1009
1010
1011 __PACKAGE__->register_method (
1012         method          => "hold_pull_list",
1013         api_name                => "open-ils.circ.hold_pull_list.retrieve",
1014         signature       => q/
1015                 Returns a list of holds that need to be "pulled"
1016                 by a given location
1017         /
1018 );
1019
1020 __PACKAGE__->register_method (
1021         method          => "hold_pull_list",
1022         api_name                => "open-ils.circ.hold_pull_list.id_list.retrieve",
1023         signature       => q/
1024                 Returns a list of hold ID's that need to be "pulled"
1025                 by a given location
1026         /
1027 );
1028
1029 __PACKAGE__->register_method (
1030         method          => "hold_pull_list",
1031         api_name                => "open-ils.circ.hold_pull_list.retrieve.count",
1032         signature       => q/
1033                 Returns a list of holds that need to be "pulled"
1034                 by a given location
1035         /
1036 );
1037
1038
1039 sub hold_pull_list {
1040         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1041         my( $reqr, $evt ) = $U->checkses($authtoken);
1042         return $evt if $evt;
1043
1044         my $org = $reqr->ws_ou || $reqr->home_ou;
1045         # the perm locaiton shouldn't really matter here since holds
1046         # will exist all over and VIEW_HOLDS should be universal
1047         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1048         return $evt if $evt;
1049
1050     if($self->api_name =~ /count/) {
1051
1052                 my $count = $U->storagereq(
1053                         'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1054                         $org, $limit, $offset ); 
1055
1056         $logger->info("Grabbing pull list for org unit $org with $count items");
1057         return $count
1058
1059     } elsif( $self->api_name =~ /id_list/ ) {
1060                 return $U->storagereq(
1061                         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1062                         $org, $limit, $offset ); 
1063
1064         } else {
1065                 return $U->storagereq(
1066                         'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1067                         $org, $limit, $offset ); 
1068         }
1069 }
1070
1071 __PACKAGE__->register_method (
1072         method          => 'fetch_hold_notify',
1073         api_name                => 'open-ils.circ.hold_notification.retrieve_by_hold',
1074     authoritative => 1,
1075         signature       => q/ 
1076                 Returns a list of hold notification objects based on hold id.
1077                 @param authtoken The loggin session key
1078                 @param holdid The id of the hold whose notifications we want to retrieve
1079                 @return An array of hold notification objects, event on error.
1080         /
1081 );
1082
1083 sub fetch_hold_notify {
1084         my( $self, $conn, $authtoken, $holdid ) = @_;
1085         my( $requestor, $evt ) = $U->checkses($authtoken);
1086         return $evt if $evt;
1087         my ($hold, $patron);
1088         ($hold, $evt) = $U->fetch_hold($holdid);
1089         return $evt if $evt;
1090         ($patron, $evt) = $U->fetch_user($hold->usr);
1091         return $evt if $evt;
1092
1093         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
1094         return $evt if $evt;
1095
1096         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
1097         return $U->cstorereq(
1098                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
1099 }
1100
1101
1102 __PACKAGE__->register_method (
1103         method          => 'create_hold_notify',
1104         api_name                => 'open-ils.circ.hold_notification.create',
1105         signature       => q/
1106                 Creates a new hold notification object
1107                 @param authtoken The login session key
1108                 @param notification The hold notification object to create
1109                 @return ID of the new object on success, Event on error
1110                 /
1111 );
1112
1113 sub create_hold_notify {
1114    my( $self, $conn, $auth, $note ) = @_;
1115    my $e = new_editor(authtoken=>$auth, xact=>1);
1116    return $e->die_event unless $e->checkauth;
1117
1118    my $hold = $e->retrieve_action_hold_request($note->hold)
1119       or return $e->die_event;
1120    my $patron = $e->retrieve_actor_user($hold->usr) 
1121       or return $e->die_event;
1122
1123    return $e->die_event unless 
1124       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1125
1126         $note->notify_staff($e->requestor->id);
1127    $e->create_action_hold_notification($note) or return $e->die_event;
1128    $e->commit;
1129    return $note->id;
1130 }
1131
1132 __PACKAGE__->register_method (
1133         method          => 'create_hold_note',
1134         api_name                => 'open-ils.circ.hold_note.create',
1135         signature       => q/
1136                 Creates a new hold request note object
1137                 @param authtoken The login session key
1138                 @param note The hold note object to create
1139                 @return ID of the new object on success, Event on error
1140                 /
1141 );
1142
1143 sub create_hold_note {
1144    my( $self, $conn, $auth, $note ) = @_;
1145    my $e = new_editor(authtoken=>$auth, xact=>1);
1146    return $e->die_event unless $e->checkauth;
1147
1148    my $hold = $e->retrieve_action_hold_request($note->hold)
1149       or return $e->die_event;
1150    my $patron = $e->retrieve_actor_user($hold->usr) 
1151       or return $e->die_event;
1152
1153    return $e->die_event unless 
1154       $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn.  Probably want something more specific
1155
1156    $e->create_action_hold_request_note($note) or return $e->die_event;
1157    $e->commit;
1158    return $note->id;
1159 }
1160
1161 __PACKAGE__->register_method(
1162         method  => 'reset_hold',
1163         api_name        => 'open-ils.circ.hold.reset',
1164         signature       => q/
1165                 Un-captures and un-targets a hold, essentially returning
1166                 it to the state it was in directly after it was placed,
1167                 then attempts to re-target the hold
1168                 @param authtoken The login session key
1169                 @param holdid The id of the hold
1170         /
1171 );
1172
1173
1174 sub reset_hold {
1175         my( $self, $conn, $auth, $holdid ) = @_;
1176         my $reqr;
1177         my ($hold, $evt) = $U->fetch_hold($holdid);
1178         return $evt if $evt;
1179         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1180         return $evt if $evt;
1181         $evt = _reset_hold($self, $reqr, $hold);
1182         return $evt if $evt;
1183         return 1;
1184 }
1185
1186
1187 __PACKAGE__->register_method(
1188         method  => 'reset_hold_batch',
1189         api_name        => 'open-ils.circ.hold.reset.batch'
1190 );
1191
1192 sub reset_hold_batch {
1193     my($self, $conn, $auth, $hold_ids) = @_;
1194
1195     my $e = new_editor(authtoken => $auth);
1196     return $e->event unless $e->checkauth;
1197
1198     for my $hold_id ($hold_ids) {
1199
1200         my $hold = $e->retrieve_action_hold_request(
1201             [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}]) 
1202             or return $e->event;
1203
1204             next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1205         _reset_hold($self, $e->requestor, $hold);
1206     }
1207
1208     return 1;
1209 }
1210
1211
1212 sub _reset_hold {
1213         my ($self, $reqr, $hold) = @_;
1214
1215         my $e = new_editor(xact =>1, requestor => $reqr);
1216
1217         $logger->info("reseting hold ".$hold->id);
1218
1219         my $hid = $hold->id;
1220
1221         if( $hold->capture_time and $hold->current_copy ) {
1222
1223                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1224                         or return $e->event;
1225
1226                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1227                         $logger->info("setting copy to status 'reshelving' on hold retarget");
1228                         $copy->status(OILS_COPY_STATUS_RESHELVING);
1229                         $copy->editor($e->requestor->id);
1230                         $copy->edit_date('now');
1231                         $e->update_asset_copy($copy) or return $e->event;
1232
1233                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1234
1235                         # We don't want the copy to remain "in transit"
1236                         $copy->status(OILS_COPY_STATUS_RESHELVING);
1237                         $logger->warn("! reseting hold [$hid] that is in transit");
1238                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1239
1240                         if( $transid ) {
1241                                 my $trans = $e->retrieve_action_transit_copy($transid);
1242                                 if( $trans ) {
1243                                         $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1244                                         my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
1245                                         $logger->info("Transit abort completed with result $evt");
1246                                         return $evt unless "$evt" eq 1;
1247                                 }
1248                         }
1249                 }
1250         }
1251
1252         $hold->clear_capture_time;
1253         $hold->clear_current_copy;
1254         $hold->clear_shelf_time;
1255         $hold->clear_shelf_expire_time;
1256
1257         $e->update_action_hold_request($hold) or return $e->event;
1258         $e->commit;
1259
1260         $U->storagereq(
1261                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1262
1263         return undef;
1264 }
1265
1266
1267 __PACKAGE__->register_method(
1268         method => 'fetch_open_title_holds',
1269         api_name        => 'open-ils.circ.open_holds.retrieve',
1270         signature       => q/
1271                 Returns a list ids of un-fulfilled holds for a given title id
1272                 @param authtoken The login session key
1273                 @param id the id of the item whose holds we want to retrieve
1274                 @param type The hold type - M, T, V, C
1275         /
1276 );
1277
1278 sub fetch_open_title_holds {
1279         my( $self, $conn, $auth, $id, $type, $org ) = @_;
1280         my $e = new_editor( authtoken => $auth );
1281         return $e->event unless $e->checkauth;
1282
1283         $type ||= "T";
1284         $org ||= $e->requestor->ws_ou;
1285
1286 #       return $e->search_action_hold_request(
1287 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
1288
1289         # XXX make me return IDs in the future ^--
1290         my $holds = $e->search_action_hold_request(
1291                 { 
1292                         target                          => $id, 
1293                         cancel_time                     => undef, 
1294                         hold_type                       => $type, 
1295                         fulfillment_time        => undef 
1296                 }
1297         );
1298
1299         flesh_hold_transits($holds);
1300         return $holds;
1301 }
1302
1303
1304 sub flesh_hold_transits {
1305         my $holds = shift;
1306         for my $hold ( @$holds ) {
1307                 $hold->transit(
1308                         $apputils->simplereq(
1309                                 'open-ils.cstore',
1310                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
1311                                 { hold => $hold->id },
1312                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
1313                         )->[0]
1314                 );
1315         }
1316 }
1317
1318 sub flesh_hold_notices {
1319         my( $holds, $e ) = @_;
1320         $e ||= new_editor();
1321
1322         for my $hold (@$holds) {
1323                 my $notices = $e->search_action_hold_notification(
1324                         [
1325                                 { hold => $hold->id },
1326                                 { order_by => { anh => 'notify_time desc' } },
1327                         ],
1328                         {idlist=>1}
1329                 );
1330
1331                 $hold->notify_count(scalar(@$notices));
1332                 if( @$notices ) {
1333                         my $n = $e->retrieve_action_hold_notification($$notices[0])
1334                                 or return $e->event;
1335                         $hold->notify_time($n->notify_time);
1336                 }
1337         }
1338 }
1339
1340
1341 __PACKAGE__->register_method(
1342         method => 'fetch_captured_holds',
1343         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1344     stream => 1,
1345         signature       => q/
1346                 Returns a list of un-fulfilled holds for a given title id
1347                 @param authtoken The login session key
1348                 @param org The org id of the location in question
1349         /
1350 );
1351
1352 __PACKAGE__->register_method(
1353         method => 'fetch_captured_holds',
1354         api_name        => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1355     stream => 1,
1356         signature       => q/
1357                 Returns a list ids of un-fulfilled holds for a given title id
1358                 @param authtoken The login session key
1359                 @param org The org id of the location in question
1360         /
1361 );
1362
1363 sub fetch_captured_holds {
1364         my( $self, $conn, $auth, $org ) = @_;
1365
1366         my $e = new_editor(authtoken => $auth);
1367         return $e->event unless $e->checkauth;
1368         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1369
1370         $org ||= $e->requestor->ws_ou;
1371
1372     my $hold_ids = $e->json_query(
1373         { 
1374             select => { ahr => ['id'] },
1375             from => {
1376                 ahr => {
1377                     acp => {
1378                         field => 'id',
1379                         fkey => 'current_copy'
1380                     },
1381                 }
1382             }, 
1383             where => {
1384                 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1385                 '+ahr' => {
1386                     capture_time                => { "!=" => undef },
1387                     current_copy                => { "!=" => undef },
1388                     fulfillment_time    => undef,
1389                     pickup_lib                  => $org,
1390                     cancel_time                 => undef,
1391                 }
1392             }
1393         },
1394     );
1395
1396     for my $hold_id (@$hold_ids) {
1397         if($self->api_name =~ /id_list/) {
1398             $conn->respond($hold_id->{id});
1399             next;
1400         } else {
1401             $conn->respond(
1402                 $e->retrieve_action_hold_request([
1403                     $hold_id->{id},
1404                     {
1405                         flesh => 1,
1406                         flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
1407                         order_by => {anh => 'notify_time desc'}
1408                     }
1409                 ])
1410             );
1411         }
1412     }
1413
1414     return undef;
1415 }
1416 __PACKAGE__->register_method(
1417         method  => "check_title_hold",
1418         api_name        => "open-ils.circ.title_hold.is_possible",
1419         notes           => q/
1420                 Determines if a hold were to be placed by a given user,
1421                 whether or not said hold would have any potential copies
1422                 to fulfill it.
1423                 @param authtoken The login session key
1424                 @param params A hash of named params including:
1425                         patronid  - the id of the hold recipient
1426                         titleid (brn) - the id of the title to be held
1427                         depth   - the hold range depth (defaults to 0)
1428         /);
1429
1430 sub check_title_hold {
1431         my( $self, $client, $authtoken, $params ) = @_;
1432
1433         my %params              = %$params;
1434         my $titleid             = $params{titleid} ||"";
1435         my $volid               = $params{volume_id};
1436         my $copyid              = $params{copy_id};
1437         my $mrid                = $params{mrid} ||"";
1438         my $depth               = $params{depth} || 0;
1439         my $pickup_lib  = $params{pickup_lib};
1440         my $hold_type   = $params{hold_type} || 'T';
1441     my $selection_ou = $params{selection_ou} || $pickup_lib;
1442
1443         my $e = new_editor(authtoken=>$authtoken);
1444         return $e->event unless $e->checkauth;
1445         my $patron = $e->retrieve_actor_user($params{patronid})
1446                 or return $e->event;
1447
1448         if( $e->requestor->id ne $patron->id ) {
1449                 return $e->event unless 
1450                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1451         }
1452
1453         return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1454
1455         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1456                 or return $e->event;
1457
1458     my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1459     my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1460
1461     if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1462         # work up the tree and as soon as we find a potential copy, use that depth
1463         # also, make sure we don't go past the hard boundary if it exists
1464
1465         # our min boundary is the greater of user-specified boundary or hard boundary
1466         my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?  
1467             $hard_boundary : $$params{depth};
1468
1469         my $depth = $soft_boundary;
1470         while($depth >= $min_depth) {
1471             $logger->info("performing hold possibility check with soft boundary $depth");
1472             my @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1473             return {success => 1, depth => $depth, local_avail => $status[1]} if $status[0];
1474             $depth--;
1475         }
1476         return {success => 0};
1477
1478     } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1479         # there is no soft boundary, enforce the hard boundary if it exists
1480         $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1481         my @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
1482         if($status[0]) {
1483             return {success => 1, depth => $hard_boundary, local_avail => $status[1]}
1484         } else {
1485             return {success => 0};
1486         }
1487
1488     } else {
1489         # no boundaries defined, fall back to user specifed boundary or no boundary
1490         $logger->info("performing hold possibility check with no boundary");
1491         my @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
1492         if($status[0]) {
1493             return {success => 1, depth => $hard_boundary, local_avail => $status[1]};
1494         } else {
1495             return {success => 0};
1496         }
1497     }
1498 }
1499
1500 sub do_possibility_checks {
1501     my($e, $patron, $request_lib, $depth, %params) = @_;
1502
1503         my $titleid             = $params{titleid} ||"";
1504         my $volid               = $params{volume_id};
1505         my $copyid              = $params{copy_id};
1506         my $mrid                = $params{mrid} ||"";
1507         my $pickup_lib  = $params{pickup_lib};
1508         my $hold_type   = $params{hold_type} || 'T';
1509     my $selection_ou = $params{selection_ou} || $pickup_lib;
1510
1511
1512         my $copy;
1513         my $volume;
1514         my $title;
1515
1516         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1517
1518                 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1519                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1520                         or return $e->event;
1521                 $title = $e->retrieve_biblio_record_entry($volume->record)
1522                         or return $e->event;
1523                 return verify_copy_for_hold( 
1524                         $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1525
1526         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1527
1528                 $volume = $e->retrieve_asset_call_number($volid)
1529                         or return $e->event;
1530                 $title = $e->retrieve_biblio_record_entry($volume->record)
1531                         or return $e->event;
1532
1533                 return _check_volume_hold_is_possible(
1534                         $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1535
1536         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1537
1538                 return _check_title_hold_is_possible(
1539                         $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1540
1541         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1542
1543                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1544                 my @recs = map { $_->source } @$maps;
1545                 for my $rec (@recs) {
1546             my @status = _check_title_hold_is_possible(
1547                                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1548             return @status if $status[1];
1549                 }
1550                 return (0);     
1551         }
1552 }
1553
1554 my %prox_cache;
1555 sub create_ranged_org_filter {
1556     my($e, $selection_ou, $depth) = @_;
1557
1558     # find the orgs from which this hold may be fulfilled, 
1559     # based on the selection_ou and depth
1560
1561     my $top_org = $e->search_actor_org_unit([
1562         {parent_ou => undef}, 
1563         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1564     my %org_filter;
1565
1566     return () if $depth == $top_org->ou_type->depth;
1567
1568     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1569     %org_filter = (circ_lib => []);
1570     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1571
1572     $logger->info("hold org filter at depth $depth and selection_ou ".
1573         "$selection_ou created list of @{$org_filter{circ_lib}}");
1574
1575     return %org_filter;
1576 }
1577
1578
1579 sub _check_title_hold_is_possible {
1580         my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1581    
1582     my $e = new_editor();
1583     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1584
1585     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1586     my $copies = $e->json_query(
1587         { 
1588             select => { acp => ['id', 'circ_lib'] },
1589             from => {
1590                 acp => {
1591                     acn => {
1592                         field => 'id',
1593                         fkey => 'call_number',
1594                         'join' => {
1595                             bre => {
1596                                 field => 'id',
1597                                 filter => { id => $titleid },
1598                                 fkey => 'record'
1599                             }
1600                         }
1601                     },
1602                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1603                     ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1604                 }
1605             }, 
1606             where => {
1607                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1608             }
1609         }
1610     );
1611
1612    $logger->info("title possible found ".scalar(@$copies)." potential copies");
1613    return (0) unless @$copies;
1614
1615    # -----------------------------------------------------------------------
1616    # sort the copies into buckets based on their circ_lib proximity to 
1617    # the patron's home_ou.  
1618    # -----------------------------------------------------------------------
1619
1620    my $home_org = $patron->home_ou;
1621    my $req_org = $request_lib->id;
1622
1623     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1624
1625     $prox_cache{$home_org} = 
1626         $e->search_actor_org_unit_proximity({from_org => $home_org})
1627         unless $prox_cache{$home_org};
1628     my $home_prox = $prox_cache{$home_org};
1629
1630    my %buckets;
1631    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1632    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1633
1634    my @keys = sort { $a <=> $b } keys %buckets;
1635
1636
1637    if( $home_org ne $req_org ) {
1638       # -----------------------------------------------------------------------
1639       # shove the copies close to the request_lib into the primary buckets 
1640       # directly before the farthest away copies.  That way, they are not 
1641       # given priority, but they are checked before the farthest copies.
1642       # -----------------------------------------------------------------------
1643         $prox_cache{$req_org} = 
1644             $e->search_actor_org_unit_proximity({from_org => $req_org})
1645             unless $prox_cache{$req_org};
1646         my $req_prox = $prox_cache{$req_org};
1647
1648
1649       my %buckets2;
1650       my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1651       push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1652
1653       my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1654       my $new_key = $highest_key - 0.5; # right before the farthest prox
1655       my @keys2 = sort { $a <=> $b } keys %buckets2;
1656       for my $key (@keys2) {
1657          last if $key >= $highest_key;
1658          push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1659       }
1660    }
1661
1662    @keys = sort { $a <=> $b } keys %buckets;
1663
1664    my $title;
1665    my %seen;
1666    for my $key (@keys) {
1667       my @cps = @{$buckets{$key}};
1668
1669       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1670
1671       for my $copyid (@cps) {
1672
1673          next if $seen{$copyid};
1674          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1675          my $copy = $e->retrieve_asset_copy($copyid);
1676          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1677
1678          unless($title) { # grab the title if we don't already have it
1679             my $vol = $e->retrieve_asset_call_number(
1680                [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1681             $title = $vol->record;
1682          }
1683    
1684          my @status = verify_copy_for_hold( 
1685             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1686
1687         return @status if $status[0];
1688       }
1689    }
1690
1691    return (0);
1692 }
1693
1694
1695 sub _check_volume_hold_is_possible {
1696         my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1697     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1698         my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1699         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1700         for my $copy ( @$copies ) {
1701         my @status = verify_copy_for_hold( 
1702                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1703         return @status if $status[0];
1704         }
1705         return (0);
1706 }
1707
1708
1709
1710 sub verify_copy_for_hold {
1711         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1712         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1713     my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1714                 {       patron                          => $patron, 
1715                         requestor                       => $requestor, 
1716                         copy                            => $copy,
1717                         title                           => $title, 
1718                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1719                         pickup_lib                      => $pickup_lib,
1720                         request_lib                     => $request_lib,
1721             new_hold            => 1
1722                 } 
1723         );
1724
1725     return (
1726         $permitted,
1727         (
1728                 ($copy->circ_lib == $pickup_lib) and 
1729             ($copy->status == OILS_COPY_STATUS_AVAILABLE)
1730         )
1731     );
1732 }
1733
1734
1735
1736 sub find_nearest_permitted_hold {
1737
1738         my $class       = shift;
1739         my $editor      = shift; # CStoreEditor object
1740         my $copy                = shift; # copy to target
1741         my $user                = shift; # staff 
1742         my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1743         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1744
1745         my $bc = $copy->barcode;
1746
1747         # find any existing holds that already target this copy
1748         my $old_holds = $editor->search_action_hold_request(
1749                 {       current_copy => $copy->id, 
1750                         cancel_time => undef, 
1751                         capture_time => undef 
1752                 } 
1753         );
1754
1755         # hold->type "R" means we need this copy
1756         for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1757
1758
1759     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1760
1761         $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1762         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1763
1764         # search for what should be the best holds for this copy to fulfill
1765         my $best_holds = $U->storagereq(
1766                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1767                 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1768
1769         unless(@$best_holds) {
1770
1771                 if( my $hold = $$old_holds[0] ) {
1772                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1773                         return ($hold);
1774                 }
1775
1776                 $logger->info("circulator: no suitable holds found for copy $bc");
1777                 return (undef, $evt);
1778         }
1779
1780
1781         my $best_hold;
1782
1783         # for each potential hold, we have to run the permit script
1784         # to make sure the hold is actually permitted.
1785         for my $holdid (@$best_holds) {
1786                 next unless $holdid;
1787                 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1788
1789                 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1790                 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1791                 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1792
1793                 # see if this hold is permitted
1794                 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1795                         {       patron_id                       => $hold->usr,
1796                                 requestor                       => $reqr,
1797                                 copy                            => $copy,
1798                                 pickup_lib                      => $hold->pickup_lib,
1799                                 request_lib                     => $rlib,
1800                         } 
1801                 );
1802
1803                 if( $permitted ) {
1804                         $best_hold = $hold;
1805                         last;
1806                 }
1807         }
1808
1809
1810         unless( $best_hold ) { # no "good" permitted holds were found
1811                 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1812                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1813                         return ($hold);
1814                 }
1815
1816                 # we got nuthin
1817                 $logger->info("circulator: no suitable holds found for copy $bc");
1818                 return (undef, $evt);
1819         }
1820
1821         $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1822
1823         # indicate a permitted hold was found
1824         return $best_hold if $check_only;
1825
1826         # we've found a permitted hold.  we need to "grab" the copy 
1827         # to prevent re-targeted holds (next part) from re-grabbing the copy
1828         $best_hold->current_copy($copy->id);
1829         $editor->update_action_hold_request($best_hold) 
1830                 or return (undef, $editor->event);
1831
1832
1833     my @retarget;
1834
1835         # re-target any other holds that already target this copy
1836         for my $old_hold (@$old_holds) {
1837                 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1838                 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1839             $old_hold->id." after a better hold [".$best_hold->id."] was found");
1840         $old_hold->clear_current_copy;
1841         $old_hold->clear_prev_check_time;
1842         $editor->update_action_hold_request($old_hold) 
1843             or return (undef, $editor->event);
1844         push(@retarget, $old_hold->id);
1845         }
1846
1847         return ($best_hold, undef, (@retarget) ? \@retarget : undef);
1848 }
1849
1850
1851
1852
1853
1854
1855 __PACKAGE__->register_method(
1856         method => 'all_rec_holds',
1857         api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1858 );
1859
1860 sub all_rec_holds {
1861         my( $self, $conn, $auth, $title_id, $args ) = @_;
1862
1863         my $e = new_editor(authtoken=>$auth);
1864         $e->checkauth or return $e->event;
1865         $e->allowed('VIEW_HOLD') or return $e->event;
1866
1867         $args ||= {};
1868     $args->{fulfillment_time} = undef; #  we don't want to see old fulfilled holds
1869         $args->{cancel_time} = undef;
1870
1871         my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1872
1873     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1874     if($mr_map) {
1875         $resp->{metarecord_holds} = $e->search_action_hold_request(
1876             {   hold_type => OILS_HOLD_TYPE_METARECORD,
1877                 target => $mr_map->metarecord,
1878                 %$args 
1879             }, {idlist => 1}
1880         );
1881     }
1882
1883         $resp->{title_holds} = $e->search_action_hold_request(
1884                 { 
1885                         hold_type => OILS_HOLD_TYPE_TITLE, 
1886                         target => $title_id, 
1887                         %$args 
1888                 }, {idlist=>1} );
1889
1890         my $vols = $e->search_asset_call_number(
1891                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1892
1893         return $resp unless @$vols;
1894
1895         $resp->{volume_holds} = $e->search_action_hold_request(
1896                 { 
1897                         hold_type => OILS_HOLD_TYPE_VOLUME, 
1898                         target => $vols,
1899                         %$args }, 
1900                 {idlist=>1} );
1901
1902         my $copies = $e->search_asset_copy(
1903                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1904
1905         return $resp unless @$copies;
1906
1907         $resp->{copy_holds} = $e->search_action_hold_request(
1908                 { 
1909                         hold_type => OILS_HOLD_TYPE_COPY,
1910                         target => $copies,
1911                         %$args }, 
1912                 {idlist=>1} );
1913
1914         return $resp;
1915 }
1916
1917
1918
1919
1920
1921 __PACKAGE__->register_method(
1922         method => 'uber_hold',
1923     authoritative => 1,
1924         api_name => 'open-ils.circ.hold.details.retrieve'
1925 );
1926
1927 sub uber_hold {
1928         my($self, $client, $auth, $hold_id) = @_;
1929         my $e = new_editor(authtoken=>$auth);
1930         $e->checkauth or return $e->event;
1931     return uber_hold_impl($e, $hold_id);
1932 }
1933
1934 __PACKAGE__->register_method(
1935         method => 'batch_uber_hold',
1936     authoritative => 1,
1937     stream => 1,
1938         api_name => 'open-ils.circ.hold.details.batch.retrieve'
1939 );
1940
1941 sub batch_uber_hold {
1942         my($self, $client, $auth, $hold_ids) = @_;
1943         my $e = new_editor(authtoken=>$auth);
1944         $e->checkauth or return $e->event;
1945     $client->respond(uber_hold_impl($e, $_)) for @$hold_ids;
1946     return undef;
1947 }
1948
1949 sub uber_hold_impl {
1950     my($e, $hold_id) = @_;
1951
1952         my $resp = {};
1953
1954         my $hold = $e->retrieve_action_hold_request(
1955                 [
1956                         $hold_id,
1957                         {
1958                                 flesh => 1,
1959                                 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
1960                         }
1961                 ]
1962         ) or return $e->event;
1963
1964     if($hold->usr->id ne $e->requestor->id) {
1965         # A user is allowed to see his/her own holds
1966             $e->allowed('VIEW_HOLD') or return $e->event;
1967     }
1968
1969         my $user = $hold->usr;
1970         $hold->usr($user->id);
1971
1972         my $card = $e->retrieve_actor_card($user->card)
1973                 or return $e->event;
1974
1975         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1976
1977         flesh_hold_notices([$hold], $e);
1978         flesh_hold_transits([$hold]);
1979
1980     my $details = retrieve_hold_queue_status_impl($e, $hold);
1981
1982         return {
1983                 hold            => $hold,
1984                 copy            => $copy,
1985                 volume  => $volume,
1986                 mvr             => $mvr,
1987                 patron_first => $user->first_given_name,
1988                 patron_last  => $user->family_name,
1989                 patron_barcode => $card->barcode,
1990         %$details
1991         };
1992 }
1993
1994
1995
1996 # -----------------------------------------------------
1997 # Returns the MVR object that represents what the
1998 # hold is all about
1999 # -----------------------------------------------------
2000 sub find_hold_mvr {
2001         my( $e, $hold ) = @_;
2002
2003         my $tid;
2004         my $copy;
2005         my $volume;
2006
2007         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2008                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
2009                         or return $e->event;
2010                 $tid = $mr->master_record;
2011
2012         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
2013                 $tid = $hold->target;
2014
2015         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2016                 $volume = $e->retrieve_asset_call_number($hold->target)
2017                         or return $e->event;
2018                 $tid = $volume->record;
2019
2020         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
2021                 $copy = $e->retrieve_asset_copy($hold->target)
2022                         or return $e->event;
2023                 $volume = $e->retrieve_asset_call_number($copy->call_number)
2024                         or return $e->event;
2025                 $tid = $volume->record;
2026         }
2027
2028         if(!$copy and ref $hold->current_copy ) {
2029                 $copy = $hold->current_copy;
2030                 $hold->current_copy($copy->id);
2031         }
2032
2033         if(!$volume and $copy) {
2034                 $volume = $e->retrieve_asset_call_number($copy->call_number);
2035         }
2036
2037     # TODO return metarcord mvr for M holds
2038         my $title = $e->retrieve_biblio_record_entry($tid);
2039         return ( $U->record_to_mvr($title), $volume, $copy );
2040 }
2041
2042
2043 __PACKAGE__->register_method(
2044         method => 'clear_shelf_process',
2045     stream => 1,
2046         api_name => 'open-ils.circ.hold.clear_shelf.process',
2047     signature => {
2048         desc => q/
2049             1. Find all holds that have expired on the holds shelf
2050             2. Cancel the holds
2051             3. If a clear-shelf status is configured, put targeted copies into this status
2052             4. Divide copies into 3 groups: items to transit, items to reshelve, and items
2053                 that are needed for holds.  No subsequent action is taken on the holds
2054                 or items after grouping.
2055         /
2056     }
2057 );
2058
2059 sub clear_shelf_process {
2060         my($self, $client, $auth, $org_id) = @_;
2061
2062         my $e = new_editor(authtoken=>$auth, xact => 1);
2063         $e->checkauth or return $e->die_event;
2064
2065     $org_id ||= $e->requestor->ws_ou;
2066         $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
2067
2068     my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
2069
2070     # Find holds on the shelf that have been there too long
2071     my $hold_ids = $e->search_action_hold_request(
2072         {   shelf_expire_time => {'<' => 'now'},
2073             pickup_lib => $org_id,
2074             cancel_time => undef,
2075             fulfillment_time => undef,
2076             shelf_time => {'!=' => undef}
2077         },
2078         { idlist => 1 }
2079     );
2080
2081
2082     my @holds;
2083     for my $hold_id (@$hold_ids) {
2084
2085         $logger->info("Clear shelf processing hold $hold_id");
2086         
2087         my $hold = $e->retrieve_action_hold_request([
2088             $hold_id, {   
2089                 flesh => 1,
2090                 flesh_fields => {ahr => ['current_copy']}
2091             }
2092         ]);
2093
2094         $hold->cancel_time('now');
2095         $hold->cancel_cause(2); # Hold Shelf expiration
2096         $e->update_action_hold_request($hold) or return $e->die_event;
2097
2098         my $copy = $hold->current_copy;
2099
2100         if($copy_status) {
2101             # if a clear-shelf copy status is defined, update the copy
2102             $copy->status($copy_status);
2103             $copy->edit_date('now');
2104             $copy->editor($e->requestor->id);
2105             $e->update_asset_copy($copy) or return $e->die_event;
2106         }
2107
2108         my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
2109
2110         if($alt_hold) {
2111
2112             # copy is needed for a hold
2113             $client->respond({action => 'hold', copy => $copy, hold_id => $hold->id});
2114
2115         } elsif($copy->circ_lib != $e->requestor->ws_ou) {
2116
2117             # copy needs to transit
2118             $client->respond({action => 'transit', copy => $copy, hold_id => $hold->id});
2119
2120         } else {
2121
2122             # copy needs to go back to the shelf
2123             $client->respond({action => 'shelf', copy => $copy, hold_id => $hold->id});
2124         }
2125
2126         push(@holds, $hold);
2127     }
2128
2129     $e->commit;
2130
2131     # tell the client we're done
2132     $client->resopnd_complete;
2133
2134     # fire off the hold cancelation trigger
2135     my $trigger = OpenSRF::AppSession->connect('open-ils.trigger');
2136
2137     for my $hold (@holds) {
2138
2139         my $req = $trigger->request(
2140             'open-ils.trigger.event.autocreate', 
2141             'hold_request.cancel.expire_holds_shelf', 
2142             $hold, $org_id);
2143
2144         # wait for response so don't flood the service
2145         $req->recv;
2146     }
2147
2148     $trigger->disconnect;
2149 }
2150
2151
2152 __PACKAGE__->register_method(
2153         method => 'usr_hold_summary',
2154         api_name => 'open-ils.circ.holds.user_summary',
2155     signature => q/
2156         Returns a summary of holds statuses for a given user
2157     /
2158 );
2159
2160 sub usr_hold_summary {
2161     my($self, $conn, $auth, $user_id) = @_;
2162
2163         my $e = new_editor(authtoken=>$auth);
2164         $e->checkauth or return $e->event;
2165         $e->allowed('VIEW_HOLD') or return $e->event;
2166
2167     my $holds = $e->search_action_hold_request(
2168         {  
2169             usr =>  $user_id , 
2170             fulfillment_time => undef,
2171             cancel_time => undef,
2172         }
2173     );
2174
2175     my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
2176     $summary{_hold_status($e, $_)} += 1 for @$holds;
2177     return \%summary;
2178 }
2179
2180
2181
2182 __PACKAGE__->register_method(
2183         method => 'hold_has_copy_at',
2184         api_name => 'open-ils.circ.hold.has_copy_at',
2185     signature => q/
2186         Returns the ID of the found copy and name of the shelving location if there is
2187         an available copy at the specified org unit.  Returns empty hash otherwise.
2188     /
2189 );
2190
2191 sub hold_has_copy_at {
2192     my($self, $conn, $auth, $args) = @_;
2193
2194         my $e = new_editor(authtoken=>$auth);
2195         $e->checkauth or return $e->event;
2196
2197     my $hold_type = $$args{hold_type};
2198     my $hold_target = $$args{hold_target};
2199     my $org_unit = $$args{org_unit};
2200
2201     my $query = {
2202         select => {acp => ['id'], acpl => ['name']},
2203         from => {
2204             acp => {
2205                 acpl => {field => 'id', filter => { holdable => 't'}, fkey => 'location'},
2206                 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status'}
2207             }
2208         },
2209         where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit}},
2210         limit => 1
2211     };
2212
2213     if($hold_type eq 'C') {
2214
2215         $query->{where}->{'+acp'}->{id} = $hold_target;
2216
2217     } elsif($hold_type eq 'V') {
2218
2219         $query->{where}->{'+acp'}->{call_number} = $hold_target;
2220     
2221     } elsif($hold_type eq 'T') {
2222
2223         $query->{from}->{acp}->{acn} = {
2224             field => 'id',
2225             fkey => 'call_number',
2226             'join' => {
2227                 bre => {
2228                     field => 'id',
2229                     filter => {id => $hold_target},
2230                     fkey => 'record'
2231                 }
2232             }
2233         };
2234
2235     } else {
2236
2237         $query->{from}->{acp}->{acn} = {
2238             field => 'id',
2239             fkey => 'call_number',
2240             join => {
2241                 bre => {
2242                     field => 'id',
2243                     fkey => 'record',
2244                     join => {
2245                         mmrsm => {
2246                             field => 'source',
2247                             fkey => 'id',
2248                             filter => {metarecord => $hold_target},
2249                         }
2250                     }
2251                 }
2252             }
2253         };
2254     }
2255
2256     my $res = $e->json_query($query)->[0] or return {};
2257     return {copy => $res->{id}, location => $res->{name}} if $res;
2258 }
2259
2260
2261 # returns true if the user already has an item checked out 
2262 # that could be used to fulfill the requested hold.
2263 sub hold_item_is_checked_out {
2264     my($e, $user_id, $hold_type, $hold_target) = @_;
2265
2266     my $query = {
2267         select => {acp => ['id']},
2268         from => {acp => {}},
2269         where => {
2270             '+acp' => {
2271                 id => {
2272                     in => { # copies for circs the user has checked out
2273                         select => {circ => ['target_copy']},
2274                         from => 'circ',
2275                         where => {
2276                             usr => $user_id,
2277                             checkin_time => undef,
2278                             '-or' => [
2279                                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2280                                 {stop_fines => undef}
2281                             ],
2282                         }
2283                     }
2284                 }
2285             }
2286         },
2287         limit => 1
2288     };
2289
2290     if($hold_type eq 'C') {
2291
2292         $query->{where}->{'+acp'}->{id} = $hold_target;
2293
2294     } elsif($hold_type eq 'V') {
2295
2296         $query->{where}->{'+acp'}->{call_number} = $hold_target;
2297     
2298     } elsif($hold_type eq 'T') {
2299
2300         $query->{from}->{acp}->{acn} = {
2301             field => 'id',
2302             fkey => 'call_number',
2303             'join' => {
2304                 bre => {
2305                     field => 'id',
2306                     filter => {id => $hold_target},
2307                     fkey => 'record'
2308                 }
2309             }
2310         };
2311
2312     } else {
2313
2314         $query->{from}->{acp}->{acn} = {
2315             field => 'id',
2316             fkey => 'call_number',
2317             join => {
2318                 bre => {
2319                     field => 'id',
2320                     fkey => 'record',
2321                     join => {
2322                         mmrsm => {
2323                             field => 'source',
2324                             fkey => 'id',
2325                             filter => {metarecord => $hold_target},
2326                         }
2327                     }
2328                 }
2329             }
2330         };
2331     }
2332
2333     return $e->json_query($query)->[0];
2334 }
2335
2336 __PACKAGE__->register_method(
2337         method => 'change_hold_title',
2338         api_name => 'open-ils.circ.hold.change_title',
2339     signature => {
2340         desc => q/
2341             Updates all title level holds targeting the specified bibs to point a new bib./,
2342         params => [
2343             {desc => 'Authentication Token', type => 'string'},
2344             {desc => 'New Target Bib Id', type => 'number'},
2345             {desc => 'Old Target Bib Ids', type => 'array'},
2346         ],
2347         return => { desc => '1 on success' }
2348     }
2349 );
2350
2351 sub change_hold_title {
2352     my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
2353
2354     my $e = new_editor(authtoken=>$auth, xact=>1);
2355     return $e->event unless $e->checkauth;
2356
2357     my $holds = $e->json_query({
2358         "select"=>{"ahr"=>["id"]},
2359         "from"=>"ahr",
2360         "where"=>{
2361             cancel_time => undef,
2362             fulfillment_time => undef,
2363             hold_type => 'T',
2364             target => $bib_ids
2365         }
2366     });
2367
2368     for my $hold_id (@$holds) {
2369         my $hold = $e->retrieve_action_hold_request([$hold_id->{id}, {
2370                 flesh=> 1, 
2371                 flesh_fields=>{ahr=>['usr']}
2372             }
2373         ]);
2374         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->event;
2375         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
2376         $hold->target( $new_bib_id );
2377         unless ($e->update_action_hold_request($hold)) {
2378             my $evt = $e->event;
2379             $logger->error("Error updating hold " . $evt->textcode . ":" . $evt->desc . ":" . $evt->stacktrace);
2380         }
2381     }
2382
2383     $e->commit;
2384
2385     return 1;
2386 }
2387
2388
2389 1;