Bug 26633: Add REST API for managing transfer limits
[koha.git] / Koha / REST / V1 / Holds.pm
1 package Koha::REST::V1::Holds;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # 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 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18 use Modern::Perl;
19
20 use Mojo::Base 'Mojolicious::Controller';
21
22 use Mojo::JSON qw(decode_json);
23
24 use C4::Biblio;
25 use C4::Reserves;
26
27 use Koha::Items;
28 use Koha::Patrons;
29 use Koha::Holds;
30 use Koha::DateUtils;
31
32 use List::MoreUtils qw(any);
33 use Try::Tiny;
34
35 =head1 API
36
37 =head2 Methods
38
39 =head3 list
40
41 Method that handles listing Koha::Hold objects
42
43 =cut
44
45 sub list {
46     my $c = shift->openapi->valid_input or return;
47
48     return try {
49         my $holds_set = Koha::Holds->new;
50         my $holds     = $c->objects->search( $holds_set );
51         return $c->render( status => 200, openapi => $holds );
52     }
53     catch {
54         $c->unhandled_exception($_);
55     };
56 }
57
58 =head3 add
59
60 Method that handles adding a new Koha::Hold object
61
62 =cut
63
64 sub add {
65     my $c = shift->openapi->valid_input or return;
66
67     return try {
68         my $body = $c->validation->param('body');
69
70         my $biblio;
71         my $item;
72
73         my $biblio_id         = $body->{biblio_id};
74         my $pickup_library_id = $body->{pickup_library_id};
75         my $item_id           = $body->{item_id};
76         my $patron_id         = $body->{patron_id};
77         my $item_type         = $body->{item_type};
78         my $expiration_date   = $body->{expiration_date};
79         my $notes             = $body->{notes};
80         my $hold_date         = $body->{hold_date};
81         my $non_priority      = $body->{non_priority};
82
83         if(!C4::Context->preference( 'AllowHoldDateInFuture' ) && $hold_date) {
84             return $c->render(
85                 status  => 400,
86                 openapi => { error => "Hold date in future not allowed" }
87             );
88         }
89
90         if ( $item_id and $biblio_id ) {
91
92             # check they are consistent
93             unless ( Koha::Items->search( { itemnumber => $item_id, biblionumber => $biblio_id } )
94                 ->count > 0 )
95             {
96                 return $c->render(
97                     status  => 400,
98                     openapi => { error => "Item $item_id doesn't belong to biblio $biblio_id" }
99                 );
100             }
101             else {
102                 $biblio = Koha::Biblios->find($biblio_id);
103             }
104         }
105         elsif ($item_id) {
106             $item = Koha::Items->find($item_id);
107
108             unless ($item) {
109                 return $c->render(
110                     status  => 404,
111                     openapi => { error => "item_id not found." }
112                 );
113             }
114             else {
115                 $biblio = $item->biblio;
116             }
117         }
118         elsif ($biblio_id) {
119             $biblio = Koha::Biblios->find($biblio_id);
120         }
121         else {
122             return $c->render(
123                 status  => 400,
124                 openapi => { error => "At least one of biblio_id, item_id should be given" }
125             );
126         }
127
128         unless ($biblio) {
129             return $c->render(
130                 status  => 400,
131                 openapi => "Biblio not found."
132             );
133         }
134
135         my $patron = Koha::Patrons->find( $patron_id );
136         unless ($patron) {
137             return $c->render(
138                 status  => 400,
139                 openapi => { error => 'patron_id not found' }
140             );
141         }
142
143         # Validate pickup location
144         my $valid_pickup_location;
145         if ($item) {    # item-level hold
146             $valid_pickup_location =
147               any { $_->branchcode eq $pickup_library_id }
148             $item->pickup_locations(
149                 { patron => $patron } );
150         }
151         else {
152             $valid_pickup_location =
153               any { $_->branchcode eq $pickup_library_id }
154             $biblio->pickup_locations(
155                 { patron => $patron } );
156         }
157
158         return $c->render(
159             status  => 400,
160             openapi => {
161                 error => 'The supplied pickup location is not valid'
162             }
163         ) unless $valid_pickup_location;
164
165         my $can_place_hold
166             = $item_id
167             ? C4::Reserves::CanItemBeReserved( $patron_id, $item_id )
168             : C4::Reserves::CanBookBeReserved( $patron_id, $biblio_id );
169
170         if ( $patron->holds->count + 1 > C4::Context->preference('maxreserves') ) {
171             $can_place_hold->{status} = 'tooManyReserves';
172         }
173
174         my $override_header = $c->req->headers->header('x-koha-override');
175         $override_header = decode_json($override_header)
176           if $override_header;
177
178         my $can_override = $override_header->{AllowHoldPolicyOverride}
179           and C4::Context->preference('AllowHoldPolicyOverride');
180
181         unless ($can_override || $can_place_hold->{status} eq 'OK' ) {
182             return $c->render(
183                 status => 403,
184                 openapi =>
185                     { error => "Hold cannot be placed. Reason: " . $can_place_hold->{status} }
186             );
187         }
188
189         my $priority = C4::Reserves::CalculatePriority($biblio_id);
190
191         # AddReserve expects date to be in syspref format
192         if ($expiration_date) {
193             $expiration_date = output_pref( dt_from_string( $expiration_date, 'rfc3339' ) );
194         }
195
196         my $hold_id = C4::Reserves::AddReserve(
197             {
198                 branchcode       => $pickup_library_id,
199                 borrowernumber   => $patron_id,
200                 biblionumber     => $biblio_id,
201                 priority         => $priority,
202                 reservation_date => $hold_date,
203                 expiration_date  => $expiration_date,
204                 notes            => $notes,
205                 title            => $biblio->title,
206                 itemnumber       => $item_id,
207                 found            => undef,                # TODO: Why not?
208                 itemtype         => $item_type,
209                 non_priority     => $non_priority,
210             }
211         );
212
213         unless ($hold_id) {
214             return $c->render(
215                 status  => 500,
216                 openapi => 'Error placing the hold. See Koha logs for details.'
217             );
218         }
219
220         my $hold = Koha::Holds->find($hold_id);
221
222         return $c->render(
223             status  => 201,
224             openapi => $hold->to_api
225         );
226     }
227     catch {
228         if ( blessed $_ and $_->isa('Koha::Exceptions') ) {
229             if ( $_->isa('Koha::Exceptions::Object::FKConstraint') ) {
230                 my $broken_fk = $_->broken_fk;
231
232                 if ( grep { $_ eq $broken_fk } keys %{Koha::Holds->new->to_api_mapping} ) {
233                     $c->render(
234                         status  => 404,
235                         openapi => Koha::Holds->new->to_api_mapping->{$broken_fk} . ' not found.'
236                     );
237                 }
238             }
239         }
240
241         $c->unhandled_exception($_);
242     };
243 }
244
245 =head3 edit
246
247 Method that handles modifying a Koha::Hold object
248
249 =cut
250
251 sub edit {
252     my $c = shift->openapi->valid_input or return;
253
254     return try {
255         my $hold_id = $c->validation->param('hold_id');
256         my $hold = Koha::Holds->find( $hold_id );
257
258         unless ($hold) {
259             return $c->render( status  => 404,
260                             openapi => {error => "Hold not found"} );
261         }
262
263         my $body = $c->req->json;
264
265         my $pickup_library_id = $body->{pickup_library_id};
266
267         if (
268             defined $pickup_library_id
269             and not $hold->is_pickup_location_valid({ library_id => $pickup_library_id })
270           )
271         {
272             return $c->render(
273                 status  => 400,
274                 openapi => {
275                     error => 'The supplied pickup location is not valid'
276                 }
277             );
278         }
279
280         $pickup_library_id    //= $hold->branchcode;
281         my $priority          = $body->{priority} // $hold->priority;
282         # suspended_until can also be set to undef
283         my $suspended_until   = exists $body->{suspended_until} ? $body->{suspended_until} : $hold->suspend_until;
284
285         my $params = {
286             reserve_id    => $hold_id,
287             branchcode    => $pickup_library_id,
288             rank          => $priority,
289             suspend_until => $suspended_until ? output_pref(dt_from_string($suspended_until, 'rfc3339')) : '',
290             itemnumber    => $hold->itemnumber
291         };
292
293         C4::Reserves::ModReserve($params);
294         $hold->discard_changes; # refresh
295
296         return $c->render(
297             status  => 200,
298             openapi => $hold->to_api
299         );
300     }
301     catch {
302         $c->unhandled_exception($_);
303     };
304 }
305
306 =head3 delete
307
308 Method that handles deleting a Koha::Hold object
309
310 =cut
311
312 sub delete {
313     my $c = shift->openapi->valid_input or return;
314
315     my $hold_id = $c->validation->param('hold_id');
316     my $hold    = Koha::Holds->find($hold_id);
317
318     unless ($hold) {
319         return $c->render( status => 404, openapi => { error => "Hold not found." } );
320     }
321
322     return try {
323         $hold->cancel;
324
325         return $c->render(
326             status  => 204,
327             openapi => q{}
328         );
329     }
330     catch {
331         $c->unhandled_exception($_);
332     };
333 }
334
335 =head3 suspend
336
337 Method that handles suspending a hold
338
339 =cut
340
341 sub suspend {
342     my $c = shift->openapi->valid_input or return;
343
344     my $hold_id  = $c->validation->param('hold_id');
345     my $hold     = Koha::Holds->find($hold_id);
346     my $body     = $c->req->json;
347     my $end_date = ($body) ? $body->{end_date} : undef;
348
349     unless ($hold) {
350         return $c->render( status => 404, openapi => { error => 'Hold not found.' } );
351     }
352
353     return try {
354         my $date = ($end_date) ? dt_from_string( $end_date, 'rfc3339' ) : undef;
355         $hold->suspend_hold($date);
356         $hold->discard_changes;
357         $c->res->headers->location( $c->req->url->to_string );
358         my $suspend_end_date;
359         if ($hold->suspend_until) {
360             $suspend_end_date = output_pref({
361                 dt         => dt_from_string( $hold->suspend_until ),
362                 dateformat => 'rfc3339',
363                 dateonly   => 1
364                 }
365             );
366         }
367         return $c->render(
368             status  => 201,
369             openapi => {
370                 end_date => $suspend_end_date
371             }
372         );
373     }
374     catch {
375         if ( blessed $_ and $_->isa('Koha::Exceptions::Hold::CannotSuspendFound') ) {
376             return $c->render( status => 400, openapi => { error => "$_" } );
377         }
378
379         $c->unhandled_exception($_);
380     };
381 }
382
383 =head3 resume
384
385 Method that handles resuming a hold
386
387 =cut
388
389 sub resume {
390     my $c = shift->openapi->valid_input or return;
391
392     my $hold_id = $c->validation->param('hold_id');
393     my $hold    = Koha::Holds->find($hold_id);
394     my $body    = $c->req->json;
395
396     unless ($hold) {
397         return $c->render( status => 404, openapi => { error => 'Hold not found.' } );
398     }
399
400     return try {
401         $hold->resume;
402         return $c->render( status => 204, openapi => {} );
403     }
404     catch {
405         $c->unhandled_exception($_);
406     };
407 }
408
409 =head3 update_priority
410
411 Method that handles modifying a Koha::Hold object
412
413 =cut
414
415 sub update_priority {
416     my $c = shift->openapi->valid_input or return;
417
418     my $hold_id = $c->validation->param('hold_id');
419     my $hold = Koha::Holds->find($hold_id);
420
421     unless ($hold) {
422         return $c->render(
423             status  => 404,
424             openapi => { error => "Hold not found" }
425         );
426     }
427
428     return try {
429         my $priority = $c->req->json;
430         C4::Reserves::_FixPriority(
431             {
432                 reserve_id => $hold_id,
433                 rank       => $priority
434             }
435         );
436
437         return $c->render( status => 200, openapi => $priority );
438     }
439     catch {
440         $c->unhandled_exception($_);
441     };
442 }
443
444 =head3 pickup_locations
445
446 Method that returns the possible pickup_locations for a given hold
447 used for building the dropdown selector
448
449 =cut
450
451 sub pickup_locations {
452     my $c = shift->openapi->valid_input or return;
453
454     my $hold_id = $c->validation->param('hold_id');
455     my $hold = Koha::Holds->find( $hold_id, { prefetch => [ 'patron' ] } );
456
457     unless ($hold) {
458         return $c->render(
459             status  => 404,
460             openapi => { error => "Hold not found" }
461         );
462     }
463
464     return try {
465         my $ps_set;
466
467         if ( $hold->itemnumber ) {
468             $ps_set = $hold->item->pickup_locations( { patron => $hold->patron } );
469         }
470         else {
471             $ps_set = $hold->biblio->pickup_locations( { patron => $hold->patron } );
472         }
473
474         my $pickup_locations = $c->objects->search( $ps_set );
475
476         return $c->render(
477             status  => 200,
478             openapi => $pickup_locations
479         );
480     }
481     catch {
482         $c->unhandled_exception($_);
483     };
484 }
485
486 1;