]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
LP 1904737: Expand copy statuses for the pull list
[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         }, {idlist=>1} );
2899
2900     if (@$parts) {
2901         my $part_required_flag = $e->retrieve_config_global_flag('circ.holds.api_require_monographic_part_when_present');
2902         $part_required = ($part_required_flag and $U->is_true($part_required_flag->enabled));
2903         if (!$part_required) {
2904             my $np_copies = $e->json_query({
2905                 select => { acp => [{column => 'id', transform => 'count', alias => 'count'}]},
2906                 from => {acp => {acn => {}, acpm => {type => 'left'}}},
2907                 where => {
2908                     '+acp' => {deleted => 'f'},
2909                     '+acn' => {deleted => 'f', record => $titleid},
2910                     '+acpm' => {id => undef}
2911                 }
2912             });
2913             $part_required = 1 if $np_copies->[0]->{count} == 0;
2914         }
2915     }
2916     if ($part_required) {
2917         $logger->info("title hold when monographic part required");
2918         return (
2919             0, 0, [
2920                 new OpenILS::Event(
2921                     "TITLE_HOLD_WHEN_MONOGRAPHIC_PART_REQUIRED",
2922                     "payload" => {"fail_part" => "monographic_part_required"}
2923                 )
2924             ]
2925         );
2926     }
2927
2928     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2929
2930     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2931     my $copies = $e->json_query(
2932         {
2933             select => { acp => ['id', 'circ_lib'] },
2934               from => {
2935                 acp => {
2936                     acn => {
2937                         field  => 'id',
2938                         fkey   => 'call_number',
2939                         filter => { record => $titleid }
2940                     },
2941                     acpl => {
2942                                 field => 'id',
2943                                 filter => { holdable => 't', deleted => 'f' },
2944                                 fkey => 'location'
2945                             },
2946                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   },
2947                     acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2948                 }
2949             },
2950             where => {
2951                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2952                 '+acpm' => { target_copy => undef } # ignore part-linked copies
2953             }
2954         }
2955     );
2956
2957     $logger->info("title possible found ".scalar(@$copies)." potential copies");
2958     return (
2959         0, 0, [
2960             new OpenILS::Event(
2961                 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2962                 "payload" => {"fail_part" => "no_ultimate_items"}
2963             )
2964         ]
2965     ) unless @$copies;
2966
2967     # -----------------------------------------------------------------------
2968     # sort the copies into buckets based on their circ_lib proximity to
2969     # the patron's home_ou.
2970     # -----------------------------------------------------------------------
2971
2972     my $home_org = $patron->home_ou;
2973     my $req_org = $request_lib->id;
2974
2975     $prox_cache{$home_org} =
2976         $e->search_actor_org_unit_proximity({from_org => $home_org})
2977         unless $prox_cache{$home_org};
2978     my $home_prox = $prox_cache{$home_org};
2979     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2980
2981     my %buckets;
2982     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2983     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2984
2985     my @keys = sort { $a <=> $b } keys %buckets;
2986
2987
2988     if( $home_org ne $req_org ) {
2989       # -----------------------------------------------------------------------
2990       # shove the copies close to the request_lib into the primary buckets
2991       # directly before the farthest away copies.  That way, they are not
2992       # given priority, but they are checked before the farthest copies.
2993       # -----------------------------------------------------------------------
2994         $prox_cache{$req_org} =
2995             $e->search_actor_org_unit_proximity({from_org => $req_org})
2996             unless $prox_cache{$req_org};
2997         my $req_prox = $prox_cache{$req_org};
2998
2999         my %buckets2;
3000         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3001         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3002
3003         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
3004         my $new_key = $highest_key - 0.5; # right before the farthest prox
3005         my @keys2   = sort { $a <=> $b } keys %buckets2;
3006         for my $key (@keys2) {
3007             last if $key >= $highest_key;
3008             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3009         }
3010     }
3011
3012     @keys = sort { $a <=> $b } keys %buckets;
3013
3014     my $title;
3015     my %seen;
3016     my @status;
3017     my $age_protect_only = 0;
3018     OUTER: for my $key (@keys) {
3019       my @cps = @{$buckets{$key}};
3020
3021       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3022
3023       for my $copyid (@cps) {
3024
3025          next if $seen{$copyid};
3026          $seen{$copyid} = 1; # there could be dupes given the merged buckets
3027          my $copy = $e->retrieve_asset_copy($copyid);
3028          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3029
3030          unless($title) { # grab the title if we don't already have it
3031             my $vol = $e->retrieve_asset_call_number(
3032                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3033             $title = $vol->record;
3034          }
3035
3036          @status = verify_copy_for_hold(
3037             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3038
3039          $age_protect_only ||= $status[3];
3040          last OUTER if $status[0];
3041       }
3042     }
3043
3044     $status[3] = $age_protect_only;
3045     return @status;
3046 }
3047
3048 sub _check_issuance_hold_is_possible {
3049     my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3050
3051     my $e = new_editor();
3052     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3053
3054     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3055     my $copies = $e->json_query(
3056         {
3057             select => { acp => ['id', 'circ_lib'] },
3058               from => {
3059                 acp => {
3060                     sitem => {
3061                         field  => 'unit',
3062                         fkey   => 'id',
3063                         filter => { issuance => $issuanceid }
3064                     },
3065                     acpl => {
3066                         field => 'id',
3067                         filter => { holdable => 't', deleted => 'f' },
3068                         fkey => 'location'
3069                     },
3070                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
3071                 }
3072             },
3073             where => {
3074                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3075             },
3076             distinct => 1
3077         }
3078     );
3079
3080     $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
3081
3082     my $empty_ok;
3083     if (!@$copies) {
3084         $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3085         $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3086
3087         return (
3088             0, 0, [
3089                 new OpenILS::Event(
3090                     "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3091                     "payload" => {"fail_part" => "no_ultimate_items"}
3092                 )
3093             ]
3094         ) unless $empty_ok;
3095
3096         return (1, 0);
3097     }
3098
3099     # -----------------------------------------------------------------------
3100     # sort the copies into buckets based on their circ_lib proximity to
3101     # the patron's home_ou.
3102     # -----------------------------------------------------------------------
3103
3104     my $home_org = $patron->home_ou;
3105     my $req_org = $request_lib->id;
3106
3107     $prox_cache{$home_org} =
3108         $e->search_actor_org_unit_proximity({from_org => $home_org})
3109         unless $prox_cache{$home_org};
3110     my $home_prox = $prox_cache{$home_org};
3111     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3112
3113     my %buckets;
3114     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3115     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3116
3117     my @keys = sort { $a <=> $b } keys %buckets;
3118
3119
3120     if( $home_org ne $req_org ) {
3121       # -----------------------------------------------------------------------
3122       # shove the copies close to the request_lib into the primary buckets
3123       # directly before the farthest away copies.  That way, they are not
3124       # given priority, but they are checked before the farthest copies.
3125       # -----------------------------------------------------------------------
3126         $prox_cache{$req_org} =
3127             $e->search_actor_org_unit_proximity({from_org => $req_org})
3128             unless $prox_cache{$req_org};
3129         my $req_prox = $prox_cache{$req_org};
3130
3131         my %buckets2;
3132         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3133         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3134
3135         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
3136         my $new_key = $highest_key - 0.5; # right before the farthest prox
3137         my @keys2   = sort { $a <=> $b } keys %buckets2;
3138         for my $key (@keys2) {
3139             last if $key >= $highest_key;
3140             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3141         }
3142     }
3143
3144     @keys = sort { $a <=> $b } keys %buckets;
3145
3146     my $title;
3147     my %seen;
3148     my @status;
3149     my $age_protect_only = 0;
3150     OUTER: for my $key (@keys) {
3151       my @cps = @{$buckets{$key}};
3152
3153       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3154
3155       for my $copyid (@cps) {
3156
3157          next if $seen{$copyid};
3158          $seen{$copyid} = 1; # there could be dupes given the merged buckets
3159          my $copy = $e->retrieve_asset_copy($copyid);
3160          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3161
3162          unless($title) { # grab the title if we don't already have it
3163             my $vol = $e->retrieve_asset_call_number(
3164                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3165             $title = $vol->record;
3166          }
3167
3168          @status = verify_copy_for_hold(
3169             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3170
3171          $age_protect_only ||= $status[3];
3172          last OUTER if $status[0];
3173       }
3174     }
3175
3176     if (!$status[0]) {
3177         if (!defined($empty_ok)) {
3178             $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3179             $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3180         }
3181
3182         return (1,0) if ($empty_ok);
3183     }
3184     $status[3] = $age_protect_only;
3185     return @status;
3186 }
3187
3188 sub _check_monopart_hold_is_possible {
3189     my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3190
3191     my $e = new_editor();
3192     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3193
3194     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3195     my $copies = $e->json_query(
3196         {
3197             select => { acp => ['id', 'circ_lib'] },
3198               from => {
3199                 acp => {
3200                     acpm => {
3201                         field  => 'target_copy',
3202                         fkey   => 'id',
3203                         filter => { part => $partid }
3204                     },
3205                     acpl => {
3206                         field => 'id',
3207                         filter => { holdable => 't', deleted => 'f' },
3208                         fkey => 'location'
3209                     },
3210                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
3211                 }
3212             },
3213             where => {
3214                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3215             },
3216             distinct => 1
3217         }
3218     );
3219
3220     $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
3221
3222     my $empty_ok;
3223     if (!@$copies) {
3224         $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3225         $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3226
3227         return (
3228             0, 0, [
3229                 new OpenILS::Event(
3230                     "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3231                     "payload" => {"fail_part" => "no_ultimate_items"}
3232                 )
3233             ]
3234         ) unless $empty_ok;
3235
3236         return (1, 0);
3237     }
3238
3239     # -----------------------------------------------------------------------
3240     # sort the copies into buckets based on their circ_lib proximity to
3241     # the patron's home_ou.
3242     # -----------------------------------------------------------------------
3243
3244     my $home_org = $patron->home_ou;
3245     my $req_org = $request_lib->id;
3246
3247     $prox_cache{$home_org} =
3248         $e->search_actor_org_unit_proximity({from_org => $home_org})
3249         unless $prox_cache{$home_org};
3250     my $home_prox = $prox_cache{$home_org};
3251     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3252
3253     my %buckets;
3254     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3255     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3256
3257     my @keys = sort { $a <=> $b } keys %buckets;
3258
3259
3260     if( $home_org ne $req_org ) {
3261       # -----------------------------------------------------------------------
3262       # shove the copies close to the request_lib into the primary buckets
3263       # directly before the farthest away copies.  That way, they are not
3264       # given priority, but they are checked before the farthest copies.
3265       # -----------------------------------------------------------------------
3266         $prox_cache{$req_org} =
3267             $e->search_actor_org_unit_proximity({from_org => $req_org})
3268             unless $prox_cache{$req_org};
3269         my $req_prox = $prox_cache{$req_org};
3270
3271         my %buckets2;
3272         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3273         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3274
3275         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
3276         my $new_key = $highest_key - 0.5; # right before the farthest prox
3277         my @keys2   = sort { $a <=> $b } keys %buckets2;
3278         for my $key (@keys2) {
3279             last if $key >= $highest_key;
3280             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3281         }
3282     }
3283
3284     @keys = sort { $a <=> $b } keys %buckets;
3285
3286     my $title;
3287     my %seen;
3288     my @status;
3289     my $age_protect_only = 0;
3290     OUTER: for my $key (@keys) {
3291       my @cps = @{$buckets{$key}};
3292
3293       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3294
3295       for my $copyid (@cps) {
3296
3297          next if $seen{$copyid};
3298          $seen{$copyid} = 1; # there could be dupes given the merged buckets
3299          my $copy = $e->retrieve_asset_copy($copyid);
3300          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3301
3302          unless($title) { # grab the title if we don't already have it
3303             my $vol = $e->retrieve_asset_call_number(
3304                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3305             $title = $vol->record;
3306          }
3307
3308          @status = verify_copy_for_hold(
3309             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3310
3311          $age_protect_only ||= $status[3];
3312          last OUTER if $status[0];
3313       }
3314     }
3315
3316     if (!$status[0]) {
3317         if (!defined($empty_ok)) {
3318             $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3319             $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3320         }
3321
3322         return (1,0) if ($empty_ok);
3323     }
3324     $status[3] = $age_protect_only;
3325     return @status;
3326 }
3327
3328
3329 sub _check_volume_hold_is_possible {
3330     my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3331     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
3332     my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
3333     $logger->info("checking possibility of volume hold for volume ".$vol->id);
3334
3335     my $filter_copies = [];
3336     for my $copy (@$copies) {
3337         # ignore part-mapped copies for regular volume level holds
3338         push(@$filter_copies, $copy) unless
3339             new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
3340     }
3341     $copies = $filter_copies;
3342
3343     return (
3344         0, 0, [
3345             new OpenILS::Event(
3346                 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3347                 "payload" => {"fail_part" => "no_ultimate_items"}
3348             )
3349         ]
3350     ) unless @$copies;
3351
3352     my @status;
3353     my $age_protect_only = 0;
3354     for my $copy ( @$copies ) {
3355         @status = verify_copy_for_hold(
3356             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3357         $age_protect_only ||= $status[3];
3358         last if $status[0];
3359     }
3360     $status[3] = $age_protect_only;
3361     return @status;
3362 }
3363
3364
3365
3366 sub verify_copy_for_hold {
3367     my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3368     # $oargs should be undef unless we're overriding.
3369     $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3370     my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3371         {
3372             patron           => $patron,
3373             requestor        => $requestor,
3374             copy             => $copy,
3375             title            => $title,
3376             title_descriptor => $title->fixed_fields,
3377             pickup_lib       => $pickup_lib,
3378             request_lib      => $request_lib,
3379             new_hold         => 1,
3380             show_event_list  => 1
3381         }
3382     );
3383
3384     # Check for override permissions on events.
3385     if ($oargs && $permitted && scalar @$permitted) {
3386         # Remove the events from permitted that we can override.
3387         if ($oargs->{events}) {
3388             foreach my $evt (@{$oargs->{events}}) {
3389                 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3390             }
3391         }
3392         # Now, we handle the override all case by checking remaining
3393         # events against override permissions.
3394         if (scalar @$permitted && $oargs->{all}) {
3395             # Pre-set events and failed members of oargs to empty
3396             # arrays, if they are not set, yet.
3397             $oargs->{events} = [] unless ($oargs->{events});
3398             $oargs->{failed} = [] unless ($oargs->{failed});
3399             # When we're done with these checks, we swap permitted
3400             # with a reference to @disallowed.
3401             my @disallowed = ();
3402             foreach my $evt (@{$permitted}) {
3403                 # Check if we've already seen the event in this
3404                 # session and it failed.
3405                 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3406                     push(@disallowed, $evt);
3407                 } else {
3408                     # We have to check if the requestor has the
3409                     # override permission.
3410
3411                     # AppUtils::check_user_perms returns the perm if
3412                     # the user doesn't have it, undef if they do.
3413                     if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3414                         push(@disallowed, $evt);
3415                         push(@{$oargs->{failed}}, $evt->{textcode});
3416                     } else {
3417                         push(@{$oargs->{events}}, $evt->{textcode});
3418                     }
3419                 }
3420             }
3421             $permitted = \@disallowed;
3422         }
3423     }
3424
3425     my $age_protect_only = 0;
3426     if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3427         $age_protect_only = 1;
3428     }
3429
3430     return (
3431         (not scalar @$permitted), # true if permitted is an empty arrayref
3432         (   # XXX This test is of very dubious value; someone should figure
3433             # out what if anything is checking this value
3434             ($copy->circ_lib == $pickup_lib) and
3435             ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3436         ),
3437         $permitted,
3438         $age_protect_only
3439     );
3440 }
3441
3442
3443
3444 sub find_nearest_permitted_hold {
3445
3446     my $class  = shift;
3447     my $editor = shift;     # CStoreEditor object
3448     my $copy   = shift;     # copy to target
3449     my $user   = shift;     # staff
3450     my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3451
3452     my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3453
3454     my $bc = $copy->barcode;
3455
3456     # find any existing holds that already target this copy
3457     my $old_holds = $editor->search_action_hold_request(
3458         {    current_copy => $copy->id,
3459             cancel_time  => undef,
3460             capture_time => undef
3461         }
3462     );
3463
3464     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3465
3466     $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3467         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3468
3469     my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3470
3471     # the nearest_hold API call now needs this
3472     $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3473         unless ref $copy->call_number;
3474
3475     # search for what should be the best holds for this copy to fulfill
3476     my $best_holds = $U->storagereq(
3477         "open-ils.storage.action.hold_request.nearest_hold.atomic", 
3478         $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3479
3480     # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3481     if ($old_holds) {
3482         for my $holdid (@$old_holds) {
3483             next unless $holdid;
3484             push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3485         }
3486     }
3487
3488     unless(@$best_holds) {
3489         $logger->info("circulator: no suitable holds found for copy $bc");
3490         return (undef, $evt);
3491     }
3492
3493
3494     my $best_hold;
3495
3496     # for each potential hold, we have to run the permit script
3497     # to make sure the hold is actually permitted.
3498     my %reqr_cache;
3499     my %org_cache;
3500     for my $holdid (@$best_holds) {
3501         next unless $holdid;
3502         $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3503
3504         my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3505         # Force and recall holds bypass all rules
3506         if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3507             $best_hold = $hold;
3508             last;
3509         }
3510         my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3511         my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3512
3513         $reqr_cache{$hold->requestor} = $reqr;
3514         $org_cache{$hold->request_lib} = $rlib;
3515
3516         # see if this hold is permitted
3517         my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3518             {
3519                 patron_id   => $hold->usr,
3520                 requestor   => $reqr,
3521                 copy        => $copy,
3522                 pickup_lib  => $hold->pickup_lib,
3523                 request_lib => $rlib,
3524                 retarget    => 1
3525             }
3526         );
3527
3528         if( $permitted ) {
3529             $best_hold = $hold;
3530             last;
3531         }
3532     }
3533
3534
3535     unless( $best_hold ) { # no "good" permitted holds were found
3536         # we got nuthin
3537         $logger->info("circulator: no suitable holds found for copy $bc");
3538         return (undef, $evt);
3539     }
3540
3541     $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3542
3543     # indicate a permitted hold was found
3544     return $best_hold if $check_only;
3545
3546     # we've found a permitted hold.  we need to "grab" the copy
3547     # to prevent re-targeted holds (next part) from re-grabbing the copy
3548     $best_hold->current_copy($copy->id);
3549     $editor->update_action_hold_request($best_hold)
3550         or return (undef, $editor->event);
3551
3552
3553     my @retarget;
3554
3555     # re-target any other holds that already target this copy
3556     for my $old_hold (@$old_holds) {
3557         next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3558         $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3559             $old_hold->id." after a better hold [".$best_hold->id."] was found");
3560         $old_hold->clear_current_copy;
3561         $old_hold->clear_prev_check_time;
3562         $editor->update_action_hold_request($old_hold)
3563             or return (undef, $editor->event);
3564         push(@retarget, $old_hold->id);
3565     }
3566
3567     return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3568 }
3569
3570
3571
3572
3573
3574
3575 __PACKAGE__->register_method(
3576     method   => 'all_rec_holds',
3577     api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3578 );
3579
3580 sub all_rec_holds {
3581     my( $self, $conn, $auth, $title_id, $args ) = @_;
3582
3583     my $e = new_editor(authtoken=>$auth);
3584     $e->checkauth or return $e->event;
3585     $e->allowed('VIEW_HOLD') or return $e->event;
3586
3587     $args ||= {};
3588     $args->{fulfillment_time} = undef; #  we don't want to see old fulfilled holds
3589     $args->{cancel_time} = undef;
3590
3591     my $resp = {
3592           metarecord_holds => []
3593         , title_holds      => []
3594         , volume_holds     => []
3595         , copy_holds       => []
3596         , recall_holds     => []
3597         , force_holds      => []
3598         , part_holds       => []
3599         , issuance_holds   => []
3600     };
3601
3602     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3603     if($mr_map) {
3604         $resp->{metarecord_holds} = $e->search_action_hold_request(
3605             {   hold_type => OILS_HOLD_TYPE_METARECORD,
3606                 target => $mr_map->metarecord,
3607                 %$args
3608             }, {idlist => 1}
3609         );
3610     }
3611
3612     $resp->{title_holds} = $e->search_action_hold_request(
3613         {
3614             hold_type => OILS_HOLD_TYPE_TITLE,
3615             target => $title_id,
3616             %$args
3617         }, {idlist=>1} );
3618
3619     my $parts = $e->search_biblio_monograph_part(
3620         {
3621             record => $title_id
3622         }, {idlist=>1} );
3623
3624     if (@$parts) {
3625         $resp->{part_holds} = $e->search_action_hold_request(
3626             {
3627                 hold_type => OILS_HOLD_TYPE_MONOPART,
3628                 target => $parts,
3629                 %$args
3630             }, {idlist=>1} );
3631     }
3632
3633     my $subs = $e->search_serial_subscription(
3634         { record_entry => $title_id }, {idlist=>1});
3635
3636     if (@$subs) {
3637         my $issuances = $e->search_serial_issuance(
3638             {subscription => $subs}, {idlist=>1}
3639         );
3640
3641         if (@$issuances) {
3642             $resp->{issuance_holds} = $e->search_action_hold_request(
3643                 {
3644                     hold_type => OILS_HOLD_TYPE_ISSUANCE,
3645                     target => $issuances,
3646                     %$args
3647                 }, {idlist=>1}
3648             );
3649         }
3650     }
3651
3652     my $vols = $e->search_asset_call_number(
3653         { record => $title_id, deleted => 'f' }, {idlist=>1});
3654
3655     return $resp unless @$vols;
3656
3657     $resp->{volume_holds} = $e->search_action_hold_request(
3658         {
3659             hold_type => OILS_HOLD_TYPE_VOLUME,
3660             target => $vols,
3661             %$args },
3662         {idlist=>1} );
3663
3664     my $copies = $e->search_asset_copy(
3665         { call_number => $vols, deleted => 'f' }, {idlist=>1});
3666
3667     return $resp unless @$copies;
3668
3669     $resp->{copy_holds} = $e->search_action_hold_request(
3670         {
3671             hold_type => OILS_HOLD_TYPE_COPY,
3672             target => $copies,
3673             %$args },
3674         {idlist=>1} );
3675
3676     $resp->{recall_holds} = $e->search_action_hold_request(
3677         {
3678             hold_type => OILS_HOLD_TYPE_RECALL,
3679             target => $copies,
3680             %$args },
3681         {idlist=>1} );
3682
3683     $resp->{force_holds} = $e->search_action_hold_request(
3684         {
3685             hold_type => OILS_HOLD_TYPE_FORCE,
3686             target => $copies,
3687             %$args },
3688         {idlist=>1} );
3689
3690     return $resp;
3691 }
3692
3693 __PACKAGE__->register_method(
3694     method           => 'stream_wide_holds',
3695     authoritative    => 1,
3696     stream           => 1,
3697     api_name         => 'open-ils.circ.hold.wide_hash.stream'
3698 );
3699
3700 sub stream_wide_holds {
3701     my($self, $client, $auth, $restrictions, $order_by, $limit, $offset, $options) = @_;
3702     $options ||= {};
3703
3704     my $e = new_editor(authtoken=>$auth);
3705     $e->checkauth or return $e->event;
3706     $e->allowed('VIEW_HOLD') or return $e->event;
3707
3708     if ($options->{recently_canceled}) {
3709         # Map the the recently canceled holds filter into values 
3710         # wide-stream understands.
3711         my $filter = recently_canceled_holds_filter($e);
3712         $restrictions->{$_} =
3713             $filter->{where}->{$_} for keys %{$filter->{where}};
3714
3715         $limit = $filter->{limit} if $filter->{limit};
3716     }
3717
3718     my $filters = OpenSRF::Utils::JSON->perl2JSON($restrictions);
3719     $logger->info("WIDE HOLD FILTERS: $filters");
3720
3721     my $st = OpenSRF::AppSession->create('open-ils.storage');
3722     my $req = $st->request(
3723         'open-ils.storage.action.live_holds.wide_hash.atomic',
3724         $restrictions, $order_by, $limit, $offset
3725     );
3726
3727     my $results = $req->recv;
3728     if(!$results) {
3729         return 0;
3730     }
3731
3732     if(UNIVERSAL::isa($results,"Error")) {
3733         throw OpenSRF::EX::ERROR ("Error fetch hold shelf list");
3734     }
3735
3736     my @rows = @{ $results->content };
3737
3738     # Force immediate send of count response
3739     my $mbc = $client->max_bundle_count;
3740     $client->max_bundle_count(1);
3741     $client->respond(shift @rows);
3742     $client->max_bundle_count($mbc);
3743
3744     foreach my $hold (@rows) {
3745         $client->respond($hold) if $hold;
3746     }
3747
3748     $client->respond_complete;
3749 }
3750
3751
3752
3753
3754 __PACKAGE__->register_method(
3755     method        => 'uber_hold',
3756     authoritative => 1,
3757     api_name      => 'open-ils.circ.hold.details.retrieve'
3758 );
3759
3760 sub uber_hold {
3761     my($self, $client, $auth, $hold_id, $args) = @_;
3762     my $e = new_editor(authtoken=>$auth);
3763     $e->checkauth or return $e->event;
3764     return uber_hold_impl($e, $hold_id, $args);
3765 }
3766
3767 __PACKAGE__->register_method(
3768     method        => 'batch_uber_hold',
3769     authoritative => 1,
3770     stream        => 1,
3771     api_name      => 'open-ils.circ.hold.details.batch.retrieve'
3772 );
3773
3774 sub batch_uber_hold {
3775     my($self, $client, $auth, $hold_ids, $args) = @_;
3776     my $e = new_editor(authtoken=>$auth);
3777     $e->checkauth or return $e->event;
3778     $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3779     return undef;
3780 }
3781
3782 sub uber_hold_impl {
3783     my($e, $hold_id, $args) = @_;
3784     $args ||= {};
3785
3786     my $flesh_fields = ['current_copy', 'usr', 'notes'];
3787     push (@$flesh_fields, 'requestor') if $args->{include_requestor};
3788     push (@$flesh_fields, 'cancel_cause') if $args->{include_cancel_cause};
3789     push (@$flesh_fields, 'sms_carrier') if $args->{include_sms_carrier};
3790
3791     my $hold = $e->retrieve_action_hold_request([
3792         $hold_id,
3793         {flesh => 1, flesh_fields => {ahr => $flesh_fields}}
3794     ]) or return $e->event;
3795
3796     if($hold->usr->id ne $e->requestor->id) {
3797         # caller is asking for someone else's hold
3798         $e->allowed('VIEW_HOLD') or return $e->event;
3799         $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3800             [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3801
3802     } else {
3803         # caller is asking for own hold, but may not have permission to view staff notes
3804         unless($e->allowed('VIEW_HOLD')) {
3805             $hold->notes( # filter out any staff notes (unless marked as public)
3806                 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3807         }
3808     }
3809
3810     my $user = $hold->usr;
3811     $hold->usr($user->id);
3812
3813
3814     my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args);
3815
3816     flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3817     flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3818
3819     my $details = retrieve_hold_queue_status_impl($e, $hold);
3820     $hold->usr($user) if $args->{include_usr}; # re-flesh
3821
3822     my $resp = {
3823         hold    => $hold,
3824         bre_id  => $bre->id,
3825         ($copy     ? (copy           => $copy)     : ()),
3826         ($volume   ? (volume         => $volume)   : ()),
3827         ($issuance ? (issuance       => $issuance) : ()),
3828         ($part     ? (part           => $part)     : ()),
3829         ($args->{include_bre}  ?  (bre => $bre)    : ()),
3830         ($args->{suppress_mvr} ?  () : (mvr => $mvr)),
3831         %$details
3832     };
3833
3834     $resp->{copy}->location(
3835         $e->retrieve_asset_copy_location($resp->{copy}->location))
3836         if $resp->{copy} and $args->{flesh_acpl};
3837
3838     unless($args->{suppress_patron_details}) {
3839         my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3840         $resp->{patron_first}   = $user->first_given_name,
3841         $resp->{patron_last}    = $user->family_name,
3842         $resp->{patron_barcode} = $card->barcode,
3843         $resp->{patron_alias}   = $user->alias,
3844     };
3845
3846     return $resp;
3847 }
3848
3849
3850
3851 # -----------------------------------------------------
3852 # Returns the MVR object that represents what the
3853 # hold is all about
3854 # -----------------------------------------------------
3855 sub find_hold_mvr {
3856     my( $e, $hold, $args ) = @_;
3857
3858     my $tid;
3859     my $copy;
3860     my $volume;
3861     my $issuance;
3862     my $part;
3863     my $metarecord;
3864     my $no_mvr = $args->{suppress_mvr};
3865
3866     if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3867         $metarecord = $e->retrieve_metabib_metarecord($hold->target)
3868             or return $e->event;
3869         $tid = $metarecord->master_record;
3870
3871     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3872         $tid = $hold->target;
3873
3874     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3875         $volume = $e->retrieve_asset_call_number($hold->target)
3876             or return $e->event;
3877         $tid = $volume->record;
3878
3879     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3880         $issuance = $e->retrieve_serial_issuance([
3881             $hold->target,
3882             {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3883         ]) or return $e->event;
3884
3885         $tid = $issuance->subscription->record_entry;
3886
3887     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3888         $part = $e->retrieve_biblio_monograph_part([
3889             $hold->target
3890         ]) or return $e->event;
3891
3892         $tid = $part->record;
3893
3894     } 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 ) {
3895         $copy = $e->retrieve_asset_copy([
3896             $hold->target,
3897             {flesh => 1, flesh_fields => {acp => ['call_number']}}
3898         ]) or return $e->event;
3899
3900         $volume = $copy->call_number;
3901         $tid = $volume->record;
3902     }
3903
3904     if(!$copy and ref $hold->current_copy ) {
3905         $copy = $hold->current_copy;
3906         $hold->current_copy($copy->id) unless $args->{include_current_copy};
3907     }
3908
3909     if(!$volume and $copy) {
3910         $volume = $e->retrieve_asset_call_number($copy->call_number);
3911     }
3912
3913     # TODO return metarcord mvr for M holds
3914     my $title = $e->retrieve_biblio_record_entry($tid);
3915     return ( ($no_mvr) ? undef : $U->record_to_mvr($title), 
3916         $volume, $copy, $issuance, $part, $title, $metarecord);
3917 }
3918
3919 __PACKAGE__->register_method(
3920     method    => 'clear_shelf_cache',
3921     api_name  => 'open-ils.circ.hold.clear_shelf.get_cache',
3922     stream    => 1,
3923     signature => {
3924         desc => q/
3925             Returns the holds processed with the given cache key
3926         /
3927     }
3928 );
3929
3930 sub clear_shelf_cache {
3931     my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3932     my $e = new_editor(authtoken => $auth, xact => 1);
3933     return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3934
3935     $chunk_size ||= 25;
3936     $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3937
3938     my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3939
3940     if (!$hold_data) {
3941         $logger->info("no hold data found in cache"); # XXX TODO return event
3942         $e->rollback;
3943         return undef;
3944     }
3945
3946     my $maximum = 0;
3947     foreach (keys %$hold_data) {
3948         $maximum += scalar(@{ $hold_data->{$_} });
3949     }
3950     $client->respond({"maximum" => $maximum, "progress" => 0});
3951
3952     for my $action (sort keys %$hold_data) {
3953         while (@{$hold_data->{$action}}) {
3954             my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3955
3956             my $result_chunk = $e->json_query({
3957                 "select" => {
3958                     "acp" => ["barcode"],
3959                     "au" => [qw/
3960                         first_given_name second_given_name family_name alias
3961                     /],
3962                     "acn" => ["label"],
3963                     "acnp" => [{column => "label", alias => "prefix"}],
3964                     "acns" => [{column => "label", alias => "suffix"}],
3965                     "bre" => ["marc"],
3966                     "acpl" => ["name"],
3967                     "ahr" => ["id"]
3968                 },
3969                 "from" => {
3970                     "ahr" => {
3971                         "acp" => {
3972                             "field" => "id", "fkey" => "current_copy",
3973                             "join" => {
3974                                 "acn" => {
3975                                     "field" => "id", "fkey" => "call_number",
3976                                     "join" => {
3977                                         "bre" => {
3978                                             "field" => "id", "fkey" => "record"
3979                                         },
3980                                         "acnp" => {
3981                                             "field" => "id", "fkey" => "prefix"
3982                                         },
3983                                         "acns" => {
3984                                             "field" => "id", "fkey" => "suffix"
3985                                         }
3986                                     }
3987                                 },
3988                                 "acpl" => {"field" => "id", "fkey" => "location"}
3989                             }
3990                         },
3991                         "au" => {"field" => "id", "fkey" => "usr"}
3992                     }
3993                 },
3994                 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3995             }, {"substream" => 1}) or return $e->die_event;
3996
3997             $client->respond([
3998                 map {
3999                     +{"action" => $action, "hold_details" => $_}
4000                 } @$result_chunk
4001             ]);
4002         }
4003     }
4004
4005     $e->rollback;
4006     return undef;
4007 }
4008
4009
4010 __PACKAGE__->register_method(
4011     method    => 'clear_shelf_process',
4012     stream    => 1,
4013     api_name  => 'open-ils.circ.hold.clear_shelf.process',
4014     signature => {
4015         desc => q/
4016             1. Find all holds that have expired on the holds shelf
4017             2. Cancel the holds
4018             3. If a clear-shelf status is configured, put targeted copies into this status
4019             4. Divide copies into 3 groups: items to transit, items to reshelve, and items
4020                 that are needed for holds.  No subsequent action is taken on the holds
4021                 or items after grouping.
4022         /
4023     }
4024 );
4025
4026 sub clear_shelf_process {
4027     my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
4028
4029     my $e = new_editor(authtoken=>$auth);
4030     $e->checkauth or return $e->die_event;
4031     my $cache = OpenSRF::Utils::Cache->new('global');
4032
4033     $org_id ||= $e->requestor->ws_ou;
4034     $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
4035
4036     my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
4037
4038     my @hold_ids = $self->method_lookup(
4039         "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
4040     )->run($auth, $org_id, $match_copy);
4041
4042     $e->xact_begin;
4043
4044     my @holds;
4045     my @canceled_holds; # newly canceled holds
4046     $chunk_size ||= 25; # chunked status updates
4047     $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
4048
4049     my $counter = 0;
4050     for my $hold_id (@hold_ids) {
4051
4052         $logger->info("Clear shelf processing hold $hold_id");
4053
4054         my $hold = $e->retrieve_action_hold_request([
4055             $hold_id, {
4056                 flesh => 1,
4057                 flesh_fields => {ahr => ['current_copy']}
4058             }
4059         ]);
4060
4061         if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
4062             $hold->cancel_time('now');
4063             $hold->cancel_cause(2); # Hold Shelf expiration
4064             $e->update_action_hold_request($hold) or return $e->die_event;
4065             push(@canceled_holds, $hold_id);
4066         }
4067
4068         my $copy = $hold->current_copy;
4069
4070         if($copy_status or $copy_status == 0) {
4071             # if a clear-shelf copy status is defined, update the copy
4072             $copy->status($copy_status);
4073             $copy->edit_date('now');
4074             $copy->editor($e->requestor->id);
4075             $e->update_asset_copy($copy) or return $e->die_event;
4076         }
4077
4078         push(@holds, $hold);
4079         $client->respond({maximum => int(scalar(@holds)), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
4080     }
4081
4082     if ($e->commit) {
4083
4084         my %cache_data = (
4085             hold => [],
4086             transit => [],
4087             shelf => [],
4088             pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
4089         );
4090
4091         for my $hold (@holds) {
4092
4093             my $copy = $hold->current_copy;
4094             my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
4095
4096             if($alt_hold and !$match_copy) {
4097
4098                 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
4099
4100             } elsif($copy->circ_lib != $e->requestor->ws_ou) {
4101
4102                 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
4103
4104             } else {
4105
4106                 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
4107             }
4108         }
4109
4110         my $cache_key = md5_hex(time . $$ . rand());
4111         $logger->info("clear_shelf_cache: storing under $cache_key");
4112         $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours.  configurable?
4113
4114         # tell the client we're done
4115         $client->respond_complete({cache_key => $cache_key});
4116
4117         # ------------
4118         # fire off the hold cancelation trigger and wait for response so don't flood the service
4119
4120         # refetch the holds to pick up the caclulated cancel_time,
4121         # which may be needed by Action/Trigger
4122         $e->xact_begin;
4123         my $updated_holds = [];
4124         $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
4125         $e->rollback;
4126
4127         $U->create_events_for_hook(
4128             'hold_request.cancel.expire_holds_shelf',
4129             $_, $org_id, undef, undef, 1) for @$updated_holds;
4130
4131     } else {
4132         # tell the client we're done
4133         $client->respond_complete;
4134     }
4135 }
4136
4137 # returns IDs for holds that are on the holds shelf but 
4138 # have had their pickup_libs change while on the shelf.
4139 sub pickup_lib_changed_on_shelf_holds {
4140     my $e = shift;
4141     my $org_id = shift;
4142     my $ignore_holds = shift;
4143     $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
4144
4145     my $query = {
4146         select => { alhr => ['id'] },
4147         from   => {
4148             alhr => {
4149                 acp => {
4150                     field => 'id',
4151                     fkey  => 'current_copy'
4152                 },
4153             }
4154         },
4155         where => {
4156             '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
4157             '+alhr' => {
4158                 capture_time     => { "!=" => undef },
4159                 fulfillment_time => undef,
4160                 current_shelf_lib => $org_id,
4161                 pickup_lib => {'!='  => {'+alhr' => 'current_shelf_lib'}}
4162             }
4163         }
4164     };
4165
4166     $query->{where}->{'+alhr'}->{id} =
4167         {'not in' => $ignore_holds} if @$ignore_holds;
4168
4169     my $hold_ids = $e->json_query($query);
4170     return [ map { $_->{id} } @$hold_ids ];
4171 }
4172
4173 __PACKAGE__->register_method(
4174     method    => 'usr_hold_summary',
4175     api_name  => 'open-ils.circ.holds.user_summary',
4176     signature => q/
4177         Returns a summary of holds statuses for a given user
4178     /
4179 );
4180
4181 sub usr_hold_summary {
4182     my($self, $conn, $auth, $user_id) = @_;
4183
4184     my $e = new_editor(authtoken=>$auth);
4185     $e->checkauth or return $e->event;
4186     $e->allowed('VIEW_HOLD') or return $e->event;
4187
4188     my $holds = $e->search_action_hold_request(
4189         {
4190             usr =>  $user_id ,
4191             fulfillment_time => undef,
4192             cancel_time      => undef,
4193         }
4194     );
4195
4196     my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
4197     $summary{_hold_status($e, $_)} += 1 for @$holds;
4198     return \%summary;
4199 }
4200
4201
4202
4203 __PACKAGE__->register_method(
4204     method    => 'hold_has_copy_at',
4205     api_name  => 'open-ils.circ.hold.has_copy_at',
4206     signature => {
4207         desc   =>
4208                 'Returns the ID of the found copy and name of the shelving location if there is ' .
4209                 'an available copy at the specified org unit.  Returns empty hash otherwise.  '   .
4210                 'The anticipated use for this method is to determine whether an item is '         .
4211                 'available at the library where the user is placing the hold (or, alternatively, '.
4212                 'at the pickup library) to encourage bypassing the hold placement and just '      .
4213                 'checking out the item.' ,
4214         params => [
4215             { desc => 'Authentication Token', type => 'string' },
4216             { desc => 'Method Arguments.  Options include: hold_type, hold_target, org_unit.  '
4217                     . 'hold_type is the hold type code (T, V, C, M, ...).  '
4218                     . 'hold_target is the identifier of the hold target object.  '
4219                     . 'org_unit is org unit ID.',
4220               type => 'object'
4221             }
4222         ],
4223         return => {
4224             desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
4225             type => 'object'
4226         }
4227     }
4228 );
4229
4230 sub hold_has_copy_at {
4231     my($self, $conn, $auth, $args) = @_;
4232
4233     my $e = new_editor(authtoken=>$auth);
4234     $e->checkauth or return $e->event;
4235
4236     my $hold_type   = $$args{hold_type};
4237     my $hold_target = $$args{hold_target};
4238     my $org_unit    = $$args{org_unit};
4239
4240     my $query = {
4241         select => {acp => ['id'], acpl => ['name']},
4242         from   => {
4243             acp => {
4244                 acpl => {
4245                     field => 'id',
4246                     filter => { holdable => 't', deleted => 'f' },
4247                     fkey => 'location'
4248                 },
4249                 ccs  => {field => 'id', filter => {holdable => 't', is_available => 't'}, fkey => 'status'}
4250             }
4251         },
4252         where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit }},
4253         limit => 1
4254     };
4255
4256     if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
4257
4258         $query->{where}->{'+acp'}->{id} = $hold_target;
4259
4260     } elsif($hold_type eq 'V') {
4261
4262         $query->{where}->{'+acp'}->{call_number} = $hold_target;
4263
4264     } elsif($hold_type eq 'P') {
4265
4266         $query->{from}->{acp}->{acpm} = {
4267             field  => 'target_copy',
4268             fkey   => 'id',
4269             filter => {part => $hold_target},
4270         };
4271
4272     } elsif($hold_type eq 'I') {
4273
4274         $query->{from}->{acp}->{sitem} = {
4275             field  => 'unit',
4276             fkey   => 'id',
4277             filter => {issuance => $hold_target},
4278         };
4279
4280     } elsif($hold_type eq 'T') {
4281
4282         $query->{from}->{acp}->{acn} = {
4283             field  => 'id',
4284             fkey   => 'call_number',
4285             'join' => {
4286                 bre => {
4287                     field  => 'id',
4288                     filter => {id => $hold_target},
4289                     fkey   => 'record'
4290                 }
4291             }
4292         };
4293
4294     } else {
4295
4296         $query->{from}->{acp}->{acn} = {
4297             field => 'id',
4298             fkey  => 'call_number',
4299             join  => {
4300                 bre => {
4301                     field => 'id',
4302                     fkey  => 'record',
4303                     join  => {
4304                         mmrsm => {
4305                             field  => 'source',
4306                             fkey   => 'id',
4307                             filter => {metarecord => $hold_target},
4308                         }
4309                     }
4310                 }
4311             }
4312         };
4313     }
4314
4315     my $res = $e->json_query($query)->[0] or return {};
4316     return {copy => $res->{id}, location => $res->{name}} if $res;
4317 }
4318
4319
4320 # returns true if the user already has an item checked out
4321 # that could be used to fulfill the requested hold.
4322 sub hold_item_is_checked_out {
4323     my($e, $user_id, $hold_type, $hold_target) = @_;
4324
4325     my $query = {
4326         select => {acp => ['id']},
4327         from   => {acp => {}},
4328         where  => {
4329             '+acp' => {
4330                 id => {
4331                     in => { # copies for circs the user has checked out
4332                         select => {circ => ['target_copy']},
4333                         from   => 'circ',
4334                         where  => {
4335                             usr => $user_id,
4336                             checkin_time => undef,
4337                             '-or' => [
4338                                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
4339                                 {stop_fines => undef}
4340                             ],
4341                         }
4342                     }
4343                 }
4344             }
4345         },
4346         limit => 1
4347     };
4348
4349     if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
4350
4351         $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
4352
4353     } elsif($hold_type eq 'V') {
4354
4355         $query->{where}->{'+acp'}->{call_number} = $hold_target;
4356
4357      } elsif($hold_type eq 'P') {
4358
4359         $query->{from}->{acp}->{acpm} = {
4360             field  => 'target_copy',
4361             fkey   => 'id',
4362             filter => {part => $hold_target},
4363         };
4364
4365      } elsif($hold_type eq 'I') {
4366
4367         $query->{from}->{acp}->{sitem} = {
4368             field  => 'unit',
4369             fkey   => 'id',
4370             filter => {issuance => $hold_target},
4371         };
4372
4373     } elsif($hold_type eq 'T') {
4374
4375         $query->{from}->{acp}->{acn} = {
4376             field  => 'id',
4377             fkey   => 'call_number',
4378             'join' => {
4379                 bre => {
4380                     field  => 'id',
4381                     filter => {id => $hold_target},
4382                     fkey   => 'record'
4383                 }
4384             }
4385         };
4386
4387     } else {
4388
4389         $query->{from}->{acp}->{acn} = {
4390             field => 'id',
4391             fkey => 'call_number',
4392             join => {
4393                 bre => {
4394                     field => 'id',
4395                     fkey => 'record',
4396                     join => {
4397                         mmrsm => {
4398                             field => 'source',
4399                             fkey => 'id',
4400                             filter => {metarecord => $hold_target},
4401                         }
4402                     }
4403                 }
4404             }
4405         };
4406     }
4407
4408     return $e->json_query($query)->[0];
4409 }
4410
4411 __PACKAGE__->register_method(
4412     method    => 'change_hold_title',
4413     api_name  => 'open-ils.circ.hold.change_title',
4414     signature => {
4415         desc => q/
4416             Updates all title level holds targeting the specified bibs to point a new bib./,
4417         params => [
4418             { desc => 'Authentication Token', type => 'string' },
4419             { desc => 'New Target Bib Id',    type => 'number' },
4420             { desc => 'Old Target Bib Ids',   type => 'array'  },
4421         ],
4422         return => { desc => '1 on success' }
4423     }
4424 );
4425
4426 __PACKAGE__->register_method(
4427     method    => 'change_hold_title_for_specific_holds',
4428     api_name  => 'open-ils.circ.hold.change_title.specific_holds',
4429     signature => {
4430         desc => q/
4431             Updates specified holds to target new bib./,
4432         params => [
4433             { desc => 'Authentication Token', type => 'string' },
4434             { desc => 'New Target Bib Id',    type => 'number' },
4435             { desc => 'Holds Ids for holds to update',   type => 'array'  },
4436         ],
4437         return => { desc => '1 on success' }
4438     }
4439 );
4440
4441
4442 sub change_hold_title {
4443     my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4444
4445     my $e = new_editor(authtoken=>$auth, xact=>1);
4446     return $e->die_event unless $e->checkauth;
4447
4448     my $holds = $e->search_action_hold_request(
4449         [
4450             {
4451                 capture_time     => undef,
4452                 cancel_time      => undef,
4453                 fulfillment_time => undef,
4454                 hold_type        => 'T',
4455                 target           => $bib_ids
4456             },
4457             {
4458                 flesh        => 1,
4459                 flesh_fields => { ahr => ['usr'] }
4460             }
4461         ],
4462         { substream => 1 }
4463     );
4464
4465     for my $hold (@$holds) {
4466         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4467         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4468         $hold->target( $new_bib_id );
4469         $e->update_action_hold_request($hold) or return $e->die_event;
4470     }
4471
4472     $e->commit;
4473
4474     _reset_hold($self, $e->requestor, $_) for @$holds;
4475
4476     return 1;
4477 }
4478
4479 sub change_hold_title_for_specific_holds {
4480     my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4481
4482     my $e = new_editor(authtoken=>$auth, xact=>1);
4483     return $e->die_event unless $e->checkauth;
4484
4485     my $holds = $e->search_action_hold_request(
4486         [
4487             {
4488                 capture_time     => undef,
4489                 cancel_time      => undef,
4490                 fulfillment_time => undef,
4491                 hold_type        => 'T',
4492                 id               => $hold_ids
4493             },
4494             {
4495                 flesh        => 1,
4496                 flesh_fields => { ahr => ['usr'] }
4497             }
4498         ],
4499         { substream => 1 }
4500     );
4501
4502     for my $hold (@$holds) {
4503         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4504         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4505         $hold->target( $new_bib_id );
4506         $e->update_action_hold_request($hold) or return $e->die_event;
4507     }
4508
4509     $e->commit;
4510
4511     _reset_hold($self, $e->requestor, $_) for @$holds;
4512
4513     return 1;
4514 }
4515
4516 __PACKAGE__->register_method(
4517     method    => 'rec_hold_count',
4518     api_name  => 'open-ils.circ.bre.holds.count',
4519     signature => {
4520         desc => q/Returns the total number of holds that target the
4521             selected bib record or its associated copies and call_numbers/,
4522         params => [
4523             { desc => 'Bib ID', type => 'number' },
4524             { desc => q/Optional arguments.  Supported arguments include:
4525                 "pickup_lib_descendant" -> limit holds to those whose pickup
4526                 library is equal to or is a child of the provided org unit/,
4527                 type => 'object'
4528             }
4529         ],
4530         return => {desc => 'Hold count', type => 'number'}
4531     }
4532 );
4533
4534 __PACKAGE__->register_method(
4535     method    => 'rec_hold_count',
4536     api_name  => 'open-ils.circ.mmr.holds.count',
4537     signature => {
4538         desc => q/Returns the total number of holds that target the
4539             selected metarecord or its associated copies, call_numbers, and bib records/,
4540         params => [
4541             { desc => 'Metarecord ID', type => 'number' },
4542         ],
4543         return => {desc => 'Hold count', type => 'number'}
4544     }
4545 );
4546
4547 # XXX Need to add type I holds to these counts
4548 sub rec_hold_count {
4549     my($self, $conn, $target_id, $args) = @_;
4550     $args ||= {};
4551
4552     my $mmr_join = {
4553         mmrsm => {
4554             field => 'source',
4555             fkey => 'id',
4556             filter => {metarecord => $target_id}
4557         }
4558     };
4559
4560     my $bre_join = {
4561         bre => {
4562             field => 'id',
4563             filter => { id => $target_id },
4564             fkey => 'record'
4565         }
4566     };
4567
4568     if($self->api_name =~ /mmr/) {
4569         delete $bre_join->{bre}->{filter};
4570         $bre_join->{bre}->{join} = $mmr_join;
4571     }
4572
4573     my $cn_join = {
4574         acn => {
4575             field => 'id',
4576             fkey => 'call_number',
4577             join => $bre_join
4578         }
4579     };
4580
4581     my $query = {
4582         select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4583         from => 'ahr',
4584         where => {
4585             '+ahr' => {
4586                 cancel_time => undef,
4587                 fulfillment_time => undef,
4588                 '-or' => [
4589                     {
4590                         '-and' => {
4591                             hold_type => [qw/C F R/],
4592                             target => {
4593                                 in => {
4594                                     select => {acp => ['id']},
4595                                     from => { acp => $cn_join }
4596                                 }
4597                             }
4598                         }
4599                     },
4600                     {
4601                         '-and' => {
4602                             hold_type => 'V',
4603                             target => {
4604                                 in => {
4605                                     select => {acn => ['id']},
4606                                     from => {acn => $bre_join}
4607                                 }
4608                             }
4609                         }
4610                     },
4611                     {
4612                         '-and' => {
4613                             hold_type => 'P',
4614                             target => {
4615                                 in => {
4616                                     select => {bmp => ['id']},
4617                                     from => {bmp => $bre_join}
4618                                 }
4619                             }
4620                         }
4621                     },
4622                     {
4623                         '-and' => {
4624                             hold_type => 'T',
4625                             target => $target_id
4626                         }
4627                     }
4628                 ]
4629             }
4630         }
4631     };
4632
4633     if($self->api_name =~ /mmr/) {
4634         $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4635             '-and' => {
4636                 hold_type => 'T',
4637                 target => {
4638                     in => {
4639                         select => {bre => ['id']},
4640                         from => {bre => $mmr_join}
4641                     }
4642                 }
4643             }
4644         };
4645
4646         $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4647             '-and' => {
4648                 hold_type => 'M',
4649                 target => $target_id
4650             }
4651         };
4652     }
4653
4654
4655     if (my $pld = $args->{pickup_lib_descendant}) {
4656
4657         my $top_ou = new_editor()->search_actor_org_unit(
4658             {parent_ou => undef}
4659         )->[0]; # XXX Assumes single root node. Not alone in this...
4660
4661         $query->{where}->{'+ahr'}->{pickup_lib} = {
4662             in => {
4663                 select  => {aou => [{ 
4664                     column => 'id', 
4665                     transform => 'actor.org_unit_descendants', 
4666                     result_field => 'id' 
4667                 }]},
4668                 from    => 'aou',
4669                 where   => {id => $pld}
4670             }
4671         } if ($pld != $top_ou->id);
4672     }
4673
4674     # To avoid Internal Server Errors, we get an editor, then run the
4675     # query and check the result.  If anything fails, we'll return 0.
4676     my $result = 0;
4677     if (my $e = new_editor()) {
4678         my $query_result = $e->json_query($query);
4679         if ($query_result && @{$query_result}) {
4680             $result = $query_result->[0]->{count}
4681         }
4682     }
4683
4684     return $result;
4685 }
4686
4687 # A helper function to calculate a hold's expiration time at a given
4688 # org_unit. Takes the org_unit as an argument and returns either the
4689 # hold expire time as an ISO8601 string or undef if there is no hold
4690 # expiration interval set for the subject ou.
4691 sub calculate_expire_time
4692 {
4693     my $ou = shift;
4694     my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4695     if($interval) {
4696         my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
4697         return $U->epoch2ISO8601($date->epoch);
4698     }
4699     return undef;
4700 }
4701
4702
4703 __PACKAGE__->register_method(
4704     method    => 'mr_hold_filter_attrs',
4705     api_name  => 'open-ils.circ.mmr.holds.filters',
4706     authoritative => 1,
4707     stream => 1,
4708     signature => {
4709         desc => q/
4710             Returns the set of available formats and languages for the
4711             constituent records of the provided metarcord.
4712             If an array of hold IDs is also provided, information about
4713             each is returned as well.  This information includes:
4714             1. a slightly easier to read version of holdable_formats
4715             2. attributes describing the set of format icons included
4716                in the set of desired, constituent records.
4717         /,
4718         params => [
4719             {desc => 'Metarecord ID', type => 'number'},
4720             {desc => 'Context Org ID', type => 'number'},
4721             {desc => 'Hold ID List', type => 'array'},
4722         ],
4723         return => {
4724             desc => q/
4725                 Stream of objects.  The first will have a 'metarecord' key
4726                 containing non-hold-specific metarecord information, subsequent
4727                 responses will contain a 'hold' key containing hold-specific
4728                 information
4729             /, 
4730             type => 'object'
4731         }
4732     }
4733 );
4734
4735 sub mr_hold_filter_attrs { 
4736     my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4737     my $e = new_editor();
4738
4739     # by default, return MR / hold attributes for all constituent
4740     # records with holdable copies.  If there is a hard boundary,
4741     # though, limit to records with copies within the boundary,
4742     # since anything outside the boundary can never be held.
4743     my $org_depth = 0;
4744     if ($org_id) {
4745         $org_depth = $U->ou_ancestor_setting_value(
4746             $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4747     }
4748
4749     # get all org-scoped records w/ holdable copies for this metarecord
4750     my ($bre_ids) = $self->method_lookup(
4751         'open-ils.circ.holds.metarecord.filtered_records')->run(
4752             $mr_id, undef, $org_id, $org_depth);
4753
4754     my $item_lang_attr = 'item_lang'; # configurable?
4755     my $format_attr = $e->retrieve_config_global_flag(
4756         'opac.metarecord.holds.format_attr')->value;
4757
4758     # helper sub for fetching ccvms for a batch of record IDs
4759     sub get_batch_ccvms {
4760         my ($e, $attr, $bre_ids) = @_;
4761         return [] unless $bre_ids and @$bre_ids;
4762         my $vals = $e->search_metabib_record_attr_flat({
4763             attr => $attr,
4764             id => $bre_ids
4765         });
4766         return [] unless @$vals;
4767         return $e->search_config_coded_value_map({
4768             ctype => $attr,
4769             code => [map {$_->value} @$vals]
4770         });
4771     }
4772
4773     my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4774     my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4775
4776     $client->respond({
4777         metarecord => {
4778             id => $mr_id,
4779             formats => $formats,
4780             langs => $langs
4781         }
4782     });
4783
4784     return unless $hold_ids;
4785     my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4786     $icon_attr = $icon_attr ? $icon_attr->value : '';
4787
4788     for my $hold_id (@$hold_ids) {
4789         my $hold = $e->retrieve_action_hold_request($hold_id) 
4790             or return $e->event;
4791
4792         next unless $hold->hold_type eq 'M';
4793
4794         my $resp = {
4795             hold => {
4796                 id => $hold_id,
4797                 formats => [],
4798                 langs => []
4799             }
4800         };
4801
4802         # collect the ccvm's for the selected formats / language
4803         # (i.e. the holdable formats) on the MR.
4804         # this assumes a two-key structure for format / language,
4805         # though no assumption is made about the keys themselves.
4806         my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4807         my $lang_vals = [];
4808         my $format_vals = [];
4809         for my $val (values %$hformats) {
4810             # val is either a single ccvm or an array of them
4811             $val = [$val] unless ref $val eq 'ARRAY';
4812             for my $node (@$val) {
4813                 push (@$lang_vals, $node->{_val})   
4814                     if $node->{_attr} eq $item_lang_attr; 
4815                 push (@$format_vals, $node->{_val})   
4816                     if $node->{_attr} eq $format_attr;
4817             }
4818         }
4819
4820         # fetch the ccvm's for consistency with the {metarecord} blob
4821         $resp->{hold}{formats} = $e->search_config_coded_value_map({
4822             ctype => $format_attr, code => $format_vals});
4823         $resp->{hold}{langs} = $e->search_config_coded_value_map({
4824             ctype => $item_lang_attr, code => $lang_vals});
4825
4826         # find all of the bib records within this metarcord whose 
4827         # format / language match the holdable formats on the hold
4828         my ($bre_ids) = $self->method_lookup(
4829             'open-ils.circ.holds.metarecord.filtered_records')->run(
4830                 $hold->target, $hold->holdable_formats, 
4831                 $hold->selection_ou, $hold->selection_depth);
4832
4833         # now find all of the 'icon' attributes for the records
4834         $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4835         $client->respond($resp);
4836     }
4837
4838     return;
4839 }
4840
4841 __PACKAGE__->register_method(
4842     method        => "copy_has_holds_count",
4843     api_name      => "open-ils.circ.copy.has_holds_count",
4844     authoritative => 1,
4845     signature     => {
4846         desc => q/
4847             Returns the number of holds a paticular copy has
4848         /,
4849         params => [
4850             { desc => 'Authentication Token', type => 'string'},
4851             { desc => 'Copy ID', type => 'number'}
4852         ],
4853         return => {
4854             desc => q/
4855                 Simple count value
4856             /,
4857             type => 'number'
4858         }
4859     }
4860 );
4861
4862 sub copy_has_holds_count {
4863     my( $self, $conn, $auth, $copyid ) = @_;
4864     my $e = new_editor(authtoken=>$auth);
4865     return $e->event unless $e->checkauth;
4866
4867     if( $copyid && $copyid > 0 ) {
4868         my $meth = 'retrieve_action_has_holds_count';
4869         my $data = $e->$meth($copyid);
4870         if($data){
4871                 return $data->count();
4872         }
4873     }
4874     return 0;
4875 }
4876
4877 __PACKAGE__->register_method(
4878     method    => "retrieve_holds_by_usr_notify_value_staff",
4879     api_name  => "open-ils.circ.holds.retrieve_by_notify_staff",
4880     signature => {
4881         desc   => "Retrieve the hold, for the specified user using the notify value.  $ses_is_req_note",
4882         params => [
4883             { desc => 'Authentication token', type => 'string' },
4884             { desc => 'User ID',              type => 'number' },
4885             { desc => 'notify value',         type => 'string' },
4886             { desc => 'notify_type',          type => 'string' }
4887         ],
4888         return => {
4889             desc => 'Hold objects with transits attached, event on error',
4890         }
4891     }
4892 );
4893
4894 sub retrieve_holds_by_usr_notify_value_staff {
4895     
4896     my($self, $conn, $auth, $usr_id, $contact, $cType) = @_;
4897
4898     my $e = new_editor(authtoken=>$auth);
4899     $e->checkauth or return $e->event;
4900
4901     if ($e->requestor->id != $usr_id){
4902         $e->allowed('VIEW_HOLD') or return $e->event;
4903     }
4904
4905     my $q = {
4906         "select" => { "ahr" => ["id", "sms_notify", "phone_notify", "email_notify", "sms_carrier"]},
4907         "from" => "ahr",
4908         "where" => {
4909             "usr"          =>      $usr_id,
4910             "capture_time" =>      undef,
4911             "cancel_time"  =>      undef,
4912             "fulfillment_time" =>  undef,
4913         }
4914     };
4915
4916     if ($cType eq "day_phone" or $cType eq "evening_phone" or
4917         $cType eq "other_phone" or $cType eq "default_phone"){
4918             $q->{where}->{"-not"} = [
4919                 { "phone_notify" => { "=" => $contact} },
4920                 { "phone_notify" => { "<>" => undef } }
4921             ];
4922     }
4923
4924
4925     if ($cType eq "default_sms") {
4926         $q->{where}->{"-not"} = [
4927             { "sms_notify" => { "=" => $contact} },
4928             { "sms_notify" => { "<>" => undef } }
4929         ];
4930     }
4931
4932     if ($cType eq "default_sms_carrier_id") {
4933         $q->{where}->{"-not"} = [
4934             { "sms_carrier" => { "=" => int($contact)} },
4935             { "sms_carrier" => { "<>" => undef } }
4936         ];
4937     }
4938
4939     if ($cType =~ /notify/){
4940         # this is was notification pref change
4941         # we find all unfulfilled holds that match have that pref
4942         my $optr = $contact == 1 ? "<>" : "="; # unless it's email, true val means we want to query for not null
4943         my $conj = $optr eq '=' ? '-or' : '-and';
4944         if ($cType =~ /sms/) {
4945             $q->{where}->{$conj} = [ { sms_notify => { $optr => undef } }, { sms_notify => { $optr => '' } } ];
4946         }
4947         if ($cType =~ /phone/) {
4948             $q->{where}->{$conj} = [ { phone_notify => { $optr => undef } }, { phone_notify => { $optr => '' } } ];
4949         }
4950         if ($cType =~ /email/) {
4951             if ($contact) {
4952                 $q->{where}->{'+ahr'} = 'email_notify';
4953             } else {
4954                 $q->{where}->{'-not'} = {'+ahr' => 'email_notify'};
4955             }
4956         }
4957     }
4958
4959     my $holds = $e->json_query($q);
4960     #$hold_ids = [ map { $_->{id} } @$hold_ids ];
4961
4962     return $holds;
4963 }
4964
4965 __PACKAGE__->register_method(
4966     method    => "batch_update_holds_by_value_staff",
4967     api_name  => "open-ils.circ.holds.batch_update_holds_by_notify_staff",
4968     signature => {
4969         desc   => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
4970         params => [
4971             { desc => 'Authentication token', type => 'string' },
4972             { desc => 'User ID',              type => 'number' },
4973             { desc => 'Hold IDs',             type => 'array'  },
4974             { desc => 'old notify value',     type => 'string' },
4975             { desc => 'new notify value',     type => 'string' },
4976             { desc => 'field name',           type => 'string' },
4977             { desc => 'SMS carrier ID',       type => 'number' }
4978
4979         ],
4980         return => {
4981             desc => 'Hold objects with transits attached, event on error',
4982         }
4983     }
4984 );
4985
4986 sub batch_update_holds_by_value_staff {
4987     my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType, $carrierId) = @_;
4988
4989     my $e = new_editor(authtoken=>$auth, xact=>1);
4990     $e->checkauth or return $e->event;
4991     if ($e->requestor->id != $usr_id){
4992         $e->allowed('UPDATE_HOLD') or return $e->event;
4993     }
4994
4995     my @success;
4996     for my $id (@$hold_ids) {
4997         
4998         my $hold = $e->retrieve_action_hold_request($id);
4999
5000         if ($cType eq "day_phone" or $cType eq "evening_phone" or
5001             $cType eq "other_phone" or $cType eq "default_phone") {
5002
5003             if ($newval eq '') {
5004                 $hold->clear_phone_notify();
5005             }
5006             else {
5007                 $hold->phone_notify($newval);
5008             }
5009         }
5010         
5011         if ($cType eq "default_sms"){
5012             if ($newval eq '') {
5013                 $hold->clear_sms_notify();
5014                 $hold->clear_sms_carrier(); # TODO: prevent orphan sms_carrier, via db trigger
5015             }
5016             else {
5017                 $hold->sms_notify($newval);
5018                 $hold->sms_carrier($carrierId);
5019             }
5020
5021         }
5022
5023         if ($cType eq "default_sms_carrier_id") {
5024             $hold->sms_carrier($newval);
5025         }
5026
5027         if ($cType =~ /notify/){
5028             # this is a notification pref change
5029             if ($cType =~ /email/) { $hold->email_notify($newval); }
5030             if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5031             if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5032             # the other case, where x_notify is changed to true,
5033             # is covered by an actual value being assigned
5034         }
5035
5036         $e->update_action_hold_request($hold) or return $e->die_event;
5037         push @success, $id;
5038     }
5039
5040     #$e->disconnect;
5041     $e->commit; #unless $U->event_code($res);
5042     return \@success;
5043
5044 }
5045
5046
5047 __PACKAGE__->register_method(
5048     method    => "retrieve_holds_by_usr_with_notify",
5049     api_name  => "open-ils.circ.holds.retrieve.by_usr.with_notify",
5050     signature => {
5051         desc   => "Retrieve the hold, for the specified user using the notify value.  $ses_is_req_note",
5052         params => [
5053             { desc => 'Authentication token', type => 'string' },
5054             { desc => 'User ID',              type => 'number' },
5055         ],
5056         return => {
5057             desc => 'Lists of holds with notification values, event on error',
5058         }
5059     }
5060 );
5061
5062 sub retrieve_holds_by_usr_with_notify {
5063     
5064     my($self, $conn, $auth, $usr_id) = @_;
5065
5066     my $e = new_editor(authtoken=>$auth);
5067     $e->checkauth or return $e->event;
5068
5069     if ($e->requestor->id != $usr_id){
5070         $e->allowed('VIEW_HOLD') or return $e->event;
5071     }
5072
5073     my $q = {
5074         "select" => { "ahr" => ["id", "phone_notify", "email_notify", "sms_carrier", "sms_notify"]},
5075         "from" => "ahr",
5076         "where" => {
5077             "usr"          =>      $usr_id,
5078             "capture_time" =>      undef,
5079             "cancel_time"  =>      undef,
5080             "fulfillment_time" =>  undef,
5081         }
5082     };
5083
5084     my $holds = $e->json_query($q);
5085     return $holds;
5086 }
5087
5088 __PACKAGE__->register_method(
5089     method    => "batch_update_holds_by_value",
5090     api_name  => "open-ils.circ.holds.batch_update_holds_by_notify",
5091     signature => {
5092         desc   => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
5093         params => [
5094             { desc => 'Authentication token', type => 'string' },
5095             { desc => 'User ID',              type => 'number' },
5096             { desc => 'Hold IDs',             type => 'array'  },
5097             { desc => 'old notify value',     type => 'string' },
5098             { desc => 'new notify value',     type => 'string' },
5099             { desc => 'notify_type',          type => 'string' }
5100         ],
5101         return => {
5102             desc => 'Hold objects with transits attached, event on error',
5103         }
5104     }
5105 );
5106
5107 sub batch_update_holds_by_value {
5108     my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType) = @_;
5109
5110     my $e = new_editor(authtoken=>$auth, xact=>1);
5111     $e->checkauth or return $e->event;
5112     if ($e->requestor->id != $usr_id){
5113         $e->allowed('UPDATE_HOLD') or return $e->event;
5114     }
5115
5116     my @success;
5117     for my $id (@$hold_ids) {
5118         
5119         my $hold = $e->retrieve_action_hold_request(int($id));
5120
5121         if ($cType eq "day_phone" or $cType eq "evening_phone" or
5122             $cType eq "other_phone" or $cType eq "default_phone") {
5123             # change phone number value on hold
5124             $hold->phone_notify($newval);
5125         }
5126         if ($cType eq "default_sms") {
5127             # change SMS number value on hold
5128             $hold->sms_notify($newval);
5129         }
5130
5131         if ($cType eq "default_sms_carrier_id") {
5132             $hold->sms_carrier(int($newval));
5133         }
5134
5135         if ($cType =~ /notify/){
5136             # this is a notification pref change
5137             if ($cType =~ /email/) { $hold->email_notify($newval); }
5138             if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5139             if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5140             # the other case, where x_notify is changed to true,
5141             # is covered by an actual value being assigned
5142         }
5143
5144         $e->update_action_hold_request($hold) or return $e->die_event;
5145         push @success, $id;
5146     }
5147
5148     #$e->disconnect;
5149     $e->commit; #unless $U->event_code($res);
5150     return \@success;
5151 }
5152
5153 __PACKAGE__->register_method(
5154     method        => "hold_metadata",
5155     api_name      => "open-ils.circ.hold.get_metadata",
5156     authoritative => 1,
5157     stream => 1,
5158     signature     => {
5159         desc => q/
5160             Returns a stream of objects containing whatever bib, 
5161             volume, etc. data is available to the specific hold 
5162             type and target.
5163         /,
5164         params => [
5165             {desc => 'Hold Type', type => 'string'},
5166             {desc => 'Hold Target(s)', type => 'number or array'},
5167             {desc => 'Context org unit (optional)', type => 'number'}
5168         ],
5169         return => {
5170             desc => q/
5171                 Stream of hold metadata objects.
5172             /,
5173             type => 'object'
5174         }
5175     }
5176 );
5177
5178
5179 sub hold_metadata {
5180     my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
5181
5182     $hold_targets = [$hold_targets] unless ref $hold_targets;
5183
5184     my $e = new_editor();
5185     for my $target (@$hold_targets) {
5186
5187         # create a dummy hold for find_hold_mvr
5188         my $hold = Fieldmapper::action::hold_request->new;
5189         $hold->hold_type($hold_type);
5190         $hold->target($target);
5191
5192         my (undef, $volume, $copy, $issuance, $part, $bre, $metarecord) = 
5193             find_hold_mvr($e, $hold, {suppress_mvr => 1});
5194
5195         $bre->clear_marc; # avoid bulk
5196
5197         my $meta = {
5198             target => $target,
5199             copy => $copy,
5200             volume => $volume,
5201             issuance => $issuance,
5202             part => $part,
5203             parts => [],
5204             part_required => 'f',
5205             bibrecord => $bre,
5206             metarecord => $metarecord,
5207             metarecord_filters => {}
5208         };
5209
5210         # If this is a bib hold or metarecord hold, also return the
5211         # available set of MR filters (AKA "Holdable Formats") for the
5212         # hold.  For bib holds these may be used to upgrade the hold
5213         # from a bib to metarecord hold.
5214         if ($hold_type eq 'T') {
5215             my $map = $e->search_metabib_metarecord_source_map(
5216                 {source => $meta->{bibrecord}->id})->[0];
5217
5218             if ($map) {
5219                 $meta->{metarecord} = 
5220                     $e->retrieve_metabib_metarecord($map->metarecord);
5221             }
5222
5223             # Also fetch the available parts for bib-level holds.
5224             $meta->{parts} = $e->search_biblio_monograph_part(
5225                 [
5226                     {record => $bre->id, deleted => 'f'},
5227                     {order_by => {bmp => 'label_sortkey'}}
5228                 ]
5229             );
5230
5231             # T holds on records that have parts are normally OK, but if the record has
5232             # no non-part copies, the hold will ultimately fail.  When that happens,
5233             # require the user to select a part.
5234             #
5235             # If the global flag circ.holds.api_require_monographic_part_when_present is
5236             # enabled, or the library setting circ.holds.ui_require_monographic_part_when_present
5237             # is true for any involved owning_library, then also require part selection.
5238             my $part_required = 0;
5239             if ($meta->{parts}) {
5240                 my $part_required_flag = $e->retrieve_config_global_flag('circ.holds.api_require_monographic_part_when_present');
5241                 $part_required = ($part_required_flag and $U->is_true($part_required_flag->enabled));
5242                 if (!$part_required) {
5243                     my $resp = $e->json_query({
5244                         select => {
5245                             acn => ['owning_lib']
5246                         },
5247                         from => {acn => {acp => {type => 'left'}}},
5248                         where => {
5249                             '+acp' => {
5250                                 '-or' => [
5251                                     {deleted => 'f'},
5252                                     {id => undef} # left join
5253                                 ]
5254                             },
5255                             '+acn' => {deleted => 'f', record => $bre->id}
5256                         },
5257                         distinct => 't'
5258                     });
5259                     my $org_ids = [map {$_->{owning_lib}} @$resp];
5260                     foreach my $org (@$org_ids) { # FIXME: worth shortcutting/optimizing?
5261                         if ($U->ou_ancestor_setting_value($org, 'circ.holds.ui_require_monographic_part_when_present')) {
5262                             $part_required = 1;
5263                         }
5264                     }
5265                 }
5266                 if (!$part_required) {
5267                     my $np_copies = $e->json_query({
5268                         select => { acp => [{column => 'id', transform => 'count', alias => 'count'}]},
5269                         from => {acp => {acn => {}, acpm => {type => 'left'}}},
5270                         where => {
5271                             '+acp' => {deleted => 'f'},
5272                             '+acn' => {deleted => 'f', record => $bre->id},
5273                             '+acpm' => {id => undef}
5274                         }
5275                     });
5276                     $part_required = 1 if $np_copies->[0]->{count} == 0;
5277                 }
5278             }
5279             $meta->{part_required} = $part_required;
5280         }
5281
5282         if ($meta->{metarecord}) {
5283
5284             my ($filters) = 
5285                 $self->method_lookup('open-ils.circ.mmr.holds.filters')
5286                     ->run($meta->{metarecord}->id, $org_id);
5287
5288             if ($filters) {
5289                 $meta->{metarecord_filters} = $filters->{metarecord};
5290             }
5291         }
5292
5293         $client->respond($meta);
5294     }
5295
5296     return undef;
5297 }
5298
5299 1;