]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
LP 115706: Avoid Internal Server Errors with Hold Count Retrieval
[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     push (@$flesh_fields, 'sms_carrier') if $args->{include_sms_carrier};
3434
3435     my $hold = $e->retrieve_action_hold_request([
3436         $hold_id,
3437         {flesh => 1, flesh_fields => {ahr => $flesh_fields}}
3438     ]) or return $e->event;
3439
3440     if($hold->usr->id ne $e->requestor->id) {
3441         # caller is asking for someone else's hold
3442         $e->allowed('VIEW_HOLD') or return $e->event;
3443         $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3444             [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3445
3446     } else {
3447         # caller is asking for own hold, but may not have permission to view staff notes
3448         unless($e->allowed('VIEW_HOLD')) {
3449             $hold->notes( # filter out any staff notes (unless marked as public)
3450                 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3451         }
3452     }
3453
3454     my $user = $hold->usr;
3455     $hold->usr($user->id);
3456
3457
3458     my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args);
3459
3460     flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3461     flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3462
3463     my $details = retrieve_hold_queue_status_impl($e, $hold);
3464     $hold->usr($user) if $args->{include_usr}; # re-flesh
3465
3466     my $resp = {
3467         hold    => $hold,
3468         bre_id  => $bre->id,
3469         ($copy     ? (copy           => $copy)     : ()),
3470         ($volume   ? (volume         => $volume)   : ()),
3471         ($issuance ? (issuance       => $issuance) : ()),
3472         ($part     ? (part           => $part)     : ()),
3473         ($args->{include_bre}  ?  (bre => $bre)    : ()),
3474         ($args->{suppress_mvr} ?  () : (mvr => $mvr)),
3475         %$details
3476     };
3477
3478     $resp->{copy}->location(
3479         $e->retrieve_asset_copy_location($resp->{copy}->location))
3480         if $resp->{copy} and $args->{flesh_acpl};
3481
3482     unless($args->{suppress_patron_details}) {
3483         my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3484         $resp->{patron_first}   = $user->first_given_name,
3485         $resp->{patron_last}    = $user->family_name,
3486         $resp->{patron_barcode} = $card->barcode,
3487         $resp->{patron_alias}   = $user->alias,
3488     };
3489
3490     return $resp;
3491 }
3492
3493
3494
3495 # -----------------------------------------------------
3496 # Returns the MVR object that represents what the
3497 # hold is all about
3498 # -----------------------------------------------------
3499 sub find_hold_mvr {
3500     my( $e, $hold, $args ) = @_;
3501
3502     my $tid;
3503     my $copy;
3504     my $volume;
3505     my $issuance;
3506     my $part;
3507     my $metarecord;
3508     my $no_mvr = $args->{suppress_mvr};
3509
3510     if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3511         $metarecord = $e->retrieve_metabib_metarecord($hold->target)
3512             or return $e->event;
3513         $tid = $metarecord->master_record;
3514
3515     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3516         $tid = $hold->target;
3517
3518     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3519         $volume = $e->retrieve_asset_call_number($hold->target)
3520             or return $e->event;
3521         $tid = $volume->record;
3522
3523     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3524         $issuance = $e->retrieve_serial_issuance([
3525             $hold->target,
3526             {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3527         ]) or return $e->event;
3528
3529         $tid = $issuance->subscription->record_entry;
3530
3531     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3532         $part = $e->retrieve_biblio_monograph_part([
3533             $hold->target
3534         ]) or return $e->event;
3535
3536         $tid = $part->record;
3537
3538     } 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 ) {
3539         $copy = $e->retrieve_asset_copy([
3540             $hold->target,
3541             {flesh => 1, flesh_fields => {acp => ['call_number']}}
3542         ]) or return $e->event;
3543
3544         $volume = $copy->call_number;
3545         $tid = $volume->record;
3546     }
3547
3548     if(!$copy and ref $hold->current_copy ) {
3549         $copy = $hold->current_copy;
3550         $hold->current_copy($copy->id) unless $args->{include_current_copy};
3551     }
3552
3553     if(!$volume and $copy) {
3554         $volume = $e->retrieve_asset_call_number($copy->call_number);
3555     }
3556
3557     # TODO return metarcord mvr for M holds
3558     my $title = $e->retrieve_biblio_record_entry($tid);
3559     return ( ($no_mvr) ? undef : $U->record_to_mvr($title), 
3560         $volume, $copy, $issuance, $part, $title, $metarecord);
3561 }
3562
3563 __PACKAGE__->register_method(
3564     method    => 'clear_shelf_cache',
3565     api_name  => 'open-ils.circ.hold.clear_shelf.get_cache',
3566     stream    => 1,
3567     signature => {
3568         desc => q/
3569             Returns the holds processed with the given cache key
3570         /
3571     }
3572 );
3573
3574 sub clear_shelf_cache {
3575     my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3576     my $e = new_editor(authtoken => $auth, xact => 1);
3577     return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3578
3579     $chunk_size ||= 25;
3580     $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3581
3582     my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3583
3584     if (!$hold_data) {
3585         $logger->info("no hold data found in cache"); # XXX TODO return event
3586         $e->rollback;
3587         return undef;
3588     }
3589
3590     my $maximum = 0;
3591     foreach (keys %$hold_data) {
3592         $maximum += scalar(@{ $hold_data->{$_} });
3593     }
3594     $client->respond({"maximum" => $maximum, "progress" => 0});
3595
3596     for my $action (sort keys %$hold_data) {
3597         while (@{$hold_data->{$action}}) {
3598             my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3599
3600             my $result_chunk = $e->json_query({
3601                 "select" => {
3602                     "acp" => ["barcode"],
3603                     "au" => [qw/
3604                         first_given_name second_given_name family_name alias
3605                     /],
3606                     "acn" => ["label"],
3607                     "acnp" => [{column => "label", alias => "prefix"}],
3608                     "acns" => [{column => "label", alias => "suffix"}],
3609                     "bre" => ["marc"],
3610                     "acpl" => ["name"],
3611                     "ahr" => ["id"]
3612                 },
3613                 "from" => {
3614                     "ahr" => {
3615                         "acp" => {
3616                             "field" => "id", "fkey" => "current_copy",
3617                             "join" => {
3618                                 "acn" => {
3619                                     "field" => "id", "fkey" => "call_number",
3620                                     "join" => {
3621                                         "bre" => {
3622                                             "field" => "id", "fkey" => "record"
3623                                         },
3624                                         "acnp" => {
3625                                             "field" => "id", "fkey" => "prefix"
3626                                         },
3627                                         "acns" => {
3628                                             "field" => "id", "fkey" => "suffix"
3629                                         }
3630                                     }
3631                                 },
3632                                 "acpl" => {"field" => "id", "fkey" => "location"}
3633                             }
3634                         },
3635                         "au" => {"field" => "id", "fkey" => "usr"}
3636                     }
3637                 },
3638                 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3639             }, {"substream" => 1}) or return $e->die_event;
3640
3641             $client->respond([
3642                 map {
3643                     +{"action" => $action, "hold_details" => $_}
3644                 } @$result_chunk
3645             ]);
3646         }
3647     }
3648
3649     $e->rollback;
3650     return undef;
3651 }
3652
3653
3654 __PACKAGE__->register_method(
3655     method    => 'clear_shelf_process',
3656     stream    => 1,
3657     api_name  => 'open-ils.circ.hold.clear_shelf.process',
3658     signature => {
3659         desc => q/
3660             1. Find all holds that have expired on the holds shelf
3661             2. Cancel the holds
3662             3. If a clear-shelf status is configured, put targeted copies into this status
3663             4. Divide copies into 3 groups: items to transit, items to reshelve, and items
3664                 that are needed for holds.  No subsequent action is taken on the holds
3665                 or items after grouping.
3666         /
3667     }
3668 );
3669
3670 sub clear_shelf_process {
3671     my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
3672
3673     my $e = new_editor(authtoken=>$auth);
3674     $e->checkauth or return $e->die_event;
3675     my $cache = OpenSRF::Utils::Cache->new('global');
3676
3677     $org_id ||= $e->requestor->ws_ou;
3678     $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
3679
3680     my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
3681
3682     my @hold_ids = $self->method_lookup(
3683         "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
3684     )->run($auth, $org_id, $match_copy);
3685
3686     $e->xact_begin;
3687
3688     my @holds;
3689     my @canceled_holds; # newly canceled holds
3690     $chunk_size ||= 25; # chunked status updates
3691     $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3692
3693     my $counter = 0;
3694     for my $hold_id (@hold_ids) {
3695
3696         $logger->info("Clear shelf processing hold $hold_id");
3697
3698         my $hold = $e->retrieve_action_hold_request([
3699             $hold_id, {
3700                 flesh => 1,
3701                 flesh_fields => {ahr => ['current_copy']}
3702             }
3703         ]);
3704
3705         if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
3706             $hold->cancel_time('now');
3707             $hold->cancel_cause(2); # Hold Shelf expiration
3708             $e->update_action_hold_request($hold) or return $e->die_event;
3709             push(@canceled_holds, $hold_id);
3710         }
3711
3712         my $copy = $hold->current_copy;
3713
3714         if($copy_status or $copy_status == 0) {
3715             # if a clear-shelf copy status is defined, update the copy
3716             $copy->status($copy_status);
3717             $copy->edit_date('now');
3718             $copy->editor($e->requestor->id);
3719             $e->update_asset_copy($copy) or return $e->die_event;
3720         }
3721
3722         push(@holds, $hold);
3723         $client->respond({maximum => scalar(@holds), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
3724     }
3725
3726     if ($e->commit) {
3727
3728         my %cache_data = (
3729             hold => [],
3730             transit => [],
3731             shelf => [],
3732             pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
3733         );
3734
3735         for my $hold (@holds) {
3736
3737             my $copy = $hold->current_copy;
3738             my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
3739
3740             if($alt_hold and !$match_copy) {
3741
3742                 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
3743
3744             } elsif($copy->circ_lib != $e->requestor->ws_ou) {
3745
3746                 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
3747
3748             } else {
3749
3750                 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
3751             }
3752         }
3753
3754         my $cache_key = md5_hex(time . $$ . rand());
3755         $logger->info("clear_shelf_cache: storing under $cache_key");
3756         $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours.  configurable?
3757
3758         # tell the client we're done
3759         $client->respond_complete({cache_key => $cache_key});
3760
3761         # ------------
3762         # fire off the hold cancelation trigger and wait for response so don't flood the service
3763
3764         # refetch the holds to pick up the caclulated cancel_time,
3765         # which may be needed by Action/Trigger
3766         $e->xact_begin;
3767         my $updated_holds = [];
3768         $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
3769         $e->rollback;
3770
3771         $U->create_events_for_hook(
3772             'hold_request.cancel.expire_holds_shelf',
3773             $_, $org_id, undef, undef, 1) for @$updated_holds;
3774
3775     } else {
3776         # tell the client we're done
3777         $client->respond_complete;
3778     }
3779 }
3780
3781 # returns IDs for holds that are on the holds shelf but 
3782 # have had their pickup_libs change while on the shelf.
3783 sub pickup_lib_changed_on_shelf_holds {
3784     my $e = shift;
3785     my $org_id = shift;
3786     my $ignore_holds = shift;
3787     $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
3788
3789     my $query = {
3790         select => { alhr => ['id'] },
3791         from   => {
3792             alhr => {
3793                 acp => {
3794                     field => 'id',
3795                     fkey  => 'current_copy'
3796                 },
3797             }
3798         },
3799         where => {
3800             '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
3801             '+alhr' => {
3802                 capture_time     => { "!=" => undef },
3803                 fulfillment_time => undef,
3804                 current_shelf_lib => $org_id,
3805                 pickup_lib => {'!='  => {'+alhr' => 'current_shelf_lib'}}
3806             }
3807         }
3808     };
3809
3810     $query->{where}->{'+alhr'}->{id} =
3811         {'not in' => $ignore_holds} if @$ignore_holds;
3812
3813     my $hold_ids = $e->json_query($query);
3814     return [ map { $_->{id} } @$hold_ids ];
3815 }
3816
3817 __PACKAGE__->register_method(
3818     method    => 'usr_hold_summary',
3819     api_name  => 'open-ils.circ.holds.user_summary',
3820     signature => q/
3821         Returns a summary of holds statuses for a given user
3822     /
3823 );
3824
3825 sub usr_hold_summary {
3826     my($self, $conn, $auth, $user_id) = @_;
3827
3828     my $e = new_editor(authtoken=>$auth);
3829     $e->checkauth or return $e->event;
3830     $e->allowed('VIEW_HOLD') or return $e->event;
3831
3832     my $holds = $e->search_action_hold_request(
3833         {
3834             usr =>  $user_id ,
3835             fulfillment_time => undef,
3836             cancel_time      => undef,
3837         }
3838     );
3839
3840     my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
3841     $summary{_hold_status($e, $_)} += 1 for @$holds;
3842     return \%summary;
3843 }
3844
3845
3846
3847 __PACKAGE__->register_method(
3848     method    => 'hold_has_copy_at',
3849     api_name  => 'open-ils.circ.hold.has_copy_at',
3850     signature => {
3851         desc   =>
3852                 'Returns the ID of the found copy and name of the shelving location if there is ' .
3853                 'an available copy at the specified org unit.  Returns empty hash otherwise.  '   .
3854                 'The anticipated use for this method is to determine whether an item is '         .
3855                 'available at the library where the user is placing the hold (or, alternatively, '.
3856                 'at the pickup library) to encourage bypassing the hold placement and just '      .
3857                 'checking out the item.' ,
3858         params => [
3859             { desc => 'Authentication Token', type => 'string' },
3860             { desc => 'Method Arguments.  Options include: hold_type, hold_target, org_unit.  '
3861                     . 'hold_type is the hold type code (T, V, C, M, ...).  '
3862                     . 'hold_target is the identifier of the hold target object.  '
3863                     . 'org_unit is org unit ID.',
3864               type => 'object'
3865             }
3866         ],
3867         return => {
3868             desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
3869             type => 'object'
3870         }
3871     }
3872 );
3873
3874 sub hold_has_copy_at {
3875     my($self, $conn, $auth, $args) = @_;
3876
3877     my $e = new_editor(authtoken=>$auth);
3878     $e->checkauth or return $e->event;
3879
3880     my $hold_type   = $$args{hold_type};
3881     my $hold_target = $$args{hold_target};
3882     my $org_unit    = $$args{org_unit};
3883
3884     my $query = {
3885         select => {acp => ['id'], acpl => ['name']},
3886         from   => {
3887             acp => {
3888                 acpl => {
3889                     field => 'id',
3890                     filter => { holdable => 't', deleted => 'f' },
3891                     fkey => 'location'
3892                 },
3893                 ccs  => {field => 'id', filter => { holdable => 't'}, fkey => 'status'  }
3894             }
3895         },
3896         where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
3897         limit => 1
3898     };
3899
3900     if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
3901
3902         $query->{where}->{'+acp'}->{id} = $hold_target;
3903
3904     } elsif($hold_type eq 'V') {
3905
3906         $query->{where}->{'+acp'}->{call_number} = $hold_target;
3907
3908     } elsif($hold_type eq 'P') {
3909
3910         $query->{from}->{acp}->{acpm} = {
3911             field  => 'target_copy',
3912             fkey   => 'id',
3913             filter => {part => $hold_target},
3914         };
3915
3916     } elsif($hold_type eq 'I') {
3917
3918         $query->{from}->{acp}->{sitem} = {
3919             field  => 'unit',
3920             fkey   => 'id',
3921             filter => {issuance => $hold_target},
3922         };
3923
3924     } elsif($hold_type eq 'T') {
3925
3926         $query->{from}->{acp}->{acn} = {
3927             field  => 'id',
3928             fkey   => 'call_number',
3929             'join' => {
3930                 bre => {
3931                     field  => 'id',
3932                     filter => {id => $hold_target},
3933                     fkey   => 'record'
3934                 }
3935             }
3936         };
3937
3938     } else {
3939
3940         $query->{from}->{acp}->{acn} = {
3941             field => 'id',
3942             fkey  => 'call_number',
3943             join  => {
3944                 bre => {
3945                     field => 'id',
3946                     fkey  => 'record',
3947                     join  => {
3948                         mmrsm => {
3949                             field  => 'source',
3950                             fkey   => 'id',
3951                             filter => {metarecord => $hold_target},
3952                         }
3953                     }
3954                 }
3955             }
3956         };
3957     }
3958
3959     my $res = $e->json_query($query)->[0] or return {};
3960     return {copy => $res->{id}, location => $res->{name}} if $res;
3961 }
3962
3963
3964 # returns true if the user already has an item checked out
3965 # that could be used to fulfill the requested hold.
3966 sub hold_item_is_checked_out {
3967     my($e, $user_id, $hold_type, $hold_target) = @_;
3968
3969     my $query = {
3970         select => {acp => ['id']},
3971         from   => {acp => {}},
3972         where  => {
3973             '+acp' => {
3974                 id => {
3975                     in => { # copies for circs the user has checked out
3976                         select => {circ => ['target_copy']},
3977                         from   => 'circ',
3978                         where  => {
3979                             usr => $user_id,
3980                             checkin_time => undef,
3981                             '-or' => [
3982                                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
3983                                 {stop_fines => undef}
3984                             ],
3985                         }
3986                     }
3987                 }
3988             }
3989         },
3990         limit => 1
3991     };
3992
3993     if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
3994
3995         $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
3996
3997     } elsif($hold_type eq 'V') {
3998
3999         $query->{where}->{'+acp'}->{call_number} = $hold_target;
4000
4001      } elsif($hold_type eq 'P') {
4002
4003         $query->{from}->{acp}->{acpm} = {
4004             field  => 'target_copy',
4005             fkey   => 'id',
4006             filter => {part => $hold_target},
4007         };
4008
4009      } elsif($hold_type eq 'I') {
4010
4011         $query->{from}->{acp}->{sitem} = {
4012             field  => 'unit',
4013             fkey   => 'id',
4014             filter => {issuance => $hold_target},
4015         };
4016
4017     } elsif($hold_type eq 'T') {
4018
4019         $query->{from}->{acp}->{acn} = {
4020             field  => 'id',
4021             fkey   => 'call_number',
4022             'join' => {
4023                 bre => {
4024                     field  => 'id',
4025                     filter => {id => $hold_target},
4026                     fkey   => 'record'
4027                 }
4028             }
4029         };
4030
4031     } else {
4032
4033         $query->{from}->{acp}->{acn} = {
4034             field => 'id',
4035             fkey => 'call_number',
4036             join => {
4037                 bre => {
4038                     field => 'id',
4039                     fkey => 'record',
4040                     join => {
4041                         mmrsm => {
4042                             field => 'source',
4043                             fkey => 'id',
4044                             filter => {metarecord => $hold_target},
4045                         }
4046                     }
4047                 }
4048             }
4049         };
4050     }
4051
4052     return $e->json_query($query)->[0];
4053 }
4054
4055 __PACKAGE__->register_method(
4056     method    => 'change_hold_title',
4057     api_name  => 'open-ils.circ.hold.change_title',
4058     signature => {
4059         desc => q/
4060             Updates all title level holds targeting the specified bibs to point a new bib./,
4061         params => [
4062             { desc => 'Authentication Token', type => 'string' },
4063             { desc => 'New Target Bib Id',    type => 'number' },
4064             { desc => 'Old Target Bib Ids',   type => 'array'  },
4065         ],
4066         return => { desc => '1 on success' }
4067     }
4068 );
4069
4070 __PACKAGE__->register_method(
4071     method    => 'change_hold_title_for_specific_holds',
4072     api_name  => 'open-ils.circ.hold.change_title.specific_holds',
4073     signature => {
4074         desc => q/
4075             Updates specified holds to target new bib./,
4076         params => [
4077             { desc => 'Authentication Token', type => 'string' },
4078             { desc => 'New Target Bib Id',    type => 'number' },
4079             { desc => 'Holds Ids for holds to update',   type => 'array'  },
4080         ],
4081         return => { desc => '1 on success' }
4082     }
4083 );
4084
4085
4086 sub change_hold_title {
4087     my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4088
4089     my $e = new_editor(authtoken=>$auth, xact=>1);
4090     return $e->die_event unless $e->checkauth;
4091
4092     my $holds = $e->search_action_hold_request(
4093         [
4094             {
4095                 capture_time     => undef,
4096                 cancel_time      => undef,
4097                 fulfillment_time => undef,
4098                 hold_type        => 'T',
4099                 target           => $bib_ids
4100             },
4101             {
4102                 flesh        => 1,
4103                 flesh_fields => { ahr => ['usr'] }
4104             }
4105         ],
4106         { substream => 1 }
4107     );
4108
4109     for my $hold (@$holds) {
4110         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4111         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4112         $hold->target( $new_bib_id );
4113         $e->update_action_hold_request($hold) or return $e->die_event;
4114     }
4115
4116     $e->commit;
4117
4118     _reset_hold($self, $e->requestor, $_) for @$holds;
4119
4120     return 1;
4121 }
4122
4123 sub change_hold_title_for_specific_holds {
4124     my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4125
4126     my $e = new_editor(authtoken=>$auth, xact=>1);
4127     return $e->die_event unless $e->checkauth;
4128
4129     my $holds = $e->search_action_hold_request(
4130         [
4131             {
4132                 capture_time     => undef,
4133                 cancel_time      => undef,
4134                 fulfillment_time => undef,
4135                 hold_type        => 'T',
4136                 id               => $hold_ids
4137             },
4138             {
4139                 flesh        => 1,
4140                 flesh_fields => { ahr => ['usr'] }
4141             }
4142         ],
4143         { substream => 1 }
4144     );
4145
4146     for my $hold (@$holds) {
4147         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4148         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4149         $hold->target( $new_bib_id );
4150         $e->update_action_hold_request($hold) or return $e->die_event;
4151     }
4152
4153     $e->commit;
4154
4155     _reset_hold($self, $e->requestor, $_) for @$holds;
4156
4157     return 1;
4158 }
4159
4160 __PACKAGE__->register_method(
4161     method    => 'rec_hold_count',
4162     api_name  => 'open-ils.circ.bre.holds.count',
4163     signature => {
4164         desc => q/Returns the total number of holds that target the
4165             selected bib record or its associated copies and call_numbers/,
4166         params => [
4167             { desc => 'Bib ID', type => 'number' },
4168             { desc => q/Optional arguments.  Supported arguments include:
4169                 "pickup_lib_descendant" -> limit holds to those whose pickup
4170                 library is equal to or is a child of the provided org unit/,
4171                 type => 'object'
4172             }
4173         ],
4174         return => {desc => 'Hold count', type => 'number'}
4175     }
4176 );
4177
4178 __PACKAGE__->register_method(
4179     method    => 'rec_hold_count',
4180     api_name  => 'open-ils.circ.mmr.holds.count',
4181     signature => {
4182         desc => q/Returns the total number of holds that target the
4183             selected metarecord or its associated copies, call_numbers, and bib records/,
4184         params => [
4185             { desc => 'Metarecord ID', type => 'number' },
4186         ],
4187         return => {desc => 'Hold count', type => 'number'}
4188     }
4189 );
4190
4191 # XXX Need to add type I holds to these counts
4192 sub rec_hold_count {
4193     my($self, $conn, $target_id, $args) = @_;
4194     $args ||= {};
4195
4196     my $mmr_join = {
4197         mmrsm => {
4198             field => 'source',
4199             fkey => 'id',
4200             filter => {metarecord => $target_id}
4201         }
4202     };
4203
4204     my $bre_join = {
4205         bre => {
4206             field => 'id',
4207             filter => { id => $target_id },
4208             fkey => 'record'
4209         }
4210     };
4211
4212     if($self->api_name =~ /mmr/) {
4213         delete $bre_join->{bre}->{filter};
4214         $bre_join->{bre}->{join} = $mmr_join;
4215     }
4216
4217     my $cn_join = {
4218         acn => {
4219             field => 'id',
4220             fkey => 'call_number',
4221             join => $bre_join
4222         }
4223     };
4224
4225     my $query = {
4226         select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4227         from => 'ahr',
4228         where => {
4229             '+ahr' => {
4230                 cancel_time => undef,
4231                 fulfillment_time => undef,
4232                 '-or' => [
4233                     {
4234                         '-and' => {
4235                             hold_type => [qw/C F R/],
4236                             target => {
4237                                 in => {
4238                                     select => {acp => ['id']},
4239                                     from => { acp => $cn_join }
4240                                 }
4241                             }
4242                         }
4243                     },
4244                     {
4245                         '-and' => {
4246                             hold_type => 'V',
4247                             target => {
4248                                 in => {
4249                                     select => {acn => ['id']},
4250                                     from => {acn => $bre_join}
4251                                 }
4252                             }
4253                         }
4254                     },
4255                     {
4256                         '-and' => {
4257                             hold_type => 'P',
4258                             target => {
4259                                 in => {
4260                                     select => {bmp => ['id']},
4261                                     from => {bmp => $bre_join}
4262                                 }
4263                             }
4264                         }
4265                     },
4266                     {
4267                         '-and' => {
4268                             hold_type => 'T',
4269                             target => $target_id
4270                         }
4271                     }
4272                 ]
4273             }
4274         }
4275     };
4276
4277     if($self->api_name =~ /mmr/) {
4278         $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4279             '-and' => {
4280                 hold_type => 'T',
4281                 target => {
4282                     in => {
4283                         select => {bre => ['id']},
4284                         from => {bre => $mmr_join}
4285                     }
4286                 }
4287             }
4288         };
4289
4290         $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4291             '-and' => {
4292                 hold_type => 'M',
4293                 target => $target_id
4294             }
4295         };
4296     }
4297
4298
4299     if (my $pld = $args->{pickup_lib_descendant}) {
4300
4301         my $top_ou = new_editor()->search_actor_org_unit(
4302             {parent_ou => undef}
4303         )->[0]; # XXX Assumes single root node. Not alone in this...
4304
4305         $query->{where}->{'+ahr'}->{pickup_lib} = {
4306             in => {
4307                 select  => {aou => [{ 
4308                     column => 'id', 
4309                     transform => 'actor.org_unit_descendants', 
4310                     result_field => 'id' 
4311                 }]},
4312                 from    => 'aou',
4313                 where   => {id => $pld}
4314             }
4315         } if ($pld != $top_ou->id);
4316     }
4317
4318     # To avoid Internal Server Errors, we get an editor, then run the
4319     # query and check the result.  If anything fails, we'll return 0.
4320     my $result = 0;
4321     if (my $e = new_editor()) {
4322         my $query_result = $e->json_query($query);
4323         if ($query_result && @{$query_result}) {
4324             $result = $query_result->[0]->{count}
4325         }
4326     }
4327
4328     return $result;
4329 }
4330
4331 # A helper function to calculate a hold's expiration time at a given
4332 # org_unit. Takes the org_unit as an argument and returns either the
4333 # hold expire time as an ISO8601 string or undef if there is no hold
4334 # expiration interval set for the subject ou.
4335 sub calculate_expire_time
4336 {
4337     my $ou = shift;
4338     my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4339     if($interval) {
4340         my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
4341         return $U->epoch2ISO8601($date->epoch);
4342     }
4343     return undef;
4344 }
4345
4346
4347 __PACKAGE__->register_method(
4348     method    => 'mr_hold_filter_attrs',
4349     api_name  => 'open-ils.circ.mmr.holds.filters',
4350     authoritative => 1,
4351     stream => 1,
4352     signature => {
4353         desc => q/
4354             Returns the set of available formats and languages for the
4355             constituent records of the provided metarcord.
4356             If an array of hold IDs is also provided, information about
4357             each is returned as well.  This information includes:
4358             1. a slightly easier to read version of holdable_formats
4359             2. attributes describing the set of format icons included
4360                in the set of desired, constituent records.
4361         /,
4362         params => [
4363             {desc => 'Metarecord ID', type => 'number'},
4364             {desc => 'Context Org ID', type => 'number'},
4365             {desc => 'Hold ID List', type => 'array'},
4366         ],
4367         return => {
4368             desc => q/
4369                 Stream of objects.  The first will have a 'metarecord' key
4370                 containing non-hold-specific metarecord information, subsequent
4371                 responses will contain a 'hold' key containing hold-specific
4372                 information
4373             /, 
4374             type => 'object'
4375         }
4376     }
4377 );
4378
4379 sub mr_hold_filter_attrs { 
4380     my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4381     my $e = new_editor();
4382
4383     # by default, return MR / hold attributes for all constituent
4384     # records with holdable copies.  If there is a hard boundary,
4385     # though, limit to records with copies within the boundary,
4386     # since anything outside the boundary can never be held.
4387     my $org_depth = 0;
4388     if ($org_id) {
4389         $org_depth = $U->ou_ancestor_setting_value(
4390             $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4391     }
4392
4393     # get all org-scoped records w/ holdable copies for this metarecord
4394     my ($bre_ids) = $self->method_lookup(
4395         'open-ils.circ.holds.metarecord.filtered_records')->run(
4396             $mr_id, undef, $org_id, $org_depth);
4397
4398     my $item_lang_attr = 'item_lang'; # configurable?
4399     my $format_attr = $e->retrieve_config_global_flag(
4400         'opac.metarecord.holds.format_attr')->value;
4401
4402     # helper sub for fetching ccvms for a batch of record IDs
4403     sub get_batch_ccvms {
4404         my ($e, $attr, $bre_ids) = @_;
4405         return [] unless $bre_ids and @$bre_ids;
4406         my $vals = $e->search_metabib_record_attr_flat({
4407             attr => $attr,
4408             id => $bre_ids
4409         });
4410         return [] unless @$vals;
4411         return $e->search_config_coded_value_map({
4412             ctype => $attr,
4413             code => [map {$_->value} @$vals]
4414         });
4415     }
4416
4417     my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4418     my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4419
4420     $client->respond({
4421         metarecord => {
4422             id => $mr_id,
4423             formats => $formats,
4424             langs => $langs
4425         }
4426     });
4427
4428     return unless $hold_ids;
4429     my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4430     $icon_attr = $icon_attr ? $icon_attr->value : '';
4431
4432     for my $hold_id (@$hold_ids) {
4433         my $hold = $e->retrieve_action_hold_request($hold_id) 
4434             or return $e->event;
4435
4436         next unless $hold->hold_type eq 'M';
4437
4438         my $resp = {
4439             hold => {
4440                 id => $hold_id,
4441                 formats => [],
4442                 langs => []
4443             }
4444         };
4445
4446         # collect the ccvm's for the selected formats / language
4447         # (i.e. the holdable formats) on the MR.
4448         # this assumes a two-key structure for format / language,
4449         # though no assumption is made about the keys themselves.
4450         my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4451         my $lang_vals = [];
4452         my $format_vals = [];
4453         for my $val (values %$hformats) {
4454             # val is either a single ccvm or an array of them
4455             $val = [$val] unless ref $val eq 'ARRAY';
4456             for my $node (@$val) {
4457                 push (@$lang_vals, $node->{_val})   
4458                     if $node->{_attr} eq $item_lang_attr; 
4459                 push (@$format_vals, $node->{_val})   
4460                     if $node->{_attr} eq $format_attr;
4461             }
4462         }
4463
4464         # fetch the ccvm's for consistency with the {metarecord} blob
4465         $resp->{hold}{formats} = $e->search_config_coded_value_map({
4466             ctype => $format_attr, code => $format_vals});
4467         $resp->{hold}{langs} = $e->search_config_coded_value_map({
4468             ctype => $item_lang_attr, code => $lang_vals});
4469
4470         # find all of the bib records within this metarcord whose 
4471         # format / language match the holdable formats on the hold
4472         my ($bre_ids) = $self->method_lookup(
4473             'open-ils.circ.holds.metarecord.filtered_records')->run(
4474                 $hold->target, $hold->holdable_formats, 
4475                 $hold->selection_ou, $hold->selection_depth);
4476
4477         # now find all of the 'icon' attributes for the records
4478         $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4479         $client->respond($resp);
4480     }
4481
4482     return;
4483 }
4484
4485 __PACKAGE__->register_method(
4486     method        => "copy_has_holds_count",
4487     api_name      => "open-ils.circ.copy.has_holds_count",
4488     authoritative => 1,
4489     signature     => {
4490         desc => q/
4491             Returns the number of holds a paticular copy has
4492         /,
4493         params => [
4494             { desc => 'Authentication Token', type => 'string'},
4495             { desc => 'Copy ID', type => 'number'}
4496         ],
4497         return => {
4498             desc => q/
4499                 Simple count value
4500             /,
4501             type => 'number'
4502         }
4503     }
4504 );
4505
4506 sub copy_has_holds_count {
4507     my( $self, $conn, $auth, $copyid ) = @_;
4508     my $e = new_editor(authtoken=>$auth);
4509     return $e->event unless $e->checkauth;
4510
4511     if( $copyid && $copyid > 0 ) {
4512         my $meth = 'retrieve_action_has_holds_count';
4513         my $data = $e->$meth($copyid);
4514         if($data){
4515                 return $data->count();
4516         }
4517     }
4518     return 0;
4519 }
4520
4521 __PACKAGE__->register_method(
4522     method        => "hold_metadata",
4523     api_name      => "open-ils.circ.hold.get_metadata",
4524     authoritative => 1,
4525     stream => 1,
4526     signature     => {
4527         desc => q/
4528             Returns a stream of objects containing whatever bib, 
4529             volume, etc. data is available to the specific hold 
4530             type and target.
4531         /,
4532         params => [
4533             {desc => 'Hold Type', type => 'string'},
4534             {desc => 'Hold Target(s)', type => 'number or array'},
4535             {desc => 'Context org unit (optional)', type => 'number'}
4536         ],
4537         return => {
4538             desc => q/
4539                 Stream of hold metadata objects.
4540             /,
4541             type => 'object'
4542         }
4543     }
4544 );
4545
4546 sub hold_metadata {
4547     my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
4548
4549     $hold_targets = [$hold_targets] unless ref $hold_targets;
4550
4551     my $e = new_editor();
4552     for my $target (@$hold_targets) {
4553
4554         # create a dummy hold for find_hold_mvr
4555         my $hold = Fieldmapper::action::hold_request->new;
4556         $hold->hold_type($hold_type);
4557         $hold->target($target);
4558
4559         my (undef, $volume, $copy, $issuance, $part, $bre, $metarecord) = 
4560             find_hold_mvr($e, $hold, {suppress_mvr => 1});
4561
4562         $bre->clear_marc; # avoid bulk
4563
4564         my $meta = {
4565             target => $target,
4566             copy => $copy,
4567             volume => $volume,
4568             issuance => $issuance,
4569             part => $part,
4570             bibrecord => $bre,
4571             metarecord => $metarecord,
4572             metarecord_filters => {}
4573         };
4574
4575         # If this is a bib hold or metarecord hold, also return the
4576         # available set of MR filters (AKA "Holdable Formats") for the
4577         # hold.  For bib holds these may be used to upgrade the hold
4578         # from a bib to metarecord hold.
4579         if ($hold_type eq 'T') {
4580             my $map = $e->search_metabib_metarecord_source_map(
4581                 {source => $meta->{bibrecord}->id})->[0];
4582
4583             if ($map) {
4584                 $meta->{metarecord} = 
4585                     $e->retrieve_metabib_metarecord($map->metarecord);
4586             }
4587         }
4588
4589         if ($meta->{metarecord}) {
4590
4591             my ($filters) = 
4592                 $self->method_lookup('open-ils.circ.mmr.holds.filters')
4593                     ->run($meta->{metarecord}->id, $org_id);
4594
4595             if ($filters) {
4596                 $meta->{metarecord_filters} = $filters->{metarecord};
4597             }
4598         }
4599
4600         $client->respond($meta);
4601     }
4602
4603     return undef;
4604 }
4605
4606 1;