]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
merging MR holds topic branch
[working/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 => q/Returns object with total_holds count, queue_position, potential_copies count, and status code/,
969     }
970 );
971
972 sub retrieve_hold_queue_stats {
973     my($self, $conn, $auth, $hold_id) = @_;
974         my $e = new_editor(authtoken => $auth);
975         return $e->event unless $e->checkauth;
976         my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
977         if($e->requestor->id != $hold->usr) {
978                 return $e->event unless $e->allowed('VIEW_HOLD');
979         }
980     return retrieve_hold_queue_status_impl($e, $hold);
981 }
982
983 sub retrieve_hold_queue_status_impl {
984     my $e = shift;
985     my $hold = shift;
986
987     # The holds queue is defined as the distinct set of holds that share at 
988     # least one potential copy with the context hold, plus any holds that
989     # share the same hold type and target.  The latter part exists to
990     # accomodate holds that currently have no potential copies
991     my $q_holds = $e->json_query({
992
993         # fetch request_time since it's in the order_by and we're asking for distinct values
994         select => {ahr => ['id', 'request_time']},
995         from   => {
996             ahr => {
997                 ahcm => {type => 'left'} # there may be no copy maps 
998             }
999         },
1000         order_by => {ahr => ['request_time']},
1001         distinct => 1,
1002         where    => {
1003             '-or' => [
1004                 {
1005                     '+ahcm' => {
1006                         target_copy => {
1007                             in => {
1008                                 select => {ahcm => ['target_copy']},
1009                                 from   => 'ahcm',
1010                                 where  => {hold => $hold->id}
1011                             } 
1012                         } 
1013                     }
1014                 },
1015                 {
1016                     '+ahr' => {
1017                         hold_type => $hold->hold_type,
1018                         target    => $hold->target
1019                     }
1020                 }
1021             ]
1022         }, 
1023     });
1024
1025     my $qpos = 1;
1026     for my $h (@$q_holds) {
1027         last if $h->{id} == $hold->id;
1028         $qpos++;
1029     }
1030
1031     # total count of potential copies
1032     my $num_potentials = $e->json_query({
1033         select => {ahcm => [{column => 'id', transform => 'count', alias => 'count'}]},
1034         from => 'ahcm',
1035         where => {hold => $hold->id}
1036     })->[0];
1037
1038     my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1039     my $default_hold_interval = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
1040     my $estimated_wait = $qpos * ($default_hold_interval / $num_potentials) if $default_hold_interval;
1041
1042     return {
1043         total_holds      => scalar(@$q_holds),
1044         queue_position   => $qpos,
1045         potential_copies => $num_potentials->{count},
1046         status           => _hold_status( $e, $hold ),
1047         estimated_wait   => int($estimated_wait)
1048     };
1049 }
1050
1051
1052 sub fetch_open_hold_by_current_copy {
1053         my $class = shift;
1054         my $copyid = shift;
1055         my $hold = $apputils->simplereq(
1056                 'open-ils.cstore', 
1057                 'open-ils.cstore.direct.action.hold_request.search.atomic',
1058                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
1059         return $hold->[0] if ref($hold);
1060         return undef;
1061 }
1062
1063 sub fetch_related_holds {
1064         my $class = shift;
1065         my $copyid = shift;
1066         return $apputils->simplereq(
1067                 'open-ils.cstore', 
1068                 'open-ils.cstore.direct.action.hold_request.search.atomic',
1069                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
1070 }
1071
1072
1073 __PACKAGE__->register_method(
1074     method    => "hold_pull_list",
1075     api_name  => "open-ils.circ.hold_pull_list.retrieve",
1076     signature => {
1077         desc   => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1078                   'The location is determined by the login session.',
1079         params => [
1080             { desc => 'Limit (optional)',  type => 'number'},
1081             { desc => 'Offset (optional)', type => 'number'},
1082         ],
1083         return => {
1084             desc => 'reference to a list of holds, or event on failure',
1085         }
1086     }
1087 );
1088
1089 __PACKAGE__->register_method(
1090     method    => "hold_pull_list",
1091     api_name  => "open-ils.circ.hold_pull_list.id_list.retrieve",
1092     signature => {
1093         desc   => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1094                   'The location is determined by the login session.',
1095         params => [
1096             { desc => 'Limit (optional)',  type => 'number'},
1097             { desc => 'Offset (optional)', type => 'number'},
1098         ],
1099         return => {
1100             desc => 'reference to a list of holds, or event on failure',
1101         }
1102     }
1103 );
1104
1105 __PACKAGE__->register_method(
1106     method    => "hold_pull_list",
1107     api_name  => "open-ils.circ.hold_pull_list.retrieve.count",
1108     signature => {
1109         desc   => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1110                   'The location is determined by the login session.',
1111         params => [
1112             { desc => 'Limit (optional)',  type => 'number'},
1113             { desc => 'Offset (optional)', type => 'number'},
1114         ],
1115         return => {
1116             desc => 'Holds count (integer), or event on failure',
1117             # type => 'number'
1118         }
1119     }
1120 );
1121
1122
1123 sub hold_pull_list {
1124         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1125         my( $reqr, $evt ) = $U->checkses($authtoken);
1126         return $evt if $evt;
1127
1128         my $org = $reqr->ws_ou || $reqr->home_ou;
1129         # the perm locaiton shouldn't really matter here since holds
1130         # will exist all over and VIEW_HOLDS should be universal
1131         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1132         return $evt if $evt;
1133
1134     if($self->api_name =~ /count/) {
1135
1136                 my $count = $U->storagereq(
1137                         'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1138                         $org, $limit, $offset ); 
1139
1140         $logger->info("Grabbing pull list for org unit $org with $count items");
1141         return $count;
1142
1143     } elsif( $self->api_name =~ /id_list/ ) {
1144                 return $U->storagereq(
1145                         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1146                         $org, $limit, $offset ); 
1147
1148         } else {
1149                 return $U->storagereq(
1150                         'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1151                         $org, $limit, $offset ); 
1152         }
1153 }
1154
1155 __PACKAGE__->register_method(
1156     method        => 'fetch_hold_notify',
1157     api_name      => 'open-ils.circ.hold_notification.retrieve_by_hold',
1158     authoritative => 1,
1159     signature     => q/ 
1160 Returns a list of hold notification objects based on hold id.
1161 @param authtoken The loggin session key
1162 @param holdid The id of the hold whose notifications we want to retrieve
1163 @return An array of hold notification objects, event on error.
1164 /
1165 );
1166
1167 sub fetch_hold_notify {
1168         my( $self, $conn, $authtoken, $holdid ) = @_;
1169         my( $requestor, $evt ) = $U->checkses($authtoken);
1170         return $evt if $evt;
1171         my ($hold, $patron);
1172         ($hold, $evt) = $U->fetch_hold($holdid);
1173         return $evt if $evt;
1174         ($patron, $evt) = $U->fetch_user($hold->usr);
1175         return $evt if $evt;
1176
1177         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
1178         return $evt if $evt;
1179
1180         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
1181         return $U->cstorereq(
1182                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
1183 }
1184
1185
1186 __PACKAGE__->register_method(
1187     method    => 'create_hold_notify',
1188     api_name  => 'open-ils.circ.hold_notification.create',
1189     signature => q/
1190 Creates a new hold notification object
1191 @param authtoken The login session key
1192 @param notification The hold notification object to create
1193 @return ID of the new object on success, Event on error
1194 /
1195 );
1196
1197 sub create_hold_notify {
1198    my( $self, $conn, $auth, $note ) = @_;
1199    my $e = new_editor(authtoken=>$auth, xact=>1);
1200    return $e->die_event unless $e->checkauth;
1201
1202    my $hold = $e->retrieve_action_hold_request($note->hold)
1203       or return $e->die_event;
1204    my $patron = $e->retrieve_actor_user($hold->usr) 
1205       or return $e->die_event;
1206
1207    return $e->die_event unless 
1208       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1209
1210         $note->notify_staff($e->requestor->id);
1211    $e->create_action_hold_notification($note) or return $e->die_event;
1212    $e->commit;
1213    return $note->id;
1214 }
1215
1216 __PACKAGE__->register_method(
1217     method    => 'create_hold_note',
1218     api_name  => 'open-ils.circ.hold_note.create',
1219     signature => q/
1220                 Creates a new hold request note object
1221                 @param authtoken The login session key
1222                 @param note The hold note object to create
1223                 @return ID of the new object on success, Event on error
1224                 /
1225 );
1226
1227 sub create_hold_note {
1228    my( $self, $conn, $auth, $note ) = @_;
1229    my $e = new_editor(authtoken=>$auth, xact=>1);
1230    return $e->die_event unless $e->checkauth;
1231
1232    my $hold = $e->retrieve_action_hold_request($note->hold)
1233       or return $e->die_event;
1234    my $patron = $e->retrieve_actor_user($hold->usr) 
1235       or return $e->die_event;
1236
1237    return $e->die_event unless 
1238       $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn.  Probably want something more specific
1239
1240    $e->create_action_hold_request_note($note) or return $e->die_event;
1241    $e->commit;
1242    return $note->id;
1243 }
1244
1245 __PACKAGE__->register_method(
1246     method    => 'reset_hold',
1247     api_name  => 'open-ils.circ.hold.reset',
1248     signature => q/
1249                 Un-captures and un-targets a hold, essentially returning
1250                 it to the state it was in directly after it was placed,
1251                 then attempts to re-target the hold
1252                 @param authtoken The login session key
1253                 @param holdid The id of the hold
1254         /
1255 );
1256
1257
1258 sub reset_hold {
1259         my( $self, $conn, $auth, $holdid ) = @_;
1260         my $reqr;
1261         my ($hold, $evt) = $U->fetch_hold($holdid);
1262         return $evt if $evt;
1263         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1264         return $evt if $evt;
1265         $evt = _reset_hold($self, $reqr, $hold);
1266         return $evt if $evt;
1267         return 1;
1268 }
1269
1270
1271 __PACKAGE__->register_method(
1272     method   => 'reset_hold_batch',
1273     api_name => 'open-ils.circ.hold.reset.batch'
1274 );
1275
1276 sub reset_hold_batch {
1277     my($self, $conn, $auth, $hold_ids) = @_;
1278
1279     my $e = new_editor(authtoken => $auth);
1280     return $e->event unless $e->checkauth;
1281
1282     for my $hold_id ($hold_ids) {
1283
1284         my $hold = $e->retrieve_action_hold_request(
1285             [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}]) 
1286             or return $e->event;
1287
1288             next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1289         _reset_hold($self, $e->requestor, $hold);
1290     }
1291
1292     return 1;
1293 }
1294
1295
1296 sub _reset_hold {
1297         my ($self, $reqr, $hold) = @_;
1298
1299         my $e = new_editor(xact =>1, requestor => $reqr);
1300
1301         $logger->info("reseting hold ".$hold->id);
1302
1303         my $hid = $hold->id;
1304
1305         if( $hold->capture_time and $hold->current_copy ) {
1306
1307                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1308                         or return $e->event;
1309
1310                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1311                         $logger->info("setting copy to status 'reshelving' on hold retarget");
1312                         $copy->status(OILS_COPY_STATUS_RESHELVING);
1313                         $copy->editor($e->requestor->id);
1314                         $copy->edit_date('now');
1315                         $e->update_asset_copy($copy) or return $e->event;
1316
1317                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1318
1319                         # We don't want the copy to remain "in transit"
1320                         $copy->status(OILS_COPY_STATUS_RESHELVING);
1321                         $logger->warn("! reseting hold [$hid] that is in transit");
1322                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1323
1324                         if( $transid ) {
1325                                 my $trans = $e->retrieve_action_transit_copy($transid);
1326                                 if( $trans ) {
1327                                         $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1328                                         my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
1329                                         $logger->info("Transit abort completed with result $evt");
1330                                         return $evt unless "$evt" eq 1;
1331                                 }
1332                         }
1333                 }
1334         }
1335
1336         $hold->clear_capture_time;
1337         $hold->clear_current_copy;
1338         $hold->clear_shelf_time;
1339         $hold->clear_shelf_expire_time;
1340
1341         $e->update_action_hold_request($hold) or return $e->event;
1342         $e->commit;
1343
1344         $U->storagereq(
1345                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1346
1347         return undef;
1348 }
1349
1350
1351 __PACKAGE__->register_method(
1352     method    => 'fetch_open_title_holds',
1353     api_name  => 'open-ils.circ.open_holds.retrieve',
1354     signature => q/
1355                 Returns a list ids of un-fulfilled holds for a given title id
1356                 @param authtoken The login session key
1357                 @param id the id of the item whose holds we want to retrieve
1358                 @param type The hold type - M, T, V, C
1359         /
1360 );
1361
1362 sub fetch_open_title_holds {
1363         my( $self, $conn, $auth, $id, $type, $org ) = @_;
1364         my $e = new_editor( authtoken => $auth );
1365         return $e->event unless $e->checkauth;
1366
1367         $type ||= "T";
1368         $org  ||= $e->requestor->ws_ou;
1369
1370 #       return $e->search_action_hold_request(
1371 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
1372
1373         # XXX make me return IDs in the future ^--
1374         my $holds = $e->search_action_hold_request(
1375                 { 
1376                         target                          => $id, 
1377                         cancel_time                     => undef, 
1378                         hold_type                       => $type, 
1379                         fulfillment_time        => undef 
1380                 }
1381         );
1382
1383         flesh_hold_transits($holds);
1384         return $holds;
1385 }
1386
1387
1388 sub flesh_hold_transits {
1389         my $holds = shift;
1390         for my $hold ( @$holds ) {
1391                 $hold->transit(
1392                         $apputils->simplereq(
1393                                 'open-ils.cstore',
1394                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
1395                                 { hold => $hold->id },
1396                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
1397                         )->[0]
1398                 );
1399         }
1400 }
1401
1402 sub flesh_hold_notices {
1403         my( $holds, $e ) = @_;
1404         $e ||= new_editor();
1405
1406         for my $hold (@$holds) {
1407                 my $notices = $e->search_action_hold_notification(
1408                         [
1409                                 { hold => $hold->id },
1410                                 { order_by => { anh => 'notify_time desc' } },
1411                         ],
1412                         {idlist=>1}
1413                 );
1414
1415                 $hold->notify_count(scalar(@$notices));
1416                 if( @$notices ) {
1417                         my $n = $e->retrieve_action_hold_notification($$notices[0])
1418                                 or return $e->event;
1419                         $hold->notify_time($n->notify_time);
1420                 }
1421         }
1422 }
1423
1424
1425 __PACKAGE__->register_method(
1426     method    => 'fetch_captured_holds',
1427     api_name  => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1428     stream    => 1,
1429     signature => q/
1430                 Returns a list of un-fulfilled holds for a given title id
1431                 @param authtoken The login session key
1432                 @param org The org id of the location in question
1433         /
1434 );
1435
1436 __PACKAGE__->register_method(
1437     method    => 'fetch_captured_holds',
1438     api_name  => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1439     stream    => 1,
1440     signature => q/
1441                 Returns a list ids of un-fulfilled holds for a given title id
1442                 @param authtoken The login session key
1443                 @param org The org id of the location in question
1444         /
1445 );
1446
1447 sub fetch_captured_holds {
1448         my( $self, $conn, $auth, $org ) = @_;
1449
1450         my $e = new_editor(authtoken => $auth);
1451         return $e->event unless $e->checkauth;
1452         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1453
1454         $org ||= $e->requestor->ws_ou;
1455
1456     my $hold_ids = $e->json_query(
1457         { 
1458             select => { ahr => ['id'] },
1459             from   => {
1460                 ahr => {
1461                     acp => {
1462                         field => 'id',
1463                         fkey  => 'current_copy'
1464                     },
1465                 }
1466             }, 
1467             where => {
1468                 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1469                 '+ahr' => {
1470                     capture_time     => { "!=" => undef },
1471                     current_copy     => { "!=" => undef },
1472                     fulfillment_time => undef,
1473                     pickup_lib       => $org,
1474                     cancel_time      => undef,
1475                   }
1476             }
1477         },
1478     );
1479
1480     for my $hold_id (@$hold_ids) {
1481         if($self->api_name =~ /id_list/) {
1482             $conn->respond($hold_id->{id});
1483             next;
1484         } else {
1485             $conn->respond(
1486                 $e->retrieve_action_hold_request([
1487                     $hold_id->{id},
1488                     {
1489                         flesh => 1,
1490                         flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
1491                         order_by => {anh => 'notify_time desc'}
1492                     }
1493                 ])
1494             );
1495         }
1496     }
1497
1498     return undef;
1499 }
1500
1501
1502 __PACKAGE__->register_method(
1503     method    => "check_title_hold",
1504     api_name  => "open-ils.circ.title_hold.is_possible",
1505     signature => {
1506         desc  => 'Determines if a hold were to be placed by a given user, ' .
1507              'whether or not said hold would have any potential copies to fulfill it.' .
1508              'The named paramaters of the second argument include: ' .
1509              'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
1510              'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' , 
1511         params => [
1512             { desc => 'Authentication token',     type => 'string'},
1513             { desc => 'Hash of named parameters', type => 'object'},
1514         ],
1515         return => {
1516             desc => 'List of new message IDs (empty if none)',
1517             type => 'array'
1518         }
1519     }
1520 );
1521
1522 =head3 check_title_hold (token, hash)
1523
1524 The named fields in the hash are: 
1525
1526  patronid     - ID of the hold recipient  (required)
1527  depth        - hold range depth          (default 0)
1528  pickup_lib   - destination for hold, fallback value for selection_ou
1529  selection_ou - ID of org_unit establishing hard and soft hold boundary settings
1530  titleid      - ID (BRN) of the title to be held, required for Title level hold
1531  volume_id    - required for Volume level hold
1532  copy_id      - required for Copy level hold
1533  mrid         - required for Meta-record level hold
1534  hold_type    - T,C,V or M for Title, Copy, Volume or Meta-record  (default "T")
1535
1536 All key/value pairs are passed on to do_possibility_checks.
1537
1538 =cut
1539
1540 # FIXME: better params checking.  what other params are required, if any?
1541 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
1542 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still 
1543 # used in conditionals, where it may be undefined, causing a warning.
1544 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
1545
1546 sub check_title_hold {
1547     my( $self, $client, $authtoken, $params ) = @_;
1548     my $e = new_editor(authtoken=>$authtoken);
1549     return $e->event unless $e->checkauth;
1550
1551     my %params       = %$params;
1552     my $depth        = $params{depth}        || 0;
1553     my $selection_ou = $params{selection_ou} || $params{pickup_lib};
1554
1555         my $patron = $e->retrieve_actor_user($params{patronid})
1556                 or return $e->event;
1557
1558         if( $e->requestor->id ne $patron->id ) {
1559                 return $e->event unless 
1560                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1561         }
1562
1563         return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1564
1565         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1566                 or return $e->event;
1567
1568     my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1569     my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1570
1571     if(defined $soft_boundary and $depth < $soft_boundary) {
1572         # work up the tree and as soon as we find a potential copy, use that depth
1573         # also, make sure we don't go past the hard boundary if it exists
1574
1575         # our min boundary is the greater of user-specified boundary or hard boundary
1576         my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?  
1577             $hard_boundary : $depth;
1578
1579         my $depth = $soft_boundary;
1580         while($depth >= $min_depth) {
1581             $logger->info("performing hold possibility check with soft boundary $depth");
1582             my @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1583             return {success => 1, depth => $depth, local_avail => $status[1]} if $status[0];
1584             $depth--;
1585         }
1586     } elsif(defined $hard_boundary and $depth < $hard_boundary) {
1587         # there is no soft boundary, enforce the hard boundary if it exists
1588         $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1589         my @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
1590         if($status[0]) {
1591             return {success => 1, depth => $hard_boundary, local_avail => $status[1]}
1592         }
1593     } else {
1594         # no boundaries defined, fall back to user specifed boundary or no boundary
1595         $logger->info("performing hold possibility check with no boundary");
1596         my @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
1597         if($status[0]) {
1598             return {success => 1, depth => $hard_boundary, local_avail => $status[1]};
1599         }
1600     }
1601     return {success => 0};
1602 }
1603
1604 sub do_possibility_checks {
1605     my($e, $patron, $request_lib, $depth, %params) = @_;
1606
1607     my $titleid      = $params{titleid}      || "";
1608     my $volid        = $params{volume_id};
1609     my $copyid       = $params{copy_id};
1610     my $mrid         = $params{mrid}         || "";
1611     my $pickup_lib   = $params{pickup_lib};
1612     my $hold_type    = $params{hold_type}    || 'T';
1613     my $selection_ou = $params{selection_ou} || $pickup_lib;
1614
1615
1616         my $copy;
1617         my $volume;
1618         my $title;
1619
1620         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1621
1622         return $e->event unless $copy   = $e->retrieve_asset_copy($copyid);
1623         return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
1624         return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
1625
1626         return verify_copy_for_hold( 
1627             $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib
1628         );
1629
1630         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1631
1632                 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
1633                 return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
1634
1635                 return _check_volume_hold_is_possible(
1636                         $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
1637         );
1638
1639         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1640
1641                 return _check_title_hold_is_possible(
1642                         $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
1643         );
1644
1645         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1646
1647                 my $maps = $e->search_metabib_metarecord_source_map({metarecord=>$mrid});
1648                 my @recs = map { $_->source } @$maps;
1649                 for my $rec (@recs) {
1650             my @status = _check_title_hold_is_possible(
1651                                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1652             return @status if $status[1];
1653                 }
1654                 return (0);     
1655         }
1656 #   else { Unrecognized hold_type ! }   # FIXME: return error? or 0?
1657 }
1658
1659 my %prox_cache;
1660 sub create_ranged_org_filter {
1661     my($e, $selection_ou, $depth) = @_;
1662
1663     # find the orgs from which this hold may be fulfilled, 
1664     # based on the selection_ou and depth
1665
1666     my $top_org = $e->search_actor_org_unit([
1667         {parent_ou => undef}, 
1668         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1669     my %org_filter;
1670
1671     return () if $depth == $top_org->ou_type->depth;
1672
1673     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1674     %org_filter = (circ_lib => []);
1675     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1676
1677     $logger->info("hold org filter at depth $depth and selection_ou ".
1678         "$selection_ou created list of @{$org_filter{circ_lib}}");
1679
1680     return %org_filter;
1681 }
1682
1683
1684 sub _check_title_hold_is_possible {
1685     my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1686    
1687     my $e = new_editor();
1688     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1689
1690     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1691     my $copies = $e->json_query(
1692         { 
1693             select => { acp => ['id', 'circ_lib'] },
1694               from => {
1695                 acp => {
1696                     acn => {
1697                         field  => 'id',
1698                         fkey   => 'call_number',
1699                         'join' => {
1700                             bre => {
1701                                 field  => 'id',
1702                                 filter => { id => $titleid },
1703                                 fkey   => 'record'
1704                             }
1705                         }
1706                     },
1707                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1708                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
1709                 }
1710             }, 
1711             where => {
1712                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1713             }
1714         }
1715     );
1716
1717     $logger->info("title possible found ".scalar(@$copies)." potential copies");
1718     return (0) unless @$copies;
1719
1720     # -----------------------------------------------------------------------
1721     # sort the copies into buckets based on their circ_lib proximity to 
1722     # the patron's home_ou.  
1723     # -----------------------------------------------------------------------
1724
1725     my $home_org = $patron->home_ou;
1726     my $req_org = $request_lib->id;
1727
1728     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1729
1730     $prox_cache{$home_org} = 
1731         $e->search_actor_org_unit_proximity({from_org => $home_org})
1732         unless $prox_cache{$home_org};
1733     my $home_prox = $prox_cache{$home_org};
1734
1735     my %buckets;
1736     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1737     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1738
1739     my @keys = sort { $a <=> $b } keys %buckets;
1740
1741
1742     if( $home_org ne $req_org ) {
1743       # -----------------------------------------------------------------------
1744       # shove the copies close to the request_lib into the primary buckets 
1745       # directly before the farthest away copies.  That way, they are not 
1746       # given priority, but they are checked before the farthest copies.
1747       # -----------------------------------------------------------------------
1748         $prox_cache{$req_org} = 
1749             $e->search_actor_org_unit_proximity({from_org => $req_org})
1750             unless $prox_cache{$req_org};
1751         my $req_prox = $prox_cache{$req_org};
1752
1753         my %buckets2;
1754         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1755         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1756
1757         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1758         my $new_key = $highest_key - 0.5; # right before the farthest prox
1759         my @keys2   = sort { $a <=> $b } keys %buckets2;
1760         for my $key (@keys2) {
1761             last if $key >= $highest_key;
1762             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1763         }
1764     }
1765
1766     @keys = sort { $a <=> $b } keys %buckets;
1767
1768     my $title;
1769     my %seen;
1770     for my $key (@keys) {
1771       my @cps = @{$buckets{$key}};
1772
1773       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1774
1775       for my $copyid (@cps) {
1776
1777          next if $seen{$copyid};
1778          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1779          my $copy = $e->retrieve_asset_copy($copyid);
1780          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1781
1782          unless($title) { # grab the title if we don't already have it
1783             my $vol = $e->retrieve_asset_call_number(
1784                [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1785             $title = $vol->record;
1786          }
1787    
1788          my @status = verify_copy_for_hold( 
1789             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1790
1791         return @status if $status[0];
1792       }
1793     }
1794
1795     return (0);
1796 }
1797
1798
1799 sub _check_volume_hold_is_possible {
1800         my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1801     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1802         my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1803         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1804         for my $copy ( @$copies ) {
1805         my @status = verify_copy_for_hold( 
1806                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1807         return @status if $status[0];
1808         }
1809         return (0);
1810 }
1811
1812
1813
1814 sub verify_copy_for_hold {
1815         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1816         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1817     my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1818                 {       patron                          => $patron, 
1819                         requestor                       => $requestor, 
1820                         copy                            => $copy,
1821                         title                           => $title, 
1822                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1823                         pickup_lib                      => $pickup_lib,
1824                         request_lib                     => $request_lib,
1825             new_hold            => 1
1826                 } 
1827         );
1828
1829     return (
1830         $permitted,
1831         (
1832                 ($copy->circ_lib == $pickup_lib) and 
1833             ($copy->status == OILS_COPY_STATUS_AVAILABLE)
1834         )
1835     );
1836 }
1837
1838
1839
1840 sub find_nearest_permitted_hold {
1841
1842     my $class  = shift;
1843     my $editor = shift;     # CStoreEditor object
1844     my $copy   = shift;     # copy to target
1845     my $user   = shift;     # staff
1846     my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1847       
1848     my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1849
1850     my $bc = $copy->barcode;
1851
1852         # find any existing holds that already target this copy
1853         my $old_holds = $editor->search_action_hold_request(
1854                 {       current_copy => $copy->id, 
1855                         cancel_time  => undef, 
1856                         capture_time => undef 
1857                 } 
1858         );
1859
1860         # hold->type "R" means we need this copy
1861         for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1862
1863
1864     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1865
1866         $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1867         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1868
1869         # search for what should be the best holds for this copy to fulfill
1870         my $best_holds = $U->storagereq(
1871                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1872                 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1873
1874         unless(@$best_holds) {
1875
1876                 if( my $hold = $$old_holds[0] ) {
1877                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1878                         return ($hold);
1879                 }
1880
1881                 $logger->info("circulator: no suitable holds found for copy $bc");
1882                 return (undef, $evt);
1883         }
1884
1885
1886         my $best_hold;
1887
1888         # for each potential hold, we have to run the permit script
1889         # to make sure the hold is actually permitted.
1890         for my $holdid (@$best_holds) {
1891                 next unless $holdid;
1892                 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1893
1894                 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1895                 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1896                 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1897
1898                 # see if this hold is permitted
1899                 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1900                         {       patron_id                       => $hold->usr,
1901                                 requestor                       => $reqr,
1902                                 copy                            => $copy,
1903                                 pickup_lib                      => $hold->pickup_lib,
1904                                 request_lib                     => $rlib,
1905                         } 
1906                 );
1907
1908                 if( $permitted ) {
1909                         $best_hold = $hold;
1910                         last;
1911                 }
1912         }
1913
1914
1915         unless( $best_hold ) { # no "good" permitted holds were found
1916                 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1917                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1918                         return ($hold);
1919                 }
1920
1921                 # we got nuthin
1922                 $logger->info("circulator: no suitable holds found for copy $bc");
1923                 return (undef, $evt);
1924         }
1925
1926         $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1927
1928         # indicate a permitted hold was found
1929         return $best_hold if $check_only;
1930
1931         # we've found a permitted hold.  we need to "grab" the copy 
1932         # to prevent re-targeted holds (next part) from re-grabbing the copy
1933         $best_hold->current_copy($copy->id);
1934         $editor->update_action_hold_request($best_hold) 
1935                 or return (undef, $editor->event);
1936
1937
1938     my @retarget;
1939
1940         # re-target any other holds that already target this copy
1941         for my $old_hold (@$old_holds) {
1942                 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1943                 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1944             $old_hold->id." after a better hold [".$best_hold->id."] was found");
1945         $old_hold->clear_current_copy;
1946         $old_hold->clear_prev_check_time;
1947         $editor->update_action_hold_request($old_hold) 
1948             or return (undef, $editor->event);
1949         push(@retarget, $old_hold->id);
1950         }
1951
1952         return ($best_hold, undef, (@retarget) ? \@retarget : undef);
1953 }
1954
1955
1956
1957
1958
1959
1960 __PACKAGE__->register_method(
1961     method   => 'all_rec_holds',
1962     api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1963 );
1964
1965 sub all_rec_holds {
1966         my( $self, $conn, $auth, $title_id, $args ) = @_;
1967
1968         my $e = new_editor(authtoken=>$auth);
1969         $e->checkauth or return $e->event;
1970         $e->allowed('VIEW_HOLD') or return $e->event;
1971
1972         $args ||= {};
1973     $args->{fulfillment_time} = undef; #  we don't want to see old fulfilled holds
1974         $args->{cancel_time} = undef;
1975
1976         my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1977
1978     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1979     if($mr_map) {
1980         $resp->{metarecord_holds} = $e->search_action_hold_request(
1981             {   hold_type => OILS_HOLD_TYPE_METARECORD,
1982                 target => $mr_map->metarecord,
1983                 %$args 
1984             }, {idlist => 1}
1985         );
1986     }
1987
1988         $resp->{title_holds} = $e->search_action_hold_request(
1989                 { 
1990                         hold_type => OILS_HOLD_TYPE_TITLE, 
1991                         target => $title_id, 
1992                         %$args 
1993                 }, {idlist=>1} );
1994
1995         my $vols = $e->search_asset_call_number(
1996                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1997
1998         return $resp unless @$vols;
1999
2000         $resp->{volume_holds} = $e->search_action_hold_request(
2001                 { 
2002                         hold_type => OILS_HOLD_TYPE_VOLUME, 
2003                         target => $vols,
2004                         %$args }, 
2005                 {idlist=>1} );
2006
2007         my $copies = $e->search_asset_copy(
2008                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
2009
2010         return $resp unless @$copies;
2011
2012         $resp->{copy_holds} = $e->search_action_hold_request(
2013                 { 
2014                         hold_type => OILS_HOLD_TYPE_COPY,
2015                         target => $copies,
2016                         %$args }, 
2017                 {idlist=>1} );
2018
2019         return $resp;
2020 }
2021
2022
2023
2024
2025
2026 __PACKAGE__->register_method(
2027     method        => 'uber_hold',
2028     authoritative => 1,
2029     api_name      => 'open-ils.circ.hold.details.retrieve'
2030 );
2031
2032 sub uber_hold {
2033         my($self, $client, $auth, $hold_id) = @_;
2034         my $e = new_editor(authtoken=>$auth);
2035         $e->checkauth or return $e->event;
2036     return uber_hold_impl($e, $hold_id);
2037 }
2038
2039 __PACKAGE__->register_method(
2040     method        => 'batch_uber_hold',
2041     authoritative => 1,
2042     stream        => 1,
2043     api_name      => 'open-ils.circ.hold.details.batch.retrieve'
2044 );
2045
2046 sub batch_uber_hold {
2047         my($self, $client, $auth, $hold_ids) = @_;
2048         my $e = new_editor(authtoken=>$auth);
2049         $e->checkauth or return $e->event;
2050     $client->respond(uber_hold_impl($e, $_)) for @$hold_ids;
2051     return undef;
2052 }
2053
2054 sub uber_hold_impl {
2055     my($e, $hold_id) = @_;
2056
2057         my $resp = {};
2058
2059         my $hold = $e->retrieve_action_hold_request(
2060                 [
2061                         $hold_id,
2062                         {
2063                                 flesh => 1,
2064                                 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
2065                         }
2066                 ]
2067         ) or return $e->event;
2068
2069     if($hold->usr->id ne $e->requestor->id) {
2070         # A user is allowed to see his/her own holds
2071             $e->allowed('VIEW_HOLD') or return $e->event;
2072     }
2073
2074         my $user = $hold->usr;
2075         $hold->usr($user->id);
2076
2077         my $card = $e->retrieve_actor_card($user->card)
2078                 or return $e->event;
2079
2080         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
2081
2082         flesh_hold_notices([$hold], $e);
2083         flesh_hold_transits([$hold]);
2084
2085     my $details = retrieve_hold_queue_status_impl($e, $hold);
2086
2087     return {
2088         hold           => $hold,
2089         copy           => $copy,
2090         volume         => $volume,
2091         mvr            => $mvr,
2092         patron_first   => $user->first_given_name,
2093         patron_last    => $user->family_name,
2094         patron_barcode => $card->barcode,
2095         %$details
2096     };
2097 }
2098
2099
2100
2101 # -----------------------------------------------------
2102 # Returns the MVR object that represents what the
2103 # hold is all about
2104 # -----------------------------------------------------
2105 sub find_hold_mvr {
2106         my( $e, $hold ) = @_;
2107
2108         my $tid;
2109         my $copy;
2110         my $volume;
2111
2112         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2113                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
2114                         or return $e->event;
2115                 $tid = $mr->master_record;
2116
2117         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
2118                 $tid = $hold->target;
2119
2120         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2121                 $volume = $e->retrieve_asset_call_number($hold->target)
2122                         or return $e->event;
2123                 $tid = $volume->record;
2124
2125         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
2126                 $copy = $e->retrieve_asset_copy($hold->target)
2127                         or return $e->event;
2128                 $volume = $e->retrieve_asset_call_number($copy->call_number)
2129                         or return $e->event;
2130                 $tid = $volume->record;
2131         }
2132
2133         if(!$copy and ref $hold->current_copy ) {
2134                 $copy = $hold->current_copy;
2135                 $hold->current_copy($copy->id);
2136         }
2137
2138         if(!$volume and $copy) {
2139                 $volume = $e->retrieve_asset_call_number($copy->call_number);
2140         }
2141
2142     # TODO return metarcord mvr for M holds
2143         my $title = $e->retrieve_biblio_record_entry($tid);
2144         return ( $U->record_to_mvr($title), $volume, $copy );
2145 }
2146
2147
2148 __PACKAGE__->register_method(
2149     method    => 'clear_shelf_process',
2150     stream    => 1,
2151     api_name  => 'open-ils.circ.hold.clear_shelf.process',
2152     signature => {
2153         desc => q/
2154             1. Find all holds that have expired on the holds shelf
2155             2. Cancel the holds
2156             3. If a clear-shelf status is configured, put targeted copies into this status
2157             4. Divide copies into 3 groups: items to transit, items to reshelve, and items
2158                 that are needed for holds.  No subsequent action is taken on the holds
2159                 or items after grouping.
2160         /
2161     }
2162 );
2163
2164 sub clear_shelf_process {
2165         my($self, $client, $auth, $org_id) = @_;
2166
2167         my $e = new_editor(authtoken=>$auth, xact => 1);
2168         $e->checkauth or return $e->die_event;
2169
2170     $org_id ||= $e->requestor->ws_ou;
2171         $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
2172
2173     my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
2174
2175     # Find holds on the shelf that have been there too long
2176     my $hold_ids = $e->search_action_hold_request(
2177         {   shelf_expire_time => {'<' => 'now'},
2178             pickup_lib        => $org_id,
2179             cancel_time       => undef,
2180             fulfillment_time  => undef,
2181             shelf_time        => {'!=' => undef}
2182         },
2183         { idlist => 1 }
2184     );
2185
2186
2187     my @holds;
2188     for my $hold_id (@$hold_ids) {
2189
2190         $logger->info("Clear shelf processing hold $hold_id");
2191         
2192         my $hold = $e->retrieve_action_hold_request([
2193             $hold_id, {   
2194                 flesh => 1,
2195                 flesh_fields => {ahr => ['current_copy']}
2196             }
2197         ]);
2198
2199         $hold->cancel_time('now');
2200         $hold->cancel_cause(2); # Hold Shelf expiration
2201         $e->update_action_hold_request($hold) or return $e->die_event;
2202
2203         my $copy = $hold->current_copy;
2204
2205         if($copy_status) {
2206             # if a clear-shelf copy status is defined, update the copy
2207             $copy->status($copy_status);
2208             $copy->edit_date('now');
2209             $copy->editor($e->requestor->id);
2210             $e->update_asset_copy($copy) or return $e->die_event;
2211         }
2212
2213         my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
2214
2215         if($alt_hold) {
2216
2217             # copy is needed for a hold
2218             $client->respond({action => 'hold', copy => $copy, hold_id => $hold->id});
2219
2220         } elsif($copy->circ_lib != $e->requestor->ws_ou) {
2221
2222             # copy needs to transit
2223             $client->respond({action => 'transit', copy => $copy, hold_id => $hold->id});
2224
2225         } else {
2226
2227             # copy needs to go back to the shelf
2228             $client->respond({action => 'shelf', copy => $copy, hold_id => $hold->id});
2229         }
2230
2231         push(@holds, $hold);
2232     }
2233
2234     $e->commit;
2235
2236     # tell the client we're done
2237     $client->resopnd_complete;
2238
2239     # fire off the hold cancelation trigger
2240     my $trigger = OpenSRF::AppSession->connect('open-ils.trigger');
2241
2242     for my $hold (@holds) {
2243
2244         my $req = $trigger->request(
2245             'open-ils.trigger.event.autocreate', 
2246             'hold_request.cancel.expire_holds_shelf', 
2247             $hold, $org_id);
2248
2249         # wait for response so don't flood the service
2250         $req->recv;
2251     }
2252
2253     $trigger->disconnect;
2254 }
2255
2256
2257 __PACKAGE__->register_method(
2258     method    => 'usr_hold_summary',
2259     api_name  => 'open-ils.circ.holds.user_summary',
2260     signature => q/
2261         Returns a summary of holds statuses for a given user
2262     /
2263 );
2264
2265 sub usr_hold_summary {
2266     my($self, $conn, $auth, $user_id) = @_;
2267
2268         my $e = new_editor(authtoken=>$auth);
2269         $e->checkauth or return $e->event;
2270         $e->allowed('VIEW_HOLD') or return $e->event;
2271
2272     my $holds = $e->search_action_hold_request(
2273         {  
2274             usr =>  $user_id , 
2275             fulfillment_time => undef,
2276             cancel_time      => undef,
2277         }
2278     );
2279
2280     my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
2281     $summary{_hold_status($e, $_)} += 1 for @$holds;
2282     return \%summary;
2283 }
2284
2285
2286
2287 __PACKAGE__->register_method(
2288     method    => 'hold_has_copy_at',
2289     api_name  => 'open-ils.circ.hold.has_copy_at',
2290     signature => {
2291         desc   => 
2292                 'Returns the ID of the found copy and name of the shelving location if there is ' .
2293                 'an available copy at the specified org unit.  Returns empty hash otherwise.  '   .
2294                 'The anticipated use for this method is to determine whether an item is '         .
2295                 'available at the library where the user is placing the hold (or, alternatively, '.
2296                 'at the pickup library) to encourage bypassing the hold placement and just '      .
2297                 'checking out the item.' ,
2298         params => {
2299             { desc => 'Authentication Token', type => 'string' },
2300             { desc => 'Method Arguments.  Options include: hold_type, hold_target, org_unit.  ' 
2301                     . 'hold_type is the hold type code (T, V, C, M, ...).  '
2302                     . 'hold_target is the identifier of the hold target object.  ' 
2303                     . 'org_unit is org unit ID.', 
2304               type => 'object' 
2305             },
2306         },
2307         return => { 
2308             desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
2309             type => 'object' 
2310         }
2311     }
2312 );
2313
2314 sub hold_has_copy_at {
2315     my($self, $conn, $auth, $args) = @_;
2316
2317         my $e = new_editor(authtoken=>$auth);
2318         $e->checkauth or return $e->event;
2319
2320     my $hold_type   = $$args{hold_type};
2321     my $hold_target = $$args{hold_target};
2322     my $org_unit    = $$args{org_unit};
2323
2324     my $query = {
2325         select => {acp => ['id'], acpl => ['name']},
2326         from   => {
2327             acp => {
2328                 acpl => {field => 'id', filter => { holdable => 't'}, fkey => 'location'},
2329                 ccs  => {field => 'id', filter => { holdable => 't'}, fkey => 'status'  }
2330             }
2331         },
2332         where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit}},
2333         limit => 1
2334     };
2335
2336     if($hold_type eq 'C') {
2337
2338         $query->{where}->{'+acp'}->{id} = $hold_target;
2339
2340     } elsif($hold_type eq 'V') {
2341
2342         $query->{where}->{'+acp'}->{call_number} = $hold_target;
2343     
2344     } elsif($hold_type eq 'T') {
2345
2346         $query->{from}->{acp}->{acn} = {
2347             field  => 'id',
2348             fkey   => 'call_number',
2349             'join' => {
2350                 bre => {
2351                     field  => 'id',
2352                     filter => {id => $hold_target},
2353                     fkey   => 'record'
2354                 }
2355             }
2356         };
2357
2358     } else {
2359
2360         $query->{from}->{acp}->{acn} = {
2361             field => 'id',
2362             fkey  => 'call_number',
2363             join  => {
2364                 bre => {
2365                     field => 'id',
2366                     fkey  => 'record',
2367                     join  => {
2368                         mmrsm => {
2369                             field  => 'source',
2370                             fkey   => 'id',
2371                             filter => {metarecord => $hold_target},
2372                         }
2373                     }
2374                 }
2375             }
2376         };
2377     }
2378
2379     my $res = $e->json_query($query)->[0] or return {};
2380     return {copy => $res->{id}, location => $res->{name}} if $res;
2381 }
2382
2383
2384 # returns true if the user already has an item checked out 
2385 # that could be used to fulfill the requested hold.
2386 sub hold_item_is_checked_out {
2387     my($e, $user_id, $hold_type, $hold_target) = @_;
2388
2389     my $query = {
2390         select => {acp => ['id']},
2391         from   => {acp => {}},
2392         where  => {
2393             '+acp' => {
2394                 id => {
2395                     in => { # copies for circs the user has checked out
2396                         select => {circ => ['target_copy']},
2397                         from   => 'circ',
2398                         where  => {
2399                             usr => $user_id,
2400                             checkin_time => undef,
2401                             '-or' => [
2402                                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2403                                 {stop_fines => undef}
2404                             ],
2405                         }
2406                     }
2407                 }
2408             }
2409         },
2410         limit => 1
2411     };
2412
2413     if($hold_type eq 'C') {
2414
2415         $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
2416
2417     } elsif($hold_type eq 'V') {
2418
2419         $query->{where}->{'+acp'}->{call_number} = $hold_target;
2420     
2421     } elsif($hold_type eq 'T') {
2422
2423         $query->{from}->{acp}->{acn} = {
2424             field  => 'id',
2425             fkey   => 'call_number',
2426             'join' => {
2427                 bre => {
2428                     field  => 'id',
2429                     filter => {id => $hold_target},
2430                     fkey   => 'record'
2431                 }
2432             }
2433         };
2434
2435     } else {
2436
2437         $query->{from}->{acp}->{acn} = {
2438             field => 'id',
2439             fkey => 'call_number',
2440             join => {
2441                 bre => {
2442                     field => 'id',
2443                     fkey => 'record',
2444                     join => {
2445                         mmrsm => {
2446                             field => 'source',
2447                             fkey => 'id',
2448                             filter => {metarecord => $hold_target},
2449                         }
2450                     }
2451                 }
2452             }
2453         };
2454     }
2455
2456     return $e->json_query($query)->[0];
2457 }
2458
2459 __PACKAGE__->register_method(
2460     method    => 'change_hold_title',
2461     api_name  => 'open-ils.circ.hold.change_title',
2462     signature => {
2463         desc => q/
2464             Updates all title level holds targeting the specified bibs to point a new bib./,
2465         params => [
2466             { desc => 'Authentication Token', type => 'string' },
2467             { desc => 'New Target Bib Id',    type => 'number' },
2468             { desc => 'Old Target Bib Ids',   type => 'array'  },
2469         ],
2470         return => { desc => '1 on success' }
2471     }
2472 );
2473
2474 sub change_hold_title {
2475     my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
2476
2477     my $e = new_editor(authtoken=>$auth, xact=>1);
2478     return $e->event unless $e->checkauth;
2479
2480     my $holds = $e->search_action_hold_request(
2481         [
2482             {
2483                 cancel_time      => undef,
2484                 fulfillment_time => undef,
2485                 hold_type        => 'T',
2486                 target           => $bib_ids
2487             },
2488             {
2489                 flesh        => 1,
2490                 flesh_fields => { ahr => ['usr'] }
2491             }
2492         ],
2493         { substream => 1 }
2494     );
2495
2496     for my $hold (@$holds) {
2497         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
2498         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
2499         $hold->target( $new_bib_id );
2500         $e->update_action_hold_request($hold) or return $e->die_event;
2501     }
2502
2503     $e->commit;
2504
2505     return 1;
2506 }
2507
2508
2509 1;