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