Bug 29002: (QA follow-up) Correct param retrieval in controllers
[koha.git] / Koha / REST / V1 / Items.pm
1 package Koha::REST::V1::Items;
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 C4::Circulation qw( barcodedecode );
23
24 use Koha::Items;
25
26 use List::MoreUtils qw( any );
27 use Try::Tiny qw( catch try );
28
29 =head1 NAME
30
31 Koha::REST::V1::Items - Koha REST API for handling items (V1)
32
33 =head1 API
34
35 =head2 Methods
36
37 =cut
38
39 =head3 list
40
41 Controller function that handles listing Koha::Item objects
42
43 =cut
44
45 sub list {
46     my $c = shift->openapi->valid_input or return;
47
48     return try {
49         my $items_set = Koha::Items->new;
50         my $items     = $c->objects->search( $items_set );
51         return $c->render(
52             status  => 200,
53             openapi => $items
54         );
55     }
56     catch {
57         $c->unhandled_exception($_);
58     };
59 }
60
61 =head3 list_public
62
63 Controller function that handles listing Koha::Item objects available to the opac
64
65 =cut
66
67 sub list_public {
68     my $c = shift->openapi->valid_input or return;
69
70     return try {
71         my $patron = $c->stash('koha.user');
72
73         my $items_set =
74           Koha::Items->filter_by_visible_in_opac( { patron => $patron } );
75         my $items = $c->objects->search($items_set);
76
77         return $c->render(
78             status  => 200,
79             openapi => $items
80         );
81     }
82     catch {
83         $c->unhandled_exception($_);
84     };
85 }
86
87 =head3 get
88
89 Controller function that handles retrieving a single Koha::Item
90
91 =cut
92
93 sub get {
94     my $c = shift->openapi->valid_input or return;
95
96     try {
97         my $items_rs = Koha::Items->new;
98         my $item = $c->objects->find($items_rs, $c->param('item_id'));
99         unless ( $item ) {
100             return $c->render(
101                 status => 404,
102                 openapi => { error => 'Item not found'}
103             );
104         }
105         return $c->render( status => 200, openapi => $item );
106     }
107     catch {
108         $c->unhandled_exception($_);
109     };
110 }
111
112 =head3 delete
113
114 Controller function that handles deleting a single Koha::Item
115
116 =cut
117
118 sub delete {
119     my $c = shift->openapi->valid_input or return;
120
121     return try {
122         my $item = Koha::Items->find($c->param('item_id'));
123         unless ( $item ) {
124             return $c->render(
125                 status => 404,
126                 openapi => { error => 'Item not found'}
127             );
128         }
129
130         my $safe_to_delete = $item->safe_to_delete;
131
132         if ( !$safe_to_delete ) {
133
134             # Pick the first error, if any
135             my ( $error ) = grep { $_->type eq 'error' } @{ $safe_to_delete->messages };
136
137             unless ( $error ) {
138                 Koha::Exception->throw('Koha::Item->safe_to_delete returned false but carried no error message');
139             }
140
141             my $errors = {
142                 book_on_loan       => { code => 'checked_out',        description => 'The item is checked out' },
143                 book_reserved      => { code => 'found_hold',         description => 'Waiting or in-transit hold for the item' },
144                 last_item_for_hold => { code => 'last_item_for_hold', description => 'The item is the last one on a record on which a biblio-level hold is placed' },
145                 linked_analytics   => { code => 'linked_analytics',   description => 'The item has linked analytic records' },
146                 not_same_branch    => { code => 'not_same_branch',    description => 'The item is blocked by independent branches' },
147             };
148
149             if ( any { $error->message eq $_ } keys %{$errors} ) {
150
151                 my $code = $error->message;
152
153                 return $c->render(
154                     status  => 409,
155                     openapi => {
156                         error      => $errors->{ $code }->{description},
157                         error_code => $errors->{ $code }->{code},
158                     }
159                 );
160             } else {
161                 Koha::Exception->throw( 'Koha::Patron->safe_to_delete carried an unexpected message: ' . $error->message );
162             }
163         }
164
165         $item->safe_delete;
166
167         return $c->render(
168             status  => 204,
169             openapi => q{}
170         );
171     }
172     catch {
173         $c->unhandled_exception($_);
174     };
175 }
176
177 =head3 get_bookings
178
179 Controller function that handles retrieving item's bookings
180
181 =cut
182
183 sub get_bookings {
184     my $c = shift->openapi->valid_input or return;
185
186     my $item = Koha::Items->find( { itemnumber => $c->param('item_id') }, { prefetch => ['bookings'] } );
187
188     unless ($item) {
189         return $c->render(
190             status  => 404,
191             openapi => { error => "Object not found." }
192         );
193     }
194
195     return try {
196
197         my $bookings_rs = $item->bookings;
198         my $bookings    = $c->objects->search($bookings_rs);
199         return $c->render(
200             status  => 200,
201             openapi => $bookings
202         );
203     } catch {
204         $c->unhandled_exception($_);
205     };
206 }
207
208 =head3 pickup_locations
209
210 Method that returns the possible pickup_locations for a given item
211 used for building the dropdown selector
212
213 =cut
214
215 sub pickup_locations {
216     my $c = shift->openapi->valid_input or return;
217
218     my $item_id = $c->param('item_id');
219     my $item = Koha::Items->find( $item_id );
220
221     unless ($item) {
222         return $c->render(
223             status  => 404,
224             openapi => { error => "Item not found" }
225         );
226     }
227
228     my $patron_id = $c->param('patron_id');
229     my $patron    = Koha::Patrons->find( $patron_id );
230
231     $c->req->params->remove('patron_id');
232
233     unless ($patron) {
234         return $c->render(
235             status  => 400,
236             openapi => { error => "Patron not found" }
237         );
238     }
239
240     return try {
241
242         my $pl_set = $item->pickup_locations( { patron => $patron } );
243
244         my @response = ();
245         if ( C4::Context->preference('AllowHoldPolicyOverride') ) {
246
247             my $libraries_rs = Koha::Libraries->search( { pickup_location => 1 } );
248             my $libraries    = $c->objects->search($libraries_rs);
249
250             @response = map {
251                 my $library = $_;
252                 $library->{needs_override} = (
253                     any { $_->branchcode eq $library->{library_id} }
254                     @{ $pl_set->as_list }
255                   )
256                   ? Mojo::JSON->false
257                   : Mojo::JSON->true;
258                 $library;
259             } @{$libraries};
260         }
261         else {
262
263             my $pickup_locations = $c->objects->search($pl_set);
264             @response = map { $_->{needs_override} = Mojo::JSON->false; $_; } @{$pickup_locations};
265         }
266
267         return $c->render(
268             status  => 200,
269             openapi => \@response
270         );
271     }
272     catch {
273         $c->unhandled_exception($_);
274     };
275 }
276
277 =head3 bundled_items
278
279 Controller function that handles bundled_items Koha::Item objects
280
281 =cut
282
283 sub bundled_items {
284     my $c = shift->openapi->valid_input or return;
285
286     my $item_id = $c->param('item_id');
287     my $item = Koha::Items->find( $item_id );
288
289     unless ($item) {
290         return $c->render(
291             status  => 404,
292             openapi => { error => "Item not found" }
293         );
294     }
295
296     return try {
297         my $items_set = $item->bundle_items;
298         my $items     = $c->objects->search( $items_set );
299         return $c->render(
300             status  => 200,
301             openapi => $items
302         );
303     }
304     catch {
305         $c->unhandled_exception($_);
306     };
307 }
308
309 =head3 add_to_bundle
310
311 Controller function that handles adding items to this bundle
312
313 =cut
314
315 sub add_to_bundle {
316     my $c = shift->openapi->valid_input or return;
317
318     my $item_id = $c->param('item_id');
319     my $item = Koha::Items->find( $item_id );
320
321     unless ($item) {
322         return $c->render(
323             status  => 404,
324             openapi => { error => "Item not found" }
325         );
326     }
327
328     my $body = $c->req->json;
329
330     my $bundle_item_id = $body->{'external_id'};
331     $bundle_item_id = barcodedecode($bundle_item_id);
332     my $bundle_item = Koha::Items->find( { barcode => $bundle_item_id } );
333
334     unless ($bundle_item) {
335         return $c->render(
336             status  => 404,
337             openapi => { error => "Bundle item not found" }
338         );
339     }
340
341     return try {
342         my $options = {
343             force_checkin => $body->{force_checkin},
344             ignore_holds => $body->{ignore_holds},
345         };
346
347         my $link = $item->add_to_bundle($bundle_item, $options);
348         return $c->render(
349             status  => 201,
350             openapi => $bundle_item
351         );
352     }
353     catch {
354         if ( ref($_) eq 'Koha::Exceptions::Object::DuplicateID' ) {
355             return $c->render(
356                 status  => 409,
357                 openapi => {
358                     error      => 'Item is already bundled',
359                     error_code => 'already_bundled',
360                     key        => $_->duplicate_id
361                 }
362             );
363         }
364         elsif ( ref($_) eq 'Koha::Exceptions::Item::Bundle::BundleIsCheckedOut' ) {
365             return $c->render(
366                 status  => 409,
367                 openapi => {
368                     error      => 'Bundle is checked out',
369                     error_code => 'bundle_checked_out'
370                 }
371             );
372         } elsif ( ref($_) eq 'Koha::Exceptions::Item::Bundle::ItemIsCheckedOut' ) {
373             return $c->render(
374                 status  => 409,
375                 openapi => {
376                     error      => 'Item is checked out',
377                     error_code => 'checked_out'
378                 }
379             );
380         }
381         elsif ( ref($_) eq 'Koha::Exceptions::Checkin::FailedCheckin' ) {
382             return $c->render(
383                 status  => 409,
384                 openapi => {
385                     error      => 'Item cannot be checked in',
386                     error_code => 'failed_checkin'
387                 }
388             );
389         }
390         elsif ( ref($_) eq 'Koha::Exceptions::Item::Bundle::ItemHasHolds' ) {
391             return $c->render(
392                 status  => 409,
393                 openapi => {
394                     error      => 'Item is reserved',
395                     error_code => 'reserved'
396                 }
397             );
398         }
399         elsif ( ref($_) eq 'Koha::Exceptions::Item::Bundle::IsBundle' ) {
400             return $c->render(
401                 status  => 400,
402                 openapi => {
403                     error      => 'Bundles cannot be nested',
404                     error_code => 'failed_nesting'
405                 }
406             );
407         }
408         else {
409             $c->unhandled_exception($_);
410         }
411     };
412 }
413
414 =head3 remove_from_bundle
415
416 Controller function that handles removing items from this bundle
417
418 =cut
419
420 sub remove_from_bundle {
421     my $c = shift->openapi->valid_input or return;
422
423     my $item_id = $c->param('item_id');
424     my $item = Koha::Items->find( $item_id );
425
426     unless ($item) {
427         return $c->render(
428             status  => 404,
429             openapi => { error => "Item not found" }
430         );
431     }
432
433     my $bundle_item_id = $c->param('bundled_item_id');
434     $bundle_item_id = barcodedecode($bundle_item_id);
435     my $bundle_item = Koha::Items->find( { itemnumber => $bundle_item_id } );
436
437     unless ($bundle_item) {
438         return $c->render(
439             status  => 404,
440             openapi => { error => "Bundle item not found" }
441         );
442     }
443
444     return try {
445         $bundle_item->remove_from_bundle;
446         return $c->render(
447             status  => 204,
448             openapi => q{}
449         );
450     } catch {
451         if ( ref($_) eq 'Koha::Exceptions::Item::Bundle::BundleIsCheckedOut' ) {
452             return $c->render(
453                 status  => 409,
454                 openapi => {
455                     error      => 'Bundle is checked out',
456                     error_code => 'bundle_checked_out'
457                 }
458             );
459         } else {
460             $c->unhandled_exception($_);
461         }
462     };
463 }
464
465 1;