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