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