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