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