]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
fixed perm checks
[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/OpenSRF::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
31 my $apputils = "OpenILS::Application::AppUtils";
32 my $U = $apputils;
33
34
35
36 __PACKAGE__->register_method(
37         method  => "create_hold",
38         api_name        => "open-ils.circ.holds.create",
39         notes           => <<NOTE);
40 Create a new hold for an item.  From a permissions perspective, 
41 the login session is used as the 'requestor' of the hold.  
42 The hold recipient is determined by the 'usr' setting within
43 the hold object.
44
45 First we verify the requestion has holds request permissions.
46 Then we verify that the recipient is allowed to make the given hold.
47 If not, we see if the requestor has "override" capabilities.  If not,
48 a permission exception is returned.  If permissions allow, we cycle
49 through the set of holds objects and create.
50
51 If the recipient does not have permission to place multiple holds
52 on a single title and said operation is attempted, a permission
53 exception is returned
54 NOTE
55
56
57 __PACKAGE__->register_method(
58         method  => "create_hold",
59         api_name        => "open-ils.circ.holds.create.override",
60         signature       => q/
61                 If the recipient is not allowed to receive the requested hold,
62                 call this method to attempt the override
63                 @see open-ils.circ.holds.create
64         /
65 );
66
67 sub create_hold {
68         my( $self, $conn, $auth, @holds ) = @_;
69         my $e = new_editor(authtoken=>$auth, xact=>1);
70         return $e->event unless $e->checkauth;
71
72         my $override = 1 if $self->api_name =~ /override/;
73
74         my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
75
76         for my $hold (@$holds) {
77
78                 next unless $hold;
79                 my @events;
80
81                 my $requestor = $e->requestor;
82                 my $recipient = $requestor;
83
84
85                 if( $requestor->id ne $hold->usr ) {
86                         # Make sure the requestor is allowed to place holds for 
87                         # the recipient if they are not the same people
88                         $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
89                         $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
90                 }
91
92
93                 # Now make sure the recipient is allowed to receive the specified hold
94                 my $pevt;
95                 my $porg                = $recipient->home_ou;
96                 my $rid         = $e->requestor->id;
97                 my $t                   = $hold->hold_type;
98
99                 # See if a duplicate hold already exists
100                 my $sargs = {
101                         usr                     => $recipient->id, 
102                         hold_type       => $t, 
103                         fulfillment_time => undef, 
104                         target          => $hold->target,
105                         cancel_time     => undef,
106                 };
107
108                 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
109                         
110                 my $existing = $e->search_action_hold_request($sargs); 
111                 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
112
113                 if( $t eq 'M' ) { $pevt = $e->event unless $e->checkperm($rid, $porg, 'MR_HOLDS'); }
114                 if( $t eq 'T' ) { $pevt = $e->event unless $e->checkperm($rid, $porg, 'TITLE_HOLDS');  }
115                 if( $t eq 'V' ) { $pevt = $e->event unless $e->checkperm($rid, $porg, 'VOLUME_HOLDS'); }
116                 if( $t eq 'C' ) { $pevt = $e->event unless $e->checkperm($rid, $porg, 'COPY_HOLDS'); }
117
118                 return $pevt if $pevt;
119
120                 if( @events ) {
121                         if( $override ) {
122                                 for my $evt (@events) {
123                                         next unless $evt;
124                                         my $name = $evt->{textcode};
125                                         return $e->event unless $e->allowed("$name.override", $porg);
126                                 }
127                         } else {
128                                 return \@events;
129                         }
130                 }
131
132
133 #               if( $eevt ) {
134 #                       if( $override ) {
135 #                               return $e->event unless $e->allowed('CREATE_DUPLICATE_HOLDS', $porg);
136 #                       } else {
137 #                               return $eevt;
138 #                       }
139 #               }
140
141
142                 $hold->requestor($e->requestor->id); 
143                 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
144                 $e->create_action_hold_request($hold) or return $e->event;
145         }
146
147         $e->commit;
148         return 1;
149 }
150
151 sub __create_hold {
152         my( $self, $client, $login_session, @holds) = @_;
153
154         if(!@holds){return 0;}
155         my( $user, $evt ) = $apputils->checkses($login_session);
156         return $evt if $evt;
157
158         my $holds;
159         if(ref($holds[0]) eq 'ARRAY') {
160                 $holds = $holds[0];
161         } else { $holds = [ @holds ]; }
162
163         $logger->debug("Iterating over holds requests...");
164
165         for my $hold (@$holds) {
166
167                 if(!$hold){next};
168                 my $type = $hold->hold_type;
169
170                 $logger->activity("User " . $user->id . 
171                         " creating new hold of type $type for user " . $hold->usr);
172
173                 my $recipient;
174                 if($user->id ne $hold->usr) {
175                         ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
176                         return $evt if $evt;
177
178                 } else {
179                         $recipient = $user;
180                 }
181
182
183                 my $perm = undef;
184
185                 # am I allowed to place holds for this user?
186                 if($hold->requestor ne $hold->usr) {
187                         $perm = _check_request_holds_perm($user->id, $user->home_ou);
188                         if($perm) { return $perm; }
189                 }
190
191                 # is this user allowed to have holds of this type?
192                 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
193                 if($perm) { 
194                         #if there is a requestor, see if the requestor has override privelages
195                         if($hold->requestor ne $hold->usr) {
196                                 $perm = _check_request_holds_override($user->id, $user->home_ou);
197                                 if($perm) {return $perm;}
198
199                         } else {
200                                 return $perm; 
201                         }
202                 }
203
204                 #enforce the fact that the login is the one requesting the hold
205                 $hold->requestor($user->id); 
206                 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
207
208                 my $resp = $apputils->simplereq(
209                         'open-ils.storage',
210                         'open-ils.storage.direct.action.hold_request.create', $hold );
211
212                 if(!$resp) { 
213                         return OpenSRF::EX::ERROR ("Error creating hold"); 
214                 }
215         }
216
217         return 1;
218 }
219
220 # makes sure that a user has permission to place the type of requested hold
221 # returns the Perm exception if not allowed, returns undef if all is well
222 sub _check_holds_perm {
223         my($type, $user_id, $org_id) = @_;
224
225         my $evt;
226         if($type eq "M") {
227                 if($evt = $apputils->check_perms(
228                         $user_id, $org_id, "MR_HOLDS")) {
229                         return $evt;
230                 } 
231
232         } elsif ($type eq "T") {
233                 if($evt = $apputils->check_perms(
234                         $user_id, $org_id, "TITLE_HOLDS")) {
235                         return $evt;
236                 }
237
238         } elsif($type eq "V") {
239                 if($evt = $apputils->check_perms(
240                         $user_id, $org_id, "VOLUME_HOLDS")) {
241                         return $evt;
242                 }
243
244         } elsif($type eq "C") {
245                 if($evt = $apputils->check_perms(
246                         $user_id, $org_id, "COPY_HOLDS")) {
247                         return $evt;
248                 }
249         }
250
251         return undef;
252 }
253
254 # tests if the given user is allowed to place holds on another's behalf
255 sub _check_request_holds_perm {
256         my $user_id = shift;
257         my $org_id = shift;
258         if(my $evt = $apputils->check_perms(
259                 $user_id, $org_id, "REQUEST_HOLDS")) {
260                 return $evt;
261         }
262 }
263
264 sub _check_request_holds_override {
265         my $user_id = shift;
266         my $org_id = shift;
267         if(my $evt = $apputils->check_perms(
268                 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
269                 return $evt;
270         }
271 }
272
273 __PACKAGE__->register_method(
274         method  => "retrieve_holds_by_id",
275         api_name        => "open-ils.circ.holds.retrieve_by_id",
276         notes           => <<NOTE);
277 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
278 different from the user, then the requestor must have VIEW_HOLD permissions.
279 NOTE
280
281
282 sub retrieve_holds_by_id {
283         my($self, $client, $login_session, $hold_id) = @_;
284
285         #FIXME
286         #my( $user, $target, $evt ) = $apputils->checkses_requestor(
287         #       $login_session, $user_id, 'VIEW_HOLD' );
288         #return $evt if $evt;
289
290         my $holds = $apputils->simplereq(
291                 'open-ils.cstore',
292                 "open-ils.cstore.direct.action.hold_request.search.atomic",
293                 { id =>  $hold_id , fulfillment_time => undef }, 
294                 { order_by => { ahr => "request_time" } }
295         );
296
297         flesh_hold_transits($holds);
298         return $holds;
299 }
300
301
302 __PACKAGE__->register_method(
303         method  => "retrieve_holds",
304         api_name        => "open-ils.circ.holds.retrieve",
305         notes           => <<NOTE);
306 Retrieves all the holds, with hold transits attached, for the specified
307 user id.  The login session is the requestor and if the requestor is
308 different from the user, then the requestor must have VIEW_HOLD permissions.
309 NOTE
310
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         for my $hold ( @$holds ) {
331                 $hold->transit(
332                         $apputils->simplereq(
333                                 'open-ils.cstore',
334                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
335                                 { hold => $hold->id },
336                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
337                         )->[0]
338                 );
339         }
340
341         return $holds;
342 }
343
344 __PACKAGE__->register_method(
345         method  => "retrieve_holds_by_pickup_lib",
346         api_name        => "open-ils.circ.holds.retrieve_by_pickup_lib",
347         notes           => <<NOTE);
348 Retrieves all the holds, with hold transits attached, for the specified
349 pickup_ou id. 
350 NOTE
351
352
353 sub retrieve_holds_by_pickup_lib {
354         my($self, $client, $login_session, $ou_id) = @_;
355
356         #FIXME -- put an appropriate permission check here
357         #my( $user, $target, $evt ) = $apputils->checkses_requestor(
358         #       $login_session, $user_id, 'VIEW_HOLD' );
359         #return $evt if $evt;
360
361         my $holds = $apputils->simplereq(
362                 'open-ils.cstore',
363                 "open-ils.cstore.direct.action.hold_request.search.atomic",
364                 { 
365                         pickup_lib =>  $ou_id , 
366                         fulfillment_time => undef,
367                         cancel_time => undef
368                 }, 
369                 { order_by => { ahr => "request_time" } });
370
371
372         flesh_hold_transits($holds);
373         return $holds;
374 }
375
376
377 __PACKAGE__->register_method(
378         method  => "cancel_hold",
379         api_name        => "open-ils.circ.hold.cancel",
380         notes           => <<"  NOTE");
381         Cancels the specified hold.  The login session
382         is the requestor and if the requestor is different from the usr field
383         on the hold, the requestor must have CANCEL_HOLDS permissions.
384         the hold may be either the hold object or the hold id
385         NOTE
386
387 sub cancel_hold {
388         my($self, $client, $auth, $holdid) = @_;
389
390         my $e = new_editor(authtoken=>$auth, xact=>1);
391         return $e->event unless $e->checkauth;
392
393         my $hold = $e->retrieve_action_hold_request($holdid)
394                 or return $e->event;
395
396         if( $e->requestor->id ne $hold->usr ) {
397                 return $e->event unless $e->allowed('CANCEL_HOLDS');
398         }
399
400         return 1 if $hold->cancel_time;
401
402         # If the hold is captured, reset the copy status
403         if( $hold->capture_time and $hold->current_copy ) {
404
405                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
406                         or return $e->event;
407
408                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
409                         $logger->info("setting copy to status 'reshelving' on hold cancel");
410                         $copy->status(OILS_COPY_STATUS_RESHELVING);
411                         $copy->editor($e->requestor->id);
412                         $copy->edit_date('now');
413                         $e->update_asset_copy($copy) or return $e->event;
414                 }
415         }
416
417         $hold->cancel_time('now');
418         $e->update_action_hold_request($hold)
419                 or return $e->event;
420
421         $self->delete_hold_copy_maps($e, $hold->id);
422
423         $e->commit;
424         return 1;
425 }
426
427 sub delete_hold_copy_maps {
428         my $class = shift;
429         my $editor = shift;
430         my $holdid = shift;
431
432         my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
433         for(@$maps) {
434                 $editor->delete_action_hold_copy_map($_) 
435                         or return $editor->event;
436         }
437         return undef;
438 }
439
440
441 __PACKAGE__->register_method(
442         method  => "update_hold",
443         api_name        => "open-ils.circ.hold.update",
444         notes           => <<"  NOTE");
445         Updates the specified hold.  The login session
446         is the requestor and if the requestor is different from the usr field
447         on the hold, the requestor must have UPDATE_HOLDS permissions.
448         NOTE
449
450 sub update_hold {
451         my($self, $client, $login_session, $hold) = @_;
452
453         my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
454                 $login_session, $hold->usr, 'UPDATE_HOLD' );
455         return $evt if $evt;
456
457         $logger->activity('User ' . $requestor->id . 
458                 ' updating hold ' . $hold->id . ' for user ' . $target->id );
459
460         return $U->storagereq(
461                 "open-ils.storage.direct.action.hold_request.update", $hold );
462 }
463
464
465 __PACKAGE__->register_method(
466         method  => "retrieve_hold_status",
467         api_name        => "open-ils.circ.hold.status.retrieve",
468         notes           => <<"  NOTE");
469         Calculates the current status of the hold.
470         the requestor must have VIEW_HOLD permissions if the hold is for a user
471         other than the requestor.
472         Returns -1  on error (for now)
473         Returns 1 for 'waiting for copy to become available'
474         Returns 2 for 'waiting for copy capture'
475         Returns 3 for 'in transit'
476         Returns 4 for 'arrived'
477         NOTE
478
479 sub retrieve_hold_status {
480         my($self, $client, $auth, $hold_id) = @_;
481
482         my $e = new_editor(authtoken => $auth);
483         return $e->event unless $e->checkauth;
484         my $hold = $e->retrieve_action_hold_request($hold_id)
485                 or return $e->event;
486
487         if( $e->requestor->id != $hold->usr ) {
488                 return $e->event unless $e->allowed('VIEW_HOLD');
489         }
490
491         return 1 unless $hold->current_copy;
492         return 2 unless $hold->capture_time;
493
494         my $copy = $e->retrieve_asset_copy($hold->current_copy)
495                 or return $e->event;
496
497         return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
498         return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
499
500         return -1;
501 }
502
503
504
505
506
507 =head DEPRECATED
508 __PACKAGE__->register_method(
509         method  => "capture_copy",
510         api_name        => "open-ils.circ.hold.capture_copy.barcode",
511         notes           => <<"  NOTE");
512         Captures a copy to fulfil a hold
513         Params is login session and copy barcode
514         Optional param is 'flesh'.  If set, we also return the
515         relevant copy and title
516         login mus have COPY_CHECKIN permissions (since this is essentially
517         copy checkin)
518         NOTE
519
520 # XXX deprecate me XXX
521
522 sub capture_copy {
523         my( $self, $client, $login_session, $params ) = @_;
524         my %params = %$params;
525         my $barcode = $params{barcode};
526
527
528         my( $user, $target, $copy, $hold, $evt );
529
530         ( $user, $evt ) = $apputils->checkses($login_session);
531         return $evt if $evt;
532
533         # am I allowed to checkin a copy?
534         $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
535         return $evt if $evt;
536
537         $logger->info("Capturing copy with barcode $barcode");
538
539         my $session = $apputils->start_db_session();
540
541         ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
542         return $evt if $evt;
543
544         $logger->debug("Capturing copy " . $copy->id);
545
546         #( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
547         ( $hold, $evt ) = $self->find_nearest_permitted_hold($session, $copy, $user);
548         return $evt if $evt;
549
550         warn "Found hold " . $hold->id . "\n";
551         $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
552
553         $hold->current_copy($copy->id);
554         $hold->capture_time("now"); 
555
556         #update the hold
557         my $stat = $session->request(
558                         "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
559         if(!$stat) { throw OpenSRF::EX::ERROR 
560                 ("Error updating hold request " . $copy->id); }
561
562         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF); #status on holds shelf
563
564         # if the staff member capturing this item is not at the pickup lib
565         if( $user->home_ou ne $hold->pickup_lib ) {
566                 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
567         }
568
569         $copy->editor($user->id);
570         $copy->edit_date("now");
571         $stat = $session->request(
572                 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
573         if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
574
575         my $payload = { hold => $hold };
576         $payload->{copy} = $copy if $params{flesh_copy};
577
578         if($params{flesh_record}) {
579                 my $record;
580                 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
581                 return $evt if $evt;
582                 $record = $apputils->record_to_mvr($record);
583                 $payload->{record} = $record;
584         }
585
586         $apputils->commit_db_session($session);
587
588         return OpenILS::Event->new('ROUTE_ITEM', 
589                 route_to => $hold->pickup_lib, payload => $payload );
590 }
591
592 sub _build_hold_transit {
593         my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
594         my $trans = Fieldmapper::action::hold_transit_copy->new;
595
596         $trans->hold($hold->id);
597         $trans->source($user->home_ou);
598         $trans->dest($hold->pickup_lib);
599         $trans->source_send_time("now");
600         $trans->target_copy($copy->id);
601         $trans->copy_status($copy->status);
602
603         my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
604         my ($stat) = $meth->run( $login_session, $trans, $session );
605         if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
606         else { $copy->status(6); } #status in transit 
607 }
608
609
610
611 __PACKAGE__->register_method(
612         method  => "create_hold_transit",
613         api_name        => "open-ils.circ.hold_transit.create",
614         notes           => <<"  NOTE");
615         Creates a new transit object
616         NOTE
617
618 sub create_hold_transit {
619         my( $self, $client, $login_session, $transit, $session ) = @_;
620
621         my( $user, $evt ) = $apputils->checkses($login_session);
622         return $evt if $evt;
623         $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
624         return $evt if $evt;
625
626         my $ses;
627         if($session) { $ses = $session; } 
628         else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
629
630         return $ses->request(
631                 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
632 }
633
634 =cut
635
636
637 sub find_local_hold {
638         my( $class, $session, $copy, $user ) = @_;
639         return $class->find_nearest_permitted_hold($session, $copy, $user);
640 }
641
642
643
644
645
646
647 sub fetch_open_hold_by_current_copy {
648         my $class = shift;
649         my $copyid = shift;
650         my $hold = $apputils->simplereq(
651                 'open-ils.cstore', 
652                 'open-ils.cstore.direct.action.hold_request.search.atomic',
653                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
654         return $hold->[0] if ref($hold);
655         return undef;
656 }
657
658 sub fetch_related_holds {
659         my $class = shift;
660         my $copyid = shift;
661         return $apputils->simplereq(
662                 'open-ils.cstore', 
663                 'open-ils.cstore.direct.action.hold_request.search.atomic',
664                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
665 }
666
667
668 __PACKAGE__->register_method (
669         method          => "hold_pull_list",
670         api_name                => "open-ils.circ.hold_pull_list.retrieve",
671         signature       => q/
672                 Returns a list of hold ID's that need to be "pulled"
673                 by a given location
674         /
675 );
676
677 sub hold_pull_list {
678         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
679         my( $reqr, $evt ) = $U->checkses($authtoken);
680         return $evt if $evt;
681
682         my $org = $reqr->ws_ou || $reqr->home_ou;
683         # the perm locaiton shouldn't really matter here since holds
684         # will exist all over and VIEW_HOLDS should be universal
685         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
686         return $evt if $evt;
687
688         return $U->storagereq(
689                 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
690                 $org, $limit, $offset ); 
691 }
692
693 __PACKAGE__->register_method (
694         method          => 'fetch_hold_notify',
695         api_name                => 'open-ils.circ.hold_notification.retrieve_by_hold',
696         signature       => q/ 
697                 Returns a list of hold notification objects based on hold id.
698                 @param authtoken The loggin session key
699                 @param holdid The id of the hold whose notifications we want to retrieve
700                 @return An array of hold notification objects, event on error.
701         /
702 );
703
704 sub fetch_hold_notify {
705         my( $self, $conn, $authtoken, $holdid ) = @_;
706         my( $requestor, $evt ) = $U->checkses($authtoken);
707         return $evt if $evt;
708         my ($hold, $patron);
709         ($hold, $evt) = $U->fetch_hold($holdid);
710         return $evt if $evt;
711         ($patron, $evt) = $U->fetch_user($hold->usr);
712         return $evt if $evt;
713
714         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
715         return $evt if $evt;
716
717         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
718         return $U->cstorereq(
719                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
720 }
721
722
723 __PACKAGE__->register_method (
724         method          => 'create_hold_notify',
725         api_name                => 'open-ils.circ.hold_notification.create',
726         signature       => q/
727                 Creates a new hold notification object
728                 @param authtoken The login session key
729                 @param notification The hold notification object to create
730                 @return ID of the new object on success, Event on error
731                 /
732 );
733 sub create_hold_notify {
734         my( $self, $conn, $authtoken, $notification ) = @_;
735         my( $requestor, $evt ) = $U->checkses($authtoken);
736         return $evt if $evt;
737         my ($hold, $patron);
738         ($hold, $evt) = $U->fetch_hold($notification->hold);
739         return $evt if $evt;
740         ($patron, $evt) = $U->fetch_user($hold->usr);
741         return $evt if $evt;
742
743         # XXX perm depth probably doesn't matter here -- should always be consortium level
744         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
745         return $evt if $evt;
746
747         # Set the proper notifier 
748         $notification->notify_staff($requestor->id);
749         my $id = $U->storagereq(
750                 'open-ils.storage.direct.action.hold_notification.create', $notification );
751         return $U->DB_UPDATE_FAILED($notification) unless $id;
752         $logger->info("User ".$requestor->id." successfully created new hold notification $id");
753         return $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 = $self->_reset_hold($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         if( $hold->capture_time and $hold->current_copy ) {
790
791                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
792                         or return $e->event;
793
794                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
795                         $logger->info("setting copy to status 'reshelving' on hold retarget");
796                         $copy->status(OILS_COPY_STATUS_RESHELVING);
797                         $copy->editor($e->requestor->id);
798                         $copy->edit_date('now');
799                         $e->update_asset_copy($copy) or return $e->event;
800
801                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
802                         $logger->warn("reseting hold that is in transit: ".$hold->id);
803                         # is this allowed?      
804                 }
805         }
806
807         $hold->clear_capture_time;
808         $hold->clear_current_copy;
809
810         $e->update_action_hold_request($hold) or return $e->event;
811
812         $e->commit;
813
814         $U->storagereq(
815                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
816
817         return undef;
818 }
819
820
821 __PACKAGE__->register_method(
822         method => 'fetch_open_title_holds',
823         api_name        => 'open-ils.circ.open_holds.retrieve',
824         signature       => q/
825                 Returns a list ids of un-fulfilled holds for a given title id
826                 @param authtoken The login session key
827                 @param id the id of the item whose holds we want to retrieve
828                 @param type The hold type - M, T, V, C
829         /
830 );
831
832 sub fetch_open_title_holds {
833         my( $self, $conn, $auth, $id, $type, $org ) = @_;
834         my $e = new_editor( authtoken => $auth );
835         return $e->event unless $e->checkauth;
836
837         $type ||= "T";
838         $org ||= $e->requestor->ws_ou;
839
840 #       return $e->search_action_hold_request(
841 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
842
843         # XXX make me return IDs in the future ^--
844         my $holds = $e->search_action_hold_request(
845                 { 
846                         target                          => $id, 
847                         cancel_time                     => undef, 
848                         hold_type                       => $type, 
849                         fulfillment_time        => undef 
850                 }
851         );
852
853         flesh_hold_transits($holds);
854         return $holds;
855 }
856
857
858 sub flesh_hold_transits {
859         my $holds = shift;
860         for my $hold ( @$holds ) {
861                 $hold->transit(
862                         $apputils->simplereq(
863                                 'open-ils.cstore',
864                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
865                                 { hold => $hold->id },
866                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
867                         )->[0]
868                 );
869         }
870 }
871
872
873
874
875 __PACKAGE__->register_method(
876         method => 'fetch_captured_holds',
877         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
878         signature       => q/
879                 Returns a list ids of un-fulfilled holds for a given title id
880                 @param authtoken The login session key
881                 @param org The org id of the location in question
882         /
883 );
884 sub fetch_captured_holds {
885         my( $self, $conn, $auth, $org ) = @_;
886
887         my $e = new_editor(authtoken => $auth);
888         return $e->event unless $e->checkauth;
889         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
890
891         $org ||= $e->requestor->ws_ou;
892
893         my $holds = $e->search_action_hold_request(
894                 { 
895                         capture_time            => { "!=" => undef },
896                         current_copy            => { "!=" => undef },
897                         fulfillment_time        => undef,
898                         pickup_lib                      => $org,
899                         cancel_time                     => undef,
900                 }
901         );
902
903         my @res;
904         for my $h (@$holds) {
905                 my $copy = $e->retrieve_asset_copy($h->current_copy)
906                         or return $e->event;
907                 push( @res, $h ) if 
908                         $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
909         }
910
911         flesh_hold_transits(\@res);
912         return \@res;
913 }
914
915
916
917
918
919 __PACKAGE__->register_method(
920         method  => "check_title_hold",
921         api_name        => "open-ils.circ.title_hold.is_possible",
922         notes           => q/
923                 Determines if a hold were to be placed by a given user,
924                 whether or not said hold would have any potential copies
925                 to fulfill it.
926                 @param authtoken The login session key
927                 @param params A hash of named params including:
928                         patronid  - the id of the hold recipient
929                         titleid (brn) - the id of the title to be held
930                         depth   - the hold range depth (defaults to 0)
931         /);
932
933 sub check_title_hold {
934         my( $self, $client, $authtoken, $params ) = @_;
935
936         my %params              = %$params;
937         my $titleid             = $params{titleid} ||"";
938         my $mrid                        = $params{mrid} ||"";
939         my $depth               = $params{depth} || 0;
940         my $pickup_lib  = $params{pickup_lib};
941         my $hold_type   = $params{hold_type} || 'T';
942
943         my $e = new_editor(authtoken=>$authtoken);
944         return $e->event unless $e->checkauth;
945         my $patron = $e->retrieve_actor_user($params{patronid})
946                 or return $e->event;
947
948         if( $e->requestor->id ne $patron->id ) {
949                 return $e->event unless 
950                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
951         }
952
953         return OpenILS::Event->new('PATRON_BARRED') 
954                 if $patron->barred and 
955                         ($patron->barred =~ /t/i or $patron->barred == 1);
956
957         my $rangelib    = $params{range_lib} || $patron->home_ou;
958
959         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
960                 or return $e->event;
961
962         if( $hold_type eq 'T' ) {
963                 return _check_title_hold_is_possible(
964                         $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
965         }
966
967         if( $hold_type eq 'M' ) {
968                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
969                 my @recs = map { $_->source } @$maps;
970                 for my $rec (@recs) {
971                         return 1 if (_check_title_hold_is_possible(
972                                 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
973                 }
974         }
975 }
976
977
978
979 sub _check_title_hold_is_possible {
980         my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
981
982         my $limit       = 10;
983         my $offset      = 0;
984         my $title;
985
986         $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
987
988         while( $title = $U->storagereq(
989                                 'open-ils.storage.biblio.record_entry.ranged_tree', 
990                                 $titleid, $rangelib, $depth, $limit, $offset ) ) {
991
992                 last unless 
993                         ref($title) and 
994                         ref($title->call_numbers) and 
995                         @{$title->call_numbers};
996
997                 for my $cn (@{$title->call_numbers}) {
998         
999                         $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1000         
1001                         for my $copy (@{$cn->copies}) {
1002         
1003                                 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1004         
1005                                 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1006                                         {       patron                          => $patron, 
1007                                                 requestor                       => $requestor, 
1008                                                 copy                                    => $copy,
1009                                                 title                                   => $title, 
1010                                                 title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1011                                                 pickup_lib                      => $pickup_lib,
1012                                                 request_lib                     => $request_lib 
1013                                         } 
1014                                 );
1015         
1016                                 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1017                         }
1018                 }
1019
1020                 $offset += $limit;
1021         }
1022         return 0;
1023 }
1024
1025
1026
1027 sub find_nearest_permitted_hold {
1028
1029         my $class       = shift;
1030         my $session = shift;
1031         my $copy                = shift;
1032         my $user                = shift;
1033         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1034
1035         # first see if this copy has already been selected to fulfill a hold
1036         my $hold  = $session->request(
1037                 "open-ils.storage.direct.action.hold_request.search_where",
1038                 { current_copy => $copy->id, cancel_time => undef, capture_time => undef } )->gather(1);
1039
1040         if( $hold ) {
1041                 $logger->info("hold found which can be fulfilled by copy ".$copy->id);
1042                 return $hold;
1043         }
1044
1045         # We know this hold is permitted, so just return it
1046         return $hold if $hold;
1047
1048         $logger->debug("searching for potential holds at org ". 
1049                 $user->ws_ou." and copy ".$copy->id);
1050
1051         my $holds = $session->request(
1052                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1053                 $user->ws_ou, $copy->id, 5 )->gather(1);
1054
1055         return (undef, $evt) unless @$holds;
1056
1057         # for each potential hold, we have to run the permit script
1058         # to make sure the hold is actually permitted.
1059
1060         for my $holdid (@$holds) {
1061                 next unless $holdid;
1062                 $logger->info("Checking if hold $holdid is permitted for user ".$user->id);
1063
1064                 my ($hold) = $U->fetch_hold($holdid);
1065                 next unless $hold;
1066                 my ($reqr) = $U->fetch_user($hold->requestor);
1067
1068                 return ($hold) if OpenILS::Utils::PermitHold::permit_copy_hold(
1069                         {
1070                                 patron_id                       => $hold->usr,
1071                                 requestor                       => $reqr->id,
1072                                 copy                                    => $copy,
1073                                 pickup_lib                      => $hold->pickup_lib,
1074                                 request_lib                     => $hold->request_lib 
1075                         } 
1076                 );
1077         }
1078
1079         return (undef, $evt);
1080 }
1081
1082
1083 1;