Bug 30979: Add public endpoints
[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     return try {
193         my $item = Koha::Items->find($item_id);
194         unless ($item) {
195             return $c->render(
196                 status  => 409,
197                 openapi => {
198                     error      => 'Item not found',
199                     error_code => 'ITEM_NOT_FOUND',
200                 }
201             );
202         }
203
204         my $patron = Koha::Patrons->find($patron_id);
205         unless ($patron) {
206             return $c->render(
207                 status  => 409,
208                 openapi => {
209                     error      => 'Patron not found',
210                     error_code => 'PATRON_NOT_FOUND',
211                 }
212             );
213         }
214
215         my ( $impossible, $confirmation, $warnings ) =
216           $c->_check_availability( $patron, $item );
217
218         # * Fail for blockers - render 403
219         if ( keys %{$impossible} ) {
220             my @errors = keys %{$impossible};
221             return $c->render(
222                 status  => 403,
223                 openapi => { error => "Checkout not authorized (@errors)" }
224             );
225         }
226
227         # * If confirmation required, check variable set above
228         #   and render 412 if variable is false
229         if ( keys %{$confirmation} ) {
230             my $confirmed = 0;
231
232             # Check for existence of confirmation token
233             # and if exists check validity
234             if ( my $token = $c->param('confirmation') ) {
235                 my $confirm_keys = join( ":", sort keys %{$confirmation} );
236                 $confirm_keys = $user->id . ":" . $item->id . ":" . $confirm_keys;
237                 $confirmed = Koha::Token->new->check_jwt(
238                     { id => $confirm_keys, token => $token } );
239             }
240
241             unless ($confirmed) {
242                 return $c->render(
243                     status  => 412,
244                     openapi => { error => "Confirmation error" }
245                 );
246             }
247         }
248
249         # Call 'AddIssue'
250         my $checkout = AddIssue( $patron->unblessed, $item->barcode );
251         if ($checkout) {
252             $c->res->headers->location(
253                 $c->req->url->to_string . '/' . $checkout->id );
254             return $c->render(
255                 status  => 201,
256                 openapi => $checkout->to_api
257             );
258         }
259         else {
260             return $c->render(
261                 status  => 500,
262                 openapi => { error => 'Unknown error during checkout' }
263             );
264         }
265     }
266     catch {
267         $c->unhandled_exception($_);
268     };
269 }
270
271 =head3 get_renewals
272
273 List Koha::Checkout::Renewals
274
275 =cut
276
277 sub get_renewals {
278     my $c = shift->openapi->valid_input or return;
279
280     try {
281         my $checkout_id = $c->validation->param('checkout_id');
282         my $checkout    = Koha::Checkouts->find($checkout_id);
283         $checkout = Koha::Old::Checkouts->find($checkout_id)
284           unless ($checkout);
285
286         unless ($checkout) {
287             return $c->render(
288                 status  => 404,
289                 openapi => { error => "Checkout doesn't exist" }
290             );
291         }
292
293         my $renewals_rs = $checkout->renewals;
294         my $renewals = $c->objects->search( $renewals_rs );
295
296         return $c->render(
297             status  => 200,
298             openapi => $renewals
299         );
300     }
301     catch {
302         $c->unhandled_exception($_);
303     };
304 }
305
306
307 =head3 renew
308
309 Renew a checkout
310
311 =cut
312
313 sub renew {
314     my $c = shift->openapi->valid_input or return;
315
316     my $checkout_id = $c->validation->param('checkout_id');
317     my $seen = $c->validation->param('seen') || 1;
318     my $checkout = Koha::Checkouts->find( $checkout_id );
319
320     unless ($checkout) {
321         return $c->render(
322             status => 404,
323             openapi => { error => "Checkout doesn't exist" }
324         );
325     }
326
327     return try {
328         my ($can_renew, $error) = CanBookBeRenewed($checkout->patron, $checkout);
329
330         if (!$can_renew) {
331             return $c->render(
332                 status => 403,
333                 openapi => { error => "Renewal not authorized ($error)" }
334             );
335         }
336
337         AddRenewal(
338             $checkout->borrowernumber,
339             $checkout->itemnumber,
340             $checkout->branchcode,
341             undef,
342             undef,
343             $seen
344         );
345         $checkout = Koha::Checkouts->find($checkout_id);
346
347         $c->res->headers->location( $c->req->url->to_string );
348         return $c->render(
349             status  => 201,
350             openapi => $checkout->to_api
351         );
352     }
353     catch {
354         $c->unhandled_exception($_);
355     };
356 }
357
358 =head3 allows_renewal
359
360 Checks if the checkout could be renewed and return the related information.
361
362 =cut
363
364 sub allows_renewal {
365     my $c = shift->openapi->valid_input or return;
366
367     my $checkout_id = $c->validation->param('checkout_id');
368     my $checkout = Koha::Checkouts->find( $checkout_id );
369
370     unless ($checkout) {
371         return $c->render(
372             status => 404,
373             openapi => { error => "Checkout doesn't exist" }
374         );
375     }
376
377     return try {
378         my ($can_renew, $error) = CanBookBeRenewed($checkout->patron, $checkout);
379
380         my $renewable = Mojo::JSON->false;
381         $renewable = Mojo::JSON->true if $can_renew;
382
383         my $rule = Koha::CirculationRules->get_effective_rule(
384             {
385                 categorycode => $checkout->patron->categorycode,
386                 itemtype     => $checkout->item->effective_itemtype,
387                 branchcode   => $checkout->branchcode,
388                 rule_name    => 'renewalsallowed',
389             }
390         );
391         return $c->render(
392             status => 200,
393             openapi => {
394                 allows_renewal => $renewable,
395                 max_renewals => $rule->rule_value,
396                 current_renewals => $checkout->renewals_count,
397                 unseen_renewals => $checkout->unseen_renewals,
398                 error => $error
399             }
400         );
401     }
402     catch {
403         $c->unhandled_exception($_);
404     };
405 }
406
407 1;