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