]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
LP1615805 No inputs after submit in patron search (AngularJS)
[working/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}
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                             "ccs" => {
2064                                 "field" => "id",
2065                                 "fkey" => "status",
2066                                 "filter" => {
2067                                     "holdable" => "t",
2068                                     "is_available" => "t"
2069                                 }
2070                             }
2071                         }
2072                     }
2073                 }
2074             },
2075             "where" => {
2076                 "+ahr" => {
2077                     "capture_time" => undef,
2078                     "cancel_time" => undef,
2079                     "-or" => [
2080                         {"expire_time" => undef },
2081                         {"expire_time" => {">" => "now"}}
2082                     ]
2083                 }
2084             },
2085             (@$sort ? (order_by => $sort) : ()),
2086             ($$params{limit} ? (limit => $$params{limit}) : ()),
2087             ($$params{offset} ? (offset => $$params{offset}) : ())
2088         }, {"substream" => 1}
2089     ) or return $e->die_event;
2090
2091     $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
2092
2093     my @chunk;
2094     for my $hid (@$holds_ids) {
2095         push @chunk, $e->retrieve_action_hold_request([
2096             $hid->{"id"}, {
2097                 "flesh" => 3,
2098                 "flesh_fields" => {
2099                     "ahr" => ["usr", "current_copy"],
2100                     "au"  => ["card"],
2101                     "acp" => ["location", "call_number", "parts"],
2102                     "acn" => ["record","prefix","suffix"]
2103                 }
2104             }
2105         ]);
2106
2107         if (@chunk >= $$params{chunk_size}) {
2108             $client->respond( \@chunk );
2109             @chunk = ();
2110         }
2111     }
2112     $client->respond_complete( \@chunk ) if (@chunk);
2113     $e->disconnect;
2114     return undef;
2115 }
2116
2117
2118
2119 __PACKAGE__->register_method(
2120     method        => 'fetch_hold_notify',
2121     api_name      => 'open-ils.circ.hold_notification.retrieve_by_hold',
2122     authoritative => 1,
2123     signature     => q/
2124 Returns a list of hold notification objects based on hold id.
2125 @param authtoken The loggin session key
2126 @param holdid The id of the hold whose notifications we want to retrieve
2127 @return An array of hold notification objects, event on error.
2128 /
2129 );
2130
2131 sub fetch_hold_notify {
2132     my( $self, $conn, $authtoken, $holdid ) = @_;
2133     my( $requestor, $evt ) = $U->checkses($authtoken);
2134     return $evt if $evt;
2135     my ($hold, $patron);
2136     ($hold, $evt) = $U->fetch_hold($holdid);
2137     return $evt if $evt;
2138     ($patron, $evt) = $U->fetch_user($hold->usr);
2139     return $evt if $evt;
2140
2141     $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
2142     return $evt if $evt;
2143
2144     $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
2145     return $U->cstorereq(
2146         'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
2147 }
2148
2149
2150 __PACKAGE__->register_method(
2151     method    => 'create_hold_notify',
2152     api_name  => 'open-ils.circ.hold_notification.create',
2153     signature => q/
2154 Creates a new hold notification object
2155 @param authtoken The login session key
2156 @param notification The hold notification object to create
2157 @return ID of the new object on success, Event on error
2158 /
2159 );
2160
2161 sub create_hold_notify {
2162    my( $self, $conn, $auth, $note ) = @_;
2163    my $e = new_editor(authtoken=>$auth, xact=>1);
2164    return $e->die_event unless $e->checkauth;
2165
2166    my $hold = $e->retrieve_action_hold_request($note->hold)
2167       or return $e->die_event;
2168    my $patron = $e->retrieve_actor_user($hold->usr)
2169       or return $e->die_event;
2170
2171    return $e->die_event unless
2172       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
2173
2174    $note->notify_staff($e->requestor->id);
2175    $e->create_action_hold_notification($note) or return $e->die_event;
2176    $e->commit;
2177    return $note->id;
2178 }
2179
2180 __PACKAGE__->register_method(
2181     method    => 'create_hold_note',
2182     api_name  => 'open-ils.circ.hold_note.create',
2183     signature => q/
2184         Creates a new hold request note object
2185         @param authtoken The login session key
2186         @param note The hold note object to create
2187         @return ID of the new object on success, Event on error
2188         /
2189 );
2190
2191 sub create_hold_note {
2192    my( $self, $conn, $auth, $note ) = @_;
2193    my $e = new_editor(authtoken=>$auth, xact=>1);
2194    return $e->die_event unless $e->checkauth;
2195
2196    my $hold = $e->retrieve_action_hold_request($note->hold)
2197       or return $e->die_event;
2198    my $patron = $e->retrieve_actor_user($hold->usr)
2199       or return $e->die_event;
2200
2201    return $e->die_event unless
2202       $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn.  Probably want something more specific
2203
2204    $e->create_action_hold_request_note($note) or return $e->die_event;
2205    $e->commit;
2206    return $note->id;
2207 }
2208
2209 __PACKAGE__->register_method(
2210     method    => 'reset_hold',
2211     api_name  => 'open-ils.circ.hold.reset',
2212     signature => q/
2213         Un-captures and un-targets a hold, essentially returning
2214         it to the state it was in directly after it was placed,
2215         then attempts to re-target the hold
2216         @param authtoken The login session key
2217         @param holdid The id of the hold
2218     /
2219 );
2220
2221
2222 sub reset_hold {
2223     my( $self, $conn, $auth, $holdid ) = @_;
2224     my $reqr;
2225     my ($hold, $evt) = $U->fetch_hold($holdid);
2226     return $evt if $evt;
2227     ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
2228     return $evt if $evt;
2229     $evt = _reset_hold($self, $reqr, $hold);
2230     return $evt if $evt;
2231     return 1;
2232 }
2233
2234
2235 __PACKAGE__->register_method(
2236     method   => 'reset_hold_batch',
2237     api_name => 'open-ils.circ.hold.reset.batch'
2238 );
2239
2240 sub reset_hold_batch {
2241     my($self, $conn, $auth, $hold_ids) = @_;
2242
2243     my $e = new_editor(authtoken => $auth);
2244     return $e->event unless $e->checkauth;
2245
2246     for my $hold_id ($hold_ids) {
2247
2248         my $hold = $e->retrieve_action_hold_request(
2249             [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
2250             or return $e->event;
2251
2252         next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
2253         _reset_hold($self, $e->requestor, $hold);
2254     }
2255
2256     return 1;
2257 }
2258
2259
2260 sub _reset_hold {
2261     my ($self, $reqr, $hold) = @_;
2262
2263     my $e = new_editor(xact =>1, requestor => $reqr);
2264
2265     $logger->info("reseting hold ".$hold->id);
2266
2267     my $hid = $hold->id;
2268
2269     if( $hold->capture_time and $hold->current_copy ) {
2270
2271         my $copy = $e->retrieve_asset_copy($hold->current_copy)
2272             or return $e->die_event;
2273
2274         if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2275             $logger->info("setting copy to status 'reshelving' on hold retarget");
2276             $copy->status(OILS_COPY_STATUS_RESHELVING);
2277             $copy->editor($e->requestor->id);
2278             $copy->edit_date('now');
2279             $e->update_asset_copy($copy) or return $e->die_event;
2280
2281         } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
2282
2283             $logger->warn("! reseting hold [$hid] that is in transit");
2284             my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
2285
2286             if( $transid ) {
2287                 my $trans = $e->retrieve_action_transit_copy($transid);
2288                 if( $trans ) {
2289                     $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
2290                     my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
2291                     $logger->info("Transit abort completed with result $evt");
2292                     unless ("$evt" eq 1) {
2293                         $e->rollback;
2294                         return $evt;
2295                     }
2296                 }
2297             }
2298         }
2299     }
2300
2301     $hold->clear_capture_time;
2302     $hold->clear_current_copy;
2303     $hold->clear_shelf_time;
2304     $hold->clear_shelf_expire_time;
2305     $hold->clear_current_shelf_lib;
2306
2307     $e->update_action_hold_request($hold) or return $e->die_event;
2308     $e->commit;
2309
2310     $U->simplereq('open-ils.hold-targeter', 
2311         'open-ils.hold-targeter.target', {hold => $hold->id});
2312
2313     return undef;
2314 }
2315
2316
2317 __PACKAGE__->register_method(
2318     method    => 'fetch_open_title_holds',
2319     api_name  => 'open-ils.circ.open_holds.retrieve',
2320     signature => q/
2321         Returns a list ids of un-fulfilled holds for a given title id
2322         @param authtoken The login session key
2323         @param id the id of the item whose holds we want to retrieve
2324         @param type The hold type - M, T, I, V, C, F, R
2325     /
2326 );
2327
2328 sub fetch_open_title_holds {
2329     my( $self, $conn, $auth, $id, $type, $org ) = @_;
2330     my $e = new_editor( authtoken => $auth );
2331     return $e->event unless $e->checkauth;
2332
2333     $type ||= "T";
2334     $org  ||= $e->requestor->ws_ou;
2335
2336 #    return $e->search_action_hold_request(
2337 #        { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2338
2339     # XXX make me return IDs in the future ^--
2340     my $holds = $e->search_action_hold_request(
2341         {
2342             target           => $id,
2343             cancel_time      => undef,
2344             hold_type        => $type,
2345             fulfillment_time => undef
2346         }
2347     );
2348
2349     flesh_hold_transits($holds);
2350     return $holds;
2351 }
2352
2353
2354 sub flesh_hold_transits {
2355     my $holds = shift;
2356     for my $hold ( @$holds ) {
2357         $hold->transit(
2358             $apputils->simplereq(
2359                 'open-ils.cstore',
2360                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2361                 { hold => $hold->id, cancel_time => undef },
2362                 { order_by => { ahtc => 'id desc' }, limit => 1 }
2363             )->[0]
2364         );
2365     }
2366 }
2367
2368 sub flesh_hold_notices {
2369     my( $holds, $e ) = @_;
2370     $e ||= new_editor();
2371
2372     for my $hold (@$holds) {
2373         my $notices = $e->search_action_hold_notification(
2374             [
2375                 { hold => $hold->id },
2376                 { order_by => { anh => 'notify_time desc' } },
2377             ],
2378             {idlist=>1}
2379         );
2380
2381         $hold->notify_count(scalar(@$notices));
2382         if( @$notices ) {
2383             my $n = $e->retrieve_action_hold_notification($$notices[0])
2384                 or return $e->event;
2385             $hold->notify_time($n->notify_time);
2386         }
2387     }
2388 }
2389
2390
2391 __PACKAGE__->register_method(
2392     method    => 'fetch_captured_holds',
2393     api_name  => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2394     stream    => 1,
2395     authoritative => 1,
2396     signature => q/
2397         Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2398         @param authtoken The login session key
2399         @param org The org id of the location in question
2400         @param match_copy A specific copy to limit to
2401     /
2402 );
2403
2404 __PACKAGE__->register_method(
2405     method    => 'fetch_captured_holds',
2406     api_name  => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2407     stream    => 1,
2408     authoritative => 1,
2409     signature => q/
2410         Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2411         @param authtoken The login session key
2412         @param org The org id of the location in question
2413         @param match_copy A specific copy to limit to
2414     /
2415 );
2416
2417 __PACKAGE__->register_method(
2418     method    => 'fetch_captured_holds',
2419     api_name  => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2420     stream    => 1,
2421     authoritative => 1,
2422     signature => q/
2423         Returns list ids of shelf-expired un-fulfilled holds for a given title id
2424         @param authtoken The login session key
2425         @param org The org id of the location in question
2426         @param match_copy A specific copy to limit to
2427     /
2428 );
2429
2430 __PACKAGE__->register_method(
2431     method    => 'fetch_captured_holds',
2432     api_name  => 
2433       'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2434     stream    => 1,
2435     authoritative => 1,
2436     signature => q/
2437         Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2438         for a given shelf lib
2439     /
2440 );
2441
2442 __PACKAGE__->register_method(
2443     method    => 'fetch_captured_holds',
2444     api_name  => 
2445       'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2446     stream    => 1,
2447     authoritative => 1,
2448     signature => q/
2449         Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2450         for a given shelf lib
2451     /
2452 );
2453
2454
2455 sub fetch_captured_holds {
2456     my( $self, $conn, $auth, $org, $match_copy ) = @_;
2457
2458     my $e = new_editor(authtoken => $auth);
2459     return $e->die_event unless $e->checkauth;
2460     return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2461
2462     $org ||= $e->requestor->ws_ou;
2463
2464     my $current_copy = { '!=' => undef };
2465     $current_copy = { '=' => $match_copy } if $match_copy;
2466
2467     my $query = {
2468         select => { alhr => ['id'] },
2469         from   => {
2470             alhr => {
2471                 acp => {
2472                     field => 'id',
2473                     fkey  => 'current_copy'
2474                 },
2475             }
2476         },
2477         where => {
2478             '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2479             '+alhr' => {
2480                 capture_time      => { "!=" => undef },
2481                 current_copy      => $current_copy,
2482                 fulfillment_time  => undef,
2483                 current_shelf_lib => $org
2484             }
2485         }
2486     };
2487     if($self->api_name =~ /expired/) {
2488         $query->{'where'}->{'+alhr'}->{'-or'} = {
2489                 shelf_expire_time => { '<' => 'today'},
2490                 cancel_time => { '!=' => undef },
2491         };
2492     }
2493     my $hold_ids = $e->json_query( $query );
2494
2495     if ($self->api_name =~ /wrong_shelf/) {
2496         # fetch holds whose current_shelf_lib is $org, but whose pickup 
2497         # lib is some other org unit.  Ignore already-retrieved holds.
2498         my $wrong_shelf =
2499             pickup_lib_changed_on_shelf_holds(
2500                 $e, $org, [map {$_->{id}} @$hold_ids]);
2501         # match the layout of other items in $hold_ids
2502         push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2503     }
2504
2505
2506     for my $hold_id (@$hold_ids) {
2507         if($self->api_name =~ /id_list/) {
2508             $conn->respond($hold_id->{id});
2509             next;
2510         } else {
2511             $conn->respond(
2512                 $e->retrieve_action_hold_request([
2513                     $hold_id->{id},
2514                     {
2515                         flesh => 1,
2516                         flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2517                         order_by => {anh => 'notify_time desc'}
2518                     }
2519                 ])
2520             );
2521         }
2522     }
2523
2524     return undef;
2525 }
2526
2527 __PACKAGE__->register_method(
2528     method    => "print_expired_holds_stream",
2529     api_name  => "open-ils.circ.captured_holds.expired.print.stream",
2530     stream    => 1
2531 );
2532
2533 sub print_expired_holds_stream {
2534     my ($self, $client, $auth, $params) = @_;
2535
2536     # No need to check specific permissions: we're going to call another method
2537     # that will do that.
2538     my $e = new_editor("authtoken" => $auth);
2539     return $e->die_event unless $e->checkauth;
2540
2541     delete($$params{org_id}) unless (int($$params{org_id}));
2542     delete($$params{limit}) unless (int($$params{limit}));
2543     delete($$params{offset}) unless (int($$params{offset}));
2544     delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2545     delete($$params{chunk_size}) if  ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2546     $$params{chunk_size} ||= 10;
2547     $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2548
2549     $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2550
2551     my @hold_ids = $self->method_lookup(
2552         "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2553     )->run($auth, $params->{"org_id"});
2554
2555     if (!@hold_ids) {
2556         $e->disconnect;
2557         return;
2558     } elsif (defined $U->event_code($hold_ids[0])) {
2559         $e->disconnect;
2560         return $hold_ids[0];
2561     }
2562
2563     $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2564
2565     while (@hold_ids) {
2566         my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2567
2568         my $result_chunk = $e->json_query({
2569             "select" => {
2570                 "acp" => ["barcode"],
2571                 "au" => [qw/
2572                     first_given_name second_given_name family_name alias
2573                 /],
2574                 "acn" => ["label"],
2575                 "bre" => ["marc"],
2576                 "acpl" => ["name"]
2577             },
2578             "from" => {
2579                 "ahr" => {
2580                     "acp" => {
2581                         "field" => "id", "fkey" => "current_copy",
2582                         "join" => {
2583                             "acn" => {
2584                                 "field" => "id", "fkey" => "call_number",
2585                                 "join" => {
2586                                     "bre" => {
2587                                         "field" => "id", "fkey" => "record"
2588                                     }
2589                                 }
2590                             },
2591                             "acpl" => {"field" => "id", "fkey" => "location"}
2592                         }
2593                     },
2594                     "au" => {"field" => "id", "fkey" => "usr"}
2595                 }
2596             },
2597             "where" => {"+ahr" => {"id" => \@hid_chunk}}
2598         }) or return $e->die_event;
2599         $client->respond($result_chunk);
2600     }
2601
2602     $e->disconnect;
2603     undef;
2604 }
2605
2606 __PACKAGE__->register_method(
2607     method    => "check_title_hold_batch",
2608     api_name  => "open-ils.circ.title_hold.is_possible.batch",
2609     stream    => 1,
2610     signature => {
2611         desc  => '@see open-ils.circ.title_hold.is_possible.batch',
2612         params => [
2613             { desc => 'Authentication token',     type => 'string'},
2614             { desc => 'Array of Hash of named parameters', type => 'array'},
2615         ],
2616         return => {
2617             desc => 'Array of response objects',
2618             type => 'array'
2619         }
2620     }
2621 );
2622
2623 sub check_title_hold_batch {
2624     my($self, $client, $authtoken, $param_list, $oargs) = @_;
2625     foreach (@$param_list) {
2626         my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2627         $client->respond($res);
2628     }
2629     return undef;
2630 }
2631
2632
2633 __PACKAGE__->register_method(
2634     method    => "check_title_hold",
2635     api_name  => "open-ils.circ.title_hold.is_possible",
2636     signature => {
2637         desc  => 'Determines if a hold were to be placed by a given user, ' .
2638              'whether or not said hold would have any potential copies to fulfill it.' .
2639              'The named paramaters of the second argument include: ' .
2640              'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2641              'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2642         params => [
2643             { desc => 'Authentication token',     type => 'string'},
2644             { desc => 'Hash of named parameters', type => 'object'},
2645         ],
2646         return => {
2647             desc => 'List of new message IDs (empty if none)',
2648             type => 'array'
2649         }
2650     }
2651 );
2652
2653 =head3 check_title_hold (token, hash)
2654
2655 The named fields in the hash are:
2656
2657  patronid     - ID of the hold recipient  (required)
2658  depth        - hold range depth          (default 0)
2659  pickup_lib   - destination for hold, fallback value for selection_ou
2660  selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2661  issuanceid   - ID of the issuance to be held, required for Issuance level hold
2662  partid       - ID of the monograph part to be held, required for monograph part level hold
2663  titleid      - ID (BRN) of the title to be held, required for Title level hold
2664  volume_id    - required for Volume level hold
2665  copy_id      - required for Copy level hold
2666  mrid         - required for Meta-record level hold
2667  hold_type    - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record  (default "T")
2668
2669 All key/value pairs are passed on to do_possibility_checks.
2670
2671 =cut
2672
2673 # FIXME: better params checking.  what other params are required, if any?
2674 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2675 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2676 # used in conditionals, where it may be undefined, causing a warning.
2677 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2678
2679 sub check_title_hold {
2680     my( $self, $client, $authtoken, $params ) = @_;
2681     my $e = new_editor(authtoken=>$authtoken);
2682     return $e->event unless $e->checkauth;
2683
2684     my %params       = %$params;
2685     my $depth        = $params{depth}        || 0;
2686     $params{depth} = $depth;   #define $params{depth} if unset, since it gets used later
2687     my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2688     my $oargs        = $params{oargs}        || {};
2689
2690     if($oargs->{events}) {
2691         @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2692     }
2693
2694
2695     my $patron = $e->retrieve_actor_user($params{patronid})
2696         or return $e->event;
2697
2698     if( $e->requestor->id ne $patron->id ) {
2699         return $e->event unless
2700             $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2701     }
2702
2703     return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2704
2705     my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2706         or return $e->event;
2707
2708     my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2709     my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2710
2711     my @status = ();
2712     my $return_depth = $hard_boundary; # default depth to return on success
2713     if(defined $soft_boundary and $depth < $soft_boundary) {
2714         # work up the tree and as soon as we find a potential copy, use that depth
2715         # also, make sure we don't go past the hard boundary if it exists
2716
2717         # our min boundary is the greater of user-specified boundary or hard boundary
2718         my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2719             $hard_boundary : $depth;
2720
2721         my $depth = $soft_boundary;
2722         while($depth >= $min_depth) {
2723             $logger->info("performing hold possibility check with soft boundary $depth");
2724             @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2725             if ($status[0]) {
2726                 $return_depth = $depth;
2727                 last;
2728             }
2729             $depth--;
2730         }
2731     } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2732         # there is no soft boundary, enforce the hard boundary if it exists
2733         $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2734         @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2735     } else {
2736         # no boundaries defined, fall back to user specifed boundary or no boundary
2737         $logger->info("performing hold possibility check with no boundary");
2738         @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2739     }
2740
2741     my $place_unfillable = 0;
2742     $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2743
2744     if ($status[0]) {
2745         return {
2746             "success" => 1,
2747             "depth" => $return_depth,
2748             "local_avail" => $status[1]
2749         };
2750     } elsif ($status[2]) {
2751         my $n = scalar @{$status[2]};
2752         return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2753     } else {
2754         return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2755     }
2756 }
2757
2758
2759
2760 sub do_possibility_checks {
2761     my($e, $patron, $request_lib, $depth, %params) = @_;
2762
2763     my $issuanceid   = $params{issuanceid}      || "";
2764     my $partid       = $params{partid}      || "";
2765     my $titleid      = $params{titleid}      || "";
2766     my $volid        = $params{volume_id};
2767     my $copyid       = $params{copy_id};
2768     my $mrid         = $params{mrid}         || "";
2769     my $pickup_lib   = $params{pickup_lib};
2770     my $hold_type    = $params{hold_type}    || 'T';
2771     my $selection_ou = $params{selection_ou} || $pickup_lib;
2772     my $holdable_formats = $params{holdable_formats};
2773     my $oargs        = $params{oargs}        || {};
2774
2775
2776     my $copy;
2777     my $volume;
2778     my $title;
2779
2780     if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2781
2782         return $e->event unless $copy   = $e->retrieve_asset_copy($copyid);
2783         return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2784         return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
2785
2786         return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2787         return verify_copy_for_hold(
2788             $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2789         );
2790
2791     } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2792
2793         return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2794         return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
2795
2796         return _check_volume_hold_is_possible(
2797             $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2798         );
2799
2800     } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2801
2802         return _check_title_hold_is_possible(
2803             $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2804         );
2805
2806     } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2807
2808         return _check_issuance_hold_is_possible(
2809             $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2810         );
2811
2812     } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2813
2814         return _check_monopart_hold_is_possible(
2815             $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2816         );
2817
2818     } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2819
2820         # pasing undef as the depth to filtered_records causes the depth
2821         # of the selection_ou to be used, which is not what we want here.
2822         $depth ||= 0;
2823
2824         my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2825         my @status = ();
2826         for my $rec (@$recs) {
2827             @status = _check_title_hold_is_possible(
2828                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2829             );
2830             last if $status[0];
2831         }
2832         return @status;
2833     }
2834 #   else { Unrecognized hold_type ! }   # FIXME: return error? or 0?
2835 }
2836
2837 sub MR_filter_records {
2838     my $self = shift;
2839     my $client = shift;
2840     my $m = shift;
2841     my $f = shift;
2842     my $o = shift;
2843     my $d = shift;
2844     my $opac_visible = shift;
2845     
2846     my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2847     return $U->storagereq(
2848         'open-ils.storage.metarecord.filtered_records.atomic', 
2849         $m, $f, $org_at_depth, $opac_visible
2850     );
2851 }
2852 __PACKAGE__->register_method(
2853     method   => 'MR_filter_records',
2854     api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2855 );
2856
2857
2858 my %prox_cache;
2859 sub create_ranged_org_filter {
2860     my($e, $selection_ou, $depth) = @_;
2861
2862     # find the orgs from which this hold may be fulfilled,
2863     # based on the selection_ou and depth
2864
2865     my $top_org = $e->search_actor_org_unit([
2866         {parent_ou => undef},
2867         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2868     my %org_filter;
2869
2870     return () if $depth == $top_org->ou_type->depth;
2871
2872     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2873     %org_filter = (circ_lib => []);
2874     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2875
2876     $logger->info("hold org filter at depth $depth and selection_ou ".
2877         "$selection_ou created list of @{$org_filter{circ_lib}}");
2878
2879     return %org_filter;
2880 }
2881
2882
2883 sub _check_title_hold_is_possible {
2884     my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2885     # $holdable_formats is now unused. We pre-filter the MR's records.
2886
2887     my $e = new_editor();
2888
2889     # T holds on records that have parts are normally OK, but if the record has
2890     # no non-part copies, the hold will ultimately fail, so let's test for that.
2891     #
2892     # If the global flag circ.holds.api_require_monographic_part_when_present is
2893     # enabled, then any configured parts for the bib is enough to disallow title holds.
2894     my $part_required = 0;
2895     my $parts = $e->search_biblio_monograph_part(
2896         {
2897             record => $titleid,
2898             deleted => 'f'
2899         }, {idlist=>1} );
2900
2901     if (@$parts) {
2902         my $part_required_flag = $e->retrieve_config_global_flag('circ.holds.api_require_monographic_part_when_present');
2903         $part_required = ($part_required_flag and $U->is_true($part_required_flag->enabled));
2904         if (!$part_required) {
2905             my $np_copies = $e->json_query({
2906                 select => { acp => [{column => 'id', transform => 'count', alias => 'count'}]},
2907                 from => {acp => {acn => {}, acpm => {type => 'left'}}},
2908                 where => {
2909                     '+acp' => {deleted => 'f'},
2910                     '+acn' => {deleted => 'f', record => $titleid},
2911                     '+acpm' => {id => undef}
2912                 }
2913             });
2914             $part_required = 1 if $np_copies->[0]->{count} == 0;
2915         }
2916     }
2917     if ($part_required) {
2918         $logger->info("title hold when monographic part required");
2919         return (
2920             0, 0, [
2921                 new OpenILS::Event(
2922                     "TITLE_HOLD_WHEN_MONOGRAPHIC_PART_REQUIRED",
2923                     "payload" => {"fail_part" => "monographic_part_required"}
2924                 )
2925             ]
2926         );
2927     }
2928
2929     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2930
2931     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2932     my $copies = $e->json_query(
2933         {
2934             select => { acp => ['id', 'circ_lib'] },
2935               from => {
2936                 acp => {
2937                     acn => {
2938                         field  => 'id',
2939                         fkey   => 'call_number',
2940                         filter => { record => $titleid }
2941                     },
2942                     acpl => {
2943                                 field => 'id',
2944                                 filter => { holdable => 't', deleted => 'f' },
2945                                 fkey => 'location'
2946                             },
2947                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   },
2948                     acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2949                 }
2950             },
2951             where => {
2952                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2953                 '+acpm' => { target_copy => undef } # ignore part-linked copies
2954             }
2955         }
2956     );
2957
2958     $logger->info("title possible found ".scalar(@$copies)." potential copies");
2959     return (
2960         0, 0, [
2961             new OpenILS::Event(
2962                 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2963                 "payload" => {"fail_part" => "no_ultimate_items"}
2964             )
2965         ]
2966     ) unless @$copies;
2967
2968     # -----------------------------------------------------------------------
2969     # sort the copies into buckets based on their circ_lib proximity to
2970     # the patron's home_ou.
2971     # -----------------------------------------------------------------------
2972
2973     my $home_org = $patron->home_ou;
2974     my $req_org = $request_lib->id;
2975
2976     $prox_cache{$home_org} =
2977         $e->search_actor_org_unit_proximity({from_org => $home_org})
2978         unless $prox_cache{$home_org};
2979     my $home_prox = $prox_cache{$home_org};
2980     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2981
2982     my %buckets;
2983     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2984     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2985
2986     my @keys = sort { $a <=> $b } keys %buckets;
2987
2988
2989     if( $home_org ne $req_org ) {
2990       # -----------------------------------------------------------------------
2991       # shove the copies close to the request_lib into the primary buckets
2992       # directly before the farthest away copies.  That way, they are not
2993       # given priority, but they are checked before the farthest copies.
2994       # -----------------------------------------------------------------------
2995         $prox_cache{$req_org} =
2996             $e->search_actor_org_unit_proximity({from_org => $req_org})
2997             unless $prox_cache{$req_org};
2998         my $req_prox = $prox_cache{$req_org};
2999
3000         my %buckets2;
3001         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3002         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3003
3004         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
3005         my $new_key = $highest_key - 0.5; # right before the farthest prox
3006         my @keys2   = sort { $a <=> $b } keys %buckets2;
3007         for my $key (@keys2) {
3008             last if $key >= $highest_key;
3009             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3010         }
3011     }
3012
3013     @keys = sort { $a <=> $b } keys %buckets;
3014
3015     my $title;
3016     my %seen;
3017     my @status;
3018     my $age_protect_only = 0;
3019     OUTER: for my $key (@keys) {
3020       my @cps = @{$buckets{$key}};
3021
3022       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3023
3024       for my $copyid (@cps) {
3025
3026          next if $seen{$copyid};
3027          $seen{$copyid} = 1; # there could be dupes given the merged buckets
3028          my $copy = $e->retrieve_asset_copy($copyid);
3029          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3030
3031          unless($title) { # grab the title if we don't already have it
3032             my $vol = $e->retrieve_asset_call_number(
3033                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3034             $title = $vol->record;
3035          }
3036
3037          @status = verify_copy_for_hold(
3038             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3039
3040          $age_protect_only ||= $status[3];
3041          last OUTER if $status[0];
3042       }
3043     }
3044
3045     $status[3] = $age_protect_only;
3046     return @status;
3047 }
3048
3049 sub _check_issuance_hold_is_possible {
3050     my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3051
3052     my $e = new_editor();
3053     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3054
3055     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3056     my $copies = $e->json_query(
3057         {
3058             select => { acp => ['id', 'circ_lib'] },
3059               from => {
3060                 acp => {
3061                     sitem => {
3062                         field  => 'unit',
3063                         fkey   => 'id',
3064                         filter => { issuance => $issuanceid }
3065                     },
3066                     acpl => {
3067                         field => 'id',
3068                         filter => { holdable => 't', deleted => 'f' },
3069                         fkey => 'location'
3070                     },
3071                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
3072                 }
3073             },
3074             where => {
3075                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3076             },
3077             distinct => 1
3078         }
3079     );
3080
3081     $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
3082
3083     my $empty_ok;
3084     if (!@$copies) {
3085         $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3086         $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3087
3088         return (
3089             0, 0, [
3090                 new OpenILS::Event(
3091                     "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3092                     "payload" => {"fail_part" => "no_ultimate_items"}
3093                 )
3094             ]
3095         ) unless $empty_ok;
3096
3097         return (1, 0);
3098     }
3099
3100     # -----------------------------------------------------------------------
3101     # sort the copies into buckets based on their circ_lib proximity to
3102     # the patron's home_ou.
3103     # -----------------------------------------------------------------------
3104
3105     my $home_org = $patron->home_ou;
3106     my $req_org = $request_lib->id;
3107
3108     $prox_cache{$home_org} =
3109         $e->search_actor_org_unit_proximity({from_org => $home_org})
3110         unless $prox_cache{$home_org};
3111     my $home_prox = $prox_cache{$home_org};
3112     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3113
3114     my %buckets;
3115     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3116     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3117
3118     my @keys = sort { $a <=> $b } keys %buckets;
3119
3120
3121     if( $home_org ne $req_org ) {
3122       # -----------------------------------------------------------------------
3123       # shove the copies close to the request_lib into the primary buckets
3124       # directly before the farthest away copies.  That way, they are not
3125       # given priority, but they are checked before the farthest copies.
3126       # -----------------------------------------------------------------------
3127         $prox_cache{$req_org} =
3128             $e->search_actor_org_unit_proximity({from_org => $req_org})
3129             unless $prox_cache{$req_org};
3130         my $req_prox = $prox_cache{$req_org};
3131
3132         my %buckets2;
3133         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3134         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3135
3136         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
3137         my $new_key = $highest_key - 0.5; # right before the farthest prox
3138         my @keys2   = sort { $a <=> $b } keys %buckets2;
3139         for my $key (@keys2) {
3140             last if $key >= $highest_key;
3141             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3142         }
3143     }
3144
3145     @keys = sort { $a <=> $b } keys %buckets;
3146
3147     my $title;
3148     my %seen;
3149     my @status;
3150     my $age_protect_only = 0;
3151     OUTER: for my $key (@keys) {
3152       my @cps = @{$buckets{$key}};
3153
3154       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3155
3156       for my $copyid (@cps) {
3157
3158          next if $seen{$copyid};
3159          $seen{$copyid} = 1; # there could be dupes given the merged buckets
3160          my $copy = $e->retrieve_asset_copy($copyid);
3161          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3162
3163          unless($title) { # grab the title if we don't already have it
3164             my $vol = $e->retrieve_asset_call_number(
3165                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3166             $title = $vol->record;
3167          }
3168
3169          @status = verify_copy_for_hold(
3170             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3171
3172          $age_protect_only ||= $status[3];
3173          last OUTER if $status[0];
3174       }
3175     }
3176
3177     if (!$status[0]) {
3178         if (!defined($empty_ok)) {
3179             $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3180             $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3181         }
3182
3183         return (1,0) if ($empty_ok);
3184     }
3185     $status[3] = $age_protect_only;
3186     return @status;
3187 }
3188
3189 sub _check_monopart_hold_is_possible {
3190     my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3191
3192     my $e = new_editor();
3193     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3194
3195     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3196     my $copies = $e->json_query(
3197         {
3198             select => { acp => ['id', 'circ_lib'] },
3199               from => {
3200                 acp => {
3201                     acpm => {
3202                         field  => 'target_copy',
3203                         fkey   => 'id',
3204                         filter => { part => $partid }
3205                     },
3206                     acpl => {
3207                         field => 'id',
3208                         filter => { holdable => 't', deleted => 'f' },
3209                         fkey => 'location'
3210                     },
3211                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
3212                 }
3213             },
3214             where => {
3215                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3216             },
3217             distinct => 1
3218         }
3219     );
3220
3221     $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
3222
3223     my $empty_ok;
3224     if (!@$copies) {
3225         $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3226         $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3227
3228         return (
3229             0, 0, [
3230                 new OpenILS::Event(
3231                     "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3232                     "payload" => {"fail_part" => "no_ultimate_items"}
3233                 )
3234             ]
3235         ) unless $empty_ok;
3236
3237         return (1, 0);
3238     }
3239
3240     # -----------------------------------------------------------------------
3241     # sort the copies into buckets based on their circ_lib proximity to
3242     # the patron's home_ou.
3243     # -----------------------------------------------------------------------
3244
3245     my $home_org = $patron->home_ou;
3246     my $req_org = $request_lib->id;
3247
3248     $prox_cache{$home_org} =
3249         $e->search_actor_org_unit_proximity({from_org => $home_org})
3250         unless $prox_cache{$home_org};
3251     my $home_prox = $prox_cache{$home_org};
3252     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3253
3254     my %buckets;
3255     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3256     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3257
3258     my @keys = sort { $a <=> $b } keys %buckets;
3259
3260
3261     if( $home_org ne $req_org ) {
3262       # -----------------------------------------------------------------------
3263       # shove the copies close to the request_lib into the primary buckets
3264       # directly before the farthest away copies.  That way, they are not
3265       # given priority, but they are checked before the farthest copies.
3266       # -----------------------------------------------------------------------
3267         $prox_cache{$req_org} =
3268             $e->search_actor_org_unit_proximity({from_org => $req_org})
3269             unless $prox_cache{$req_org};
3270         my $req_prox = $prox_cache{$req_org};
3271
3272         my %buckets2;
3273         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3274         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3275
3276         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
3277         my $new_key = $highest_key - 0.5; # right before the farthest prox
3278         my @keys2   = sort { $a <=> $b } keys %buckets2;
3279         for my $key (@keys2) {
3280             last if $key >= $highest_key;
3281             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3282         }
3283     }
3284
3285     @keys = sort { $a <=> $b } keys %buckets;
3286
3287     my $title;
3288     my %seen;
3289     my @status;
3290     my $age_protect_only = 0;
3291     OUTER: for my $key (@keys) {
3292       my @cps = @{$buckets{$key}};
3293
3294       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3295
3296       for my $copyid (@cps) {
3297
3298          next if $seen{$copyid};
3299          $seen{$copyid} = 1; # there could be dupes given the merged buckets
3300          my $copy = $e->retrieve_asset_copy($copyid);
3301          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3302
3303          unless($title) { # grab the title if we don't already have it
3304             my $vol = $e->retrieve_asset_call_number(
3305                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3306             $title = $vol->record;
3307          }
3308
3309          @status = verify_copy_for_hold(
3310             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3311
3312          $age_protect_only ||= $status[3];
3313          last OUTER if $status[0];
3314       }
3315     }
3316
3317     if (!$status[0]) {
3318         if (!defined($empty_ok)) {
3319             $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3320             $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3321         }
3322
3323         return (1,0) if ($empty_ok);
3324     }
3325     $status[3] = $age_protect_only;
3326     return @status;
3327 }
3328
3329
3330 sub _check_volume_hold_is_possible {
3331     my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3332     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
3333     my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
3334     $logger->info("checking possibility of volume hold for volume ".$vol->id);
3335
3336     my $filter_copies = [];
3337     for my $copy (@$copies) {
3338         # ignore part-mapped copies for regular volume level holds
3339         push(@$filter_copies, $copy) unless
3340             new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
3341     }
3342     $copies = $filter_copies;
3343
3344     return (
3345         0, 0, [
3346             new OpenILS::Event(
3347                 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3348                 "payload" => {"fail_part" => "no_ultimate_items"}
3349             )
3350         ]
3351     ) unless @$copies;
3352
3353     my @status;
3354     my $age_protect_only = 0;
3355     for my $copy ( @$copies ) {
3356         @status = verify_copy_for_hold(
3357             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3358         $age_protect_only ||= $status[3];
3359         last if $status[0];
3360     }
3361     $status[3] = $age_protect_only;
3362     return @status;
3363 }
3364
3365
3366
3367 sub verify_copy_for_hold {
3368     my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3369     # $oargs should be undef unless we're overriding.
3370     $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3371     my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3372         {
3373             patron           => $patron,
3374             requestor        => $requestor,
3375             copy             => $copy,
3376             title            => $title,
3377             title_descriptor => $title->fixed_fields,
3378             pickup_lib       => $pickup_lib,
3379             request_lib      => $request_lib,
3380             new_hold         => 1,
3381             show_event_list  => 1
3382         }
3383     );
3384
3385     # Check for override permissions on events.
3386     if ($oargs && $permitted && scalar @$permitted) {
3387         # Remove the events from permitted that we can override.
3388         if ($oargs->{events}) {
3389             foreach my $evt (@{$oargs->{events}}) {
3390                 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3391             }
3392         }
3393         # Now, we handle the override all case by checking remaining
3394         # events against override permissions.
3395         if (scalar @$permitted && $oargs->{all}) {
3396             # Pre-set events and failed members of oargs to empty
3397             # arrays, if they are not set, yet.
3398             $oargs->{events} = [] unless ($oargs->{events});
3399             $oargs->{failed} = [] unless ($oargs->{failed});
3400             # When we're done with these checks, we swap permitted
3401             # with a reference to @disallowed.
3402             my @disallowed = ();
3403             foreach my $evt (@{$permitted}) {
3404                 # Check if we've already seen the event in this
3405                 # session and it failed.
3406                 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3407                     push(@disallowed, $evt);
3408                 } else {
3409                     # We have to check if the requestor has the
3410                     # override permission.
3411
3412                     # AppUtils::check_user_perms returns the perm if
3413                     # the user doesn't have it, undef if they do.
3414                     if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3415                         push(@disallowed, $evt);
3416                         push(@{$oargs->{failed}}, $evt->{textcode});
3417                     } else {
3418                         push(@{$oargs->{events}}, $evt->{textcode});
3419                     }
3420                 }
3421             }
3422             $permitted = \@disallowed;
3423         }
3424     }
3425
3426     my $age_protect_only = 0;
3427     if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3428         $age_protect_only = 1;
3429     }
3430
3431     return (
3432         (not scalar @$permitted), # true if permitted is an empty arrayref
3433         (   # XXX This test is of very dubious value; someone should figure
3434             # out what if anything is checking this value
3435             ($copy->circ_lib == $pickup_lib) and
3436             ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3437         ),
3438         $permitted,
3439         $age_protect_only
3440     );
3441 }
3442
3443
3444
3445 sub find_nearest_permitted_hold {
3446
3447     my $class  = shift;
3448     my $editor = shift;     # CStoreEditor object
3449     my $copy   = shift;     # copy to target
3450     my $user   = shift;     # staff
3451     my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3452
3453     my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3454
3455     my $bc = $copy->barcode;
3456
3457     # find any existing holds that already target this copy
3458     my $old_holds = $editor->search_action_hold_request(
3459         {    current_copy => $copy->id,
3460             cancel_time  => undef,
3461             capture_time => undef
3462         }
3463     );
3464
3465     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3466
3467     $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3468         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3469
3470     my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3471
3472     # the nearest_hold API call now needs this
3473     $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3474         unless ref $copy->call_number;
3475
3476     # search for what should be the best holds for this copy to fulfill
3477     my $best_holds = $U->storagereq(
3478         "open-ils.storage.action.hold_request.nearest_hold.atomic", 
3479         $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3480
3481     # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3482     if ($old_holds) {
3483         for my $holdid (@$old_holds) {
3484             next unless $holdid;
3485             push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3486         }
3487     }
3488
3489     unless(@$best_holds) {
3490         $logger->info("circulator: no suitable holds found for copy $bc");
3491         return (undef, $evt);
3492     }
3493
3494
3495     my $best_hold;
3496
3497     # for each potential hold, we have to run the permit script
3498     # to make sure the hold is actually permitted.
3499     my %reqr_cache;
3500     my %org_cache;
3501     for my $holdid (@$best_holds) {
3502         next unless $holdid;
3503         $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3504
3505         my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3506         # Force and recall holds bypass all rules
3507         if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3508             $best_hold = $hold;
3509             last;
3510         }
3511         my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3512         my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3513
3514         $reqr_cache{$hold->requestor} = $reqr;
3515         $org_cache{$hold->request_lib} = $rlib;
3516
3517         # see if this hold is permitted
3518         my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3519             {
3520                 patron_id   => $hold->usr,
3521                 requestor   => $reqr,
3522                 copy        => $copy,
3523                 pickup_lib  => $hold->pickup_lib,
3524                 request_lib => $rlib,
3525                 retarget    => 1
3526             }
3527         );
3528
3529         if( $permitted ) {
3530             $best_hold = $hold;
3531             last;
3532         }
3533     }
3534
3535
3536     unless( $best_hold ) { # no "good" permitted holds were found
3537         # we got nuthin
3538         $logger->info("circulator: no suitable holds found for copy $bc");
3539         return (undef, $evt);
3540     }
3541
3542     $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3543
3544     # indicate a permitted hold was found
3545     return $best_hold if $check_only;
3546
3547     # we've found a permitted hold.  we need to "grab" the copy
3548     # to prevent re-targeted holds (next part) from re-grabbing the copy
3549     $best_hold->current_copy($copy->id);
3550     $editor->update_action_hold_request($best_hold)
3551         or return (undef, $editor->event);
3552
3553
3554     my @retarget;
3555
3556     # re-target any other holds that already target this copy
3557     for my $old_hold (@$old_holds) {
3558         next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3559         $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3560             $old_hold->id." after a better hold [".$best_hold->id."] was found");
3561         $old_hold->clear_current_copy;
3562         $old_hold->clear_prev_check_time;
3563         $editor->update_action_hold_request($old_hold)
3564             or return (undef, $editor->event);
3565         push(@retarget, $old_hold->id);
3566     }
3567
3568     return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3569 }
3570
3571
3572
3573
3574
3575
3576 __PACKAGE__->register_method(
3577     method   => 'all_rec_holds',
3578     api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3579 );
3580
3581 sub all_rec_holds {
3582     my( $self, $conn, $auth, $title_id, $args ) = @_;
3583
3584     my $e = new_editor(authtoken=>$auth);
3585     $e->checkauth or return $e->event;
3586     $e->allowed('VIEW_HOLD') or return $e->event;
3587
3588     $args ||= {};
3589     $args->{fulfillment_time} = undef; #  we don't want to see old fulfilled holds
3590     $args->{cancel_time} = undef;
3591
3592     my $resp = {
3593           metarecord_holds => []
3594         , title_holds      => []
3595         , volume_holds     => []
3596         , copy_holds       => []
3597         , recall_holds     => []
3598         , force_holds      => []
3599         , part_holds       => []
3600         , issuance_holds   => []
3601     };
3602
3603     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3604     if($mr_map) {
3605         $resp->{metarecord_holds} = $e->search_action_hold_request(
3606             {   hold_type => OILS_HOLD_TYPE_METARECORD,
3607                 target => $mr_map->metarecord,
3608                 %$args
3609             }, {idlist => 1}
3610         );
3611     }
3612
3613     $resp->{title_holds} = $e->search_action_hold_request(
3614         {
3615             hold_type => OILS_HOLD_TYPE_TITLE,
3616             target => $title_id,
3617             %$args
3618         }, {idlist=>1} );
3619
3620     my $parts = $e->search_biblio_monograph_part(
3621         {
3622             record => $title_id
3623         }, {idlist=>1} );
3624
3625     if (@$parts) {
3626         $resp->{part_holds} = $e->search_action_hold_request(
3627             {
3628                 hold_type => OILS_HOLD_TYPE_MONOPART,
3629                 target => $parts,
3630                 %$args
3631             }, {idlist=>1} );
3632     }
3633
3634     my $subs = $e->search_serial_subscription(
3635         { record_entry => $title_id }, {idlist=>1});
3636
3637     if (@$subs) {
3638         my $issuances = $e->search_serial_issuance(
3639             {subscription => $subs}, {idlist=>1}
3640         );
3641
3642         if (@$issuances) {
3643             $resp->{issuance_holds} = $e->search_action_hold_request(
3644                 {
3645                     hold_type => OILS_HOLD_TYPE_ISSUANCE,
3646                     target => $issuances,
3647                     %$args
3648                 }, {idlist=>1}
3649             );
3650         }
3651     }
3652
3653     my $vols = $e->search_asset_call_number(
3654         { record => $title_id, deleted => 'f' }, {idlist=>1});
3655
3656     return $resp unless @$vols;
3657
3658     $resp->{volume_holds} = $e->search_action_hold_request(
3659         {
3660             hold_type => OILS_HOLD_TYPE_VOLUME,
3661             target => $vols,
3662             %$args },
3663         {idlist=>1} );
3664
3665     my $copies = $e->search_asset_copy(
3666         { call_number => $vols, deleted => 'f' }, {idlist=>1});
3667
3668     return $resp unless @$copies;
3669
3670     $resp->{copy_holds} = $e->search_action_hold_request(
3671         {
3672             hold_type => OILS_HOLD_TYPE_COPY,
3673             target => $copies,
3674             %$args },
3675         {idlist=>1} );
3676
3677     $resp->{recall_holds} = $e->search_action_hold_request(
3678         {
3679             hold_type => OILS_HOLD_TYPE_RECALL,
3680             target => $copies,
3681             %$args },
3682         {idlist=>1} );
3683
3684     $resp->{force_holds} = $e->search_action_hold_request(
3685         {
3686             hold_type => OILS_HOLD_TYPE_FORCE,
3687             target => $copies,
3688             %$args },
3689         {idlist=>1} );
3690
3691     return $resp;
3692 }
3693
3694 __PACKAGE__->register_method(
3695     method           => 'stream_wide_holds',
3696     authoritative    => 1,
3697     stream           => 1,
3698     api_name         => 'open-ils.circ.hold.wide_hash.stream'
3699 );
3700
3701 sub stream_wide_holds {
3702     my($self, $client, $auth, $restrictions, $order_by, $limit, $offset, $options) = @_;
3703     $options ||= {};
3704
3705     my $e = new_editor(authtoken=>$auth);
3706     $e->checkauth or return $e->event;
3707     $e->allowed('VIEW_HOLD') or return $e->event;
3708
3709     if ($options->{recently_canceled}) {
3710         # Map the the recently canceled holds filter into values 
3711         # wide-stream understands.
3712         my $filter = recently_canceled_holds_filter($e);
3713         $restrictions->{$_} =
3714             $filter->{where}->{$_} for keys %{$filter->{where}};
3715
3716         $limit = $filter->{limit} if $filter->{limit};
3717     }
3718
3719     my $filters = OpenSRF::Utils::JSON->perl2JSON($restrictions);
3720     $logger->info("WIDE HOLD FILTERS: $filters");
3721
3722     my $st = OpenSRF::AppSession->create('open-ils.storage');
3723     my $req = $st->request(
3724         'open-ils.storage.action.live_holds.wide_hash.atomic',
3725         $restrictions, $order_by, $limit, $offset
3726     );
3727
3728     my $results = $req->recv;
3729     if(!$results) {
3730         return 0;
3731     }
3732
3733     if(UNIVERSAL::isa($results,"Error")) {
3734         throw OpenSRF::EX::ERROR ("Error fetch hold shelf list");
3735     }
3736
3737     my @rows = @{ $results->content };
3738
3739     # Force immediate send of count response
3740     my $mbc = $client->max_bundle_count;
3741     $client->max_bundle_count(1);
3742     $client->respond(shift @rows);
3743     $client->max_bundle_count($mbc);
3744
3745     foreach my $hold (@rows) {
3746         $client->respond($hold) if $hold;
3747     }
3748
3749     $client->respond_complete;
3750 }
3751
3752
3753
3754
3755 __PACKAGE__->register_method(
3756     method        => 'uber_hold',
3757     authoritative => 1,
3758     api_name      => 'open-ils.circ.hold.details.retrieve'
3759 );
3760
3761 sub uber_hold {
3762     my($self, $client, $auth, $hold_id, $args) = @_;
3763     my $e = new_editor(authtoken=>$auth);
3764     $e->checkauth or return $e->event;
3765     return uber_hold_impl($e, $hold_id, $args);
3766 }
3767
3768 __PACKAGE__->register_method(
3769     method        => 'batch_uber_hold',
3770     authoritative => 1,
3771     stream        => 1,
3772     api_name      => 'open-ils.circ.hold.details.batch.retrieve'
3773 );
3774
3775 sub batch_uber_hold {
3776     my($self, $client, $auth, $hold_ids, $args) = @_;
3777     my $e = new_editor(authtoken=>$auth);
3778     $e->checkauth or return $e->event;
3779     $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3780     return undef;
3781 }
3782
3783 sub uber_hold_impl {
3784     my($e, $hold_id, $args) = @_;
3785     $args ||= {};
3786
3787     my $flesh_fields = ['current_copy', 'usr', 'notes'];
3788     push (@$flesh_fields, 'requestor') if $args->{include_requestor};
3789     push (@$flesh_fields, 'cancel_cause') if $args->{include_cancel_cause};
3790     push (@$flesh_fields, 'sms_carrier') if $args->{include_sms_carrier};
3791
3792     my $hold = $e->retrieve_action_hold_request([
3793         $hold_id,
3794         {flesh => 1, flesh_fields => {ahr => $flesh_fields}}
3795     ]) or return $e->event;
3796
3797     if($hold->usr->id ne $e->requestor->id) {
3798         # caller is asking for someone else's hold
3799         $e->allowed('VIEW_HOLD') or return $e->event;
3800         $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3801             [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3802
3803     } else {
3804         # caller is asking for own hold, but may not have permission to view staff notes
3805         unless($e->allowed('VIEW_HOLD')) {
3806             $hold->notes( # filter out any staff notes (unless marked as public)
3807                 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3808         }
3809     }
3810
3811     my $user = $hold->usr;
3812     $hold->usr($user->id);
3813
3814
3815     my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args);
3816
3817     flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3818     flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3819
3820     my $details = retrieve_hold_queue_status_impl($e, $hold);
3821     $hold->usr($user) if $args->{include_usr}; # re-flesh
3822
3823     my $resp = {
3824         hold    => $hold,
3825         bre_id  => $bre->id,
3826         ($copy     ? (copy           => $copy)     : ()),
3827         ($volume   ? (volume         => $volume)   : ()),
3828         ($issuance ? (issuance       => $issuance) : ()),
3829         ($part     ? (part           => $part)     : ()),
3830         ($args->{include_bre}  ?  (bre => $bre)    : ()),
3831         ($args->{suppress_mvr} ?  () : (mvr => $mvr)),
3832         %$details
3833     };
3834
3835     $resp->{copy}->location(
3836         $e->retrieve_asset_copy_location($resp->{copy}->location))
3837         if $resp->{copy} and $args->{flesh_acpl};
3838
3839     unless($args->{suppress_patron_details}) {
3840         my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3841         $resp->{patron_first}   = $user->first_given_name,
3842         $resp->{patron_last}    = $user->family_name,
3843         $resp->{patron_barcode} = $card->barcode,
3844         $resp->{patron_alias}   = $user->alias,
3845     };
3846
3847     return $resp;
3848 }
3849
3850
3851
3852 # -----------------------------------------------------
3853 # Returns the MVR object that represents what the
3854 # hold is all about
3855 # -----------------------------------------------------
3856 sub find_hold_mvr {
3857     my( $e, $hold, $args ) = @_;
3858
3859     my $tid;
3860     my $copy;
3861     my $volume;
3862     my $issuance;
3863     my $part;
3864     my $metarecord;
3865     my $no_mvr = $args->{suppress_mvr};
3866
3867     if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3868         $metarecord = $e->retrieve_metabib_metarecord($hold->target)
3869             or return $e->event;
3870         $tid = $metarecord->master_record;
3871
3872     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3873         $tid = $hold->target;
3874
3875     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3876         $volume = $e->retrieve_asset_call_number($hold->target)
3877             or return $e->event;
3878         $tid = $volume->record;
3879
3880     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3881         $issuance = $e->retrieve_serial_issuance([
3882             $hold->target,
3883             {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3884         ]) or return $e->event;
3885
3886         $tid = $issuance->subscription->record_entry;
3887
3888     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3889         $part = $e->retrieve_biblio_monograph_part([
3890             $hold->target
3891         ]) or return $e->event;
3892
3893         $tid = $part->record;
3894
3895     } 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 ) {
3896         $copy = $e->retrieve_asset_copy([
3897             $hold->target,
3898             {flesh => 1, flesh_fields => {acp => ['call_number']}}
3899         ]) or return $e->event;
3900
3901         $volume = $copy->call_number;
3902         $tid = $volume->record;
3903     }
3904
3905     if(!$copy and ref $hold->current_copy ) {
3906         $copy = $hold->current_copy;
3907         $hold->current_copy($copy->id) unless $args->{include_current_copy};
3908     }
3909
3910     if(!$volume and $copy) {
3911         $volume = $e->retrieve_asset_call_number($copy->call_number);
3912     }
3913
3914     # TODO return metarcord mvr for M holds
3915     my $title = $e->retrieve_biblio_record_entry($tid);
3916     return ( ($no_mvr) ? undef : $U->record_to_mvr($title), 
3917         $volume, $copy, $issuance, $part, $title, $metarecord);
3918 }
3919
3920 __PACKAGE__->register_method(
3921     method    => 'clear_shelf_cache',
3922     api_name  => 'open-ils.circ.hold.clear_shelf.get_cache',
3923     stream    => 1,
3924     signature => {
3925         desc => q/
3926             Returns the holds processed with the given cache key
3927         /
3928     }
3929 );
3930
3931 sub clear_shelf_cache {
3932     my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3933     my $e = new_editor(authtoken => $auth, xact => 1);
3934     return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3935
3936     $chunk_size ||= 25;
3937     $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3938
3939     my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3940
3941     if (!$hold_data) {
3942         $logger->info("no hold data found in cache"); # XXX TODO return event
3943         $e->rollback;
3944         return undef;
3945     }
3946
3947     my $maximum = 0;
3948     foreach (keys %$hold_data) {
3949         $maximum += scalar(@{ $hold_data->{$_} });
3950     }
3951     $client->respond({"maximum" => $maximum, "progress" => 0});
3952
3953     for my $action (sort keys %$hold_data) {
3954         while (@{$hold_data->{$action}}) {
3955             my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3956
3957             my $result_chunk = $e->json_query({
3958                 "select" => {
3959                     "acp" => ["barcode"],
3960                     "au" => [qw/
3961                         first_given_name second_given_name family_name alias
3962                     /],
3963                     "acn" => ["label"],
3964                     "acnp" => [{column => "label", alias => "prefix"}],
3965                     "acns" => [{column => "label", alias => "suffix"}],
3966                     "bre" => ["marc"],
3967                     "acpl" => ["name"],
3968                     "ahr" => ["id"]
3969                 },
3970                 "from" => {
3971                     "ahr" => {
3972                         "acp" => {
3973                             "field" => "id", "fkey" => "current_copy",
3974                             "join" => {
3975                                 "acn" => {
3976                                     "field" => "id", "fkey" => "call_number",
3977                                     "join" => {
3978                                         "bre" => {
3979                                             "field" => "id", "fkey" => "record"
3980                                         },
3981                                         "acnp" => {
3982                                             "field" => "id", "fkey" => "prefix"
3983                                         },
3984                                         "acns" => {
3985                                             "field" => "id", "fkey" => "suffix"
3986                                         }
3987                                     }
3988                                 },
3989                                 "acpl" => {"field" => "id", "fkey" => "location"}
3990                             }
3991                         },
3992                         "au" => {"field" => "id", "fkey" => "usr"}
3993                     }
3994                 },
3995                 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3996             }, {"substream" => 1}) or return $e->die_event;
3997
3998             $client->respond([
3999                 map {
4000                     +{"action" => $action, "hold_details" => $_}
4001                 } @$result_chunk
4002             ]);
4003         }
4004     }
4005
4006     $e->rollback;
4007     return undef;
4008 }
4009
4010
4011 __PACKAGE__->register_method(
4012     method    => 'clear_shelf_process',
4013     stream    => 1,
4014     api_name  => 'open-ils.circ.hold.clear_shelf.process',
4015     signature => {
4016         desc => q/
4017             1. Find all holds that have expired on the holds shelf
4018             2. Cancel the holds
4019             3. If a clear-shelf status is configured, put targeted copies into this status
4020             4. Divide copies into 3 groups: items to transit, items to reshelve, and items
4021                 that are needed for holds.  No subsequent action is taken on the holds
4022                 or items after grouping.
4023         /
4024     }
4025 );
4026
4027 sub clear_shelf_process {
4028     my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
4029
4030     my $e = new_editor(authtoken=>$auth);
4031     $e->checkauth or return $e->die_event;
4032     my $cache = OpenSRF::Utils::Cache->new('global');
4033
4034     $org_id ||= $e->requestor->ws_ou;
4035     $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
4036
4037     my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
4038
4039     my @hold_ids = $self->method_lookup(
4040         "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
4041     )->run($auth, $org_id, $match_copy);
4042
4043     $e->xact_begin;
4044
4045     my @holds;
4046     my @canceled_holds; # newly canceled holds
4047     $chunk_size ||= 25; # chunked status updates
4048     $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
4049
4050     my $counter = 0;
4051     for my $hold_id (@hold_ids) {
4052
4053         $logger->info("Clear shelf processing hold $hold_id");
4054
4055         my $hold = $e->retrieve_action_hold_request([
4056             $hold_id, {
4057                 flesh => 1,
4058                 flesh_fields => {ahr => ['current_copy']}
4059             }
4060         ]);
4061
4062         if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
4063             $hold->cancel_time('now');
4064             $hold->cancel_cause(2); # Hold Shelf expiration
4065             $e->update_action_hold_request($hold) or return $e->die_event;
4066             push(@canceled_holds, $hold_id);
4067         }
4068
4069         my $copy = $hold->current_copy;
4070
4071         if($copy_status or $copy_status == 0) {
4072             # if a clear-shelf copy status is defined, update the copy
4073             $copy->status($copy_status);
4074             $copy->edit_date('now');
4075             $copy->editor($e->requestor->id);
4076             $e->update_asset_copy($copy) or return $e->die_event;
4077         }
4078
4079         push(@holds, $hold);
4080         $client->respond({maximum => int(scalar(@holds)), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
4081     }
4082
4083     if ($e->commit) {
4084
4085         my %cache_data = (
4086             hold => [],
4087             transit => [],
4088             shelf => [],
4089             pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
4090         );
4091
4092         for my $hold (@holds) {
4093
4094             my $copy = $hold->current_copy;
4095             my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
4096
4097             if($alt_hold and !$match_copy) {
4098
4099                 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
4100
4101             } elsif($copy->circ_lib != $e->requestor->ws_ou) {
4102
4103                 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
4104
4105             } else {
4106
4107                 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
4108             }
4109         }
4110
4111         my $cache_key = md5_hex(time . $$ . rand());
4112         $logger->info("clear_shelf_cache: storing under $cache_key");
4113         $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours.  configurable?
4114
4115         # tell the client we're done
4116         $client->respond_complete({cache_key => $cache_key});
4117
4118         # ------------
4119         # fire off the hold cancelation trigger and wait for response so don't flood the service
4120
4121         # refetch the holds to pick up the caclulated cancel_time,
4122         # which may be needed by Action/Trigger
4123         $e->xact_begin;
4124         my $updated_holds = [];
4125         $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
4126         $e->rollback;
4127
4128         $U->create_events_for_hook(
4129             'hold_request.cancel.expire_holds_shelf',
4130             $_, $org_id, undef, undef, 1) for @$updated_holds;
4131
4132     } else {
4133         # tell the client we're done
4134         $client->respond_complete;
4135     }
4136 }
4137
4138 # returns IDs for holds that are on the holds shelf but 
4139 # have had their pickup_libs change while on the shelf.
4140 sub pickup_lib_changed_on_shelf_holds {
4141     my $e = shift;
4142     my $org_id = shift;
4143     my $ignore_holds = shift;
4144     $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
4145
4146     my $query = {
4147         select => { alhr => ['id'] },
4148         from   => {
4149             alhr => {
4150                 acp => {
4151                     field => 'id',
4152                     fkey  => 'current_copy'
4153                 },
4154             }
4155         },
4156         where => {
4157             '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
4158             '+alhr' => {
4159                 capture_time     => { "!=" => undef },
4160                 fulfillment_time => undef,
4161                 current_shelf_lib => $org_id,
4162                 pickup_lib => {'!='  => {'+alhr' => 'current_shelf_lib'}}
4163             }
4164         }
4165     };
4166
4167     $query->{where}->{'+alhr'}->{id} =
4168         {'not in' => $ignore_holds} if @$ignore_holds;
4169
4170     my $hold_ids = $e->json_query($query);
4171     return [ map { $_->{id} } @$hold_ids ];
4172 }
4173
4174 __PACKAGE__->register_method(
4175     method    => 'usr_hold_summary',
4176     api_name  => 'open-ils.circ.holds.user_summary',
4177     signature => q/
4178         Returns a summary of holds statuses for a given user
4179     /
4180 );
4181
4182 sub usr_hold_summary {
4183     my($self, $conn, $auth, $user_id) = @_;
4184
4185     my $e = new_editor(authtoken=>$auth);
4186     $e->checkauth or return $e->event;
4187     $e->allowed('VIEW_HOLD') or return $e->event;
4188
4189     my $holds = $e->search_action_hold_request(
4190         {
4191             usr =>  $user_id ,
4192             fulfillment_time => undef,
4193             cancel_time      => undef,
4194         }
4195     );
4196
4197     my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
4198     $summary{_hold_status($e, $_)} += 1 for @$holds;
4199     return \%summary;
4200 }
4201
4202
4203
4204 __PACKAGE__->register_method(
4205     method    => 'hold_has_copy_at',
4206     api_name  => 'open-ils.circ.hold.has_copy_at',
4207     signature => {
4208         desc   =>
4209                 'Returns the ID of the found copy and name of the shelving location if there is ' .
4210                 'an available copy at the specified org unit.  Returns empty hash otherwise.  '   .
4211                 'The anticipated use for this method is to determine whether an item is '         .
4212                 'available at the library where the user is placing the hold (or, alternatively, '.
4213                 'at the pickup library) to encourage bypassing the hold placement and just '      .
4214                 'checking out the item.' ,
4215         params => [
4216             { desc => 'Authentication Token', type => 'string' },
4217             { desc => 'Method Arguments.  Options include: hold_type, hold_target, org_unit.  '
4218                     . 'hold_type is the hold type code (T, V, C, M, ...).  '
4219                     . 'hold_target is the identifier of the hold target object.  '
4220                     . 'org_unit is org unit ID.',
4221               type => 'object'
4222             }
4223         ],
4224         return => {
4225             desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
4226             type => 'object'
4227         }
4228     }
4229 );
4230
4231 sub hold_has_copy_at {
4232     my($self, $conn, $auth, $args) = @_;
4233
4234     my $e = new_editor(authtoken=>$auth);
4235     $e->checkauth or return $e->event;
4236
4237     my $hold_type   = $$args{hold_type};
4238     my $hold_target = $$args{hold_target};
4239     my $org_unit    = $$args{org_unit};
4240
4241     my $query = {
4242         select => {acp => ['id'], acpl => ['name']},
4243         from   => {
4244             acp => {
4245                 acpl => {
4246                     field => 'id',
4247                     filter => { holdable => 't', deleted => 'f' },
4248                     fkey => 'location'
4249                 },
4250                 ccs  => {field => 'id', filter => {holdable => 't', is_available => 't'}, fkey => 'status'}
4251             }
4252         },
4253         where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit }},
4254         limit => 1
4255     };
4256
4257     if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
4258
4259         $query->{where}->{'+acp'}->{id} = $hold_target;
4260
4261     } elsif($hold_type eq 'V') {
4262
4263         $query->{where}->{'+acp'}->{call_number} = $hold_target;
4264
4265     } elsif($hold_type eq 'P') {
4266
4267         $query->{from}->{acp}->{acpm} = {
4268             field  => 'target_copy',
4269             fkey   => 'id',
4270             filter => {part => $hold_target},
4271         };
4272
4273     } elsif($hold_type eq 'I') {
4274
4275         $query->{from}->{acp}->{sitem} = {
4276             field  => 'unit',
4277             fkey   => 'id',
4278             filter => {issuance => $hold_target},
4279         };
4280
4281     } elsif($hold_type eq 'T') {
4282
4283         $query->{from}->{acp}->{acn} = {
4284             field  => 'id',
4285             fkey   => 'call_number',
4286             'join' => {
4287                 bre => {
4288                     field  => 'id',
4289                     filter => {id => $hold_target},
4290                     fkey   => 'record'
4291                 }
4292             }
4293         };
4294
4295     } else {
4296
4297         $query->{from}->{acp}->{acn} = {
4298             field => 'id',
4299             fkey  => 'call_number',
4300             join  => {
4301                 bre => {
4302                     field => 'id',
4303                     fkey  => 'record',
4304                     join  => {
4305                         mmrsm => {
4306                             field  => 'source',
4307                             fkey   => 'id',
4308                             filter => {metarecord => $hold_target},
4309                         }
4310                     }
4311                 }
4312             }
4313         };
4314     }
4315
4316     my $res = $e->json_query($query)->[0] or return {};
4317     return {copy => $res->{id}, location => $res->{name}} if $res;
4318 }
4319
4320
4321 # returns true if the user already has an item checked out
4322 # that could be used to fulfill the requested hold.
4323 sub hold_item_is_checked_out {
4324     my($e, $user_id, $hold_type, $hold_target) = @_;
4325
4326     my $query = {
4327         select => {acp => ['id']},
4328         from   => {acp => {}},
4329         where  => {
4330             '+acp' => {
4331                 id => {
4332                     in => { # copies for circs the user has checked out
4333                         select => {circ => ['target_copy']},
4334                         from   => 'circ',
4335                         where  => {
4336                             usr => $user_id,
4337                             checkin_time => undef,
4338                             '-or' => [
4339                                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
4340                                 {stop_fines => undef}
4341                             ],
4342                         }
4343                     }
4344                 }
4345             }
4346         },
4347         limit => 1
4348     };
4349
4350     if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
4351
4352         $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
4353
4354     } elsif($hold_type eq 'V') {
4355
4356         $query->{where}->{'+acp'}->{call_number} = $hold_target;
4357
4358      } elsif($hold_type eq 'P') {
4359
4360         $query->{from}->{acp}->{acpm} = {
4361             field  => 'target_copy',
4362             fkey   => 'id',
4363             filter => {part => $hold_target},
4364         };
4365
4366      } elsif($hold_type eq 'I') {
4367
4368         $query->{from}->{acp}->{sitem} = {
4369             field  => 'unit',
4370             fkey   => 'id',
4371             filter => {issuance => $hold_target},
4372         };
4373
4374     } elsif($hold_type eq 'T') {
4375
4376         $query->{from}->{acp}->{acn} = {
4377             field  => 'id',
4378             fkey   => 'call_number',
4379             'join' => {
4380                 bre => {
4381                     field  => 'id',
4382                     filter => {id => $hold_target},
4383                     fkey   => 'record'
4384                 }
4385             }
4386         };
4387
4388     } else {
4389
4390         $query->{from}->{acp}->{acn} = {
4391             field => 'id',
4392             fkey => 'call_number',
4393             join => {
4394                 bre => {
4395                     field => 'id',
4396                     fkey => 'record',
4397                     join => {
4398                         mmrsm => {
4399                             field => 'source',
4400                             fkey => 'id',
4401                             filter => {metarecord => $hold_target},
4402                         }
4403                     }
4404                 }
4405             }
4406         };
4407     }
4408
4409     return $e->json_query($query)->[0];
4410 }
4411
4412 __PACKAGE__->register_method(
4413     method    => 'change_hold_title',
4414     api_name  => 'open-ils.circ.hold.change_title',
4415     signature => {
4416         desc => q/
4417             Updates all title level holds targeting the specified bibs to point a new bib./,
4418         params => [
4419             { desc => 'Authentication Token', type => 'string' },
4420             { desc => 'New Target Bib Id',    type => 'number' },
4421             { desc => 'Old Target Bib Ids',   type => 'array'  },
4422         ],
4423         return => { desc => '1 on success' }
4424     }
4425 );
4426
4427 __PACKAGE__->register_method(
4428     method    => 'change_hold_title_for_specific_holds',
4429     api_name  => 'open-ils.circ.hold.change_title.specific_holds',
4430     signature => {
4431         desc => q/
4432             Updates specified holds to target new bib./,
4433         params => [
4434             { desc => 'Authentication Token', type => 'string' },
4435             { desc => 'New Target Bib Id',    type => 'number' },
4436             { desc => 'Holds Ids for holds to update',   type => 'array'  },
4437         ],
4438         return => { desc => '1 on success' }
4439     }
4440 );
4441
4442
4443 sub change_hold_title {
4444     my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4445
4446     my $e = new_editor(authtoken=>$auth, xact=>1);
4447     return $e->die_event unless $e->checkauth;
4448
4449     my $holds = $e->search_action_hold_request(
4450         [
4451             {
4452                 capture_time     => undef,
4453                 cancel_time      => undef,
4454                 fulfillment_time => undef,
4455                 hold_type        => 'T',
4456                 target           => $bib_ids
4457             },
4458             {
4459                 flesh        => 1,
4460                 flesh_fields => { ahr => ['usr'] }
4461             }
4462         ],
4463         { substream => 1 }
4464     );
4465
4466     for my $hold (@$holds) {
4467         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4468         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4469         $hold->target( $new_bib_id );
4470         $e->update_action_hold_request($hold) or return $e->die_event;
4471     }
4472
4473     $e->commit;
4474
4475     _reset_hold($self, $e->requestor, $_) for @$holds;
4476
4477     return 1;
4478 }
4479
4480 sub change_hold_title_for_specific_holds {
4481     my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4482
4483     my $e = new_editor(authtoken=>$auth, xact=>1);
4484     return $e->die_event unless $e->checkauth;
4485
4486     my $holds = $e->search_action_hold_request(
4487         [
4488             {
4489                 capture_time     => undef,
4490                 cancel_time      => undef,
4491                 fulfillment_time => undef,
4492                 hold_type        => 'T',
4493                 id               => $hold_ids
4494             },
4495             {
4496                 flesh        => 1,
4497                 flesh_fields => { ahr => ['usr'] }
4498             }
4499         ],
4500         { substream => 1 }
4501     );
4502
4503     for my $hold (@$holds) {
4504         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4505         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4506         $hold->target( $new_bib_id );
4507         $e->update_action_hold_request($hold) or return $e->die_event;
4508     }
4509
4510     $e->commit;
4511
4512     _reset_hold($self, $e->requestor, $_) for @$holds;
4513
4514     return 1;
4515 }
4516
4517 __PACKAGE__->register_method(
4518     method    => 'rec_hold_count',
4519     api_name  => 'open-ils.circ.bre.holds.count',
4520     signature => {
4521         desc => q/Returns the total number of holds that target the
4522             selected bib record or its associated copies and call_numbers/,
4523         params => [
4524             { desc => 'Bib ID', type => 'number' },
4525             { desc => q/Optional arguments.  Supported arguments include:
4526                 "pickup_lib_descendant" -> limit holds to those whose pickup
4527                 library is equal to or is a child of the provided org unit/,
4528                 type => 'object'
4529             }
4530         ],
4531         return => {desc => 'Hold count', type => 'number'}
4532     }
4533 );
4534
4535 __PACKAGE__->register_method(
4536     method    => 'rec_hold_count',
4537     api_name  => 'open-ils.circ.mmr.holds.count',
4538     signature => {
4539         desc => q/Returns the total number of holds that target the
4540             selected metarecord or its associated copies, call_numbers, and bib records/,
4541         params => [
4542             { desc => 'Metarecord ID', type => 'number' },
4543         ],
4544         return => {desc => 'Hold count', type => 'number'}
4545     }
4546 );
4547
4548 # XXX Need to add type I holds to these counts
4549 sub rec_hold_count {
4550     my($self, $conn, $target_id, $args) = @_;
4551     $args ||= {};
4552
4553     my $mmr_join = {
4554         mmrsm => {
4555             field => 'source',
4556             fkey => 'id',
4557             filter => {metarecord => $target_id}
4558         }
4559     };
4560
4561     my $bre_join = {
4562         bre => {
4563             field => 'id',
4564             filter => { id => $target_id },
4565             fkey => 'record'
4566         }
4567     };
4568
4569     if($self->api_name =~ /mmr/) {
4570         delete $bre_join->{bre}->{filter};
4571         $bre_join->{bre}->{join} = $mmr_join;
4572     }
4573
4574     my $cn_join = {
4575         acn => {
4576             field => 'id',
4577             fkey => 'call_number',
4578             join => $bre_join
4579         }
4580     };
4581
4582     my $query = {
4583         select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4584         from => 'ahr',
4585         where => {
4586             '+ahr' => {
4587                 cancel_time => undef,
4588                 fulfillment_time => undef,
4589                 '-or' => [
4590                     {
4591                         '-and' => {
4592                             hold_type => [qw/C F R/],
4593                             target => {
4594                                 in => {
4595                                     select => {acp => ['id']},
4596                                     from => { acp => $cn_join }
4597                                 }
4598                             }
4599                         }
4600                     },
4601                     {
4602                         '-and' => {
4603                             hold_type => 'V',
4604                             target => {
4605                                 in => {
4606                                     select => {acn => ['id']},
4607                                     from => {acn => $bre_join}
4608                                 }
4609                             }
4610                         }
4611                     },
4612                     {
4613                         '-and' => {
4614                             hold_type => 'P',
4615                             target => {
4616                                 in => {
4617                                     select => {bmp => ['id']},
4618                                     from => {bmp => $bre_join}
4619                                 }
4620                             }
4621                         }
4622                     },
4623                     {
4624                         '-and' => {
4625                             hold_type => 'T',
4626                             target => $target_id
4627                         }
4628                     }
4629                 ]
4630             }
4631         }
4632     };
4633
4634     if($self->api_name =~ /mmr/) {
4635         $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4636             '-and' => {
4637                 hold_type => 'T',
4638                 target => {
4639                     in => {
4640                         select => {bre => ['id']},
4641                         from => {bre => $mmr_join}
4642                     }
4643                 }
4644             }
4645         };
4646
4647         $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4648             '-and' => {
4649                 hold_type => 'M',
4650                 target => $target_id
4651             }
4652         };
4653     }
4654
4655
4656     if (my $pld = $args->{pickup_lib_descendant}) {
4657
4658         my $top_ou = new_editor()->search_actor_org_unit(
4659             {parent_ou => undef}
4660         )->[0]; # XXX Assumes single root node. Not alone in this...
4661
4662         $query->{where}->{'+ahr'}->{pickup_lib} = {
4663             in => {
4664                 select  => {aou => [{ 
4665                     column => 'id', 
4666                     transform => 'actor.org_unit_descendants', 
4667                     result_field => 'id' 
4668                 }]},
4669                 from    => 'aou',
4670                 where   => {id => $pld}
4671             }
4672         } if ($pld != $top_ou->id);
4673     }
4674
4675     # To avoid Internal Server Errors, we get an editor, then run the
4676     # query and check the result.  If anything fails, we'll return 0.
4677     my $result = 0;
4678     if (my $e = new_editor()) {
4679         my $query_result = $e->json_query($query);
4680         if ($query_result && @{$query_result}) {
4681             $result = $query_result->[0]->{count}
4682         }
4683     }
4684
4685     return $result;
4686 }
4687
4688 # A helper function to calculate a hold's expiration time at a given
4689 # org_unit. Takes the org_unit as an argument and returns either the
4690 # hold expire time as an ISO8601 string or undef if there is no hold
4691 # expiration interval set for the subject ou.
4692 sub calculate_expire_time
4693 {
4694     my $ou = shift;
4695     my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4696     if($interval) {
4697         my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
4698         return $U->epoch2ISO8601($date->epoch);
4699     }
4700     return undef;
4701 }
4702
4703
4704 __PACKAGE__->register_method(
4705     method    => 'mr_hold_filter_attrs',
4706     api_name  => 'open-ils.circ.mmr.holds.filters',
4707     authoritative => 1,
4708     stream => 1,
4709     signature => {
4710         desc => q/
4711             Returns the set of available formats and languages for the
4712             constituent records of the provided metarcord.
4713             If an array of hold IDs is also provided, information about
4714             each is returned as well.  This information includes:
4715             1. a slightly easier to read version of holdable_formats
4716             2. attributes describing the set of format icons included
4717                in the set of desired, constituent records.
4718         /,
4719         params => [
4720             {desc => 'Metarecord ID', type => 'number'},
4721             {desc => 'Context Org ID', type => 'number'},
4722             {desc => 'Hold ID List', type => 'array'},
4723         ],
4724         return => {
4725             desc => q/
4726                 Stream of objects.  The first will have a 'metarecord' key
4727                 containing non-hold-specific metarecord information, subsequent
4728                 responses will contain a 'hold' key containing hold-specific
4729                 information
4730             /, 
4731             type => 'object'
4732         }
4733     }
4734 );
4735
4736 sub mr_hold_filter_attrs { 
4737     my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4738     my $e = new_editor();
4739
4740     # by default, return MR / hold attributes for all constituent
4741     # records with holdable copies.  If there is a hard boundary,
4742     # though, limit to records with copies within the boundary,
4743     # since anything outside the boundary can never be held.
4744     my $org_depth = 0;
4745     if ($org_id) {
4746         $org_depth = $U->ou_ancestor_setting_value(
4747             $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4748     }
4749
4750     # get all org-scoped records w/ holdable copies for this metarecord
4751     my ($bre_ids) = $self->method_lookup(
4752         'open-ils.circ.holds.metarecord.filtered_records')->run(
4753             $mr_id, undef, $org_id, $org_depth);
4754
4755     my $item_lang_attr = 'item_lang'; # configurable?
4756     my $format_attr = $e->retrieve_config_global_flag(
4757         'opac.metarecord.holds.format_attr')->value;
4758
4759     # helper sub for fetching ccvms for a batch of record IDs
4760     sub get_batch_ccvms {
4761         my ($e, $attr, $bre_ids) = @_;
4762         return [] unless $bre_ids and @$bre_ids;
4763         my $vals = $e->search_metabib_record_attr_flat({
4764             attr => $attr,
4765             id => $bre_ids
4766         });
4767         return [] unless @$vals;
4768         return $e->search_config_coded_value_map({
4769             ctype => $attr,
4770             code => [map {$_->value} @$vals]
4771         });
4772     }
4773
4774     my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4775     my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4776
4777     $client->respond({
4778         metarecord => {
4779             id => $mr_id,
4780             formats => $formats,
4781             langs => $langs
4782         }
4783     });
4784
4785     return unless $hold_ids;
4786     my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4787     $icon_attr = $icon_attr ? $icon_attr->value : '';
4788
4789     for my $hold_id (@$hold_ids) {
4790         my $hold = $e->retrieve_action_hold_request($hold_id) 
4791             or return $e->event;
4792
4793         next unless $hold->hold_type eq 'M';
4794
4795         my $resp = {
4796             hold => {
4797                 id => $hold_id,
4798                 formats => [],
4799                 langs => []
4800             }
4801         };
4802
4803         # collect the ccvm's for the selected formats / language
4804         # (i.e. the holdable formats) on the MR.
4805         # this assumes a two-key structure for format / language,
4806         # though no assumption is made about the keys themselves.
4807         my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4808         my $lang_vals = [];
4809         my $format_vals = [];
4810         for my $val (values %$hformats) {
4811             # val is either a single ccvm or an array of them
4812             $val = [$val] unless ref $val eq 'ARRAY';
4813             for my $node (@$val) {
4814                 push (@$lang_vals, $node->{_val})   
4815                     if $node->{_attr} eq $item_lang_attr; 
4816                 push (@$format_vals, $node->{_val})   
4817                     if $node->{_attr} eq $format_attr;
4818             }
4819         }
4820
4821         # fetch the ccvm's for consistency with the {metarecord} blob
4822         $resp->{hold}{formats} = $e->search_config_coded_value_map({
4823             ctype => $format_attr, code => $format_vals});
4824         $resp->{hold}{langs} = $e->search_config_coded_value_map({
4825             ctype => $item_lang_attr, code => $lang_vals});
4826
4827         # find all of the bib records within this metarcord whose 
4828         # format / language match the holdable formats on the hold
4829         my ($bre_ids) = $self->method_lookup(
4830             'open-ils.circ.holds.metarecord.filtered_records')->run(
4831                 $hold->target, $hold->holdable_formats, 
4832                 $hold->selection_ou, $hold->selection_depth);
4833
4834         # now find all of the 'icon' attributes for the records
4835         $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4836         $client->respond($resp);
4837     }
4838
4839     return;
4840 }
4841
4842 __PACKAGE__->register_method(
4843     method        => "copy_has_holds_count",
4844     api_name      => "open-ils.circ.copy.has_holds_count",
4845     authoritative => 1,
4846     signature     => {
4847         desc => q/
4848             Returns the number of holds a paticular copy has
4849         /,
4850         params => [
4851             { desc => 'Authentication Token', type => 'string'},
4852             { desc => 'Copy ID', type => 'number'}
4853         ],
4854         return => {
4855             desc => q/
4856                 Simple count value
4857             /,
4858             type => 'number'
4859         }
4860     }
4861 );
4862
4863 sub copy_has_holds_count {
4864     my( $self, $conn, $auth, $copyid ) = @_;
4865     my $e = new_editor(authtoken=>$auth);
4866     return $e->event unless $e->checkauth;
4867
4868     if( $copyid && $copyid > 0 ) {
4869         my $meth = 'retrieve_action_has_holds_count';
4870         my $data = $e->$meth($copyid);
4871         if($data){
4872                 return $data->count();
4873         }
4874     }
4875     return 0;
4876 }
4877
4878 __PACKAGE__->register_method(
4879     method    => "retrieve_holds_by_usr_notify_value_staff",
4880     api_name  => "open-ils.circ.holds.retrieve_by_notify_staff",
4881     signature => {
4882         desc   => "Retrieve the hold, for the specified user using the notify value.  $ses_is_req_note",
4883         params => [
4884             { desc => 'Authentication token', type => 'string' },
4885             { desc => 'User ID',              type => 'number' },
4886             { desc => 'notify value',         type => 'string' },
4887             { desc => 'notify_type',          type => 'string' }
4888         ],
4889         return => {
4890             desc => 'Hold objects with transits attached, event on error',
4891         }
4892     }
4893 );
4894
4895 sub retrieve_holds_by_usr_notify_value_staff {
4896     
4897     my($self, $conn, $auth, $usr_id, $contact, $cType) = @_;
4898
4899     my $e = new_editor(authtoken=>$auth);
4900     $e->checkauth or return $e->event;
4901
4902     if ($e->requestor->id != $usr_id){
4903         $e->allowed('VIEW_HOLD') or return $e->event;
4904     }
4905
4906     my $q = {
4907         "select" => { "ahr" => ["id", "sms_notify", "phone_notify", "email_notify", "sms_carrier"]},
4908         "from" => "ahr",
4909         "where" => {
4910             "usr"          =>      $usr_id,
4911             "capture_time" =>      undef,
4912             "cancel_time"  =>      undef,
4913             "fulfillment_time" =>  undef,
4914         }
4915     };
4916
4917     if ($cType eq "day_phone" or $cType eq "evening_phone" or
4918         $cType eq "other_phone" or $cType eq "default_phone"){
4919             $q->{where}->{"-not"} = [
4920                 { "phone_notify" => { "=" => $contact} },
4921                 { "phone_notify" => { "<>" => undef } }
4922             ];
4923     }
4924
4925
4926     if ($cType eq "default_sms") {
4927         $q->{where}->{"-not"} = [
4928             { "sms_notify" => { "=" => $contact} },
4929             { "sms_notify" => { "<>" => undef } }
4930         ];
4931     }
4932
4933     if ($cType eq "default_sms_carrier_id") {
4934         $q->{where}->{"-not"} = [
4935             { "sms_carrier" => { "=" => int($contact)} },
4936             { "sms_carrier" => { "<>" => undef } }
4937         ];
4938     }
4939
4940     if ($cType =~ /notify/){
4941         # this is was notification pref change
4942         # we find all unfulfilled holds that match have that pref
4943         my $optr = $contact == 1 ? "<>" : "="; # unless it's email, true val means we want to query for not null
4944         my $conj = $optr eq '=' ? '-or' : '-and';
4945         if ($cType =~ /sms/) {
4946             $q->{where}->{$conj} = [ { sms_notify => { $optr => undef } }, { sms_notify => { $optr => '' } } ];
4947         }
4948         if ($cType =~ /phone/) {
4949             $q->{where}->{$conj} = [ { phone_notify => { $optr => undef } }, { phone_notify => { $optr => '' } } ];
4950         }
4951         if ($cType =~ /email/) {
4952             if ($contact) {
4953                 $q->{where}->{'+ahr'} = 'email_notify';
4954             } else {
4955                 $q->{where}->{'-not'} = {'+ahr' => 'email_notify'};
4956             }
4957         }
4958     }
4959
4960     my $holds = $e->json_query($q);
4961     #$hold_ids = [ map { $_->{id} } @$hold_ids ];
4962
4963     return $holds;
4964 }
4965
4966 __PACKAGE__->register_method(
4967     method    => "batch_update_holds_by_value_staff",
4968     api_name  => "open-ils.circ.holds.batch_update_holds_by_notify_staff",
4969     signature => {
4970         desc   => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
4971         params => [
4972             { desc => 'Authentication token', type => 'string' },
4973             { desc => 'User ID',              type => 'number' },
4974             { desc => 'Hold IDs',             type => 'array'  },
4975             { desc => 'old notify value',     type => 'string' },
4976             { desc => 'new notify value',     type => 'string' },
4977             { desc => 'field name',           type => 'string' },
4978             { desc => 'SMS carrier ID',       type => 'number' }
4979
4980         ],
4981         return => {
4982             desc => 'Hold objects with transits attached, event on error',
4983         }
4984     }
4985 );
4986
4987 sub batch_update_holds_by_value_staff {
4988     my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType, $carrierId) = @_;
4989
4990     my $e = new_editor(authtoken=>$auth, xact=>1);
4991     $e->checkauth or return $e->event;
4992     if ($e->requestor->id != $usr_id){
4993         $e->allowed('UPDATE_HOLD') or return $e->event;
4994     }
4995
4996     my @success;
4997     for my $id (@$hold_ids) {
4998         
4999         my $hold = $e->retrieve_action_hold_request($id);
5000
5001         if ($cType eq "day_phone" or $cType eq "evening_phone" or
5002             $cType eq "other_phone" or $cType eq "default_phone") {
5003
5004             if ($newval eq '') {
5005                 $hold->clear_phone_notify();
5006             }
5007             else {
5008                 $hold->phone_notify($newval);
5009             }
5010         }
5011         
5012         if ($cType eq "default_sms"){
5013             if ($newval eq '') {
5014                 $hold->clear_sms_notify();
5015                 $hold->clear_sms_carrier(); # TODO: prevent orphan sms_carrier, via db trigger
5016             }
5017             else {
5018                 $hold->sms_notify($newval);
5019                 $hold->sms_carrier($carrierId);
5020             }
5021
5022         }
5023
5024         if ($cType eq "default_sms_carrier_id") {
5025             $hold->sms_carrier($newval);
5026         }
5027
5028         if ($cType =~ /notify/){
5029             # this is a notification pref change
5030             if ($cType =~ /email/) { $hold->email_notify($newval); }
5031             if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5032             if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5033             # the other case, where x_notify is changed to true,
5034             # is covered by an actual value being assigned
5035         }
5036
5037         $e->update_action_hold_request($hold) or return $e->die_event;
5038         push @success, $id;
5039     }
5040
5041     #$e->disconnect;
5042     $e->commit; #unless $U->event_code($res);
5043     return \@success;
5044
5045 }
5046
5047
5048 __PACKAGE__->register_method(
5049     method    => "retrieve_holds_by_usr_with_notify",
5050     api_name  => "open-ils.circ.holds.retrieve.by_usr.with_notify",
5051     signature => {
5052         desc   => "Retrieve the hold, for the specified user using the notify value.  $ses_is_req_note",
5053         params => [
5054             { desc => 'Authentication token', type => 'string' },
5055             { desc => 'User ID',              type => 'number' },
5056         ],
5057         return => {
5058             desc => 'Lists of holds with notification values, event on error',
5059         }
5060     }
5061 );
5062
5063 sub retrieve_holds_by_usr_with_notify {
5064     
5065     my($self, $conn, $auth, $usr_id) = @_;
5066
5067     my $e = new_editor(authtoken=>$auth);
5068     $e->checkauth or return $e->event;
5069
5070     if ($e->requestor->id != $usr_id){
5071         $e->allowed('VIEW_HOLD') or return $e->event;
5072     }
5073
5074     my $q = {
5075         "select" => { "ahr" => ["id", "phone_notify", "email_notify", "sms_carrier", "sms_notify"]},
5076         "from" => "ahr",
5077         "where" => {
5078             "usr"          =>      $usr_id,
5079             "capture_time" =>      undef,
5080             "cancel_time"  =>      undef,
5081             "fulfillment_time" =>  undef,
5082         }
5083     };
5084
5085     my $holds = $e->json_query($q);
5086     return $holds;
5087 }
5088
5089 __PACKAGE__->register_method(
5090     method    => "batch_update_holds_by_value",
5091     api_name  => "open-ils.circ.holds.batch_update_holds_by_notify",
5092     signature => {
5093         desc   => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
5094         params => [
5095             { desc => 'Authentication token', type => 'string' },
5096             { desc => 'User ID',              type => 'number' },
5097             { desc => 'Hold IDs',             type => 'array'  },
5098             { desc => 'old notify value',     type => 'string' },
5099             { desc => 'new notify value',     type => 'string' },
5100             { desc => 'notify_type',          type => 'string' }
5101         ],
5102         return => {
5103             desc => 'Hold objects with transits attached, event on error',
5104         }
5105     }
5106 );
5107
5108 sub batch_update_holds_by_value {
5109     my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType) = @_;
5110
5111     my $e = new_editor(authtoken=>$auth, xact=>1);
5112     $e->checkauth or return $e->event;
5113     if ($e->requestor->id != $usr_id){
5114         $e->allowed('UPDATE_HOLD') or return $e->event;
5115     }
5116
5117     my @success;
5118     for my $id (@$hold_ids) {
5119         
5120         my $hold = $e->retrieve_action_hold_request(int($id));
5121
5122         if ($cType eq "day_phone" or $cType eq "evening_phone" or
5123             $cType eq "other_phone" or $cType eq "default_phone") {
5124             # change phone number value on hold
5125             $hold->phone_notify($newval);
5126         }
5127         if ($cType eq "default_sms") {
5128             # change SMS number value on hold
5129             $hold->sms_notify($newval);
5130         }
5131
5132         if ($cType eq "default_sms_carrier_id") {
5133             $hold->sms_carrier(int($newval));
5134         }
5135
5136         if ($cType =~ /notify/){
5137             # this is a notification pref change
5138             if ($cType =~ /email/) { $hold->email_notify($newval); }
5139             if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5140             if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5141             # the other case, where x_notify is changed to true,
5142             # is covered by an actual value being assigned
5143         }
5144
5145         $e->update_action_hold_request($hold) or return $e->die_event;
5146         push @success, $id;
5147     }
5148
5149     #$e->disconnect;
5150     $e->commit; #unless $U->event_code($res);
5151     return \@success;
5152 }
5153
5154 __PACKAGE__->register_method(
5155     method        => "hold_metadata",
5156     api_name      => "open-ils.circ.hold.get_metadata",
5157     authoritative => 1,
5158     stream => 1,
5159     signature     => {
5160         desc => q/
5161             Returns a stream of objects containing whatever bib, 
5162             volume, etc. data is available to the specific hold 
5163             type and target.
5164         /,
5165         params => [
5166             {desc => 'Hold Type', type => 'string'},
5167             {desc => 'Hold Target(s)', type => 'number or array'},
5168             {desc => 'Context org unit (optional)', type => 'number'}
5169         ],
5170         return => {
5171             desc => q/
5172                 Stream of hold metadata objects.
5173             /,
5174             type => 'object'
5175         }
5176     }
5177 );
5178
5179
5180 sub hold_metadata {
5181     my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
5182
5183     $hold_targets = [$hold_targets] unless ref $hold_targets;
5184
5185     my $e = new_editor();
5186     for my $target (@$hold_targets) {
5187
5188         # create a dummy hold for find_hold_mvr
5189         my $hold = Fieldmapper::action::hold_request->new;
5190         $hold->hold_type($hold_type);
5191         $hold->target($target);
5192
5193         my (undef, $volume, $copy, $issuance, $part, $bre, $metarecord) = 
5194             find_hold_mvr($e, $hold, {suppress_mvr => 1});
5195
5196         $bre->clear_marc; # avoid bulk
5197
5198         my $meta = {
5199             target => $target,
5200             copy => $copy,
5201             volume => $volume,
5202             issuance => $issuance,
5203             part => $part,
5204             parts => [],
5205             part_required => 'f',
5206             bibrecord => $bre,
5207             metarecord => $metarecord,
5208             metarecord_filters => {}
5209         };
5210
5211         # If this is a bib hold or metarecord hold, also return the
5212         # available set of MR filters (AKA "Holdable Formats") for the
5213         # hold.  For bib holds these may be used to upgrade the hold
5214         # from a bib to metarecord hold.
5215         if ($hold_type eq 'T') {
5216             my $map = $e->search_metabib_metarecord_source_map(
5217                 {source => $meta->{bibrecord}->id})->[0];
5218
5219             if ($map) {
5220                 $meta->{metarecord} = 
5221                     $e->retrieve_metabib_metarecord($map->metarecord);
5222             }
5223
5224             # Also fetch the available parts for bib-level holds.
5225             $meta->{parts} = $e->search_biblio_monograph_part(
5226                 [
5227                     {record => $bre->id, deleted => 'f'},
5228                     {order_by => {bmp => 'label_sortkey'}}
5229                 ]
5230             );
5231
5232             # T holds on records that have parts are normally OK, but if the record has
5233             # no non-part copies, the hold will ultimately fail.  When that happens,
5234             # require the user to select a part.
5235             #
5236             # If the global flag circ.holds.api_require_monographic_part_when_present is
5237             # enabled, or the library setting circ.holds.ui_require_monographic_part_when_present
5238             # is true for any involved owning_library, then also require part selection.
5239             my $part_required = 0;
5240             if ($meta->{parts}) {
5241                 my $part_required_flag = $e->retrieve_config_global_flag('circ.holds.api_require_monographic_part_when_present');
5242                 $part_required = ($part_required_flag and $U->is_true($part_required_flag->enabled));
5243                 if (!$part_required) {
5244                     my $resp = $e->json_query({
5245                         select => {
5246                             acn => ['owning_lib']
5247                         },
5248                         from => {acn => {acp => {type => 'left'}}},
5249                         where => {
5250                             '+acp' => {
5251                                 '-or' => [
5252                                     {deleted => 'f'},
5253                                     {id => undef} # left join
5254                                 ]
5255                             },
5256                             '+acn' => {deleted => 'f', record => $bre->id}
5257                         },
5258                         distinct => 't'
5259                     });
5260                     my $org_ids = [map {$_->{owning_lib}} @$resp];
5261                     foreach my $org (@$org_ids) { # FIXME: worth shortcutting/optimizing?
5262                         if ($U->ou_ancestor_setting_value($org, 'circ.holds.ui_require_monographic_part_when_present')) {
5263                             $part_required = 1;
5264                         }
5265                     }
5266                 }
5267                 if (!$part_required) {
5268                     my $np_copies = $e->json_query({
5269                         select => { acp => [{column => 'id', transform => 'count', alias => 'count'}]},
5270                         from => {acp => {acn => {}, acpm => {type => 'left'}}},
5271                         where => {
5272                             '+acp' => {deleted => 'f'},
5273                             '+acn' => {deleted => 'f', record => $bre->id},
5274                             '+acpm' => {id => undef}
5275                         }
5276                     });
5277                     $part_required = 1 if $np_copies->[0]->{count} == 0;
5278                 }
5279             }
5280             $meta->{part_required} = $part_required;
5281         }
5282
5283         if ($meta->{metarecord}) {
5284
5285             my ($filters) = 
5286                 $self->method_lookup('open-ils.circ.mmr.holds.filters')
5287                     ->run($meta->{metarecord}->id, $org_id);
5288
5289             if ($filters) {
5290                 $meta->{metarecord_filters} = $filters->{metarecord};
5291             }
5292         }
5293
5294         $client->respond($meta);
5295     }
5296
5297     return undef;
5298 }
5299
5300 1;