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