77868503f3f4b99b432e5ee9150953dcf7b2ae6a
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Circ / Holds.pm
1 # ---------------------------------------------------------------
2 # Copyright (C) 2005  Georgia Public Library Service
3 # Bill Erickson <highfalutin@gmail.com>
4
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 # ---------------------------------------------------------------
15
16
17 package OpenILS::Application::Circ::Holds;
18 use base qw/OpenILS::Application/;
19 use strict; use warnings;
20 use OpenILS::Application::AppUtils;
21 use DateTime;
22 use Data::Dumper;
23 use OpenSRF::EX qw(:try);
24 use OpenILS::Perm;
25 use OpenILS::Event;
26 use OpenSRF::Utils;
27 use OpenSRF::Utils::Logger qw(:logger);
28 use OpenILS::Utils::CStoreEditor q/:funcs/;
29 use OpenILS::Utils::PermitHold;
30 use OpenSRF::Utils::SettingsClient;
31 use OpenILS::Const qw/:const/;
32 use OpenILS::Application::Circ::Transit;
33 use OpenILS::Application::Actor::Friends;
34 use DateTime;
35 use DateTime::Format::ISO8601;
36 use OpenILS::Utils::DateTime qw/:datetime/;
37 use Digest::MD5 qw(md5_hex);
38 use OpenSRF::Utils::Cache;
39 use OpenSRF::Utils::JSON;
40 my $apputils = "OpenILS::Application::AppUtils";
41 my $U = $apputils;
42
43 __PACKAGE__->register_method(
44     method    => "test_and_create_hold_batch",
45     api_name  => "open-ils.circ.holds.test_and_create.batch",
46     stream => 1,
47     signature => {
48         desc => q/This is for batch creating a set of holds where every field is identical except for the targets./,
49         params => [
50             { desc => 'Authentication token', type => 'string' },
51             { desc => 'Hash of named parameters.  Same as for open-ils.circ.title_hold.is_possible, though the pertinent target field is automatically populated based on the hold_type and the specified list of targets.', type => 'object'},
52             { desc => 'Array of target ids', type => 'array' }
53         ],
54         return => {
55             desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
56         },
57     }
58 );
59
60 __PACKAGE__->register_method(
61     method    => "test_and_create_hold_batch",
62     api_name  => "open-ils.circ.holds.test_and_create.batch.override",
63     stream => 1,
64     signature => {
65         desc  => '@see open-ils.circ.holds.test_and_create.batch',
66     }
67 );
68
69
70 sub test_and_create_hold_batch {
71     my( $self, $conn, $auth, $params, $target_list, $oargs ) = @_;
72
73     my $override = 0;
74     if ($self->api_name =~ /override/) {
75         $override = 1;
76         $oargs = { all => 1 } unless defined $oargs;
77         $$params{oargs} = $oargs; # for is_possible checking.
78     }
79
80     my $e = new_editor(authtoken=>$auth);
81     return $e->die_event unless $e->checkauth;
82     $$params{'requestor'} = $e->requestor->id;
83
84     my $target_field;
85     if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
86     elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
87     elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
88     elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
89     elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
90     elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
91     elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
92     elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
93     else { return undef; }
94
95     my $formats_map = delete $$params{holdable_formats_map};
96
97     foreach (@$target_list) {
98         $$params{$target_field} = $_;
99
100         # copy the requested formats from the target->formats map
101         # into the top-level formats attr for each hold
102         $$params{holdable_formats} = $formats_map->{$_};
103
104         my $res;
105         ($res) = $self->method_lookup(
106             'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
107         if ($res->{'success'} == 1) {
108
109             $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
110
111             # Remove oargs from params so holds can be created.
112             if ($$params{oargs}) {
113                 delete $$params{oargs};
114             }
115
116             my $ahr = construct_hold_request_object($params);
117             my ($res2) = $self->method_lookup(
118                 $override
119                 ? 'open-ils.circ.holds.create.override'
120                 : 'open-ils.circ.holds.create'
121             )->run($auth, $ahr, $oargs);
122             $res2 = {
123                 'target' => $$params{$target_field},
124                 'result' => $res2
125             };
126             $conn->respond($res2);
127         } else {
128             $res = {
129                 'target' => $$params{$target_field},
130                 'result' => $res
131             };
132             $conn->respond($res);
133         }
134     }
135     return undef;
136 }
137
138 sub construct_hold_request_object {
139     my ($params) = @_;
140
141     my $ahr = Fieldmapper::action::hold_request->new;
142     $ahr->isnew('1');
143
144     foreach my $field (keys %{ $params }) {
145         if ($field eq 'depth') { $ahr->selection_depth($$params{$field}); }
146         elsif ($field eq 'patronid') {
147             $ahr->usr($$params{$field}); }
148         elsif ($field eq 'titleid') { $ahr->target($$params{$field}); }
149         elsif ($field eq 'copy_id') { $ahr->target($$params{$field}); }
150         elsif ($field eq 'issuanceid') { $ahr->target($$params{$field}); }
151         elsif ($field eq 'volume_id') { $ahr->target($$params{$field}); }
152         elsif ($field eq 'mrid') { $ahr->target($$params{$field}); }
153         elsif ($field eq 'partid') { $ahr->target($$params{$field}); }
154         else {
155             $ahr->$field($$params{$field});
156         }
157     }
158     return $ahr;
159 }
160
161 __PACKAGE__->register_method(
162     method    => "create_hold_batch",
163     api_name  => "open-ils.circ.holds.create.batch",
164     stream => 1,
165     signature => {
166         desc => q/@see open-ils.circ.holds.create.batch/,
167         params => [
168             { desc => 'Authentication token', type => 'string' },
169             { desc => 'Array of hold objects', type => 'array' }
170         ],
171         return => {
172             desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
173         },
174     }
175 );
176
177 __PACKAGE__->register_method(
178     method    => "create_hold_batch",
179     api_name  => "open-ils.circ.holds.create.override.batch",
180     stream => 1,
181     signature => {
182         desc  => '@see open-ils.circ.holds.create.batch',
183     }
184 );
185
186
187 sub create_hold_batch {
188     my( $self, $conn, $auth, $hold_list, $oargs ) = @_;
189     (my $method = $self->api_name) =~ s/\.batch//og;
190     foreach (@$hold_list) {
191         my ($res) = $self->method_lookup($method)->run($auth, $_, $oargs);
192         $conn->respond($res);
193     }
194     return undef;
195 }
196
197
198 __PACKAGE__->register_method(
199     method    => "create_hold",
200     api_name  => "open-ils.circ.holds.create",
201     signature => {
202         desc => "Create a new hold for an item.  From a permissions perspective, " .
203                 "the login session is used as the 'requestor' of the hold.  "      .
204                 "The hold recipient is determined by the 'usr' setting within the hold object. " .
205                 'First we verify the requestor has holds request permissions.  '         .
206                 'Then we verify that the recipient is allowed to make the given hold.  ' .
207                 'If not, we see if the requestor has "override" capabilities.  If not, ' .
208                 'a permission exception is returned.  If permissions allow, we cycle '   .
209                 'through the set of holds objects and create.  '                         .
210                 'If the recipient does not have permission to place multiple holds '     .
211                 'on a single title and said operation is attempted, a permission '       .
212                 'exception is returned',
213         params => [
214             { desc => 'Authentication token',               type => 'string' },
215             { desc => 'Hold object for hold to be created',
216                 type => 'object', class => 'ahr' }
217         ],
218         return => {
219             desc => 'New ahr ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
220         },
221     }
222 );
223
224 __PACKAGE__->register_method(
225     method    => "create_hold",
226     api_name  => "open-ils.circ.holds.create.override",
227     notes     => '@see open-ils.circ.holds.create',
228     signature => {
229         desc  => "If the recipient is not allowed to receive the requested hold, " .
230                  "call this method to attempt the override",
231         params => [
232             { desc => 'Authentication token',               type => 'string' },
233             {
234                 desc => 'Hold object for hold to be created',
235                 type => 'object', class => 'ahr'
236             }
237         ],
238         return => {
239             desc => 'New hold (ahr) ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
240         },
241     }
242 );
243
244 sub create_hold {
245     my( $self, $conn, $auth, $hold, $oargs ) = @_;
246     return -1 unless $hold;
247     my $e = new_editor(authtoken=>$auth, xact=>1);
248     return $e->die_event unless $e->checkauth;
249
250     my $override = 0;
251     if ($self->api_name =~ /override/) {
252         $override = 1;
253         $oargs = { all => 1 } unless defined $oargs;
254     }
255
256     my @events;
257
258     my $requestor = $e->requestor;
259     my $recipient = $requestor;
260
261     if( $requestor->id ne $hold->usr ) {
262         # Make sure the requestor is allowed to place holds for
263         # the recipient if they are not the same people
264         $recipient = $e->retrieve_actor_user($hold->usr)  or return $e->die_event;
265         $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->die_event;
266     }
267
268     # If the related org setting tells us to, block if patron privs have expired
269     my $expire_setting = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_BLOCK_HOLD_FOR_EXPIRED_PATRON);
270     if ($expire_setting) {
271         my $expire = DateTime::Format::ISO8601->new->parse_datetime(
272             clean_ISO8601($recipient->expire_date));
273
274         push( @events, OpenILS::Event->new(
275             'PATRON_ACCOUNT_EXPIRED',
276             "payload" => {"fail_part" => "actor.usr.privs_expired"}
277             )) if( CORE::time > $expire->epoch ) ;
278     }
279
280     # Now make sure the recipient is allowed to receive the specified hold
281     my $porg = $recipient->home_ou;
282     my $rid  = $e->requestor->id;
283     my $t    = $hold->hold_type;
284
285     # See if a duplicate hold already exists
286     my $sargs = {
287         usr              => $recipient->id,
288         hold_type        => $t,
289         fulfillment_time => undef,
290         target           => $hold->target,
291         cancel_time      => undef,
292     };
293
294     $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
295
296     my $existing = $e->search_action_hold_request($sargs);
297     if (@$existing) {
298         # See if the requestor has the CREATE_DUPLICATE_HOLDS perm.
299         my $can_dup = $e->allowed('CREATE_DUPLICATE_HOLDS', $recipient->home_ou);
300         # How many are allowed.
301         my $num_dups = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_MAX_DUPLICATE_HOLDS, $e) || 0;
302         push( @events, OpenILS::Event->new('HOLD_EXISTS'))
303             unless (($t eq 'T' || $t eq 'M') && $can_dup && scalar(@$existing) < $num_dups);
304         # Note: We check for @$existing < $num_dups because we're adding a hold with this call.
305     }
306
307     my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
308     push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
309
310     if ( $t eq OILS_HOLD_TYPE_METARECORD ) {
311         return $e->die_event unless $e->allowed('MR_HOLDS',     $porg);
312     } elsif ( $t eq OILS_HOLD_TYPE_TITLE ) {
313         return $e->die_event unless $e->allowed('TITLE_HOLDS',  $porg);
314     } elsif ( $t eq OILS_HOLD_TYPE_VOLUME ) {
315         return $e->die_event unless $e->allowed('VOLUME_HOLDS', $porg);
316     } elsif ( $t eq OILS_HOLD_TYPE_MONOPART ) {
317         return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
318     } elsif ( $t eq OILS_HOLD_TYPE_ISSUANCE ) {
319         return $e->die_event unless $e->allowed('ISSUANCE_HOLDS', $porg);
320     } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
321         return $e->die_event unless $e->allowed('COPY_HOLDS',   $porg);
322     } elsif ( $t eq OILS_HOLD_TYPE_FORCE || $t eq OILS_HOLD_TYPE_RECALL ) {
323         my $copy = $e->retrieve_asset_copy($hold->target)
324             or return $e->die_event;
325         if ( $t eq OILS_HOLD_TYPE_FORCE ) {
326             return $e->die_event unless $e->allowed('COPY_HOLDS_FORCE',   $copy->circ_lib);
327         } elsif ( $t eq OILS_HOLD_TYPE_RECALL ) {
328             return $e->die_event unless $e->allowed('COPY_HOLDS_RECALL',   $copy->circ_lib);
329         }
330     }
331
332     if( @events ) {
333         if (!$override) {
334             $e->rollback;
335             return \@events;
336         }
337         for my $evt (@events) {
338             next unless $evt;
339             my $name = $evt->{textcode};
340             if($oargs->{all} || grep { $_ eq $name } @{$oargs->{events}}) {
341                 return $e->die_event unless $e->allowed("$name.override", $porg);
342             } else {
343                 $e->rollback;
344                 return \@events;
345             }
346         }
347     }
348
349         # Check for hold expiration in the past, and set it to empty string.
350         $hold->expire_time(undef) if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1);
351
352     # set the configured expire time
353     unless($hold->expire_time || $U->is_true($hold->frozen)) {
354         $hold->expire_time(calculate_expire_time($recipient->home_ou));
355     }
356
357
358     # if behind-the-desk pickup is supported at the hold pickup lib,
359     # set the value to the patron default, unless a value has already
360     # been applied.  If it's not supported, force the value to false.
361
362     my $bdous = $U->ou_ancestor_setting_value(
363         $hold->pickup_lib, 
364         'circ.holds.behind_desk_pickup_supported', $e);
365
366     if ($bdous) {
367         if (!defined $hold->behind_desk) {
368
369             my $set = $e->search_actor_user_setting({
370                 usr => $hold->usr, 
371                 name => 'circ.holds_behind_desk'
372             })->[0];
373         
374             $hold->behind_desk('t') if $set and 
375                 OpenSRF::Utils::JSON->JSON2perl($set->value);
376         }
377     } else {
378         # behind the desk not supported, force it to false
379         $hold->behind_desk('f');
380     }
381
382     $hold->requestor($e->requestor->id);
383     $hold->request_lib($e->requestor->ws_ou);
384     $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
385     $hold = $e->create_action_hold_request($hold) or return $e->die_event;
386
387     $e->commit;
388
389     $conn->respond_complete($hold->id);
390
391     $U->simplereq('open-ils.hold-targeter',
392         'open-ils.hold-targeter.target', {hold => $hold->id}
393     ) unless $U->is_true($hold->frozen);
394
395     return undef;
396 }
397
398 # makes sure that a user has permission to place the type of requested hold
399 # returns the Perm exception if not allowed, returns undef if all is well
400 sub _check_holds_perm {
401     my($type, $user_id, $org_id) = @_;
402
403     my $evt;
404     if ($type eq "M") {
405         $evt = $apputils->check_perms($user_id, $org_id, "MR_HOLDS"    );
406     } elsif ($type eq "T") {
407         $evt = $apputils->check_perms($user_id, $org_id, "TITLE_HOLDS" );
408     } elsif($type eq "V") {
409         $evt = $apputils->check_perms($user_id, $org_id, "VOLUME_HOLDS");
410     } elsif($type eq "C") {
411         $evt = $apputils->check_perms($user_id, $org_id, "COPY_HOLDS"  );
412     }
413
414     return $evt if $evt;
415     return undef;
416 }
417
418 # tests if the given user is allowed to place holds on another's behalf
419 sub _check_request_holds_perm {
420     my $user_id = shift;
421     my $org_id  = shift;
422     if (my $evt = $apputils->check_perms(
423         $user_id, $org_id, "REQUEST_HOLDS")) {
424         return $evt;
425     }
426 }
427
428 my $ses_is_req_note = 'The login session is the requestor.  If the requestor is different from the user, ' .
429                       'then the requestor must have VIEW_HOLD permissions';
430
431 __PACKAGE__->register_method(
432     method    => "retrieve_holds_by_id",
433     api_name  => "open-ils.circ.holds.retrieve_by_id",
434     signature => {
435         desc   => "Retrieve the hold, with hold transits attached, for the specified ID.  $ses_is_req_note",
436         params => [
437             { desc => 'Authentication token', type => 'string' },
438             { desc => 'Hold ID',              type => 'number' }
439         ],
440         return => {
441             desc => 'Hold object with transits attached, event on error',
442         }
443     }
444 );
445
446
447 sub retrieve_holds_by_id {
448     my($self, $client, $auth, $hold_id) = @_;
449     my $e = new_editor(authtoken=>$auth);
450     $e->checkauth or return $e->event;
451     $e->allowed('VIEW_HOLD') or return $e->event;
452
453     my $holds = $e->search_action_hold_request(
454         [
455             { id =>  $hold_id , fulfillment_time => undef },
456             {
457                 order_by => { ahr => "request_time" },
458                 flesh => 1,
459                 flesh_fields => {ahr => ['notes']}
460             }
461         ]
462     );
463
464     flesh_hold_transits($holds);
465     flesh_hold_notices($holds, $e);
466     return $holds;
467 }
468
469
470 __PACKAGE__->register_method(
471     method    => "retrieve_holds",
472     api_name  => "open-ils.circ.holds.retrieve",
473     signature => {
474         desc   => "Retrieves all the holds, with hold transits attached, for the specified user.  $ses_is_req_note",
475         params => [
476             { desc => 'Authentication token', type => 'string'  },
477             { desc => 'User ID',              type => 'integer' },
478             { desc => 'Available Only',       type => 'boolean' }
479         ],
480         return => {
481             desc => 'list of holds, event on error',
482         }
483    }
484 );
485
486 __PACKAGE__->register_method(
487     method        => "retrieve_holds",
488     api_name      => "open-ils.circ.holds.id_list.retrieve",
489     authoritative => 1,
490     signature     => {
491         desc   => "Retrieves all the hold IDs, for the specified user.  $ses_is_req_note",
492         params => [
493             { desc => 'Authentication token', type => 'string'  },
494             { desc => 'User ID',              type => 'integer' },
495             { desc => 'Available Only',       type => 'boolean' }
496         ],
497         return => {
498             desc => 'list of holds, event on error',
499         }
500    }
501 );
502
503 __PACKAGE__->register_method(
504     method        => "retrieve_holds",
505     api_name      => "open-ils.circ.holds.canceled.retrieve",
506     authoritative => 1,
507     signature     => {
508         desc   => "Retrieves all the cancelled holds for the specified user.  $ses_is_req_note",
509         params => [
510             { desc => 'Authentication token', type => 'string'  },
511             { desc => 'User ID',              type => 'integer' }
512         ],
513         return => {
514             desc => 'list of holds, event on error',
515         }
516    }
517 );
518
519 __PACKAGE__->register_method(
520     method        => "retrieve_holds",
521     api_name      => "open-ils.circ.holds.canceled.id_list.retrieve",
522     authoritative => 1,
523     signature     => {
524         desc   => "Retrieves list of cancelled hold IDs for the specified user.  $ses_is_req_note",
525         params => [
526             { desc => 'Authentication token', type => 'string'  },
527             { desc => 'User ID',              type => 'integer' }
528         ],
529         return => {
530             desc => 'list of hold IDs, event on error',
531         }
532    }
533 );
534
535
536 sub retrieve_holds {
537     my ($self, $client, $auth, $user_id, $available) = @_;
538
539     my $e = new_editor(authtoken=>$auth);
540     return $e->event unless $e->checkauth;
541     $user_id = $e->requestor->id unless defined $user_id;
542
543     my $notes_filter = {staff => 'f'};
544     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
545     unless($user_id == $e->requestor->id) {
546         if($e->allowed('VIEW_HOLD', $user->home_ou)) {
547             $notes_filter = {staff => 't'}
548         } else {
549             my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
550                 $e, $user_id, $e->requestor->id, 'hold.view');
551             return $e->event unless $allowed;
552         }
553     } else {
554         # staff member looking at his/her own holds can see staff and non-staff notes
555         $notes_filter = {} if $e->allowed('VIEW_HOLD', $user->home_ou);
556     }
557
558     my $holds_query = {
559         select => {ahr => ['id']},
560         from => 'ahr',
561         where => {usr => $user_id, fulfillment_time => undef}
562     };
563
564     if($self->api_name =~ /canceled/) {
565
566         # Fetch the canceled holds
567         # order cancelled holds by cancel time, most recent first
568
569         $holds_query->{order_by} = [{class => 'ahr', field => 'cancel_time', direction => 'desc'}];
570
571         my $cancel_age;
572         my $cancel_count = $U->ou_ancestor_setting_value(
573                 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
574
575         unless($cancel_count) {
576             $cancel_age = $U->ou_ancestor_setting_value(
577                 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
578
579             # if no settings are defined, default to last 10 cancelled holds
580             $cancel_count = 10 unless $cancel_age;
581         }
582
583         if($cancel_count) { # limit by count
584
585             $holds_query->{where}->{cancel_time} = {'!=' => undef};
586             $holds_query->{limit} = $cancel_count;
587
588         } elsif($cancel_age) { # limit by age
589
590             # find all of the canceled holds that were canceled within the configured time frame
591             my $date = DateTime->now->subtract(seconds => OpenILS::Utils::DateTime->interval_to_seconds($cancel_age));
592             $date = $U->epoch2ISO8601($date->epoch);
593             $holds_query->{where}->{cancel_time} = {'>=' => $date};
594         }
595
596     } else {
597
598         # order non-cancelled holds by ready-for-pickup, then active, followed by suspended
599         # "compare" sorts false values to the front.  testing pickup_lib != current_shelf_lib
600         # will sort by pl = csl > pl != csl > followed by csl is null;
601         $holds_query->{order_by} = [
602             {   class => 'ahr',
603                 field => 'pickup_lib',
604                 compare => {'!='  => {'+ahr' => 'current_shelf_lib'}}},
605             {class => 'ahr', field => 'shelf_time'},
606             {class => 'ahr', field => 'frozen'},
607             {class => 'ahr', field => 'request_time'}
608
609         ];
610         $holds_query->{where}->{cancel_time} = undef;
611         if($available) {
612             $holds_query->{where}->{shelf_time} = {'!=' => undef};
613             # Maybe?
614             $holds_query->{where}->{pickup_lib} = {'=' => {'+ahr' => 'current_shelf_lib'}};
615         }
616     }
617
618     my $hold_ids = $e->json_query($holds_query);
619     $hold_ids = [ map { $_->{id} } @$hold_ids ];
620
621     return $hold_ids if $self->api_name =~ /id_list/;
622
623     my @holds;
624     for my $hold_id ( @$hold_ids ) {
625
626         my $hold = $e->retrieve_action_hold_request($hold_id);
627         $hold->notes($e->search_action_hold_request_note({hold => $hold_id, %$notes_filter}));
628
629         $hold->transit(
630             $e->search_action_hold_transit_copy([
631                 {hold => $hold->id, cancel_time => undef},
632                 {order_by => {ahtc => 'source_send_time desc'}, limit => 1}])->[0]
633         );
634
635         push(@holds, $hold);
636     }
637
638     return \@holds;
639 }
640
641
642 __PACKAGE__->register_method(
643     method   => 'user_hold_count',
644     api_name => 'open-ils.circ.hold.user.count'
645 );
646
647 sub user_hold_count {
648     my ( $self, $conn, $auth, $userid ) = @_;
649     my $e = new_editor( authtoken => $auth );
650     return $e->event unless $e->checkauth;
651     my $patron = $e->retrieve_actor_user($userid)
652         or return $e->event;
653     return $e->event unless $e->allowed( 'VIEW_HOLD', $patron->home_ou );
654     return __user_hold_count( $self, $e, $userid );
655 }
656
657 sub __user_hold_count {
658     my ( $self, $e, $userid ) = @_;
659     my $holds = $e->search_action_hold_request(
660         {
661             usr              => $userid,
662             fulfillment_time => undef,
663             cancel_time      => undef,
664         },
665         { idlist => 1 }
666     );
667
668     return scalar(@$holds);
669 }
670
671
672 __PACKAGE__->register_method(
673     method   => "retrieve_holds_by_pickup_lib",
674     api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
675     notes    =>
676       "Retrieves all the holds, with hold transits attached, for the specified pickup_ou id."
677 );
678
679 __PACKAGE__->register_method(
680     method   => "retrieve_holds_by_pickup_lib",
681     api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
682     notes    => "Retrieves all the hold ids for the specified pickup_ou id. "
683 );
684
685 sub retrieve_holds_by_pickup_lib {
686     my ($self, $client, $login_session, $ou_id) = @_;
687
688     #FIXME -- put an appropriate permission check here
689     #my( $user, $target, $evt ) = $apputils->checkses_requestor(
690     #    $login_session, $user_id, 'VIEW_HOLD' );
691     #return $evt if $evt;
692
693     my $holds = $apputils->simplereq(
694         'open-ils.cstore',
695         "open-ils.cstore.direct.action.hold_request.search.atomic",
696         {
697             pickup_lib =>  $ou_id ,
698             fulfillment_time => undef,
699             cancel_time => undef
700         },
701         { order_by => { ahr => "request_time" } }
702     );
703
704     if ( ! $self->api_name =~ /id_list/ ) {
705         flesh_hold_transits($holds);
706         return $holds;
707     }
708     # else id_list
709     return [ map { $_->id } @$holds ];
710 }
711
712
713 __PACKAGE__->register_method(
714     method   => "uncancel_hold",
715     api_name => "open-ils.circ.hold.uncancel"
716 );
717
718 sub uncancel_hold {
719     my($self, $client, $auth, $hold_id) = @_;
720     my $e = new_editor(authtoken=>$auth, xact=>1);
721     return $e->die_event unless $e->checkauth;
722
723     my $hold = $e->retrieve_action_hold_request($hold_id)
724         or return $e->die_event;
725     return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
726
727     if ($hold->fulfillment_time) {
728         $e->rollback;
729         return 0;
730     }
731     unless ($hold->cancel_time) {
732         $e->rollback;
733         return 1;
734     }
735
736     # if configured to reset the request time, also reset the expire time
737     if($U->ou_ancestor_setting_value(
738         $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
739
740         $hold->request_time('now');
741         $hold->expire_time(calculate_expire_time($hold->request_lib));
742     }
743
744     $hold->clear_cancel_time;
745     $hold->clear_cancel_cause;
746     $hold->clear_cancel_note;
747     $hold->clear_shelf_time;
748     $hold->clear_current_copy;
749     $hold->clear_capture_time;
750     $hold->clear_prev_check_time;
751     $hold->clear_shelf_expire_time;
752     $hold->clear_current_shelf_lib;
753
754     $e->update_action_hold_request($hold) or return $e->die_event;
755     $e->commit;
756
757     $U->simplereq('open-ils.hold-targeter',
758         'open-ils.hold-targeter.target', {hold => $hold_id});
759
760     return 1;
761 }
762
763
764 __PACKAGE__->register_method(
765     method    => "cancel_hold",
766     api_name  => "open-ils.circ.hold.cancel",
767     signature => {
768         desc   => 'Cancels the specified hold.  The login session is the requestor.  If the requestor is different from the usr field ' .
769                   'on the hold, the requestor must have CANCEL_HOLDS permissions. The hold may be either the hold object or the hold id',
770         param  => [
771             {desc => 'Authentication token',  type => 'string'},
772             {desc => 'Hold ID',               type => 'number'},
773             {desc => 'Cause of Cancellation', type => 'string'},
774             {desc => 'Note',                  type => 'string'}
775         ],
776         return => {
777             desc => '1 on success, event on error'
778         }
779     }
780 );
781
782 sub cancel_hold {
783     my($self, $client, $auth, $holdid, $cause, $note) = @_;
784
785     my $e = new_editor(authtoken=>$auth, xact=>1);
786     return $e->die_event unless $e->checkauth;
787
788     my $hold = $e->retrieve_action_hold_request($holdid)
789         or return $e->die_event;
790
791     if( $e->requestor->id ne $hold->usr ) {
792         return $e->die_event unless $e->allowed('CANCEL_HOLDS');
793     }
794
795     if ($hold->cancel_time) {
796         $e->rollback;
797         return 1;
798     }
799
800     # If the hold is captured, reset the copy status
801     if( $hold->capture_time and $hold->current_copy ) {
802
803         my $copy = $e->retrieve_asset_copy($hold->current_copy)
804             or return $e->die_event;
805
806         if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
807          $logger->info("canceling hold $holdid whose item is on the holds shelf");
808 #            $logger->info("setting copy to status 'reshelving' on hold cancel");
809 #            $copy->status(OILS_COPY_STATUS_RESHELVING);
810 #            $copy->editor($e->requestor->id);
811 #            $copy->edit_date('now');
812 #            $e->update_asset_copy($copy) or return $e->event;
813
814         } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
815
816             my $hid = $hold->id;
817             $logger->warn("! canceling hold [$hid] that is in transit");
818             my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
819
820             if( $transid ) {
821                 my $trans = $e->retrieve_action_transit_copy($transid);
822                 # Leave the transit alive, but  set the copy status to
823                 # reshelving so it will be properly reshelved when it gets back home
824                 if( $trans ) {
825                     $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
826                     $e->update_action_transit_copy($trans) or return $e->die_event;
827                 }
828             }
829         }
830     }
831
832     $hold->cancel_time('now');
833     $hold->cancel_cause($cause);
834     $hold->cancel_note($note);
835     $e->update_action_hold_request($hold)
836         or return $e->die_event;
837
838     $e->commit;
839
840     # re-fetch the hold to pick up the real cancel_time (not "now") for A/T
841     $e->xact_begin;
842     $hold = $e->retrieve_action_hold_request($hold->id) or return $e->die_event;
843     $e->rollback;
844
845     if ($e->requestor->id == $hold->usr) {
846         $U->create_events_for_hook('hold_request.cancel.patron', $hold, $hold->pickup_lib);
847     } else {
848         $U->create_events_for_hook('hold_request.cancel.staff', $hold, $hold->pickup_lib);
849     }
850
851     return 1;
852 }
853
854 my $update_hold_desc = 'The login session is the requestor. '       .
855    'If the requestor is different from the usr field on the hold, ' .
856    'the requestor must have UPDATE_HOLDS permissions. '             .
857    'If supplying a hash of hold data, "id" must be included. '      .
858    'The hash is ignored if a hold object is supplied, '             .
859    'so you should supply only one kind of hold data argument.'      ;
860
861 __PACKAGE__->register_method(
862     method    => "update_hold",
863     api_name  => "open-ils.circ.hold.update",
864     signature => {
865         desc   => "Updates the specified hold.  $update_hold_desc",
866         params => [
867             {desc => 'Authentication token',         type => 'string'},
868             {desc => 'Hold Object',                  type => 'object'},
869             {desc => 'Hash of values to be applied', type => 'object'}
870         ],
871         return => {
872             desc => 'Hold ID on success, event on error',
873             # type => 'number'
874         }
875     }
876 );
877
878 __PACKAGE__->register_method(
879     method    => "batch_update_hold",
880     api_name  => "open-ils.circ.hold.update.batch",
881     stream    => 1,
882     signature => {
883         desc   => "Updates the specified hold(s).  $update_hold_desc",
884         params => [
885             {desc => 'Authentication token',                    type => 'string'},
886             {desc => 'Array of hold obejcts',                   type => 'array' },
887             {desc => 'Array of hashes of values to be applied', type => 'array' }
888         ],
889         return => {
890             desc => 'Hold ID per success, event per error',
891         }
892     }
893 );
894
895 sub update_hold {
896     my($self, $client, $auth, $hold, $values) = @_;
897     my $e = new_editor(authtoken=>$auth, xact=>1);
898     return $e->die_event unless $e->checkauth;
899     my $resp = update_hold_impl($self, $e, $hold, $values);
900     if ($U->event_code($resp)) {
901         $e->rollback;
902         return $resp;
903     }
904     $e->commit;     # FIXME: update_hold_impl already does $e->commit  ??
905     return $resp;
906 }
907
908 sub batch_update_hold {
909     my($self, $client, $auth, $hold_list, $values_list) = @_;
910     my $e = new_editor(authtoken=>$auth);
911     return $e->die_event unless $e->checkauth;
912
913     my $count = ($hold_list) ? scalar(@$hold_list) : scalar(@$values_list);     # FIXME: we don't know for sure that we got $values_list.  we could have neither list.
914     $hold_list   ||= [];
915     $values_list ||= [];      # FIXME: either move this above $count declaration, or send an event if both lists undef.  Probably the latter.
916
917 # FIXME: Failing over to [] guarantees warnings for "Use of unitialized value" in update_hold_impl call.
918 # FIXME: We should be sure we only call update_hold_impl with hold object OR hash, not both.
919
920     for my $idx (0..$count-1) {
921         $e->xact_begin;
922         my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
923         $e->xact_commit unless $U->event_code($resp);
924         $client->respond($resp);
925     }
926
927     $e->disconnect;
928     return undef;       # not in the register return type, assuming we should always have at least one list populated
929 }
930
931 sub update_hold_impl {
932     my($self, $e, $hold, $values) = @_;
933     my $hold_status;
934     my $need_retarget = 0;
935
936     unless($hold) {
937         $hold = $e->retrieve_action_hold_request($values->{id})
938             or return $e->die_event;
939         for my $k (keys %$values) {
940             # Outside of pickup_lib (covered by the first regex) I don't know when these would currently change.
941             # But hey, why not cover things that may happen later?
942             if ($k =~ '_(lib|ou)$' || $k eq 'target' || $k eq 'hold_type' || $k eq 'requestor' || $k eq 'selection_depth' || $k eq 'holdable_formats') {
943                 if (defined $values->{$k} && defined $hold->$k() && $values->{$k} ne $hold->$k()) {
944                     # Value changed? RETARGET!
945                     $need_retarget = 1;
946                 } elsif (defined $hold->$k() != defined $values->{$k}) {
947                     # Value being set or cleared? RETARGET!
948                     $need_retarget = 1;
949                 }
950             }
951             if (defined $values->{$k}) {
952                 $hold->$k($values->{$k});
953             } else {
954                 my $f = "clear_$k"; $hold->$f();
955             }
956         }
957     }
958
959     my $orig_hold = $e->retrieve_action_hold_request($hold->id)
960         or return $e->die_event;
961
962     # don't allow the user to be changed
963     return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
964
965     if($hold->usr ne $e->requestor->id) {
966         # if the hold is for a different user, make sure the
967         # requestor has the appropriate permissions
968         my $usr = $e->retrieve_actor_user($hold->usr)
969             or return $e->die_event;
970         return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
971     }
972
973
974     # --------------------------------------------------------------
975     # Changing the request time is like playing God
976     # --------------------------------------------------------------
977     if($hold->request_time ne $orig_hold->request_time) {
978         return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
979         return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
980     }
981
982
983     # --------------------------------------------------------------
984     # Code for making sure staff have appropriate permissons for cut_in_line
985     # This, as is, doesn't prevent a user from cutting their own holds in line
986     # but needs to
987     # --------------------------------------------------------------
988     if($U->is_true($hold->cut_in_line) ne $U->is_true($orig_hold->cut_in_line)) {
989         return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
990     }
991
992
993     # --------------------------------------------------------------
994     # Disallow hold suspencion if the hold is already captured.
995     # --------------------------------------------------------------
996     if ($U->is_true($hold->frozen) and not $U->is_true($orig_hold->frozen)) {
997         $hold_status = _hold_status($e, $hold);
998         if ($hold_status > 2 && $hold_status != 7) { # hold is captured
999             $logger->info("bypassing hold freeze on captured hold");
1000             return OpenILS::Event->new('HOLD_SUSPEND_AFTER_CAPTURE');
1001         }
1002     }
1003
1004
1005     # --------------------------------------------------------------
1006     # if the hold is on the holds shelf or in transit and the pickup
1007     # lib changes we need to create a new transit.
1008     # --------------------------------------------------------------
1009     if($orig_hold->pickup_lib ne $hold->pickup_lib) {
1010
1011         $hold_status = _hold_status($e, $hold) unless $hold_status;
1012
1013         if($hold_status == 3) { # in transit
1014
1015             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
1016             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
1017
1018             $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
1019
1020             # update the transit to reflect the new pickup location
1021             my $transit = $e->search_action_hold_transit_copy(
1022                 {hold=>$hold->id, cancel_time => undef, dest_recv_time => undef})->[0]
1023                 or return $e->die_event;
1024
1025             $transit->prev_dest($transit->dest); # mark the previous destination on the transit
1026             $transit->dest($hold->pickup_lib);
1027             $e->update_action_hold_transit_copy($transit) or return $e->die_event;
1028
1029         } elsif($hold_status == 4 or $hold_status == 5 or $hold_status == 8) { # on holds shelf
1030
1031             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
1032             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
1033
1034             $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
1035
1036             if ($hold->pickup_lib eq $orig_hold->current_shelf_lib) {
1037                 # This can happen if the pickup lib is changed while the hold is
1038                 # on the shelf, then changed back to the original pickup lib.
1039                 # Restore the original shelf_expire_time to prevent abuse.
1040                 set_hold_shelf_expire_time(undef, $hold, $e, $hold->shelf_time);
1041
1042             } else {
1043                 # clear to prevent premature shelf expiration
1044                 $hold->clear_shelf_expire_time;
1045             }
1046         }
1047     }
1048
1049     if($U->is_true($hold->frozen)) {
1050         $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1051         $hold->clear_current_copy;
1052         $hold->clear_prev_check_time;
1053         # Clear expire_time to prevent frozen holds from expiring.
1054         $logger->info("clearing expire_time for frozen hold ".$hold->id);
1055         $hold->clear_expire_time;
1056     }
1057
1058     # If the hold_expire_time is in the past && is not equal to the
1059     # original expire_time, then reset the expire time to be in the
1060     # future.
1061     if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1 && $U->datecmp($hold->expire_time, $orig_hold->expire_time) != 0) {
1062         $hold->expire_time(calculate_expire_time($hold->request_lib));
1063     }
1064
1065     # If the hold is reactivated, reset the expire_time.
1066     if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1067         $logger->info("Reset expire_time on activated hold ".$hold->id);
1068         $hold->expire_time(calculate_expire_time($hold->request_lib));
1069     }
1070
1071     $e->update_action_hold_request($hold) or return $e->die_event;
1072     $e->commit;
1073
1074     if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1075         $logger->info("Running targeter on activated hold ".$hold->id);
1076         $U->simplereq('open-ils.hold-targeter', 
1077             'open-ils.hold-targeter.target', {hold => $hold->id});
1078     }
1079
1080     # a change to mint-condition changes the set of potential copies, so retarget the hold;
1081     if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
1082         _reset_hold($self, $e->requestor, $hold)
1083     } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
1084         $U->simplereq('open-ils.hold-targeter', 
1085             'open-ils.hold-targeter.target', {hold => $hold->id});
1086     }
1087
1088     return $hold->id;
1089 }
1090
1091 # this does not update the hold in the DB.  It only
1092 # sets the shelf_expire_time field on the hold object.
1093 # start_time is optional and defaults to 'now'
1094 sub set_hold_shelf_expire_time {
1095     my ($class, $hold, $editor, $start_time) = @_;
1096
1097     my $shelf_expire = $U->ou_ancestor_setting_value(
1098         $hold->pickup_lib,
1099         'circ.holds.default_shelf_expire_interval',
1100         $editor
1101     );
1102
1103     return undef unless $shelf_expire;
1104
1105     $start_time = ($start_time) ?
1106         DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time)) :
1107         DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
1108
1109     my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($shelf_expire);
1110     my $expire_time = $start_time->add(seconds => $seconds);
1111
1112     # if the shelf expire time overlaps with a pickup lib's
1113     # closed date, push it out to the first open date
1114     my $dateinfo = $U->storagereq(
1115         'open-ils.storage.actor.org_unit.closed_date.overlap',
1116         $hold->pickup_lib, $expire_time->strftime('%FT%T%z'));
1117
1118     if($dateinfo) {
1119         my $dt_parser = DateTime::Format::ISO8601->new;
1120         $expire_time = $dt_parser->parse_datetime(clean_ISO8601($dateinfo->{end}));
1121
1122         # TODO: enable/disable time bump via setting?
1123         $expire_time->set(hour => '23', minute => '59', second => '59');
1124
1125         $logger->info("circulator: shelf_expire_time overlaps".
1126             " with closed date, pushing expire time to $expire_time");
1127     }
1128
1129     $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
1130     return undef;
1131 }
1132
1133
1134 sub transit_hold {
1135     my($e, $orig_hold, $hold, $copy) = @_;
1136     my $src  = $orig_hold->pickup_lib;
1137     my $dest = $hold->pickup_lib;
1138
1139     $logger->info("putting hold into transit on pickup_lib update");
1140
1141     my $transit = Fieldmapper::action::hold_transit_copy->new;
1142     $transit->hold($hold->id);
1143     $transit->source($src);
1144     $transit->dest($dest);
1145     $transit->target_copy($copy->id);
1146     $transit->source_send_time('now');
1147     $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1148
1149     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1150     $copy->editor($e->requestor->id);
1151     $copy->edit_date('now');
1152
1153     $e->create_action_hold_transit_copy($transit) or return $e->die_event;
1154     $e->update_asset_copy($copy) or return $e->die_event;
1155     return undef;
1156 }
1157
1158 # if the hold is frozen, this method ensures that the hold is not "targeted",
1159 # that is, it clears the current_copy and prev_check_time to essentiallly
1160 # reset the hold.  If it is being activated, it runs the targeter in the background
1161 sub update_hold_if_frozen {
1162     my($self, $e, $hold, $orig_hold) = @_;
1163     return if $hold->capture_time;
1164
1165     if($U->is_true($hold->frozen)) {
1166         $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1167         $hold->clear_current_copy;
1168         $hold->clear_prev_check_time;
1169
1170     } else {
1171         if($U->is_true($orig_hold->frozen)) {
1172             $logger->info("Running targeter on activated hold ".$hold->id);
1173             $U->simplereq('open-ils.hold-targeter', 
1174                 'open-ils.hold-targeter.target', {hold => $hold->id});
1175         }
1176     }
1177 }
1178
1179 __PACKAGE__->register_method(
1180     method    => "hold_note_CUD",
1181     api_name  => "open-ils.circ.hold_request.note.cud",
1182     signature => {
1183         desc   => 'Create, update or delete a hold request note.  If the operator (from Auth. token) '
1184                 . 'is not the owner of the hold, the UPDATE_HOLD permission is required',
1185         params => [
1186             { desc => 'Authentication token', type => 'string' },
1187             { desc => 'Hold note object',     type => 'object' }
1188         ],
1189         return => {
1190             desc => 'Returns the note ID, event on error'
1191         },
1192     }
1193 );
1194
1195 sub hold_note_CUD {
1196     my($self, $conn, $auth, $note) = @_;
1197
1198     my $e = new_editor(authtoken => $auth, xact => 1);
1199     return $e->die_event unless $e->checkauth;
1200
1201     my $hold = $e->retrieve_action_hold_request($note->hold)
1202         or return $e->die_event;
1203
1204     if($hold->usr ne $e->requestor->id) {
1205         my $usr = $e->retrieve_actor_user($hold->usr);
1206         return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1207         $note->staff('t') if $note->isnew;
1208     }
1209
1210     if($note->isnew) {
1211         $e->create_action_hold_request_note($note) or return $e->die_event;
1212     } elsif($note->ischanged) {
1213         $e->update_action_hold_request_note($note) or return $e->die_event;
1214     } elsif($note->isdeleted) {
1215         $e->delete_action_hold_request_note($note) or return $e->die_event;
1216     }
1217
1218     $e->commit;
1219     return $note->id;
1220 }
1221
1222
1223 __PACKAGE__->register_method(
1224     method    => "retrieve_hold_status",
1225     api_name  => "open-ils.circ.hold.status.retrieve",
1226     signature => {
1227         desc   => 'Calculates the current status of the hold. The requestor must have '      .
1228                   'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
1229         param  => [
1230             { desc => 'Hold ID', type => 'number' }
1231         ],
1232         return => {
1233             # type => 'number',     # event sometimes
1234             desc => <<'END_OF_DESC'
1235 Returns event on error or:
1236 -1 on error (for now),
1237  1 for 'waiting for copy to become available',
1238  2 for 'waiting for copy capture',
1239  3 for 'in transit',
1240  4 for 'arrived',
1241  5 for 'hold-shelf-delay'
1242  6 for 'canceled'
1243  7 for 'suspended'
1244  8 for 'captured, on wrong hold shelf'
1245  9 for 'fulfilled'
1246 END_OF_DESC
1247         }
1248     }
1249 );
1250
1251 sub retrieve_hold_status {
1252     my($self, $client, $auth, $hold_id) = @_;
1253
1254     my $e = new_editor(authtoken => $auth);
1255     return $e->event unless $e->checkauth;
1256     my $hold = $e->retrieve_action_hold_request($hold_id)
1257         or return $e->event;
1258
1259     if( $e->requestor->id != $hold->usr ) {
1260         return $e->event unless $e->allowed('VIEW_HOLD');
1261     }
1262
1263     return _hold_status($e, $hold);
1264
1265 }
1266
1267 sub _hold_status {
1268     my($e, $hold) = @_;
1269     if ($hold->cancel_time) {
1270         return 6;
1271     }
1272     if ($U->is_true($hold->frozen) && !$hold->capture_time) {
1273         return 7;
1274     }
1275     if ($hold->current_shelf_lib and $hold->current_shelf_lib ne $hold->pickup_lib) {
1276         return 8;
1277     }
1278     if ($hold->fulfillment_time) {
1279         return 9;
1280     }
1281     return 1 unless $hold->current_copy;
1282     return 2 unless $hold->capture_time;
1283
1284     my $copy = $hold->current_copy;
1285     unless( ref $copy ) {
1286         $copy = $e->retrieve_asset_copy($hold->current_copy)
1287             or return $e->event;
1288     }
1289
1290     return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
1291
1292     if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
1293
1294         my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
1295         return 4 unless $hs_wait_interval;
1296
1297         # if a hold_shelf_status_delay interval is defined and start_time plus
1298         # the interval is greater than now, consider the hold to be in the virtual
1299         # "on its way to the holds shelf" status. Return 5.
1300
1301         my $transit    = $e->search_action_hold_transit_copy({
1302                             hold           => $hold->id,
1303                             target_copy    => $copy->id,
1304                             cancel_time     => undef,
1305                             dest_recv_time => {'!=' => undef},
1306                          })->[0];
1307         my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
1308         $start_time    = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time));
1309         my $end_time   = $start_time->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($hs_wait_interval));
1310
1311         return 5 if $end_time > DateTime->now;
1312         return 4;
1313     }
1314
1315     return -1;  # error
1316 }
1317
1318
1319
1320 __PACKAGE__->register_method(
1321     method    => "retrieve_hold_queue_stats",
1322     api_name  => "open-ils.circ.hold.queue_stats.retrieve",
1323     signature => {
1324         desc   => 'Returns summary data about the state of a hold',
1325         params => [
1326             { desc => 'Authentication token',  type => 'string'},
1327             { desc => 'Hold ID', type => 'number'},
1328         ],
1329         return => {
1330             desc => q/Summary object with keys:
1331                 total_holds : total holds in queue
1332                 queue_position : current queue position
1333                 potential_copies : number of potential copies for this hold
1334                 estimated_wait : estimated wait time in days
1335                 status : hold status
1336                      -1 => error or unexpected state,
1337                      1 => 'waiting for copy to become available',
1338                      2 => 'waiting for copy capture',
1339                      3 => 'in transit',
1340                      4 => 'arrived',
1341                      5 => 'hold-shelf-delay'
1342             /,
1343             type => 'object'
1344         }
1345     }
1346 );
1347
1348 sub retrieve_hold_queue_stats {
1349     my($self, $conn, $auth, $hold_id) = @_;
1350     my $e = new_editor(authtoken => $auth);
1351     return $e->event unless $e->checkauth;
1352     my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1353     if($e->requestor->id != $hold->usr) {
1354         return $e->event unless $e->allowed('VIEW_HOLD');
1355     }
1356     return retrieve_hold_queue_status_impl($e, $hold);
1357 }
1358
1359 sub retrieve_hold_queue_status_impl {
1360     my $e = shift;
1361     my $hold = shift;
1362
1363     # The holds queue is defined as the distinct set of holds that share at
1364     # least one potential copy with the context hold, plus any holds that
1365     # share the same hold type and target.  The latter part exists to
1366     # accomodate holds that currently have no potential copies
1367     my $q_holds = $e->json_query({
1368
1369         # fetch cut_in_line and request_time since they're in the order_by
1370         # and we're asking for distinct values
1371         select => {ahr => ['id', 'cut_in_line', 'request_time']},
1372         from   => 'ahr',
1373         where => {
1374             id => { in => {
1375                 select => { ahcm => ['hold'] },
1376                 from   => {
1377                     'ahcm' => {
1378                         'ahcm2' => {
1379                             'class' => 'ahcm',
1380                             'field' => 'target_copy',
1381                             'fkey'  => 'target_copy'
1382                         }
1383                     }
1384                 },
1385                 where => { '+ahcm2' => { hold => $hold->id } },
1386                 distinct => 1
1387             }}
1388         },
1389         order_by => [
1390             {
1391                 "class" => "ahr",
1392                 "field" => "cut_in_line",
1393                 "transform" => "coalesce",
1394                 "params" => [ 0 ],
1395                 "direction" => "desc"
1396             },
1397             { "class" => "ahr", "field" => "request_time" }
1398         ],
1399         distinct => 1
1400     });
1401
1402     if (!@$q_holds) { # none? maybe we don't have a map ...
1403         $q_holds = $e->json_query({
1404             select => {ahr => ['id', 'cut_in_line', 'request_time']},
1405             from   => 'ahr',
1406             order_by => [
1407                 {
1408                     "class" => "ahr",
1409                     "field" => "cut_in_line",
1410                     "transform" => "coalesce",
1411                     "params" => [ 0 ],
1412                     "direction" => "desc"
1413                 },
1414                 { "class" => "ahr", "field" => "request_time" }
1415             ],
1416             where    => {
1417                 hold_type => $hold->hold_type,
1418                 target    => $hold->target,
1419                 capture_time => undef,
1420                 cancel_time => undef,
1421                 '-or' => [
1422                     {expire_time => undef },
1423                     {expire_time => {'>' => 'now'}}
1424                 ]
1425            }
1426         });
1427     }
1428
1429
1430     my $qpos = 1;
1431     for my $h (@$q_holds) {
1432         last if $h->{id} == $hold->id;
1433         $qpos++;
1434     }
1435
1436     my $hold_data = $e->json_query({
1437         select => {
1438             acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
1439             ccm => [ {column =>'avg_wait_time'} ]
1440         },
1441         from => {
1442             ahcm => {
1443                 acp => {
1444                     join => {
1445                         ccm => {type => 'left'}
1446                     }
1447                 }
1448             }
1449         },
1450         where => {'+ahcm' => {hold => $hold->id} }
1451     });
1452
1453     my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1454
1455     my $default_wait = $U->ou_ancestor_setting_value(
1456         $user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL, $e);
1457     my $min_wait = $U->ou_ancestor_setting_value(
1458         $user_org, 'circ.holds.min_estimated_wait_interval', $e);
1459     $min_wait = OpenILS::Utils::DateTime->interval_to_seconds($min_wait || '0 seconds');
1460     $default_wait ||= '0 seconds';
1461
1462     # Estimated wait time is the average wait time across the set
1463     # of potential copies, divided by the number of potential copies
1464     # times the queue position.
1465
1466     my $combined_secs = 0;
1467     my $num_potentials = 0;
1468
1469     for my $wait_data (@$hold_data) {
1470         my $count += $wait_data->{count};
1471         $combined_secs += $count *
1472             OpenILS::Utils::DateTime->interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1473         $num_potentials += $count;
1474     }
1475
1476     my $estimated_wait = -1;
1477
1478     if($num_potentials) {
1479         my $avg_wait = $combined_secs / $num_potentials;
1480         $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1481         $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1482     }
1483
1484     return {
1485         total_holds      => scalar(@$q_holds),
1486         queue_position   => $qpos,
1487         potential_copies => $num_potentials,
1488         status           => _hold_status( $e, $hold ),
1489         estimated_wait   => int($estimated_wait)
1490     };
1491 }
1492
1493
1494 sub fetch_open_hold_by_current_copy {
1495     my $class = shift;
1496     my $copyid = shift;
1497     my $hold = $apputils->simplereq(
1498         'open-ils.cstore',
1499         'open-ils.cstore.direct.action.hold_request.search.atomic',
1500         { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
1501     return $hold->[0] if ref($hold);
1502     return undef;
1503 }
1504
1505 sub fetch_related_holds {
1506     my $class = shift;
1507     my $copyid = shift;
1508     return $apputils->simplereq(
1509         'open-ils.cstore',
1510         'open-ils.cstore.direct.action.hold_request.search.atomic',
1511         { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
1512 }
1513
1514
1515 __PACKAGE__->register_method(
1516     method    => "hold_pull_list",
1517     api_name  => "open-ils.circ.hold_pull_list.retrieve",
1518     signature => {
1519         desc   => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1520                   'The location is determined by the login session.',
1521         params => [
1522             { desc => 'Limit (optional)',  type => 'number'},
1523             { desc => 'Offset (optional)', type => 'number'},
1524         ],
1525         return => {
1526             desc => 'reference to a list of holds, or event on failure',
1527         }
1528     }
1529 );
1530
1531 __PACKAGE__->register_method(
1532     method    => "hold_pull_list",
1533     api_name  => "open-ils.circ.hold_pull_list.id_list.retrieve",
1534     signature => {
1535         desc   => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1536                   'The location is determined by the login session.',
1537         params => [
1538             { desc => 'Limit (optional)',  type => 'number'},
1539             { desc => 'Offset (optional)', type => 'number'},
1540         ],
1541         return => {
1542             desc => 'reference to a list of holds, or event on failure',
1543         }
1544     }
1545 );
1546
1547 __PACKAGE__->register_method(
1548     method    => "hold_pull_list",
1549     api_name  => "open-ils.circ.hold_pull_list.retrieve.count",
1550     signature => {
1551         desc   => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1552                   'The location is determined by the login session.',
1553         params => [
1554             { desc => 'Limit (optional)',  type => 'number'},
1555             { desc => 'Offset (optional)', type => 'number'},
1556         ],
1557         return => {
1558             desc => 'Holds count (integer), or event on failure',
1559             # type => 'number'
1560         }
1561     }
1562 );
1563
1564 __PACKAGE__->register_method(
1565     method    => "hold_pull_list",
1566     stream => 1,
1567     # TODO: tag with api_level 2 once fully supported
1568     api_name  => "open-ils.circ.hold_pull_list.fleshed.stream",
1569     signature => {
1570         desc   => q/Returns a stream of fleshed holds  that need to be 
1571                     "pulled" by a given location.  The location is 
1572                     determined by the login session.  
1573                     This API calls always run in authoritative mode./,
1574         params => [
1575             { desc => 'Limit (optional)',  type => 'number'},
1576             { desc => 'Offset (optional)', type => 'number'},
1577         ],
1578         return => {
1579             desc => 'Stream of holds holds, or event on failure',
1580         }
1581     }
1582 );
1583
1584 sub hold_pull_list {
1585     my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1586     my( $reqr, $evt ) = $U->checkses($authtoken);
1587     return $evt if $evt;
1588
1589     my $org = $reqr->ws_ou || $reqr->home_ou;
1590     # the perm locaiton shouldn't really matter here since holds
1591     # will exist all over and VIEW_HOLDS should be universal
1592     $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1593     return $evt if $evt;
1594
1595     if($self->api_name =~ /count/) {
1596
1597         my $count = $U->storagereq(
1598             'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1599             $org, $limit, $offset );
1600
1601         $logger->info("Grabbing pull list for org unit $org with $count items");
1602         return $count;
1603
1604     } elsif( $self->api_name =~ /id_list/ ) {
1605         $U->storagereq(
1606             'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1607             $org, $limit, $offset );
1608
1609     } elsif ($self->api_name =~ /fleshed/) {
1610
1611         my $ids = $U->storagereq(
1612             'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1613             $org, $limit, $offset );
1614
1615         my $e = new_editor(xact => 1, requestor => $reqr);
1616         $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
1617         $e->rollback;
1618         $conn->respond_complete;
1619         return;
1620
1621     } else {
1622         $U->storagereq(
1623             'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1624             $org, $limit, $offset );
1625     }
1626 }
1627
1628 __PACKAGE__->register_method(
1629     method    => "print_hold_pull_list",
1630     api_name  => "open-ils.circ.hold_pull_list.print",
1631     signature => {
1632         desc   => 'Returns an HTML-formatted holds pull list',
1633         params => [
1634             { desc => 'Authtoken', type => 'string'},
1635             { desc => 'Org unit ID.  Optional, defaults to workstation org unit', type => 'number'},
1636         ],
1637         return => {
1638             desc => 'HTML string',
1639             type => 'string'
1640         }
1641     }
1642 );
1643
1644 sub print_hold_pull_list {
1645     my($self, $client, $auth, $org_id) = @_;
1646
1647     my $e = new_editor(authtoken=>$auth);
1648     return $e->event unless $e->checkauth;
1649
1650     $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
1651     return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1652
1653     my $hold_ids = $U->storagereq(
1654         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1655         $org_id, 10000);
1656
1657     return undef unless @$hold_ids;
1658
1659     $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1660
1661     # Holds will /NOT/ be in order after this ...
1662     my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
1663     $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1664
1665     # ... so we must resort.
1666     my $hold_map = +{map { $_->id => $_ } @$holds};
1667     my $sorted_holds = [];
1668     push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
1669
1670     return $U->fire_object_event(
1671         undef, "ahr.format.pull_list", $sorted_holds,
1672         $org_id, undef, undef, $client
1673     );
1674
1675 }
1676
1677 __PACKAGE__->register_method(
1678     method    => "print_hold_pull_list_stream",
1679     stream   => 1,
1680     api_name  => "open-ils.circ.hold_pull_list.print.stream",
1681     signature => {
1682         desc   => 'Returns a stream of fleshed holds',
1683         params => [
1684             { desc => 'Authtoken', type => 'string'},
1685             { desc => 'Hash of optional param: Org unit ID (defaults to workstation org unit), limit, offset, sort (array of: acplo.position, prefix, call_number, suffix, request_time)',
1686               type => 'object'
1687             },
1688         ],
1689         return => {
1690             desc => 'A stream of fleshed holds',
1691             type => 'object'
1692         }
1693     }
1694 );
1695
1696 sub print_hold_pull_list_stream {
1697     my($self, $client, $auth, $params) = @_;
1698
1699     my $e = new_editor(authtoken=>$auth);
1700     return $e->die_event unless $e->checkauth;
1701
1702     delete($$params{org_id}) unless (int($$params{org_id}));
1703     delete($$params{limit}) unless (int($$params{limit}));
1704     delete($$params{offset}) unless (int($$params{offset}));
1705     delete($$params{chunk_size}) unless (int($$params{chunk_size}));
1706     delete($$params{chunk_size}) if  ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
1707     $$params{chunk_size} ||= 10;
1708     $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
1709
1710     $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
1711     return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
1712
1713     my $sort = [];
1714     if ($$params{sort} && @{ $$params{sort} }) {
1715         for my $s (@{ $$params{sort} }) {
1716             if ($s eq 'acplo.position') {
1717                 push @$sort, {
1718                     "class" => "acplo", "field" => "position",
1719                     "transform" => "coalesce", "params" => [999]
1720                 };
1721             } elsif ($s eq 'prefix') {
1722                 push @$sort, {"class" => "acnp", "field" => "label_sortkey"};
1723             } elsif ($s eq 'call_number') {
1724                 push @$sort, {"class" => "acn", "field" => "label_sortkey"};
1725             } elsif ($s eq 'suffix') {
1726                 push @$sort, {"class" => "acns", "field" => "label_sortkey"};
1727             } elsif ($s eq 'request_time') {
1728                 push @$sort, {"class" => "ahr", "field" => "request_time"};
1729             }
1730         }
1731     } else {
1732         push @$sort, {"class" => "ahr", "field" => "request_time"};
1733     }
1734
1735     my $holds_ids = $e->json_query(
1736         {
1737             "select" => {"ahr" => ["id"]},
1738             "from" => {
1739                 "ahr" => {
1740                     "acp" => {
1741                         "field" => "id",
1742                         "fkey" => "current_copy",
1743                         "filter" => {
1744                             "circ_lib" => $$params{org_id}, "status" => [0,7]
1745                         },
1746                         "join" => {
1747                             "acn" => {
1748                                 "field" => "id",
1749                                 "fkey" => "call_number",
1750                                 "join" => {
1751                                     "acnp" => {
1752                                         "field" => "id",
1753                                         "fkey" => "prefix"
1754                                     },
1755                                     "acns" => {
1756                                         "field" => "id",
1757                                         "fkey" => "suffix"
1758                                     }
1759                                 }
1760                             },
1761                             "acplo" => {
1762                                 "field" => "org",
1763                                 "fkey" => "circ_lib",
1764                                 "type" => "left",
1765                                 "filter" => {
1766                                     "location" => {"=" => {"+acp" => "location"}}
1767                                 }
1768                             }
1769                         }
1770                     }
1771                 }
1772             },
1773             "where" => {
1774                 "+ahr" => {
1775                     "capture_time" => undef,
1776                     "cancel_time" => undef,
1777                     "-or" => [
1778                         {"expire_time" => undef },
1779                         {"expire_time" => {">" => "now"}}
1780                     ]
1781                 }
1782             },
1783             (@$sort ? (order_by => $sort) : ()),
1784             ($$params{limit} ? (limit => $$params{limit}) : ()),
1785             ($$params{offset} ? (offset => $$params{offset}) : ())
1786         }, {"substream" => 1}
1787     ) or return $e->die_event;
1788
1789     $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
1790
1791     my @chunk;
1792     for my $hid (@$holds_ids) {
1793         push @chunk, $e->retrieve_action_hold_request([
1794             $hid->{"id"}, {
1795                 "flesh" => 3,
1796                 "flesh_fields" => {
1797                     "ahr" => ["usr", "current_copy"],
1798                     "au"  => ["card"],
1799                     "acp" => ["location", "call_number", "parts"],
1800                     "acn" => ["record","prefix","suffix"]
1801                 }
1802             }
1803         ]);
1804
1805         if (@chunk >= $$params{chunk_size}) {
1806             $client->respond( \@chunk );
1807             @chunk = ();
1808         }
1809     }
1810     $client->respond_complete( \@chunk ) if (@chunk);
1811     $e->disconnect;
1812     return undef;
1813 }
1814
1815
1816
1817 __PACKAGE__->register_method(
1818     method        => 'fetch_hold_notify',
1819     api_name      => 'open-ils.circ.hold_notification.retrieve_by_hold',
1820     authoritative => 1,
1821     signature     => q/
1822 Returns a list of hold notification objects based on hold id.
1823 @param authtoken The loggin session key
1824 @param holdid The id of the hold whose notifications we want to retrieve
1825 @return An array of hold notification objects, event on error.
1826 /
1827 );
1828
1829 sub fetch_hold_notify {
1830     my( $self, $conn, $authtoken, $holdid ) = @_;
1831     my( $requestor, $evt ) = $U->checkses($authtoken);
1832     return $evt if $evt;
1833     my ($hold, $patron);
1834     ($hold, $evt) = $U->fetch_hold($holdid);
1835     return $evt if $evt;
1836     ($patron, $evt) = $U->fetch_user($hold->usr);
1837     return $evt if $evt;
1838
1839     $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
1840     return $evt if $evt;
1841
1842     $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
1843     return $U->cstorereq(
1844         'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
1845 }
1846
1847
1848 __PACKAGE__->register_method(
1849     method    => 'create_hold_notify',
1850     api_name  => 'open-ils.circ.hold_notification.create',
1851     signature => q/
1852 Creates a new hold notification object
1853 @param authtoken The login session key
1854 @param notification The hold notification object to create
1855 @return ID of the new object on success, Event on error
1856 /
1857 );
1858
1859 sub create_hold_notify {
1860    my( $self, $conn, $auth, $note ) = @_;
1861    my $e = new_editor(authtoken=>$auth, xact=>1);
1862    return $e->die_event unless $e->checkauth;
1863
1864    my $hold = $e->retrieve_action_hold_request($note->hold)
1865       or return $e->die_event;
1866    my $patron = $e->retrieve_actor_user($hold->usr)
1867       or return $e->die_event;
1868
1869    return $e->die_event unless
1870       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1871
1872    $note->notify_staff($e->requestor->id);
1873    $e->create_action_hold_notification($note) or return $e->die_event;
1874    $e->commit;
1875    return $note->id;
1876 }
1877
1878 __PACKAGE__->register_method(
1879     method    => 'create_hold_note',
1880     api_name  => 'open-ils.circ.hold_note.create',
1881     signature => q/
1882         Creates a new hold request note object
1883         @param authtoken The login session key
1884         @param note The hold note object to create
1885         @return ID of the new object on success, Event on error
1886         /
1887 );
1888
1889 sub create_hold_note {
1890    my( $self, $conn, $auth, $note ) = @_;
1891    my $e = new_editor(authtoken=>$auth, xact=>1);
1892    return $e->die_event unless $e->checkauth;
1893
1894    my $hold = $e->retrieve_action_hold_request($note->hold)
1895       or return $e->die_event;
1896    my $patron = $e->retrieve_actor_user($hold->usr)
1897       or return $e->die_event;
1898
1899    return $e->die_event unless
1900       $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn.  Probably want something more specific
1901
1902    $e->create_action_hold_request_note($note) or return $e->die_event;
1903    $e->commit;
1904    return $note->id;
1905 }
1906
1907 __PACKAGE__->register_method(
1908     method    => 'reset_hold',
1909     api_name  => 'open-ils.circ.hold.reset',
1910     signature => q/
1911         Un-captures and un-targets a hold, essentially returning
1912         it to the state it was in directly after it was placed,
1913         then attempts to re-target the hold
1914         @param authtoken The login session key
1915         @param holdid The id of the hold
1916     /
1917 );
1918
1919
1920 sub reset_hold {
1921     my( $self, $conn, $auth, $holdid ) = @_;
1922     my $reqr;
1923     my ($hold, $evt) = $U->fetch_hold($holdid);
1924     return $evt if $evt;
1925     ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1926     return $evt if $evt;
1927     $evt = _reset_hold($self, $reqr, $hold);
1928     return $evt if $evt;
1929     return 1;
1930 }
1931
1932
1933 __PACKAGE__->register_method(
1934     method   => 'reset_hold_batch',
1935     api_name => 'open-ils.circ.hold.reset.batch'
1936 );
1937
1938 sub reset_hold_batch {
1939     my($self, $conn, $auth, $hold_ids) = @_;
1940
1941     my $e = new_editor(authtoken => $auth);
1942     return $e->event unless $e->checkauth;
1943
1944     for my $hold_id ($hold_ids) {
1945
1946         my $hold = $e->retrieve_action_hold_request(
1947             [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
1948             or return $e->event;
1949
1950         next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1951         _reset_hold($self, $e->requestor, $hold);
1952     }
1953
1954     return 1;
1955 }
1956
1957
1958 sub _reset_hold {
1959     my ($self, $reqr, $hold) = @_;
1960
1961     my $e = new_editor(xact =>1, requestor => $reqr);
1962
1963     $logger->info("reseting hold ".$hold->id);
1964
1965     my $hid = $hold->id;
1966
1967     if( $hold->capture_time and $hold->current_copy ) {
1968
1969         my $copy = $e->retrieve_asset_copy($hold->current_copy)
1970             or return $e->die_event;
1971
1972         if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1973             $logger->info("setting copy to status 'reshelving' on hold retarget");
1974             $copy->status(OILS_COPY_STATUS_RESHELVING);
1975             $copy->editor($e->requestor->id);
1976             $copy->edit_date('now');
1977             $e->update_asset_copy($copy) or return $e->die_event;
1978
1979         } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1980
1981             $logger->warn("! reseting hold [$hid] that is in transit");
1982             my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
1983
1984             if( $transid ) {
1985                 my $trans = $e->retrieve_action_transit_copy($transid);
1986                 if( $trans ) {
1987                     $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1988                     my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
1989                     $logger->info("Transit abort completed with result $evt");
1990                     unless ("$evt" eq 1) {
1991                         $e->rollback;
1992                         return $evt;
1993                     }
1994                 }
1995             }
1996         }
1997     }
1998
1999     $hold->clear_capture_time;
2000     $hold->clear_current_copy;
2001     $hold->clear_shelf_time;
2002     $hold->clear_shelf_expire_time;
2003     $hold->clear_current_shelf_lib;
2004
2005     $e->update_action_hold_request($hold) or return $e->die_event;
2006     $e->commit;
2007
2008     $U->simplereq('open-ils.hold-targeter', 
2009         'open-ils.hold-targeter.target', {hold => $hold->id});
2010
2011     return undef;
2012 }
2013
2014
2015 __PACKAGE__->register_method(
2016     method    => 'fetch_open_title_holds',
2017     api_name  => 'open-ils.circ.open_holds.retrieve',
2018     signature => q/
2019         Returns a list ids of un-fulfilled holds for a given title id
2020         @param authtoken The login session key
2021         @param id the id of the item whose holds we want to retrieve
2022         @param type The hold type - M, T, I, V, C, F, R
2023     /
2024 );
2025
2026 sub fetch_open_title_holds {
2027     my( $self, $conn, $auth, $id, $type, $org ) = @_;
2028     my $e = new_editor( authtoken => $auth );
2029     return $e->event unless $e->checkauth;
2030
2031     $type ||= "T";
2032     $org  ||= $e->requestor->ws_ou;
2033
2034 #    return $e->search_action_hold_request(
2035 #        { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2036
2037     # XXX make me return IDs in the future ^--
2038     my $holds = $e->search_action_hold_request(
2039         {
2040             target           => $id,
2041             cancel_time      => undef,
2042             hold_type        => $type,
2043             fulfillment_time => undef
2044         }
2045     );
2046
2047     flesh_hold_transits($holds);
2048     return $holds;
2049 }
2050
2051
2052 sub flesh_hold_transits {
2053     my $holds = shift;
2054     for my $hold ( @$holds ) {
2055         $hold->transit(
2056             $apputils->simplereq(
2057                 'open-ils.cstore',
2058                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2059                 { hold => $hold->id, cancel_time => undef },
2060                 { order_by => { ahtc => 'id desc' }, limit => 1 }
2061             )->[0]
2062         );
2063     }
2064 }
2065
2066 sub flesh_hold_notices {
2067     my( $holds, $e ) = @_;
2068     $e ||= new_editor();
2069
2070     for my $hold (@$holds) {
2071         my $notices = $e->search_action_hold_notification(
2072             [
2073                 { hold => $hold->id },
2074                 { order_by => { anh => 'notify_time desc' } },
2075             ],
2076             {idlist=>1}
2077         );
2078
2079         $hold->notify_count(scalar(@$notices));
2080         if( @$notices ) {
2081             my $n = $e->retrieve_action_hold_notification($$notices[0])
2082                 or return $e->event;
2083             $hold->notify_time($n->notify_time);
2084         }
2085     }
2086 }
2087
2088
2089 __PACKAGE__->register_method(
2090     method    => 'fetch_captured_holds',
2091     api_name  => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2092     stream    => 1,
2093     authoritative => 1,
2094     signature => q/
2095         Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2096         @param authtoken The login session key
2097         @param org The org id of the location in question
2098         @param match_copy A specific copy to limit to
2099     /
2100 );
2101
2102 __PACKAGE__->register_method(
2103     method    => 'fetch_captured_holds',
2104     api_name  => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2105     stream    => 1,
2106     authoritative => 1,
2107     signature => q/
2108         Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2109         @param authtoken The login session key
2110         @param org The org id of the location in question
2111         @param match_copy A specific copy to limit to
2112     /
2113 );
2114
2115 __PACKAGE__->register_method(
2116     method    => 'fetch_captured_holds',
2117     api_name  => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2118     stream    => 1,
2119     authoritative => 1,
2120     signature => q/
2121         Returns list ids of shelf-expired un-fulfilled holds for a given title id
2122         @param authtoken The login session key
2123         @param org The org id of the location in question
2124         @param match_copy A specific copy to limit to
2125     /
2126 );
2127
2128 __PACKAGE__->register_method(
2129     method    => 'fetch_captured_holds',
2130     api_name  => 
2131       'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2132     stream    => 1,
2133     authoritative => 1,
2134     signature => q/
2135         Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2136         for a given shelf lib
2137     /
2138 );
2139
2140 __PACKAGE__->register_method(
2141     method    => 'fetch_captured_holds',
2142     api_name  => 
2143       'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2144     stream    => 1,
2145     authoritative => 1,
2146     signature => q/
2147         Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2148         for a given shelf lib
2149     /
2150 );
2151
2152
2153 sub fetch_captured_holds {
2154     my( $self, $conn, $auth, $org, $match_copy ) = @_;
2155
2156     my $e = new_editor(authtoken => $auth);
2157     return $e->die_event unless $e->checkauth;
2158     return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2159
2160     $org ||= $e->requestor->ws_ou;
2161
2162     my $current_copy = { '!=' => undef };
2163     $current_copy = { '=' => $match_copy } if $match_copy;
2164
2165     my $query = {
2166         select => { alhr => ['id'] },
2167         from   => {
2168             alhr => {
2169                 acp => {
2170                     field => 'id',
2171                     fkey  => 'current_copy'
2172                 },
2173             }
2174         },
2175         where => {
2176             '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2177             '+alhr' => {
2178                 capture_time      => { "!=" => undef },
2179                 current_copy      => $current_copy,
2180                 fulfillment_time  => undef,
2181                 current_shelf_lib => $org
2182             }
2183         }
2184     };
2185     if($self->api_name =~ /expired/) {
2186         $query->{'where'}->{'+alhr'}->{'-or'} = {
2187                 shelf_expire_time => { '<' => 'today'},
2188                 cancel_time => { '!=' => undef },
2189         };
2190     }
2191     my $hold_ids = $e->json_query( $query );
2192
2193     if ($self->api_name =~ /wrong_shelf/) {
2194         # fetch holds whose current_shelf_lib is $org, but whose pickup 
2195         # lib is some other org unit.  Ignore already-retrieved holds.
2196         my $wrong_shelf =
2197             pickup_lib_changed_on_shelf_holds(
2198                 $e, $org, [map {$_->{id}} @$hold_ids]);
2199         # match the layout of other items in $hold_ids
2200         push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2201     }
2202
2203
2204     for my $hold_id (@$hold_ids) {
2205         if($self->api_name =~ /id_list/) {
2206             $conn->respond($hold_id->{id});
2207             next;
2208         } else {
2209             $conn->respond(
2210                 $e->retrieve_action_hold_request([
2211                     $hold_id->{id},
2212                     {
2213                         flesh => 1,
2214                         flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2215                         order_by => {anh => 'notify_time desc'}
2216                     }
2217                 ])
2218             );
2219         }
2220     }
2221
2222     return undef;
2223 }
2224
2225 __PACKAGE__->register_method(
2226     method    => "print_expired_holds_stream",
2227     api_name  => "open-ils.circ.captured_holds.expired.print.stream",
2228     stream    => 1
2229 );
2230
2231 sub print_expired_holds_stream {
2232     my ($self, $client, $auth, $params) = @_;
2233
2234     # No need to check specific permissions: we're going to call another method
2235     # that will do that.
2236     my $e = new_editor("authtoken" => $auth);
2237     return $e->die_event unless $e->checkauth;
2238
2239     delete($$params{org_id}) unless (int($$params{org_id}));
2240     delete($$params{limit}) unless (int($$params{limit}));
2241     delete($$params{offset}) unless (int($$params{offset}));
2242     delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2243     delete($$params{chunk_size}) if  ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2244     $$params{chunk_size} ||= 10;
2245     $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2246
2247     $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2248
2249     my @hold_ids = $self->method_lookup(
2250         "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2251     )->run($auth, $params->{"org_id"});
2252
2253     if (!@hold_ids) {
2254         $e->disconnect;
2255         return;
2256     } elsif (defined $U->event_code($hold_ids[0])) {
2257         $e->disconnect;
2258         return $hold_ids[0];
2259     }
2260
2261     $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2262
2263     while (@hold_ids) {
2264         my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2265
2266         my $result_chunk = $e->json_query({
2267             "select" => {
2268                 "acp" => ["barcode"],
2269                 "au" => [qw/
2270                     first_given_name second_given_name family_name alias
2271                 /],
2272                 "acn" => ["label"],
2273                 "bre" => ["marc"],
2274                 "acpl" => ["name"]
2275             },
2276             "from" => {
2277                 "ahr" => {
2278                     "acp" => {
2279                         "field" => "id", "fkey" => "current_copy",
2280                         "join" => {
2281                             "acn" => {
2282                                 "field" => "id", "fkey" => "call_number",
2283                                 "join" => {
2284                                     "bre" => {
2285                                         "field" => "id", "fkey" => "record"
2286                                     }
2287                                 }
2288                             },
2289                             "acpl" => {"field" => "id", "fkey" => "location"}
2290                         }
2291                     },
2292                     "au" => {"field" => "id", "fkey" => "usr"}
2293                 }
2294             },
2295             "where" => {"+ahr" => {"id" => \@hid_chunk}}
2296         }) or return $e->die_event;
2297         $client->respond($result_chunk);
2298     }
2299
2300     $e->disconnect;
2301     undef;
2302 }
2303
2304 __PACKAGE__->register_method(
2305     method    => "check_title_hold_batch",
2306     api_name  => "open-ils.circ.title_hold.is_possible.batch",
2307     stream    => 1,
2308     signature => {
2309         desc  => '@see open-ils.circ.title_hold.is_possible.batch',
2310         params => [
2311             { desc => 'Authentication token',     type => 'string'},
2312             { desc => 'Array of Hash of named parameters', type => 'array'},
2313         ],
2314         return => {
2315             desc => 'Array of response objects',
2316             type => 'array'
2317         }
2318     }
2319 );
2320
2321 sub check_title_hold_batch {
2322     my($self, $client, $authtoken, $param_list, $oargs) = @_;
2323     foreach (@$param_list) {
2324         my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2325         $client->respond($res);
2326     }
2327     return undef;
2328 }
2329
2330
2331 __PACKAGE__->register_method(
2332     method    => "check_title_hold",
2333     api_name  => "open-ils.circ.title_hold.is_possible",
2334     signature => {
2335         desc  => 'Determines if a hold were to be placed by a given user, ' .
2336              'whether or not said hold would have any potential copies to fulfill it.' .
2337              'The named paramaters of the second argument include: ' .
2338              'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2339              'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2340         params => [
2341             { desc => 'Authentication token',     type => 'string'},
2342             { desc => 'Hash of named parameters', type => 'object'},
2343         ],
2344         return => {
2345             desc => 'List of new message IDs (empty if none)',
2346             type => 'array'
2347         }
2348     }
2349 );
2350
2351 =head3 check_title_hold (token, hash)
2352
2353 The named fields in the hash are:
2354
2355  patronid     - ID of the hold recipient  (required)
2356  depth        - hold range depth          (default 0)
2357  pickup_lib   - destination for hold, fallback value for selection_ou
2358  selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2359  issuanceid   - ID of the issuance to be held, required for Issuance level hold
2360  partid       - ID of the monograph part to be held, required for monograph part level hold
2361  titleid      - ID (BRN) of the title to be held, required for Title level hold
2362  volume_id    - required for Volume level hold
2363  copy_id      - required for Copy level hold
2364  mrid         - required for Meta-record level hold
2365  hold_type    - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record  (default "T")
2366
2367 All key/value pairs are passed on to do_possibility_checks.
2368
2369 =cut
2370
2371 # FIXME: better params checking.  what other params are required, if any?
2372 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2373 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2374 # used in conditionals, where it may be undefined, causing a warning.
2375 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2376
2377 sub check_title_hold {
2378     my( $self, $client, $authtoken, $params ) = @_;
2379     my $e = new_editor(authtoken=>$authtoken);
2380     return $e->event unless $e->checkauth;
2381
2382     my %params       = %$params;
2383     my $depth        = $params{depth}        || 0;
2384     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 # -----------------------------------------------------