]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
LP#1712854: Speed improvements for two hold interfaces
[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 OpenSRF::Utils 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             cleanse_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 => OpenSRF::Utils::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(cleanse_ISO8601($start_time)) :
1107         DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
1108
1109     my $seconds = OpenSRF::Utils->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(cleanse_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(cleanse_ISO8601($start_time));
1309         my $end_time   = $start_time->add(seconds => OpenSRF::Utils::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 = OpenSRF::Utils::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             OpenSRF::Utils::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     my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2385     my $oargs        = $params{oargs}        || {};
2386
2387     if($oargs->{events}) {
2388         @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2389     }
2390
2391
2392     my $patron = $e->retrieve_actor_user($params{patronid})
2393         or return $e->event;
2394
2395     if( $e->requestor->id ne $patron->id ) {
2396         return $e->event unless
2397             $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2398     }
2399
2400     return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2401
2402     my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2403         or return $e->event;
2404
2405     my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2406     my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2407
2408     my @status = ();
2409     my $return_depth = $hard_boundary; # default depth to return on success
2410     if(defined $soft_boundary and $depth < $soft_boundary) {
2411         # work up the tree and as soon as we find a potential copy, use that depth
2412         # also, make sure we don't go past the hard boundary if it exists
2413
2414         # our min boundary is the greater of user-specified boundary or hard boundary
2415         my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2416             $hard_boundary : $depth;
2417
2418         my $depth = $soft_boundary;
2419         while($depth >= $min_depth) {
2420             $logger->info("performing hold possibility check with soft boundary $depth");
2421             @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2422             if ($status[0]) {
2423                 $return_depth = $depth;
2424                 last;
2425             }
2426             $depth--;
2427         }
2428     } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2429         # there is no soft boundary, enforce the hard boundary if it exists
2430         $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2431         @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2432     } else {
2433         # no boundaries defined, fall back to user specifed boundary or no boundary
2434         $logger->info("performing hold possibility check with no boundary");
2435         @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2436     }
2437
2438     my $place_unfillable = 0;
2439     $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2440
2441     if ($status[0]) {
2442         return {
2443             "success" => 1,
2444             "depth" => $return_depth,
2445             "local_avail" => $status[1]
2446         };
2447     } elsif ($status[2]) {
2448         my $n = scalar @{$status[2]};
2449         return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2450     } else {
2451         return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2452     }
2453 }
2454
2455
2456
2457 sub do_possibility_checks {
2458     my($e, $patron, $request_lib, $depth, %params) = @_;
2459
2460     my $issuanceid   = $params{issuanceid}      || "";
2461     my $partid       = $params{partid}      || "";
2462     my $titleid      = $params{titleid}      || "";
2463     my $volid        = $params{volume_id};
2464     my $copyid       = $params{copy_id};
2465     my $mrid         = $params{mrid}         || "";
2466     my $pickup_lib   = $params{pickup_lib};
2467     my $hold_type    = $params{hold_type}    || 'T';
2468     my $selection_ou = $params{selection_ou} || $pickup_lib;
2469     my $holdable_formats = $params{holdable_formats};
2470     my $oargs        = $params{oargs}        || {};
2471
2472
2473     my $copy;
2474     my $volume;
2475     my $title;
2476
2477     if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2478
2479         return $e->event unless $copy   = $e->retrieve_asset_copy($copyid);
2480         return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2481         return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
2482
2483         return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2484         return verify_copy_for_hold(
2485             $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2486         );
2487
2488     } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2489
2490         return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2491         return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
2492
2493         return _check_volume_hold_is_possible(
2494             $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2495         );
2496
2497     } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2498
2499         return _check_title_hold_is_possible(
2500             $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2501         );
2502
2503     } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2504
2505         return _check_issuance_hold_is_possible(
2506             $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2507         );
2508
2509     } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2510
2511         return _check_monopart_hold_is_possible(
2512             $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2513         );
2514
2515     } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2516
2517         # pasing undef as the depth to filtered_records causes the depth
2518         # of the selection_ou to be used, which is not what we want here.
2519         $depth ||= 0;
2520
2521         my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2522         my @status = ();
2523         for my $rec (@$recs) {
2524             @status = _check_title_hold_is_possible(
2525                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2526             );
2527             last if $status[0];
2528         }
2529         return @status;
2530     }
2531 #   else { Unrecognized hold_type ! }   # FIXME: return error? or 0?
2532 }
2533
2534 sub MR_filter_records {
2535     my $self = shift;
2536     my $client = shift;
2537     my $m = shift;
2538     my $f = shift;
2539     my $o = shift;
2540     my $d = shift;
2541     my $opac_visible = shift;
2542     
2543     my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2544     return $U->storagereq(
2545         'open-ils.storage.metarecord.filtered_records.atomic', 
2546         $m, $f, $org_at_depth, $opac_visible
2547     );
2548 }
2549 __PACKAGE__->register_method(
2550     method   => 'MR_filter_records',
2551     api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2552 );
2553
2554
2555 my %prox_cache;
2556 sub create_ranged_org_filter {
2557     my($e, $selection_ou, $depth) = @_;
2558
2559     # find the orgs from which this hold may be fulfilled,
2560     # based on the selection_ou and depth
2561
2562     my $top_org = $e->search_actor_org_unit([
2563         {parent_ou => undef},
2564         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2565     my %org_filter;
2566
2567     return () if $depth == $top_org->ou_type->depth;
2568
2569     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2570     %org_filter = (circ_lib => []);
2571     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2572
2573     $logger->info("hold org filter at depth $depth and selection_ou ".
2574         "$selection_ou created list of @{$org_filter{circ_lib}}");
2575
2576     return %org_filter;
2577 }
2578
2579
2580 sub _check_title_hold_is_possible {
2581     my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2582     # $holdable_formats is now unused. We pre-filter the MR's records.
2583
2584     my $e = new_editor();
2585     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2586
2587     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2588     my $copies = $e->json_query(
2589         {
2590             select => { acp => ['id', 'circ_lib'] },
2591               from => {
2592                 acp => {
2593                     acn => {
2594                         field  => 'id',
2595                         fkey   => 'call_number',
2596                         filter => { record => $titleid }
2597                     },
2598                     acpl => {
2599                                 field => 'id',
2600                                 filter => { holdable => 't', deleted => 'f' },
2601                                 fkey => 'location'
2602                             },
2603                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   },
2604                     acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2605                 }
2606             },
2607             where => {
2608                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2609                 '+acpm' => { target_copy => undef } # ignore part-linked copies
2610             }
2611         }
2612     );
2613
2614     $logger->info("title possible found ".scalar(@$copies)." potential copies");
2615     return (
2616         0, 0, [
2617             new OpenILS::Event(
2618                 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2619                 "payload" => {"fail_part" => "no_ultimate_items"}
2620             )
2621         ]
2622     ) unless @$copies;
2623
2624     # -----------------------------------------------------------------------
2625     # sort the copies into buckets based on their circ_lib proximity to
2626     # the patron's home_ou.
2627     # -----------------------------------------------------------------------
2628
2629     my $home_org = $patron->home_ou;
2630     my $req_org = $request_lib->id;
2631
2632     $prox_cache{$home_org} =
2633         $e->search_actor_org_unit_proximity({from_org => $home_org})
2634         unless $prox_cache{$home_org};
2635     my $home_prox = $prox_cache{$home_org};
2636     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2637
2638     my %buckets;
2639     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2640     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2641
2642     my @keys = sort { $a <=> $b } keys %buckets;
2643
2644
2645     if( $home_org ne $req_org ) {
2646       # -----------------------------------------------------------------------
2647       # shove the copies close to the request_lib into the primary buckets
2648       # directly before the farthest away copies.  That way, they are not
2649       # given priority, but they are checked before the farthest copies.
2650       # -----------------------------------------------------------------------
2651         $prox_cache{$req_org} =
2652             $e->search_actor_org_unit_proximity({from_org => $req_org})
2653             unless $prox_cache{$req_org};
2654         my $req_prox = $prox_cache{$req_org};
2655
2656         my %buckets2;
2657         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2658         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2659
2660         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
2661         my $new_key = $highest_key - 0.5; # right before the farthest prox
2662         my @keys2   = sort { $a <=> $b } keys %buckets2;
2663         for my $key (@keys2) {
2664             last if $key >= $highest_key;
2665             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2666         }
2667     }
2668
2669     @keys = sort { $a <=> $b } keys %buckets;
2670
2671     my $title;
2672     my %seen;
2673     my @status;
2674     my $age_protect_only = 0;
2675     OUTER: for my $key (@keys) {
2676       my @cps = @{$buckets{$key}};
2677
2678       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2679
2680       for my $copyid (@cps) {
2681
2682          next if $seen{$copyid};
2683          $seen{$copyid} = 1; # there could be dupes given the merged buckets
2684          my $copy = $e->retrieve_asset_copy($copyid);
2685          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2686
2687          unless($title) { # grab the title if we don't already have it
2688             my $vol = $e->retrieve_asset_call_number(
2689                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2690             $title = $vol->record;
2691          }
2692
2693          @status = verify_copy_for_hold(
2694             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2695
2696          $age_protect_only ||= $status[3];
2697          last OUTER if $status[0];
2698       }
2699     }
2700
2701     $status[3] = $age_protect_only;
2702     return @status;
2703 }
2704
2705 sub _check_issuance_hold_is_possible {
2706     my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2707
2708     my $e = new_editor();
2709     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2710
2711     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2712     my $copies = $e->json_query(
2713         {
2714             select => { acp => ['id', 'circ_lib'] },
2715               from => {
2716                 acp => {
2717                     sitem => {
2718                         field  => 'unit',
2719                         fkey   => 'id',
2720                         filter => { issuance => $issuanceid }
2721                     },
2722                     acpl => {
2723                         field => 'id',
2724                         filter => { holdable => 't', deleted => 'f' },
2725                         fkey => 'location'
2726                     },
2727                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
2728                 }
2729             },
2730             where => {
2731                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2732             },
2733             distinct => 1
2734         }
2735     );
2736
2737     $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
2738
2739     my $empty_ok;
2740     if (!@$copies) {
2741         $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2742         $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2743
2744         return (
2745             0, 0, [
2746                 new OpenILS::Event(
2747                     "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2748                     "payload" => {"fail_part" => "no_ultimate_items"}
2749                 )
2750             ]
2751         ) unless $empty_ok;
2752
2753         return (1, 0);
2754     }
2755
2756     # -----------------------------------------------------------------------
2757     # sort the copies into buckets based on their circ_lib proximity to
2758     # the patron's home_ou.
2759     # -----------------------------------------------------------------------
2760
2761     my $home_org = $patron->home_ou;
2762     my $req_org = $request_lib->id;
2763
2764     $prox_cache{$home_org} =
2765         $e->search_actor_org_unit_proximity({from_org => $home_org})
2766         unless $prox_cache{$home_org};
2767     my $home_prox = $prox_cache{$home_org};
2768     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2769
2770     my %buckets;
2771     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2772     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2773
2774     my @keys = sort { $a <=> $b } keys %buckets;
2775
2776
2777     if( $home_org ne $req_org ) {
2778       # -----------------------------------------------------------------------
2779       # shove the copies close to the request_lib into the primary buckets
2780       # directly before the farthest away copies.  That way, they are not
2781       # given priority, but they are checked before the farthest copies.
2782       # -----------------------------------------------------------------------
2783         $prox_cache{$req_org} =
2784             $e->search_actor_org_unit_proximity({from_org => $req_org})
2785             unless $prox_cache{$req_org};
2786         my $req_prox = $prox_cache{$req_org};
2787
2788         my %buckets2;
2789         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2790         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2791
2792         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
2793         my $new_key = $highest_key - 0.5; # right before the farthest prox
2794         my @keys2   = sort { $a <=> $b } keys %buckets2;
2795         for my $key (@keys2) {
2796             last if $key >= $highest_key;
2797             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2798         }
2799     }
2800
2801     @keys = sort { $a <=> $b } keys %buckets;
2802
2803     my $title;
2804     my %seen;
2805     my @status;
2806     my $age_protect_only = 0;
2807     OUTER: for my $key (@keys) {
2808       my @cps = @{$buckets{$key}};
2809
2810       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2811
2812       for my $copyid (@cps) {
2813
2814          next if $seen{$copyid};
2815          $seen{$copyid} = 1; # there could be dupes given the merged buckets
2816          my $copy = $e->retrieve_asset_copy($copyid);
2817          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2818
2819          unless($title) { # grab the title if we don't already have it
2820             my $vol = $e->retrieve_asset_call_number(
2821                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2822             $title = $vol->record;
2823          }
2824
2825          @status = verify_copy_for_hold(
2826             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2827
2828          $age_protect_only ||= $status[3];
2829          last OUTER if $status[0];
2830       }
2831     }
2832
2833     if (!$status[0]) {
2834         if (!defined($empty_ok)) {
2835             $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2836             $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2837         }
2838
2839         return (1,0) if ($empty_ok);
2840     }
2841     $status[3] = $age_protect_only;
2842     return @status;
2843 }
2844
2845 sub _check_monopart_hold_is_possible {
2846     my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2847
2848     my $e = new_editor();
2849     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2850
2851     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2852     my $copies = $e->json_query(
2853         {
2854             select => { acp => ['id', 'circ_lib'] },
2855               from => {
2856                 acp => {
2857                     acpm => {
2858                         field  => 'target_copy',
2859                         fkey   => 'id',
2860                         filter => { part => $partid }
2861                     },
2862                     acpl => {
2863                         field => 'id',
2864                         filter => { holdable => 't', deleted => 'f' },
2865                         fkey => 'location'
2866                     },
2867                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
2868                 }
2869             },
2870             where => {
2871                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2872             },
2873             distinct => 1
2874         }
2875     );
2876
2877     $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
2878
2879     my $empty_ok;
2880     if (!@$copies) {
2881         $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
2882         $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2883
2884         return (
2885             0, 0, [
2886                 new OpenILS::Event(
2887                     "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2888                     "payload" => {"fail_part" => "no_ultimate_items"}
2889                 )
2890             ]
2891         ) unless $empty_ok;
2892
2893         return (1, 0);
2894     }
2895
2896     # -----------------------------------------------------------------------
2897     # sort the copies into buckets based on their circ_lib proximity to
2898     # the patron's home_ou.
2899     # -----------------------------------------------------------------------
2900
2901     my $home_org = $patron->home_ou;
2902     my $req_org = $request_lib->id;
2903
2904     $prox_cache{$home_org} =
2905         $e->search_actor_org_unit_proximity({from_org => $home_org})
2906         unless $prox_cache{$home_org};
2907     my $home_prox = $prox_cache{$home_org};
2908     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2909
2910     my %buckets;
2911     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2912     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2913
2914     my @keys = sort { $a <=> $b } keys %buckets;
2915
2916
2917     if( $home_org ne $req_org ) {
2918       # -----------------------------------------------------------------------
2919       # shove the copies close to the request_lib into the primary buckets
2920       # directly before the farthest away copies.  That way, they are not
2921       # given priority, but they are checked before the farthest copies.
2922       # -----------------------------------------------------------------------
2923         $prox_cache{$req_org} =
2924             $e->search_actor_org_unit_proximity({from_org => $req_org})
2925             unless $prox_cache{$req_org};
2926         my $req_prox = $prox_cache{$req_org};
2927
2928         my %buckets2;
2929         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2930         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2931
2932         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
2933         my $new_key = $highest_key - 0.5; # right before the farthest prox
2934         my @keys2   = sort { $a <=> $b } keys %buckets2;
2935         for my $key (@keys2) {
2936             last if $key >= $highest_key;
2937             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2938         }
2939     }
2940
2941     @keys = sort { $a <=> $b } keys %buckets;
2942
2943     my $title;
2944     my %seen;
2945     my @status;
2946     my $age_protect_only = 0;
2947     OUTER: for my $key (@keys) {
2948       my @cps = @{$buckets{$key}};
2949
2950       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2951
2952       for my $copyid (@cps) {
2953
2954          next if $seen{$copyid};
2955          $seen{$copyid} = 1; # there could be dupes given the merged buckets
2956          my $copy = $e->retrieve_asset_copy($copyid);
2957          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2958
2959          unless($title) { # grab the title if we don't already have it
2960             my $vol = $e->retrieve_asset_call_number(
2961                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2962             $title = $vol->record;
2963          }
2964
2965          @status = verify_copy_for_hold(
2966             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2967
2968          $age_protect_only ||= $status[3];
2969          last OUTER if $status[0];
2970       }
2971     }
2972
2973     if (!$status[0]) {
2974         if (!defined($empty_ok)) {
2975             $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
2976             $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2977         }
2978
2979         return (1,0) if ($empty_ok);
2980     }
2981     $status[3] = $age_protect_only;
2982     return @status;
2983 }
2984
2985
2986 sub _check_volume_hold_is_possible {
2987     my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2988     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
2989     my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
2990     $logger->info("checking possibility of volume hold for volume ".$vol->id);
2991
2992     my $filter_copies = [];
2993     for my $copy (@$copies) {
2994         # ignore part-mapped copies for regular volume level holds
2995         push(@$filter_copies, $copy) unless
2996             new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
2997     }
2998     $copies = $filter_copies;
2999
3000     return (
3001         0, 0, [
3002             new OpenILS::Event(
3003                 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3004                 "payload" => {"fail_part" => "no_ultimate_items"}
3005             )
3006         ]
3007     ) unless @$copies;
3008
3009     my @status;
3010     my $age_protect_only = 0;
3011     for my $copy ( @$copies ) {
3012         @status = verify_copy_for_hold(
3013             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3014         $age_protect_only ||= $status[3];
3015         last if $status[0];
3016     }
3017     $status[3] = $age_protect_only;
3018     return @status;
3019 }
3020
3021
3022
3023 sub verify_copy_for_hold {
3024     my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3025     # $oargs should be undef unless we're overriding.
3026     $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3027     my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3028         {
3029             patron           => $patron,
3030             requestor        => $requestor,
3031             copy             => $copy,
3032             title            => $title,
3033             title_descriptor => $title->fixed_fields,
3034             pickup_lib       => $pickup_lib,
3035             request_lib      => $request_lib,
3036             new_hold         => 1,
3037             show_event_list  => 1
3038         }
3039     );
3040
3041     # Check for override permissions on events.
3042     if ($oargs && $permitted && scalar @$permitted) {
3043         # Remove the events from permitted that we can override.
3044         if ($oargs->{events}) {
3045             foreach my $evt (@{$oargs->{events}}) {
3046                 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3047             }
3048         }
3049         # Now, we handle the override all case by checking remaining
3050         # events against override permissions.
3051         if (scalar @$permitted && $oargs->{all}) {
3052             # Pre-set events and failed members of oargs to empty
3053             # arrays, if they are not set, yet.
3054             $oargs->{events} = [] unless ($oargs->{events});
3055             $oargs->{failed} = [] unless ($oargs->{failed});
3056             # When we're done with these checks, we swap permitted
3057             # with a reference to @disallowed.
3058             my @disallowed = ();
3059             foreach my $evt (@{$permitted}) {
3060                 # Check if we've already seen the event in this
3061                 # session and it failed.
3062                 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3063                     push(@disallowed, $evt);
3064                 } else {
3065                     # We have to check if the requestor has the
3066                     # override permission.
3067
3068                     # AppUtils::check_user_perms returns the perm if
3069                     # the user doesn't have it, undef if they do.
3070                     if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3071                         push(@disallowed, $evt);
3072                         push(@{$oargs->{failed}}, $evt->{textcode});
3073                     } else {
3074                         push(@{$oargs->{events}}, $evt->{textcode});
3075                     }
3076                 }
3077             }
3078             $permitted = \@disallowed;
3079         }
3080     }
3081
3082     my $age_protect_only = 0;
3083     if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3084         $age_protect_only = 1;
3085     }
3086
3087     return (
3088         (not scalar @$permitted), # true if permitted is an empty arrayref
3089         (   # XXX This test is of very dubious value; someone should figure
3090             # out what if anything is checking this value
3091             ($copy->circ_lib == $pickup_lib) and
3092             ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3093         ),
3094         $permitted,
3095         $age_protect_only
3096     );
3097 }
3098
3099
3100
3101 sub find_nearest_permitted_hold {
3102
3103     my $class  = shift;
3104     my $editor = shift;     # CStoreEditor object
3105     my $copy   = shift;     # copy to target
3106     my $user   = shift;     # staff
3107     my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3108
3109     my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3110
3111     my $bc = $copy->barcode;
3112
3113     # find any existing holds that already target this copy
3114     my $old_holds = $editor->search_action_hold_request(
3115         {    current_copy => $copy->id,
3116             cancel_time  => undef,
3117             capture_time => undef
3118         }
3119     );
3120
3121     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3122
3123     $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3124         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3125
3126     my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3127
3128     # the nearest_hold API call now needs this
3129     $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3130         unless ref $copy->call_number;
3131
3132     # search for what should be the best holds for this copy to fulfill
3133     my $best_holds = $U->storagereq(
3134         "open-ils.storage.action.hold_request.nearest_hold.atomic", 
3135         $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3136
3137     # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3138     if ($old_holds) {
3139         for my $holdid (@$old_holds) {
3140             next unless $holdid;
3141             push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3142         }
3143     }
3144
3145     unless(@$best_holds) {
3146         $logger->info("circulator: no suitable holds found for copy $bc");
3147         return (undef, $evt);
3148     }
3149
3150
3151     my $best_hold;
3152
3153     # for each potential hold, we have to run the permit script
3154     # to make sure the hold is actually permitted.
3155     my %reqr_cache;
3156     my %org_cache;
3157     for my $holdid (@$best_holds) {
3158         next unless $holdid;
3159         $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3160
3161         my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3162         # Force and recall holds bypass all rules
3163         if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3164             $best_hold = $hold;
3165             last;
3166         }
3167         my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3168         my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3169
3170         $reqr_cache{$hold->requestor} = $reqr;
3171         $org_cache{$hold->request_lib} = $rlib;
3172
3173         # see if this hold is permitted
3174         my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3175             {
3176                 patron_id   => $hold->usr,
3177                 requestor   => $reqr,
3178                 copy        => $copy,
3179                 pickup_lib  => $hold->pickup_lib,
3180                 request_lib => $rlib,
3181                 retarget    => 1
3182             }
3183         );
3184
3185         if( $permitted ) {
3186             $best_hold = $hold;
3187             last;
3188         }
3189     }
3190
3191
3192     unless( $best_hold ) { # no "good" permitted holds were found
3193         # we got nuthin
3194         $logger->info("circulator: no suitable holds found for copy $bc");
3195         return (undef, $evt);
3196     }
3197
3198     $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3199
3200     # indicate a permitted hold was found
3201     return $best_hold if $check_only;
3202
3203     # we've found a permitted hold.  we need to "grab" the copy
3204     # to prevent re-targeted holds (next part) from re-grabbing the copy
3205     $best_hold->current_copy($copy->id);
3206     $editor->update_action_hold_request($best_hold)
3207         or return (undef, $editor->event);
3208
3209
3210     my @retarget;
3211
3212     # re-target any other holds that already target this copy
3213     for my $old_hold (@$old_holds) {
3214         next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3215         $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3216             $old_hold->id." after a better hold [".$best_hold->id."] was found");
3217         $old_hold->clear_current_copy;
3218         $old_hold->clear_prev_check_time;
3219         $editor->update_action_hold_request($old_hold)
3220             or return (undef, $editor->event);
3221         push(@retarget, $old_hold->id);
3222     }
3223
3224     return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3225 }
3226
3227
3228
3229
3230
3231
3232 __PACKAGE__->register_method(
3233     method   => 'all_rec_holds',
3234     api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3235 );
3236
3237 sub all_rec_holds {
3238     my( $self, $conn, $auth, $title_id, $args ) = @_;
3239
3240     my $e = new_editor(authtoken=>$auth);
3241     $e->checkauth or return $e->event;
3242     $e->allowed('VIEW_HOLD') or return $e->event;
3243
3244     $args ||= {};
3245     $args->{fulfillment_time} = undef; #  we don't want to see old fulfilled holds
3246     $args->{cancel_time} = undef;
3247
3248     my $resp = {
3249           metarecord_holds => []
3250         , title_holds      => []
3251         , volume_holds     => []
3252         , copy_holds       => []
3253         , recall_holds     => []
3254         , force_holds      => []
3255         , part_holds       => []
3256         , issuance_holds   => []
3257     };
3258
3259     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3260     if($mr_map) {
3261         $resp->{metarecord_holds} = $e->search_action_hold_request(
3262             {   hold_type => OILS_HOLD_TYPE_METARECORD,
3263                 target => $mr_map->metarecord,
3264                 %$args
3265             }, {idlist => 1}
3266         );
3267     }
3268
3269     $resp->{title_holds} = $e->search_action_hold_request(
3270         {
3271             hold_type => OILS_HOLD_TYPE_TITLE,
3272             target => $title_id,
3273             %$args
3274         }, {idlist=>1} );
3275
3276     my $parts = $e->search_biblio_monograph_part(
3277         {
3278             record => $title_id
3279         }, {idlist=>1} );
3280
3281     if (@$parts) {
3282         $resp->{part_holds} = $e->search_action_hold_request(
3283             {
3284                 hold_type => OILS_HOLD_TYPE_MONOPART,
3285                 target => $parts,
3286                 %$args
3287             }, {idlist=>1} );
3288     }
3289
3290     my $subs = $e->search_serial_subscription(
3291         { record_entry => $title_id }, {idlist=>1});
3292
3293     if (@$subs) {
3294         my $issuances = $e->search_serial_issuance(
3295             {subscription => $subs}, {idlist=>1}
3296         );
3297
3298         if (@$issuances) {
3299             $resp->{issuance_holds} = $e->search_action_hold_request(
3300                 {
3301                     hold_type => OILS_HOLD_TYPE_ISSUANCE,
3302                     target => $issuances,
3303                     %$args
3304                 }, {idlist=>1}
3305             );
3306         }
3307     }
3308
3309     my $vols = $e->search_asset_call_number(
3310         { record => $title_id, deleted => 'f' }, {idlist=>1});
3311
3312     return $resp unless @$vols;
3313
3314     $resp->{volume_holds} = $e->search_action_hold_request(
3315         {
3316             hold_type => OILS_HOLD_TYPE_VOLUME,
3317             target => $vols,
3318             %$args },
3319         {idlist=>1} );
3320
3321     my $copies = $e->search_asset_copy(
3322         { call_number => $vols, deleted => 'f' }, {idlist=>1});
3323
3324     return $resp unless @$copies;
3325
3326     $resp->{copy_holds} = $e->search_action_hold_request(
3327         {
3328             hold_type => OILS_HOLD_TYPE_COPY,
3329             target => $copies,
3330             %$args },
3331         {idlist=>1} );
3332
3333     $resp->{recall_holds} = $e->search_action_hold_request(
3334         {
3335             hold_type => OILS_HOLD_TYPE_RECALL,
3336             target => $copies,
3337             %$args },
3338         {idlist=>1} );
3339
3340     $resp->{force_holds} = $e->search_action_hold_request(
3341         {
3342             hold_type => OILS_HOLD_TYPE_FORCE,
3343             target => $copies,
3344             %$args },
3345         {idlist=>1} );
3346
3347     return $resp;
3348 }
3349
3350 __PACKAGE__->register_method(
3351     method           => 'stream_wide_holds',
3352     authoritative    => 1,
3353     stream           => 1,
3354     api_name         => 'open-ils.circ.hold.wide_hash.stream'
3355 );
3356
3357 sub stream_wide_holds {
3358     my($self, $client, $auth, $restrictions, $order_by, $limit, $offset) = @_;
3359
3360     my $e = new_editor(authtoken=>$auth);
3361     $e->checkauth or return $e->event;
3362     $e->allowed('VIEW_HOLD') or return $e->event;
3363
3364     my $st = OpenSRF::AppSession->create('open-ils.storage');
3365     my $req = $st->request(
3366         'open-ils.storage.action.live_holds.wide_hash',
3367         $restrictions, $order_by, $limit, $offset
3368     );
3369
3370     my $count = $req->recv;
3371     if(!$count) {
3372         return 0;
3373     }
3374
3375     if(UNIVERSAL::isa($count,"Error")) {
3376         throw $count ($count->stringify);
3377     }
3378
3379     $count = $count->content;
3380
3381     # Force immediate send of count response
3382     my $mbc = $client->max_bundle_count;
3383     $client->max_bundle_count(1);
3384     $client->respond($count);
3385     $client->max_bundle_count($mbc);
3386
3387     while (my $hold = $req->recv) {
3388         $client->respond($hold->content) if $hold->content;
3389     }
3390
3391     $client->respond_complete;
3392 }
3393
3394
3395
3396
3397 __PACKAGE__->register_method(
3398     method        => 'uber_hold',
3399     authoritative => 1,
3400     api_name      => 'open-ils.circ.hold.details.retrieve'
3401 );
3402
3403 sub uber_hold {
3404     my($self, $client, $auth, $hold_id, $args) = @_;
3405     my $e = new_editor(authtoken=>$auth);
3406     $e->checkauth or return $e->event;
3407     return uber_hold_impl($e, $hold_id, $args);
3408 }
3409
3410 __PACKAGE__->register_method(
3411     method        => 'batch_uber_hold',
3412     authoritative => 1,
3413     stream        => 1,
3414     api_name      => 'open-ils.circ.hold.details.batch.retrieve'
3415 );
3416
3417 sub batch_uber_hold {
3418     my($self, $client, $auth, $hold_ids, $args) = @_;
3419     my $e = new_editor(authtoken=>$auth);
3420     $e->checkauth or return $e->event;
3421     $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3422     return undef;
3423 }
3424
3425 sub uber_hold_impl {
3426     my($e, $hold_id, $args) = @_;
3427     $args ||= {};
3428
3429     my $flesh_fields = ['current_copy', 'usr', 'notes'];
3430     push (@$flesh_fields, 'requestor') if $args->{include_requestor};
3431     push (@$flesh_fields, 'cancel_cause') if $args->{include_cancel_cause};
3432
3433     my $hold = $e->retrieve_action_hold_request([
3434         $hold_id,
3435         {flesh => 1, flesh_fields => {ahr => $flesh_fields}}
3436     ]) or return $e->event;
3437
3438     if($hold->usr->id ne $e->requestor->id) {
3439         # caller is asking for someone else's hold
3440         $e->allowed('VIEW_HOLD') or return $e->event;
3441         $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3442             [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3443
3444     } else {
3445         # caller is asking for own hold, but may not have permission to view staff notes
3446         unless($e->allowed('VIEW_HOLD')) {
3447             $hold->notes( # filter out any staff notes (unless marked as public)
3448                 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3449         }
3450     }
3451
3452     my $user = $hold->usr;
3453     $hold->usr($user->id);
3454
3455
3456     my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args);
3457
3458     flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3459     flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3460
3461     my $details = retrieve_hold_queue_status_impl($e, $hold);
3462     $hold->usr($user) if $args->{include_usr}; # re-flesh
3463
3464     my $resp = {
3465         hold    => $hold,
3466         bre_id  => $bre->id,
3467         ($copy     ? (copy           => $copy)     : ()),
3468         ($volume   ? (volume         => $volume)   : ()),
3469         ($issuance ? (issuance       => $issuance) : ()),
3470         ($part     ? (part           => $part)     : ()),
3471         ($args->{include_bre}  ?  (bre => $bre)    : ()),
3472         ($args->{suppress_mvr} ?  () : (mvr => $mvr)),
3473         %$details
3474     };
3475
3476     $resp->{copy}->location(
3477         $e->retrieve_asset_copy_location($resp->{copy}->location))
3478         if $resp->{copy} and $args->{flesh_acpl};
3479
3480     unless($args->{suppress_patron_details}) {
3481         my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3482         $resp->{patron_first}   = $user->first_given_name,
3483         $resp->{patron_last}    = $user->family_name,
3484         $resp->{patron_barcode} = $card->barcode,
3485         $resp->{patron_alias}   = $user->alias,
3486     };
3487
3488     return $resp;
3489 }
3490
3491
3492
3493 # -----------------------------------------------------
3494 # Returns the MVR object that represents what the
3495 # hold is all about
3496 # -----------------------------------------------------
3497 sub find_hold_mvr {
3498     my( $e, $hold, $args ) = @_;
3499
3500     my $tid;
3501     my $copy;
3502     my $volume;
3503     my $issuance;
3504     my $part;
3505     my $no_mvr = $args->{suppress_mvr};
3506
3507     if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3508         my $mr = $e->retrieve_metabib_metarecord($hold->target)
3509             or return $e->event;
3510         $tid = $mr->master_record;
3511
3512     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3513         $tid = $hold->target;
3514
3515     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3516         $volume = $e->retrieve_asset_call_number($hold->target)
3517             or return $e->event;
3518         $tid = $volume->record;
3519
3520     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3521         $issuance = $e->retrieve_serial_issuance([
3522             $hold->target,
3523             {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3524         ]) or return $e->event;
3525
3526         $tid = $issuance->subscription->record_entry;
3527
3528     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3529         $part = $e->retrieve_biblio_monograph_part([
3530             $hold->target
3531         ]) or return $e->event;
3532
3533         $tid = $part->record;
3534
3535     } 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 ) {
3536         $copy = $e->retrieve_asset_copy([
3537             $hold->target,
3538             {flesh => 1, flesh_fields => {acp => ['call_number']}}
3539         ]) or return $e->event;
3540
3541         $volume = $copy->call_number;
3542         $tid = $volume->record;
3543     }
3544
3545     if(!$copy and ref $hold->current_copy ) {
3546         $copy = $hold->current_copy;
3547         $hold->current_copy($copy->id) unless $args->{include_current_copy};
3548     }
3549
3550     if(!$volume and $copy) {
3551         $volume = $e->retrieve_asset_call_number($copy->call_number);
3552     }
3553
3554     # TODO return metarcord mvr for M holds
3555     my $title = $e->retrieve_biblio_record_entry($tid);
3556     return ( ($no_mvr) ? undef : $U->record_to_mvr($title), $volume, $copy, $issuance, $part, $title );
3557 }
3558
3559 __PACKAGE__->register_method(
3560     method    => 'clear_shelf_cache',
3561     api_name  => 'open-ils.circ.hold.clear_shelf.get_cache',
3562     stream    => 1,
3563     signature => {
3564         desc => q/
3565             Returns the holds processed with the given cache key
3566         /
3567     }
3568 );
3569
3570 sub clear_shelf_cache {
3571     my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3572     my $e = new_editor(authtoken => $auth, xact => 1);
3573     return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3574
3575     $chunk_size ||= 25;
3576     $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3577
3578     my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3579
3580     if (!$hold_data) {
3581         $logger->info("no hold data found in cache"); # XXX TODO return event
3582         $e->rollback;
3583         return undef;
3584     }
3585
3586     my $maximum = 0;
3587     foreach (keys %$hold_data) {
3588         $maximum += scalar(@{ $hold_data->{$_} });
3589     }
3590     $client->respond({"maximum" => $maximum, "progress" => 0});
3591
3592     for my $action (sort keys %$hold_data) {
3593         while (@{$hold_data->{$action}}) {
3594             my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3595
3596             my $result_chunk = $e->json_query({
3597                 "select" => {
3598                     "acp" => ["barcode"],
3599                     "au" => [qw/
3600                         first_given_name second_given_name family_name alias
3601                     /],
3602                     "acn" => ["label"],
3603                     "acnp" => [{column => "label", alias => "prefix"}],
3604                     "acns" => [{column => "label", alias => "suffix"}],
3605                     "bre" => ["marc"],
3606                     "acpl" => ["name"],
3607                     "ahr" => ["id"]
3608                 },
3609                 "from" => {
3610                     "ahr" => {
3611                         "acp" => {
3612                             "field" => "id", "fkey" => "current_copy",
3613                             "join" => {
3614                                 "acn" => {
3615                                     "field" => "id", "fkey" => "call_number",
3616                                     "join" => {
3617                                         "bre" => {
3618                                             "field" => "id", "fkey" => "record"
3619                                         },
3620                                         "acnp" => {
3621                                             "field" => "id", "fkey" => "prefix"
3622                                         },
3623                                         "acns" => {
3624                                             "field" => "id", "fkey" => "suffix"
3625                                         }
3626                                     }
3627                                 },
3628                                 "acpl" => {"field" => "id", "fkey" => "location"}
3629                             }
3630                         },
3631                         "au" => {"field" => "id", "fkey" => "usr"}
3632                     }
3633                 },
3634                 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3635             }, {"substream" => 1}) or return $e->die_event;
3636
3637             $client->respond([
3638                 map {
3639                     +{"action" => $action, "hold_details" => $_}
3640                 } @$result_chunk
3641             ]);
3642         }
3643     }
3644
3645     $e->rollback;
3646     return undef;
3647 }
3648
3649
3650 __PACKAGE__->register_method(
3651     method    => 'clear_shelf_process',
3652     stream    => 1,
3653     api_name  => 'open-ils.circ.hold.clear_shelf.process',
3654     signature => {
3655         desc => q/
3656             1. Find all holds that have expired on the holds shelf
3657             2. Cancel the holds
3658             3. If a clear-shelf status is configured, put targeted copies into this status
3659             4. Divide copies into 3 groups: items to transit, items to reshelve, and items
3660                 that are needed for holds.  No subsequent action is taken on the holds
3661                 or items after grouping.
3662         /
3663     }
3664 );
3665
3666 sub clear_shelf_process {
3667     my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
3668
3669     my $e = new_editor(authtoken=>$auth);
3670     $e->checkauth or return $e->die_event;
3671     my $cache = OpenSRF::Utils::Cache->new('global');
3672
3673     $org_id ||= $e->requestor->ws_ou;
3674     $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
3675
3676     my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
3677
3678     my @hold_ids = $self->method_lookup(
3679         "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
3680     )->run($auth, $org_id, $match_copy);
3681
3682     $e->xact_begin;
3683
3684     my @holds;
3685     my @canceled_holds; # newly canceled holds
3686     $chunk_size ||= 25; # chunked status updates
3687     $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3688
3689     my $counter = 0;
3690     for my $hold_id (@hold_ids) {
3691
3692         $logger->info("Clear shelf processing hold $hold_id");
3693
3694         my $hold = $e->retrieve_action_hold_request([
3695             $hold_id, {
3696                 flesh => 1,
3697                 flesh_fields => {ahr => ['current_copy']}
3698             }
3699         ]);
3700
3701         if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
3702             $hold->cancel_time('now');
3703             $hold->cancel_cause(2); # Hold Shelf expiration
3704             $e->update_action_hold_request($hold) or return $e->die_event;
3705             push(@canceled_holds, $hold_id);
3706         }
3707
3708         my $copy = $hold->current_copy;
3709
3710         if($copy_status or $copy_status == 0) {
3711             # if a clear-shelf copy status is defined, update the copy
3712             $copy->status($copy_status);
3713             $copy->edit_date('now');
3714             $copy->editor($e->requestor->id);
3715             $e->update_asset_copy($copy) or return $e->die_event;
3716         }
3717
3718         push(@holds, $hold);
3719         $client->respond({maximum => scalar(@holds), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
3720     }
3721
3722     if ($e->commit) {
3723
3724         my %cache_data = (
3725             hold => [],
3726             transit => [],
3727             shelf => [],
3728             pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
3729         );
3730
3731         for my $hold (@holds) {
3732
3733             my $copy = $hold->current_copy;
3734             my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
3735
3736             if($alt_hold and !$match_copy) {
3737
3738                 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
3739
3740             } elsif($copy->circ_lib != $e->requestor->ws_ou) {
3741
3742                 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
3743
3744             } else {
3745
3746                 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
3747             }
3748         }
3749
3750         my $cache_key = md5_hex(time . $$ . rand());
3751         $logger->info("clear_shelf_cache: storing under $cache_key");
3752         $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours.  configurable?
3753
3754         # tell the client we're done
3755         $client->respond_complete({cache_key => $cache_key});
3756
3757         # ------------
3758         # fire off the hold cancelation trigger and wait for response so don't flood the service
3759
3760         # refetch the holds to pick up the caclulated cancel_time,
3761         # which may be needed by Action/Trigger
3762         $e->xact_begin;
3763         my $updated_holds = [];
3764         $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
3765         $e->rollback;
3766
3767         $U->create_events_for_hook(
3768             'hold_request.cancel.expire_holds_shelf',
3769             $_, $org_id, undef, undef, 1) for @$updated_holds;
3770
3771     } else {
3772         # tell the client we're done
3773         $client->respond_complete;
3774     }
3775 }
3776
3777 # returns IDs for holds that are on the holds shelf but 
3778 # have had their pickup_libs change while on the shelf.
3779 sub pickup_lib_changed_on_shelf_holds {
3780     my $e = shift;
3781     my $org_id = shift;
3782     my $ignore_holds = shift;
3783     $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
3784
3785     my $query = {
3786         select => { alhr => ['id'] },
3787         from   => {
3788             alhr => {
3789                 acp => {
3790                     field => 'id',
3791                     fkey  => 'current_copy'
3792                 },
3793             }
3794         },
3795         where => {
3796             '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
3797             '+alhr' => {
3798                 capture_time     => { "!=" => undef },
3799                 fulfillment_time => undef,
3800                 current_shelf_lib => $org_id,
3801                 pickup_lib => {'!='  => {'+alhr' => 'current_shelf_lib'}}
3802             }
3803         }
3804     };
3805
3806     $query->{where}->{'+alhr'}->{id} =
3807         {'not in' => $ignore_holds} if @$ignore_holds;
3808
3809     my $hold_ids = $e->json_query($query);
3810     return [ map { $_->{id} } @$hold_ids ];
3811 }
3812
3813 __PACKAGE__->register_method(
3814     method    => 'usr_hold_summary',
3815     api_name  => 'open-ils.circ.holds.user_summary',
3816     signature => q/
3817         Returns a summary of holds statuses for a given user
3818     /
3819 );
3820
3821 sub usr_hold_summary {
3822     my($self, $conn, $auth, $user_id) = @_;
3823
3824     my $e = new_editor(authtoken=>$auth);
3825     $e->checkauth or return $e->event;
3826     $e->allowed('VIEW_HOLD') or return $e->event;
3827
3828     my $holds = $e->search_action_hold_request(
3829         {
3830             usr =>  $user_id ,
3831             fulfillment_time => undef,
3832             cancel_time      => undef,
3833         }
3834     );
3835
3836     my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
3837     $summary{_hold_status($e, $_)} += 1 for @$holds;
3838     return \%summary;
3839 }
3840
3841
3842
3843 __PACKAGE__->register_method(
3844     method    => 'hold_has_copy_at',
3845     api_name  => 'open-ils.circ.hold.has_copy_at',
3846     signature => {
3847         desc   =>
3848                 'Returns the ID of the found copy and name of the shelving location if there is ' .
3849                 'an available copy at the specified org unit.  Returns empty hash otherwise.  '   .
3850                 'The anticipated use for this method is to determine whether an item is '         .
3851                 'available at the library where the user is placing the hold (or, alternatively, '.
3852                 'at the pickup library) to encourage bypassing the hold placement and just '      .
3853                 'checking out the item.' ,
3854         params => [
3855             { desc => 'Authentication Token', type => 'string' },
3856             { desc => 'Method Arguments.  Options include: hold_type, hold_target, org_unit.  '
3857                     . 'hold_type is the hold type code (T, V, C, M, ...).  '
3858                     . 'hold_target is the identifier of the hold target object.  '
3859                     . 'org_unit is org unit ID.',
3860               type => 'object'
3861             }
3862         ],
3863         return => {
3864             desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
3865             type => 'object'
3866         }
3867     }
3868 );
3869
3870 sub hold_has_copy_at {
3871     my($self, $conn, $auth, $args) = @_;
3872
3873     my $e = new_editor(authtoken=>$auth);
3874     $e->checkauth or return $e->event;
3875
3876     my $hold_type   = $$args{hold_type};
3877     my $hold_target = $$args{hold_target};
3878     my $org_unit    = $$args{org_unit};
3879
3880     my $query = {
3881         select => {acp => ['id'], acpl => ['name']},
3882         from   => {
3883             acp => {
3884                 acpl => {
3885                     field => 'id',
3886                     filter => { holdable => 't', deleted => 'f' },
3887                     fkey => 'location'
3888                 },
3889                 ccs  => {field => 'id', filter => { holdable => 't'}, fkey => 'status'  }
3890             }
3891         },
3892         where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
3893         limit => 1
3894     };
3895
3896     if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
3897
3898         $query->{where}->{'+acp'}->{id} = $hold_target;
3899
3900     } elsif($hold_type eq 'V') {
3901
3902         $query->{where}->{'+acp'}->{call_number} = $hold_target;
3903
3904     } elsif($hold_type eq 'P') {
3905
3906         $query->{from}->{acp}->{acpm} = {
3907             field  => 'target_copy',
3908             fkey   => 'id',
3909             filter => {part => $hold_target},
3910         };
3911
3912     } elsif($hold_type eq 'I') {
3913
3914         $query->{from}->{acp}->{sitem} = {
3915             field  => 'unit',
3916             fkey   => 'id',
3917             filter => {issuance => $hold_target},
3918         };
3919
3920     } elsif($hold_type eq 'T') {
3921
3922         $query->{from}->{acp}->{acn} = {
3923             field  => 'id',
3924             fkey   => 'call_number',
3925             'join' => {
3926                 bre => {
3927                     field  => 'id',
3928                     filter => {id => $hold_target},
3929                     fkey   => 'record'
3930                 }
3931             }
3932         };
3933
3934     } else {
3935
3936         $query->{from}->{acp}->{acn} = {
3937             field => 'id',
3938             fkey  => 'call_number',
3939             join  => {
3940                 bre => {
3941                     field => 'id',
3942                     fkey  => 'record',
3943                     join  => {
3944                         mmrsm => {
3945                             field  => 'source',
3946                             fkey   => 'id',
3947                             filter => {metarecord => $hold_target},
3948                         }
3949                     }
3950                 }
3951             }
3952         };
3953     }
3954
3955     my $res = $e->json_query($query)->[0] or return {};
3956     return {copy => $res->{id}, location => $res->{name}} if $res;
3957 }
3958
3959
3960 # returns true if the user already has an item checked out
3961 # that could be used to fulfill the requested hold.
3962 sub hold_item_is_checked_out {
3963     my($e, $user_id, $hold_type, $hold_target) = @_;
3964
3965     my $query = {
3966         select => {acp => ['id']},
3967         from   => {acp => {}},
3968         where  => {
3969             '+acp' => {
3970                 id => {
3971                     in => { # copies for circs the user has checked out
3972                         select => {circ => ['target_copy']},
3973                         from   => 'circ',
3974                         where  => {
3975                             usr => $user_id,
3976                             checkin_time => undef,
3977                             '-or' => [
3978                                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
3979                                 {stop_fines => undef}
3980                             ],
3981                         }
3982                     }
3983                 }
3984             }
3985         },
3986         limit => 1
3987     };
3988
3989     if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
3990
3991         $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
3992
3993     } elsif($hold_type eq 'V') {
3994
3995         $query->{where}->{'+acp'}->{call_number} = $hold_target;
3996
3997      } elsif($hold_type eq 'P') {
3998
3999         $query->{from}->{acp}->{acpm} = {
4000             field  => 'target_copy',
4001             fkey   => 'id',
4002             filter => {part => $hold_target},
4003         };
4004
4005      } elsif($hold_type eq 'I') {
4006
4007         $query->{from}->{acp}->{sitem} = {
4008             field  => 'unit',
4009             fkey   => 'id',
4010             filter => {issuance => $hold_target},
4011         };
4012
4013     } elsif($hold_type eq 'T') {
4014
4015         $query->{from}->{acp}->{acn} = {
4016             field  => 'id',
4017             fkey   => 'call_number',
4018             'join' => {
4019                 bre => {
4020                     field  => 'id',
4021                     filter => {id => $hold_target},
4022                     fkey   => 'record'
4023                 }
4024             }
4025         };
4026
4027     } else {
4028
4029         $query->{from}->{acp}->{acn} = {
4030             field => 'id',
4031             fkey => 'call_number',
4032             join => {
4033                 bre => {
4034                     field => 'id',
4035                     fkey => 'record',
4036                     join => {
4037                         mmrsm => {
4038                             field => 'source',
4039                             fkey => 'id',
4040                             filter => {metarecord => $hold_target},
4041                         }
4042                     }
4043                 }
4044             }
4045         };
4046     }
4047
4048     return $e->json_query($query)->[0];
4049 }
4050
4051 __PACKAGE__->register_method(
4052     method    => 'change_hold_title',
4053     api_name  => 'open-ils.circ.hold.change_title',
4054     signature => {
4055         desc => q/
4056             Updates all title level holds targeting the specified bibs to point a new bib./,
4057         params => [
4058             { desc => 'Authentication Token', type => 'string' },
4059             { desc => 'New Target Bib Id',    type => 'number' },
4060             { desc => 'Old Target Bib Ids',   type => 'array'  },
4061         ],
4062         return => { desc => '1 on success' }
4063     }
4064 );
4065
4066 __PACKAGE__->register_method(
4067     method    => 'change_hold_title_for_specific_holds',
4068     api_name  => 'open-ils.circ.hold.change_title.specific_holds',
4069     signature => {
4070         desc => q/
4071             Updates specified holds to target new bib./,
4072         params => [
4073             { desc => 'Authentication Token', type => 'string' },
4074             { desc => 'New Target Bib Id',    type => 'number' },
4075             { desc => 'Holds Ids for holds to update',   type => 'array'  },
4076         ],
4077         return => { desc => '1 on success' }
4078     }
4079 );
4080
4081
4082 sub change_hold_title {
4083     my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4084
4085     my $e = new_editor(authtoken=>$auth, xact=>1);
4086     return $e->die_event unless $e->checkauth;
4087
4088     my $holds = $e->search_action_hold_request(
4089         [
4090             {
4091                 capture_time     => undef,
4092                 cancel_time      => undef,
4093                 fulfillment_time => undef,
4094                 hold_type        => 'T',
4095                 target           => $bib_ids
4096             },
4097             {
4098                 flesh        => 1,
4099                 flesh_fields => { ahr => ['usr'] }
4100             }
4101         ],
4102         { substream => 1 }
4103     );
4104
4105     for my $hold (@$holds) {
4106         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4107         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4108         $hold->target( $new_bib_id );
4109         $e->update_action_hold_request($hold) or return $e->die_event;
4110     }
4111
4112     $e->commit;
4113
4114     _reset_hold($self, $e->requestor, $_) for @$holds;
4115
4116     return 1;
4117 }
4118
4119 sub change_hold_title_for_specific_holds {
4120     my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4121
4122     my $e = new_editor(authtoken=>$auth, xact=>1);
4123     return $e->die_event unless $e->checkauth;
4124
4125     my $holds = $e->search_action_hold_request(
4126         [
4127             {
4128                 capture_time     => undef,
4129                 cancel_time      => undef,
4130                 fulfillment_time => undef,
4131                 hold_type        => 'T',
4132                 id               => $hold_ids
4133             },
4134             {
4135                 flesh        => 1,
4136                 flesh_fields => { ahr => ['usr'] }
4137             }
4138         ],
4139         { substream => 1 }
4140     );
4141
4142     for my $hold (@$holds) {
4143         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4144         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4145         $hold->target( $new_bib_id );
4146         $e->update_action_hold_request($hold) or return $e->die_event;
4147     }
4148
4149     $e->commit;
4150
4151     _reset_hold($self, $e->requestor, $_) for @$holds;
4152
4153     return 1;
4154 }
4155
4156 __PACKAGE__->register_method(
4157     method    => 'rec_hold_count',
4158     api_name  => 'open-ils.circ.bre.holds.count',
4159     signature => {
4160         desc => q/Returns the total number of holds that target the
4161             selected bib record or its associated copies and call_numbers/,
4162         params => [
4163             { desc => 'Bib ID', type => 'number' },
4164             { desc => q/Optional arguments.  Supported arguments include:
4165                 "pickup_lib_descendant" -> limit holds to those whose pickup
4166                 library is equal to or is a child of the provided org unit/,
4167                 type => 'object'
4168             }
4169         ],
4170         return => {desc => 'Hold count', type => 'number'}
4171     }
4172 );
4173
4174 __PACKAGE__->register_method(
4175     method    => 'rec_hold_count',
4176     api_name  => 'open-ils.circ.mmr.holds.count',
4177     signature => {
4178         desc => q/Returns the total number of holds that target the
4179             selected metarecord or its associated copies, call_numbers, and bib records/,
4180         params => [
4181             { desc => 'Metarecord ID', type => 'number' },
4182         ],
4183         return => {desc => 'Hold count', type => 'number'}
4184     }
4185 );
4186
4187 # XXX Need to add type I holds to these counts
4188 sub rec_hold_count {
4189     my($self, $conn, $target_id, $args) = @_;
4190     $args ||= {};
4191
4192     my $mmr_join = {
4193         mmrsm => {
4194             field => 'source',
4195             fkey => 'id',
4196             filter => {metarecord => $target_id}
4197         }
4198     };
4199
4200     my $bre_join = {
4201         bre => {
4202             field => 'id',
4203             filter => { id => $target_id },
4204             fkey => 'record'
4205         }
4206     };
4207
4208     if($self->api_name =~ /mmr/) {
4209         delete $bre_join->{bre}->{filter};
4210         $bre_join->{bre}->{join} = $mmr_join;
4211     }
4212
4213     my $cn_join = {
4214         acn => {
4215             field => 'id',
4216             fkey => 'call_number',
4217             join => $bre_join
4218         }
4219     };
4220
4221     my $query = {
4222         select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4223         from => 'ahr',
4224         where => {
4225             '+ahr' => {
4226                 cancel_time => undef,
4227                 fulfillment_time => undef,
4228                 '-or' => [
4229                     {
4230                         '-and' => {
4231                             hold_type => [qw/C F R/],
4232                             target => {
4233                                 in => {
4234                                     select => {acp => ['id']},
4235                                     from => { acp => $cn_join }
4236                                 }
4237                             }
4238                         }
4239                     },
4240                     {
4241                         '-and' => {
4242                             hold_type => 'V',
4243                             target => {
4244                                 in => {
4245                                     select => {acn => ['id']},
4246                                     from => {acn => $bre_join}
4247                                 }
4248                             }
4249                         }
4250                     },
4251                     {
4252                         '-and' => {
4253                             hold_type => 'P',
4254                             target => {
4255                                 in => {
4256                                     select => {bmp => ['id']},
4257                                     from => {bmp => $bre_join}
4258                                 }
4259                             }
4260                         }
4261                     },
4262                     {
4263                         '-and' => {
4264                             hold_type => 'T',
4265                             target => $target_id
4266                         }
4267                     }
4268                 ]
4269             }
4270         }
4271     };
4272
4273     if($self->api_name =~ /mmr/) {
4274         $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4275             '-and' => {
4276                 hold_type => 'T',
4277                 target => {
4278                     in => {
4279                         select => {bre => ['id']},
4280                         from => {bre => $mmr_join}
4281                     }
4282                 }
4283             }
4284         };
4285
4286         $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4287             '-and' => {
4288                 hold_type => 'M',
4289                 target => $target_id
4290             }
4291         };
4292     }
4293
4294
4295     if (my $pld = $args->{pickup_lib_descendant}) {
4296
4297         my $top_ou = new_editor()->search_actor_org_unit(
4298             {parent_ou => undef}
4299         )->[0]; # XXX Assumes single root node. Not alone in this...
4300
4301         $query->{where}->{'+ahr'}->{pickup_lib} = {
4302             in => {
4303                 select  => {aou => [{ 
4304                     column => 'id', 
4305                     transform => 'actor.org_unit_descendants', 
4306                     result_field => 'id' 
4307                 }]},
4308                 from    => 'aou',
4309                 where   => {id => $pld}
4310             }
4311         } if ($pld != $top_ou->id);
4312     }
4313
4314
4315     return new_editor()->json_query($query)->[0]->{count};
4316 }
4317
4318 # A helper function to calculate a hold's expiration time at a given
4319 # org_unit. Takes the org_unit as an argument and returns either the
4320 # hold expire time as an ISO8601 string or undef if there is no hold
4321 # expiration interval set for the subject ou.
4322 sub calculate_expire_time
4323 {
4324     my $ou = shift;
4325     my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4326     if($interval) {
4327         my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
4328         return $U->epoch2ISO8601($date->epoch);
4329     }
4330     return undef;
4331 }
4332
4333
4334 __PACKAGE__->register_method(
4335     method    => 'mr_hold_filter_attrs',
4336     api_name  => 'open-ils.circ.mmr.holds.filters',
4337     authoritative => 1,
4338     stream => 1,
4339     signature => {
4340         desc => q/
4341             Returns the set of available formats and languages for the
4342             constituent records of the provided metarcord.
4343             If an array of hold IDs is also provided, information about
4344             each is returned as well.  This information includes:
4345             1. a slightly easier to read version of holdable_formats
4346             2. attributes describing the set of format icons included
4347                in the set of desired, constituent records.
4348         /,
4349         params => [
4350             {desc => 'Metarecord ID', type => 'number'},
4351             {desc => 'Context Org ID', type => 'number'},
4352             {desc => 'Hold ID List', type => 'array'},
4353         ],
4354         return => {
4355             desc => q/
4356                 Stream of objects.  The first will have a 'metarecord' key
4357                 containing non-hold-specific metarecord information, subsequent
4358                 responses will contain a 'hold' key containing hold-specific
4359                 information
4360             /, 
4361             type => 'object'
4362         }
4363     }
4364 );
4365
4366 sub mr_hold_filter_attrs { 
4367     my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4368     my $e = new_editor();
4369
4370     # by default, return MR / hold attributes for all constituent
4371     # records with holdable copies.  If there is a hard boundary,
4372     # though, limit to records with copies within the boundary,
4373     # since anything outside the boundary can never be held.
4374     my $org_depth = 0;
4375     if ($org_id) {
4376         $org_depth = $U->ou_ancestor_setting_value(
4377             $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4378     }
4379
4380     # get all org-scoped records w/ holdable copies for this metarecord
4381     my ($bre_ids) = $self->method_lookup(
4382         'open-ils.circ.holds.metarecord.filtered_records')->run(
4383             $mr_id, undef, $org_id, $org_depth);
4384
4385     my $item_lang_attr = 'item_lang'; # configurable?
4386     my $format_attr = $e->retrieve_config_global_flag(
4387         'opac.metarecord.holds.format_attr')->value;
4388
4389     # helper sub for fetching ccvms for a batch of record IDs
4390     sub get_batch_ccvms {
4391         my ($e, $attr, $bre_ids) = @_;
4392         return [] unless $bre_ids and @$bre_ids;
4393         my $vals = $e->search_metabib_record_attr_flat({
4394             attr => $attr,
4395             id => $bre_ids
4396         });
4397         return [] unless @$vals;
4398         return $e->search_config_coded_value_map({
4399             ctype => $attr,
4400             code => [map {$_->value} @$vals]
4401         });
4402     }
4403
4404     my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4405     my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4406
4407     $client->respond({
4408         metarecord => {
4409             id => $mr_id,
4410             formats => $formats,
4411             langs => $langs
4412         }
4413     });
4414
4415     return unless $hold_ids;
4416     my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4417     $icon_attr = $icon_attr ? $icon_attr->value : '';
4418
4419     for my $hold_id (@$hold_ids) {
4420         my $hold = $e->retrieve_action_hold_request($hold_id) 
4421             or return $e->event;
4422
4423         next unless $hold->hold_type eq 'M';
4424
4425         my $resp = {
4426             hold => {
4427                 id => $hold_id,
4428                 formats => [],
4429                 langs => []
4430             }
4431         };
4432
4433         # collect the ccvm's for the selected formats / language
4434         # (i.e. the holdable formats) on the MR.
4435         # this assumes a two-key structure for format / language,
4436         # though no assumption is made about the keys themselves.
4437         my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4438         my $lang_vals = [];
4439         my $format_vals = [];
4440         for my $val (values %$hformats) {
4441             # val is either a single ccvm or an array of them
4442             $val = [$val] unless ref $val eq 'ARRAY';
4443             for my $node (@$val) {
4444                 push (@$lang_vals, $node->{_val})   
4445                     if $node->{_attr} eq $item_lang_attr; 
4446                 push (@$format_vals, $node->{_val})   
4447                     if $node->{_attr} eq $format_attr;
4448             }
4449         }
4450
4451         # fetch the ccvm's for consistency with the {metarecord} blob
4452         $resp->{hold}{formats} = $e->search_config_coded_value_map({
4453             ctype => $format_attr, code => $format_vals});
4454         $resp->{hold}{langs} = $e->search_config_coded_value_map({
4455             ctype => $item_lang_attr, code => $lang_vals});
4456
4457         # find all of the bib records within this metarcord whose 
4458         # format / language match the holdable formats on the hold
4459         my ($bre_ids) = $self->method_lookup(
4460             'open-ils.circ.holds.metarecord.filtered_records')->run(
4461                 $hold->target, $hold->holdable_formats, 
4462                 $hold->selection_ou, $hold->selection_depth);
4463
4464         # now find all of the 'icon' attributes for the records
4465         $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4466         $client->respond($resp);
4467     }
4468
4469     return;
4470 }
4471
4472 __PACKAGE__->register_method(
4473     method        => "copy_has_holds_count",
4474     api_name      => "open-ils.circ.copy.has_holds_count",
4475     authoritative => 1,
4476     signature     => {
4477         desc => q/
4478             Returns the number of holds a paticular copy has
4479         /,
4480         params => [
4481             { desc => 'Authentication Token', type => 'string'},
4482             { desc => 'Copy ID', type => 'number'}
4483         ],
4484         return => {
4485             desc => q/
4486                 Simple count value
4487             /,
4488             type => 'number'
4489         }
4490     }
4491 );
4492
4493 sub copy_has_holds_count {
4494     my( $self, $conn, $auth, $copyid ) = @_;
4495     my $e = new_editor(authtoken=>$auth);
4496     return $e->event unless $e->checkauth;
4497
4498     if( $copyid && $copyid > 0 ) {
4499         my $meth = 'retrieve_action_has_holds_count';
4500         my $data = $e->$meth($copyid);
4501         if($data){
4502                 return $data->count();
4503         }
4504     }
4505     return 0;
4506 }
4507
4508 1;