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