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