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