]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
LP#1570072: update hold notification methods upon preference changes
[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} = {
4050             field  => 'id',
4051             fkey   => 'call_number',
4052             'join' => {
4053                 bre => {
4054                     field  => 'id',
4055                     filter => {id => $hold_target},
4056                     fkey   => 'record'
4057                 }
4058             }
4059         };
4060
4061     } else {
4062
4063         $query->{from}->{acp}->{acn} = {
4064             field => 'id',
4065             fkey => 'call_number',
4066             join => {
4067                 bre => {
4068                     field => 'id',
4069                     fkey => 'record',
4070                     join => {
4071                         mmrsm => {
4072                             field => 'source',
4073                             fkey => 'id',
4074                             filter => {metarecord => $hold_target},
4075                         }
4076                     }
4077                 }
4078             }
4079         };
4080     }
4081
4082     return $e->json_query($query)->[0];
4083 }
4084
4085 __PACKAGE__->register_method(
4086     method    => 'change_hold_title',
4087     api_name  => 'open-ils.circ.hold.change_title',
4088     signature => {
4089         desc => q/
4090             Updates all title level holds targeting the specified bibs to point a new bib./,
4091         params => [
4092             { desc => 'Authentication Token', type => 'string' },
4093             { desc => 'New Target Bib Id',    type => 'number' },
4094             { desc => 'Old Target Bib Ids',   type => 'array'  },
4095         ],
4096         return => { desc => '1 on success' }
4097     }
4098 );
4099
4100 __PACKAGE__->register_method(
4101     method    => 'change_hold_title_for_specific_holds',
4102     api_name  => 'open-ils.circ.hold.change_title.specific_holds',
4103     signature => {
4104         desc => q/
4105             Updates specified holds to target new bib./,
4106         params => [
4107             { desc => 'Authentication Token', type => 'string' },
4108             { desc => 'New Target Bib Id',    type => 'number' },
4109             { desc => 'Holds Ids for holds to update',   type => 'array'  },
4110         ],
4111         return => { desc => '1 on success' }
4112     }
4113 );
4114
4115
4116 sub change_hold_title {
4117     my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4118
4119     my $e = new_editor(authtoken=>$auth, xact=>1);
4120     return $e->die_event unless $e->checkauth;
4121
4122     my $holds = $e->search_action_hold_request(
4123         [
4124             {
4125                 capture_time     => undef,
4126                 cancel_time      => undef,
4127                 fulfillment_time => undef,
4128                 hold_type        => 'T',
4129                 target           => $bib_ids
4130             },
4131             {
4132                 flesh        => 1,
4133                 flesh_fields => { ahr => ['usr'] }
4134             }
4135         ],
4136         { substream => 1 }
4137     );
4138
4139     for my $hold (@$holds) {
4140         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4141         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4142         $hold->target( $new_bib_id );
4143         $e->update_action_hold_request($hold) or return $e->die_event;
4144     }
4145
4146     $e->commit;
4147
4148     _reset_hold($self, $e->requestor, $_) for @$holds;
4149
4150     return 1;
4151 }
4152
4153 sub change_hold_title_for_specific_holds {
4154     my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4155
4156     my $e = new_editor(authtoken=>$auth, xact=>1);
4157     return $e->die_event unless $e->checkauth;
4158
4159     my $holds = $e->search_action_hold_request(
4160         [
4161             {
4162                 capture_time     => undef,
4163                 cancel_time      => undef,
4164                 fulfillment_time => undef,
4165                 hold_type        => 'T',
4166                 id               => $hold_ids
4167             },
4168             {
4169                 flesh        => 1,
4170                 flesh_fields => { ahr => ['usr'] }
4171             }
4172         ],
4173         { substream => 1 }
4174     );
4175
4176     for my $hold (@$holds) {
4177         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4178         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4179         $hold->target( $new_bib_id );
4180         $e->update_action_hold_request($hold) or return $e->die_event;
4181     }
4182
4183     $e->commit;
4184
4185     _reset_hold($self, $e->requestor, $_) for @$holds;
4186
4187     return 1;
4188 }
4189
4190 __PACKAGE__->register_method(
4191     method    => 'rec_hold_count',
4192     api_name  => 'open-ils.circ.bre.holds.count',
4193     signature => {
4194         desc => q/Returns the total number of holds that target the
4195             selected bib record or its associated copies and call_numbers/,
4196         params => [
4197             { desc => 'Bib ID', type => 'number' },
4198             { desc => q/Optional arguments.  Supported arguments include:
4199                 "pickup_lib_descendant" -> limit holds to those whose pickup
4200                 library is equal to or is a child of the provided org unit/,
4201                 type => 'object'
4202             }
4203         ],
4204         return => {desc => 'Hold count', type => 'number'}
4205     }
4206 );
4207
4208 __PACKAGE__->register_method(
4209     method    => 'rec_hold_count',
4210     api_name  => 'open-ils.circ.mmr.holds.count',
4211     signature => {
4212         desc => q/Returns the total number of holds that target the
4213             selected metarecord or its associated copies, call_numbers, and bib records/,
4214         params => [
4215             { desc => 'Metarecord ID', type => 'number' },
4216         ],
4217         return => {desc => 'Hold count', type => 'number'}
4218     }
4219 );
4220
4221 # XXX Need to add type I holds to these counts
4222 sub rec_hold_count {
4223     my($self, $conn, $target_id, $args) = @_;
4224     $args ||= {};
4225
4226     my $mmr_join = {
4227         mmrsm => {
4228             field => 'source',
4229             fkey => 'id',
4230             filter => {metarecord => $target_id}
4231         }
4232     };
4233
4234     my $bre_join = {
4235         bre => {
4236             field => 'id',
4237             filter => { id => $target_id },
4238             fkey => 'record'
4239         }
4240     };
4241
4242     if($self->api_name =~ /mmr/) {
4243         delete $bre_join->{bre}->{filter};
4244         $bre_join->{bre}->{join} = $mmr_join;
4245     }
4246
4247     my $cn_join = {
4248         acn => {
4249             field => 'id',
4250             fkey => 'call_number',
4251             join => $bre_join
4252         }
4253     };
4254
4255     my $query = {
4256         select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4257         from => 'ahr',
4258         where => {
4259             '+ahr' => {
4260                 cancel_time => undef,
4261                 fulfillment_time => undef,
4262                 '-or' => [
4263                     {
4264                         '-and' => {
4265                             hold_type => [qw/C F R/],
4266                             target => {
4267                                 in => {
4268                                     select => {acp => ['id']},
4269                                     from => { acp => $cn_join }
4270                                 }
4271                             }
4272                         }
4273                     },
4274                     {
4275                         '-and' => {
4276                             hold_type => 'V',
4277                             target => {
4278                                 in => {
4279                                     select => {acn => ['id']},
4280                                     from => {acn => $bre_join}
4281                                 }
4282                             }
4283                         }
4284                     },
4285                     {
4286                         '-and' => {
4287                             hold_type => 'P',
4288                             target => {
4289                                 in => {
4290                                     select => {bmp => ['id']},
4291                                     from => {bmp => $bre_join}
4292                                 }
4293                             }
4294                         }
4295                     },
4296                     {
4297                         '-and' => {
4298                             hold_type => 'T',
4299                             target => $target_id
4300                         }
4301                     }
4302                 ]
4303             }
4304         }
4305     };
4306
4307     if($self->api_name =~ /mmr/) {
4308         $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4309             '-and' => {
4310                 hold_type => 'T',
4311                 target => {
4312                     in => {
4313                         select => {bre => ['id']},
4314                         from => {bre => $mmr_join}
4315                     }
4316                 }
4317             }
4318         };
4319
4320         $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4321             '-and' => {
4322                 hold_type => 'M',
4323                 target => $target_id
4324             }
4325         };
4326     }
4327
4328
4329     if (my $pld = $args->{pickup_lib_descendant}) {
4330
4331         my $top_ou = new_editor()->search_actor_org_unit(
4332             {parent_ou => undef}
4333         )->[0]; # XXX Assumes single root node. Not alone in this...
4334
4335         $query->{where}->{'+ahr'}->{pickup_lib} = {
4336             in => {
4337                 select  => {aou => [{ 
4338                     column => 'id', 
4339                     transform => 'actor.org_unit_descendants', 
4340                     result_field => 'id' 
4341                 }]},
4342                 from    => 'aou',
4343                 where   => {id => $pld}
4344             }
4345         } if ($pld != $top_ou->id);
4346     }
4347
4348     # To avoid Internal Server Errors, we get an editor, then run the
4349     # query and check the result.  If anything fails, we'll return 0.
4350     my $result = 0;
4351     if (my $e = new_editor()) {
4352         my $query_result = $e->json_query($query);
4353         if ($query_result && @{$query_result}) {
4354             $result = $query_result->[0]->{count}
4355         }
4356     }
4357
4358     return $result;
4359 }
4360
4361 # A helper function to calculate a hold's expiration time at a given
4362 # org_unit. Takes the org_unit as an argument and returns either the
4363 # hold expire time as an ISO8601 string or undef if there is no hold
4364 # expiration interval set for the subject ou.
4365 sub calculate_expire_time
4366 {
4367     my $ou = shift;
4368     my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4369     if($interval) {
4370         my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
4371         return $U->epoch2ISO8601($date->epoch);
4372     }
4373     return undef;
4374 }
4375
4376
4377 __PACKAGE__->register_method(
4378     method    => 'mr_hold_filter_attrs',
4379     api_name  => 'open-ils.circ.mmr.holds.filters',
4380     authoritative => 1,
4381     stream => 1,
4382     signature => {
4383         desc => q/
4384             Returns the set of available formats and languages for the
4385             constituent records of the provided metarcord.
4386             If an array of hold IDs is also provided, information about
4387             each is returned as well.  This information includes:
4388             1. a slightly easier to read version of holdable_formats
4389             2. attributes describing the set of format icons included
4390                in the set of desired, constituent records.
4391         /,
4392         params => [
4393             {desc => 'Metarecord ID', type => 'number'},
4394             {desc => 'Context Org ID', type => 'number'},
4395             {desc => 'Hold ID List', type => 'array'},
4396         ],
4397         return => {
4398             desc => q/
4399                 Stream of objects.  The first will have a 'metarecord' key
4400                 containing non-hold-specific metarecord information, subsequent
4401                 responses will contain a 'hold' key containing hold-specific
4402                 information
4403             /, 
4404             type => 'object'
4405         }
4406     }
4407 );
4408
4409 sub mr_hold_filter_attrs { 
4410     my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4411     my $e = new_editor();
4412
4413     # by default, return MR / hold attributes for all constituent
4414     # records with holdable copies.  If there is a hard boundary,
4415     # though, limit to records with copies within the boundary,
4416     # since anything outside the boundary can never be held.
4417     my $org_depth = 0;
4418     if ($org_id) {
4419         $org_depth = $U->ou_ancestor_setting_value(
4420             $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4421     }
4422
4423     # get all org-scoped records w/ holdable copies for this metarecord
4424     my ($bre_ids) = $self->method_lookup(
4425         'open-ils.circ.holds.metarecord.filtered_records')->run(
4426             $mr_id, undef, $org_id, $org_depth);
4427
4428     my $item_lang_attr = 'item_lang'; # configurable?
4429     my $format_attr = $e->retrieve_config_global_flag(
4430         'opac.metarecord.holds.format_attr')->value;
4431
4432     # helper sub for fetching ccvms for a batch of record IDs
4433     sub get_batch_ccvms {
4434         my ($e, $attr, $bre_ids) = @_;
4435         return [] unless $bre_ids and @$bre_ids;
4436         my $vals = $e->search_metabib_record_attr_flat({
4437             attr => $attr,
4438             id => $bre_ids
4439         });
4440         return [] unless @$vals;
4441         return $e->search_config_coded_value_map({
4442             ctype => $attr,
4443             code => [map {$_->value} @$vals]
4444         });
4445     }
4446
4447     my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4448     my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4449
4450     $client->respond({
4451         metarecord => {
4452             id => $mr_id,
4453             formats => $formats,
4454             langs => $langs
4455         }
4456     });
4457
4458     return unless $hold_ids;
4459     my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4460     $icon_attr = $icon_attr ? $icon_attr->value : '';
4461
4462     for my $hold_id (@$hold_ids) {
4463         my $hold = $e->retrieve_action_hold_request($hold_id) 
4464             or return $e->event;
4465
4466         next unless $hold->hold_type eq 'M';
4467
4468         my $resp = {
4469             hold => {
4470                 id => $hold_id,
4471                 formats => [],
4472                 langs => []
4473             }
4474         };
4475
4476         # collect the ccvm's for the selected formats / language
4477         # (i.e. the holdable formats) on the MR.
4478         # this assumes a two-key structure for format / language,
4479         # though no assumption is made about the keys themselves.
4480         my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4481         my $lang_vals = [];
4482         my $format_vals = [];
4483         for my $val (values %$hformats) {
4484             # val is either a single ccvm or an array of them
4485             $val = [$val] unless ref $val eq 'ARRAY';
4486             for my $node (@$val) {
4487                 push (@$lang_vals, $node->{_val})   
4488                     if $node->{_attr} eq $item_lang_attr; 
4489                 push (@$format_vals, $node->{_val})   
4490                     if $node->{_attr} eq $format_attr;
4491             }
4492         }
4493
4494         # fetch the ccvm's for consistency with the {metarecord} blob
4495         $resp->{hold}{formats} = $e->search_config_coded_value_map({
4496             ctype => $format_attr, code => $format_vals});
4497         $resp->{hold}{langs} = $e->search_config_coded_value_map({
4498             ctype => $item_lang_attr, code => $lang_vals});
4499
4500         # find all of the bib records within this metarcord whose 
4501         # format / language match the holdable formats on the hold
4502         my ($bre_ids) = $self->method_lookup(
4503             'open-ils.circ.holds.metarecord.filtered_records')->run(
4504                 $hold->target, $hold->holdable_formats, 
4505                 $hold->selection_ou, $hold->selection_depth);
4506
4507         # now find all of the 'icon' attributes for the records
4508         $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4509         $client->respond($resp);
4510     }
4511
4512     return;
4513 }
4514
4515 __PACKAGE__->register_method(
4516     method        => "copy_has_holds_count",
4517     api_name      => "open-ils.circ.copy.has_holds_count",
4518     authoritative => 1,
4519     signature     => {
4520         desc => q/
4521             Returns the number of holds a paticular copy has
4522         /,
4523         params => [
4524             { desc => 'Authentication Token', type => 'string'},
4525             { desc => 'Copy ID', type => 'number'}
4526         ],
4527         return => {
4528             desc => q/
4529                 Simple count value
4530             /,
4531             type => 'number'
4532         }
4533     }
4534 );
4535
4536 sub copy_has_holds_count {
4537     my( $self, $conn, $auth, $copyid ) = @_;
4538     my $e = new_editor(authtoken=>$auth);
4539     return $e->event unless $e->checkauth;
4540
4541     if( $copyid && $copyid > 0 ) {
4542         my $meth = 'retrieve_action_has_holds_count';
4543         my $data = $e->$meth($copyid);
4544         if($data){
4545                 return $data->count();
4546         }
4547     }
4548     return 0;
4549 }
4550
4551 __PACKAGE__->register_method(
4552     method    => "retrieve_holds_by_usr_notify_value_staff",
4553     api_name  => "open-ils.circ.holds.retrieve_by_notify_staff",
4554     signature => {
4555         desc   => "Retrieve the hold, for the specified user using the notify value.  $ses_is_req_note",
4556         params => [
4557             { desc => 'Authentication token', type => 'string' },
4558             { desc => 'User ID',              type => 'number' },
4559             { desc => 'notify value',         type => 'string' },
4560             { desc => 'notify_type',          type => 'string' }
4561         ],
4562         return => {
4563             desc => 'Hold objects with transits attached, event on error',
4564         }
4565     }
4566 );
4567
4568 sub retrieve_holds_by_usr_notify_value_staff {
4569     
4570     my($self, $conn, $auth, $usr_id, $contact, $cType) = @_;
4571
4572     my $e = new_editor(authtoken=>$auth);
4573     $e->checkauth or return $e->event;
4574
4575     if ($e->requestor->id != $usr_id){
4576         $e->allowed('VIEW_HOLD') or return $e->event;
4577     }
4578
4579     my $q = {
4580         "select" => { "ahr" => ["id", "sms_notify", "phone_notify", "email_notify", "sms_carrier"]},
4581         "from" => "ahr",
4582         "where" => {
4583             "usr"          =>      $usr_id,
4584             "capture_time" =>      undef,
4585             "cancel_time"  =>      undef,
4586             "fulfillment_time" =>  undef,
4587         }
4588     };
4589
4590     if ($cType eq "day_phone" or $cType eq "evening_phone" or
4591         $cType eq "other_phone" or $cType eq "default_phone"){
4592             $q->{where}->{"-not"} = [
4593                 { "phone_notify" => { "=" => $contact} },
4594                 { "phone_notify" => { "<>" => undef } }
4595             ];
4596     }
4597
4598
4599     if ($cType eq "default_sms") {
4600         $q->{where}->{"-not"} = [
4601             { "sms_notify" => { "=" => $contact} },
4602             { "sms_notify" => { "<>" => undef } }
4603         ];
4604     }
4605
4606     if ($cType eq "default_sms_carrier_id") {
4607         $q->{where}->{"-not"} = [
4608             { "sms_carrier" => { "=" => int($contact)} },
4609             { "sms_carrier" => { "<>" => undef } }
4610         ];
4611     }
4612
4613     if ($cType =~ /notify/){
4614         # this is was notification pref change
4615         # we find all unfulfilled holds that match have that pref
4616         my $optr = $contact == 1 ? "<>" : "="; # unless it's email, true val means we want to query for not null
4617         my $conj = $optr eq '=' ? '-or' : '-and';
4618         if ($cType =~ /sms/) {
4619             $q->{where}->{$conj} = [ { sms_notify => { $optr => undef } }, { sms_notify => { $optr => '' } } ];
4620         }
4621         if ($cType =~ /phone/) {
4622             $q->{where}->{$conj} = [ { phone_notify => { $optr => undef } }, { phone_notify => { $optr => '' } } ];
4623         }
4624         if ($cType =~ /email/) {
4625             if ($contact) {
4626                 $q->{where}->{'+ahr'} = 'email_notify';
4627             } else {
4628                 $q->{where}->{'-not'} = {'+ahr' => 'email_notify'};
4629             }
4630         }
4631     }
4632
4633     my $holds = $e->json_query($q);
4634     #$hold_ids = [ map { $_->{id} } @$hold_ids ];
4635
4636     return $holds;
4637 }
4638
4639 __PACKAGE__->register_method(
4640     method    => "batch_update_holds_by_value_staff",
4641     api_name  => "open-ils.circ.holds.batch_update_holds_by_notify_staff",
4642     signature => {
4643         desc   => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
4644         params => [
4645             { desc => 'Authentication token', type => 'string' },
4646             { desc => 'User ID',              type => 'number' },
4647             { desc => 'Hold IDs',             type => 'array'  },
4648             { desc => 'old notify value',     type => 'string' },
4649             { desc => 'new notify value',     type => 'string' },
4650             { desc => 'field name',           type => 'string' },
4651             { desc => 'SMS carrier ID',       type => 'number' }
4652
4653         ],
4654         return => {
4655             desc => 'Hold objects with transits attached, event on error',
4656         }
4657     }
4658 );
4659
4660 sub batch_update_holds_by_value_staff {
4661     my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType, $carrierId) = @_;
4662
4663     my $e = new_editor(authtoken=>$auth, xact=>1);
4664     $e->checkauth or return $e->event;
4665     if ($e->requestor->id != $usr_id){
4666         $e->allowed('UPDATE_HOLD') or return $e->event;
4667     }
4668
4669     my @success;
4670     for my $id (@$hold_ids) {
4671         
4672         my $hold = $e->retrieve_action_hold_request($id);
4673
4674         if ($cType eq "day_phone" or $cType eq "evening_phone" or
4675             $cType eq "other_phone" or $cType eq "default_phone") {
4676
4677             if ($newval eq '') {
4678                 $hold->clear_phone_notify();
4679             }
4680             else {
4681                 $hold->phone_notify($newval);
4682             }
4683         }
4684         
4685         if ($cType eq "default_sms"){
4686             if ($newval eq '') {
4687                 $hold->clear_sms_notify();
4688                 $hold->clear_sms_carrier(); # TODO: prevent orphan sms_carrier, via db trigger
4689             }
4690             else {
4691                 $hold->sms_notify($newval);
4692                 $hold->sms_carrier($carrierId);
4693             }
4694
4695         }
4696
4697         if ($cType eq "default_sms_carrier_id") {
4698             $hold->sms_carrier($newval);
4699         }
4700
4701         if ($cType =~ /notify/){
4702             # this is a notification pref change
4703             if ($cType =~ /email/) { $hold->email_notify($newval); }
4704             if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
4705             if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
4706             # the other case, where x_notify is changed to true,
4707             # is covered by an actual value being assigned
4708         }
4709
4710         $e->update_action_hold_request($hold) or return $e->die_event;
4711         push @success, $id;
4712     }
4713
4714     #$e->disconnect;
4715     $e->commit; #unless $U->event_code($res);
4716     return \@success;
4717
4718 }
4719
4720
4721 __PACKAGE__->register_method(
4722     method    => "retrieve_holds_by_usr_with_notify",
4723     api_name  => "open-ils.circ.holds.retrieve.by_usr.with_notify",
4724     signature => {
4725         desc   => "Retrieve the hold, for the specified user using the notify value.  $ses_is_req_note",
4726         params => [
4727             { desc => 'Authentication token', type => 'string' },
4728             { desc => 'User ID',              type => 'number' },
4729         ],
4730         return => {
4731             desc => 'Lists of holds with notification values, event on error',
4732         }
4733     }
4734 );
4735
4736 sub retrieve_holds_by_usr_with_notify {
4737     
4738     my($self, $conn, $auth, $usr_id) = @_;
4739
4740     my $e = new_editor(authtoken=>$auth);
4741     $e->checkauth or return $e->event;
4742
4743     if ($e->requestor->id != $usr_id){
4744         $e->allowed('VIEW_HOLD') or return $e->event;
4745     }
4746
4747     my $q = {
4748         "select" => { "ahr" => ["id", "phone_notify", "email_notify", "sms_carrier", "sms_notify"]},
4749         "from" => "ahr",
4750         "where" => {
4751             "usr"          =>      $usr_id,
4752             "capture_time" =>      undef,
4753             "cancel_time"  =>      undef,
4754             "fulfillment_time" =>  undef,
4755         }
4756     };
4757
4758     my $holds = $e->json_query($q);
4759     return $holds;
4760 }
4761
4762 __PACKAGE__->register_method(
4763     method    => "batch_update_holds_by_value",
4764     api_name  => "open-ils.circ.holds.batch_update_holds_by_notify",
4765     signature => {
4766         desc   => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
4767         params => [
4768             { desc => 'Authentication token', type => 'string' },
4769             { desc => 'User ID',              type => 'number' },
4770             { desc => 'Hold IDs',             type => 'array'  },
4771             { desc => 'old notify value',     type => 'string' },
4772             { desc => 'new notify value',     type => 'string' },
4773             { desc => 'notify_type',          type => 'string' }
4774         ],
4775         return => {
4776             desc => 'Hold objects with transits attached, event on error',
4777         }
4778     }
4779 );
4780
4781 sub batch_update_holds_by_value {
4782     my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType) = @_;
4783
4784     my $e = new_editor(authtoken=>$auth, xact=>1);
4785     $e->checkauth or return $e->event;
4786     if ($e->requestor->id != $usr_id){
4787         $e->allowed('UPDATE_HOLD') or return $e->event;
4788     }
4789
4790     my @success;
4791     for my $id (@$hold_ids) {
4792         
4793         my $hold = $e->retrieve_action_hold_request(int($id));
4794
4795         if ($cType eq "day_phone" or $cType eq "evening_phone" or
4796             $cType eq "other_phone" or $cType eq "default_phone") {
4797             # change phone number value on hold
4798             $hold->phone_notify($newval);
4799         }
4800         if ($cType eq "default_sms") {
4801             # change SMS number value on hold
4802             $hold->sms_notify($newval);
4803         }
4804
4805         if ($cType eq "default_sms_carrier_id") {
4806             $hold->sms_carrier(int($newval));
4807         }
4808
4809         if ($cType =~ /notify/){
4810             # this is a notification pref change
4811             if ($cType =~ /email/) { $hold->email_notify($newval); }
4812             if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
4813             if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
4814             # the other case, where x_notify is changed to true,
4815             # is covered by an actual value being assigned
4816         }
4817
4818         $e->update_action_hold_request($hold) or return $e->die_event;
4819         push @success, $id;
4820     }
4821
4822     #$e->disconnect;
4823     $e->commit; #unless $U->event_code($res);
4824     return \@success;
4825 }
4826
4827 __PACKAGE__->register_method(
4828     method        => "hold_metadata",
4829     api_name      => "open-ils.circ.hold.get_metadata",
4830     authoritative => 1,
4831     stream => 1,
4832     signature     => {
4833         desc => q/
4834             Returns a stream of objects containing whatever bib, 
4835             volume, etc. data is available to the specific hold 
4836             type and target.
4837         /,
4838         params => [
4839             {desc => 'Hold Type', type => 'string'},
4840             {desc => 'Hold Target(s)', type => 'number or array'},
4841             {desc => 'Context org unit (optional)', type => 'number'}
4842         ],
4843         return => {
4844             desc => q/
4845                 Stream of hold metadata objects.
4846             /,
4847             type => 'object'
4848         }
4849     }
4850 );
4851
4852
4853 sub hold_metadata {
4854     my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
4855
4856     $hold_targets = [$hold_targets] unless ref $hold_targets;
4857
4858     my $e = new_editor();
4859     for my $target (@$hold_targets) {
4860
4861         # create a dummy hold for find_hold_mvr
4862         my $hold = Fieldmapper::action::hold_request->new;
4863         $hold->hold_type($hold_type);
4864         $hold->target($target);
4865
4866         my (undef, $volume, $copy, $issuance, $part, $bre, $metarecord) = 
4867             find_hold_mvr($e, $hold, {suppress_mvr => 1});
4868
4869         $bre->clear_marc; # avoid bulk
4870
4871         my $meta = {
4872             target => $target,
4873             copy => $copy,
4874             volume => $volume,
4875             issuance => $issuance,
4876             part => $part,
4877             bibrecord => $bre,
4878             metarecord => $metarecord,
4879             metarecord_filters => {}
4880         };
4881
4882         # If this is a bib hold or metarecord hold, also return the
4883         # available set of MR filters (AKA "Holdable Formats") for the
4884         # hold.  For bib holds these may be used to upgrade the hold
4885         # from a bib to metarecord hold.
4886         if ($hold_type eq 'T') {
4887             my $map = $e->search_metabib_metarecord_source_map(
4888                 {source => $meta->{bibrecord}->id})->[0];
4889
4890             if ($map) {
4891                 $meta->{metarecord} = 
4892                     $e->retrieve_metabib_metarecord($map->metarecord);
4893             }
4894         }
4895
4896         if ($meta->{metarecord}) {
4897
4898             my ($filters) = 
4899                 $self->method_lookup('open-ils.circ.mmr.holds.filters')
4900                     ->run($meta->{metarecord}->id, $org_id);
4901
4902             if ($filters) {
4903                 $meta->{metarecord_filters} = $filters->{metarecord};
4904             }
4905         }
4906
4907         $client->respond($meta);
4908     }
4909
4910     return undef;
4911 }
4912
4913 1;