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