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