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