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