]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
bug #600021: fix lookup of FIFO holds OU setting
[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         $hold->$_($values->{$_}) for keys %$values;
745     }
746
747     my $orig_hold = $e->retrieve_action_hold_request($hold->id)
748         or return $e->die_event;
749
750     # don't allow the user to be changed
751     return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
752
753     if($hold->usr ne $e->requestor->id) {
754         # if the hold is for a different user, make sure the 
755         # requestor has the appropriate permissions
756         my $usr = $e->retrieve_actor_user($hold->usr)
757             or return $e->die_event;
758         return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
759     }
760
761
762     # --------------------------------------------------------------
763     # Changing the request time is like playing God
764     # --------------------------------------------------------------
765     if($hold->request_time ne $orig_hold->request_time) {
766         return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
767         return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
768     }
769
770     # --------------------------------------------------------------
771     # if the hold is on the holds shelf or in transit and the pickup 
772     # lib changes we need to create a new transit.
773     # --------------------------------------------------------------
774     if($orig_hold->pickup_lib ne $hold->pickup_lib) {
775
776         my $status = _hold_status($e, $hold);
777
778         if($status == 3) { # in transit
779
780             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
781             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
782
783             $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
784
785             # update the transit to reflect the new pickup location
786                         my $transit = $e->search_action_hold_transit_copy(
787                 {hold=>$hold->id, dest_recv_time => undef})->[0] 
788                 or return $e->die_event;
789
790             $transit->prev_dest($transit->dest); # mark the previous destination on the transit
791             $transit->dest($hold->pickup_lib);
792             $e->update_action_hold_transit_copy($transit) or return $e->die_event;
793
794         } elsif($status == 4) { # on holds shelf
795
796             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
797             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
798
799             $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
800
801             # create the new transit
802             my $evt = transit_hold($e, $orig_hold, $hold, $e->retrieve_asset_copy($hold->current_copy));
803             return $evt if $evt;
804         }
805     } 
806
807     update_hold_if_frozen($self, $e, $hold, $orig_hold);
808     $e->update_action_hold_request($hold) or return $e->die_event;
809     $e->commit;
810
811     # a change to mint-condition changes the set of potential copies, so retarget the hold;
812     if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
813         _reset_hold($self, $e->requestor, $hold) 
814     }
815
816     return $hold->id;
817 }
818
819 sub transit_hold {
820     my($e, $orig_hold, $hold, $copy) = @_;
821     my $src  = $orig_hold->pickup_lib;
822     my $dest = $hold->pickup_lib;
823
824     $logger->info("putting hold into transit on pickup_lib update");
825
826     my $transit = Fieldmapper::action::hold_transit_copy->new;
827     $transit->hold($hold->id);
828     $transit->source($src);
829     $transit->dest($dest);
830     $transit->target_copy($copy->id);
831     $transit->source_send_time('now');
832     $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
833
834     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
835     $copy->editor($e->requestor->id);
836     $copy->edit_date('now');
837
838     $e->create_action_hold_transit_copy($transit) or return $e->die_event;
839     $e->update_asset_copy($copy) or return $e->die_event;
840     return undef;
841 }
842
843 # if the hold is frozen, this method ensures that the hold is not "targeted", 
844 # that is, it clears the current_copy and prev_check_time to essentiallly 
845 # reset the hold.  If it is being activated, it runs the targeter in the background
846 sub update_hold_if_frozen {
847     my($self, $e, $hold, $orig_hold) = @_;
848     return if $hold->capture_time;
849
850     if($U->is_true($hold->frozen)) {
851         $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
852         $hold->clear_current_copy;
853         $hold->clear_prev_check_time;
854
855     } else {
856         if($U->is_true($orig_hold->frozen)) {
857             $logger->info("Running targeter on activated hold ".$hold->id);
858             $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
859         }
860     }
861 }
862
863 __PACKAGE__->register_method(
864     method    => "hold_note_CUD",
865     api_name  => "open-ils.circ.hold_request.note.cud",
866     signature => {
867         desc   => 'Create, update or delete a hold request note.  If the operator (from Auth. token) '
868                 . 'is not the owner of the hold, the UPDATE_HOLD permission is required',
869         params => [
870             { desc => 'Authentication token', type => 'string' },
871             { desc => 'Hold note object',     type => 'object' }
872         ],
873         return => {
874             desc => 'Returns the note ID, event on error'
875         },
876     }
877 );
878
879 sub hold_note_CUD {
880         my($self, $conn, $auth, $note) = @_;
881
882     my $e = new_editor(authtoken => $auth, xact => 1);
883     return $e->die_event unless $e->checkauth;
884
885     my $hold = $e->retrieve_action_hold_request($note->hold)
886         or return $e->die_event;
887
888     if($hold->usr ne $e->requestor->id) {
889         my $usr = $e->retrieve_actor_user($hold->usr);
890         return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
891         $note->staff('t') if $note->isnew;
892     }
893
894     if($note->isnew) {
895         $e->create_action_hold_request_note($note) or return $e->die_event;
896     } elsif($note->ischanged) {
897         $e->update_action_hold_request_note($note) or return $e->die_event;
898     } elsif($note->isdeleted) {
899         $e->delete_action_hold_request_note($note) or return $e->die_event;
900     }
901
902     $e->commit;
903     return $note->id;
904 }
905
906
907 __PACKAGE__->register_method(
908     method    => "retrieve_hold_status",
909     api_name  => "open-ils.circ.hold.status.retrieve",
910     signature => {
911         desc   => 'Calculates the current status of the hold. The requestor must have '      .
912                   'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
913         param  => [
914             { desc => 'Hold ID', type => 'number' }
915         ],
916         return => {
917             # type => 'number',     # event sometimes
918             desc => <<'END_OF_DESC'
919 Returns event on error or:
920 -1 on error (for now),
921  1 for 'waiting for copy to become available',
922  2 for 'waiting for copy capture',
923  3 for 'in transit',
924  4 for 'arrived',
925  5 for 'hold-shelf-delay'
926 END_OF_DESC
927         }
928     }
929 );
930
931 sub retrieve_hold_status {
932         my($self, $client, $auth, $hold_id) = @_;
933
934         my $e = new_editor(authtoken => $auth);
935         return $e->event unless $e->checkauth;
936         my $hold = $e->retrieve_action_hold_request($hold_id)
937                 or return $e->event;
938
939         if( $e->requestor->id != $hold->usr ) {
940                 return $e->event unless $e->allowed('VIEW_HOLD');
941         }
942
943         return _hold_status($e, $hold);
944
945 }
946
947 sub _hold_status {
948         my($e, $hold) = @_;
949         return 1 unless $hold->current_copy;
950         return 2 unless $hold->capture_time;
951
952         my $copy = $hold->current_copy;
953         unless( ref $copy ) {
954                 $copy = $e->retrieve_asset_copy($hold->current_copy)
955                         or return $e->event;
956         }
957
958         return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
959
960         if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
961
962         my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
963         return 4 unless $hs_wait_interval;
964
965         # if a hold_shelf_status_delay interval is defined and start_time plus 
966         # the interval is greater than now, consider the hold to be in the virtual 
967         # "on its way to the holds shelf" status. Return 5.
968
969         my $transit    = $e->search_action_hold_transit_copy({hold => $hold->id})->[0];
970         my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
971         $start_time    = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time));
972         my $end_time   = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
973
974         return 5 if $end_time > DateTime->now;
975         return 4;
976     }
977
978     return -1;  # error
979 }
980
981
982
983 __PACKAGE__->register_method(
984     method    => "retrieve_hold_queue_stats",
985     api_name  => "open-ils.circ.hold.queue_stats.retrieve",
986     signature => {
987         desc   => 'Returns summary data about the state of a hold',
988         params => [
989             { desc => 'Authentication token',  type => 'string'},
990             { desc => 'Hold ID', type => 'number'},
991         ],
992         return => {
993             desc => q/Summary object with keys: 
994                 total_holds : total holds in queue
995                 queue_position : current queue position
996                 potential_copies : number of potential copies for this hold
997                 estimated_wait : estimated wait time in days
998                 status : hold status  
999                      -1 => error or unexpected state,
1000                      1 => 'waiting for copy to become available',
1001                      2 => 'waiting for copy capture',
1002                      3 => 'in transit',
1003                      4 => 'arrived',
1004                      5 => 'hold-shelf-delay'
1005             /,
1006             type => 'object'
1007         }
1008     }
1009 );
1010
1011 sub retrieve_hold_queue_stats {
1012     my($self, $conn, $auth, $hold_id) = @_;
1013         my $e = new_editor(authtoken => $auth);
1014         return $e->event unless $e->checkauth;
1015         my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1016         if($e->requestor->id != $hold->usr) {
1017                 return $e->event unless $e->allowed('VIEW_HOLD');
1018         }
1019     return retrieve_hold_queue_status_impl($e, $hold);
1020 }
1021
1022 sub retrieve_hold_queue_status_impl {
1023     my $e = shift;
1024     my $hold = shift;
1025
1026     # The holds queue is defined as the distinct set of holds that share at 
1027     # least one potential copy with the context hold, plus any holds that
1028     # share the same hold type and target.  The latter part exists to
1029     # accomodate holds that currently have no potential copies
1030     my $q_holds = $e->json_query({
1031
1032         # fetch cut_in_line and request_time since they're in the order_by
1033         # and we're asking for distinct values
1034         select => {ahr => ['id', 'cut_in_line', 'request_time']},
1035         from   => {
1036             ahr => {
1037                 ahcm => {type => 'left'} # there may be no copy maps 
1038             }
1039         },
1040         order_by => [
1041             {
1042                 "class" => "ahr",
1043                 "field" => "cut_in_line",
1044                 "transform" => "coalesce",
1045                 "params" => [ 0 ],
1046                 "direction" => "desc"
1047             },
1048             { "class" => "ahr", "field" => "request_time" }
1049         ],
1050         distinct => 1,
1051         where    => {
1052             '-or' => [
1053                 {
1054                     '+ahcm' => {
1055                         target_copy => {
1056                             in => {
1057                                 select => {ahcm => ['target_copy']},
1058                                 from   => 'ahcm',
1059                                 where  => {hold => $hold->id}
1060                             } 
1061                         } 
1062                     }
1063                 },
1064                 {
1065                     '+ahr' => {
1066                         hold_type => $hold->hold_type,
1067                         target    => $hold->target
1068                     }
1069                 }
1070             ]
1071         }, 
1072     });
1073
1074     my $qpos = 1;
1075     for my $h (@$q_holds) {
1076         last if $h->{id} == $hold->id;
1077         $qpos++;
1078     }
1079
1080     my $hold_data = $e->json_query({
1081         select => {
1082             ccm => [
1083                 {column => 'code', transform => 'count', aggregate => 1, alias => 'count'}, 
1084                 {column =>'avg_wait_time'}
1085             ]
1086         }, 
1087         from => {ahcm => {acp => {join => 'ccm'}}}, 
1088         where => {'+ahcm' => {hold => $hold->id}}
1089     });
1090
1091     my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1092
1093     my $default_wait = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
1094     my $min_wait = $U->ou_ancestor_setting_value($user_org, 'circ.holds.min_estimated_wait_interval');
1095     $min_wait = OpenSRF::Utils::interval_to_seconds($min_wait || '0 seconds');
1096     $default_wait ||= '0 seconds';
1097
1098     # Estimated wait time is the average wait time across the set 
1099     # of potential copies, divided by the number of potential copies
1100     # times the queue position.  
1101
1102     my $combined_secs = 0;
1103     my $num_potentials = 0;
1104
1105     for my $wait_data (@$hold_data) {
1106         my $count += $wait_data->{count};
1107         $combined_secs += $count * 
1108             OpenSRF::Utils::interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1109         $num_potentials += $count;
1110     }
1111
1112     my $estimated_wait = -1;
1113
1114     if($num_potentials) {
1115         my $avg_wait = $combined_secs / $num_potentials;
1116         $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1117         $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1118     }
1119
1120     return {
1121         total_holds      => scalar(@$q_holds),
1122         queue_position   => $qpos,
1123         potential_copies => $num_potentials,
1124         status           => _hold_status( $e, $hold ),
1125         estimated_wait   => int($estimated_wait)
1126     };
1127 }
1128
1129
1130 sub fetch_open_hold_by_current_copy {
1131         my $class = shift;
1132         my $copyid = shift;
1133         my $hold = $apputils->simplereq(
1134                 'open-ils.cstore', 
1135                 'open-ils.cstore.direct.action.hold_request.search.atomic',
1136                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
1137         return $hold->[0] if ref($hold);
1138         return undef;
1139 }
1140
1141 sub fetch_related_holds {
1142         my $class = shift;
1143         my $copyid = shift;
1144         return $apputils->simplereq(
1145                 'open-ils.cstore', 
1146                 'open-ils.cstore.direct.action.hold_request.search.atomic',
1147                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
1148 }
1149
1150
1151 __PACKAGE__->register_method(
1152     method    => "hold_pull_list",
1153     api_name  => "open-ils.circ.hold_pull_list.retrieve",
1154     signature => {
1155         desc   => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1156                   'The location is determined by the login session.',
1157         params => [
1158             { desc => 'Limit (optional)',  type => 'number'},
1159             { desc => 'Offset (optional)', type => 'number'},
1160         ],
1161         return => {
1162             desc => 'reference to a list of holds, or event on failure',
1163         }
1164     }
1165 );
1166
1167 __PACKAGE__->register_method(
1168     method    => "hold_pull_list",
1169     api_name  => "open-ils.circ.hold_pull_list.id_list.retrieve",
1170     signature => {
1171         desc   => 'Returns (reference to) a list of holds IDs 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.retrieve.count",
1186     signature => {
1187         desc   => 'Returns a count of holds 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 => 'Holds count (integer), or event on failure',
1195             # type => 'number'
1196         }
1197     }
1198 );
1199
1200
1201 sub hold_pull_list {
1202         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1203         my( $reqr, $evt ) = $U->checkses($authtoken);
1204         return $evt if $evt;
1205
1206         my $org = $reqr->ws_ou || $reqr->home_ou;
1207         # the perm locaiton shouldn't really matter here since holds
1208         # will exist all over and VIEW_HOLDS should be universal
1209         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1210         return $evt if $evt;
1211
1212     if($self->api_name =~ /count/) {
1213
1214                 my $count = $U->storagereq(
1215                         'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1216                         $org, $limit, $offset ); 
1217
1218         $logger->info("Grabbing pull list for org unit $org with $count items");
1219         return $count;
1220
1221     } elsif( $self->api_name =~ /id_list/ ) {
1222                 return $U->storagereq(
1223                         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1224                         $org, $limit, $offset ); 
1225
1226         } else {
1227                 return $U->storagereq(
1228                         'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1229                         $org, $limit, $offset ); 
1230         }
1231 }
1232
1233 __PACKAGE__->register_method(
1234     method        => 'fetch_hold_notify',
1235     api_name      => 'open-ils.circ.hold_notification.retrieve_by_hold',
1236     authoritative => 1,
1237     signature     => q/ 
1238 Returns a list of hold notification objects based on hold id.
1239 @param authtoken The loggin session key
1240 @param holdid The id of the hold whose notifications we want to retrieve
1241 @return An array of hold notification objects, event on error.
1242 /
1243 );
1244
1245 sub fetch_hold_notify {
1246         my( $self, $conn, $authtoken, $holdid ) = @_;
1247         my( $requestor, $evt ) = $U->checkses($authtoken);
1248         return $evt if $evt;
1249         my ($hold, $patron);
1250         ($hold, $evt) = $U->fetch_hold($holdid);
1251         return $evt if $evt;
1252         ($patron, $evt) = $U->fetch_user($hold->usr);
1253         return $evt if $evt;
1254
1255         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
1256         return $evt if $evt;
1257
1258         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
1259         return $U->cstorereq(
1260                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
1261 }
1262
1263
1264 __PACKAGE__->register_method(
1265     method    => 'create_hold_notify',
1266     api_name  => 'open-ils.circ.hold_notification.create',
1267     signature => q/
1268 Creates a new hold notification object
1269 @param authtoken The login session key
1270 @param notification The hold notification object to create
1271 @return ID of the new object on success, Event on error
1272 /
1273 );
1274
1275 sub create_hold_notify {
1276    my( $self, $conn, $auth, $note ) = @_;
1277    my $e = new_editor(authtoken=>$auth, xact=>1);
1278    return $e->die_event unless $e->checkauth;
1279
1280    my $hold = $e->retrieve_action_hold_request($note->hold)
1281       or return $e->die_event;
1282    my $patron = $e->retrieve_actor_user($hold->usr) 
1283       or return $e->die_event;
1284
1285    return $e->die_event unless 
1286       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1287
1288         $note->notify_staff($e->requestor->id);
1289    $e->create_action_hold_notification($note) or return $e->die_event;
1290    $e->commit;
1291    return $note->id;
1292 }
1293
1294 __PACKAGE__->register_method(
1295     method    => 'create_hold_note',
1296     api_name  => 'open-ils.circ.hold_note.create',
1297     signature => q/
1298                 Creates a new hold request note object
1299                 @param authtoken The login session key
1300                 @param note The hold note object to create
1301                 @return ID of the new object on success, Event on error
1302                 /
1303 );
1304
1305 sub create_hold_note {
1306    my( $self, $conn, $auth, $note ) = @_;
1307    my $e = new_editor(authtoken=>$auth, xact=>1);
1308    return $e->die_event unless $e->checkauth;
1309
1310    my $hold = $e->retrieve_action_hold_request($note->hold)
1311       or return $e->die_event;
1312    my $patron = $e->retrieve_actor_user($hold->usr) 
1313       or return $e->die_event;
1314
1315    return $e->die_event unless 
1316       $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn.  Probably want something more specific
1317
1318    $e->create_action_hold_request_note($note) or return $e->die_event;
1319    $e->commit;
1320    return $note->id;
1321 }
1322
1323 __PACKAGE__->register_method(
1324     method    => 'reset_hold',
1325     api_name  => 'open-ils.circ.hold.reset',
1326     signature => q/
1327                 Un-captures and un-targets a hold, essentially returning
1328                 it to the state it was in directly after it was placed,
1329                 then attempts to re-target the hold
1330                 @param authtoken The login session key
1331                 @param holdid The id of the hold
1332         /
1333 );
1334
1335
1336 sub reset_hold {
1337         my( $self, $conn, $auth, $holdid ) = @_;
1338         my $reqr;
1339         my ($hold, $evt) = $U->fetch_hold($holdid);
1340         return $evt if $evt;
1341         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1342         return $evt if $evt;
1343         $evt = _reset_hold($self, $reqr, $hold);
1344         return $evt if $evt;
1345         return 1;
1346 }
1347
1348
1349 __PACKAGE__->register_method(
1350     method   => 'reset_hold_batch',
1351     api_name => 'open-ils.circ.hold.reset.batch'
1352 );
1353
1354 sub reset_hold_batch {
1355     my($self, $conn, $auth, $hold_ids) = @_;
1356
1357     my $e = new_editor(authtoken => $auth);
1358     return $e->event unless $e->checkauth;
1359
1360     for my $hold_id ($hold_ids) {
1361
1362         my $hold = $e->retrieve_action_hold_request(
1363             [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}]) 
1364             or return $e->event;
1365
1366             next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1367         _reset_hold($self, $e->requestor, $hold);
1368     }
1369
1370     return 1;
1371 }
1372
1373
1374 sub _reset_hold {
1375         my ($self, $reqr, $hold) = @_;
1376
1377         my $e = new_editor(xact =>1, requestor => $reqr);
1378
1379         $logger->info("reseting hold ".$hold->id);
1380
1381         my $hid = $hold->id;
1382
1383         if( $hold->capture_time and $hold->current_copy ) {
1384
1385                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1386                         or return $e->event;
1387
1388                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1389                         $logger->info("setting copy to status 'reshelving' on hold retarget");
1390                         $copy->status(OILS_COPY_STATUS_RESHELVING);
1391                         $copy->editor($e->requestor->id);
1392                         $copy->edit_date('now');
1393                         $e->update_asset_copy($copy) or return $e->event;
1394
1395                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1396
1397                         # We don't want the copy to remain "in transit"
1398                         $copy->status(OILS_COPY_STATUS_RESHELVING);
1399                         $logger->warn("! reseting hold [$hid] that is in transit");
1400                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1401
1402                         if( $transid ) {
1403                                 my $trans = $e->retrieve_action_transit_copy($transid);
1404                                 if( $trans ) {
1405                                         $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1406                                         my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
1407                                         $logger->info("Transit abort completed with result $evt");
1408                                         return $evt unless "$evt" eq 1;
1409                                 }
1410                         }
1411                 }
1412         }
1413
1414         $hold->clear_capture_time;
1415         $hold->clear_current_copy;
1416         $hold->clear_shelf_time;
1417         $hold->clear_shelf_expire_time;
1418
1419         $e->update_action_hold_request($hold) or return $e->event;
1420         $e->commit;
1421
1422         $U->storagereq(
1423                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1424
1425         return undef;
1426 }
1427
1428
1429 __PACKAGE__->register_method(
1430     method    => 'fetch_open_title_holds',
1431     api_name  => 'open-ils.circ.open_holds.retrieve',
1432     signature => q/
1433                 Returns a list ids of un-fulfilled holds for a given title id
1434                 @param authtoken The login session key
1435                 @param id the id of the item whose holds we want to retrieve
1436                 @param type The hold type - M, T, V, C
1437         /
1438 );
1439
1440 sub fetch_open_title_holds {
1441         my( $self, $conn, $auth, $id, $type, $org ) = @_;
1442         my $e = new_editor( authtoken => $auth );
1443         return $e->event unless $e->checkauth;
1444
1445         $type ||= "T";
1446         $org  ||= $e->requestor->ws_ou;
1447
1448 #       return $e->search_action_hold_request(
1449 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
1450
1451         # XXX make me return IDs in the future ^--
1452         my $holds = $e->search_action_hold_request(
1453                 { 
1454                         target                          => $id, 
1455                         cancel_time                     => undef, 
1456                         hold_type                       => $type, 
1457                         fulfillment_time        => undef 
1458                 }
1459         );
1460
1461         flesh_hold_transits($holds);
1462         return $holds;
1463 }
1464
1465
1466 sub flesh_hold_transits {
1467         my $holds = shift;
1468         for my $hold ( @$holds ) {
1469                 $hold->transit(
1470                         $apputils->simplereq(
1471                                 'open-ils.cstore',
1472                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
1473                                 { hold => $hold->id },
1474                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
1475                         )->[0]
1476                 );
1477         }
1478 }
1479
1480 sub flesh_hold_notices {
1481         my( $holds, $e ) = @_;
1482         $e ||= new_editor();
1483
1484         for my $hold (@$holds) {
1485                 my $notices = $e->search_action_hold_notification(
1486                         [
1487                                 { hold => $hold->id },
1488                                 { order_by => { anh => 'notify_time desc' } },
1489                         ],
1490                         {idlist=>1}
1491                 );
1492
1493                 $hold->notify_count(scalar(@$notices));
1494                 if( @$notices ) {
1495                         my $n = $e->retrieve_action_hold_notification($$notices[0])
1496                                 or return $e->event;
1497                         $hold->notify_time($n->notify_time);
1498                 }
1499         }
1500 }
1501
1502
1503 __PACKAGE__->register_method(
1504     method    => 'fetch_captured_holds',
1505     api_name  => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1506     stream    => 1,
1507     signature => q/
1508                 Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
1509                 @param authtoken The login session key
1510                 @param org The org id of the location in question
1511         /
1512 );
1513
1514 __PACKAGE__->register_method(
1515     method    => 'fetch_captured_holds',
1516     api_name  => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1517     stream    => 1,
1518     signature => q/
1519                 Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
1520                 @param authtoken The login session key
1521                 @param org The org id of the location in question
1522         /
1523 );
1524
1525 __PACKAGE__->register_method(
1526     method    => 'fetch_captured_holds',
1527     api_name  => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
1528     stream    => 1,
1529     signature => q/
1530                 Returns list ids of shelf-expired un-fulfilled holds for a given title id
1531                 @param authtoken The login session key
1532                 @param org The org id of the location in question
1533         /
1534 );
1535
1536
1537 sub fetch_captured_holds {
1538         my( $self, $conn, $auth, $org ) = @_;
1539
1540         my $e = new_editor(authtoken => $auth);
1541         return $e->event unless $e->checkauth;
1542         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1543
1544         $org ||= $e->requestor->ws_ou;
1545
1546     my $query = { 
1547         select => { ahr => ['id'] },
1548         from   => {
1549             ahr => {
1550                 acp => {
1551                     field => 'id',
1552                     fkey  => 'current_copy'
1553                 },
1554             }
1555         }, 
1556         where => {
1557             '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1558             '+ahr' => {
1559                 capture_time     => { "!=" => undef },
1560                 current_copy     => { "!=" => undef },
1561                 fulfillment_time => undef,
1562                 pickup_lib       => $org,
1563                 cancel_time      => undef,
1564               }
1565         }
1566     };
1567     if($self->api_name =~ /expired/) {
1568         $query->{'where'}->{'+ahr'}->{'shelf_expire_time'} = {'<' => 'now'};
1569         $query->{'where'}->{'+ahr'}->{'shelf_time'} = {'!=' => undef};
1570     }
1571     my $hold_ids = $e->json_query( $query );
1572
1573     for my $hold_id (@$hold_ids) {
1574         if($self->api_name =~ /id_list/) {
1575             $conn->respond($hold_id->{id});
1576             next;
1577         } else {
1578             $conn->respond(
1579                 $e->retrieve_action_hold_request([
1580                     $hold_id->{id},
1581                     {
1582                         flesh => 1,
1583                         flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
1584                         order_by => {anh => 'notify_time desc'}
1585                     }
1586                 ])
1587             );
1588         }
1589     }
1590
1591     return undef;
1592 }
1593
1594
1595 __PACKAGE__->register_method(
1596     method    => "check_title_hold",
1597     api_name  => "open-ils.circ.title_hold.is_possible",
1598     signature => {
1599         desc  => 'Determines if a hold were to be placed by a given user, ' .
1600              'whether or not said hold would have any potential copies to fulfill it.' .
1601              'The named paramaters of the second argument include: ' .
1602              'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
1603              'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' , 
1604         params => [
1605             { desc => 'Authentication token',     type => 'string'},
1606             { desc => 'Hash of named parameters', type => 'object'},
1607         ],
1608         return => {
1609             desc => 'List of new message IDs (empty if none)',
1610             type => 'array'
1611         }
1612     }
1613 );
1614
1615 =head3 check_title_hold (token, hash)
1616
1617 The named fields in the hash are: 
1618
1619  patronid     - ID of the hold recipient  (required)
1620  depth        - hold range depth          (default 0)
1621  pickup_lib   - destination for hold, fallback value for selection_ou
1622  selection_ou - ID of org_unit establishing hard and soft hold boundary settings
1623  titleid      - ID (BRN) of the title to be held, required for Title level hold
1624  volume_id    - required for Volume level hold
1625  copy_id      - required for Copy level hold
1626  mrid         - required for Meta-record level hold
1627  hold_type    - T,C,V or M for Title, Copy, Volume or Meta-record  (default "T")
1628
1629 All key/value pairs are passed on to do_possibility_checks.
1630
1631 =cut
1632
1633 # FIXME: better params checking.  what other params are required, if any?
1634 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
1635 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still 
1636 # used in conditionals, where it may be undefined, causing a warning.
1637 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
1638
1639 sub check_title_hold {
1640     my( $self, $client, $authtoken, $params ) = @_;
1641     my $e = new_editor(authtoken=>$authtoken);
1642     return $e->event unless $e->checkauth;
1643
1644     my %params       = %$params;
1645     my $depth        = $params{depth}        || 0;
1646     my $selection_ou = $params{selection_ou} || $params{pickup_lib};
1647
1648         my $patron = $e->retrieve_actor_user($params{patronid})
1649                 or return $e->event;
1650
1651         if( $e->requestor->id ne $patron->id ) {
1652                 return $e->event unless 
1653                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1654         }
1655
1656         return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1657
1658         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1659                 or return $e->event;
1660
1661     my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1662     my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1663
1664     my @status = ();
1665     my $return_depth = $hard_boundary; # default depth to return on success
1666     if(defined $soft_boundary and $depth < $soft_boundary) {
1667         # work up the tree and as soon as we find a potential copy, use that depth
1668         # also, make sure we don't go past the hard boundary if it exists
1669
1670         # our min boundary is the greater of user-specified boundary or hard boundary
1671         my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?  
1672             $hard_boundary : $depth;
1673
1674         my $depth = $soft_boundary;
1675         while($depth >= $min_depth) {
1676             $logger->info("performing hold possibility check with soft boundary $depth");
1677             @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1678             if ($status[0]) {
1679                 $return_depth = $depth;
1680                 last;
1681             }
1682             $depth--;
1683         }
1684     } elsif(defined $hard_boundary and $depth < $hard_boundary) {
1685         # there is no soft boundary, enforce the hard boundary if it exists
1686         $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1687         @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
1688     } else {
1689         # no boundaries defined, fall back to user specifed boundary or no boundary
1690         $logger->info("performing hold possibility check with no boundary");
1691         @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
1692     }
1693
1694     if ($status[0]) {
1695         return {
1696             "success" => 1,
1697             "depth" => $return_depth,
1698             "local_avail" => $status[1]
1699         };
1700     } elsif ($status[2]) {
1701         my $n = scalar @{$status[2]};
1702         return {"success" => 0, "last_event" => $status[2]->[$n - 1]};
1703     } else {
1704         return {"success" => 0};
1705     }
1706 }
1707
1708 sub do_possibility_checks {
1709     my($e, $patron, $request_lib, $depth, %params) = @_;
1710
1711     my $titleid      = $params{titleid}      || "";
1712     my $volid        = $params{volume_id};
1713     my $copyid       = $params{copy_id};
1714     my $mrid         = $params{mrid}         || "";
1715     my $pickup_lib   = $params{pickup_lib};
1716     my $hold_type    = $params{hold_type}    || 'T';
1717     my $selection_ou = $params{selection_ou} || $pickup_lib;
1718
1719
1720         my $copy;
1721         my $volume;
1722         my $title;
1723
1724         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1725
1726         return $e->event unless $copy   = $e->retrieve_asset_copy($copyid);
1727         return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
1728         return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
1729
1730         return verify_copy_for_hold( 
1731             $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib
1732         );
1733
1734         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1735
1736                 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
1737                 return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
1738
1739                 return _check_volume_hold_is_possible(
1740                         $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
1741         );
1742
1743         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1744
1745                 return _check_title_hold_is_possible(
1746                         $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
1747         );
1748
1749         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1750
1751                 my $maps = $e->search_metabib_metarecord_source_map({metarecord=>$mrid});
1752                 my @recs = map { $_->source } @$maps;
1753                 my @status = ();
1754                 for my $rec (@recs) {
1755                         @status = _check_title_hold_is_possible(
1756                                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
1757                         );
1758                         last if $status[1];
1759                 }
1760                 return @status;
1761         }
1762 #   else { Unrecognized hold_type ! }   # FIXME: return error? or 0?
1763 }
1764
1765 my %prox_cache;
1766 sub create_ranged_org_filter {
1767     my($e, $selection_ou, $depth) = @_;
1768
1769     # find the orgs from which this hold may be fulfilled, 
1770     # based on the selection_ou and depth
1771
1772     my $top_org = $e->search_actor_org_unit([
1773         {parent_ou => undef}, 
1774         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1775     my %org_filter;
1776
1777     return () if $depth == $top_org->ou_type->depth;
1778
1779     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1780     %org_filter = (circ_lib => []);
1781     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1782
1783     $logger->info("hold org filter at depth $depth and selection_ou ".
1784         "$selection_ou created list of @{$org_filter{circ_lib}}");
1785
1786     return %org_filter;
1787 }
1788
1789
1790 sub _check_title_hold_is_possible {
1791     my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1792    
1793     my $e = new_editor();
1794     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1795
1796     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1797     my $copies = $e->json_query(
1798         { 
1799             select => { acp => ['id', 'circ_lib'] },
1800               from => {
1801                 acp => {
1802                     acn => {
1803                         field  => 'id',
1804                         fkey   => 'call_number',
1805                         'join' => {
1806                             bre => {
1807                                 field  => 'id',
1808                                 filter => { id => $titleid },
1809                                 fkey   => 'record'
1810                             }
1811                         }
1812                     },
1813                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1814                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
1815                 }
1816             }, 
1817             where => {
1818                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1819             }
1820         }
1821     );
1822
1823     $logger->info("title possible found ".scalar(@$copies)." potential copies");
1824     return (
1825         0, 0, [
1826             new OpenILS::Event(
1827                 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
1828                 "payload" => {"fail_part" => "no_ultimate_items"}
1829             )
1830         ]
1831     ) unless @$copies;
1832
1833     # -----------------------------------------------------------------------
1834     # sort the copies into buckets based on their circ_lib proximity to 
1835     # the patron's home_ou.  
1836     # -----------------------------------------------------------------------
1837
1838     my $home_org = $patron->home_ou;
1839     my $req_org = $request_lib->id;
1840
1841     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1842
1843     $prox_cache{$home_org} = 
1844         $e->search_actor_org_unit_proximity({from_org => $home_org})
1845         unless $prox_cache{$home_org};
1846     my $home_prox = $prox_cache{$home_org};
1847
1848     my %buckets;
1849     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1850     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1851
1852     my @keys = sort { $a <=> $b } keys %buckets;
1853
1854
1855     if( $home_org ne $req_org ) {
1856       # -----------------------------------------------------------------------
1857       # shove the copies close to the request_lib into the primary buckets 
1858       # directly before the farthest away copies.  That way, they are not 
1859       # given priority, but they are checked before the farthest copies.
1860       # -----------------------------------------------------------------------
1861         $prox_cache{$req_org} = 
1862             $e->search_actor_org_unit_proximity({from_org => $req_org})
1863             unless $prox_cache{$req_org};
1864         my $req_prox = $prox_cache{$req_org};
1865
1866         my %buckets2;
1867         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1868         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1869
1870         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1871         my $new_key = $highest_key - 0.5; # right before the farthest prox
1872         my @keys2   = sort { $a <=> $b } keys %buckets2;
1873         for my $key (@keys2) {
1874             last if $key >= $highest_key;
1875             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1876         }
1877     }
1878
1879     @keys = sort { $a <=> $b } keys %buckets;
1880
1881     my $title;
1882     my %seen;
1883     my @status;
1884     OUTER: for my $key (@keys) {
1885       my @cps = @{$buckets{$key}};
1886
1887       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1888
1889       for my $copyid (@cps) {
1890
1891          next if $seen{$copyid};
1892          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1893          my $copy = $e->retrieve_asset_copy($copyid);
1894          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1895
1896          unless($title) { # grab the title if we don't already have it
1897             my $vol = $e->retrieve_asset_call_number(
1898                [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1899             $title = $vol->record;
1900          }
1901    
1902          @status = verify_copy_for_hold(
1903             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib);
1904
1905          last OUTER if $status[0];
1906       }
1907     }
1908
1909     return @status;
1910 }
1911
1912
1913 sub _check_volume_hold_is_possible {
1914         my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1915     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1916         my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1917         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1918
1919     return (
1920         0, 0, [
1921             new OpenILS::Event(
1922                 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
1923                 "payload" => {"fail_part" => "no_ultimate_items"}
1924             )
1925         ]
1926     ) unless @$copies;
1927
1928     my @status;
1929         for my $copy ( @$copies ) {
1930         @status = verify_copy_for_hold(
1931                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1932         last if $status[0];
1933         }
1934         return @status;
1935 }
1936
1937
1938
1939 sub verify_copy_for_hold {
1940         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1941         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1942     my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1943                 {       patron                          => $patron, 
1944                         requestor                       => $requestor, 
1945                         copy                            => $copy,
1946                         title                           => $title, 
1947                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1948                         pickup_lib                      => $pickup_lib,
1949                         request_lib                     => $request_lib,
1950             new_hold            => 1,
1951             show_event_list     => 1
1952                 } 
1953         );
1954
1955     return (
1956         (not scalar @$permitted), # true if permitted is an empty arrayref
1957         (
1958                 ($copy->circ_lib == $pickup_lib) and 
1959             ($copy->status == OILS_COPY_STATUS_AVAILABLE)
1960         ),
1961         $permitted
1962     );
1963 }
1964
1965
1966
1967 sub find_nearest_permitted_hold {
1968
1969     my $class  = shift;
1970     my $editor = shift;     # CStoreEditor object
1971     my $copy   = shift;     # copy to target
1972     my $user   = shift;     # staff
1973     my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1974       
1975     my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1976
1977     my $bc = $copy->barcode;
1978
1979         # find any existing holds that already target this copy
1980         my $old_holds = $editor->search_action_hold_request(
1981                 {       current_copy => $copy->id, 
1982                         cancel_time  => undef, 
1983                         capture_time => undef 
1984                 } 
1985         );
1986
1987         # hold->type "R" means we need this copy
1988         for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1989
1990
1991     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1992
1993         $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1994         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1995
1996         my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
1997
1998         # search for what should be the best holds for this copy to fulfill
1999         my $best_holds = $U->storagereq(
2000         "open-ils.storage.action.hold_request.nearest_hold.atomic", 
2001                 $user->ws_ou, $copy->id, 10, $hold_stall_interval, $fifo );
2002
2003         unless(@$best_holds) {
2004
2005                 if( my $hold = $$old_holds[0] ) {
2006                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
2007                         return ($hold);
2008                 }
2009
2010                 $logger->info("circulator: no suitable holds found for copy $bc");
2011                 return (undef, $evt);
2012         }
2013
2014
2015         my $best_hold;
2016
2017         # for each potential hold, we have to run the permit script
2018         # to make sure the hold is actually permitted.
2019         for my $holdid (@$best_holds) {
2020                 next unless $holdid;
2021                 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
2022
2023                 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
2024                 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
2025                 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
2026
2027                 # see if this hold is permitted
2028                 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
2029                         {       patron_id                       => $hold->usr,
2030                                 requestor                       => $reqr,
2031                                 copy                            => $copy,
2032                                 pickup_lib                      => $hold->pickup_lib,
2033                                 request_lib                     => $rlib,
2034                         } 
2035                 );
2036
2037                 if( $permitted ) {
2038                         $best_hold = $hold;
2039                         last;
2040                 }
2041         }
2042
2043
2044         unless( $best_hold ) { # no "good" permitted holds were found
2045                 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
2046                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
2047                         return ($hold);
2048                 }
2049
2050                 # we got nuthin
2051                 $logger->info("circulator: no suitable holds found for copy $bc");
2052                 return (undef, $evt);
2053         }
2054
2055         $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
2056
2057         # indicate a permitted hold was found
2058         return $best_hold if $check_only;
2059
2060         # we've found a permitted hold.  we need to "grab" the copy 
2061         # to prevent re-targeted holds (next part) from re-grabbing the copy
2062         $best_hold->current_copy($copy->id);
2063         $editor->update_action_hold_request($best_hold) 
2064                 or return (undef, $editor->event);
2065
2066
2067     my @retarget;
2068
2069         # re-target any other holds that already target this copy
2070         for my $old_hold (@$old_holds) {
2071                 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
2072                 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
2073             $old_hold->id." after a better hold [".$best_hold->id."] was found");
2074         $old_hold->clear_current_copy;
2075         $old_hold->clear_prev_check_time;
2076         $editor->update_action_hold_request($old_hold) 
2077             or return (undef, $editor->event);
2078         push(@retarget, $old_hold->id);
2079         }
2080
2081         return ($best_hold, undef, (@retarget) ? \@retarget : undef);
2082 }
2083
2084
2085
2086
2087
2088
2089 __PACKAGE__->register_method(
2090     method   => 'all_rec_holds',
2091     api_name => 'open-ils.circ.holds.retrieve_all_from_title',
2092 );
2093
2094 sub all_rec_holds {
2095         my( $self, $conn, $auth, $title_id, $args ) = @_;
2096
2097         my $e = new_editor(authtoken=>$auth);
2098         $e->checkauth or return $e->event;
2099         $e->allowed('VIEW_HOLD') or return $e->event;
2100
2101         $args ||= {};
2102     $args->{fulfillment_time} = undef; #  we don't want to see old fulfilled holds
2103         $args->{cancel_time} = undef;
2104
2105         my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
2106
2107     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
2108     if($mr_map) {
2109         $resp->{metarecord_holds} = $e->search_action_hold_request(
2110             {   hold_type => OILS_HOLD_TYPE_METARECORD,
2111                 target => $mr_map->metarecord,
2112                 %$args 
2113             }, {idlist => 1}
2114         );
2115     }
2116
2117         $resp->{title_holds} = $e->search_action_hold_request(
2118                 { 
2119                         hold_type => OILS_HOLD_TYPE_TITLE, 
2120                         target => $title_id, 
2121                         %$args 
2122                 }, {idlist=>1} );
2123
2124         my $vols = $e->search_asset_call_number(
2125                 { record => $title_id, deleted => 'f' }, {idlist=>1});
2126
2127         return $resp unless @$vols;
2128
2129         $resp->{volume_holds} = $e->search_action_hold_request(
2130                 { 
2131                         hold_type => OILS_HOLD_TYPE_VOLUME, 
2132                         target => $vols,
2133                         %$args }, 
2134                 {idlist=>1} );
2135
2136         my $copies = $e->search_asset_copy(
2137                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
2138
2139         return $resp unless @$copies;
2140
2141         $resp->{copy_holds} = $e->search_action_hold_request(
2142                 { 
2143                         hold_type => OILS_HOLD_TYPE_COPY,
2144                         target => $copies,
2145                         %$args }, 
2146                 {idlist=>1} );
2147
2148         return $resp;
2149 }
2150
2151
2152
2153
2154
2155 __PACKAGE__->register_method(
2156     method        => 'uber_hold',
2157     authoritative => 1,
2158     api_name      => 'open-ils.circ.hold.details.retrieve'
2159 );
2160
2161 sub uber_hold {
2162         my($self, $client, $auth, $hold_id) = @_;
2163         my $e = new_editor(authtoken=>$auth);
2164         $e->checkauth or return $e->event;
2165     return uber_hold_impl($e, $hold_id);
2166 }
2167
2168 __PACKAGE__->register_method(
2169     method        => 'batch_uber_hold',
2170     authoritative => 1,
2171     stream        => 1,
2172     api_name      => 'open-ils.circ.hold.details.batch.retrieve'
2173 );
2174
2175 sub batch_uber_hold {
2176         my($self, $client, $auth, $hold_ids) = @_;
2177         my $e = new_editor(authtoken=>$auth);
2178         $e->checkauth or return $e->event;
2179     $client->respond(uber_hold_impl($e, $_)) for @$hold_ids;
2180     return undef;
2181 }
2182
2183 sub uber_hold_impl {
2184     my($e, $hold_id) = @_;
2185
2186         my $resp = {};
2187
2188         my $hold = $e->retrieve_action_hold_request(
2189                 [
2190                         $hold_id,
2191                         {
2192                                 flesh => 1,
2193                                 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
2194                         }
2195                 ]
2196         ) or return $e->event;
2197
2198     if($hold->usr->id ne $e->requestor->id) {
2199         # A user is allowed to see his/her own holds
2200             $e->allowed('VIEW_HOLD') or return $e->event;
2201         $hold->notes( # filter out any non-staff ("private") notes
2202             [ grep { !$U->is_true($_->staff) } @{$hold->notes} ] );
2203
2204     } else {
2205         # caller is asking for own hold, but may not have permission to view staff notes
2206             unless($e->allowed('VIEW_HOLD')) {
2207             $hold->notes( # filter out any staff notes
2208                 [ grep { $U->is_true($_->staff) } @{$hold->notes} ] );
2209         }
2210     }
2211
2212         my $user = $hold->usr;
2213         $hold->usr($user->id);
2214
2215         my $card = $e->retrieve_actor_card($user->card)
2216                 or return $e->event;
2217
2218         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
2219
2220         flesh_hold_notices([$hold], $e);
2221         flesh_hold_transits([$hold]);
2222
2223     my $details = retrieve_hold_queue_status_impl($e, $hold);
2224
2225     return {
2226         hold           => $hold,
2227         copy           => $copy,
2228         volume         => $volume,
2229         mvr            => $mvr,
2230         patron_first   => $user->first_given_name,
2231         patron_last    => $user->family_name,
2232         patron_barcode => $card->barcode,
2233         %$details
2234     };
2235 }
2236
2237
2238
2239 # -----------------------------------------------------
2240 # Returns the MVR object that represents what the
2241 # hold is all about
2242 # -----------------------------------------------------
2243 sub find_hold_mvr {
2244         my( $e, $hold ) = @_;
2245
2246         my $tid;
2247         my $copy;
2248         my $volume;
2249
2250         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2251                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
2252                         or return $e->event;
2253                 $tid = $mr->master_record;
2254
2255         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
2256                 $tid = $hold->target;
2257
2258         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2259                 $volume = $e->retrieve_asset_call_number($hold->target)
2260                         or return $e->event;
2261                 $tid = $volume->record;
2262
2263         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
2264                 $copy = $e->retrieve_asset_copy($hold->target)
2265                         or return $e->event;
2266                 $volume = $e->retrieve_asset_call_number($copy->call_number)
2267                         or return $e->event;
2268                 $tid = $volume->record;
2269         }
2270
2271         if(!$copy and ref $hold->current_copy ) {
2272                 $copy = $hold->current_copy;
2273                 $hold->current_copy($copy->id);
2274         }
2275
2276         if(!$volume and $copy) {
2277                 $volume = $e->retrieve_asset_call_number($copy->call_number);
2278         }
2279
2280     # TODO return metarcord mvr for M holds
2281         my $title = $e->retrieve_biblio_record_entry($tid);
2282         return ( $U->record_to_mvr($title), $volume, $copy );
2283 }
2284
2285
2286 __PACKAGE__->register_method(
2287     method    => 'clear_shelf_process',
2288     stream    => 1,
2289     api_name  => 'open-ils.circ.hold.clear_shelf.process',
2290     signature => {
2291         desc => q/
2292             1. Find all holds that have expired on the holds shelf
2293             2. Cancel the holds
2294             3. If a clear-shelf status is configured, put targeted copies into this status
2295             4. Divide copies into 3 groups: items to transit, items to reshelve, and items
2296                 that are needed for holds.  No subsequent action is taken on the holds
2297                 or items after grouping.
2298         /
2299     }
2300 );
2301
2302 sub clear_shelf_process {
2303         my($self, $client, $auth, $org_id) = @_;
2304
2305         my $e = new_editor(authtoken=>$auth, xact => 1);
2306         $e->checkauth or return $e->die_event;
2307
2308     $org_id ||= $e->requestor->ws_ou;
2309         $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
2310
2311     my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
2312
2313     # Find holds on the shelf that have been there too long
2314     my $hold_ids = $e->search_action_hold_request(
2315         {   shelf_expire_time => {'<' => 'now'},
2316             pickup_lib        => $org_id,
2317             cancel_time       => undef,
2318             fulfillment_time  => undef,
2319             shelf_time        => {'!=' => undef}
2320         },
2321         { idlist => 1 }
2322     );
2323
2324
2325     my @holds;
2326     for my $hold_id (@$hold_ids) {
2327
2328         $logger->info("Clear shelf processing hold $hold_id");
2329         
2330         my $hold = $e->retrieve_action_hold_request([
2331             $hold_id, {   
2332                 flesh => 1,
2333                 flesh_fields => {ahr => ['current_copy']}
2334             }
2335         ]);
2336
2337         $hold->cancel_time('now');
2338         $hold->cancel_cause(2); # Hold Shelf expiration
2339         $e->update_action_hold_request($hold) or return $e->die_event;
2340
2341         my $copy = $hold->current_copy;
2342
2343         if($copy_status or $copy_status == 0) {
2344             # if a clear-shelf copy status is defined, update the copy
2345             $copy->status($copy_status);
2346             $copy->edit_date('now');
2347             $copy->editor($e->requestor->id);
2348             $e->update_asset_copy($copy) or return $e->die_event;
2349         }
2350
2351         push(@holds, $hold);
2352     }
2353
2354     if ($e->commit) {
2355
2356         for my $hold (@holds) {
2357
2358             my $copy = $hold->current_copy;
2359
2360             my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
2361
2362             if($alt_hold) {
2363
2364                 # copy is needed for a hold
2365                 $client->respond({action => 'hold', copy => $copy, hold_id => $hold->id});
2366
2367             } elsif($copy->circ_lib != $e->requestor->ws_ou) {
2368
2369                 # copy needs to transit
2370                 $client->respond({action => 'transit', copy => $copy, hold_id => $hold->id});
2371
2372             } else {
2373
2374                 # copy needs to go back to the shelf
2375                 $client->respond({action => 'shelf', copy => $copy, hold_id => $hold->id});
2376             }
2377         }
2378
2379         # tell the client we're done
2380         $client->respond_complete;
2381
2382         # fire off the hold cancelation trigger
2383         my $trigger = OpenSRF::AppSession->connect('open-ils.trigger');
2384
2385         for my $hold (@holds) {
2386
2387             my $req = $trigger->request(
2388                 'open-ils.trigger.event.autocreate', 
2389                 'hold_request.cancel.expire_holds_shelf', 
2390                 $hold, $org_id);
2391
2392             # wait for response so don't flood the service
2393             $req->recv;
2394         }
2395
2396         $trigger->disconnect;
2397
2398     } else {
2399         # tell the client we're done
2400         $client->respond_complete;
2401     }
2402 }
2403
2404 __PACKAGE__->register_method(
2405     method    => 'usr_hold_summary',
2406     api_name  => 'open-ils.circ.holds.user_summary',
2407     signature => q/
2408         Returns a summary of holds statuses for a given user
2409     /
2410 );
2411
2412 sub usr_hold_summary {
2413     my($self, $conn, $auth, $user_id) = @_;
2414
2415         my $e = new_editor(authtoken=>$auth);
2416         $e->checkauth or return $e->event;
2417         $e->allowed('VIEW_HOLD') or return $e->event;
2418
2419     my $holds = $e->search_action_hold_request(
2420         {  
2421             usr =>  $user_id , 
2422             fulfillment_time => undef,
2423             cancel_time      => undef,
2424         }
2425     );
2426
2427     my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
2428     $summary{_hold_status($e, $_)} += 1 for @$holds;
2429     return \%summary;
2430 }
2431
2432
2433
2434 __PACKAGE__->register_method(
2435     method    => 'hold_has_copy_at',
2436     api_name  => 'open-ils.circ.hold.has_copy_at',
2437     signature => {
2438         desc   => 
2439                 'Returns the ID of the found copy and name of the shelving location if there is ' .
2440                 'an available copy at the specified org unit.  Returns empty hash otherwise.  '   .
2441                 'The anticipated use for this method is to determine whether an item is '         .
2442                 'available at the library where the user is placing the hold (or, alternatively, '.
2443                 'at the pickup library) to encourage bypassing the hold placement and just '      .
2444                 'checking out the item.' ,
2445         params => {
2446             { desc => 'Authentication Token', type => 'string' },
2447             { desc => 'Method Arguments.  Options include: hold_type, hold_target, org_unit.  ' 
2448                     . 'hold_type is the hold type code (T, V, C, M, ...).  '
2449                     . 'hold_target is the identifier of the hold target object.  ' 
2450                     . 'org_unit is org unit ID.', 
2451               type => 'object' 
2452             },
2453         },
2454         return => { 
2455             desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
2456             type => 'object' 
2457         }
2458     }
2459 );
2460
2461 sub hold_has_copy_at {
2462     my($self, $conn, $auth, $args) = @_;
2463
2464         my $e = new_editor(authtoken=>$auth);
2465         $e->checkauth or return $e->event;
2466
2467     my $hold_type   = $$args{hold_type};
2468     my $hold_target = $$args{hold_target};
2469     my $org_unit    = $$args{org_unit};
2470
2471     my $query = {
2472         select => {acp => ['id'], acpl => ['name']},
2473         from   => {
2474             acp => {
2475                 acpl => {field => 'id', filter => { holdable => 't'}, fkey => 'location'},
2476                 ccs  => {field => 'id', filter => { holdable => 't'}, fkey => 'status'  }
2477             }
2478         },
2479         where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit}},
2480         limit => 1
2481     };
2482
2483     if($hold_type eq 'C') {
2484
2485         $query->{where}->{'+acp'}->{id} = $hold_target;
2486
2487     } elsif($hold_type eq 'V') {
2488
2489         $query->{where}->{'+acp'}->{call_number} = $hold_target;
2490     
2491     } elsif($hold_type eq 'T') {
2492
2493         $query->{from}->{acp}->{acn} = {
2494             field  => 'id',
2495             fkey   => 'call_number',
2496             'join' => {
2497                 bre => {
2498                     field  => 'id',
2499                     filter => {id => $hold_target},
2500                     fkey   => 'record'
2501                 }
2502             }
2503         };
2504
2505     } else {
2506
2507         $query->{from}->{acp}->{acn} = {
2508             field => 'id',
2509             fkey  => 'call_number',
2510             join  => {
2511                 bre => {
2512                     field => 'id',
2513                     fkey  => 'record',
2514                     join  => {
2515                         mmrsm => {
2516                             field  => 'source',
2517                             fkey   => 'id',
2518                             filter => {metarecord => $hold_target},
2519                         }
2520                     }
2521                 }
2522             }
2523         };
2524     }
2525
2526     my $res = $e->json_query($query)->[0] or return {};
2527     return {copy => $res->{id}, location => $res->{name}} if $res;
2528 }
2529
2530
2531 # returns true if the user already has an item checked out 
2532 # that could be used to fulfill the requested hold.
2533 sub hold_item_is_checked_out {
2534     my($e, $user_id, $hold_type, $hold_target) = @_;
2535
2536     my $query = {
2537         select => {acp => ['id']},
2538         from   => {acp => {}},
2539         where  => {
2540             '+acp' => {
2541                 id => {
2542                     in => { # copies for circs the user has checked out
2543                         select => {circ => ['target_copy']},
2544                         from   => 'circ',
2545                         where  => {
2546                             usr => $user_id,
2547                             checkin_time => undef,
2548                             '-or' => [
2549                                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2550                                 {stop_fines => undef}
2551                             ],
2552                         }
2553                     }
2554                 }
2555             }
2556         },
2557         limit => 1
2558     };
2559
2560     if($hold_type eq 'C') {
2561
2562         $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
2563
2564     } elsif($hold_type eq 'V') {
2565
2566         $query->{where}->{'+acp'}->{call_number} = $hold_target;
2567     
2568     } elsif($hold_type eq 'T') {
2569
2570         $query->{from}->{acp}->{acn} = {
2571             field  => 'id',
2572             fkey   => 'call_number',
2573             'join' => {
2574                 bre => {
2575                     field  => 'id',
2576                     filter => {id => $hold_target},
2577                     fkey   => 'record'
2578                 }
2579             }
2580         };
2581
2582     } else {
2583
2584         $query->{from}->{acp}->{acn} = {
2585             field => 'id',
2586             fkey => 'call_number',
2587             join => {
2588                 bre => {
2589                     field => 'id',
2590                     fkey => 'record',
2591                     join => {
2592                         mmrsm => {
2593                             field => 'source',
2594                             fkey => 'id',
2595                             filter => {metarecord => $hold_target},
2596                         }
2597                     }
2598                 }
2599             }
2600         };
2601     }
2602
2603     return $e->json_query($query)->[0];
2604 }
2605
2606 __PACKAGE__->register_method(
2607     method    => 'change_hold_title',
2608     api_name  => 'open-ils.circ.hold.change_title',
2609     signature => {
2610         desc => q/
2611             Updates all title level holds targeting the specified bibs to point a new bib./,
2612         params => [
2613             { desc => 'Authentication Token', type => 'string' },
2614             { desc => 'New Target Bib Id',    type => 'number' },
2615             { desc => 'Old Target Bib Ids',   type => 'array'  },
2616         ],
2617         return => { desc => '1 on success' }
2618     }
2619 );
2620
2621 sub change_hold_title {
2622     my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
2623
2624     my $e = new_editor(authtoken=>$auth, xact=>1);
2625     return $e->event unless $e->checkauth;
2626
2627     my $holds = $e->search_action_hold_request(
2628         [
2629             {
2630                 cancel_time      => undef,
2631                 fulfillment_time => undef,
2632                 hold_type        => 'T',
2633                 target           => $bib_ids
2634             },
2635             {
2636                 flesh        => 1,
2637                 flesh_fields => { ahr => ['usr'] }
2638             }
2639         ],
2640         { substream => 1 }
2641     );
2642
2643     for my $hold (@$holds) {
2644         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
2645         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
2646         $hold->target( $new_bib_id );
2647         $e->update_action_hold_request($hold) or return $e->die_event;
2648     }
2649
2650     $e->commit;
2651
2652     return 1;
2653 }
2654
2655
2656 __PACKAGE__->register_method(
2657     method    => 'rec_hold_count',
2658     api_name  => 'open-ils.circ.bre.holds.count',
2659     signature => {
2660         desc => q/Returns the total number of holds that target the 
2661             selected bib record or its associated copies and call_numbers/,
2662         params => [
2663             { desc => 'Bib ID', type => 'number' },
2664         ],
2665         return => {desc => 'Hold count', type => 'number'}
2666     }
2667 );
2668
2669 __PACKAGE__->register_method(
2670     method    => 'rec_hold_count',
2671     api_name  => 'open-ils.circ.mmr.holds.count',
2672     signature => {
2673         desc => q/Returns the total number of holds that target the 
2674             selected metarecord or its associated copies, call_numbers, and bib records/,
2675         params => [
2676             { desc => 'Metarecord ID', type => 'number' },
2677         ],
2678         return => {desc => 'Hold count', type => 'number'}
2679     }
2680 );
2681
2682 sub rec_hold_count {
2683     my($self, $conn, $target_id) = @_;
2684
2685
2686     my $mmr_join = {
2687         mmrsm => {
2688             field => 'id',
2689             fkey => 'source',
2690             filter => {metarecord => $target_id}
2691         }
2692     };
2693
2694     my $bre_join = {
2695         bre => {
2696             field => 'id',
2697             filter => { id => $target_id },
2698             fkey => 'record'
2699         }
2700     };
2701
2702     if($self->api_name =~ /mmr/) {
2703         delete $bre_join->{bre}->{filter};
2704         $bre_join->{bre}->{join} = $mmr_join;
2705     }
2706
2707     my $cn_join = {
2708         acn => {
2709             field => 'id',
2710             fkey => 'call_number',
2711             join => $bre_join
2712         }
2713     };
2714
2715     my $query = {
2716         select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
2717         from => 'ahr',
2718         where => {
2719             '+ahr' => {
2720                 cancel_time => undef, 
2721                 fulfillment_time => undef,
2722                 '-or' => [
2723                     {
2724                         '-and' => {
2725                             hold_type => 'C',
2726                             target => {
2727                                 in => {
2728                                     select => {acp => ['id']},
2729                                     from => { acp => $cn_join }
2730                                 }
2731                             }
2732                         }
2733                     },
2734                     {
2735                         '-and' => {
2736                             hold_type => 'V',
2737                             target => {
2738                                 in => {
2739                                     select => {acn => ['id']},
2740                                     from => {acn => $bre_join}
2741                                 }
2742                             }
2743                         }
2744                     },
2745                     {
2746                         '-and' => {
2747                             hold_type => 'T',
2748                             target => $target_id
2749                         }
2750                     }
2751                 ]
2752             }
2753         }
2754     };
2755
2756     if($self->api_name =~ /mmr/) {
2757         $query->{where}->{'+ahr'}->{'-or'}->[2] = {
2758             '-and' => {
2759                 hold_type => 'T',
2760                 target => {
2761                     in => {
2762                         select => {bre => ['id']},
2763                         from => {bre => $mmr_join}
2764                     }
2765                 }
2766             }
2767         };
2768
2769         $query->{where}->{'+ahr'}->{'-or'}->[3] = {
2770             '-and' => {
2771                 hold_type => 'M',
2772                 target => $target_id
2773             }
2774         };
2775     }
2776
2777
2778     return new_editor()->json_query($query)->[0]->{count};
2779 }
2780
2781
2782
2783
2784
2785
2786 1;