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