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