9e5b07d4e720e8caadda05d3aed576c3cc0d6003
[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 Data::Dumper;
22 use OpenSRF::EX qw(:try);
23 use OpenILS::Perm;
24 use OpenILS::Event;
25 use OpenSRF::Utils::Logger qw(:logger);
26 use OpenILS::Utils::CStoreEditor q/:funcs/;
27 use OpenILS::Utils::PermitHold;
28 use OpenSRF::Utils::SettingsClient;
29 use OpenILS::Const qw/:const/;
30 use OpenILS::Application::Circ::Transit;
31
32 my $apputils = "OpenILS::Application::AppUtils";
33 my $U = $apputils;
34
35
36
37 __PACKAGE__->register_method(
38         method  => "create_hold",
39         api_name        => "open-ils.circ.holds.create",
40         notes           => <<NOTE);
41 Create a new hold for an item.  From a permissions perspective, 
42 the login session is used as the 'requestor' of the hold.  
43 The hold recipient is determined by the 'usr' setting within
44 the hold object.
45
46 First we verify the requestion has holds request permissions.
47 Then we verify that the recipient is allowed to make the given hold.
48 If not, we see if the requestor has "override" capabilities.  If not,
49 a permission exception is returned.  If permissions allow, we cycle
50 through the set of holds objects and create.
51
52 If the recipient does not have permission to place multiple holds
53 on a single title and said operation is attempted, a permission
54 exception is returned
55 NOTE
56
57
58 __PACKAGE__->register_method(
59         method  => "create_hold",
60         api_name        => "open-ils.circ.holds.create.override",
61         signature       => q/
62                 If the recipient is not allowed to receive the requested hold,
63                 call this method to attempt the override
64                 @see open-ils.circ.holds.create
65         /
66 );
67
68 sub create_hold {
69         my( $self, $conn, $auth, @holds ) = @_;
70         my $e = new_editor(authtoken=>$auth, xact=>1);
71         return $e->event unless $e->checkauth;
72
73         my $override = 1 if $self->api_name =~ /override/;
74
75         my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
76
77 #       my @copyholds;
78
79         for my $hold (@$holds) {
80
81                 next unless $hold;
82                 my @events;
83
84                 my $requestor = $e->requestor;
85                 my $recipient = $requestor;
86
87
88                 if( $requestor->id ne $hold->usr ) {
89                         # Make sure the requestor is allowed to place holds for 
90                         # the recipient if they are not the same people
91                         $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
92                         $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
93                 }
94
95                 # Now make sure the recipient is allowed to receive the specified hold
96                 my $pevt;
97                 my $porg                = $recipient->home_ou;
98                 my $rid         = $e->requestor->id;
99                 my $t                   = $hold->hold_type;
100
101                 # See if a duplicate hold already exists
102                 my $sargs = {
103                         usr                     => $recipient->id, 
104                         hold_type       => $t, 
105                         fulfillment_time => undef, 
106                         target          => $hold->target,
107                         cancel_time     => undef,
108                 };
109
110                 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
111                         
112                 my $existing = $e->search_action_hold_request($sargs); 
113                 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
114
115                 if( $t eq OILS_HOLD_TYPE_METARECORD ) 
116                         { $pevt = $e->event unless $e->allowed('MR_HOLDS', $porg); }
117
118                 if( $t eq OILS_HOLD_TYPE_TITLE ) 
119                         { $pevt = $e->event unless $e->allowed('TITLE_HOLDS', $porg);  }
120
121                 if( $t eq OILS_HOLD_TYPE_VOLUME ) 
122                         { $pevt = $e->event unless $e->allowed('VOLUME_HOLDS', $porg); }
123
124                 if( $t eq OILS_HOLD_TYPE_COPY ) 
125                         { $pevt = $e->event unless $e->allowed('COPY_HOLDS', $porg); }
126
127                 return $pevt if $pevt;
128
129                 if( @events ) {
130                         if( $override ) {
131                                 for my $evt (@events) {
132                                         next unless $evt;
133                                         my $name = $evt->{textcode};
134                                         return $e->event unless $e->allowed("$name.override", $porg);
135                                 }
136                         } else {
137                                 return \@events;
138                         }
139                 }
140
141                 $hold->requestor($e->requestor->id); 
142                 $hold->request_lib($e->requestor->ws_ou);
143                 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
144                 $hold = $e->create_action_hold_request($hold) or return $e->event;
145         }
146
147         $e->commit;
148
149         $conn->respond_complete(1);
150
151     for(@holds) {
152         next if $_->frozen;
153             $U->storagereq(
154                     'open-ils.storage.action.hold_request.copy_targeter', 
155                     undef, $_->id );
156     }
157
158         return undef;
159 }
160
161 sub __create_hold {
162         my( $self, $client, $login_session, @holds) = @_;
163
164         if(!@holds){return 0;}
165         my( $user, $evt ) = $apputils->checkses($login_session);
166         return $evt if $evt;
167
168         my $holds;
169         if(ref($holds[0]) eq 'ARRAY') {
170                 $holds = $holds[0];
171         } else { $holds = [ @holds ]; }
172
173         $logger->debug("Iterating over holds requests...");
174
175         for my $hold (@$holds) {
176
177                 if(!$hold){next};
178                 my $type = $hold->hold_type;
179
180                 $logger->activity("User " . $user->id . 
181                         " creating new hold of type $type for user " . $hold->usr);
182
183                 my $recipient;
184                 if($user->id ne $hold->usr) {
185                         ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
186                         return $evt if $evt;
187
188                 } else {
189                         $recipient = $user;
190                 }
191
192
193                 my $perm = undef;
194
195                 # am I allowed to place holds for this user?
196                 if($hold->requestor ne $hold->usr) {
197                         $perm = _check_request_holds_perm($user->id, $user->home_ou);
198                         if($perm) { return $perm; }
199                 }
200
201                 # is this user allowed to have holds of this type?
202                 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
203         return $perm if $perm;
204
205                 #enforce the fact that the login is the one requesting the hold
206                 $hold->requestor($user->id); 
207                 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
208
209                 my $resp = $apputils->simplereq(
210                         'open-ils.storage',
211                         'open-ils.storage.direct.action.hold_request.create', $hold );
212
213                 if(!$resp) { 
214                         return OpenSRF::EX::ERROR ("Error creating hold"); 
215                 }
216         }
217
218         return 1;
219 }
220
221 # makes sure that a user has permission to place the type of requested hold
222 # returns the Perm exception if not allowed, returns undef if all is well
223 sub _check_holds_perm {
224         my($type, $user_id, $org_id) = @_;
225
226         my $evt;
227         if($type eq "M") {
228                 if($evt = $apputils->check_perms(
229                         $user_id, $org_id, "MR_HOLDS")) {
230                         return $evt;
231                 } 
232
233         } elsif ($type eq "T") {
234                 if($evt = $apputils->check_perms(
235                         $user_id, $org_id, "TITLE_HOLDS")) {
236                         return $evt;
237                 }
238
239         } elsif($type eq "V") {
240                 if($evt = $apputils->check_perms(
241                         $user_id, $org_id, "VOLUME_HOLDS")) {
242                         return $evt;
243                 }
244
245         } elsif($type eq "C") {
246                 if($evt = $apputils->check_perms(
247                         $user_id, $org_id, "COPY_HOLDS")) {
248                         return $evt;
249                 }
250         }
251
252         return undef;
253 }
254
255 # tests if the given user is allowed to place holds on another's behalf
256 sub _check_request_holds_perm {
257         my $user_id = shift;
258         my $org_id = shift;
259         if(my $evt = $apputils->check_perms(
260                 $user_id, $org_id, "REQUEST_HOLDS")) {
261                 return $evt;
262         }
263 }
264
265 __PACKAGE__->register_method(
266         method  => "retrieve_holds_by_id",
267         api_name        => "open-ils.circ.holds.retrieve_by_id",
268         notes           => <<NOTE);
269 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
270 different from the user, then the requestor must have VIEW_HOLD permissions.
271 NOTE
272
273
274 sub retrieve_holds_by_id {
275         my($self, $client, $auth, $hold_id) = @_;
276         my $e = new_editor(authtoken=>$auth);
277         $e->checkauth or return $e->event;
278         $e->allowed('VIEW_HOLD') or return $e->event;
279
280         my $holds = $e->search_action_hold_request(
281                 [
282                         { id =>  $hold_id , fulfillment_time => undef }, 
283                         { order_by => { ahr => "request_time" } }
284                 ]
285         );
286
287         flesh_hold_transits($holds);
288         flesh_hold_notices($holds, $e);
289         return $holds;
290 }
291
292
293 __PACKAGE__->register_method(
294         method  => "retrieve_holds",
295         api_name        => "open-ils.circ.holds.retrieve",
296         notes           => <<NOTE);
297 Retrieves all the holds, with hold transits attached, for the specified
298 user id.  The login session is the requestor and if the requestor is
299 different from the user, then the requestor must have VIEW_HOLD permissions.
300 NOTE
301
302 __PACKAGE__->register_method(
303         method  => "retrieve_holds",
304     authoritative => 1,
305         api_name        => "open-ils.circ.holds.id_list.retrieve",
306         notes           => <<NOTE);
307 Retrieves all the hold ids for the specified
308 user id.  The login session is the requestor and if the requestor is
309 different from the user, then the requestor must have VIEW_HOLD permissions.
310 NOTE
311
312 sub retrieve_holds {
313         my($self, $client, $login_session, $user_id) = @_;
314
315         my( $user, $target, $evt ) = $apputils->checkses_requestor(
316                 $login_session, $user_id, 'VIEW_HOLD' );
317         return $evt if $evt;
318
319         my $holds = $apputils->simplereq(
320                 'open-ils.cstore',
321                 "open-ils.cstore.direct.action.hold_request.search.atomic",
322                 { 
323                         usr =>  $user_id , 
324                         fulfillment_time => undef,
325                         cancel_time => undef,
326                 }, 
327                 { order_by => { ahr => "request_time" } }
328         );
329         
330         if( ! $self->api_name =~ /id_list/ ) {
331                 for my $hold ( @$holds ) {
332                         $hold->transit(
333                                 $apputils->simplereq(
334                                         'open-ils.cstore',
335                                         "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
336                                         { hold => $hold->id },
337                                         { order_by => { ahtc => 'id desc' }, limit => 1 }
338                                 )->[0]
339                         );
340                 }
341         }
342
343         if( $self->api_name =~ /id_list/ ) {
344                 return [ map { $_->id } @$holds ];
345         } else {
346                 return $holds;
347         }
348 }
349
350
351 __PACKAGE__->register_method(
352    method => 'user_hold_count',
353    api_name => 'open-ils.circ.hold.user.count');
354
355 sub user_hold_count {
356    my( $self, $conn, $auth, $userid ) = @_;
357    my $e = new_editor(authtoken=>$auth);
358    return $e->event unless $e->checkauth;
359    my $patron = $e->retrieve_actor_user($userid)
360       or return $e->event;
361    return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
362    return __user_hold_count($self, $e, $userid);
363 }
364
365 sub __user_hold_count {
366    my( $self, $e, $userid ) = @_;
367    my $holds = $e->search_action_hold_request(
368       {  usr =>  $userid , 
369          fulfillment_time => undef,
370          cancel_time => undef,
371       }, 
372       {idlist => 1}
373    );
374
375    return scalar(@$holds);
376 }
377
378
379 __PACKAGE__->register_method(
380         method  => "retrieve_holds_by_pickup_lib",
381         api_name        => "open-ils.circ.holds.retrieve_by_pickup_lib",
382         notes           => <<NOTE);
383 Retrieves all the holds, with hold transits attached, for the specified
384 pickup_ou id. 
385 NOTE
386
387 __PACKAGE__->register_method(
388         method  => "retrieve_holds_by_pickup_lib",
389         api_name        => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
390         notes           => <<NOTE);
391 Retrieves all the hold ids for the specified
392 pickup_ou id. 
393 NOTE
394
395 sub retrieve_holds_by_pickup_lib {
396         my($self, $client, $login_session, $ou_id) = @_;
397
398         #FIXME -- put an appropriate permission check here
399         #my( $user, $target, $evt ) = $apputils->checkses_requestor(
400         #       $login_session, $user_id, 'VIEW_HOLD' );
401         #return $evt if $evt;
402
403         my $holds = $apputils->simplereq(
404                 'open-ils.cstore',
405                 "open-ils.cstore.direct.action.hold_request.search.atomic",
406                 { 
407                         pickup_lib =>  $ou_id , 
408                         fulfillment_time => undef,
409                         cancel_time => undef
410                 }, 
411                 { order_by => { ahr => "request_time" } });
412
413
414         if( ! $self->api_name =~ /id_list/ ) {
415                 flesh_hold_transits($holds);
416         }
417
418         if( $self->api_name =~ /id_list/ ) {
419                 return [ map { $_->id } @$holds ];
420         } else {
421                 return $holds;
422         }
423 }
424
425 __PACKAGE__->register_method(
426         method  => "cancel_hold",
427         api_name        => "open-ils.circ.hold.cancel",
428         notes           => <<"  NOTE");
429         Cancels the specified hold.  The login session
430         is the requestor and if the requestor is different from the usr field
431         on the hold, the requestor must have CANCEL_HOLDS permissions.
432         the hold may be either the hold object or the hold id
433         NOTE
434
435 sub cancel_hold {
436         my($self, $client, $auth, $holdid) = @_;
437
438         my $e = new_editor(authtoken=>$auth, xact=>1);
439         return $e->event unless $e->checkauth;
440
441         my $hold = $e->retrieve_action_hold_request($holdid)
442                 or return $e->event;
443
444         if( $e->requestor->id ne $hold->usr ) {
445                 return $e->event unless $e->allowed('CANCEL_HOLDS');
446         }
447
448         return 1 if $hold->cancel_time;
449
450         # If the hold is captured, reset the copy status
451         if( $hold->capture_time and $hold->current_copy ) {
452
453                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
454                         or return $e->event;
455
456                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
457          $logger->info("canceling hold $holdid whose item is on the holds shelf");
458 #                       $logger->info("setting copy to status 'reshelving' on hold cancel");
459 #                       $copy->status(OILS_COPY_STATUS_RESHELVING);
460 #                       $copy->editor($e->requestor->id);
461 #                       $copy->edit_date('now');
462 #                       $e->update_asset_copy($copy) or return $e->event;
463
464                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
465
466                         my $hid = $hold->id;
467                         $logger->warn("! canceling hold [$hid] that is in transit");
468                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
469
470                         if( $transid ) {
471                                 my $trans = $e->retrieve_action_transit_copy($transid);
472                                 # Leave the transit alive, but  set the copy status to 
473                                 # reshelving so it will be properly reshelved when it gets back home
474                                 if( $trans ) {
475                                         $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
476                                         $e->update_action_transit_copy($trans) or return $e->die_event;
477                                 }
478                         }
479                 }
480         }
481
482         $hold->cancel_time('now');
483         $e->update_action_hold_request($hold)
484                 or return $e->event;
485
486         delete_hold_copy_maps($self, $e, $hold->id);
487
488         $e->commit;
489         return 1;
490 }
491
492 sub delete_hold_copy_maps {
493         my $class = shift;
494         my $editor = shift;
495         my $holdid = shift;
496
497         my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
498         for(@$maps) {
499                 $editor->delete_action_hold_copy_map($_) 
500                         or return $editor->event;
501         }
502         return undef;
503 }
504
505
506 __PACKAGE__->register_method(
507         method  => "update_hold",
508         api_name        => "open-ils.circ.hold.update",
509         notes           => <<"  NOTE");
510         Updates the specified hold.  The login session
511         is the requestor and if the requestor is different from the usr field
512         on the hold, the requestor must have UPDATE_HOLDS permissions.
513         NOTE
514
515 sub update_hold {
516         my($self, $client, $auth, $hold) = @_;
517
518     my $e = new_editor(authtoken=>$auth, xact=>1);
519     return $e->die_event unless $e->checkauth;
520
521     if($hold->usr ne $e->requestor->id) {
522         # if the hold is for a different user, make sure the 
523         # requestor has the appropriate permissions
524         my $usr = $e->retrieve_actor_user($hold->usr)
525             or return $e->die_event;
526         return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
527     }
528
529     my $evt = update_hold_if_frozen($self, $e, $hold);
530     return $evt if $evt;
531
532     $e->update_action_hold_request($hold)
533         or return $e->die_event;
534
535     $e->commit;
536     return $hold->id;
537 }
538
539
540 # if the hold is frozen, this method ensures that the hold is not "targeted", 
541 # that is, it clears the current_copy and prev_check_time to essentiallly 
542 # reset the hold
543 sub update_hold_if_frozen {
544     my($self, $e, $hold) = @_;
545     return undef if $hold->capture_time;
546     if($hold->frozen and ($hold->current_copy or $hold->prev_check_time)) {
547         $logger->info("clearing current_copy and check_time for frozen hold");
548         $hold->clear_current_copy;
549         $hold->clear_prev_check_time;
550         $e->update_action_hold_request($hold) or return $e->die_event;
551     }
552     return undef;
553 }
554
555
556 __PACKAGE__->register_method(
557         method  => "retrieve_hold_status",
558         api_name        => "open-ils.circ.hold.status.retrieve",
559         notes           => <<"  NOTE");
560         Calculates the current status of the hold.
561         the requestor must have VIEW_HOLD permissions if the hold is for a user
562         other than the requestor.
563         Returns -1  on error (for now)
564         Returns 1 for 'waiting for copy to become available'
565         Returns 2 for 'waiting for copy capture'
566         Returns 3 for 'in transit'
567         Returns 4 for 'arrived'
568         NOTE
569
570 sub retrieve_hold_status {
571         my($self, $client, $auth, $hold_id) = @_;
572
573         my $e = new_editor(authtoken => $auth);
574         return $e->event unless $e->checkauth;
575         my $hold = $e->retrieve_action_hold_request($hold_id)
576                 or return $e->event;
577
578         if( $e->requestor->id != $hold->usr ) {
579                 return $e->event unless $e->allowed('VIEW_HOLD');
580         }
581
582         return _hold_status($e, $hold);
583
584 }
585
586 sub _hold_status {
587         my($e, $hold) = @_;
588         return 1 unless $hold->current_copy;
589         return 2 unless $hold->capture_time;
590
591         my $copy = $hold->current_copy;
592         unless( ref $copy ) {
593                 $copy = $e->retrieve_asset_copy($hold->current_copy)
594                         or return $e->event;
595         }
596
597         return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
598         return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
599
600         return -1;
601 }
602
603
604 #sub find_local_hold {
605 #       my( $class, $session, $copy, $user ) = @_;
606 #       return $class->find_nearest_permitted_hold($session, $copy, $user);
607 #}
608
609
610 sub fetch_open_hold_by_current_copy {
611         my $class = shift;
612         my $copyid = shift;
613         my $hold = $apputils->simplereq(
614                 'open-ils.cstore', 
615                 'open-ils.cstore.direct.action.hold_request.search.atomic',
616                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
617         return $hold->[0] if ref($hold);
618         return undef;
619 }
620
621 sub fetch_related_holds {
622         my $class = shift;
623         my $copyid = shift;
624         return $apputils->simplereq(
625                 'open-ils.cstore', 
626                 'open-ils.cstore.direct.action.hold_request.search.atomic',
627                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
628 }
629
630
631 __PACKAGE__->register_method (
632         method          => "hold_pull_list",
633         api_name                => "open-ils.circ.hold_pull_list.retrieve",
634         signature       => q/
635                 Returns a list of holds that need to be "pulled"
636                 by a given location
637         /
638 );
639
640 __PACKAGE__->register_method (
641         method          => "hold_pull_list",
642         api_name                => "open-ils.circ.hold_pull_list.id_list.retrieve",
643         signature       => q/
644                 Returns a list of hold ID's that need to be "pulled"
645                 by a given location
646         /
647 );
648
649
650 sub hold_pull_list {
651         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
652         my( $reqr, $evt ) = $U->checkses($authtoken);
653         return $evt if $evt;
654
655         my $org = $reqr->ws_ou || $reqr->home_ou;
656         # the perm locaiton shouldn't really matter here since holds
657         # will exist all over and VIEW_HOLDS should be universal
658         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
659         return $evt if $evt;
660
661         if( $self->api_name =~ /id_list/ ) {
662                 return $U->storagereq(
663                         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
664                         $org, $limit, $offset ); 
665         } else {
666                 return $U->storagereq(
667                         'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
668                         $org, $limit, $offset ); 
669         }
670 }
671
672 __PACKAGE__->register_method (
673         method          => 'fetch_hold_notify',
674         api_name                => 'open-ils.circ.hold_notification.retrieve_by_hold',
675         signature       => q/ 
676                 Returns a list of hold notification objects based on hold id.
677                 @param authtoken The loggin session key
678                 @param holdid The id of the hold whose notifications we want to retrieve
679                 @return An array of hold notification objects, event on error.
680         /
681 );
682
683 sub fetch_hold_notify {
684         my( $self, $conn, $authtoken, $holdid ) = @_;
685         my( $requestor, $evt ) = $U->checkses($authtoken);
686         return $evt if $evt;
687         my ($hold, $patron);
688         ($hold, $evt) = $U->fetch_hold($holdid);
689         return $evt if $evt;
690         ($patron, $evt) = $U->fetch_user($hold->usr);
691         return $evt if $evt;
692
693         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
694         return $evt if $evt;
695
696         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
697         return $U->cstorereq(
698                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
699 }
700
701
702 __PACKAGE__->register_method (
703         method          => 'create_hold_notify',
704         api_name                => 'open-ils.circ.hold_notification.create',
705         signature       => q/
706                 Creates a new hold notification object
707                 @param authtoken The login session key
708                 @param notification The hold notification object to create
709                 @return ID of the new object on success, Event on error
710                 /
711 );
712 =head old
713 sub __create_hold_notify {
714         my( $self, $conn, $authtoken, $notification ) = @_;
715         my( $requestor, $evt ) = $U->checkses($authtoken);
716         return $evt if $evt;
717         my ($hold, $patron);
718         ($hold, $evt) = $U->fetch_hold($notification->hold);
719         return $evt if $evt;
720         ($patron, $evt) = $U->fetch_user($hold->usr);
721         return $evt if $evt;
722
723         # XXX perm depth probably doesn't matter here -- should always be consortium level
724         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
725         return $evt if $evt;
726
727         # Set the proper notifier 
728         $notification->notify_staff($requestor->id);
729         my $id = $U->storagereq(
730                 'open-ils.storage.direct.action.hold_notification.create', $notification );
731         return $U->DB_UPDATE_FAILED($notification) unless $id;
732         $logger->info("User ".$requestor->id." successfully created new hold notification $id");
733         return $id;
734 }
735 =cut
736
737 sub create_hold_notify {
738    my( $self, $conn, $auth, $note ) = @_;
739    my $e = new_editor(authtoken=>$auth, xact=>1);
740    return $e->die_event unless $e->checkauth;
741
742    my $hold = $e->retrieve_action_hold_request($note->hold)
743       or return $e->die_event;
744    my $patron = $e->retrieve_actor_user($hold->usr) 
745       or return $e->die_event;
746
747    return $e->die_event unless 
748       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
749
750         $note->notify_staff($e->requestor->id);
751    $e->create_action_hold_notification($note) or return $e->die_event;
752    $e->commit;
753    return $note->id;
754 }
755
756
757 __PACKAGE__->register_method(
758         method  => 'reset_hold',
759         api_name        => 'open-ils.circ.hold.reset',
760         signature       => q/
761                 Un-captures and un-targets a hold, essentially returning
762                 it to the state it was in directly after it was placed,
763                 then attempts to re-target the hold
764                 @param authtoken The login session key
765                 @param holdid The id of the hold
766         /
767 );
768
769
770 sub reset_hold {
771         my( $self, $conn, $auth, $holdid ) = @_;
772         my $reqr;
773         my ($hold, $evt) = $U->fetch_hold($holdid);
774         return $evt if $evt;
775         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
776         return $evt if $evt;
777         $evt = _reset_hold($self, $reqr, $hold);
778         return $evt if $evt;
779         return 1;
780 }
781
782 sub _reset_hold {
783         my ($self, $reqr, $hold) = @_;
784
785         my $e = new_editor(xact =>1, requestor => $reqr);
786
787         $logger->info("reseting hold ".$hold->id);
788
789         my $hid = $hold->id;
790
791         if( $hold->capture_time and $hold->current_copy ) {
792
793                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
794                         or return $e->event;
795
796                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
797                         $logger->info("setting copy to status 'reshelving' on hold retarget");
798                         $copy->status(OILS_COPY_STATUS_RESHELVING);
799                         $copy->editor($e->requestor->id);
800                         $copy->edit_date('now');
801                         $e->update_asset_copy($copy) or return $e->event;
802
803                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
804
805                         # We don't want the copy to remain "in transit"
806                         $copy->status(OILS_COPY_STATUS_RESHELVING);
807                         $logger->warn("! reseting hold [$hid] that is in transit");
808                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
809
810                         if( $transid ) {
811                                 my $trans = $e->retrieve_action_transit_copy($transid);
812                                 if( $trans ) {
813                                         $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
814                                         my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
815                                         $logger->info("Transit abort completed with result $evt");
816                                         return $evt unless "$evt" eq 1;
817                                 }
818                         }
819                 }
820         }
821
822         $hold->clear_capture_time;
823         $hold->clear_current_copy;
824
825         $e->update_action_hold_request($hold) or return $e->event;
826         $e->commit;
827
828         $U->storagereq(
829                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
830
831         return undef;
832 }
833
834
835 __PACKAGE__->register_method(
836         method => 'fetch_open_title_holds',
837         api_name        => 'open-ils.circ.open_holds.retrieve',
838         signature       => q/
839                 Returns a list ids of un-fulfilled holds for a given title id
840                 @param authtoken The login session key
841                 @param id the id of the item whose holds we want to retrieve
842                 @param type The hold type - M, T, V, C
843         /
844 );
845
846 sub fetch_open_title_holds {
847         my( $self, $conn, $auth, $id, $type, $org ) = @_;
848         my $e = new_editor( authtoken => $auth );
849         return $e->event unless $e->checkauth;
850
851         $type ||= "T";
852         $org ||= $e->requestor->ws_ou;
853
854 #       return $e->search_action_hold_request(
855 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
856
857         # XXX make me return IDs in the future ^--
858         my $holds = $e->search_action_hold_request(
859                 { 
860                         target                          => $id, 
861                         cancel_time                     => undef, 
862                         hold_type                       => $type, 
863                         fulfillment_time        => undef 
864                 }
865         );
866
867         flesh_hold_transits($holds);
868         return $holds;
869 }
870
871
872 sub flesh_hold_transits {
873         my $holds = shift;
874         for my $hold ( @$holds ) {
875                 $hold->transit(
876                         $apputils->simplereq(
877                                 'open-ils.cstore',
878                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
879                                 { hold => $hold->id },
880                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
881                         )->[0]
882                 );
883         }
884 }
885
886 sub flesh_hold_notices {
887         my( $holds, $e ) = @_;
888         $e ||= new_editor();
889
890         for my $hold (@$holds) {
891                 my $notices = $e->search_action_hold_notification(
892                         [
893                                 { hold => $hold->id },
894                                 { order_by => { anh => 'notify_time desc' } },
895                         ],
896                         {idlist=>1}
897                 );
898
899                 $hold->notify_count(scalar(@$notices));
900                 if( @$notices ) {
901                         my $n = $e->retrieve_action_hold_notification($$notices[0])
902                                 or return $e->event;
903                         $hold->notify_time($n->notify_time);
904                 }
905         }
906 }
907
908
909
910
911 __PACKAGE__->register_method(
912         method => 'fetch_captured_holds',
913         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
914         signature       => q/
915                 Returns a list of un-fulfilled holds for a given title id
916                 @param authtoken The login session key
917                 @param org The org id of the location in question
918         /
919 );
920
921 __PACKAGE__->register_method(
922         method => 'fetch_captured_holds',
923         api_name        => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
924         signature       => q/
925                 Returns a list ids of un-fulfilled holds for a given title id
926                 @param authtoken The login session key
927                 @param org The org id of the location in question
928         /
929 );
930
931 sub fetch_captured_holds {
932         my( $self, $conn, $auth, $org ) = @_;
933
934         my $e = new_editor(authtoken => $auth);
935         return $e->event unless $e->checkauth;
936         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
937
938         $org ||= $e->requestor->ws_ou;
939
940         my $holds = $e->search_action_hold_request(
941                 { 
942                         capture_time            => { "!=" => undef },
943                         current_copy            => { "!=" => undef },
944                         fulfillment_time        => undef,
945                         pickup_lib                      => $org,
946                         cancel_time                     => undef,
947                 }
948         );
949
950         my @res;
951         for my $h (@$holds) {
952                 my $copy = $e->retrieve_asset_copy($h->current_copy)
953                         or return $e->event;
954                 push( @res, $h ) if 
955                         $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
956         }
957
958         if( ! $self->api_name =~ /id_list/ ) {
959                 flesh_hold_transits(\@res);
960                 flesh_hold_notices(\@res, $e);
961         }
962
963         if( $self->api_name =~ /id_list/ ) {
964                 return [ map { $_->id } @res ];
965         } else {
966                 return \@res;
967         }
968 }
969
970
971 __PACKAGE__->register_method(
972         method  => "check_title_hold",
973         api_name        => "open-ils.circ.title_hold.is_possible",
974         notes           => q/
975                 Determines if a hold were to be placed by a given user,
976                 whether or not said hold would have any potential copies
977                 to fulfill it.
978                 @param authtoken The login session key
979                 @param params A hash of named params including:
980                         patronid  - the id of the hold recipient
981                         titleid (brn) - the id of the title to be held
982                         depth   - the hold range depth (defaults to 0)
983         /);
984
985 sub check_title_hold {
986         my( $self, $client, $authtoken, $params ) = @_;
987
988         my %params              = %$params;
989         my $titleid             = $params{titleid} ||"";
990         my $volid               = $params{volume_id};
991         my $copyid              = $params{copy_id};
992         my $mrid                        = $params{mrid} ||"";
993         my $depth               = $params{depth} || 0;
994         my $pickup_lib  = $params{pickup_lib};
995         my $hold_type   = $params{hold_type} || 'T';
996     my $selection_ou = $params{selection_ou} || $pickup_lib;
997
998         my $e = new_editor(authtoken=>$authtoken);
999         return $e->event unless $e->checkauth;
1000         my $patron = $e->retrieve_actor_user($params{patronid})
1001                 or return $e->event;
1002
1003         if( $e->requestor->id ne $patron->id ) {
1004                 return $e->event unless 
1005                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1006         }
1007
1008         return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1009
1010         my $rangelib    = $params{range_lib} || $patron->home_ou;
1011
1012         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1013                 or return $e->event;
1014
1015         $logger->info("checking hold possibility with type $hold_type");
1016
1017         my $copy;
1018         my $volume;
1019         my $title;
1020
1021         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1022
1023                 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1024                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1025                         or return $e->event;
1026                 $title = $e->retrieve_biblio_record_entry($volume->record)
1027                         or return $e->event;
1028                 return verify_copy_for_hold( 
1029                         $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1030
1031         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1032
1033                 $volume = $e->retrieve_asset_call_number($volid)
1034                         or return $e->event;
1035                 $title = $e->retrieve_biblio_record_entry($volume->record)
1036                         or return $e->event;
1037
1038                 return _check_volume_hold_is_possible(
1039                         $volume, $title, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1040
1041         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1042
1043                 return _check_title_hold_is_possible(
1044                         $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1045
1046         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1047
1048                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1049                 my @recs = map { $_->source } @$maps;
1050                 for my $rec (@recs) {
1051                         return 1 if (_check_title_hold_is_possible(
1052                                 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou));
1053                 }
1054                 return 0;       
1055         }
1056 }
1057
1058
1059
1060 sub ___check_title_hold_is_possible {
1061         my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1062
1063         my $limit       = 10;
1064         my $offset      = 0;
1065         my $title;
1066
1067         $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1068
1069         while( $title = $U->storagereq(
1070                                 'open-ils.storage.biblio.record_entry.ranged_tree', 
1071                                 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1072
1073                 last unless 
1074                         ref($title) and 
1075                         ref($title->call_numbers) and 
1076                         @{$title->call_numbers};
1077
1078                 for my $cn (@{$title->call_numbers}) {
1079         
1080                         $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1081         
1082                         for my $copy (@{$cn->copies}) {
1083                                 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1084                                 return 1 if verify_copy_for_hold( 
1085                                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1086                                 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1087                         }
1088                 }
1089
1090                 $offset += $limit;
1091         }
1092         return 0;
1093 }
1094
1095 my %prox_cache;
1096
1097 sub _check_metarecord_hold_is_possible {
1098         my( $mrid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1099    
1100    my $e = new_editor();
1101
1102     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given metarecord
1103     my $copies = $e->json_query(
1104         { 
1105             select => { acp => ['id', 'circ_lib'] },
1106             from => {
1107                 acp => {
1108                     acn => {
1109                         field => 'id',
1110                         fkey => 'call_number',
1111                         'join' => {
1112                             mmrsm => {
1113                                 field => 'source',
1114                                 fkey => 'record',
1115                                 filter => { metarecord => $mrid }
1116                             }
1117                         }
1118                     },
1119                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1120                     ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1121                 }
1122             }, 
1123             where => {
1124                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't' }
1125             }
1126         }
1127     );
1128
1129    return $e->event unless defined $copies;
1130    $logger->info("metarecord possible found ".scalar(@$copies)." potential copies");
1131    return 0 unless @$copies;
1132
1133    # -----------------------------------------------------------------------
1134    # sort the copies into buckets based on their circ_lib proximity to 
1135    # the patron's home_ou.  
1136    # -----------------------------------------------------------------------
1137
1138    my $home_org = $patron->home_ou;
1139    my $req_org = $request_lib->id;
1140
1141    my $home_prox = 
1142       ($prox_cache{$home_org}) ? 
1143          $prox_cache{$home_org} :
1144          $prox_cache{$home_org} = $e->search_actor_org_unit_proximity({from_org => $home_org});
1145
1146    my %buckets;
1147    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1148    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1149
1150    my @keys = sort { $a <=> $b } keys %buckets;
1151
1152
1153    if( $home_org ne $req_org ) {
1154       # -----------------------------------------------------------------------
1155       # shove the copies close to the request_lib into the primary buckets 
1156       # directly before the farthest away copies.  That way, they are not 
1157       # given priority, but they are checked before the farthest copies.
1158       # -----------------------------------------------------------------------
1159       my $req_prox = 
1160          ($prox_cache{$req_org}) ? 
1161             $prox_cache{$req_org} :
1162             $prox_cache{$req_org} = $e->search_actor_org_unit_proximity({from_org => $req_org});
1163
1164       my %buckets2;
1165       my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1166       push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1167
1168       my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1169       my $new_key = $highest_key - 0.5; # right before the farthest prox
1170       my @keys2 = sort { $a <=> $b } keys %buckets2;
1171       for my $key (@keys2) {
1172          last if $key >= $highest_key;
1173          push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1174       }
1175    }
1176
1177    @keys = sort { $a <=> $b } keys %buckets;
1178
1179    my %seen;
1180    for my $key (@keys) {
1181       my @cps = @{$buckets{$key}};
1182
1183       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1184
1185       for my $copyid (@cps) {
1186
1187          next if $seen{$copyid};
1188          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1189          my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1190          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1191
1192          my $vol = $e->retrieve_asset_call_number(
1193            [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1194
1195          return 1 if verify_copy_for_hold( 
1196             $patron, $requestor, $vol->record, $copy, $pickup_lib, $request_lib );
1197    
1198       }
1199    }
1200
1201    return 0;
1202 }
1203
1204 sub create_ranged_org_filter {
1205     my($e, $selection_ou, $depth) = @_;
1206
1207     # find the orgs from which this hold may be fulfilled, 
1208     # based on the selection_ou and depth
1209
1210     my $top_org = $e->search_actor_org_unit([
1211         {parent_ou => undef}, 
1212         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1213     my %org_filter;
1214
1215     return () if $depth == $top_org->ou_type->depth;
1216
1217     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1218     %org_filter = (circ_lib => []);
1219     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1220
1221     $logger->info("hold org filter at depth $depth and selection_ou ".
1222         "$selection_ou created list of @{$org_filter{circ_lib}}");
1223
1224     return %org_filter;
1225 }
1226
1227
1228 sub _check_title_hold_is_possible {
1229         my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1230    
1231     my $e = new_editor();
1232     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1233
1234     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1235     my $copies = $e->json_query(
1236         { 
1237             select => { acp => ['id', 'circ_lib'] },
1238             from => {
1239                 acp => {
1240                     acn => {
1241                         field => 'id',
1242                         fkey => 'call_number',
1243                         'join' => {
1244                             bre => {
1245                                 field => 'id',
1246                                 filter => { id => $titleid },
1247                                 fkey => 'record'
1248                             }
1249                         }
1250                     },
1251                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1252                     ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1253                 }
1254             }, 
1255             where => {
1256                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1257             }
1258         }
1259     );
1260
1261    return $e->event unless defined $copies;
1262    $logger->info("title possible found ".scalar(@$copies)." potential copies");
1263    return 0 unless @$copies;
1264
1265    # -----------------------------------------------------------------------
1266    # sort the copies into buckets based on their circ_lib proximity to 
1267    # the patron's home_ou.  
1268    # -----------------------------------------------------------------------
1269
1270    my $home_org = $patron->home_ou;
1271    my $req_org = $request_lib->id;
1272
1273    my $home_prox = 
1274       ($prox_cache{$home_org}) ? 
1275          $prox_cache{$home_org} :
1276          $prox_cache{$home_org} = $e->search_actor_org_unit_proximity({from_org => $home_org});
1277
1278    my %buckets;
1279    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1280    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1281
1282    my @keys = sort { $a <=> $b } keys %buckets;
1283
1284
1285    if( $home_org ne $req_org ) {
1286       # -----------------------------------------------------------------------
1287       # shove the copies close to the request_lib into the primary buckets 
1288       # directly before the farthest away copies.  That way, they are not 
1289       # given priority, but they are checked before the farthest copies.
1290       # -----------------------------------------------------------------------
1291       my $req_prox = 
1292          ($prox_cache{$req_org}) ? 
1293             $prox_cache{$req_org} :
1294             $prox_cache{$req_org} = $e->search_actor_org_unit_proximity({from_org => $req_org});
1295
1296       my %buckets2;
1297       my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1298       push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1299
1300       my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1301       my $new_key = $highest_key - 0.5; # right before the farthest prox
1302       my @keys2 = sort { $a <=> $b } keys %buckets2;
1303       for my $key (@keys2) {
1304          last if $key >= $highest_key;
1305          push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1306       }
1307    }
1308
1309    @keys = sort { $a <=> $b } keys %buckets;
1310
1311    my $title;
1312    my %seen;
1313    for my $key (@keys) {
1314       my @cps = @{$buckets{$key}};
1315
1316       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1317
1318       for my $copyid (@cps) {
1319
1320          next if $seen{$copyid};
1321          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1322          my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1323          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1324
1325          unless($title) { # grab the title if we don't already have it
1326             my $vol = $e->retrieve_asset_call_number(
1327                [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1328             $title = $vol->record;
1329          }
1330    
1331          return 1 if verify_copy_for_hold( 
1332             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1333    
1334       }
1335    }
1336
1337    return 0;
1338 }
1339
1340
1341 sub _check_volume_hold_is_possible {
1342         my( $vol, $title, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1343     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1344         my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1345         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1346         for my $copy ( @$copies ) {
1347                 return 1 if verify_copy_for_hold( 
1348                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1349         }
1350         return 0;
1351 }
1352
1353
1354
1355 sub verify_copy_for_hold {
1356         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1357         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1358         return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1359                 {       patron                          => $patron, 
1360                         requestor                       => $requestor, 
1361                         copy                            => $copy,
1362                         title                           => $title, 
1363                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1364                         pickup_lib                      => $pickup_lib,
1365                         request_lib                     => $request_lib,
1366             new_hold            => 1
1367                 } 
1368         );
1369         return 0;
1370 }
1371
1372
1373
1374 sub find_nearest_permitted_hold {
1375
1376         my $class       = shift;
1377         my $editor      = shift; # CStoreEditor object
1378         my $copy                = shift; # copy to target
1379         my $user                = shift; # staff 
1380         my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1381         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1382
1383         my $bc = $copy->barcode;
1384
1385         # find any existing holds that already target this copy
1386         my $old_holds = $editor->search_action_hold_request(
1387                 {       current_copy => $copy->id, 
1388                         cancel_time => undef, 
1389                         capture_time => undef 
1390                 } 
1391         );
1392
1393         # hold->type "R" means we need this copy
1394         for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1395
1396
1397     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.hold_stalling.soft');
1398
1399         $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1400         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1401
1402         # search for what should be the best holds for this copy to fulfill
1403         my $best_holds = $U->storagereq(
1404                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1405                 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1406
1407         unless(@$best_holds) {
1408
1409                 if( my $hold = $$old_holds[0] ) {
1410                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1411                         return ($hold);
1412                 }
1413
1414                 $logger->info("circulator: no suitable holds found for copy $bc");
1415                 return (undef, $evt);
1416         }
1417
1418
1419         my $best_hold;
1420
1421         # for each potential hold, we have to run the permit script
1422         # to make sure the hold is actually permitted.
1423         for my $holdid (@$best_holds) {
1424                 next unless $holdid;
1425                 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1426
1427                 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1428                 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1429                 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1430
1431                 # see if this hold is permitted
1432                 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1433                         {       patron_id                       => $hold->usr,
1434                                 requestor                       => $reqr,
1435                                 copy                            => $copy,
1436                                 pickup_lib                      => $hold->pickup_lib,
1437                                 request_lib                     => $rlib,
1438                         } 
1439                 );
1440
1441                 if( $permitted ) {
1442                         $best_hold = $hold;
1443                         last;
1444                 }
1445         }
1446
1447
1448         unless( $best_hold ) { # no "good" permitted holds were found
1449                 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1450                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1451                         return ($hold);
1452                 }
1453
1454                 # we got nuthin
1455                 $logger->info("circulator: no suitable holds found for copy $bc");
1456                 return (undef, $evt);
1457         }
1458
1459         $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1460
1461         # indicate a permitted hold was found
1462         return $best_hold if $check_only;
1463
1464         # we've found a permitted hold.  we need to "grab" the copy 
1465         # to prevent re-targeted holds (next part) from re-grabbing the copy
1466         $best_hold->current_copy($copy->id);
1467         $editor->update_action_hold_request($best_hold) 
1468                 or return (undef, $editor->event);
1469
1470
1471     my $retarget = 0;
1472
1473         # re-target any other holds that already target this copy
1474         for my $old_hold (@$old_holds) {
1475                 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1476                 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1477             $old_hold->id." after a better hold [".$best_hold->id."] was found");
1478         $old_hold->clear_current_copy;
1479         $old_hold->clear_prev_check_time;
1480         $editor->update_action_hold_request($old_hold) 
1481             or return (undef, $editor->event);
1482         $retarget = 1;
1483         }
1484
1485         return ($best_hold, undef, $retarget);
1486 }
1487
1488
1489
1490
1491
1492
1493 __PACKAGE__->register_method(
1494         method => 'all_rec_holds',
1495         api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1496 );
1497
1498 sub all_rec_holds {
1499         my( $self, $conn, $auth, $title_id, $args ) = @_;
1500
1501         my $e = new_editor(authtoken=>$auth);
1502         $e->checkauth or return $e->event;
1503         $e->allowed('VIEW_HOLD') or return $e->event;
1504
1505         $args ||= { fulfillment_time => undef };
1506         $args->{cancel_time} = undef;
1507
1508         my $resp = { volume_holds => [], copy_holds => [] };
1509
1510         $resp->{title_holds} = $e->search_action_hold_request(
1511                 { 
1512                         hold_type => OILS_HOLD_TYPE_TITLE, 
1513                         target => $title_id, 
1514                         %$args 
1515                 }, {idlist=>1} );
1516
1517         my $vols = $e->search_asset_call_number(
1518                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1519
1520         return $resp unless @$vols;
1521
1522         $resp->{volume_holds} = $e->search_action_hold_request(
1523                 { 
1524                         hold_type => OILS_HOLD_TYPE_VOLUME, 
1525                         target => $vols,
1526                         %$args }, 
1527                 {idlist=>1} );
1528
1529         my $copies = $e->search_asset_copy(
1530                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1531
1532         return $resp unless @$copies;
1533
1534         $resp->{copy_holds} = $e->search_action_hold_request(
1535                 { 
1536                         hold_type => OILS_HOLD_TYPE_COPY,
1537                         target => $copies,
1538                         %$args }, 
1539                 {idlist=>1} );
1540
1541         return $resp;
1542 }
1543
1544
1545
1546
1547
1548 __PACKAGE__->register_method(
1549         method => 'uber_hold',
1550     authoritative => 1,
1551         api_name => 'open-ils.circ.hold.details.retrieve'
1552 );
1553
1554 sub uber_hold {
1555         my($self, $client, $auth, $hold_id) = @_;
1556         my $e = new_editor(authtoken=>$auth);
1557         $e->checkauth or return $e->event;
1558         $e->allowed('VIEW_HOLD') or return $e->event;
1559
1560         my $resp = {};
1561
1562         my $hold = $e->retrieve_action_hold_request(
1563                 [
1564                         $hold_id,
1565                         {
1566                                 flesh => 1,
1567                                 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1568                         }
1569                 ]
1570         ) or return $e->event;
1571
1572         my $user = $hold->usr;
1573         $hold->usr($user->id);
1574
1575         my $card = $e->retrieve_actor_card($user->card)
1576                 or return $e->event;
1577
1578         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1579
1580         flesh_hold_notices([$hold], $e);
1581         flesh_hold_transits([$hold]);
1582
1583         return {
1584                 hold            => $hold,
1585                 copy            => $copy,
1586                 volume  => $volume,
1587                 mvr             => $mvr,
1588                 status  => _hold_status($e, $hold),
1589                 patron_first => $user->first_given_name,
1590                 patron_last  => $user->family_name,
1591                 patron_barcode => $card->barcode,
1592         };
1593 }
1594
1595
1596
1597 # -----------------------------------------------------
1598 # Returns the MVR object that represents what the
1599 # hold is all about
1600 # -----------------------------------------------------
1601 sub find_hold_mvr {
1602         my( $e, $hold ) = @_;
1603
1604         my $tid;
1605         my $copy;
1606         my $volume;
1607
1608         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1609                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1610                         or return $e->event;
1611                 $tid = $mr->master_record;
1612
1613         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1614                 $tid = $hold->target;
1615
1616         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1617                 $volume = $e->retrieve_asset_call_number($hold->target)
1618                         or return $e->event;
1619                 $tid = $volume->record;
1620
1621         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1622                 $copy = $e->retrieve_asset_copy($hold->target)
1623                         or return $e->event;
1624                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1625                         or return $e->event;
1626                 $tid = $volume->record;
1627         }
1628
1629         if(!$copy and ref $hold->current_copy ) {
1630                 $copy = $hold->current_copy;
1631                 $hold->current_copy($copy->id);
1632         }
1633
1634         if(!$volume and $copy) {
1635                 $volume = $e->retrieve_asset_call_number($copy->call_number);
1636         }
1637
1638         my $title = $e->retrieve_biblio_record_entry($tid);
1639         return ( $U->record_to_mvr($title), $volume, $copy );
1640 }
1641
1642
1643
1644
1645 1;