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