]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
updated opportunistic hold-capture logic to find the best hold and not necessarily...
[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 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->checkperm($rid, $porg, 'MR_HOLDS'); }
117
118                 if( $t eq OILS_HOLD_TYPE_TITLE ) 
119                         { $pevt = $e->event unless $e->checkperm($rid, $porg, 'TITLE_HOLDS');  }
120
121                 if( $t eq OILS_HOLD_TYPE_VOLUME ) 
122                         { $pevt = $e->event unless $e->checkperm($rid, $porg, 'VOLUME_HOLDS'); }
123
124                 if( $t eq OILS_HOLD_TYPE_COPY ) 
125                         { $pevt = $e->event unless $e->checkperm($rid, $porg, 'COPY_HOLDS'); }
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($recipient->home_ou) unless $hold->selection_ou;
144                 $hold = $e->create_action_hold_request($hold) or return $e->event;
145 #               push( @copyholds, $hold ) if $hold->hold_type eq OILS_HOLD_TYPE_COPY;
146         }
147
148         $e->commit;
149
150         $conn->respond_complete(1);
151
152         # Go ahead and target the copy-level holds
153         $U->storagereq(
154                 'open-ils.storage.action.hold_request.copy_targeter', 
155                 undef, $_->id ) for @holds;
156
157         return undef;
158 }
159
160 sub __create_hold {
161         my( $self, $client, $login_session, @holds) = @_;
162
163         if(!@holds){return 0;}
164         my( $user, $evt ) = $apputils->checkses($login_session);
165         return $evt if $evt;
166
167         my $holds;
168         if(ref($holds[0]) eq 'ARRAY') {
169                 $holds = $holds[0];
170         } else { $holds = [ @holds ]; }
171
172         $logger->debug("Iterating over holds requests...");
173
174         for my $hold (@$holds) {
175
176                 if(!$hold){next};
177                 my $type = $hold->hold_type;
178
179                 $logger->activity("User " . $user->id . 
180                         " creating new hold of type $type for user " . $hold->usr);
181
182                 my $recipient;
183                 if($user->id ne $hold->usr) {
184                         ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
185                         return $evt if $evt;
186
187                 } else {
188                         $recipient = $user;
189                 }
190
191
192                 my $perm = undef;
193
194                 # am I allowed to place holds for this user?
195                 if($hold->requestor ne $hold->usr) {
196                         $perm = _check_request_holds_perm($user->id, $user->home_ou);
197                         if($perm) { return $perm; }
198                 }
199
200                 # is this user allowed to have holds of this type?
201                 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
202                 if($perm) { 
203                         #if there is a requestor, see if the requestor has override privelages
204                         if($hold->requestor ne $hold->usr) {
205                                 $perm = _check_request_holds_override($user->id, $user->home_ou);
206                                 if($perm) {return $perm;}
207
208                         } else {
209                                 return $perm; 
210                         }
211                 }
212
213                 #enforce the fact that the login is the one requesting the hold
214                 $hold->requestor($user->id); 
215                 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
216
217                 my $resp = $apputils->simplereq(
218                         'open-ils.storage',
219                         'open-ils.storage.direct.action.hold_request.create', $hold );
220
221                 if(!$resp) { 
222                         return OpenSRF::EX::ERROR ("Error creating hold"); 
223                 }
224         }
225
226         return 1;
227 }
228
229 # makes sure that a user has permission to place the type of requested hold
230 # returns the Perm exception if not allowed, returns undef if all is well
231 sub _check_holds_perm {
232         my($type, $user_id, $org_id) = @_;
233
234         my $evt;
235         if($type eq "M") {
236                 if($evt = $apputils->check_perms(
237                         $user_id, $org_id, "MR_HOLDS")) {
238                         return $evt;
239                 } 
240
241         } elsif ($type eq "T") {
242                 if($evt = $apputils->check_perms(
243                         $user_id, $org_id, "TITLE_HOLDS")) {
244                         return $evt;
245                 }
246
247         } elsif($type eq "V") {
248                 if($evt = $apputils->check_perms(
249                         $user_id, $org_id, "VOLUME_HOLDS")) {
250                         return $evt;
251                 }
252
253         } elsif($type eq "C") {
254                 if($evt = $apputils->check_perms(
255                         $user_id, $org_id, "COPY_HOLDS")) {
256                         return $evt;
257                 }
258         }
259
260         return undef;
261 }
262
263 # tests if the given user is allowed to place holds on another's behalf
264 sub _check_request_holds_perm {
265         my $user_id = shift;
266         my $org_id = shift;
267         if(my $evt = $apputils->check_perms(
268                 $user_id, $org_id, "REQUEST_HOLDS")) {
269                 return $evt;
270         }
271 }
272
273 sub _check_request_holds_override {
274         my $user_id = shift;
275         my $org_id = shift;
276         if(my $evt = $apputils->check_perms(
277                 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
278                 return $evt;
279         }
280 }
281
282 __PACKAGE__->register_method(
283         method  => "retrieve_holds_by_id",
284         api_name        => "open-ils.circ.holds.retrieve_by_id",
285         notes           => <<NOTE);
286 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
287 different from the user, then the requestor must have VIEW_HOLD permissions.
288 NOTE
289
290
291 sub retrieve_holds_by_id {
292         my($self, $client, $auth, $hold_id) = @_;
293         my $e = new_editor(authtoken=>$auth);
294         $e->checkauth or return $e->event;
295         $e->allowed('VIEW_HOLD') or return $e->event;
296
297         my $holds = $e->search_action_hold_request(
298                 [
299                         { id =>  $hold_id , fulfillment_time => undef }, 
300                         { order_by => { ahr => "request_time" } }
301                 ]
302         );
303
304         flesh_hold_transits($holds);
305         flesh_hold_notices($holds, $e);
306         return $holds;
307 }
308
309
310 __PACKAGE__->register_method(
311         method  => "retrieve_holds",
312         api_name        => "open-ils.circ.holds.retrieve",
313         notes           => <<NOTE);
314 Retrieves all the holds, with hold transits attached, for the specified
315 user id.  The login session is the requestor and if the requestor is
316 different from the user, then the requestor must have VIEW_HOLD permissions.
317 NOTE
318
319 __PACKAGE__->register_method(
320         method  => "retrieve_holds",
321         api_name        => "open-ils.circ.holds.id_list.retrieve",
322         notes           => <<NOTE);
323 Retrieves all the hold ids for the specified
324 user id.  The login session is the requestor and if the requestor is
325 different from the user, then the requestor must have VIEW_HOLD permissions.
326 NOTE
327
328 sub retrieve_holds {
329         my($self, $client, $login_session, $user_id) = @_;
330
331         my( $user, $target, $evt ) = $apputils->checkses_requestor(
332                 $login_session, $user_id, 'VIEW_HOLD' );
333         return $evt if $evt;
334
335         my $holds = $apputils->simplereq(
336                 'open-ils.cstore',
337                 "open-ils.cstore.direct.action.hold_request.search.atomic",
338                 { 
339                         usr =>  $user_id , 
340                         fulfillment_time => undef,
341                         cancel_time => undef,
342                 }, 
343                 { order_by => { ahr => "request_time" } }
344         );
345         
346         if( ! $self->api_name =~ /id_list/ ) {
347                 for my $hold ( @$holds ) {
348                         $hold->transit(
349                                 $apputils->simplereq(
350                                         'open-ils.cstore',
351                                         "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
352                                         { hold => $hold->id },
353                                         { order_by => { ahtc => 'id desc' }, limit => 1 }
354                                 )->[0]
355                         );
356                 }
357         }
358
359         if( $self->api_name =~ /id_list/ ) {
360                 return [ map { $_->id } @$holds ];
361         } else {
362                 return $holds;
363         }
364 }
365
366 __PACKAGE__->register_method(
367         method  => "retrieve_holds_by_pickup_lib",
368         api_name        => "open-ils.circ.holds.retrieve_by_pickup_lib",
369         notes           => <<NOTE);
370 Retrieves all the holds, with hold transits attached, for the specified
371 pickup_ou id. 
372 NOTE
373
374 __PACKAGE__->register_method(
375         method  => "retrieve_holds_by_pickup_lib",
376         api_name        => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
377         notes           => <<NOTE);
378 Retrieves all the hold ids for the specified
379 pickup_ou id. 
380 NOTE
381
382 sub retrieve_holds_by_pickup_lib {
383         my($self, $client, $login_session, $ou_id) = @_;
384
385         #FIXME -- put an appropriate permission check here
386         #my( $user, $target, $evt ) = $apputils->checkses_requestor(
387         #       $login_session, $user_id, 'VIEW_HOLD' );
388         #return $evt if $evt;
389
390         my $holds = $apputils->simplereq(
391                 'open-ils.cstore',
392                 "open-ils.cstore.direct.action.hold_request.search.atomic",
393                 { 
394                         pickup_lib =>  $ou_id , 
395                         fulfillment_time => undef,
396                         cancel_time => undef
397                 }, 
398                 { order_by => { ahr => "request_time" } });
399
400
401         if( ! $self->api_name =~ /id_list/ ) {
402                 flesh_hold_transits($holds);
403         }
404
405         if( $self->api_name =~ /id_list/ ) {
406                 return [ map { $_->id } @$holds ];
407         } else {
408                 return $holds;
409         }
410 }
411
412 __PACKAGE__->register_method(
413         method  => "cancel_hold",
414         api_name        => "open-ils.circ.hold.cancel",
415         notes           => <<"  NOTE");
416         Cancels the specified hold.  The login session
417         is the requestor and if the requestor is different from the usr field
418         on the hold, the requestor must have CANCEL_HOLDS permissions.
419         the hold may be either the hold object or the hold id
420         NOTE
421
422 sub cancel_hold {
423         my($self, $client, $auth, $holdid) = @_;
424
425         my $e = new_editor(authtoken=>$auth, xact=>1);
426         return $e->event unless $e->checkauth;
427
428         my $hold = $e->retrieve_action_hold_request($holdid)
429                 or return $e->event;
430
431         if( $e->requestor->id ne $hold->usr ) {
432                 return $e->event unless $e->allowed('CANCEL_HOLDS');
433         }
434
435         return 1 if $hold->cancel_time;
436
437         # If the hold is captured, reset the copy status
438         if( $hold->capture_time and $hold->current_copy ) {
439
440                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
441                         or return $e->event;
442
443                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
444                         $logger->info("setting copy to status 'reshelving' on hold cancel");
445                         $copy->status(OILS_COPY_STATUS_RESHELVING);
446                         $copy->editor($e->requestor->id);
447                         $copy->edit_date('now');
448                         $e->update_asset_copy($copy) or return $e->event;
449
450                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
451
452                         my $hid = $hold->id;
453                         $logger->warn("! canceling hold [$hid] that is in transit");
454                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
455
456                         if( $transid ) {
457                                 my $trans = $e->retrieve_action_transit_copy($transid);
458                                 # Leave the transit alive, but  set the copy status to 
459                                 # reshelving so it will be properly reshelved when it gets back home
460                                 if( $trans ) {
461                                         $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
462                                         $e->update_action_transit_copy($trans) or return $e->die_event;
463                                 }
464                                 
465                         }
466                 }
467         }
468
469         $hold->cancel_time('now');
470         $e->update_action_hold_request($hold)
471                 or return $e->event;
472
473         $self->delete_hold_copy_maps($e, $hold->id);
474
475         $e->commit;
476         return 1;
477 }
478
479 sub delete_hold_copy_maps {
480         my $class = shift;
481         my $editor = shift;
482         my $holdid = shift;
483
484         my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
485         for(@$maps) {
486                 $editor->delete_action_hold_copy_map($_) 
487                         or return $editor->event;
488         }
489         return undef;
490 }
491
492
493 __PACKAGE__->register_method(
494         method  => "update_hold",
495         api_name        => "open-ils.circ.hold.update",
496         notes           => <<"  NOTE");
497         Updates the specified hold.  The login session
498         is the requestor and if the requestor is different from the usr field
499         on the hold, the requestor must have UPDATE_HOLDS permissions.
500         NOTE
501
502 sub update_hold {
503         my($self, $client, $login_session, $hold) = @_;
504
505         my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
506                 $login_session, $hold->usr, 'UPDATE_HOLD' );
507         return $evt if $evt;
508
509         $logger->activity('User ' . $requestor->id . 
510                 ' updating hold ' . $hold->id . ' for user ' . $target->id );
511
512         return $U->storagereq(
513                 "open-ils.storage.direct.action.hold_request.update", $hold );
514 }
515
516
517 __PACKAGE__->register_method(
518         method  => "retrieve_hold_status",
519         api_name        => "open-ils.circ.hold.status.retrieve",
520         notes           => <<"  NOTE");
521         Calculates the current status of the hold.
522         the requestor must have VIEW_HOLD permissions if the hold is for a user
523         other than the requestor.
524         Returns -1  on error (for now)
525         Returns 1 for 'waiting for copy to become available'
526         Returns 2 for 'waiting for copy capture'
527         Returns 3 for 'in transit'
528         Returns 4 for 'arrived'
529         NOTE
530
531 sub retrieve_hold_status {
532         my($self, $client, $auth, $hold_id) = @_;
533
534         my $e = new_editor(authtoken => $auth);
535         return $e->event unless $e->checkauth;
536         my $hold = $e->retrieve_action_hold_request($hold_id)
537                 or return $e->event;
538
539         if( $e->requestor->id != $hold->usr ) {
540                 return $e->event unless $e->allowed('VIEW_HOLD');
541         }
542
543         return _hold_status($e, $hold);
544
545 }
546
547 sub _hold_status {
548         my($e, $hold) = @_;
549         return 1 unless $hold->current_copy;
550         return 2 unless $hold->capture_time;
551
552         my $copy = $hold->current_copy;
553         unless( ref $copy ) {
554                 $copy = $e->retrieve_asset_copy($hold->current_copy)
555                         or return $e->event;
556         }
557
558         return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
559         return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
560
561         return -1;
562 }
563
564
565 #sub find_local_hold {
566 #       my( $class, $session, $copy, $user ) = @_;
567 #       return $class->find_nearest_permitted_hold($session, $copy, $user);
568 #}
569
570
571 sub fetch_open_hold_by_current_copy {
572         my $class = shift;
573         my $copyid = shift;
574         my $hold = $apputils->simplereq(
575                 'open-ils.cstore', 
576                 'open-ils.cstore.direct.action.hold_request.search.atomic',
577                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
578         return $hold->[0] if ref($hold);
579         return undef;
580 }
581
582 sub fetch_related_holds {
583         my $class = shift;
584         my $copyid = shift;
585         return $apputils->simplereq(
586                 'open-ils.cstore', 
587                 'open-ils.cstore.direct.action.hold_request.search.atomic',
588                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
589 }
590
591
592 __PACKAGE__->register_method (
593         method          => "hold_pull_list",
594         api_name                => "open-ils.circ.hold_pull_list.retrieve",
595         signature       => q/
596                 Returns a list of holds that need to be "pulled"
597                 by a given location
598         /
599 );
600
601 __PACKAGE__->register_method (
602         method          => "hold_pull_list",
603         api_name                => "open-ils.circ.hold_pull_list.id_list.retrieve",
604         signature       => q/
605                 Returns a list of hold ID's that need to be "pulled"
606                 by a given location
607         /
608 );
609
610
611 sub hold_pull_list {
612         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
613         my( $reqr, $evt ) = $U->checkses($authtoken);
614         return $evt if $evt;
615
616         my $org = $reqr->ws_ou || $reqr->home_ou;
617         # the perm locaiton shouldn't really matter here since holds
618         # will exist all over and VIEW_HOLDS should be universal
619         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
620         return $evt if $evt;
621
622         if( $self->api_name =~ /id_list/ ) {
623                 return $U->storagereq(
624                         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
625                         $org, $limit, $offset ); 
626         } else {
627                 return $U->storagereq(
628                         'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
629                         $org, $limit, $offset ); 
630         }
631 }
632
633 __PACKAGE__->register_method (
634         method          => 'fetch_hold_notify',
635         api_name                => 'open-ils.circ.hold_notification.retrieve_by_hold',
636         signature       => q/ 
637                 Returns a list of hold notification objects based on hold id.
638                 @param authtoken The loggin session key
639                 @param holdid The id of the hold whose notifications we want to retrieve
640                 @return An array of hold notification objects, event on error.
641         /
642 );
643
644 sub fetch_hold_notify {
645         my( $self, $conn, $authtoken, $holdid ) = @_;
646         my( $requestor, $evt ) = $U->checkses($authtoken);
647         return $evt if $evt;
648         my ($hold, $patron);
649         ($hold, $evt) = $U->fetch_hold($holdid);
650         return $evt if $evt;
651         ($patron, $evt) = $U->fetch_user($hold->usr);
652         return $evt if $evt;
653
654         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
655         return $evt if $evt;
656
657         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
658         return $U->cstorereq(
659                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
660 }
661
662
663 __PACKAGE__->register_method (
664         method          => 'create_hold_notify',
665         api_name                => 'open-ils.circ.hold_notification.create',
666         signature       => q/
667                 Creates a new hold notification object
668                 @param authtoken The login session key
669                 @param notification The hold notification object to create
670                 @return ID of the new object on success, Event on error
671                 /
672 );
673 sub create_hold_notify {
674         my( $self, $conn, $authtoken, $notification ) = @_;
675         my( $requestor, $evt ) = $U->checkses($authtoken);
676         return $evt if $evt;
677         my ($hold, $patron);
678         ($hold, $evt) = $U->fetch_hold($notification->hold);
679         return $evt if $evt;
680         ($patron, $evt) = $U->fetch_user($hold->usr);
681         return $evt if $evt;
682
683         # XXX perm depth probably doesn't matter here -- should always be consortium level
684         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
685         return $evt if $evt;
686
687         # Set the proper notifier 
688         $notification->notify_staff($requestor->id);
689         my $id = $U->storagereq(
690                 'open-ils.storage.direct.action.hold_notification.create', $notification );
691         return $U->DB_UPDATE_FAILED($notification) unless $id;
692         $logger->info("User ".$requestor->id." successfully created new hold notification $id");
693         return $id;
694 }
695
696
697 __PACKAGE__->register_method(
698         method  => 'reset_hold',
699         api_name        => 'open-ils.circ.hold.reset',
700         signature       => q/
701                 Un-captures and un-targets a hold, essentially returning
702                 it to the state it was in directly after it was placed,
703                 then attempts to re-target the hold
704                 @param authtoken The login session key
705                 @param holdid The id of the hold
706         /
707 );
708
709
710 sub reset_hold {
711         my( $self, $conn, $auth, $holdid ) = @_;
712         my $reqr;
713         my ($hold, $evt) = $U->fetch_hold($holdid);
714         return $evt if $evt;
715         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
716         return $evt if $evt;
717         $evt = $self->_reset_hold($reqr, $hold);
718         return $evt if $evt;
719         return 1;
720 }
721
722 sub _reset_hold {
723         my ($self, $reqr, $hold) = @_;
724
725         my $e = new_editor(xact =>1, requestor => $reqr);
726
727         $logger->info("reseting hold ".$hold->id);
728
729         my $hid = $hold->id;
730
731         if( $hold->capture_time and $hold->current_copy ) {
732
733                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
734                         or return $e->event;
735
736                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
737                         $logger->info("setting copy to status 'reshelving' on hold retarget");
738                         $copy->status(OILS_COPY_STATUS_RESHELVING);
739                         $copy->editor($e->requestor->id);
740                         $copy->edit_date('now');
741                         $e->update_asset_copy($copy) or return $e->event;
742
743                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
744
745                         # We don't want the copy to remain "in transit"
746                         $copy->status(OILS_COPY_STATUS_RESHELVING);
747                         $logger->warn("! reseting hold [$hid] that is in transit");
748                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
749
750                         if( $transid ) {
751                                 my $trans = $e->retrieve_action_transit_copy($transid);
752                                 if( $trans ) {
753                                         $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
754                                         my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
755                                         $logger->info("Transit abort completed with result $evt");
756                                         return $evt unless "$evt" eq 1;
757                                 }
758                         }
759                 }
760         }
761
762         $hold->clear_capture_time;
763         $hold->clear_current_copy;
764
765         $e->update_action_hold_request($hold) or return $e->event;
766         $e->commit;
767
768         $U->storagereq(
769                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
770
771         return undef;
772 }
773
774
775 __PACKAGE__->register_method(
776         method => 'fetch_open_title_holds',
777         api_name        => 'open-ils.circ.open_holds.retrieve',
778         signature       => q/
779                 Returns a list ids of un-fulfilled holds for a given title id
780                 @param authtoken The login session key
781                 @param id the id of the item whose holds we want to retrieve
782                 @param type The hold type - M, T, V, C
783         /
784 );
785
786 sub fetch_open_title_holds {
787         my( $self, $conn, $auth, $id, $type, $org ) = @_;
788         my $e = new_editor( authtoken => $auth );
789         return $e->event unless $e->checkauth;
790
791         $type ||= "T";
792         $org ||= $e->requestor->ws_ou;
793
794 #       return $e->search_action_hold_request(
795 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
796
797         # XXX make me return IDs in the future ^--
798         my $holds = $e->search_action_hold_request(
799                 { 
800                         target                          => $id, 
801                         cancel_time                     => undef, 
802                         hold_type                       => $type, 
803                         fulfillment_time        => undef 
804                 }
805         );
806
807         flesh_hold_transits($holds);
808         return $holds;
809 }
810
811
812 sub flesh_hold_transits {
813         my $holds = shift;
814         for my $hold ( @$holds ) {
815                 $hold->transit(
816                         $apputils->simplereq(
817                                 'open-ils.cstore',
818                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
819                                 { hold => $hold->id },
820                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
821                         )->[0]
822                 );
823         }
824 }
825
826 sub flesh_hold_notices {
827         my( $holds, $e ) = @_;
828         $e ||= new_editor();
829
830         for my $hold (@$holds) {
831                 my $notices = $e->search_action_hold_notification(
832                         [
833                                 { hold => $hold->id },
834                                 { order_by => { anh => { 'notify_time desc' } } },
835                         ],
836                         {idlist=>1}
837                 );
838
839                 $hold->notify_count(scalar(@$notices));
840                 if( @$notices ) {
841                         my $n = $e->retrieve_action_hold_notification($$notices[0])
842                                 or return $e->event;
843                         $hold->notify_time($n->notify_time);
844                 }
845         }
846 }
847
848
849
850
851 __PACKAGE__->register_method(
852         method => 'fetch_captured_holds',
853         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
854         signature       => q/
855                 Returns a list of un-fulfilled holds for a given title id
856                 @param authtoken The login session key
857                 @param org The org id of the location in question
858         /
859 );
860
861 __PACKAGE__->register_method(
862         method => 'fetch_captured_holds',
863         api_name        => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
864         signature       => q/
865                 Returns a list ids of un-fulfilled holds for a given title id
866                 @param authtoken The login session key
867                 @param org The org id of the location in question
868         /
869 );
870
871 sub fetch_captured_holds {
872         my( $self, $conn, $auth, $org ) = @_;
873
874         my $e = new_editor(authtoken => $auth);
875         return $e->event unless $e->checkauth;
876         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
877
878         $org ||= $e->requestor->ws_ou;
879
880         my $holds = $e->search_action_hold_request(
881                 { 
882                         capture_time            => { "!=" => undef },
883                         current_copy            => { "!=" => undef },
884                         fulfillment_time        => undef,
885                         pickup_lib                      => $org,
886                         cancel_time                     => undef,
887                 }
888         );
889
890         my @res;
891         for my $h (@$holds) {
892                 my $copy = $e->retrieve_asset_copy($h->current_copy)
893                         or return $e->event;
894                 push( @res, $h ) if 
895                         $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
896         }
897
898         if( ! $self->api_name =~ /id_list/ ) {
899                 flesh_hold_transits(\@res);
900                 flesh_hold_notices(\@res, $e);
901         }
902
903         if( $self->api_name =~ /id_list/ ) {
904                 return [ map { $_->id } @res ];
905         } else {
906                 return \@res;
907         }
908 }
909
910
911 __PACKAGE__->register_method(
912         method  => "check_title_hold",
913         api_name        => "open-ils.circ.title_hold.is_possible",
914         notes           => q/
915                 Determines if a hold were to be placed by a given user,
916                 whether or not said hold would have any potential copies
917                 to fulfill it.
918                 @param authtoken The login session key
919                 @param params A hash of named params including:
920                         patronid  - the id of the hold recipient
921                         titleid (brn) - the id of the title to be held
922                         depth   - the hold range depth (defaults to 0)
923         /);
924
925 sub check_title_hold {
926         my( $self, $client, $authtoken, $params ) = @_;
927
928         my %params              = %$params;
929         my $titleid             = $params{titleid} ||"";
930         my $volid               = $params{volume_id};
931         my $copyid              = $params{copy_id};
932         my $mrid                        = $params{mrid} ||"";
933         my $depth               = $params{depth} || 0;
934         my $pickup_lib  = $params{pickup_lib};
935         my $hold_type   = $params{hold_type} || 'T';
936
937         my $e = new_editor(authtoken=>$authtoken);
938         return $e->event unless $e->checkauth;
939         my $patron = $e->retrieve_actor_user($params{patronid})
940                 or return $e->event;
941
942         if( $e->requestor->id ne $patron->id ) {
943                 return $e->event unless 
944                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
945         }
946
947         return OpenILS::Event->new('PATRON_BARRED') 
948                 if $patron->barred and 
949                         ($patron->barred =~ /t/i or $patron->barred == 1);
950
951         my $rangelib    = $params{range_lib} || $patron->home_ou;
952
953         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
954                 or return $e->event;
955
956         $logger->info("checking hold possibility with type $hold_type");
957
958         my $copy;
959         my $volume;
960         my $title;
961
962         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
963
964                 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
965                 $volume = $e->retrieve_asset_call_number($copy->call_number)
966                         or return $e->event;
967                 $title = $e->retrieve_biblio_record_entry($volume->record)
968                         or return $e->event;
969                 return verify_copy_for_hold( 
970                         $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
971
972         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
973
974                 $volume = $e->retrieve_asset_call_number($volid)
975                         or return $e->event;
976                 $title = $e->retrieve_biblio_record_entry($volume->record)
977                         or return $e->event;
978
979                 return _check_volume_hold_is_possible(
980                         $volume, $title, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
981
982         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
983
984                 return _check_title_hold_is_possible(
985                         $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
986
987         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
988
989                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
990                 my @recs = map { $_->source } @$maps;
991                 for my $rec (@recs) {
992                         return 1 if (_check_title_hold_is_possible(
993                                 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
994                 }
995                 return 0;       
996         }
997 }
998
999
1000
1001 sub _check_title_hold_is_possible {
1002         my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1003
1004         my $limit       = 10;
1005         my $offset      = 0;
1006         my $title;
1007
1008         $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1009
1010         while( $title = $U->storagereq(
1011                                 'open-ils.storage.biblio.record_entry.ranged_tree', 
1012                                 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1013
1014                 last unless 
1015                         ref($title) and 
1016                         ref($title->call_numbers) and 
1017                         @{$title->call_numbers};
1018
1019                 for my $cn (@{$title->call_numbers}) {
1020         
1021                         $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1022         
1023                         for my $copy (@{$cn->copies}) {
1024                                 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1025                                 return 1 if verify_copy_for_hold( 
1026                                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1027                                 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1028                         }
1029                 }
1030
1031                 $offset += $limit;
1032         }
1033         return 0;
1034 }
1035
1036 sub _check_volume_hold_is_possible {
1037         my( $vol, $title, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1038         my $copies = new_editor->search_asset_copy({call_number => $vol->id});
1039         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1040         for my $copy ( @$copies ) {
1041                 return 1 if verify_copy_for_hold( 
1042                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1043         }
1044         return 0;
1045 }
1046
1047
1048
1049 sub verify_copy_for_hold {
1050         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1051         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1052         return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1053                 {       patron                          => $patron, 
1054                         requestor                       => $requestor, 
1055                         copy                                    => $copy,
1056                         title                                   => $title, 
1057                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1058                         pickup_lib                      => $pickup_lib,
1059                         request_lib                     => $request_lib 
1060                 } 
1061         );
1062         return 0;
1063 }
1064
1065
1066
1067 sub find_nearest_permitted_hold {
1068
1069         my $class       = shift;
1070         my $editor      = shift; # CStoreEditor object
1071         my $copy                = shift; # copy to target
1072         my $user                = shift; # hold recipient
1073         my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1074         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1075
1076         my $bc = $copy->barcode;
1077
1078         # find any existing holds that already target this copy
1079         my $old_holds = $editor->search_action_hold_request(
1080                 {       current_copy => $copy->id, 
1081                         cancel_time => undef, 
1082                         capture_time => undef 
1083                 } 
1084         );
1085
1086         $logger->info("circulator: searching for best hold at org ".$user->ws_ou." and copy $bc");
1087
1088         # search for what should be the best holds for this copy to fulfill
1089         my $best_holds = $U->storagereq(
1090                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1091                 $user->ws_ou, $copy->id, 10 );
1092
1093         unless(@$best_holds) {
1094
1095                 if( my $hold = $$old_holds[0] ) {
1096                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1097                         return ($hold);
1098                 }
1099
1100                 $logger->info("circulator: no suitable holds found for copy $bc");
1101                 return (undef, $evt);
1102         }
1103
1104
1105         my $best_hold;
1106
1107         # for each potential hold, we have to run the permit script
1108         # to make sure the hold is actually permitted.
1109         for my $holdid (@$best_holds) {
1110                 next unless $holdid;
1111                 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1112
1113                 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1114                 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1115                 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1116
1117                 # see if this hold is permitted
1118                 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1119                         {       patron_id                       => $hold->usr,
1120                                 requestor                       => $reqr->id,
1121                                 copy                                    => $copy,
1122                                 pickup_lib                      => $hold->pickup_lib,
1123                                 request_lib                     => $rlib,
1124                         } 
1125                 );
1126
1127                 if( $permitted ) {
1128                         $best_hold = $hold;
1129                         last;
1130                 }
1131         }
1132
1133
1134         unless( $best_hold ) { # no "good" permitted holds were found
1135                 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1136                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1137                         return ($hold);
1138                 }
1139
1140                 # we got nuthin
1141                 $logger->info("circulator: no suitable holds found for copy $bc");
1142                 return (undef, $evt);
1143         }
1144
1145         $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1146
1147         # indicate a permitted hold was found
1148         return $best_hold if $check_only;
1149
1150         # we've found a permitted hold.  we need to "grab" the copy 
1151         # to prevent re-targeted holds (next part) from re-grabbing the copy
1152         $best_hold->current_copy($copy->id);
1153         $editor->update_action_hold_request($best_hold) 
1154                 or return (undef, $editor->event);
1155
1156
1157         # re-target any other holds that already target this copy
1158         for my $old_hold (@$old_holds) {
1159                 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1160                 $logger->info("circulator: re-targeting hold ".$old_hold->id.
1161                         " after a better hold [".$best_hold->id."] was found");
1162                 $U->storagereq( 
1163                         'open-ils.storage.action.hold_request.copy_targeter', undef, $old_hold->id );
1164         }
1165
1166         return ($best_hold);
1167 }
1168
1169
1170
1171
1172
1173
1174 __PACKAGE__->register_method(
1175         method => 'all_rec_holds',
1176         api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1177 );
1178
1179 sub all_rec_holds {
1180         my( $self, $conn, $auth, $title_id, $args ) = @_;
1181
1182         my $e = new_editor(authtoken=>$auth);
1183         $e->checkauth or return $e->event;
1184         $e->allowed('VIEW_HOLD') or return $e->event;
1185
1186         $args ||= { fulfillment_time => undef };
1187         $args->{cancel_time} = undef;
1188
1189         my $resp = { volume_holds => [], copy_holds => [] };
1190
1191         $resp->{title_holds} = $e->search_action_hold_request(
1192                 { 
1193                         hold_type => OILS_HOLD_TYPE_TITLE, 
1194                         target => $title_id, 
1195                         %$args 
1196                 }, {idlist=>1} );
1197
1198         my $vols = $e->search_asset_call_number(
1199                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1200
1201         return $resp unless @$vols;
1202
1203         $resp->{volume_holds} = $e->search_action_hold_request(
1204                 { 
1205                         hold_type => OILS_HOLD_TYPE_VOLUME, 
1206                         target => $vols,
1207                         %$args }, 
1208                 {idlist=>1} );
1209
1210         my $copies = $e->search_asset_copy(
1211                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1212
1213         return $resp unless @$copies;
1214
1215         $resp->{copy_holds} = $e->search_action_hold_request(
1216                 { 
1217                         hold_type => OILS_HOLD_TYPE_COPY,
1218                         target => $copies,
1219                         %$args }, 
1220                 {idlist=>1} );
1221
1222         return $resp;
1223 }
1224
1225
1226
1227
1228
1229 __PACKAGE__->register_method(
1230         method => 'uber_hold',
1231         api_name => 'open-ils.circ.hold.details.retrieve'
1232 );
1233
1234 sub uber_hold {
1235         my($self, $client, $auth, $hold_id) = @_;
1236         my $e = new_editor(authtoken=>$auth);
1237         $e->checkauth or return $e->event;
1238         $e->allowed('VIEW_HOLD') or return $e->event;
1239
1240         my $resp = {};
1241
1242         my $hold = $e->retrieve_action_hold_request(
1243                 [
1244                         $hold_id,
1245                         {
1246                                 flesh => 1,
1247                                 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1248                         }
1249                 ]
1250         ) or return $e->event;
1251
1252         my $user = $hold->usr;
1253         $hold->usr($user->id);
1254
1255         my $card = $e->retrieve_actor_card($user->card)
1256                 or return $e->event;
1257
1258         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1259
1260         flesh_hold_notices([$hold], $e);
1261         flesh_hold_transits([$hold]);
1262
1263         return {
1264                 hold            => $hold,
1265                 copy            => $copy,
1266                 volume  => $volume,
1267                 mvr             => $mvr,
1268                 status  => _hold_status($e, $hold),
1269                 patron_first => $user->first_given_name,
1270                 patron_last  => $user->family_name,
1271                 patron_barcode => $card->barcode,
1272         };
1273 }
1274
1275
1276
1277 # -----------------------------------------------------
1278 # Returns the MVR object that represents what the
1279 # hold is all about
1280 # -----------------------------------------------------
1281 sub find_hold_mvr {
1282         my( $e, $hold ) = @_;
1283
1284         my $tid;
1285         my $copy;
1286         my $volume;
1287
1288         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1289                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1290                         or return $e->event;
1291                 $tid = $mr->master_record;
1292
1293         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1294                 $tid = $hold->target;
1295
1296         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1297                 $volume = $e->retrieve_asset_call_number($hold->target)
1298                         or return $e->event;
1299                 $tid = $volume->record;
1300
1301         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1302                 $copy = $e->retrieve_asset_copy($hold->target)
1303                         or return $e->event;
1304                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1305                         or return $e->event;
1306                 $tid = $volume->record;
1307         }
1308
1309         if(!$copy and ref $hold->current_copy ) {
1310                 $copy = $hold->current_copy;
1311                 $hold->current_copy($copy->id);
1312         }
1313
1314         if(!$volume and $copy) {
1315                 $volume = $e->retrieve_asset_call_number($copy->call_number);
1316         }
1317
1318         my $title = $e->retrieve_biblio_record_entry($tid);
1319         return ( $U->record_to_mvr($title), $volume, $copy );
1320 }
1321
1322
1323
1324
1325 1;