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