]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
LP1915464 follow-up: use spaces, not tabs; remove extra comma
[Evergreen.git] / Open-ILS / src / perlmods / lib / 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 List::Util qw(shuffle);
21 use OpenILS::Application::AppUtils;
22 use DateTime;
23 use Data::Dumper;
24 use OpenSRF::EX qw(:try);
25 use OpenILS::Perm;
26 use OpenILS::Event;
27 use OpenSRF::Utils;
28 use OpenSRF::Utils::Logger qw(:logger);
29 use OpenILS::Utils::CStoreEditor q/:funcs/;
30 use OpenILS::Utils::PermitHold;
31 use OpenSRF::Utils::SettingsClient;
32 use OpenILS::Const qw/:const/;
33 use OpenILS::Application::Circ::Transit;
34 use OpenILS::Application::Actor::Friends;
35 use DateTime;
36 use DateTime::Format::ISO8601;
37 use OpenILS::Utils::DateTime qw/:datetime/;
38 use Digest::MD5 qw(md5_hex);
39 use OpenSRF::Utils::Cache;
40 use OpenSRF::Utils::JSON;
41 my $apputils = "OpenILS::Application::AppUtils";
42 my $U = $apputils;
43
44 __PACKAGE__->register_method(
45     method    => "test_and_create_hold_batch",
46     api_name  => "open-ils.circ.holds.test_and_create.batch",
47     stream => 1,
48     signature => {
49         desc => q/This is for batch creating a set of holds where every field is identical except for the targets./,
50         params => [
51             { desc => 'Authentication token', type => 'string' },
52             { desc => 'Hash of named parameters.  Same as for open-ils.circ.title_hold.is_possible, though the pertinent target field is automatically populated based on the hold_type and the specified list of targets.', type => 'object'},
53             { desc => 'Array of target ids', type => 'array' }
54         ],
55         return => {
56             desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
57         },
58     }
59 );
60
61 __PACKAGE__->register_method(
62     method    => "test_and_create_hold_batch",
63     api_name  => "open-ils.circ.holds.test_and_create.batch.override",
64     stream => 1,
65     signature => {
66         desc  => '@see open-ils.circ.holds.test_and_create.batch',
67     }
68 );
69
70
71 sub test_and_create_hold_batch {
72     my( $self, $conn, $auth, $params, $target_list, $oargs ) = @_;
73
74     my $override = 0;
75     if ($self->api_name =~ /override/) {
76         $override = 1;
77         $oargs = { all => 1 } unless defined $oargs;
78         $$params{oargs} = $oargs; # for is_possible checking.
79     }
80
81     my $e = new_editor(authtoken=>$auth);
82     return $e->die_event unless $e->checkauth;
83     $$params{'requestor'} = $e->requestor->id;
84
85     my $target_field;
86     if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
87     elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
88     elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
89     elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
90     elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
91     elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
92     elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
93     elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
94     else { return undef; }
95
96     my $formats_map = delete $$params{holdable_formats_map};
97
98     foreach (@$target_list) {
99         $$params{$target_field} = $_;
100
101         # copy the requested formats from the target->formats map
102         # into the top-level formats attr for each hold
103         $$params{holdable_formats} = $formats_map->{$_};
104
105         my $res;
106         ($res) = $self->method_lookup(
107             'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
108         if ($res->{'success'} == 1 || ($override && $res->{place_unfillable})) {
109
110             $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
111
112             if ($$oargs{honor_user_settings}) {
113                 my $recipient = $e->retrieve_actor_user($$params{patronid})
114                     or return $e->die_event;
115                 my $opac_hold_notify = $e->search_actor_user_setting(
116                     {usr => $$params{patronid}, name => 'opac.hold_notify'})->[0];
117                 if ($opac_hold_notify) {
118                     if ($opac_hold_notify->value =~ 'email') {
119                         $$params{email_notify} = 1;
120                     }
121                     if ($opac_hold_notify->value =~ 'phone') {
122                         my $opac_default_phone = $e->search_actor_user_setting(
123                             {usr => $$params{patronid}, name => 'opac.default_phone'})->[0];
124                         # FIXME - what's up with the ->value putting quotes around the string?
125                         if ($opac_default_phone && $opac_default_phone->value =~ /^"(.*)"$/) {
126                             $$params{phone_notify} = $1;
127                         }
128                     }
129                     if ($opac_hold_notify->value =~ 'sms') {
130                         my $opac_default_sms_carrier = $e->search_actor_user_setting(
131                             {usr => $$params{patronid}, name => 'opac.default_sms_carrier'})->[0];
132                         $$params{sms_carrier} = $opac_default_sms_carrier->value if $opac_default_sms_carrier;
133                         my $opac_default_sms_notify = $e->search_actor_user_setting(
134                             {usr => $$params{patronid}, name => 'opac.default_sms_notify'})->[0];
135                         if ($opac_default_sms_notify && $opac_default_sms_notify->value =~ /^"(.*)"$/) {
136                             $$params{sms_notify} = $1;
137                         }
138                     }
139                 }
140             }
141
142             # Remove oargs from params so holds can be created.
143             if ($$params{oargs}) {
144                 delete $$params{oargs};
145             }
146
147             my $ahr = construct_hold_request_object($params);
148             my ($res2) = $self->method_lookup(
149                 $override
150                 ? 'open-ils.circ.holds.create.override'
151                 : 'open-ils.circ.holds.create'
152             )->run($auth, $ahr, $oargs);
153             $res2 = {
154                 'target' => $$params{$target_field},
155                 'result' => $res2
156             };
157             $conn->respond($res2);
158         } else {
159             $res = {
160                 'target' => $$params{$target_field},
161                 'result' => $res
162             };
163             $conn->respond($res);
164         }
165     }
166     return undef;
167 }
168
169 __PACKAGE__->register_method(
170     method    => "test_and_create_batch_hold_event",
171     api_name  => "open-ils.circ.holds.test_and_create.subscription_batch",
172     stream => 1,
173     signature => {
174         desc => q/This is for batch creating a set of holds where every field is identical except for the target users./,
175         params => [
176             { desc => 'Authentication token', type => 'string' },
177             { desc => 'Hash of named parameters.  Same as for open-ils.circ.title_hold.is_possible, though the pertinent target field is automatically populated based on the hold_type and the specified list of target users.', type => 'object'},
178             { desc => 'Container ID of the user bucket holding target users', type => 'number' },
179             { desc => 'target object ID (clarified by hold_type in param hash)', type => 'number' },
180             { desc => 'Randomize flag, set to 0 for "not randomized"', type => 'bool' }
181         ],
182         return => {
183             desc => 'Stream of objects structured as {total=>X, count=>Y, target=>Z, patronid=>A, result=>$hold_id} on success, -1 on missing arg, event (or stream of events on "result" key of object) on error(s)',
184         },
185     }
186 );
187
188 __PACKAGE__->register_method(
189     method    => "test_and_create_batch_hold_event",
190     api_name  => "open-ils.circ.holds.test_and_create.subscription_batch.override",
191     stream => 1,
192     signature => {
193         desc  => '@see open-ils.circ.holds.test_and_create.subscription_batch',
194     }
195 );
196
197
198 sub test_and_create_batch_hold_event {
199     my( $self, $conn, $auth, $params, $target_bucket, $target_id, $randomize, $oargs ) = @_;
200
201
202     $randomize //= 1; # default to random hold creation order
203     $$params{hold_type} //= 'T'; # default to title holds
204
205     my $override = 0;
206     if ($self->api_name =~ /override/) {
207         $override = 1;
208         $oargs = { all => 1 } unless defined $oargs;
209         $$params{oargs} = $oargs; # for is_possible checking.
210     }
211
212     my $e = new_editor(authtoken=>$auth);
213     return $e->die_event unless $e->checkauth;
214     $$params{'requestor'} = $e->requestor->id;
215
216
217     my $org = $e->requestor->ws_ou || $e->requestor->home_ou;
218     # the perm locaiton shouldn't really matter here since holds
219     # will exist all over and MANAGE_HOLD_GROUPS should be universal
220     my $evt = $U->check_perms($e->requestor->id, $org, 'MANAGE_HOLD_GROUPS');
221     return $evt if $evt;
222
223     my $rand_setting = $U->ou_ancestor_setting_value($org, 'holds.subscription.randomize');
224     $randomize = $rand_setting if (defined $rand_setting);
225
226     my $target_field;
227     if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
228     elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
229     elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
230     elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
231     elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
232     elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
233     elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
234     elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
235     else { return undef; }
236
237     # Check for a valid record.
238     # XXX For now, because only title holds are allowed, we'll add only that check.
239     my $target_check = $e->json_query({
240         select => {bre => ['id']},
241         from   => 'bre',
242         where  => {deleted => 'f', id => $target_id}
243     });
244     return {error=>'invalid_target'} if (!@$target_check);
245
246     my $formats_map = delete($$params{holdable_formats_map}) || {};
247
248     my $target_list = $e->search_container_user_bucket_item({bucket => $target_bucket});
249     @$target_list = shuffle(@$target_list) if $randomize;
250
251     # Record the request...
252     $e->xact_begin;
253     my $bhe = Fieldmapper::action::batch_hold_event->new;
254     $bhe->isnew(1);
255     $bhe->staff($e->requestor->id);
256     $bhe->bucket($target_bucket);
257     $bhe->target($target_id);
258     $bhe->hold_type($$params{hold_type});
259     $bhe = $e->create_action_batch_hold_event($bhe) or return $e->die_event;
260     $e->xact_commit;
261
262     my $total = scalar(@$target_list);
263     my $count = 0;
264     $conn->respond({total => $total, count => $count});
265
266     my $triggers = OpenSRF::AppSession->connect('open-ils.trigger');
267     foreach (@$target_list) {
268         $count++;
269         $$params{$target_field} = $target_id;
270         $$params{patronid} = $_->target_user;
271
272         my $usr = $e->retrieve_actor_user([
273             $$params{patronid},
274             {   
275                 flesh => 1,
276                 flesh_fields => {
277                     au => ['settings']
278                 }
279             }
280         ]);
281         my $user_setting_map = {
282             map { $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) }
283                 @{ $usr->settings }
284         };
285
286         $$params{pickup_lib} = $$user_setting_map{'opac.default_pickup_location'} || $usr->home_ou;
287
288         if ($user_setting_map->{'opac.hold_notify'} =~ /email/) {
289             $$params{email_notify} = 1;
290         } else {
291             delete $$params{email_notify};
292         }
293
294         if ($user_setting_map->{'opac.default_phone'} && $user_setting_map->{'opac.hold_notify'} =~ /phone/) {
295             $$params{phone_notify} = $user_setting_map->{'opac.default_phone'};
296         } else {
297             delete $$params{phone_notify};
298         }
299
300         if ($user_setting_map->{'opac.default_sms_carrier'}
301             && $user_setting_map->{'opac.default_sms_notify'}
302             && $user_setting_map->{'opac.hold_notify'} =~ /sms/) {
303             $$params{sms_carrier} = $user_setting_map->{'opac.default_sms_carrier'};
304             $$params{sms_notify} = $user_setting_map->{'opac.default_sms_notify'};
305         } else {
306             delete $$params{sms_carrier};
307             delete $$params{sms_notify};
308         }
309
310         # copy the requested formats from the target->formats map
311         # into the top-level formats attr for each hold ... empty for now, T holds only
312         $$params{holdable_formats} = $formats_map->{$_};
313
314         my $res;
315         ($res) = $self->method_lookup(
316             'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
317         if ($res->{'success'} == 1) {
318
319             $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
320
321             # Remove oargs from params so holds can be created.
322             if ($$params{oargs}) {
323                 delete $$params{oargs};
324             }
325
326             my $ahr = construct_hold_request_object($params);
327             my ($res2) = $self->method_lookup(
328                 $override
329                 ? 'open-ils.circ.holds.create.override'
330                 : 'open-ils.circ.holds.create'
331             )->run($auth, $ahr, $oargs);
332             $res2 = {
333                 total => $total, count => $count,
334                 'patronid' => $$params{patronid},
335                 'target' => $$params{$target_field},
336                 'result' => $res2
337             };
338             $conn->respond($res2);
339
340             unless (ref($res2->{result})) { # success returns a hold id only
341                 $e->xact_begin;
342                 my $bhem = Fieldmapper::action::batch_hold_event_map->new;
343                 $bhem->isnew(1);
344                 $bhem->batch_hold_event($bhe->id);
345                 $bhem->hold($res2->{result});
346                 $e->create_action_batch_hold_event_map($bhem) or return $e->die_event;
347                 $e->xact_commit;
348
349                 my $hold = $e->retrieve_action_hold_request($bhem->hold);
350                 $triggers->request('open-ils.trigger.event.autocreate', 'hold_request.success', $hold, $hold->pickup_lib);
351             }
352
353         } else {
354             $res = {
355                 total => $total, count => $count,
356                 'target' => $$params{$target_field},
357                 'failedpatronid' => $$params{patronid},
358                 'result' => $res
359             };
360             $conn->respond($res);
361         }
362     }
363     $triggers->kill_me;
364     return undef;
365 }
366
367 __PACKAGE__->register_method(
368     method    => "rollback_batch_hold_event",
369     api_name  => "open-ils.circ.holds.rollback.subscription_batch",
370     stream => 1,
371     signature => {
372         desc => q/This is for batch creating a set of holds where every field is identical except for the target users./,
373         params => [
374             { desc => 'Authentication token', type => 'string' },
375             { desc => 'Hold Group Event ID to roll back', type => 'number' },
376         ],
377         return => {
378             desc => 'Stream of objects structured as {total=>X, count=>Y} on success, event on error',
379         },
380     }
381 );
382
383 sub rollback_batch_hold_event {
384     my( $self, $conn, $auth, $event_id ) = @_;
385
386     my $e = new_editor(authtoken=>$auth,xact=>1);
387     return $e->die_event unless $e->checkauth;
388
389     my $org = $e->requestor->ws_ou || $e->requestor->home_ou;
390     my $evt = $U->check_perms($e->requestor->id, $org, 'MANAGE_HOLD_GROUPS');
391     return $evt if $evt;
392
393     my $batch_event = $e->retrieve_action_batch_hold_event($event_id);
394     my $target_list = $e->search_action_batch_hold_event_map({batch_hold_event => $event_id});
395
396     my $total = scalar(@$target_list);
397     my $count = 0;
398     $conn->respond({total => $total, count => $count});
399
400     for my $target (@$target_list) {
401         $count++;
402         $self->method_lookup('open-ils.circ.hold.cancel')->run($auth, $target->hold, 8);
403         $conn->respond({ total => $total, count => $count });
404     }
405
406     $batch_event->cancelled('now');
407     $e->update_action_batch_hold_event($batch_event);
408     $e->commit;
409     return undef;
410 }
411
412 sub construct_hold_request_object {
413     my ($params) = @_;
414
415     my $ahr = Fieldmapper::action::hold_request->new;
416     $ahr->isnew('1');
417
418     foreach my $field (keys %{ $params }) {
419         if ($field eq 'depth') { $ahr->selection_depth($$params{$field}); }
420         elsif ($field eq 'patronid') {
421             $ahr->usr($$params{$field}); }
422         elsif ($field eq 'titleid') { $ahr->target($$params{$field}); }
423         elsif ($field eq 'copy_id') { $ahr->target($$params{$field}); }
424         elsif ($field eq 'issuanceid') { $ahr->target($$params{$field}); }
425         elsif ($field eq 'volume_id') { $ahr->target($$params{$field}); }
426         elsif ($field eq 'mrid') { $ahr->target($$params{$field}); }
427         elsif ($field eq 'partid') { $ahr->target($$params{$field}); }
428         else {
429             $ahr->$field($$params{$field});
430         }
431     }
432     return $ahr;
433 }
434
435 __PACKAGE__->register_method(
436     method    => "create_hold_batch",
437     api_name  => "open-ils.circ.holds.create.batch",
438     stream => 1,
439     signature => {
440         desc => q/@see open-ils.circ.holds.create.batch/,
441         params => [
442             { desc => 'Authentication token', type => 'string' },
443             { desc => 'Array of hold objects', type => 'array' }
444         ],
445         return => {
446             desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
447         },
448     }
449 );
450
451 __PACKAGE__->register_method(
452     method    => "create_hold_batch",
453     api_name  => "open-ils.circ.holds.create.override.batch",
454     stream => 1,
455     signature => {
456         desc  => '@see open-ils.circ.holds.create.batch',
457     }
458 );
459
460
461 sub create_hold_batch {
462     my( $self, $conn, $auth, $hold_list, $oargs ) = @_;
463     (my $method = $self->api_name) =~ s/\.batch//og;
464     foreach (@$hold_list) {
465         my ($res) = $self->method_lookup($method)->run($auth, $_, $oargs);
466         $conn->respond($res);
467     }
468     return undef;
469 }
470
471
472 __PACKAGE__->register_method(
473     method    => "create_hold",
474     api_name  => "open-ils.circ.holds.create",
475     signature => {
476         desc => "Create a new hold for an item.  From a permissions perspective, " .
477                 "the login session is used as the 'requestor' of the hold.  "      .
478                 "The hold recipient is determined by the 'usr' setting within the hold object. " .
479                 'First we verify the requestor has holds request permissions.  '         .
480                 'Then we verify that the recipient is allowed to make the given hold.  ' .
481                 'If not, we see if the requestor has "override" capabilities.  If not, ' .
482                 'a permission exception is returned.  If permissions allow, we cycle '   .
483                 'through the set of holds objects and create.  '                         .
484                 'If the recipient does not have permission to place multiple holds '     .
485                 'on a single title and said operation is attempted, a permission '       .
486                 'exception is returned',
487         params => [
488             { desc => 'Authentication token',               type => 'string' },
489             { desc => 'Hold object for hold to be created',
490                 type => 'object', class => 'ahr' }
491         ],
492         return => {
493             desc => 'New ahr ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
494         },
495     }
496 );
497
498 __PACKAGE__->register_method(
499     method    => "create_hold",
500     api_name  => "open-ils.circ.holds.create.override",
501     notes     => '@see open-ils.circ.holds.create',
502     signature => {
503         desc  => "If the recipient is not allowed to receive the requested hold, " .
504                  "call this method to attempt the override",
505         params => [
506             { desc => 'Authentication token',               type => 'string' },
507             {
508                 desc => 'Hold object for hold to be created',
509                 type => 'object', class => 'ahr'
510             }
511         ],
512         return => {
513             desc => 'New hold (ahr) ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
514         },
515     }
516 );
517
518 sub create_hold {
519     my( $self, $conn, $auth, $hold, $oargs ) = @_;
520     return -1 unless $hold;
521     my $e = new_editor(authtoken=>$auth, xact=>1);
522     return $e->die_event unless $e->checkauth;
523
524     my $override = 0;
525     if ($self->api_name =~ /override/) {
526         $override = 1;
527         $oargs = { all => 1 } unless defined $oargs;
528     }
529
530     my @events;
531
532     my $requestor = $e->requestor;
533     my $recipient = $requestor;
534
535     if( $requestor->id ne $hold->usr ) {
536         # Make sure the requestor is allowed to place holds for
537         # the recipient if they are not the same people
538         $recipient = $e->retrieve_actor_user($hold->usr)  or return $e->die_event;
539         $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->die_event;
540     }
541
542     # If the related org setting tells us to, block if patron privs have expired
543     my $expire_setting = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_BLOCK_HOLD_FOR_EXPIRED_PATRON);
544     if ($expire_setting) {
545         my $expire = DateTime::Format::ISO8601->new->parse_datetime(
546             clean_ISO8601($recipient->expire_date));
547
548         push( @events, OpenILS::Event->new(
549             'PATRON_ACCOUNT_EXPIRED',
550             "payload" => {"fail_part" => "actor.usr.privs_expired"}
551             )) if( CORE::time > $expire->epoch ) ;
552     }
553
554     # Now make sure the recipient is allowed to receive the specified hold
555     my $porg = $recipient->home_ou;
556     my $rid  = $e->requestor->id;
557     my $t    = $hold->hold_type;
558
559     # See if a duplicate hold already exists
560     my $sargs = {
561         usr              => $recipient->id,
562         hold_type        => $t,
563         fulfillment_time => undef,
564         target           => $hold->target,
565         cancel_time      => undef,
566     };
567
568     $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
569
570     my $existing = $e->search_action_hold_request($sargs);
571     if (@$existing) {
572         # See if the requestor has the CREATE_DUPLICATE_HOLDS perm.
573         my $can_dup = $e->allowed('CREATE_DUPLICATE_HOLDS', $recipient->home_ou);
574         # How many are allowed.
575         my $num_dups = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_MAX_DUPLICATE_HOLDS, $e) || 0;
576         push( @events, OpenILS::Event->new('HOLD_EXISTS'))
577             unless (($t eq 'T' || $t eq 'M') && $can_dup && scalar(@$existing) < $num_dups);
578         # Note: We check for @$existing < $num_dups because we're adding a hold with this call.
579     }
580
581     my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
582     push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
583
584     if ( $t eq OILS_HOLD_TYPE_METARECORD ) {
585         return $e->die_event unless $e->allowed('MR_HOLDS',     $porg);
586     } elsif ( $t eq OILS_HOLD_TYPE_TITLE ) {
587         return $e->die_event unless $e->allowed('TITLE_HOLDS',  $porg);
588     } elsif ( $t eq OILS_HOLD_TYPE_VOLUME ) {
589         return $e->die_event unless $e->allowed('VOLUME_HOLDS', $porg);
590     } elsif ( $t eq OILS_HOLD_TYPE_MONOPART ) {
591         return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
592     } elsif ( $t eq OILS_HOLD_TYPE_ISSUANCE ) {
593         return $e->die_event unless $e->allowed('ISSUANCE_HOLDS', $porg);
594     } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
595         return $e->die_event unless $e->allowed('COPY_HOLDS',   $porg);
596     } elsif ( $t eq OILS_HOLD_TYPE_FORCE || $t eq OILS_HOLD_TYPE_RECALL ) {
597         my $copy = $e->retrieve_asset_copy($hold->target)
598             or return $e->die_event;
599         if ( $t eq OILS_HOLD_TYPE_FORCE ) {
600             return $e->die_event unless $e->allowed('COPY_HOLDS_FORCE',   $copy->circ_lib);
601         } elsif ( $t eq OILS_HOLD_TYPE_RECALL ) {
602             return $e->die_event unless $e->allowed('COPY_HOLDS_RECALL',   $copy->circ_lib);
603         }
604     }
605
606     if( @events ) {
607         if (!$override) {
608             $e->rollback;
609             return \@events;
610         }
611         for my $evt (@events) {
612             next unless $evt;
613             my $name = $evt->{textcode};
614             if($oargs->{all} || grep { $_ eq $name } @{$oargs->{events}}) {
615                 return $e->die_event unless $e->allowed("$name.override", $porg);
616             } else {
617                 $e->rollback;
618                 return \@events;
619             }
620         }
621     }
622
623         # Check for hold expiration in the past, and set it to empty string.
624         $hold->expire_time(undef) if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1);
625
626     # set the configured expire time
627     unless($hold->expire_time || $U->is_true($hold->frozen)) {
628         $hold->expire_time(calculate_expire_time($recipient->home_ou));
629     }
630
631
632     # if behind-the-desk pickup is supported at the hold pickup lib,
633     # set the value to the patron default, unless a value has already
634     # been applied.  If it's not supported, force the value to false.
635
636     my $bdous = $U->ou_ancestor_setting_value(
637         $hold->pickup_lib, 
638         'circ.holds.behind_desk_pickup_supported', $e);
639
640     if ($bdous) {
641         if (!defined $hold->behind_desk) {
642
643             my $set = $e->search_actor_user_setting({
644                 usr => $hold->usr, 
645                 name => 'circ.holds_behind_desk'
646             })->[0];
647         
648             $hold->behind_desk('t') if $set and 
649                 OpenSRF::Utils::JSON->JSON2perl($set->value);
650         }
651     } else {
652         # behind the desk not supported, force it to false
653         $hold->behind_desk('f');
654     }
655
656     $hold->requestor($e->requestor->id);
657     $hold->request_lib($e->requestor->ws_ou);
658     $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
659     $hold = $e->create_action_hold_request($hold) or return $e->die_event;
660
661     $e->commit;
662
663     $conn->respond_complete($hold->id);
664
665     $U->simplereq('open-ils.hold-targeter',
666         'open-ils.hold-targeter.target', {hold => $hold->id}
667     ) unless $U->is_true($hold->frozen);
668
669     return undef;
670 }
671
672 # makes sure that a user has permission to place the type of requested hold
673 # returns the Perm exception if not allowed, returns undef if all is well
674 sub _check_holds_perm {
675     my($type, $user_id, $org_id) = @_;
676
677     my $evt;
678     if ($type eq "M") {
679         $evt = $apputils->check_perms($user_id, $org_id, "MR_HOLDS"    );
680     } elsif ($type eq "T") {
681         $evt = $apputils->check_perms($user_id, $org_id, "TITLE_HOLDS" );
682     } elsif($type eq "V") {
683         $evt = $apputils->check_perms($user_id, $org_id, "VOLUME_HOLDS");
684     } elsif($type eq "C") {
685         $evt = $apputils->check_perms($user_id, $org_id, "COPY_HOLDS"  );
686     }
687
688     return $evt if $evt;
689     return undef;
690 }
691
692 # tests if the given user is allowed to place holds on another's behalf
693 sub _check_request_holds_perm {
694     my $user_id = shift;
695     my $org_id  = shift;
696     if (my $evt = $apputils->check_perms(
697         $user_id, $org_id, "REQUEST_HOLDS")) {
698         return $evt;
699     }
700 }
701
702 my $ses_is_req_note = 'The login session is the requestor.  If the requestor is different from the user, ' .
703                       'then the requestor must have VIEW_HOLD permissions';
704
705 __PACKAGE__->register_method(
706     method    => "retrieve_holds_by_id",
707     api_name  => "open-ils.circ.holds.retrieve_by_id",
708     signature => {
709         desc   => "Retrieve the hold, with hold transits attached, for the specified ID.  $ses_is_req_note",
710         params => [
711             { desc => 'Authentication token', type => 'string' },
712             { desc => 'Hold ID',              type => 'number' }
713         ],
714         return => {
715             desc => 'Hold object with transits attached, event on error',
716         }
717     }
718 );
719
720
721 sub retrieve_holds_by_id {
722     my($self, $client, $auth, $hold_id) = @_;
723     my $e = new_editor(authtoken=>$auth);
724     $e->checkauth or return $e->event;
725     $e->allowed('VIEW_HOLD') or return $e->event;
726
727     my $holds = $e->search_action_hold_request(
728         [
729             { id =>  $hold_id , fulfillment_time => undef },
730             {
731                 order_by => { ahr => "request_time" },
732                 flesh => 1,
733                 flesh_fields => {ahr => ['notes']}
734             }
735         ]
736     );
737
738     flesh_hold_transits($holds);
739     flesh_hold_notices($holds, $e);
740     return $holds;
741 }
742
743
744 __PACKAGE__->register_method(
745     method    => "retrieve_holds",
746     api_name  => "open-ils.circ.holds.retrieve",
747     signature => {
748         desc   => "Retrieves all the holds, with hold transits attached, for the specified user.  $ses_is_req_note",
749         params => [
750             { desc => 'Authentication token', type => 'string'  },
751             { desc => 'User ID',              type => 'integer' },
752             { desc => 'Available Only',       type => 'boolean' }
753         ],
754         return => {
755             desc => 'list of holds, event on error',
756         }
757    }
758 );
759
760 __PACKAGE__->register_method(
761     method        => "retrieve_holds",
762     api_name      => "open-ils.circ.holds.id_list.retrieve",
763     authoritative => 1,
764     signature     => {
765         desc   => "Retrieves all the hold IDs, for the specified user.  $ses_is_req_note",
766         params => [
767             { desc => 'Authentication token', type => 'string'  },
768             { desc => 'User ID',              type => 'integer' },
769             { desc => 'Available Only',       type => 'boolean' }
770         ],
771         return => {
772             desc => 'list of holds, event on error',
773         }
774    }
775 );
776
777 __PACKAGE__->register_method(
778     method        => "retrieve_holds",
779     api_name      => "open-ils.circ.holds.canceled.retrieve",
780     authoritative => 1,
781     signature     => {
782         desc   => "Retrieves all the cancelled holds for the specified user.  $ses_is_req_note",
783         params => [
784             { desc => 'Authentication token', type => 'string'  },
785             { desc => 'User ID',              type => 'integer' }
786         ],
787         return => {
788             desc => 'list of holds, event on error',
789         }
790    }
791 );
792
793 __PACKAGE__->register_method(
794     method        => "retrieve_holds",
795     api_name      => "open-ils.circ.holds.canceled.id_list.retrieve",
796     authoritative => 1,
797     signature     => {
798         desc   => "Retrieves list of cancelled hold IDs for the specified user.  $ses_is_req_note",
799         params => [
800             { desc => 'Authentication token', type => 'string'  },
801             { desc => 'User ID',              type => 'integer' }
802         ],
803         return => {
804             desc => 'list of hold IDs, event on error',
805         }
806    }
807 );
808
809
810 sub retrieve_holds {
811     my ($self, $client, $auth, $user_id, $available) = @_;
812
813     my $e = new_editor(authtoken=>$auth);
814     return $e->event unless $e->checkauth;
815     $user_id = $e->requestor->id unless defined $user_id;
816
817     my $notes_filter = {staff => 'f'};
818     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
819     unless($user_id == $e->requestor->id) {
820         if($e->allowed('VIEW_HOLD', $user->home_ou)) {
821             $notes_filter = {staff => 't'}
822         } else {
823             my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
824                 $e, $user_id, $e->requestor->id, 'hold.view');
825             return $e->event unless $allowed;
826         }
827     } else {
828         # staff member looking at his/her own holds can see staff and non-staff notes
829         $notes_filter = {} if $e->allowed('VIEW_HOLD', $user->home_ou);
830     }
831
832     my $holds_query = {
833         select => {ahr => ['id']},
834         from => 'ahr',
835         where => {usr => $user_id, fulfillment_time => undef}
836     };
837
838     if($self->api_name =~ /canceled/) {
839
840         # Fetch the canceled holds
841         # order cancelled holds by cancel time, most recent first
842
843         $holds_query->{order_by} = [{class => 'ahr', field => 'cancel_time', direction => 'desc'}];
844
845         my $cancel_age;
846         my $cancel_count = $U->ou_ancestor_setting_value(
847                 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
848
849         unless($cancel_count) {
850             $cancel_age = $U->ou_ancestor_setting_value(
851                 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
852
853             # if no settings are defined, default to last 10 cancelled holds
854             $cancel_count = 10 unless $cancel_age;
855         }
856
857         if($cancel_count) { # limit by count
858
859             $holds_query->{where}->{cancel_time} = {'!=' => undef};
860             $holds_query->{limit} = $cancel_count;
861
862         } elsif($cancel_age) { # limit by age
863
864             # find all of the canceled holds that were canceled within the configured time frame
865             my $date = DateTime->now->subtract(seconds => OpenILS::Utils::DateTime->interval_to_seconds($cancel_age));
866             $date = $U->epoch2ISO8601($date->epoch);
867             $holds_query->{where}->{cancel_time} = {'>=' => $date};
868         }
869
870     } else {
871
872         # order non-cancelled holds by ready-for-pickup, then active, followed by suspended
873         # "compare" sorts false values to the front.  testing pickup_lib != current_shelf_lib
874         # will sort by pl = csl > pl != csl > followed by csl is null;
875         $holds_query->{order_by} = [
876             {   class => 'ahr',
877                 field => 'pickup_lib',
878                 compare => {'!='  => {'+ahr' => 'current_shelf_lib'}}},
879             {class => 'ahr', field => 'shelf_time'},
880             {class => 'ahr', field => 'frozen'},
881             {class => 'ahr', field => 'request_time'}
882
883         ];
884         $holds_query->{where}->{cancel_time} = undef;
885         if($available) {
886             $holds_query->{where}->{shelf_time} = {'!=' => undef};
887             # Maybe?
888             $holds_query->{where}->{pickup_lib} = {'=' => {'+ahr' => 'current_shelf_lib'}};
889         }
890     }
891
892     my $hold_ids = $e->json_query($holds_query);
893     $hold_ids = [ map { $_->{id} } @$hold_ids ];
894
895     return $hold_ids if $self->api_name =~ /id_list/;
896
897     my @holds;
898     for my $hold_id ( @$hold_ids ) {
899
900         my $hold = $e->retrieve_action_hold_request($hold_id);
901         $hold->notes($e->search_action_hold_request_note({hold => $hold_id, %$notes_filter}));
902
903         $hold->transit(
904             $e->search_action_hold_transit_copy([
905                 {hold => $hold->id, cancel_time => undef},
906                 {order_by => {ahtc => 'source_send_time desc'}, limit => 1}])->[0]
907         );
908
909         push(@holds, $hold);
910     }
911
912     return \@holds;
913 }
914
915
916 __PACKAGE__->register_method(
917     method   => 'user_hold_count',
918     api_name => 'open-ils.circ.hold.user.count'
919 );
920
921 sub user_hold_count {
922     my ( $self, $conn, $auth, $userid ) = @_;
923     my $e = new_editor( authtoken => $auth );
924     return $e->event unless $e->checkauth;
925     my $patron = $e->retrieve_actor_user($userid)
926         or return $e->event;
927     return $e->event unless $e->allowed( 'VIEW_HOLD', $patron->home_ou );
928     return __user_hold_count( $self, $e, $userid );
929 }
930
931 sub __user_hold_count {
932     my ( $self, $e, $userid ) = @_;
933     my $holds = $e->search_action_hold_request(
934         {
935             usr              => $userid,
936             fulfillment_time => undef,
937             cancel_time      => undef,
938         },
939         { idlist => 1 }
940     );
941
942     return scalar(@$holds);
943 }
944
945
946 __PACKAGE__->register_method(
947     method   => "retrieve_holds_by_pickup_lib",
948     api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
949     notes    =>
950       "Retrieves all the holds, with hold transits attached, for the specified pickup_ou id."
951 );
952
953 __PACKAGE__->register_method(
954     method   => "retrieve_holds_by_pickup_lib",
955     api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
956     notes    => "Retrieves all the hold ids for the specified pickup_ou id. "
957 );
958
959 sub retrieve_holds_by_pickup_lib {
960     my ($self, $client, $login_session, $ou_id) = @_;
961
962     #FIXME -- put an appropriate permission check here
963     #my( $user, $target, $evt ) = $apputils->checkses_requestor(
964     #    $login_session, $user_id, 'VIEW_HOLD' );
965     #return $evt if $evt;
966
967     my $holds = $apputils->simplereq(
968         'open-ils.cstore',
969         "open-ils.cstore.direct.action.hold_request.search.atomic",
970         {
971             pickup_lib =>  $ou_id ,
972             fulfillment_time => undef,
973             cancel_time => undef
974         },
975         { order_by => { ahr => "request_time" } }
976     );
977
978     if ( ! $self->api_name =~ /id_list/ ) {
979         flesh_hold_transits($holds);
980         return $holds;
981     }
982     # else id_list
983     return [ map { $_->id } @$holds ];
984 }
985
986
987 __PACKAGE__->register_method(
988     method   => "uncancel_hold",
989     api_name => "open-ils.circ.hold.uncancel"
990 );
991
992 sub uncancel_hold {
993     my($self, $client, $auth, $hold_id) = @_;
994     my $e = new_editor(authtoken=>$auth, xact=>1);
995     return $e->die_event unless $e->checkauth;
996
997     my $hold = $e->retrieve_action_hold_request($hold_id)
998         or return $e->die_event;
999     return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
1000
1001     if ($hold->fulfillment_time) {
1002         $e->rollback;
1003         return 0;
1004     }
1005     unless ($hold->cancel_time) {
1006         $e->rollback;
1007         return 1;
1008     }
1009
1010     # if configured to reset the request time, also reset the expire time
1011     if($U->ou_ancestor_setting_value(
1012         $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
1013
1014         $hold->request_time('now');
1015         $hold->expire_time(calculate_expire_time($hold->request_lib));
1016     }
1017
1018     $hold->clear_cancel_time;
1019     $hold->clear_cancel_cause;
1020     $hold->clear_cancel_note;
1021     $hold->clear_shelf_time;
1022     $hold->clear_current_copy;
1023     $hold->clear_capture_time;
1024     $hold->clear_prev_check_time;
1025     $hold->clear_shelf_expire_time;
1026     $hold->clear_current_shelf_lib;
1027
1028     $e->update_action_hold_request($hold) or return $e->die_event;
1029     $e->commit;
1030
1031     $U->simplereq('open-ils.hold-targeter',
1032         'open-ils.hold-targeter.target', {hold => $hold_id});
1033
1034     return 1;
1035 }
1036
1037
1038 __PACKAGE__->register_method(
1039     method    => "cancel_hold",
1040     api_name  => "open-ils.circ.hold.cancel",
1041     signature => {
1042         desc   => 'Cancels the specified hold.  The login session is the requestor.  If the requestor is different from the usr field ' .
1043                   'on the hold, the requestor must have CANCEL_HOLDS permissions. The hold may be either the hold object or the hold id',
1044         param  => [
1045             {desc => 'Authentication token',  type => 'string'},
1046             {desc => 'Hold ID',               type => 'number'},
1047             {desc => 'Cause of Cancellation', type => 'string'},
1048             {desc => 'Note',                  type => 'string'}
1049         ],
1050         return => {
1051             desc => '1 on success, event on error'
1052         }
1053     }
1054 );
1055
1056 sub cancel_hold {
1057     my($self, $client, $auth, $holdid, $cause, $note) = @_;
1058
1059     my $e = new_editor(authtoken=>$auth, xact=>1);
1060     return $e->die_event unless $e->checkauth;
1061
1062     my $hold = $e->retrieve_action_hold_request($holdid)
1063         or return $e->die_event;
1064
1065     if( $e->requestor->id ne $hold->usr ) {
1066         return $e->die_event unless $e->allowed('CANCEL_HOLDS');
1067     }
1068
1069     if ($hold->cancel_time) {
1070         $e->rollback;
1071         return 1;
1072     }
1073
1074     # If the hold is captured, reset the copy status
1075     if( $hold->capture_time and $hold->current_copy ) {
1076
1077         my $copy = $e->retrieve_asset_copy($hold->current_copy)
1078             or return $e->die_event;
1079
1080         if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1081          $logger->info("canceling hold $holdid whose item is on the holds shelf");
1082 #            $logger->info("setting copy to status 'reshelving' on hold cancel");
1083 #            $copy->status(OILS_COPY_STATUS_RESHELVING);
1084 #            $copy->editor($e->requestor->id);
1085 #            $copy->edit_date('now');
1086 #            $e->update_asset_copy($copy) or return $e->event;
1087
1088         } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1089
1090             my $hid = $hold->id;
1091             $logger->warn("! canceling hold [$hid] that is in transit");
1092             my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
1093
1094             if( $transid ) {
1095                 my $trans = $e->retrieve_action_transit_copy($transid);
1096                 # Leave the transit alive, but  set the copy status to
1097                 # reshelving so it will be properly reshelved when it gets back home
1098                 if( $trans ) {
1099                     $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
1100                     $e->update_action_transit_copy($trans) or return $e->die_event;
1101                 }
1102             }
1103         }
1104     }
1105
1106     $hold->cancel_time('now');
1107     $hold->cancel_cause($cause);
1108     $hold->cancel_note($note);
1109     $e->update_action_hold_request($hold)
1110         or return $e->die_event;
1111
1112     $e->commit;
1113
1114     # re-fetch the hold to pick up the real cancel_time (not "now") for A/T
1115     $e->xact_begin;
1116     $hold = $e->retrieve_action_hold_request($hold->id) or return $e->die_event;
1117     $e->rollback;
1118
1119     if ($e->requestor->id == $hold->usr) {
1120         $U->create_events_for_hook('hold_request.cancel.patron', $hold, $hold->pickup_lib);
1121     } else {
1122         $U->create_events_for_hook('hold_request.cancel.staff', $hold, $hold->pickup_lib);
1123     }
1124
1125     return 1;
1126 }
1127
1128 my $update_hold_desc = 'The login session is the requestor. '       .
1129    'If the requestor is different from the usr field on the hold, ' .
1130    'the requestor must have UPDATE_HOLDS permissions. '             .
1131    'If supplying a hash of hold data, "id" must be included. '      .
1132    'The hash is ignored if a hold object is supplied, '             .
1133    'so you should supply only one kind of hold data argument.'      ;
1134
1135 __PACKAGE__->register_method(
1136     method    => "update_hold",
1137     api_name  => "open-ils.circ.hold.update",
1138     signature => {
1139         desc   => "Updates the specified hold.  $update_hold_desc",
1140         params => [
1141             {desc => 'Authentication token',         type => 'string'},
1142             {desc => 'Hold Object',                  type => 'object'},
1143             {desc => 'Hash of values to be applied', type => 'object'}
1144         ],
1145         return => {
1146             desc => 'Hold ID on success, event on error',
1147             # type => 'number'
1148         }
1149     }
1150 );
1151
1152 __PACKAGE__->register_method(
1153     method    => "batch_update_hold",
1154     api_name  => "open-ils.circ.hold.update.batch",
1155     stream    => 1,
1156     signature => {
1157         desc   => "Updates the specified hold(s).  $update_hold_desc",
1158         params => [
1159             {desc => 'Authentication token',                    type => 'string'},
1160             {desc => 'Array of hold obejcts',                   type => 'array' },
1161             {desc => 'Array of hashes of values to be applied', type => 'array' }
1162         ],
1163         return => {
1164             desc => 'Hold ID per success, event per error',
1165         }
1166     }
1167 );
1168
1169 sub update_hold {
1170     my($self, $client, $auth, $hold, $values) = @_;
1171     my $e = new_editor(authtoken=>$auth, xact=>1);
1172     return $e->die_event unless $e->checkauth;
1173     my $resp = update_hold_impl($self, $e, $hold, $values);
1174     if ($U->event_code($resp)) {
1175         $e->rollback;
1176         return $resp;
1177     }
1178     $e->commit;     # FIXME: update_hold_impl already does $e->commit  ??
1179     return $resp;
1180 }
1181
1182 sub batch_update_hold {
1183     my($self, $client, $auth, $hold_list, $values_list) = @_;
1184     my $e = new_editor(authtoken=>$auth);
1185     return $e->die_event unless $e->checkauth;
1186
1187     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.
1188     $hold_list   ||= [];
1189     $values_list ||= [];      # FIXME: either move this above $count declaration, or send an event if both lists undef.  Probably the latter.
1190
1191 # FIXME: Failing over to [] guarantees warnings for "Use of unitialized value" in update_hold_impl call.
1192 # FIXME: We should be sure we only call update_hold_impl with hold object OR hash, not both.
1193
1194     for my $idx (0..$count-1) {
1195         $e->xact_begin;
1196         my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
1197         $e->xact_commit unless $U->event_code($resp);
1198         $client->respond($resp);
1199     }
1200
1201     $e->disconnect;
1202     return undef;       # not in the register return type, assuming we should always have at least one list populated
1203 }
1204
1205 sub update_hold_impl {
1206     my($self, $e, $hold, $values) = @_;
1207     my $hold_status;
1208     my $need_retarget = 0;
1209
1210     unless($hold) {
1211         $hold = $e->retrieve_action_hold_request($values->{id})
1212             or return $e->die_event;
1213         for my $k (keys %$values) {
1214             # Outside of pickup_lib (covered by the first regex) I don't know when these would currently change.
1215             # But hey, why not cover things that may happen later?
1216             if ($k =~ '_(lib|ou)$' || $k eq 'target' || $k eq 'hold_type' || $k eq 'requestor' || $k eq 'selection_depth' || $k eq 'holdable_formats') {
1217                 if (defined $values->{$k} && defined $hold->$k() && $values->{$k} ne $hold->$k()) {
1218                     # Value changed? RETARGET!
1219                     $need_retarget = 1;
1220                 } elsif (defined $hold->$k() != defined $values->{$k}) {
1221                     # Value being set or cleared? RETARGET!
1222                     $need_retarget = 1;
1223                 }
1224             }
1225             if (defined $values->{$k}) {
1226                 $hold->$k($values->{$k});
1227             } else {
1228                 my $f = "clear_$k"; $hold->$f();
1229             }
1230         }
1231     }
1232
1233     my $orig_hold = $e->retrieve_action_hold_request($hold->id)
1234         or return $e->die_event;
1235
1236     # don't allow the user to be changed
1237     return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
1238
1239     if($hold->usr ne $e->requestor->id) {
1240         # if the hold is for a different user, make sure the
1241         # requestor has the appropriate permissions
1242         my $usr = $e->retrieve_actor_user($hold->usr)
1243             or return $e->die_event;
1244         return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1245     }
1246
1247
1248     # --------------------------------------------------------------
1249     # Changing the request time is like playing God
1250     # --------------------------------------------------------------
1251     if($hold->request_time ne $orig_hold->request_time) {
1252         return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
1253         return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
1254     }
1255
1256
1257     # --------------------------------------------------------------
1258     # Code for making sure staff have appropriate permissons for cut_in_line
1259     # This, as is, doesn't prevent a user from cutting their own holds in line
1260     # but needs to
1261     # --------------------------------------------------------------
1262     if($U->is_true($hold->cut_in_line) ne $U->is_true($orig_hold->cut_in_line)) {
1263         return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
1264     }
1265
1266
1267     # --------------------------------------------------------------
1268     # Disallow hold suspencion if the hold is already captured.
1269     # --------------------------------------------------------------
1270     if ($U->is_true($hold->frozen) and not $U->is_true($orig_hold->frozen)) {
1271         $hold_status = _hold_status($e, $hold);
1272         if ($hold_status > 2 && $hold_status != 7) { # hold is captured
1273             $logger->info("bypassing hold freeze on captured hold");
1274             return OpenILS::Event->new('HOLD_SUSPEND_AFTER_CAPTURE');
1275         }
1276     }
1277
1278
1279     # --------------------------------------------------------------
1280     # if the hold is on the holds shelf or in transit and the pickup
1281     # lib changes we need to create a new transit.
1282     # --------------------------------------------------------------
1283     if($orig_hold->pickup_lib ne $hold->pickup_lib) {
1284
1285         $hold_status = _hold_status($e, $hold) unless $hold_status;
1286
1287         if($hold_status == 3) { # in transit
1288
1289             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
1290             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
1291
1292             $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
1293
1294             # update the transit to reflect the new pickup location
1295             my $transit = $e->search_action_hold_transit_copy(
1296                 {hold=>$hold->id, cancel_time => undef, dest_recv_time => undef})->[0]
1297                 or return $e->die_event;
1298
1299             $transit->prev_dest($transit->dest); # mark the previous destination on the transit
1300             $transit->dest($hold->pickup_lib);
1301             $e->update_action_hold_transit_copy($transit) or return $e->die_event;
1302
1303         } elsif($hold_status == 4 or $hold_status == 5 or $hold_status == 8) { # on holds shelf
1304
1305             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
1306             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
1307
1308             $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
1309
1310             if ($hold->pickup_lib eq $orig_hold->current_shelf_lib) {
1311                 # This can happen if the pickup lib is changed while the hold is
1312                 # on the shelf, then changed back to the original pickup lib.
1313                 # Restore the original shelf_expire_time to prevent abuse.
1314                 set_hold_shelf_expire_time(undef, $hold, $e, $hold->shelf_time);
1315
1316             } else {
1317                 # clear to prevent premature shelf expiration
1318                 $hold->clear_shelf_expire_time;
1319             }
1320           # If a copy is targeted and pickup lib changes,
1321           # clear the current_copy so a retarget will re-evaluate
1322           # the hold from scratch.
1323         } elsif ($hold_status == 2) {
1324               $logger->info("Pickup location changed and waiting for capture, clear current_copy for hold ".$hold->id);
1325               $hold->clear_current_copy;
1326         }
1327     }
1328
1329     if($U->is_true($hold->frozen)) {
1330         $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1331         $hold->clear_current_copy;
1332         $hold->clear_prev_check_time;
1333         # Clear expire_time to prevent frozen holds from expiring.
1334         $logger->info("clearing expire_time for frozen hold ".$hold->id);
1335         $hold->clear_expire_time;
1336     }
1337
1338     # If the hold_expire_time is in the past && is not equal to the
1339     # original expire_time, then reset the expire time to be in the
1340     # future.
1341     if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1 && $U->datecmp($hold->expire_time, $orig_hold->expire_time) != 0) {
1342         $hold->expire_time(calculate_expire_time($hold->request_lib));
1343     }
1344
1345     # If the hold is reactivated, reset the expire_time.
1346     if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1347         $logger->info("Reset expire_time on activated hold ".$hold->id);
1348         $hold->expire_time(calculate_expire_time($hold->request_lib));
1349     }
1350
1351     $e->update_action_hold_request($hold) or return $e->die_event;
1352     $e->commit;
1353
1354     if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1355         $logger->info("Running targeter on activated hold ".$hold->id);
1356         $U->simplereq('open-ils.hold-targeter', 
1357             'open-ils.hold-targeter.target', {hold => $hold->id});
1358     }
1359
1360     # a change to mint-condition changes the set of potential copies, so retarget the hold;
1361     if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
1362         _reset_hold($self, $e->requestor, $hold)
1363     } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
1364         $U->simplereq('open-ils.hold-targeter', 
1365             'open-ils.hold-targeter.target', {hold => $hold->id});
1366     }
1367
1368     return $hold->id;
1369 }
1370
1371 # this does not update the hold in the DB.  It only
1372 # sets the shelf_expire_time field on the hold object.
1373 # start_time is optional and defaults to 'now'
1374 sub set_hold_shelf_expire_time {
1375     my ($class, $hold, $editor, $start_time) = @_;
1376
1377     my $shelf_expire = $U->ou_ancestor_setting_value(
1378         $hold->pickup_lib,
1379         'circ.holds.default_shelf_expire_interval',
1380         $editor
1381     );
1382
1383     return undef unless $shelf_expire;
1384
1385     $start_time = ($start_time) ?
1386         DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time)) :
1387         DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
1388
1389     my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($shelf_expire);
1390     my $expire_time = $start_time->add(seconds => $seconds);
1391
1392     # if the shelf expire time overlaps with a pickup lib's
1393     # closed date, push it out to the first open date
1394     my $dateinfo = $U->storagereq(
1395         'open-ils.storage.actor.org_unit.closed_date.overlap',
1396         $hold->pickup_lib, $expire_time->strftime('%FT%T%z'));
1397
1398     if($dateinfo) {
1399         my $dt_parser = DateTime::Format::ISO8601->new;
1400         $expire_time = $dt_parser->parse_datetime(clean_ISO8601($dateinfo->{end}));
1401
1402         # TODO: enable/disable time bump via setting?
1403         $expire_time->set(hour => '23', minute => '59', second => '59');
1404
1405         $logger->info("circulator: shelf_expire_time overlaps".
1406             " with closed date, pushing expire time to $expire_time");
1407     }
1408
1409     $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
1410     return undef;
1411 }
1412
1413
1414 sub transit_hold {
1415     my($e, $orig_hold, $hold, $copy) = @_;
1416     my $src  = $orig_hold->pickup_lib;
1417     my $dest = $hold->pickup_lib;
1418
1419     $logger->info("putting hold into transit on pickup_lib update");
1420
1421     my $transit = Fieldmapper::action::hold_transit_copy->new;
1422     $transit->hold($hold->id);
1423     $transit->source($src);
1424     $transit->dest($dest);
1425     $transit->target_copy($copy->id);
1426     $transit->source_send_time('now');
1427     $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1428
1429     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1430     $copy->editor($e->requestor->id);
1431     $copy->edit_date('now');
1432
1433     $e->create_action_hold_transit_copy($transit) or return $e->die_event;
1434     $e->update_asset_copy($copy) or return $e->die_event;
1435     return undef;
1436 }
1437
1438 # if the hold is frozen, this method ensures that the hold is not "targeted",
1439 # that is, it clears the current_copy and prev_check_time to essentiallly
1440 # reset the hold.  If it is being activated, it runs the targeter in the background
1441 sub update_hold_if_frozen {
1442     my($self, $e, $hold, $orig_hold) = @_;
1443     return if $hold->capture_time;
1444
1445     if($U->is_true($hold->frozen)) {
1446         $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1447         $hold->clear_current_copy;
1448         $hold->clear_prev_check_time;
1449
1450     } else {
1451         if($U->is_true($orig_hold->frozen)) {
1452             $logger->info("Running targeter on activated hold ".$hold->id);
1453             $U->simplereq('open-ils.hold-targeter', 
1454                 'open-ils.hold-targeter.target', {hold => $hold->id});
1455         }
1456     }
1457 }
1458
1459 __PACKAGE__->register_method(
1460     method    => "hold_note_CUD",
1461     api_name  => "open-ils.circ.hold_request.note.cud",
1462     signature => {
1463         desc   => 'Create, update or delete a hold request note.  If the operator (from Auth. token) '
1464                 . 'is not the owner of the hold, the UPDATE_HOLD permission is required',
1465         params => [
1466             { desc => 'Authentication token', type => 'string' },
1467             { desc => 'Hold note object',     type => 'object' }
1468         ],
1469         return => {
1470             desc => 'Returns the note ID, event on error'
1471         },
1472     }
1473 );
1474
1475 sub hold_note_CUD {
1476     my($self, $conn, $auth, $note) = @_;
1477
1478     my $e = new_editor(authtoken => $auth, xact => 1);
1479     return $e->die_event unless $e->checkauth;
1480
1481     my $hold = $e->retrieve_action_hold_request($note->hold)
1482         or return $e->die_event;
1483
1484     if($hold->usr ne $e->requestor->id) {
1485         my $usr = $e->retrieve_actor_user($hold->usr);
1486         return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1487         $note->staff('t') if $note->isnew;
1488     }
1489
1490     if($note->isnew) {
1491         $e->create_action_hold_request_note($note) or return $e->die_event;
1492     } elsif($note->ischanged) {
1493         $e->update_action_hold_request_note($note) or return $e->die_event;
1494     } elsif($note->isdeleted) {
1495         $e->delete_action_hold_request_note($note) or return $e->die_event;
1496     }
1497
1498     $e->commit;
1499     return $note->id;
1500 }
1501
1502
1503 __PACKAGE__->register_method(
1504     method    => "retrieve_hold_status",
1505     api_name  => "open-ils.circ.hold.status.retrieve",
1506     signature => {
1507         desc   => 'Calculates the current status of the hold. The requestor must have '      .
1508                   'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
1509         param  => [
1510             { desc => 'Hold ID', type => 'number' }
1511         ],
1512         return => {
1513             # type => 'number',     # event sometimes
1514             desc => <<'END_OF_DESC'
1515 Returns event on error or:
1516 -1 on error (for now),
1517  1 for 'waiting for copy to become available',
1518  2 for 'waiting for copy capture',
1519  3 for 'in transit',
1520  4 for 'arrived',
1521  5 for 'hold-shelf-delay'
1522  6 for 'canceled'
1523  7 for 'suspended'
1524  8 for 'captured, on wrong hold shelf'
1525  9 for 'fulfilled'
1526 END_OF_DESC
1527         }
1528     }
1529 );
1530
1531 sub retrieve_hold_status {
1532     my($self, $client, $auth, $hold_id) = @_;
1533
1534     my $e = new_editor(authtoken => $auth);
1535     return $e->event unless $e->checkauth;
1536     my $hold = $e->retrieve_action_hold_request($hold_id)
1537         or return $e->event;
1538
1539     if( $e->requestor->id != $hold->usr ) {
1540         return $e->event unless $e->allowed('VIEW_HOLD');
1541     }
1542
1543     return _hold_status($e, $hold);
1544
1545 }
1546
1547 sub _hold_status {
1548     my($e, $hold) = @_;
1549     if ($hold->cancel_time) {
1550         return 6;
1551     }
1552     if ($U->is_true($hold->frozen) && !$hold->capture_time) {
1553         return 7;
1554     }
1555     if ($hold->current_shelf_lib and $hold->current_shelf_lib ne $hold->pickup_lib) {
1556         return 8;
1557     }
1558     if ($hold->fulfillment_time) {
1559         return 9;
1560     }
1561     return 1 unless $hold->current_copy;
1562     return 2 unless $hold->capture_time;
1563
1564     my $copy = $hold->current_copy;
1565     unless( ref $copy ) {
1566         $copy = $e->retrieve_asset_copy($hold->current_copy)
1567             or return $e->event;
1568     }
1569
1570     return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
1571
1572     if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
1573
1574         my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
1575         return 4 unless $hs_wait_interval;
1576
1577         # if a hold_shelf_status_delay interval is defined and start_time plus
1578         # the interval is greater than now, consider the hold to be in the virtual
1579         # "on its way to the holds shelf" status. Return 5.
1580
1581         my $transit    = $e->search_action_hold_transit_copy({
1582                             hold           => $hold->id,
1583                             target_copy    => $copy->id,
1584                             cancel_time     => undef,
1585                             dest_recv_time => {'!=' => undef},
1586                          })->[0];
1587         my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
1588         $start_time    = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time));
1589         my $end_time   = $start_time->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($hs_wait_interval));
1590
1591         return 5 if $end_time > DateTime->now;
1592         return 4;
1593     }
1594
1595     return -1;  # error
1596 }
1597
1598
1599
1600 __PACKAGE__->register_method(
1601     method    => "retrieve_hold_queue_stats",
1602     api_name  => "open-ils.circ.hold.queue_stats.retrieve",
1603     signature => {
1604         desc   => 'Returns summary data about the state of a hold',
1605         params => [
1606             { desc => 'Authentication token',  type => 'string'},
1607             { desc => 'Hold ID', type => 'number'},
1608         ],
1609         return => {
1610             desc => q/Summary object with keys:
1611                 total_holds : total holds in queue
1612                 queue_position : current queue position
1613                 potential_copies : number of potential copies for this hold
1614                 estimated_wait : estimated wait time in days
1615                 status : hold status
1616                      -1 => error or unexpected state,
1617                      1 => 'waiting for copy to become available',
1618                      2 => 'waiting for copy capture',
1619                      3 => 'in transit',
1620                      4 => 'arrived',
1621                      5 => 'hold-shelf-delay'
1622             /,
1623             type => 'object'
1624         }
1625     }
1626 );
1627
1628 sub retrieve_hold_queue_stats {
1629     my($self, $conn, $auth, $hold_id) = @_;
1630     my $e = new_editor(authtoken => $auth);
1631     return $e->event unless $e->checkauth;
1632     my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1633     if($e->requestor->id != $hold->usr) {
1634         return $e->event unless $e->allowed('VIEW_HOLD');
1635     }
1636     return retrieve_hold_queue_status_impl($e, $hold);
1637 }
1638
1639 sub retrieve_hold_queue_status_impl {
1640     my $e = shift;
1641     my $hold = shift;
1642
1643     # The holds queue is defined as the distinct set of holds that share at
1644     # least one potential copy with the context hold, plus any holds that
1645     # share the same hold type and target.  The latter part exists to
1646     # accomodate holds that currently have no potential copies
1647     my $q_holds = $e->json_query({
1648
1649         # fetch cut_in_line and request_time since they're in the order_by
1650         # and we're asking for distinct values
1651         select => {ahr => ['id', 'cut_in_line', 'request_time']},
1652         from   => 'ahr',
1653         where => {
1654             id => { in => {
1655                 select => { ahcm => ['hold'] },
1656                 from   => {
1657                     'ahcm' => {
1658                         'ahcm2' => {
1659                             'class' => 'ahcm',
1660                             'field' => 'target_copy',
1661                             'fkey'  => 'target_copy'
1662                         }
1663                     }
1664                 },
1665                 where => { '+ahcm2' => { hold => $hold->id } },
1666                 distinct => 1
1667             }}
1668         },
1669         order_by => [
1670             {
1671                 "class" => "ahr",
1672                 "field" => "cut_in_line",
1673                 "transform" => "coalesce",
1674                 "params" => [ 0 ],
1675                 "direction" => "desc"
1676             },
1677             { "class" => "ahr", "field" => "request_time" }
1678         ],
1679         distinct => 1
1680     });
1681
1682     if (!@$q_holds) { # none? maybe we don't have a map ...
1683         $q_holds = $e->json_query({
1684             select => {ahr => ['id', 'cut_in_line', 'request_time']},
1685             from   => 'ahr',
1686             order_by => [
1687                 {
1688                     "class" => "ahr",
1689                     "field" => "cut_in_line",
1690                     "transform" => "coalesce",
1691                     "params" => [ 0 ],
1692                     "direction" => "desc"
1693                 },
1694                 { "class" => "ahr", "field" => "request_time" }
1695             ],
1696             where    => {
1697                 hold_type => $hold->hold_type,
1698                 target    => $hold->target,
1699                 capture_time => undef,
1700                 cancel_time => undef,
1701                 '-or' => [
1702                     {expire_time => undef },
1703                     {expire_time => {'>' => 'now'}}
1704                 ]
1705            }
1706         });
1707     }
1708
1709
1710     my $qpos = 1;
1711     for my $h (@$q_holds) {
1712         last if $h->{id} == $hold->id;
1713         $qpos++;
1714     }
1715
1716     my $hold_data = $e->json_query({
1717         select => {
1718             acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
1719             ccm => [ {column =>'avg_wait_time'} ]
1720         },
1721         from => {
1722             ahcm => {
1723                 acp => {
1724                     join => {
1725                         ccm => {type => 'left'}
1726                     }
1727                 }
1728             }
1729         },
1730         where => {'+ahcm' => {hold => $hold->id} }
1731     });
1732
1733     my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1734
1735     my $default_wait = $U->ou_ancestor_setting_value(
1736         $user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL, $e);
1737     my $min_wait = $U->ou_ancestor_setting_value(
1738         $user_org, 'circ.holds.min_estimated_wait_interval', $e);
1739     $min_wait = OpenILS::Utils::DateTime->interval_to_seconds($min_wait || '0 seconds');
1740     $default_wait ||= '0 seconds';
1741
1742     # Estimated wait time is the average wait time across the set
1743     # of potential copies, divided by the number of potential copies
1744     # times the queue position.
1745
1746     my $combined_secs = 0;
1747     my $num_potentials = 0;
1748
1749     for my $wait_data (@$hold_data) {
1750         my $count += $wait_data->{count};
1751         $combined_secs += $count *
1752             OpenILS::Utils::DateTime->interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1753         $num_potentials += $count;
1754     }
1755
1756     my $estimated_wait = -1;
1757
1758     if($num_potentials) {
1759         my $avg_wait = $combined_secs / $num_potentials;
1760         $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1761         $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1762     }
1763
1764     return {
1765         total_holds      => scalar(@$q_holds),
1766         queue_position   => $qpos,
1767         potential_copies => $num_potentials,
1768         status           => _hold_status( $e, $hold ),
1769         estimated_wait   => int($estimated_wait)
1770     };
1771 }
1772
1773
1774 sub fetch_open_hold_by_current_copy {
1775     my $class = shift;
1776     my $copyid = shift;
1777     my $hold = $apputils->simplereq(
1778         'open-ils.cstore',
1779         'open-ils.cstore.direct.action.hold_request.search.atomic',
1780         { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
1781     return $hold->[0] if ref($hold);
1782     return undef;
1783 }
1784
1785 sub fetch_related_holds {
1786     my $class = shift;
1787     my $copyid = shift;
1788     return $apputils->simplereq(
1789         'open-ils.cstore',
1790         'open-ils.cstore.direct.action.hold_request.search.atomic',
1791         { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
1792 }
1793
1794
1795 __PACKAGE__->register_method(
1796     method    => "hold_pull_list",
1797     api_name  => "open-ils.circ.hold_pull_list.retrieve",
1798     signature => {
1799         desc   => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1800                   'The location is determined by the login session.',
1801         params => [
1802             { desc => 'Limit (optional)',  type => 'number'},
1803             { desc => 'Offset (optional)', type => 'number'},
1804         ],
1805         return => {
1806             desc => 'reference to a list of holds, or event on failure',
1807         }
1808     }
1809 );
1810
1811 __PACKAGE__->register_method(
1812     method    => "hold_pull_list",
1813     api_name  => "open-ils.circ.hold_pull_list.id_list.retrieve",
1814     signature => {
1815         desc   => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1816                   'The location is determined by the login session.',
1817         params => [
1818             { desc => 'Limit (optional)',  type => 'number'},
1819             { desc => 'Offset (optional)', type => 'number'},
1820         ],
1821         return => {
1822             desc => 'reference to a list of holds, or event on failure',
1823         }
1824     }
1825 );
1826
1827 __PACKAGE__->register_method(
1828     method    => "hold_pull_list",
1829     api_name  => "open-ils.circ.hold_pull_list.retrieve.count",
1830     signature => {
1831         desc   => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1832                   'The location is determined by the login session.',
1833         params => [
1834             { desc => 'Limit (optional)',  type => 'number'},
1835             { desc => 'Offset (optional)', type => 'number'},
1836         ],
1837         return => {
1838             desc => 'Holds count (integer), or event on failure',
1839             # type => 'number'
1840         }
1841     }
1842 );
1843
1844 __PACKAGE__->register_method(
1845     method    => "hold_pull_list",
1846     stream => 1,
1847     # TODO: tag with api_level 2 once fully supported
1848     api_name  => "open-ils.circ.hold_pull_list.fleshed.stream",
1849     signature => {
1850         desc   => q/Returns a stream of fleshed holds  that need to be 
1851                     "pulled" by a given location.  The location is 
1852                     determined by the login session.  
1853                     This API calls always run in authoritative mode./,
1854         params => [
1855             { desc => 'Limit (optional)',  type => 'number'},
1856             { desc => 'Offset (optional)', type => 'number'},
1857         ],
1858         return => {
1859             desc => 'Stream of holds holds, or event on failure',
1860         }
1861     }
1862 );
1863
1864 sub hold_pull_list {
1865     my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1866     my( $reqr, $evt ) = $U->checkses($authtoken);
1867     return $evt if $evt;
1868
1869     my $org = $reqr->ws_ou || $reqr->home_ou;
1870     # the perm locaiton shouldn't really matter here since holds
1871     # will exist all over and VIEW_HOLDS should be universal
1872     $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1873     return $evt if $evt;
1874
1875     if($self->api_name =~ /count/) {
1876
1877         my $count = $U->storagereq(
1878             'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1879             $org, $limit, $offset );
1880
1881         $logger->info("Grabbing pull list for org unit $org with $count items");
1882         return $count;
1883
1884     } elsif( $self->api_name =~ /id_list/ ) {
1885         $U->storagereq(
1886             'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1887             $org, $limit, $offset );
1888
1889     } elsif ($self->api_name =~ /fleshed/) {
1890
1891         my $ids = $U->storagereq(
1892             'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1893             $org, $limit, $offset );
1894
1895         my $e = new_editor(xact => 1, requestor => $reqr);
1896         $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
1897         $e->rollback;
1898         $conn->respond_complete;
1899         return;
1900
1901     } else {
1902         $U->storagereq(
1903             'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1904             $org, $limit, $offset );
1905     }
1906 }
1907
1908 __PACKAGE__->register_method(
1909     method    => "print_hold_pull_list",
1910     api_name  => "open-ils.circ.hold_pull_list.print",
1911     signature => {
1912         desc   => 'Returns an HTML-formatted holds pull list',
1913         params => [
1914             { desc => 'Authtoken', type => 'string'},
1915             { desc => 'Org unit ID.  Optional, defaults to workstation org unit', type => 'number'},
1916         ],
1917         return => {
1918             desc => 'HTML string',
1919             type => 'string'
1920         }
1921     }
1922 );
1923
1924 sub print_hold_pull_list {
1925     my($self, $client, $auth, $org_id) = @_;
1926
1927     my $e = new_editor(authtoken=>$auth);
1928     return $e->event unless $e->checkauth;
1929
1930     $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
1931     return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1932
1933     my $hold_ids = $U->storagereq(
1934         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1935         $org_id, 10000);
1936
1937     return undef unless @$hold_ids;
1938
1939     $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1940
1941     # Holds will /NOT/ be in order after this ...
1942     my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
1943     $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1944
1945     # ... so we must resort.
1946     my $hold_map = +{map { $_->id => $_ } @$holds};
1947     my $sorted_holds = [];
1948     push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
1949
1950     return $U->fire_object_event(
1951         undef, "ahr.format.pull_list", $sorted_holds,
1952         $org_id, undef, undef, $client
1953     );
1954
1955 }
1956
1957 __PACKAGE__->register_method(
1958     method    => "print_hold_pull_list_stream",
1959     stream   => 1,
1960     api_name  => "open-ils.circ.hold_pull_list.print.stream",
1961     signature => {
1962         desc   => 'Returns a stream of fleshed holds',
1963         params => [
1964             { desc => 'Authtoken', type => 'string'},
1965             { desc => 'Hash of optional param: Org unit ID (defaults to workstation org unit), limit, offset, sort (array of: acplo.position, prefix, call_number, suffix, request_time)',
1966               type => 'object'
1967             },
1968         ],
1969         return => {
1970             desc => 'A stream of fleshed holds',
1971             type => 'object'
1972         }
1973     }
1974 );
1975
1976 sub print_hold_pull_list_stream {
1977     my($self, $client, $auth, $params) = @_;
1978
1979     my $e = new_editor(authtoken=>$auth);
1980     return $e->die_event unless $e->checkauth;
1981
1982     delete($$params{org_id}) unless (int($$params{org_id}));
1983     delete($$params{limit}) unless (int($$params{limit}));
1984     delete($$params{offset}) unless (int($$params{offset}));
1985     delete($$params{chunk_size}) unless (int($$params{chunk_size}));
1986     delete($$params{chunk_size}) if  ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
1987     $$params{chunk_size} ||= 10;
1988     $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
1989
1990     $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
1991     return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
1992
1993     my $sort = [];
1994     if ($$params{sort} && @{ $$params{sort} }) {
1995         for my $s (@{ $$params{sort} }) {
1996             if ($s eq 'acplo.position') {
1997                 push @$sort, {
1998                     "class" => "acplo", "field" => "position",
1999                     "transform" => "coalesce", "params" => [999]
2000                 };
2001             } elsif ($s eq 'prefix') {
2002                 push @$sort, {"class" => "acnp", "field" => "label_sortkey"};
2003             } elsif ($s eq 'call_number') {
2004                 push @$sort, {"class" => "acn", "field" => "label_sortkey"};
2005             } elsif ($s eq 'suffix') {
2006                 push @$sort, {"class" => "acns", "field" => "label_sortkey"};
2007             } elsif ($s eq 'request_time') {
2008                 push @$sort, {"class" => "ahr", "field" => "request_time"};
2009             }
2010         }
2011     } else {
2012         push @$sort, {"class" => "ahr", "field" => "request_time"};
2013     }
2014
2015     my $holds_ids = $e->json_query(
2016         {
2017             "select" => {"ahr" => ["id"]},
2018             "from" => {
2019                 "ahr" => {
2020                     "acp" => {
2021                         "field" => "id",
2022                         "fkey" => "current_copy",
2023                         "filter" => {
2024                             "circ_lib" => $$params{org_id}, "status" => [0,7]
2025                         },
2026                         "join" => {
2027                             "acn" => {
2028                                 "field" => "id",
2029                                 "fkey" => "call_number",
2030                                 "join" => {
2031                                     "acnp" => {
2032                                         "field" => "id",
2033                                         "fkey" => "prefix"
2034                                     },
2035                                     "acns" => {
2036                                         "field" => "id",
2037                                         "fkey" => "suffix"
2038                                     }
2039                                 }
2040                             },
2041                             "acplo" => {
2042                                 "field" => "org",
2043                                 "fkey" => "circ_lib",
2044                                 "type" => "left",
2045                                 "filter" => {
2046                                     "location" => {"=" => {"+acp" => "location"}}
2047                                 }
2048                             }
2049                         }
2050                     }
2051                 }
2052             },
2053             "where" => {
2054                 "+ahr" => {
2055                     "capture_time" => undef,
2056                     "cancel_time" => undef,
2057                     "-or" => [
2058                         {"expire_time" => undef },
2059                         {"expire_time" => {">" => "now"}}
2060                     ]
2061                 }
2062             },
2063             (@$sort ? (order_by => $sort) : ()),
2064             ($$params{limit} ? (limit => $$params{limit}) : ()),
2065             ($$params{offset} ? (offset => $$params{offset}) : ())
2066         }, {"substream" => 1}
2067     ) or return $e->die_event;
2068
2069     $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
2070
2071     my @chunk;
2072     for my $hid (@$holds_ids) {
2073         push @chunk, $e->retrieve_action_hold_request([
2074             $hid->{"id"}, {
2075                 "flesh" => 3,
2076                 "flesh_fields" => {
2077                     "ahr" => ["usr", "current_copy"],
2078                     "au"  => ["card"],
2079                     "acp" => ["location", "call_number", "parts"],
2080                     "acn" => ["record","prefix","suffix"]
2081                 }
2082             }
2083         ]);
2084
2085         if (@chunk >= $$params{chunk_size}) {
2086             $client->respond( \@chunk );
2087             @chunk = ();
2088         }
2089     }
2090     $client->respond_complete( \@chunk ) if (@chunk);
2091     $e->disconnect;
2092     return undef;
2093 }
2094
2095
2096
2097 __PACKAGE__->register_method(
2098     method        => 'fetch_hold_notify',
2099     api_name      => 'open-ils.circ.hold_notification.retrieve_by_hold',
2100     authoritative => 1,
2101     signature     => q/
2102 Returns a list of hold notification objects based on hold id.
2103 @param authtoken The loggin session key
2104 @param holdid The id of the hold whose notifications we want to retrieve
2105 @return An array of hold notification objects, event on error.
2106 /
2107 );
2108
2109 sub fetch_hold_notify {
2110     my( $self, $conn, $authtoken, $holdid ) = @_;
2111     my( $requestor, $evt ) = $U->checkses($authtoken);
2112     return $evt if $evt;
2113     my ($hold, $patron);
2114     ($hold, $evt) = $U->fetch_hold($holdid);
2115     return $evt if $evt;
2116     ($patron, $evt) = $U->fetch_user($hold->usr);
2117     return $evt if $evt;
2118
2119     $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
2120     return $evt if $evt;
2121
2122     $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
2123     return $U->cstorereq(
2124         'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
2125 }
2126
2127
2128 __PACKAGE__->register_method(
2129     method    => 'create_hold_notify',
2130     api_name  => 'open-ils.circ.hold_notification.create',
2131     signature => q/
2132 Creates a new hold notification object
2133 @param authtoken The login session key
2134 @param notification The hold notification object to create
2135 @return ID of the new object on success, Event on error
2136 /
2137 );
2138
2139 sub create_hold_notify {
2140    my( $self, $conn, $auth, $note ) = @_;
2141    my $e = new_editor(authtoken=>$auth, xact=>1);
2142    return $e->die_event unless $e->checkauth;
2143
2144    my $hold = $e->retrieve_action_hold_request($note->hold)
2145       or return $e->die_event;
2146    my $patron = $e->retrieve_actor_user($hold->usr)
2147       or return $e->die_event;
2148
2149    return $e->die_event unless
2150       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
2151
2152    $note->notify_staff($e->requestor->id);
2153    $e->create_action_hold_notification($note) or return $e->die_event;
2154    $e->commit;
2155    return $note->id;
2156 }
2157
2158 __PACKAGE__->register_method(
2159     method    => 'create_hold_note',
2160     api_name  => 'open-ils.circ.hold_note.create',
2161     signature => q/
2162         Creates a new hold request note object
2163         @param authtoken The login session key
2164         @param note The hold note object to create
2165         @return ID of the new object on success, Event on error
2166         /
2167 );
2168
2169 sub create_hold_note {
2170    my( $self, $conn, $auth, $note ) = @_;
2171    my $e = new_editor(authtoken=>$auth, xact=>1);
2172    return $e->die_event unless $e->checkauth;
2173
2174    my $hold = $e->retrieve_action_hold_request($note->hold)
2175       or return $e->die_event;
2176    my $patron = $e->retrieve_actor_user($hold->usr)
2177       or return $e->die_event;
2178
2179    return $e->die_event unless
2180       $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn.  Probably want something more specific
2181
2182    $e->create_action_hold_request_note($note) or return $e->die_event;
2183    $e->commit;
2184    return $note->id;
2185 }
2186
2187 __PACKAGE__->register_method(
2188     method    => 'reset_hold',
2189     api_name  => 'open-ils.circ.hold.reset',
2190     signature => q/
2191         Un-captures and un-targets a hold, essentially returning
2192         it to the state it was in directly after it was placed,
2193         then attempts to re-target the hold
2194         @param authtoken The login session key
2195         @param holdid The id of the hold
2196     /
2197 );
2198
2199
2200 sub reset_hold {
2201     my( $self, $conn, $auth, $holdid ) = @_;
2202     my $reqr;
2203     my ($hold, $evt) = $U->fetch_hold($holdid);
2204     return $evt if $evt;
2205     ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
2206     return $evt if $evt;
2207     $evt = _reset_hold($self, $reqr, $hold);
2208     return $evt if $evt;
2209     return 1;
2210 }
2211
2212
2213 __PACKAGE__->register_method(
2214     method   => 'reset_hold_batch',
2215     api_name => 'open-ils.circ.hold.reset.batch'
2216 );
2217
2218 sub reset_hold_batch {
2219     my($self, $conn, $auth, $hold_ids) = @_;
2220
2221     my $e = new_editor(authtoken => $auth);
2222     return $e->event unless $e->checkauth;
2223
2224     for my $hold_id ($hold_ids) {
2225
2226         my $hold = $e->retrieve_action_hold_request(
2227             [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
2228             or return $e->event;
2229
2230         next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
2231         _reset_hold($self, $e->requestor, $hold);
2232     }
2233
2234     return 1;
2235 }
2236
2237
2238 sub _reset_hold {
2239     my ($self, $reqr, $hold) = @_;
2240
2241     my $e = new_editor(xact =>1, requestor => $reqr);
2242
2243     $logger->info("reseting hold ".$hold->id);
2244
2245     my $hid = $hold->id;
2246
2247     if( $hold->capture_time and $hold->current_copy ) {
2248
2249         my $copy = $e->retrieve_asset_copy($hold->current_copy)
2250             or return $e->die_event;
2251
2252         if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2253             $logger->info("setting copy to status 'reshelving' on hold retarget");
2254             $copy->status(OILS_COPY_STATUS_RESHELVING);
2255             $copy->editor($e->requestor->id);
2256             $copy->edit_date('now');
2257             $e->update_asset_copy($copy) or return $e->die_event;
2258
2259         } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
2260
2261             $logger->warn("! reseting hold [$hid] that is in transit");
2262             my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
2263
2264             if( $transid ) {
2265                 my $trans = $e->retrieve_action_transit_copy($transid);
2266                 if( $trans ) {
2267                     $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
2268                     my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
2269                     $logger->info("Transit abort completed with result $evt");
2270                     unless ("$evt" eq 1) {
2271                         $e->rollback;
2272                         return $evt;
2273                     }
2274                 }
2275             }
2276         }
2277     }
2278
2279     $hold->clear_capture_time;
2280     $hold->clear_current_copy;
2281     $hold->clear_shelf_time;
2282     $hold->clear_shelf_expire_time;
2283     $hold->clear_current_shelf_lib;
2284
2285     $e->update_action_hold_request($hold) or return $e->die_event;
2286     $e->commit;
2287
2288     $U->simplereq('open-ils.hold-targeter', 
2289         'open-ils.hold-targeter.target', {hold => $hold->id});
2290
2291     return undef;
2292 }
2293
2294
2295 __PACKAGE__->register_method(
2296     method    => 'fetch_open_title_holds',
2297     api_name  => 'open-ils.circ.open_holds.retrieve',
2298     signature => q/
2299         Returns a list ids of un-fulfilled holds for a given title id
2300         @param authtoken The login session key
2301         @param id the id of the item whose holds we want to retrieve
2302         @param type The hold type - M, T, I, V, C, F, R
2303     /
2304 );
2305
2306 sub fetch_open_title_holds {
2307     my( $self, $conn, $auth, $id, $type, $org ) = @_;
2308     my $e = new_editor( authtoken => $auth );
2309     return $e->event unless $e->checkauth;
2310
2311     $type ||= "T";
2312     $org  ||= $e->requestor->ws_ou;
2313
2314 #    return $e->search_action_hold_request(
2315 #        { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2316
2317     # XXX make me return IDs in the future ^--
2318     my $holds = $e->search_action_hold_request(
2319         {
2320             target           => $id,
2321             cancel_time      => undef,
2322             hold_type        => $type,
2323             fulfillment_time => undef
2324         }
2325     );
2326
2327     flesh_hold_transits($holds);
2328     return $holds;
2329 }
2330
2331
2332 sub flesh_hold_transits {
2333     my $holds = shift;
2334     for my $hold ( @$holds ) {
2335         $hold->transit(
2336             $apputils->simplereq(
2337                 'open-ils.cstore',
2338                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2339                 { hold => $hold->id, cancel_time => undef },
2340                 { order_by => { ahtc => 'id desc' }, limit => 1 }
2341             )->[0]
2342         );
2343     }
2344 }
2345
2346 sub flesh_hold_notices {
2347     my( $holds, $e ) = @_;
2348     $e ||= new_editor();
2349
2350     for my $hold (@$holds) {
2351         my $notices = $e->search_action_hold_notification(
2352             [
2353                 { hold => $hold->id },
2354                 { order_by => { anh => 'notify_time desc' } },
2355             ],
2356             {idlist=>1}
2357         );
2358
2359         $hold->notify_count(scalar(@$notices));
2360         if( @$notices ) {
2361             my $n = $e->retrieve_action_hold_notification($$notices[0])
2362                 or return $e->event;
2363             $hold->notify_time($n->notify_time);
2364         }
2365     }
2366 }
2367
2368
2369 __PACKAGE__->register_method(
2370     method    => 'fetch_captured_holds',
2371     api_name  => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2372     stream    => 1,
2373     authoritative => 1,
2374     signature => q/
2375         Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2376         @param authtoken The login session key
2377         @param org The org id of the location in question
2378         @param match_copy A specific copy to limit to
2379     /
2380 );
2381
2382 __PACKAGE__->register_method(
2383     method    => 'fetch_captured_holds',
2384     api_name  => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2385     stream    => 1,
2386     authoritative => 1,
2387     signature => q/
2388         Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2389         @param authtoken The login session key
2390         @param org The org id of the location in question
2391         @param match_copy A specific copy to limit to
2392     /
2393 );
2394
2395 __PACKAGE__->register_method(
2396     method    => 'fetch_captured_holds',
2397     api_name  => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2398     stream    => 1,
2399     authoritative => 1,
2400     signature => q/
2401         Returns list ids of shelf-expired un-fulfilled holds for a given title id
2402         @param authtoken The login session key
2403         @param org The org id of the location in question
2404         @param match_copy A specific copy to limit to
2405     /
2406 );
2407
2408 __PACKAGE__->register_method(
2409     method    => 'fetch_captured_holds',
2410     api_name  => 
2411       'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2412     stream    => 1,
2413     authoritative => 1,
2414     signature => q/
2415         Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2416         for a given shelf lib
2417     /
2418 );
2419
2420 __PACKAGE__->register_method(
2421     method    => 'fetch_captured_holds',
2422     api_name  => 
2423       'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2424     stream    => 1,
2425     authoritative => 1,
2426     signature => q/
2427         Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2428         for a given shelf lib
2429     /
2430 );
2431
2432
2433 sub fetch_captured_holds {
2434     my( $self, $conn, $auth, $org, $match_copy ) = @_;
2435
2436     my $e = new_editor(authtoken => $auth);
2437     return $e->die_event unless $e->checkauth;
2438     return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2439
2440     $org ||= $e->requestor->ws_ou;
2441
2442     my $current_copy = { '!=' => undef };
2443     $current_copy = { '=' => $match_copy } if $match_copy;
2444
2445     my $query = {
2446         select => { alhr => ['id'] },
2447         from   => {
2448             alhr => {
2449                 acp => {
2450                     field => 'id',
2451                     fkey  => 'current_copy'
2452                 },
2453             }
2454         },
2455         where => {
2456             '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2457             '+alhr' => {
2458                 capture_time      => { "!=" => undef },
2459                 current_copy      => $current_copy,
2460                 fulfillment_time  => undef,
2461                 current_shelf_lib => $org
2462             }
2463         }
2464     };
2465     if($self->api_name =~ /expired/) {
2466         $query->{'where'}->{'+alhr'}->{'-or'} = {
2467                 shelf_expire_time => { '<' => 'today'},
2468                 cancel_time => { '!=' => undef },
2469         };
2470     }
2471     my $hold_ids = $e->json_query( $query );
2472
2473     if ($self->api_name =~ /wrong_shelf/) {
2474         # fetch holds whose current_shelf_lib is $org, but whose pickup 
2475         # lib is some other org unit.  Ignore already-retrieved holds.
2476         my $wrong_shelf =
2477             pickup_lib_changed_on_shelf_holds(
2478                 $e, $org, [map {$_->{id}} @$hold_ids]);
2479         # match the layout of other items in $hold_ids
2480         push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2481     }
2482
2483
2484     for my $hold_id (@$hold_ids) {
2485         if($self->api_name =~ /id_list/) {
2486             $conn->respond($hold_id->{id});
2487             next;
2488         } else {
2489             $conn->respond(
2490                 $e->retrieve_action_hold_request([
2491                     $hold_id->{id},
2492                     {
2493                         flesh => 1,
2494                         flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2495                         order_by => {anh => 'notify_time desc'}
2496                     }
2497                 ])
2498             );
2499         }
2500     }
2501
2502     return undef;
2503 }
2504
2505 __PACKAGE__->register_method(
2506     method    => "print_expired_holds_stream",
2507     api_name  => "open-ils.circ.captured_holds.expired.print.stream",
2508     stream    => 1
2509 );
2510
2511 sub print_expired_holds_stream {
2512     my ($self, $client, $auth, $params) = @_;
2513
2514     # No need to check specific permissions: we're going to call another method
2515     # that will do that.
2516     my $e = new_editor("authtoken" => $auth);
2517     return $e->die_event unless $e->checkauth;
2518
2519     delete($$params{org_id}) unless (int($$params{org_id}));
2520     delete($$params{limit}) unless (int($$params{limit}));
2521     delete($$params{offset}) unless (int($$params{offset}));
2522     delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2523     delete($$params{chunk_size}) if  ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2524     $$params{chunk_size} ||= 10;
2525     $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2526
2527     $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2528
2529     my @hold_ids = $self->method_lookup(
2530         "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2531     )->run($auth, $params->{"org_id"});
2532
2533     if (!@hold_ids) {
2534         $e->disconnect;
2535         return;
2536     } elsif (defined $U->event_code($hold_ids[0])) {
2537         $e->disconnect;
2538         return $hold_ids[0];
2539     }
2540
2541     $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2542
2543     while (@hold_ids) {
2544         my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2545
2546         my $result_chunk = $e->json_query({
2547             "select" => {
2548                 "acp" => ["barcode"],
2549                 "au" => [qw/
2550                     first_given_name second_given_name family_name alias
2551                 /],
2552                 "acn" => ["label"],
2553                 "bre" => ["marc"],
2554                 "acpl" => ["name"]
2555             },
2556             "from" => {
2557                 "ahr" => {
2558                     "acp" => {
2559                         "field" => "id", "fkey" => "current_copy",
2560                         "join" => {
2561                             "acn" => {
2562                                 "field" => "id", "fkey" => "call_number",
2563                                 "join" => {
2564                                     "bre" => {
2565                                         "field" => "id", "fkey" => "record"
2566                                     }
2567                                 }
2568                             },
2569                             "acpl" => {"field" => "id", "fkey" => "location"}
2570                         }
2571                     },
2572                     "au" => {"field" => "id", "fkey" => "usr"}
2573                 }
2574             },
2575             "where" => {"+ahr" => {"id" => \@hid_chunk}}
2576         }) or return $e->die_event;
2577         $client->respond($result_chunk);
2578     }
2579
2580     $e->disconnect;
2581     undef;
2582 }
2583
2584 __PACKAGE__->register_method(
2585     method    => "check_title_hold_batch",
2586     api_name  => "open-ils.circ.title_hold.is_possible.batch",
2587     stream    => 1,
2588     signature => {
2589         desc  => '@see open-ils.circ.title_hold.is_possible.batch',
2590         params => [
2591             { desc => 'Authentication token',     type => 'string'},
2592             { desc => 'Array of Hash of named parameters', type => 'array'},
2593         ],
2594         return => {
2595             desc => 'Array of response objects',
2596             type => 'array'
2597         }
2598     }
2599 );
2600
2601 sub check_title_hold_batch {
2602     my($self, $client, $authtoken, $param_list, $oargs) = @_;
2603     foreach (@$param_list) {
2604         my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2605         $client->respond($res);
2606     }
2607     return undef;
2608 }
2609
2610
2611 __PACKAGE__->register_method(
2612     method    => "check_title_hold",
2613     api_name  => "open-ils.circ.title_hold.is_possible",
2614     signature => {
2615         desc  => 'Determines if a hold were to be placed by a given user, ' .
2616              'whether or not said hold would have any potential copies to fulfill it.' .
2617              'The named paramaters of the second argument include: ' .
2618              'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2619              'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2620         params => [
2621             { desc => 'Authentication token',     type => 'string'},
2622             { desc => 'Hash of named parameters', type => 'object'},
2623         ],
2624         return => {
2625             desc => 'List of new message IDs (empty if none)',
2626             type => 'array'
2627         }
2628     }
2629 );
2630
2631 =head3 check_title_hold (token, hash)
2632
2633 The named fields in the hash are:
2634
2635  patronid     - ID of the hold recipient  (required)
2636  depth        - hold range depth          (default 0)
2637  pickup_lib   - destination for hold, fallback value for selection_ou
2638  selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2639  issuanceid   - ID of the issuance to be held, required for Issuance level hold
2640  partid       - ID of the monograph part to be held, required for monograph part level hold
2641  titleid      - ID (BRN) of the title to be held, required for Title level hold
2642  volume_id    - required for Volume level hold
2643  copy_id      - required for Copy level hold
2644  mrid         - required for Meta-record level hold
2645  hold_type    - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record  (default "T")
2646
2647 All key/value pairs are passed on to do_possibility_checks.
2648
2649 =cut
2650
2651 # FIXME: better params checking.  what other params are required, if any?
2652 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2653 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2654 # used in conditionals, where it may be undefined, causing a warning.
2655 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2656
2657 sub check_title_hold {
2658     my( $self, $client, $authtoken, $params ) = @_;
2659     my $e = new_editor(authtoken=>$authtoken);
2660     return $e->event unless $e->checkauth;
2661
2662     my %params       = %$params;
2663     my $depth        = $params{depth}        || 0;
2664     $params{depth} = $depth;   #define $params{depth} if unset, since it gets used later
2665     my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2666     my $oargs        = $params{oargs}        || {};
2667
2668     if($oargs->{events}) {
2669         @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2670     }
2671
2672
2673     my $patron = $e->retrieve_actor_user($params{patronid})
2674         or return $e->event;
2675
2676     if( $e->requestor->id ne $patron->id ) {
2677         return $e->event unless
2678             $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2679     }
2680
2681     return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2682
2683     my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2684         or return $e->event;
2685
2686     my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2687     my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2688
2689     my @status = ();
2690     my $return_depth = $hard_boundary; # default depth to return on success
2691     if(defined $soft_boundary and $depth < $soft_boundary) {
2692         # work up the tree and as soon as we find a potential copy, use that depth
2693         # also, make sure we don't go past the hard boundary if it exists
2694
2695         # our min boundary is the greater of user-specified boundary or hard boundary
2696         my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2697             $hard_boundary : $depth;
2698
2699         my $depth = $soft_boundary;
2700         while($depth >= $min_depth) {
2701             $logger->info("performing hold possibility check with soft boundary $depth");
2702             @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2703             if ($status[0]) {
2704                 $return_depth = $depth;
2705                 last;
2706             }
2707             $depth--;
2708         }
2709     } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2710         # there is no soft boundary, enforce the hard boundary if it exists
2711         $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2712         @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2713     } else {
2714         # no boundaries defined, fall back to user specifed boundary or no boundary
2715         $logger->info("performing hold possibility check with no boundary");
2716         @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2717     }
2718
2719     my $place_unfillable = 0;
2720     $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2721
2722     if ($status[0]) {
2723         return {
2724             "success" => 1,
2725             "depth" => $return_depth,
2726             "local_avail" => $status[1]
2727         };
2728     } elsif ($status[2]) {
2729         my $n = scalar @{$status[2]};
2730         return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2731     } else {
2732         return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2733     }
2734 }
2735
2736
2737
2738 sub do_possibility_checks {
2739     my($e, $patron, $request_lib, $depth, %params) = @_;
2740
2741     my $issuanceid   = $params{issuanceid}      || "";
2742     my $partid       = $params{partid}      || "";
2743     my $titleid      = $params{titleid}      || "";
2744     my $volid        = $params{volume_id};
2745     my $copyid       = $params{copy_id};
2746     my $mrid         = $params{mrid}         || "";
2747     my $pickup_lib   = $params{pickup_lib};
2748     my $hold_type    = $params{hold_type}    || 'T';
2749     my $selection_ou = $params{selection_ou} || $pickup_lib;
2750     my $holdable_formats = $params{holdable_formats};
2751     my $oargs        = $params{oargs}        || {};
2752
2753
2754     my $copy;
2755     my $volume;
2756     my $title;
2757
2758     if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2759
2760         return $e->event unless $copy   = $e->retrieve_asset_copy($copyid);
2761         return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2762         return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
2763
2764         return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2765         return verify_copy_for_hold(
2766             $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2767         );
2768
2769     } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2770
2771         return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2772         return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
2773
2774         return _check_volume_hold_is_possible(
2775             $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2776         );
2777
2778     } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2779
2780         return _check_title_hold_is_possible(
2781             $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2782         );
2783
2784     } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2785
2786         return _check_issuance_hold_is_possible(
2787             $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2788         );
2789
2790     } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2791
2792         return _check_monopart_hold_is_possible(
2793             $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2794         );
2795
2796     } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2797
2798         # pasing undef as the depth to filtered_records causes the depth
2799         # of the selection_ou to be used, which is not what we want here.
2800         $depth ||= 0;
2801
2802         my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2803         my @status = ();
2804         for my $rec (@$recs) {
2805             @status = _check_title_hold_is_possible(
2806                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2807             );
2808             last if $status[0];
2809         }
2810         return @status;
2811     }
2812 #   else { Unrecognized hold_type ! }   # FIXME: return error? or 0?
2813 }
2814
2815 sub MR_filter_records {
2816     my $self = shift;
2817     my $client = shift;
2818     my $m = shift;
2819     my $f = shift;
2820     my $o = shift;
2821     my $d = shift;
2822     my $opac_visible = shift;
2823     
2824     my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2825     return $U->storagereq(
2826         'open-ils.storage.metarecord.filtered_records.atomic', 
2827         $m, $f, $org_at_depth, $opac_visible
2828     );
2829 }
2830 __PACKAGE__->register_method(
2831     method   => 'MR_filter_records',
2832     api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2833 );
2834
2835
2836 my %prox_cache;
2837 sub create_ranged_org_filter {
2838     my($e, $selection_ou, $depth) = @_;
2839
2840     # find the orgs from which this hold may be fulfilled,
2841     # based on the selection_ou and depth
2842
2843     my $top_org = $e->search_actor_org_unit([
2844         {parent_ou => undef},
2845         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2846     my %org_filter;
2847
2848     return () if $depth == $top_org->ou_type->depth;
2849
2850     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2851     %org_filter = (circ_lib => []);
2852     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2853
2854     $logger->info("hold org filter at depth $depth and selection_ou ".
2855         "$selection_ou created list of @{$org_filter{circ_lib}}");
2856
2857     return %org_filter;
2858 }
2859
2860
2861 sub _check_title_hold_is_possible {
2862     my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2863     # $holdable_formats is now unused. We pre-filter the MR's records.
2864
2865     my $e = new_editor();
2866     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2867
2868     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2869     my $copies = $e->json_query(
2870         {
2871             select => { acp => ['id', 'circ_lib'] },
2872               from => {
2873                 acp => {
2874                     acn => {
2875                         field  => 'id',
2876                         fkey   => 'call_number',
2877                         filter => { record => $titleid }
2878                     },
2879                     acpl => {
2880                                 field => 'id',
2881                                 filter => { holdable => 't', deleted => 'f' },
2882                                 fkey => 'location'
2883                             },
2884                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   },
2885                     acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2886                 }
2887             },
2888             where => {
2889                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2890                 '+acpm' => { target_copy => undef } # ignore part-linked copies
2891             }
2892         }
2893     );
2894
2895     $logger->info("title possible found ".scalar(@$copies)." potential copies");
2896     return (
2897         0, 0, [
2898             new OpenILS::Event(
2899                 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2900                 "payload" => {"fail_part" => "no_ultimate_items"}
2901             )
2902         ]
2903     ) unless @$copies;
2904
2905     # -----------------------------------------------------------------------
2906     # sort the copies into buckets based on their circ_lib proximity to
2907     # the patron's home_ou.
2908     # -----------------------------------------------------------------------
2909
2910     my $home_org = $patron->home_ou;
2911     my $req_org = $request_lib->id;
2912
2913     $prox_cache{$home_org} =
2914         $e->search_actor_org_unit_proximity({from_org => $home_org})
2915         unless $prox_cache{$home_org};
2916     my $home_prox = $prox_cache{$home_org};
2917     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2918
2919     my %buckets;
2920     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2921     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2922
2923     my @keys = sort { $a <=> $b } keys %buckets;
2924
2925
2926     if( $home_org ne $req_org ) {
2927       # -----------------------------------------------------------------------
2928       # shove the copies close to the request_lib into the primary buckets
2929       # directly before the farthest away copies.  That way, they are not
2930       # given priority, but they are checked before the farthest copies.
2931       # -----------------------------------------------------------------------
2932         $prox_cache{$req_org} =
2933             $e->search_actor_org_unit_proximity({from_org => $req_org})
2934             unless $prox_cache{$req_org};
2935         my $req_prox = $prox_cache{$req_org};
2936
2937         my %buckets2;
2938         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2939         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2940
2941         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
2942         my $new_key = $highest_key - 0.5; # right before the farthest prox
2943         my @keys2   = sort { $a <=> $b } keys %buckets2;
2944         for my $key (@keys2) {
2945             last if $key >= $highest_key;
2946             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2947         }
2948     }
2949
2950     @keys = sort { $a <=> $b } keys %buckets;
2951
2952     my $title;
2953     my %seen;
2954     my @status;
2955     my $age_protect_only = 0;
2956     OUTER: for my $key (@keys) {
2957       my @cps = @{$buckets{$key}};
2958
2959       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2960
2961       for my $copyid (@cps) {
2962
2963          next if $seen{$copyid};
2964          $seen{$copyid} = 1; # there could be dupes given the merged buckets
2965          my $copy = $e->retrieve_asset_copy($copyid);
2966          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2967
2968          unless($title) { # grab the title if we don't already have it
2969             my $vol = $e->retrieve_asset_call_number(
2970                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2971             $title = $vol->record;
2972          }
2973
2974          @status = verify_copy_for_hold(
2975             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2976
2977          $age_protect_only ||= $status[3];
2978          last OUTER if $status[0];
2979       }
2980     }
2981
2982     $status[3] = $age_protect_only;
2983     return @status;
2984 }
2985
2986 sub _check_issuance_hold_is_possible {
2987     my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2988
2989     my $e = new_editor();
2990     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2991
2992     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2993     my $copies = $e->json_query(
2994         {
2995             select => { acp => ['id', 'circ_lib'] },
2996               from => {
2997                 acp => {
2998                     sitem => {
2999                         field  => 'unit',
3000                         fkey   => 'id',
3001                         filter => { issuance => $issuanceid }
3002                     },
3003                     acpl => {
3004                         field => 'id',
3005                         filter => { holdable => 't', deleted => 'f' },
3006                         fkey => 'location'
3007                     },
3008                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
3009                 }
3010             },
3011             where => {
3012                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3013             },
3014             distinct => 1
3015         }
3016     );
3017
3018     $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
3019
3020     my $empty_ok;
3021     if (!@$copies) {
3022         $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3023         $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3024
3025         return (
3026             0, 0, [
3027                 new OpenILS::Event(
3028                     "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3029                     "payload" => {"fail_part" => "no_ultimate_items"}
3030                 )
3031             ]
3032         ) unless $empty_ok;
3033
3034         return (1, 0);
3035     }
3036
3037     # -----------------------------------------------------------------------
3038     # sort the copies into buckets based on their circ_lib proximity to
3039     # the patron's home_ou.
3040     # -----------------------------------------------------------------------
3041
3042     my $home_org = $patron->home_ou;
3043     my $req_org = $request_lib->id;
3044
3045     $prox_cache{$home_org} =
3046         $e->search_actor_org_unit_proximity({from_org => $home_org})
3047         unless $prox_cache{$home_org};
3048     my $home_prox = $prox_cache{$home_org};
3049     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3050
3051     my %buckets;
3052     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3053     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3054
3055     my @keys = sort { $a <=> $b } keys %buckets;
3056
3057
3058     if( $home_org ne $req_org ) {
3059       # -----------------------------------------------------------------------
3060       # shove the copies close to the request_lib into the primary buckets
3061       # directly before the farthest away copies.  That way, they are not
3062       # given priority, but they are checked before the farthest copies.
3063       # -----------------------------------------------------------------------
3064         $prox_cache{$req_org} =
3065             $e->search_actor_org_unit_proximity({from_org => $req_org})
3066             unless $prox_cache{$req_org};
3067         my $req_prox = $prox_cache{$req_org};
3068
3069         my %buckets2;
3070         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3071         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3072
3073         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
3074         my $new_key = $highest_key - 0.5; # right before the farthest prox
3075         my @keys2   = sort { $a <=> $b } keys %buckets2;
3076         for my $key (@keys2) {
3077             last if $key >= $highest_key;
3078             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3079         }
3080     }
3081
3082     @keys = sort { $a <=> $b } keys %buckets;
3083
3084     my $title;
3085     my %seen;
3086     my @status;
3087     my $age_protect_only = 0;
3088     OUTER: for my $key (@keys) {
3089       my @cps = @{$buckets{$key}};
3090
3091       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3092
3093       for my $copyid (@cps) {
3094
3095          next if $seen{$copyid};
3096          $seen{$copyid} = 1; # there could be dupes given the merged buckets
3097          my $copy = $e->retrieve_asset_copy($copyid);
3098          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3099
3100          unless($title) { # grab the title if we don't already have it
3101             my $vol = $e->retrieve_asset_call_number(
3102                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3103             $title = $vol->record;
3104          }
3105
3106          @status = verify_copy_for_hold(
3107             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3108
3109          $age_protect_only ||= $status[3];
3110          last OUTER if $status[0];
3111       }
3112     }
3113
3114     if (!$status[0]) {
3115         if (!defined($empty_ok)) {
3116             $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3117             $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3118         }
3119
3120         return (1,0) if ($empty_ok);
3121     }
3122     $status[3] = $age_protect_only;
3123     return @status;
3124 }
3125
3126 sub _check_monopart_hold_is_possible {
3127     my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3128
3129     my $e = new_editor();
3130     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3131
3132     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3133     my $copies = $e->json_query(
3134         {
3135             select => { acp => ['id', 'circ_lib'] },
3136               from => {
3137                 acp => {
3138                     acpm => {
3139                         field  => 'target_copy',
3140                         fkey   => 'id',
3141                         filter => { part => $partid }
3142                     },
3143                     acpl => {
3144                         field => 'id',
3145                         filter => { holdable => 't', deleted => 'f' },
3146                         fkey => 'location'
3147                     },
3148                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
3149                 }
3150             },
3151             where => {
3152                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3153             },
3154             distinct => 1
3155         }
3156     );
3157
3158     $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
3159
3160     my $empty_ok;
3161     if (!@$copies) {
3162         $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3163         $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3164
3165         return (
3166             0, 0, [
3167                 new OpenILS::Event(
3168                     "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3169                     "payload" => {"fail_part" => "no_ultimate_items"}
3170                 )
3171             ]
3172         ) unless $empty_ok;
3173
3174         return (1, 0);
3175     }
3176
3177     # -----------------------------------------------------------------------
3178     # sort the copies into buckets based on their circ_lib proximity to
3179     # the patron's home_ou.
3180     # -----------------------------------------------------------------------
3181
3182     my $home_org = $patron->home_ou;
3183     my $req_org = $request_lib->id;
3184
3185     $prox_cache{$home_org} =
3186         $e->search_actor_org_unit_proximity({from_org => $home_org})
3187         unless $prox_cache{$home_org};
3188     my $home_prox = $prox_cache{$home_org};
3189     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3190
3191     my %buckets;
3192     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3193     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3194
3195     my @keys = sort { $a <=> $b } keys %buckets;
3196
3197
3198     if( $home_org ne $req_org ) {
3199       # -----------------------------------------------------------------------
3200       # shove the copies close to the request_lib into the primary buckets
3201       # directly before the farthest away copies.  That way, they are not
3202       # given priority, but they are checked before the farthest copies.
3203       # -----------------------------------------------------------------------
3204         $prox_cache{$req_org} =
3205             $e->search_actor_org_unit_proximity({from_org => $req_org})
3206             unless $prox_cache{$req_org};
3207         my $req_prox = $prox_cache{$req_org};
3208
3209         my %buckets2;
3210         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3211         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3212
3213         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
3214         my $new_key = $highest_key - 0.5; # right before the farthest prox
3215         my @keys2   = sort { $a <=> $b } keys %buckets2;
3216         for my $key (@keys2) {
3217             last if $key >= $highest_key;
3218             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3219         }
3220     }
3221
3222     @keys = sort { $a <=> $b } keys %buckets;
3223
3224     my $title;
3225     my %seen;
3226     my @status;
3227     my $age_protect_only = 0;
3228     OUTER: for my $key (@keys) {
3229       my @cps = @{$buckets{$key}};
3230
3231       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3232
3233       for my $copyid (@cps) {
3234
3235          next if $seen{$copyid};
3236          $seen{$copyid} = 1; # there could be dupes given the merged buckets
3237          my $copy = $e->retrieve_asset_copy($copyid);
3238          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3239
3240          unless($title) { # grab the title if we don't already have it
3241             my $vol = $e->retrieve_asset_call_number(
3242                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3243             $title = $vol->record;
3244          }
3245
3246          @status = verify_copy_for_hold(
3247             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3248
3249          $age_protect_only ||= $status[3];
3250          last OUTER if $status[0];
3251       }
3252     }
3253
3254     if (!$status[0]) {
3255         if (!defined($empty_ok)) {
3256             $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3257             $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3258         }
3259
3260         return (1,0) if ($empty_ok);
3261     }
3262     $status[3] = $age_protect_only;
3263     return @status;
3264 }
3265
3266
3267 sub _check_volume_hold_is_possible {
3268     my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3269     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
3270     my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
3271     $logger->info("checking possibility of volume hold for volume ".$vol->id);
3272
3273     my $filter_copies = [];
3274     for my $copy (@$copies) {
3275         # ignore part-mapped copies for regular volume level holds
3276         push(@$filter_copies, $copy) unless
3277             new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
3278     }
3279     $copies = $filter_copies;
3280
3281     return (
3282         0, 0, [
3283             new OpenILS::Event(
3284                 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3285                 "payload" => {"fail_part" => "no_ultimate_items"}
3286             )
3287         ]
3288     ) unless @$copies;
3289
3290     my @status;
3291     my $age_protect_only = 0;
3292     for my $copy ( @$copies ) {
3293         @status = verify_copy_for_hold(
3294             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3295         $age_protect_only ||= $status[3];
3296         last if $status[0];
3297     }
3298     $status[3] = $age_protect_only;
3299     return @status;
3300 }
3301
3302
3303
3304 sub verify_copy_for_hold {
3305     my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3306     # $oargs should be undef unless we're overriding.
3307     $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3308     my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3309         {
3310             patron           => $patron,
3311             requestor        => $requestor,
3312             copy             => $copy,
3313             title            => $title,
3314             title_descriptor => $title->fixed_fields,
3315             pickup_lib       => $pickup_lib,
3316             request_lib      => $request_lib,
3317             new_hold         => 1,
3318             show_event_list  => 1
3319         }
3320     );
3321
3322     # Check for override permissions on events.
3323     if ($oargs && $permitted && scalar @$permitted) {
3324         # Remove the events from permitted that we can override.
3325         if ($oargs->{events}) {
3326             foreach my $evt (@{$oargs->{events}}) {
3327                 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3328             }
3329         }
3330         # Now, we handle the override all case by checking remaining
3331         # events against override permissions.
3332         if (scalar @$permitted && $oargs->{all}) {
3333             # Pre-set events and failed members of oargs to empty
3334             # arrays, if they are not set, yet.
3335             $oargs->{events} = [] unless ($oargs->{events});
3336             $oargs->{failed} = [] unless ($oargs->{failed});
3337             # When we're done with these checks, we swap permitted
3338             # with a reference to @disallowed.
3339             my @disallowed = ();
3340             foreach my $evt (@{$permitted}) {
3341                 # Check if we've already seen the event in this
3342                 # session and it failed.
3343                 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3344                     push(@disallowed, $evt);
3345                 } else {
3346                     # We have to check if the requestor has the
3347                     # override permission.
3348
3349                     # AppUtils::check_user_perms returns the perm if
3350                     # the user doesn't have it, undef if they do.
3351                     if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3352                         push(@disallowed, $evt);
3353                         push(@{$oargs->{failed}}, $evt->{textcode});
3354                     } else {
3355                         push(@{$oargs->{events}}, $evt->{textcode});
3356                     }
3357                 }
3358             }
3359             $permitted = \@disallowed;
3360         }
3361     }
3362
3363     my $age_protect_only = 0;
3364     if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3365         $age_protect_only = 1;
3366     }
3367
3368     return (
3369         (not scalar @$permitted), # true if permitted is an empty arrayref
3370         (   # XXX This test is of very dubious value; someone should figure
3371             # out what if anything is checking this value
3372             ($copy->circ_lib == $pickup_lib) and
3373             ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3374         ),
3375         $permitted,
3376         $age_protect_only
3377     );
3378 }
3379
3380
3381
3382 sub find_nearest_permitted_hold {
3383
3384     my $class  = shift;
3385     my $editor = shift;     # CStoreEditor object
3386     my $copy   = shift;     # copy to target
3387     my $user   = shift;     # staff
3388     my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3389
3390     my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3391
3392     my $bc = $copy->barcode;
3393
3394     # find any existing holds that already target this copy
3395     my $old_holds = $editor->search_action_hold_request(
3396         {    current_copy => $copy->id,
3397             cancel_time  => undef,
3398             capture_time => undef
3399         }
3400     );
3401
3402     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3403
3404     $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3405         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3406
3407     my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3408
3409     # the nearest_hold API call now needs this
3410     $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3411         unless ref $copy->call_number;
3412
3413     # search for what should be the best holds for this copy to fulfill
3414     my $best_holds = $U->storagereq(
3415         "open-ils.storage.action.hold_request.nearest_hold.atomic", 
3416         $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3417
3418     # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3419     if ($old_holds) {
3420         for my $holdid (@$old_holds) {
3421             next unless $holdid;
3422             push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3423         }
3424     }
3425
3426     unless(@$best_holds) {
3427         $logger->info("circulator: no suitable holds found for copy $bc");
3428         return (undef, $evt);
3429     }
3430
3431
3432     my $best_hold;
3433
3434     # for each potential hold, we have to run the permit script
3435     # to make sure the hold is actually permitted.
3436     my %reqr_cache;
3437     my %org_cache;
3438     for my $holdid (@$best_holds) {
3439         next unless $holdid;
3440         $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3441
3442         my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3443         # Force and recall holds bypass all rules
3444         if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3445             $best_hold = $hold;
3446             last;
3447         }
3448         my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3449         my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3450
3451         $reqr_cache{$hold->requestor} = $reqr;
3452         $org_cache{$hold->request_lib} = $rlib;
3453
3454         # see if this hold is permitted
3455         my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3456             {
3457                 patron_id   => $hold->usr,
3458                 requestor   => $reqr,
3459                 copy        => $copy,
3460                 pickup_lib  => $hold->pickup_lib,
3461                 request_lib => $rlib,
3462                 retarget    => 1
3463             }
3464         );
3465
3466         if( $permitted ) {
3467             $best_hold = $hold;
3468             last;
3469         }
3470     }
3471
3472
3473     unless( $best_hold ) { # no "good" permitted holds were found
3474         # we got nuthin
3475         $logger->info("circulator: no suitable holds found for copy $bc");
3476         return (undef, $evt);
3477     }
3478
3479     $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3480
3481     # indicate a permitted hold was found
3482     return $best_hold if $check_only;
3483
3484     # we've found a permitted hold.  we need to "grab" the copy
3485     # to prevent re-targeted holds (next part) from re-grabbing the copy
3486     $best_hold->current_copy($copy->id);
3487     $editor->update_action_hold_request($best_hold)
3488         or return (undef, $editor->event);
3489
3490
3491     my @retarget;
3492
3493     # re-target any other holds that already target this copy
3494     for my $old_hold (@$old_holds) {
3495         next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3496         $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3497             $old_hold->id." after a better hold [".$best_hold->id."] was found");
3498         $old_hold->clear_current_copy;
3499         $old_hold->clear_prev_check_time;
3500         $editor->update_action_hold_request($old_hold)
3501             or return (undef, $editor->event);
3502         push(@retarget, $old_hold->id);
3503     }
3504
3505     return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3506 }
3507
3508
3509
3510
3511
3512
3513 __PACKAGE__->register_method(
3514     method   => 'all_rec_holds',
3515     api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3516 );
3517
3518 sub all_rec_holds {
3519     my( $self, $conn, $auth, $title_id, $args ) = @_;
3520
3521     my $e = new_editor(authtoken=>$auth);
3522     $e->checkauth or return $e->event;
3523     $e->allowed('VIEW_HOLD') or return $e->event;
3524
3525     $args ||= {};
3526     $args->{fulfillment_time} = undef; #  we don't want to see old fulfilled holds
3527     $args->{cancel_time} = undef;
3528
3529     my $resp = {
3530           metarecord_holds => []
3531         , title_holds      => []
3532         , volume_holds     => []
3533         , copy_holds       => []
3534         , recall_holds     => []
3535         , force_holds      => []
3536         , part_holds       => []
3537         , issuance_holds   => []
3538     };
3539
3540     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3541     if($mr_map) {
3542         $resp->{metarecord_holds} = $e->search_action_hold_request(
3543             {   hold_type => OILS_HOLD_TYPE_METARECORD,
3544                 target => $mr_map->metarecord,
3545                 %$args
3546             }, {idlist => 1}
3547         );
3548     }
3549
3550     $resp->{title_holds} = $e->search_action_hold_request(
3551         {
3552             hold_type => OILS_HOLD_TYPE_TITLE,
3553             target => $title_id,
3554             %$args
3555         }, {idlist=>1} );
3556
3557     my $parts = $e->search_biblio_monograph_part(
3558         {
3559             record => $title_id
3560         }, {idlist=>1} );
3561
3562     if (@$parts) {
3563         $resp->{part_holds} = $e->search_action_hold_request(
3564             {
3565                 hold_type => OILS_HOLD_TYPE_MONOPART,
3566                 target => $parts,
3567                 %$args
3568             }, {idlist=>1} );
3569     }
3570
3571     my $subs = $e->search_serial_subscription(
3572         { record_entry => $title_id }, {idlist=>1});
3573
3574     if (@$subs) {
3575         my $issuances = $e->search_serial_issuance(
3576             {subscription => $subs}, {idlist=>1}
3577         );
3578
3579         if (@$issuances) {
3580             $resp->{issuance_holds} = $e->search_action_hold_request(
3581                 {
3582                     hold_type => OILS_HOLD_TYPE_ISSUANCE,
3583                     target => $issuances,
3584                     %$args
3585                 }, {idlist=>1}
3586             );
3587         }
3588     }
3589
3590     my $vols = $e->search_asset_call_number(
3591         { record => $title_id, deleted => 'f' }, {idlist=>1});
3592
3593     return $resp unless @$vols;
3594
3595     $resp->{volume_holds} = $e->search_action_hold_request(
3596         {
3597             hold_type => OILS_HOLD_TYPE_VOLUME,
3598             target => $vols,
3599             %$args },
3600         {idlist=>1} );
3601
3602     my $copies = $e->search_asset_copy(
3603         { call_number => $vols, deleted => 'f' }, {idlist=>1});
3604
3605     return $resp unless @$copies;
3606
3607     $resp->{copy_holds} = $e->search_action_hold_request(
3608         {
3609             hold_type => OILS_HOLD_TYPE_COPY,
3610             target => $copies,
3611             %$args },
3612         {idlist=>1} );
3613
3614     $resp->{recall_holds} = $e->search_action_hold_request(
3615         {
3616             hold_type => OILS_HOLD_TYPE_RECALL,
3617             target => $copies,
3618             %$args },
3619         {idlist=>1} );
3620
3621     $resp->{force_holds} = $e->search_action_hold_request(
3622         {
3623             hold_type => OILS_HOLD_TYPE_FORCE,
3624             target => $copies,
3625             %$args },
3626         {idlist=>1} );
3627
3628     return $resp;
3629 }
3630
3631 __PACKAGE__->register_method(
3632     method           => 'stream_wide_holds',
3633     authoritative    => 1,
3634     stream           => 1,
3635     api_name         => 'open-ils.circ.hold.wide_hash.stream'
3636 );
3637
3638 sub stream_wide_holds {
3639     my($self, $client, $auth, $restrictions, $order_by, $limit, $offset) = @_;
3640
3641     my $e = new_editor(authtoken=>$auth);
3642     $e->checkauth or return $e->event;
3643     $e->allowed('VIEW_HOLD') or return $e->event;
3644
3645     my $st = OpenSRF::AppSession->create('open-ils.storage');
3646     my $req = $st->request(
3647         'open-ils.storage.action.live_holds.wide_hash',
3648         $restrictions, $order_by, $limit, $offset
3649     );
3650
3651     my $count = $req->recv;
3652     if(!$count) {
3653         return 0;
3654     }
3655
3656     if(UNIVERSAL::isa($count,"Error")) {
3657         throw $count ($count->stringify);
3658     }
3659
3660     $count = $count->content;
3661
3662     # Force immediate send of count response
3663     my $mbc = $client->max_bundle_count;
3664     $client->max_bundle_count(1);
3665     $client->respond($count);
3666     $client->max_bundle_count($mbc);
3667
3668     while (my $hold = $req->recv) {
3669         $client->respond($hold->content) if $hold->content;
3670     }
3671
3672     $client->respond_complete;
3673 }
3674
3675
3676
3677
3678 __PACKAGE__->register_method(
3679     method        => 'uber_hold',
3680     authoritative => 1,
3681     api_name      => 'open-ils.circ.hold.details.retrieve'
3682 );
3683
3684 sub uber_hold {
3685     my($self, $client, $auth, $hold_id, $args) = @_;
3686     my $e = new_editor(authtoken=>$auth);
3687     $e->checkauth or return $e->event;
3688     return uber_hold_impl($e, $hold_id, $args);
3689 }
3690
3691 __PACKAGE__->register_method(
3692     method        => 'batch_uber_hold',
3693     authoritative => 1,
3694     stream        => 1,
3695     api_name      => 'open-ils.circ.hold.details.batch.retrieve'
3696 );
3697
3698 sub batch_uber_hold {
3699     my($self, $client, $auth, $hold_ids, $args) = @_;
3700     my $e = new_editor(authtoken=>$auth);
3701     $e->checkauth or return $e->event;
3702     $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3703     return undef;
3704 }
3705
3706 sub uber_hold_impl {
3707     my($e, $hold_id, $args) = @_;
3708     $args ||= {};
3709
3710     my $flesh_fields = ['current_copy', 'usr', 'notes'];
3711     push (@$flesh_fields, 'requestor') if $args->{include_requestor};
3712     push (@$flesh_fields, 'cancel_cause') if $args->{include_cancel_cause};
3713     push (@$flesh_fields, 'sms_carrier') if $args->{include_sms_carrier};
3714
3715     my $hold = $e->retrieve_action_hold_request([
3716         $hold_id,
3717         {flesh => 1, flesh_fields => {ahr => $flesh_fields}}
3718     ]) or return $e->event;
3719
3720     if($hold->usr->id ne $e->requestor->id) {
3721         # caller is asking for someone else's hold
3722         $e->allowed('VIEW_HOLD') or return $e->event;
3723         $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3724             [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3725
3726     } else {
3727         # caller is asking for own hold, but may not have permission to view staff notes
3728         unless($e->allowed('VIEW_HOLD')) {
3729             $hold->notes( # filter out any staff notes (unless marked as public)
3730                 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3731         }
3732     }
3733
3734     my $user = $hold->usr;
3735     $hold->usr($user->id);
3736
3737
3738     my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args);
3739
3740     flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3741     flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3742
3743     my $details = retrieve_hold_queue_status_impl($e, $hold);
3744     $hold->usr($user) if $args->{include_usr}; # re-flesh
3745
3746     my $resp = {
3747         hold    => $hold,
3748         bre_id  => $bre->id,
3749         ($copy     ? (copy           => $copy)     : ()),
3750         ($volume   ? (volume         => $volume)   : ()),
3751         ($issuance ? (issuance       => $issuance) : ()),
3752         ($part     ? (part           => $part)     : ()),
3753         ($args->{include_bre}  ?  (bre => $bre)    : ()),
3754         ($args->{suppress_mvr} ?  () : (mvr => $mvr)),
3755         %$details
3756     };
3757
3758     $resp->{copy}->location(
3759         $e->retrieve_asset_copy_location($resp->{copy}->location))
3760         if $resp->{copy} and $args->{flesh_acpl};
3761
3762     unless($args->{suppress_patron_details}) {
3763         my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3764         $resp->{patron_first}   = $user->first_given_name,
3765         $resp->{patron_last}    = $user->family_name,
3766         $resp->{patron_barcode} = $card->barcode,
3767         $resp->{patron_alias}   = $user->alias,
3768     };
3769
3770     return $resp;
3771 }
3772
3773
3774
3775 # -----------------------------------------------------
3776 # Returns the MVR object that represents what the
3777 # hold is all about
3778 # -----------------------------------------------------
3779 sub find_hold_mvr {
3780     my( $e, $hold, $args ) = @_;
3781
3782     my $tid;
3783     my $copy;
3784     my $volume;
3785     my $issuance;
3786     my $part;
3787     my $metarecord;
3788     my $no_mvr = $args->{suppress_mvr};
3789
3790     if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3791         $metarecord = $e->retrieve_metabib_metarecord($hold->target)
3792             or return $e->event;
3793         $tid = $metarecord->master_record;
3794
3795     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3796         $tid = $hold->target;
3797
3798     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3799         $volume = $e->retrieve_asset_call_number($hold->target)
3800             or return $e->event;
3801         $tid = $volume->record;
3802
3803     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3804         $issuance = $e->retrieve_serial_issuance([
3805             $hold->target,
3806             {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3807         ]) or return $e->event;
3808
3809         $tid = $issuance->subscription->record_entry;
3810
3811     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3812         $part = $e->retrieve_biblio_monograph_part([
3813             $hold->target
3814         ]) or return $e->event;
3815
3816         $tid = $part->record;
3817
3818     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY || $hold->hold_type eq OILS_HOLD_TYPE_RECALL || $hold->hold_type eq OILS_HOLD_TYPE_FORCE ) {
3819         $copy = $e->retrieve_asset_copy([
3820             $hold->target,
3821             {flesh => 1, flesh_fields => {acp => ['call_number']}}
3822         ]) or return $e->event;
3823
3824         $volume = $copy->call_number;
3825         $tid = $volume->record;
3826     }
3827
3828     if(!$copy and ref $hold->current_copy ) {
3829         $copy = $hold->current_copy;
3830         $hold->current_copy($copy->id) unless $args->{include_current_copy};
3831     }
3832
3833     if(!$volume and $copy) {
3834         $volume = $e->retrieve_asset_call_number($copy->call_number);
3835     }
3836
3837     # TODO return metarcord mvr for M holds
3838     my $title = $e->retrieve_biblio_record_entry($tid);
3839     return ( ($no_mvr) ? undef : $U->record_to_mvr($title), 
3840         $volume, $copy, $issuance, $part, $title, $metarecord);
3841 }
3842
3843 __PACKAGE__->register_method(
3844     method    => 'clear_shelf_cache',
3845     api_name  => 'open-ils.circ.hold.clear_shelf.get_cache',
3846     stream    => 1,
3847     signature => {
3848         desc => q/
3849             Returns the holds processed with the given cache key
3850         /
3851     }
3852 );
3853
3854 sub clear_shelf_cache {
3855     my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3856     my $e = new_editor(authtoken => $auth, xact => 1);
3857     return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3858
3859     $chunk_size ||= 25;
3860     $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3861
3862     my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3863
3864     if (!$hold_data) {
3865         $logger->info("no hold data found in cache"); # XXX TODO return event
3866         $e->rollback;
3867         return undef;
3868     }
3869
3870     my $maximum = 0;
3871     foreach (keys %$hold_data) {
3872         $maximum += scalar(@{ $hold_data->{$_} });
3873     }
3874     $client->respond({"maximum" => $maximum, "progress" => 0});
3875
3876     for my $action (sort keys %$hold_data) {
3877         while (@{$hold_data->{$action}}) {
3878             my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3879
3880             my $result_chunk = $e->json_query({
3881                 "select" => {
3882                     "acp" => ["barcode"],
3883                     "au" => [qw/
3884                         first_given_name second_given_name family_name alias
3885                     /],
3886                     "acn" => ["label"],
3887                     "acnp" => [{column => "label", alias => "prefix"}],
3888                     "acns" => [{column => "label", alias => "suffix"}],
3889                     "bre" => ["marc"],
3890                     "acpl" => ["name"],
3891                     "ahr" => ["id"]
3892                 },
3893                 "from" => {
3894                     "ahr" => {
3895                         "acp" => {
3896                             "field" => "id", "fkey" => "current_copy",
3897                             "join" => {
3898                                 "acn" => {
3899                                     "field" => "id", "fkey" => "call_number",
3900                                     "join" => {
3901                                         "bre" => {
3902                                             "field" => "id", "fkey" => "record"
3903                                         },
3904                                         "acnp" => {
3905                                             "field" => "id", "fkey" => "prefix"
3906                                         },
3907                                         "acns" => {
3908                                             "field" => "id", "fkey" => "suffix"
3909                                         }
3910                                     }
3911                                 },
3912                                 "acpl" => {"field" => "id", "fkey" => "location"}
3913                             }
3914                         },
3915                         "au" => {"field" => "id", "fkey" => "usr"}
3916                     }
3917                 },
3918                 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3919             }, {"substream" => 1}) or return $e->die_event;
3920
3921             $client->respond([
3922                 map {
3923                     +{"action" => $action, "hold_details" => $_}
3924                 } @$result_chunk
3925             ]);
3926         }
3927     }
3928
3929     $e->rollback;
3930     return undef;
3931 }
3932
3933
3934 __PACKAGE__->register_method(
3935     method    => 'clear_shelf_process',
3936     stream    => 1,
3937     api_name  => 'open-ils.circ.hold.clear_shelf.process',
3938     signature => {
3939         desc => q/
3940             1. Find all holds that have expired on the holds shelf
3941             2. Cancel the holds
3942             3. If a clear-shelf status is configured, put targeted copies into this status
3943             4. Divide copies into 3 groups: items to transit, items to reshelve, and items
3944                 that are needed for holds.  No subsequent action is taken on the holds
3945                 or items after grouping.
3946         /
3947     }
3948 );
3949
3950 sub clear_shelf_process {
3951     my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
3952
3953     my $e = new_editor(authtoken=>$auth);
3954     $e->checkauth or return $e->die_event;
3955     my $cache = OpenSRF::Utils::Cache->new('global');
3956
3957     $org_id ||= $e->requestor->ws_ou;
3958     $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
3959
3960     my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
3961
3962     my @hold_ids = $self->method_lookup(
3963         "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
3964     )->run($auth, $org_id, $match_copy);
3965
3966     $e->xact_begin;
3967
3968     my @holds;
3969     my @canceled_holds; # newly canceled holds
3970     $chunk_size ||= 25; # chunked status updates
3971     $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3972
3973     my $counter = 0;
3974     for my $hold_id (@hold_ids) {
3975
3976         $logger->info("Clear shelf processing hold $hold_id");
3977
3978         my $hold = $e->retrieve_action_hold_request([
3979             $hold_id, {
3980                 flesh => 1,
3981                 flesh_fields => {ahr => ['current_copy']}
3982             }
3983         ]);
3984
3985         if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
3986             $hold->cancel_time('now');
3987             $hold->cancel_cause(2); # Hold Shelf expiration
3988             $e->update_action_hold_request($hold) or return $e->die_event;
3989             push(@canceled_holds, $hold_id);
3990         }
3991
3992         my $copy = $hold->current_copy;
3993
3994         if($copy_status or $copy_status == 0) {
3995             # if a clear-shelf copy status is defined, update the copy
3996             $copy->status($copy_status);
3997             $copy->edit_date('now');
3998             $copy->editor($e->requestor->id);
3999             $e->update_asset_copy($copy) or return $e->die_event;
4000         }
4001
4002         push(@holds, $hold);
4003         $client->respond({maximum => int(scalar(@holds)), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
4004     }
4005
4006     if ($e->commit) {
4007
4008         my %cache_data = (
4009             hold => [],
4010             transit => [],
4011             shelf => [],
4012             pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
4013         );
4014
4015         for my $hold (@holds) {
4016
4017             my $copy = $hold->current_copy;
4018             my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
4019
4020             if($alt_hold and !$match_copy) {
4021
4022                 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
4023
4024             } elsif($copy->circ_lib != $e->requestor->ws_ou) {
4025
4026                 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
4027
4028             } else {
4029
4030                 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
4031             }
4032         }
4033
4034         my $cache_key = md5_hex(time . $$ . rand());
4035         $logger->info("clear_shelf_cache: storing under $cache_key");
4036         $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours.  configurable?
4037
4038         # tell the client we're done
4039         $client->respond_complete({cache_key => $cache_key});
4040
4041         # ------------
4042         # fire off the hold cancelation trigger and wait for response so don't flood the service
4043
4044         # refetch the holds to pick up the caclulated cancel_time,
4045         # which may be needed by Action/Trigger
4046         $e->xact_begin;
4047         my $updated_holds = [];
4048         $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
4049         $e->rollback;
4050
4051         $U->create_events_for_hook(
4052             'hold_request.cancel.expire_holds_shelf',
4053             $_, $org_id, undef, undef, 1) for @$updated_holds;
4054
4055     } else {
4056         # tell the client we're done
4057         $client->respond_complete;
4058     }
4059 }
4060
4061 # returns IDs for holds that are on the holds shelf but 
4062 # have had their pickup_libs change while on the shelf.
4063 sub pickup_lib_changed_on_shelf_holds {
4064     my $e = shift;
4065     my $org_id = shift;
4066     my $ignore_holds = shift;
4067     $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
4068
4069     my $query = {
4070         select => { alhr => ['id'] },
4071         from   => {
4072             alhr => {
4073                 acp => {
4074                     field => 'id',
4075                     fkey  => 'current_copy'
4076                 },
4077             }
4078         },
4079         where => {
4080             '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
4081             '+alhr' => {
4082                 capture_time     => { "!=" => undef },
4083                 fulfillment_time => undef,
4084                 current_shelf_lib => $org_id,
4085                 pickup_lib => {'!='  => {'+alhr' => 'current_shelf_lib'}}
4086             }
4087         }
4088     };
4089
4090     $query->{where}->{'+alhr'}->{id} =
4091         {'not in' => $ignore_holds} if @$ignore_holds;
4092
4093     my $hold_ids = $e->json_query($query);
4094     return [ map { $_->{id} } @$hold_ids ];
4095 }
4096
4097 __PACKAGE__->register_method(
4098     method    => 'usr_hold_summary',
4099     api_name  => 'open-ils.circ.holds.user_summary',
4100     signature => q/
4101         Returns a summary of holds statuses for a given user
4102     /
4103 );
4104
4105 sub usr_hold_summary {
4106     my($self, $conn, $auth, $user_id) = @_;
4107
4108     my $e = new_editor(authtoken=>$auth);
4109     $e->checkauth or return $e->event;
4110     $e->allowed('VIEW_HOLD') or return $e->event;
4111
4112     my $holds = $e->search_action_hold_request(
4113         {
4114             usr =>  $user_id ,
4115             fulfillment_time => undef,
4116             cancel_time      => undef,
4117         }
4118     );
4119
4120     my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
4121     $summary{_hold_status($e, $_)} += 1 for @$holds;
4122     return \%summary;
4123 }
4124
4125
4126
4127 __PACKAGE__->register_method(
4128     method    => 'hold_has_copy_at',
4129     api_name  => 'open-ils.circ.hold.has_copy_at',
4130     signature => {
4131         desc   =>
4132                 'Returns the ID of the found copy and name of the shelving location if there is ' .
4133                 'an available copy at the specified org unit.  Returns empty hash otherwise.  '   .
4134                 'The anticipated use for this method is to determine whether an item is '         .
4135                 'available at the library where the user is placing the hold (or, alternatively, '.
4136                 'at the pickup library) to encourage bypassing the hold placement and just '      .
4137                 'checking out the item.' ,
4138         params => [
4139             { desc => 'Authentication Token', type => 'string' },
4140             { desc => 'Method Arguments.  Options include: hold_type, hold_target, org_unit.  '
4141                     . 'hold_type is the hold type code (T, V, C, M, ...).  '
4142                     . 'hold_target is the identifier of the hold target object.  '
4143                     . 'org_unit is org unit ID.',
4144               type => 'object'
4145             }
4146         ],
4147         return => {
4148             desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
4149             type => 'object'
4150         }
4151     }
4152 );
4153
4154 sub hold_has_copy_at {
4155     my($self, $conn, $auth, $args) = @_;
4156
4157     my $e = new_editor(authtoken=>$auth);
4158     $e->checkauth or return $e->event;
4159
4160     my $hold_type   = $$args{hold_type};
4161     my $hold_target = $$args{hold_target};
4162     my $org_unit    = $$args{org_unit};
4163
4164     my $query = {
4165         select => {acp => ['id'], acpl => ['name']},
4166         from   => {
4167             acp => {
4168                 acpl => {
4169                     field => 'id',
4170                     filter => { holdable => 't', deleted => 'f' },
4171                     fkey => 'location'
4172                 },
4173                 ccs  => {field => 'id', filter => { holdable => 't'}, fkey => 'status'  }
4174             }
4175         },
4176         where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
4177         limit => 1
4178     };
4179
4180     if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
4181
4182         $query->{where}->{'+acp'}->{id} = $hold_target;
4183
4184     } elsif($hold_type eq 'V') {
4185
4186         $query->{where}->{'+acp'}->{call_number} = $hold_target;
4187
4188     } elsif($hold_type eq 'P') {
4189
4190         $query->{from}->{acp}->{acpm} = {
4191             field  => 'target_copy',
4192             fkey   => 'id',
4193             filter => {part => $hold_target},
4194         };
4195
4196     } elsif($hold_type eq 'I') {
4197
4198         $query->{from}->{acp}->{sitem} = {
4199             field  => 'unit',
4200             fkey   => 'id',
4201             filter => {issuance => $hold_target},
4202         };
4203
4204     } elsif($hold_type eq 'T') {
4205
4206         $query->{from}->{acp}->{acn} = {
4207             field  => 'id',
4208             fkey   => 'call_number',
4209             'join' => {
4210                 bre => {
4211                     field  => 'id',
4212                     filter => {id => $hold_target},
4213                     fkey   => 'record'
4214                 }
4215             }
4216         };
4217
4218     } else {
4219
4220         $query->{from}->{acp}->{acn} = {
4221             field => 'id',
4222             fkey  => 'call_number',
4223             join  => {
4224                 bre => {
4225                     field => 'id',
4226                     fkey  => 'record',
4227                     join  => {
4228                         mmrsm => {
4229                             field  => 'source',
4230                             fkey   => 'id',
4231                             filter => {metarecord => $hold_target},
4232                         }
4233                     }
4234                 }
4235             }
4236         };
4237     }
4238
4239     my $res = $e->json_query($query)->[0] or return {};
4240     return {copy => $res->{id}, location => $res->{name}} if $res;
4241 }
4242
4243
4244 # returns true if the user already has an item checked out
4245 # that could be used to fulfill the requested hold.
4246 sub hold_item_is_checked_out {
4247     my($e, $user_id, $hold_type, $hold_target) = @_;
4248
4249     my $query = {
4250         select => {acp => ['id']},
4251         from   => {acp => {}},
4252         where  => {
4253             '+acp' => {
4254                 id => {
4255                     in => { # copies for circs the user has checked out
4256                         select => {circ => ['target_copy']},
4257                         from   => 'circ',
4258                         where  => {
4259                             usr => $user_id,
4260                             checkin_time => undef,
4261                             '-or' => [
4262                                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
4263                                 {stop_fines => undef}
4264                             ],
4265                         }
4266                     }
4267                 }
4268             }
4269         },
4270         limit => 1
4271     };
4272
4273     if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
4274
4275         $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
4276
4277     } elsif($hold_type eq 'V') {
4278
4279         $query->{where}->{'+acp'}->{call_number} = $hold_target;
4280
4281      } elsif($hold_type eq 'P') {
4282
4283         $query->{from}->{acp}->{acpm} = {
4284             field  => 'target_copy',
4285             fkey   => 'id',
4286             filter => {part => $hold_target},
4287         };
4288
4289      } elsif($hold_type eq 'I') {
4290
4291         $query->{from}->{acp}->{sitem} = {
4292             field  => 'unit',
4293             fkey   => 'id',
4294             filter => {issuance => $hold_target},
4295         };
4296
4297     } elsif($hold_type eq 'T') {
4298
4299         $query->{from}->{acp}->{acn} = {
4300             field  => 'id',
4301             fkey   => 'call_number',
4302             'join' => {
4303                 bre => {
4304                     field  => 'id',
4305                     filter => {id => $hold_target},
4306                     fkey   => 'record'
4307                 }
4308             }
4309         };
4310
4311     } else {
4312
4313         $query->{from}->{acp}->{acn} = {
4314             field => 'id',
4315             fkey => 'call_number',
4316             join => {
4317                 bre => {
4318                     field => 'id',
4319                     fkey => 'record',
4320                     join => {
4321                         mmrsm => {
4322                             field => 'source',
4323                             fkey => 'id',
4324                             filter => {metarecord => $hold_target},
4325                         }
4326                     }
4327                 }
4328             }
4329         };
4330     }
4331
4332     return $e->json_query($query)->[0];
4333 }
4334
4335 __PACKAGE__->register_method(
4336     method    => 'change_hold_title',
4337     api_name  => 'open-ils.circ.hold.change_title',
4338     signature => {
4339         desc => q/
4340             Updates all title level holds targeting the specified bibs to point a new bib./,
4341         params => [
4342             { desc => 'Authentication Token', type => 'string' },
4343             { desc => 'New Target Bib Id',    type => 'number' },
4344             { desc => 'Old Target Bib Ids',   type => 'array'  },
4345         ],
4346         return => { desc => '1 on success' }
4347     }
4348 );
4349
4350 __PACKAGE__->register_method(
4351     method    => 'change_hold_title_for_specific_holds',
4352     api_name  => 'open-ils.circ.hold.change_title.specific_holds',
4353     signature => {
4354         desc => q/
4355             Updates specified holds to target new bib./,
4356         params => [
4357             { desc => 'Authentication Token', type => 'string' },
4358             { desc => 'New Target Bib Id',    type => 'number' },
4359             { desc => 'Holds Ids for holds to update',   type => 'array'  },
4360         ],
4361         return => { desc => '1 on success' }
4362     }
4363 );
4364
4365
4366 sub change_hold_title {
4367     my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4368
4369     my $e = new_editor(authtoken=>$auth, xact=>1);
4370     return $e->die_event unless $e->checkauth;
4371
4372     my $holds = $e->search_action_hold_request(
4373         [
4374             {
4375                 capture_time     => undef,
4376                 cancel_time      => undef,
4377                 fulfillment_time => undef,
4378                 hold_type        => 'T',
4379                 target           => $bib_ids
4380             },
4381             {
4382                 flesh        => 1,
4383                 flesh_fields => { ahr => ['usr'] }
4384             }
4385         ],
4386         { substream => 1 }
4387     );
4388
4389     for my $hold (@$holds) {
4390         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4391         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4392         $hold->target( $new_bib_id );
4393         $e->update_action_hold_request($hold) or return $e->die_event;
4394     }
4395
4396     $e->commit;
4397
4398     _reset_hold($self, $e->requestor, $_) for @$holds;
4399
4400     return 1;
4401 }
4402
4403 sub change_hold_title_for_specific_holds {
4404     my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4405
4406     my $e = new_editor(authtoken=>$auth, xact=>1);
4407     return $e->die_event unless $e->checkauth;
4408
4409     my $holds = $e->search_action_hold_request(
4410         [
4411             {
4412                 capture_time     => undef,
4413                 cancel_time      => undef,
4414                 fulfillment_time => undef,
4415                 hold_type        => 'T',
4416                 id               => $hold_ids
4417             },
4418             {
4419                 flesh        => 1,
4420                 flesh_fields => { ahr => ['usr'] }
4421             }
4422         ],
4423         { substream => 1 }
4424     );
4425
4426     for my $hold (@$holds) {
4427         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4428         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4429         $hold->target( $new_bib_id );
4430         $e->update_action_hold_request($hold) or return $e->die_event;
4431     }
4432
4433     $e->commit;
4434
4435     _reset_hold($self, $e->requestor, $_) for @$holds;
4436
4437     return 1;
4438 }
4439
4440 __PACKAGE__->register_method(
4441     method    => 'rec_hold_count',
4442     api_name  => 'open-ils.circ.bre.holds.count',
4443     signature => {
4444         desc => q/Returns the total number of holds that target the
4445             selected bib record or its associated copies and call_numbers/,
4446         params => [
4447             { desc => 'Bib ID', type => 'number' },
4448             { desc => q/Optional arguments.  Supported arguments include:
4449                 "pickup_lib_descendant" -> limit holds to those whose pickup
4450                 library is equal to or is a child of the provided org unit/,
4451                 type => 'object'
4452             }
4453         ],
4454         return => {desc => 'Hold count', type => 'number'}
4455     }
4456 );
4457
4458 __PACKAGE__->register_method(
4459     method    => 'rec_hold_count',
4460     api_name  => 'open-ils.circ.mmr.holds.count',
4461     signature => {
4462         desc => q/Returns the total number of holds that target the
4463             selected metarecord or its associated copies, call_numbers, and bib records/,
4464         params => [
4465             { desc => 'Metarecord ID', type => 'number' },
4466         ],
4467         return => {desc => 'Hold count', type => 'number'}
4468     }
4469 );
4470
4471 # XXX Need to add type I holds to these counts
4472 sub rec_hold_count {
4473     my($self, $conn, $target_id, $args) = @_;
4474     $args ||= {};
4475
4476     my $mmr_join = {
4477         mmrsm => {
4478             field => 'source',
4479             fkey => 'id',
4480             filter => {metarecord => $target_id}
4481         }
4482     };
4483
4484     my $bre_join = {
4485         bre => {
4486             field => 'id',
4487             filter => { id => $target_id },
4488             fkey => 'record'
4489         }
4490     };
4491
4492     if($self->api_name =~ /mmr/) {
4493         delete $bre_join->{bre}->{filter};
4494         $bre_join->{bre}->{join} = $mmr_join;
4495     }
4496
4497     my $cn_join = {
4498         acn => {
4499             field => 'id',
4500             fkey => 'call_number',
4501             join => $bre_join
4502         }
4503     };
4504
4505     my $query = {
4506         select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4507         from => 'ahr',
4508         where => {
4509             '+ahr' => {
4510                 cancel_time => undef,
4511                 fulfillment_time => undef,
4512                 '-or' => [
4513                     {
4514                         '-and' => {
4515                             hold_type => [qw/C F R/],
4516                             target => {
4517                                 in => {
4518                                     select => {acp => ['id']},
4519                                     from => { acp => $cn_join }
4520                                 }
4521                             }
4522                         }
4523                     },
4524                     {
4525                         '-and' => {
4526                             hold_type => 'V',
4527                             target => {
4528                                 in => {
4529                                     select => {acn => ['id']},
4530                                     from => {acn => $bre_join}
4531                                 }
4532                             }
4533                         }
4534                     },
4535                     {
4536                         '-and' => {
4537                             hold_type => 'P',
4538                             target => {
4539                                 in => {
4540                                     select => {bmp => ['id']},
4541                                     from => {bmp => $bre_join}
4542                                 }
4543                             }
4544                         }
4545                     },
4546                     {
4547                         '-and' => {
4548                             hold_type => 'T',
4549                             target => $target_id
4550                         }
4551                     }
4552                 ]
4553             }
4554         }
4555     };
4556
4557     if($self->api_name =~ /mmr/) {
4558         $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4559             '-and' => {
4560                 hold_type => 'T',
4561                 target => {
4562                     in => {
4563                         select => {bre => ['id']},
4564                         from => {bre => $mmr_join}
4565                     }
4566                 }
4567             }
4568         };
4569
4570         $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4571             '-and' => {
4572                 hold_type => 'M',
4573                 target => $target_id
4574             }
4575         };
4576     }
4577
4578
4579     if (my $pld = $args->{pickup_lib_descendant}) {
4580
4581         my $top_ou = new_editor()->search_actor_org_unit(
4582             {parent_ou => undef}
4583         )->[0]; # XXX Assumes single root node. Not alone in this...
4584
4585         $query->{where}->{'+ahr'}->{pickup_lib} = {
4586             in => {
4587                 select  => {aou => [{ 
4588                     column => 'id', 
4589                     transform => 'actor.org_unit_descendants', 
4590                     result_field => 'id' 
4591                 }]},
4592                 from    => 'aou',
4593                 where   => {id => $pld}
4594             }
4595         } if ($pld != $top_ou->id);
4596     }
4597
4598     # To avoid Internal Server Errors, we get an editor, then run the
4599     # query and check the result.  If anything fails, we'll return 0.
4600     my $result = 0;
4601     if (my $e = new_editor()) {
4602         my $query_result = $e->json_query($query);
4603         if ($query_result && @{$query_result}) {
4604             $result = $query_result->[0]->{count}
4605         }
4606     }
4607
4608     return $result;
4609 }
4610
4611 # A helper function to calculate a hold's expiration time at a given
4612 # org_unit. Takes the org_unit as an argument and returns either the
4613 # hold expire time as an ISO8601 string or undef if there is no hold
4614 # expiration interval set for the subject ou.
4615 sub calculate_expire_time
4616 {
4617     my $ou = shift;
4618     my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4619     if($interval) {
4620         my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
4621         return $U->epoch2ISO8601($date->epoch);
4622     }
4623     return undef;
4624 }
4625
4626
4627 __PACKAGE__->register_method(
4628     method    => 'mr_hold_filter_attrs',
4629     api_name  => 'open-ils.circ.mmr.holds.filters',
4630     authoritative => 1,
4631     stream => 1,
4632     signature => {
4633         desc => q/
4634             Returns the set of available formats and languages for the
4635             constituent records of the provided metarcord.
4636             If an array of hold IDs is also provided, information about
4637             each is returned as well.  This information includes:
4638             1. a slightly easier to read version of holdable_formats
4639             2. attributes describing the set of format icons included
4640                in the set of desired, constituent records.
4641         /,
4642         params => [
4643             {desc => 'Metarecord ID', type => 'number'},
4644             {desc => 'Context Org ID', type => 'number'},
4645             {desc => 'Hold ID List', type => 'array'},
4646         ],
4647         return => {
4648             desc => q/
4649                 Stream of objects.  The first will have a 'metarecord' key
4650                 containing non-hold-specific metarecord information, subsequent
4651                 responses will contain a 'hold' key containing hold-specific
4652                 information
4653             /, 
4654             type => 'object'
4655         }
4656     }
4657 );
4658
4659 sub mr_hold_filter_attrs { 
4660     my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4661     my $e = new_editor();
4662
4663     # by default, return MR / hold attributes for all constituent
4664     # records with holdable copies.  If there is a hard boundary,
4665     # though, limit to records with copies within the boundary,
4666     # since anything outside the boundary can never be held.
4667     my $org_depth = 0;
4668     if ($org_id) {
4669         $org_depth = $U->ou_ancestor_setting_value(
4670             $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4671     }
4672
4673     # get all org-scoped records w/ holdable copies for this metarecord
4674     my ($bre_ids) = $self->method_lookup(
4675         'open-ils.circ.holds.metarecord.filtered_records')->run(
4676             $mr_id, undef, $org_id, $org_depth);
4677
4678     my $item_lang_attr = 'item_lang'; # configurable?
4679     my $format_attr = $e->retrieve_config_global_flag(
4680         'opac.metarecord.holds.format_attr')->value;
4681
4682     # helper sub for fetching ccvms for a batch of record IDs
4683     sub get_batch_ccvms {
4684         my ($e, $attr, $bre_ids) = @_;
4685         return [] unless $bre_ids and @$bre_ids;
4686         my $vals = $e->search_metabib_record_attr_flat({
4687             attr => $attr,
4688             id => $bre_ids
4689         });
4690         return [] unless @$vals;
4691         return $e->search_config_coded_value_map({
4692             ctype => $attr,
4693             code => [map {$_->value} @$vals]
4694         });
4695     }
4696
4697     my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4698     my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4699
4700     $client->respond({
4701         metarecord => {
4702             id => $mr_id,
4703             formats => $formats,
4704             langs => $langs
4705         }
4706     });
4707
4708     return unless $hold_ids;
4709     my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4710     $icon_attr = $icon_attr ? $icon_attr->value : '';
4711
4712     for my $hold_id (@$hold_ids) {
4713         my $hold = $e->retrieve_action_hold_request($hold_id) 
4714             or return $e->event;
4715
4716         next unless $hold->hold_type eq 'M';
4717
4718         my $resp = {
4719             hold => {
4720                 id => $hold_id,
4721                 formats => [],
4722                 langs => []
4723             }
4724         };
4725
4726         # collect the ccvm's for the selected formats / language
4727         # (i.e. the holdable formats) on the MR.
4728         # this assumes a two-key structure for format / language,
4729         # though no assumption is made about the keys themselves.
4730         my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4731         my $lang_vals = [];
4732         my $format_vals = [];
4733         for my $val (values %$hformats) {
4734             # val is either a single ccvm or an array of them
4735             $val = [$val] unless ref $val eq 'ARRAY';
4736             for my $node (@$val) {
4737                 push (@$lang_vals, $node->{_val})   
4738                     if $node->{_attr} eq $item_lang_attr; 
4739                 push (@$format_vals, $node->{_val})   
4740                     if $node->{_attr} eq $format_attr;
4741             }
4742         }
4743
4744         # fetch the ccvm's for consistency with the {metarecord} blob
4745         $resp->{hold}{formats} = $e->search_config_coded_value_map({
4746             ctype => $format_attr, code => $format_vals});
4747         $resp->{hold}{langs} = $e->search_config_coded_value_map({
4748             ctype => $item_lang_attr, code => $lang_vals});
4749
4750         # find all of the bib records within this metarcord whose 
4751         # format / language match the holdable formats on the hold
4752         my ($bre_ids) = $self->method_lookup(
4753             'open-ils.circ.holds.metarecord.filtered_records')->run(
4754                 $hold->target, $hold->holdable_formats, 
4755                 $hold->selection_ou, $hold->selection_depth);
4756
4757         # now find all of the 'icon' attributes for the records
4758         $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4759         $client->respond($resp);
4760     }
4761
4762     return;
4763 }
4764
4765 __PACKAGE__->register_method(
4766     method        => "copy_has_holds_count",
4767     api_name      => "open-ils.circ.copy.has_holds_count",
4768     authoritative => 1,
4769     signature     => {
4770         desc => q/
4771             Returns the number of holds a paticular copy has
4772         /,
4773         params => [
4774             { desc => 'Authentication Token', type => 'string'},
4775             { desc => 'Copy ID', type => 'number'}
4776         ],
4777         return => {
4778             desc => q/
4779                 Simple count value
4780             /,
4781             type => 'number'
4782         }
4783     }
4784 );
4785
4786 sub copy_has_holds_count {
4787     my( $self, $conn, $auth, $copyid ) = @_;
4788     my $e = new_editor(authtoken=>$auth);
4789     return $e->event unless $e->checkauth;
4790
4791     if( $copyid && $copyid > 0 ) {
4792         my $meth = 'retrieve_action_has_holds_count';
4793         my $data = $e->$meth($copyid);
4794         if($data){
4795                 return $data->count();
4796         }
4797     }
4798     return 0;
4799 }
4800
4801 __PACKAGE__->register_method(
4802     method    => "retrieve_holds_by_usr_notify_value_staff",
4803     api_name  => "open-ils.circ.holds.retrieve_by_notify_staff",
4804     signature => {
4805         desc   => "Retrieve the hold, for the specified user using the notify value.  $ses_is_req_note",
4806         params => [
4807             { desc => 'Authentication token', type => 'string' },
4808             { desc => 'User ID',              type => 'number' },
4809             { desc => 'notify value',         type => 'string' },
4810             { desc => 'notify_type',          type => 'string' }
4811         ],
4812         return => {
4813             desc => 'Hold objects with transits attached, event on error',
4814         }
4815     }
4816 );
4817
4818 sub retrieve_holds_by_usr_notify_value_staff {
4819     
4820     my($self, $conn, $auth, $usr_id, $contact, $cType) = @_;
4821
4822     my $e = new_editor(authtoken=>$auth);
4823     $e->checkauth or return $e->event;
4824
4825     if ($e->requestor->id != $usr_id){
4826         $e->allowed('VIEW_HOLD') or return $e->event;
4827     }
4828
4829     my $q = {
4830         "select" => { "ahr" => ["id", "sms_notify", "phone_notify", "email_notify", "sms_carrier"]},
4831         "from" => "ahr",
4832         "where" => {
4833             "usr"          =>      $usr_id,
4834             "capture_time" =>      undef,
4835             "cancel_time"  =>      undef,
4836             "fulfillment_time" =>  undef,
4837         }
4838     };
4839
4840     if ($cType eq "day_phone" or $cType eq "evening_phone" or
4841         $cType eq "other_phone" or $cType eq "default_phone"){
4842             $q->{where}->{"-not"} = [
4843                 { "phone_notify" => { "=" => $contact} },
4844                 { "phone_notify" => { "<>" => undef } }
4845             ];
4846     }
4847
4848
4849     if ($cType eq "default_sms") {
4850         $q->{where}->{"-not"} = [
4851             { "sms_notify" => { "=" => $contact} },
4852             { "sms_notify" => { "<>" => undef } }
4853         ];
4854     }
4855
4856     if ($cType eq "default_sms_carrier_id") {
4857         $q->{where}->{"-not"} = [
4858             { "sms_carrier" => { "=" => int($contact)} },
4859             { "sms_carrier" => { "<>" => undef } }
4860         ];
4861     }
4862
4863     if ($cType =~ /notify/){
4864         # this is was notification pref change
4865         # we find all unfulfilled holds that match have that pref
4866         my $optr = $contact == 1 ? "<>" : "="; # unless it's email, true val means we want to query for not null
4867         my $conj = $optr eq '=' ? '-or' : '-and';
4868         if ($cType =~ /sms/) {
4869             $q->{where}->{$conj} = [ { sms_notify => { $optr => undef } }, { sms_notify => { $optr => '' } } ];
4870         }
4871         if ($cType =~ /phone/) {
4872             $q->{where}->{$conj} = [ { phone_notify => { $optr => undef } }, { phone_notify => { $optr => '' } } ];
4873         }
4874         if ($cType =~ /email/) {
4875             if ($contact) {
4876                 $q->{where}->{'+ahr'} = 'email_notify';
4877             } else {
4878                 $q->{where}->{'-not'} = {'+ahr' => 'email_notify'};
4879             }
4880         }
4881     }
4882
4883     my $holds = $e->json_query($q);
4884     #$hold_ids = [ map { $_->{id} } @$hold_ids ];
4885
4886     return $holds;
4887 }
4888
4889 __PACKAGE__->register_method(
4890     method    => "batch_update_holds_by_value_staff",
4891     api_name  => "open-ils.circ.holds.batch_update_holds_by_notify_staff",
4892     signature => {
4893         desc   => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
4894         params => [
4895             { desc => 'Authentication token', type => 'string' },
4896             { desc => 'User ID',              type => 'number' },
4897             { desc => 'Hold IDs',             type => 'array'  },
4898             { desc => 'old notify value',     type => 'string' },
4899             { desc => 'new notify value',     type => 'string' },
4900             { desc => 'field name',           type => 'string' },
4901             { desc => 'SMS carrier ID',       type => 'number' }
4902
4903         ],
4904         return => {
4905             desc => 'Hold objects with transits attached, event on error',
4906         }
4907     }
4908 );
4909
4910 sub batch_update_holds_by_value_staff {
4911     my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType, $carrierId) = @_;
4912
4913     my $e = new_editor(authtoken=>$auth, xact=>1);
4914     $e->checkauth or return $e->event;
4915     if ($e->requestor->id != $usr_id){
4916         $e->allowed('UPDATE_HOLD') or return $e->event;
4917     }
4918
4919     my @success;
4920     for my $id (@$hold_ids) {
4921         
4922         my $hold = $e->retrieve_action_hold_request($id);
4923
4924         if ($cType eq "day_phone" or $cType eq "evening_phone" or
4925             $cType eq "other_phone" or $cType eq "default_phone") {
4926
4927             if ($newval eq '') {
4928                 $hold->clear_phone_notify();
4929             }
4930             else {
4931                 $hold->phone_notify($newval);
4932             }
4933         }
4934         
4935         if ($cType eq "default_sms"){
4936             if ($newval eq '') {
4937                 $hold->clear_sms_notify();
4938                 $hold->clear_sms_carrier(); # TODO: prevent orphan sms_carrier, via db trigger
4939             }
4940             else {
4941                 $hold->sms_notify($newval);
4942                 $hold->sms_carrier($carrierId);
4943             }
4944
4945         }
4946
4947         if ($cType eq "default_sms_carrier_id") {
4948             $hold->sms_carrier($newval);
4949         }
4950
4951         if ($cType =~ /notify/){
4952             # this is a notification pref change
4953             if ($cType =~ /email/) { $hold->email_notify($newval); }
4954             if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
4955             if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
4956             # the other case, where x_notify is changed to true,
4957             # is covered by an actual value being assigned
4958         }
4959
4960         $e->update_action_hold_request($hold) or return $e->die_event;
4961         push @success, $id;
4962     }
4963
4964     #$e->disconnect;
4965     $e->commit; #unless $U->event_code($res);
4966     return \@success;
4967
4968 }
4969
4970
4971 __PACKAGE__->register_method(
4972     method    => "retrieve_holds_by_usr_with_notify",
4973     api_name  => "open-ils.circ.holds.retrieve.by_usr.with_notify",
4974     signature => {
4975         desc   => "Retrieve the hold, for the specified user using the notify value.  $ses_is_req_note",
4976         params => [
4977             { desc => 'Authentication token', type => 'string' },
4978             { desc => 'User ID',              type => 'number' },
4979         ],
4980         return => {
4981             desc => 'Lists of holds with notification values, event on error',
4982         }
4983     }
4984 );
4985
4986 sub retrieve_holds_by_usr_with_notify {
4987     
4988     my($self, $conn, $auth, $usr_id) = @_;
4989
4990     my $e = new_editor(authtoken=>$auth);
4991     $e->checkauth or return $e->event;
4992
4993     if ($e->requestor->id != $usr_id){
4994         $e->allowed('VIEW_HOLD') or return $e->event;
4995     }
4996
4997     my $q = {
4998         "select" => { "ahr" => ["id", "phone_notify", "email_notify", "sms_carrier", "sms_notify"]},
4999         "from" => "ahr",
5000         "where" => {
5001             "usr"          =>      $usr_id,
5002             "capture_time" =>      undef,
5003             "cancel_time"  =>      undef,
5004             "fulfillment_time" =>  undef,
5005         }
5006     };
5007
5008     my $holds = $e->json_query($q);
5009     return $holds;
5010 }
5011
5012 __PACKAGE__->register_method(
5013     method    => "batch_update_holds_by_value",
5014     api_name  => "open-ils.circ.holds.batch_update_holds_by_notify",
5015     signature => {
5016         desc   => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
5017         params => [
5018             { desc => 'Authentication token', type => 'string' },
5019             { desc => 'User ID',              type => 'number' },
5020             { desc => 'Hold IDs',             type => 'array'  },
5021             { desc => 'old notify value',     type => 'string' },
5022             { desc => 'new notify value',     type => 'string' },
5023             { desc => 'notify_type',          type => 'string' }
5024         ],
5025         return => {
5026             desc => 'Hold objects with transits attached, event on error',
5027         }
5028     }
5029 );
5030
5031 sub batch_update_holds_by_value {
5032     my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType) = @_;
5033
5034     my $e = new_editor(authtoken=>$auth, xact=>1);
5035     $e->checkauth or return $e->event;
5036     if ($e->requestor->id != $usr_id){
5037         $e->allowed('UPDATE_HOLD') or return $e->event;
5038     }
5039
5040     my @success;
5041     for my $id (@$hold_ids) {
5042         
5043         my $hold = $e->retrieve_action_hold_request(int($id));
5044
5045         if ($cType eq "day_phone" or $cType eq "evening_phone" or
5046             $cType eq "other_phone" or $cType eq "default_phone") {
5047             # change phone number value on hold
5048             $hold->phone_notify($newval);
5049         }
5050         if ($cType eq "default_sms") {
5051             # change SMS number value on hold
5052             $hold->sms_notify($newval);
5053         }
5054
5055         if ($cType eq "default_sms_carrier_id") {
5056             $hold->sms_carrier(int($newval));
5057         }
5058
5059         if ($cType =~ /notify/){
5060             # this is a notification pref change
5061             if ($cType =~ /email/) { $hold->email_notify($newval); }
5062             if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5063             if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5064             # the other case, where x_notify is changed to true,
5065             # is covered by an actual value being assigned
5066         }
5067
5068         $e->update_action_hold_request($hold) or return $e->die_event;
5069         push @success, $id;
5070     }
5071
5072     #$e->disconnect;
5073     $e->commit; #unless $U->event_code($res);
5074     return \@success;
5075 }
5076
5077 __PACKAGE__->register_method(
5078     method        => "hold_metadata",
5079     api_name      => "open-ils.circ.hold.get_metadata",
5080     authoritative => 1,
5081     stream => 1,
5082     signature     => {
5083         desc => q/
5084             Returns a stream of objects containing whatever bib, 
5085             volume, etc. data is available to the specific hold 
5086             type and target.
5087         /,
5088         params => [
5089             {desc => 'Hold Type', type => 'string'},
5090             {desc => 'Hold Target(s)', type => 'number or array'},
5091             {desc => 'Context org unit (optional)', type => 'number'}
5092         ],
5093         return => {
5094             desc => q/
5095                 Stream of hold metadata objects.
5096             /,
5097             type => 'object'
5098         }
5099     }
5100 );
5101
5102
5103 sub hold_metadata {
5104     my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
5105
5106     $hold_targets = [$hold_targets] unless ref $hold_targets;
5107
5108     my $e = new_editor();
5109     for my $target (@$hold_targets) {
5110
5111         # create a dummy hold for find_hold_mvr
5112         my $hold = Fieldmapper::action::hold_request->new;
5113         $hold->hold_type($hold_type);
5114         $hold->target($target);
5115
5116         my (undef, $volume, $copy, $issuance, $part, $bre, $metarecord) = 
5117             find_hold_mvr($e, $hold, {suppress_mvr => 1});
5118
5119         $bre->clear_marc; # avoid bulk
5120
5121         my $meta = {
5122             target => $target,
5123             copy => $copy,
5124             volume => $volume,
5125             issuance => $issuance,
5126             part => $part,
5127             parts => [],
5128             bibrecord => $bre,
5129             metarecord => $metarecord,
5130             metarecord_filters => {}
5131         };
5132
5133         # If this is a bib hold or metarecord hold, also return the
5134         # available set of MR filters (AKA "Holdable Formats") for the
5135         # hold.  For bib holds these may be used to upgrade the hold
5136         # from a bib to metarecord hold.
5137         if ($hold_type eq 'T') {
5138             my $map = $e->search_metabib_metarecord_source_map(
5139                 {source => $meta->{bibrecord}->id})->[0];
5140
5141             if ($map) {
5142                 $meta->{metarecord} = 
5143                     $e->retrieve_metabib_metarecord($map->metarecord);
5144             }
5145
5146             # Also fetch the available parts for bib-level holds.
5147             $meta->{parts} = $e->search_biblio_monograph_part(
5148                 [
5149                     {record => $bre->id, deleted => 'f'},
5150                     {order_by => {bmp => 'label_sortkey'}}
5151                 ]
5152             );
5153         }
5154
5155         if ($meta->{metarecord}) {
5156
5157             my ($filters) = 
5158                 $self->method_lookup('open-ils.circ.mmr.holds.filters')
5159                     ->run($meta->{metarecord}->id, $org_id);
5160
5161             if ($filters) {
5162                 $meta->{metarecord_filters} = $filters->{metarecord};
5163             }
5164         }
5165
5166         $client->respond($meta);
5167     }
5168
5169     return undef;
5170 }
5171
5172 1;