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