Bug 30979: Limit public checkout endpoint using preference
[koha.git] / Koha / REST / V1 / Checkouts.pm
1 package Koha::REST::V1::Checkouts;
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 use Mojo::JSON;
22
23 use C4::Auth qw( haspermission );
24 use C4::Context;
25 use C4::Circulation qw( AddIssue AddRenewal CanBookBeRenewed );
26 use Koha::Checkouts;
27 use Koha::Old::Checkouts;
28 use Koha::Token;
29
30 use Try::Tiny qw( catch try );
31
32 =head1 NAME
33
34 Koha::REST::V1::Checkout
35
36 =head1 API
37
38 =head2 Methods
39
40 =head3 list
41
42 List Koha::Checkout objects
43
44 =cut
45
46 sub list {
47     my $c = shift->openapi->valid_input or return;
48
49     my $checked_in = delete $c->validation->output->{checked_in};
50
51     try {
52         my $checkouts_set;
53
54         if ( $checked_in ) {
55             $checkouts_set = Koha::Old::Checkouts->new;
56         } else {
57             $checkouts_set = Koha::Checkouts->new;
58         }
59
60         my $checkouts = $c->objects->search( $checkouts_set );
61
62         return $c->render(
63             status  => 200,
64             openapi => $checkouts
65         );
66     } catch {
67         $c->unhandled_exception($_);
68     };
69 }
70
71 =head3 get
72
73 get one checkout
74
75 =cut
76
77 sub get {
78     my $c = shift->openapi->valid_input or return;
79
80     my $checkout_id = $c->validation->param('checkout_id');
81     my $checkout = Koha::Checkouts->find( $checkout_id );
82     $checkout = Koha::Old::Checkouts->find( $checkout_id )
83         unless ($checkout);
84
85     unless ($checkout) {
86         return $c->render(
87             status => 404,
88             openapi => { error => "Checkout doesn't exist" }
89         );
90     }
91
92     return try {
93         return $c->render(
94             status  => 200,
95             openapi => $checkout->to_api
96         );
97     }
98     catch {
99         $c->unhandled_exception($_);
100     };
101 }
102
103 =head3 _check_availability
104
105 Internal function to call CanBookBeIssued and return an appropriately filtered
106 set return
107
108 =cut
109
110 sub _check_availability {
111     my ( $c, $patron, $item ) = @_;
112
113     my $inprocess       = 0;                   # What does this do?
114     my $ignore_reserves = 0;                   # Don't ignore reserves
115     my $params          = { item => $item };
116
117     my ( $impossible, $confirmation, $alerts, $messages ) =
118       C4::Circulation::CanBookBeIssued( $patron, undef, undef, $inprocess,
119         $ignore_reserves, $params );
120
121     if ( $c->stash('is_public') ) {
122
123         # Upgrade some confirmations to blockers
124         my @should_block =
125           qw/TOO_MANY ISSUED_TO_ANOTHER RESERVED RESERVED_WAITING TRANSFERRED PROCESSING AGE_RESTRICTION/;
126         for my $block (@should_block) {
127             if ( exists( $confirmation->{$block} ) ) {
128                 $impossible->{$block} = $confirmation->{$block};
129                 delete $confirmation->{$block};
130             }
131         }
132
133         # Remove any non-public info that's returned by CanBookBeIssued
134         my @restricted_keys =
135           qw/issued_borrowernumber issued_cardnumber issued_firstname issued_surname resborrowernumber resbranchcode rescardnumber reserve_id resfirstname resreservedate ressurname item_notforloan/;
136         for my $key (@restricted_keys) {
137             delete $confirmation->{$key};
138             delete $impossible->{$key};
139             delete $alerts->{$key};
140             delete $messages->{$key};
141         }
142     }
143
144     return ( $impossible, $confirmation, { %{$alerts}, %{$messages} } );
145 }
146
147 =head3 get_availability
148
149 Controller function that handles retrieval of Checkout availability
150
151 =cut
152
153 sub get_availability {
154     my $c = shift->openapi->valid_input or return;
155     my $user = $c->stash('koha.user');
156
157     my $patron = Koha::Patrons->find( $c->param('patron_id') );
158     my $item   = Koha::Items->find( $c->param('item_id') );
159
160     my ( $impossible, $confirmation, $warnings ) =
161       $c->_check_availability( $patron, $item );
162
163     my $confirm_keys = join( ":", sort keys %{$confirmation} );
164     $confirm_keys = $user->id . ":" . $item->id . ":" . $confirm_keys;
165     my $token = Koha::Token->new->generate_jwt( { id => $confirm_keys } );
166
167     my $response = {
168         blockers           => $impossible,
169         confirms           => $confirmation,
170         warnings           => $warnings,
171         confirmation_token => $token
172     };
173
174     return $c->render( status => 200, openapi => $response );
175 }
176
177 =head3 add
178
179 Add a new checkout
180
181 =cut
182
183 sub add {
184     my $c = shift->openapi->valid_input or return;
185     my $user = $c->stash('koha.user');
186
187     my $body      = $c->req->json;
188     my $item_id   = $body->{item_id};
189     my $patron_id = $body->{patron_id};
190     my $onsite    = $body->{onsite_checkout};
191
192     if ( $c->stash('is_public')
193         && !C4::Context->preference('OpacTrustedCheckout') )
194     {
195         return $c->render(
196             status  => 405,
197             openapi => {
198                 error      => 'Feature disabled',
199                 error_code => 'FEATURE_DISABLED'
200             }
201         );
202     }
203
204     return try {
205         my $item = Koha::Items->find($item_id);
206         unless ($item) {
207             return $c->render(
208                 status  => 409,
209                 openapi => {
210                     error      => 'Item not found',
211                     error_code => 'ITEM_NOT_FOUND',
212                 }
213             );
214         }
215
216         my $patron = Koha::Patrons->find($patron_id);
217         unless ($patron) {
218             return $c->render(
219                 status  => 409,
220                 openapi => {
221                     error      => 'Patron not found',
222                     error_code => 'PATRON_NOT_FOUND',
223                 }
224             );
225         }
226
227         my ( $impossible, $confirmation, $warnings ) =
228           $c->_check_availability( $patron, $item );
229
230         # * Fail for blockers - render 403
231         if ( keys %{$impossible} ) {
232             my @errors = keys %{$impossible};
233             return $c->render(
234                 status  => 403,
235                 openapi => { error => "Checkout not authorized (@errors)" }
236             );
237         }
238
239         # * If confirmation required, check variable set above
240         #   and render 412 if variable is false
241         if ( keys %{$confirmation} ) {
242             my $confirmed = 0;
243
244             # Check for existence of confirmation token
245             # and if exists check validity
246             if ( my $token = $c->param('confirmation') ) {
247                 my $confirm_keys = join( ":", sort keys %{$confirmation} );
248                 $confirm_keys = $user->id . ":" . $item->id . ":" . $confirm_keys;
249                 $confirmed = Koha::Token->new->check_jwt(
250                     { id => $confirm_keys, token => $token } );
251             }
252
253             unless ($confirmed) {
254                 return $c->render(
255                     status  => 412,
256                     openapi => { error => "Confirmation error" }
257                 );
258             }
259         }
260
261         # Call 'AddIssue'
262         my $checkout = AddIssue( $patron->unblessed, $item->barcode );
263         if ($checkout) {
264             $c->res->headers->location(
265                 $c->req->url->to_string . '/' . $checkout->id );
266             return $c->render(
267                 status  => 201,
268                 openapi => $checkout->to_api
269             );
270         }
271         else {
272             return $c->render(
273                 status  => 500,
274                 openapi => { error => 'Unknown error during checkout' }
275             );
276         }
277     }
278     catch {
279         $c->unhandled_exception($_);
280     };
281 }
282
283 =head3 get_renewals
284
285 List Koha::Checkout::Renewals
286
287 =cut
288
289 sub get_renewals {
290     my $c = shift->openapi->valid_input or return;
291
292     try {
293         my $checkout_id = $c->validation->param('checkout_id');
294         my $checkout    = Koha::Checkouts->find($checkout_id);
295         $checkout = Koha::Old::Checkouts->find($checkout_id)
296           unless ($checkout);
297
298         unless ($checkout) {
299             return $c->render(
300                 status  => 404,
301                 openapi => { error => "Checkout doesn't exist" }
302             );
303         }
304
305         my $renewals_rs = $checkout->renewals;
306         my $renewals = $c->objects->search( $renewals_rs );
307
308         return $c->render(
309             status  => 200,
310             openapi => $renewals
311         );
312     }
313     catch {
314         $c->unhandled_exception($_);
315     };
316 }
317
318
319 =head3 renew
320
321 Renew a checkout
322
323 =cut
324
325 sub renew {
326     my $c = shift->openapi->valid_input or return;
327
328     my $checkout_id = $c->validation->param('checkout_id');
329     my $seen = $c->validation->param('seen') || 1;
330     my $checkout = Koha::Checkouts->find( $checkout_id );
331
332     unless ($checkout) {
333         return $c->render(
334             status => 404,
335             openapi => { error => "Checkout doesn't exist" }
336         );
337     }
338
339     return try {
340         my ($can_renew, $error) = CanBookBeRenewed($checkout->patron, $checkout);
341
342         if (!$can_renew) {
343             return $c->render(
344                 status => 403,
345                 openapi => { error => "Renewal not authorized ($error)" }
346             );
347         }
348
349         AddRenewal(
350             $checkout->borrowernumber,
351             $checkout->itemnumber,
352             $checkout->branchcode,
353             undef,
354             undef,
355             $seen
356         );
357         $checkout = Koha::Checkouts->find($checkout_id);
358
359         $c->res->headers->location( $c->req->url->to_string );
360         return $c->render(
361             status  => 201,
362             openapi => $checkout->to_api
363         );
364     }
365     catch {
366         $c->unhandled_exception($_);
367     };
368 }
369
370 =head3 allows_renewal
371
372 Checks if the checkout could be renewed and return the related information.
373
374 =cut
375
376 sub allows_renewal {
377     my $c = shift->openapi->valid_input or return;
378
379     my $checkout_id = $c->validation->param('checkout_id');
380     my $checkout = Koha::Checkouts->find( $checkout_id );
381
382     unless ($checkout) {
383         return $c->render(
384             status => 404,
385             openapi => { error => "Checkout doesn't exist" }
386         );
387     }
388
389     return try {
390         my ($can_renew, $error) = CanBookBeRenewed($checkout->patron, $checkout);
391
392         my $renewable = Mojo::JSON->false;
393         $renewable = Mojo::JSON->true if $can_renew;
394
395         my $rule = Koha::CirculationRules->get_effective_rule(
396             {
397                 categorycode => $checkout->patron->categorycode,
398                 itemtype     => $checkout->item->effective_itemtype,
399                 branchcode   => $checkout->branchcode,
400                 rule_name    => 'renewalsallowed',
401             }
402         );
403         return $c->render(
404             status => 200,
405             openapi => {
406                 allows_renewal => $renewable,
407                 max_renewals => $rule->rule_value,
408                 current_renewals => $checkout->renewals_count,
409                 unseen_renewals => $checkout->unseen_renewals,
410                 error => $error
411             }
412         );
413     }
414     catch {
415         $c->unhandled_exception($_);
416     };
417 }
418
419 1;