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