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