Bug 19784: Adapt /v1/patrons to new naming guidelines
[koha.git] / Koha / REST / V1 / Patrons.pm
1 package Koha::REST::V1::Patrons;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with Koha; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18 use Modern::Perl;
19
20 use Mojo::Base 'Mojolicious::Controller';
21
22 use C4::Members qw( AddMember ModMember );
23 use Koha::Patrons;
24
25 use Scalar::Util qw(blessed);
26 use Try::Tiny;
27
28 =head1 NAME
29
30 Koha::REST::V1::Patrons
31
32 =head1 API
33
34 =head2 Methods
35
36 =head3 list
37
38 Controller function that handles listing Koha::Patron objects
39
40 =cut
41
42 sub list {
43     my $c = shift->openapi->valid_input or return;
44
45     return try {
46         my $attributes = {};
47         my $args = $c->validation->output;
48         my ( $params, $reserved_params ) = $c->extract_reserved_params( $args );
49
50         # Merge sorting into query attributes
51         $c->dbic_merge_sorting({ attributes => $attributes, params => $reserved_params });
52
53         # Merge pagination into query attributes
54         $c->dbic_merge_pagination({ filter => $attributes, params => $reserved_params });
55
56         my $restricted = $args->{restricted};
57
58         $params = _to_model($params)
59             if defined $params;
60         # deal with string params
61         $params = $c->build_query_params( $params, $reserved_params );
62
63         # translate 'restricted' => 'debarred'
64         $params->{debarred} = { '!=' => undef }
65           if $restricted;
66
67         my $patrons = Koha::Patrons->search( $params, $attributes );
68         if ( $patrons->is_paged ) {
69             $c->add_pagination_headers(
70                 {
71                     total  => $patrons->pager->total_entries,
72                     params => $args,
73                 }
74             );
75         }
76         my @patrons = $patrons->as_list;
77         @patrons = map { _to_api( $_->TO_JSON ) } @patrons;
78         return $c->render( status => 200, openapi => \@patrons );
79     }
80     catch {
81         if ( $_->isa('DBIx::Class::Exception') ) {
82             return $c->render(
83                 status  => 500,
84                 openapi => { error => $_->{msg} }
85             );
86         }
87         else {
88             return $c->render(
89                 status  => 500,
90                 openapi => { error => "Something went wrong, check the logs." }
91             );
92         }
93     };
94 }
95
96
97 =head3 get
98
99 Controller function that handles retrieving a single Koha::Patron object
100
101 =cut
102
103 sub get {
104     my $c = shift->openapi->valid_input or return;
105
106     my $patron_id = $c->validation->param('patron_id');
107     my $patron    = Koha::Patrons->find($patron_id);
108
109     unless ($patron) {
110         return $c->render( status => 404, openapi => { error => "Patron not found." } );
111     }
112
113     return $c->render( status => 200, openapi => _to_api( $patron->TO_JSON ) );
114 }
115
116 =head3 add
117
118 Controller function that handles adding a new Koha::Patron object
119
120 =cut
121
122 sub add {
123     my $c = shift->openapi->valid_input or return;
124
125     return try {
126
127         my $body = _to_model( $c->validation->param('body') );
128
129         # TODO: Use AddMember until it has been moved to Koha-namespace
130         my $patron_id = AddMember( %{ _to_model($body) } );
131         my $patron    = _to_api( Koha::Patrons->find($patron_id)->TO_JSON );
132
133         return $c->render( status => 201, openapi => $patron );
134     }
135     catch {
136         unless ( blessed $_ && $_->can('rethrow') ) {
137             return $c->render(
138                 status  => 500,
139                 openapi => { error => "Something went wrong, check Koha logs for details." }
140             );
141         }
142         if ( $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
143             return $c->render(
144                 status  => 409,
145                 openapi => { error => $_->error, conflict => $_->duplicate_id }
146             );
147         }
148         elsif ( $_->isa('Koha::Exceptions::Object::FKConstraint') ) {
149             return $c->render(
150                 status  => 400,
151                 openapi => {
152                           error => "Given "
153                         . $Koha::REST::V1::Patrons::to_api_mapping->{ $_->broken_fk }
154                         . " does not exist"
155                 }
156             );
157         }
158         elsif ( $_->isa('Koha::Exceptions::BadParameter') ) {
159             return $c->render(
160                 status  => 400,
161                 openapi => {
162                           error => "Given "
163                         . $Koha::REST::V1::Patrons::to_api_mapping->{ $_->parameter }
164                         . " does not exist"
165                 }
166             );
167         }
168         else {
169             return $c->render(
170                 status  => 500,
171                 openapi => { error => "Something went wrong, check Koha logs for details." }
172             );
173         }
174     };
175 }
176
177
178 =head3 update
179
180 Controller function that handles updating a Koha::Patron object
181
182 =cut
183
184 sub update {
185     my $c = shift->openapi->valid_input or return;
186
187     my $patron_id = $c->validation->param('patron_id');
188     my $patron    = Koha::Patrons->find( $patron_id );
189
190     unless ($patron) {
191          return $c->render(
192              status  => 404,
193              openapi => { error => "Patron not found" }
194          );
195      }
196
197     return try {
198         my $body = _to_model($c->validation->param('body'));
199
200         ## TODO: Use ModMember until it has been moved to Koha-namespace
201         # Add borrowernumber to $body, as required by ModMember
202         $body->{borrowernumber} = $patron_id;
203
204         if ( ModMember(%$body) ) {
205             # Fetch the updated Koha::Patron object
206             $patron->discard_changes;
207             return $c->render( status => 200, openapi => $patron );
208         }
209         else {
210             return $c->render(
211                 status  => 500,
212                 openapi => {
213                     error => 'Something went wrong, check Koha logs for details.'
214                 }
215             );
216         }
217     }
218     catch {
219         unless ( blessed $_ && $_->can('rethrow') ) {
220             return $c->render(
221                 status  => 500,
222                 openapi => {
223                     error => "Something went wrong, check Koha logs for details."
224                 }
225             );
226         }
227         if ( $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
228             return $c->render(
229                 status  => 409,
230                 openapi => { error => $_->error, conflict => $_->duplicate_id }
231             );
232         }
233         elsif ( $_->isa('Koha::Exceptions::Object::FKConstraint') ) {
234             return $c->render(
235                 status  => 400,
236                 openapi => { error => "Given " .
237                             $Koha::REST::V1::Patrons::to_api_mapping->{$_->broken_fk}
238                             . " does not exist" }
239             );
240         }
241         elsif ( $_->isa('Koha::Exceptions::MissingParameter') ) {
242             return $c->render(
243                 status  => 400,
244                 openapi => {
245                     error      => "Missing mandatory parameter(s)",
246                     parameters => $_->parameter
247                 }
248             );
249         }
250         elsif ( $_->isa('Koha::Exceptions::BadParameter') ) {
251             return $c->render(
252                 status  => 400,
253                 openapi => {
254                     error      => "Invalid parameter(s)",
255                     parameters => $_->parameter
256                 }
257             );
258         }
259         elsif ( $_->isa('Koha::Exceptions::NoChanges') ) {
260             return $c->render(
261                 status  => 204,
262                 openapi => { error => "No changes have been made" }
263             );
264         }
265         else {
266             return $c->render(
267                 status  => 500,
268                 openapi => {
269                     error =>
270                       "Something went wrong, check Koha logs for details."
271                 }
272             );
273         }
274     };
275 }
276
277 =head3 delete
278
279 Controller function that handles deleting a Koha::Patron object
280
281 =cut
282
283 sub delete {
284     my $c = shift->openapi->valid_input or return;
285
286     my $patron;
287
288     return try {
289         $patron = Koha::Patrons->find( $c->validation->param('patron_id') );
290
291         # check if loans, reservations, debarrment, etc. before deletion!
292         my $res = $patron->delete;
293         return $c->render( status => 200, openapi => {} );
294     }
295     catch {
296         unless ($patron) {
297             return $c->render(
298                 status  => 404,
299                 openapi => { error => "Patron not found" }
300             );
301         }
302         else {
303             return $c->render(
304                 status  => 500,
305                 openapi => {
306                     error =>
307                       "Something went wrong, check Koha logs for details."
308                 }
309             );
310         }
311     };
312 }
313
314 =head3 _to_api
315
316 Helper function that maps unblessed Koha::Patron objects into REST api
317 attribute names.
318
319 =cut
320
321 sub _to_api {
322     my $patron    = shift;
323     my $patron_id = $patron->{ borrowernumber };
324
325     # Rename attributes
326     foreach my $column ( keys %{ $Koha::REST::V1::Patrons::to_api_mapping } ) {
327         my $mapped_column = $Koha::REST::V1::Patrons::to_api_mapping->{$column};
328         if (    exists $patron->{ $column }
329              && defined $mapped_column )
330         {
331             # key != undef
332             $patron->{ $mapped_column } = delete $patron->{ $column };
333         }
334         elsif (    exists $patron->{ $column }
335                 && !defined $mapped_column )
336         {
337             # key == undef
338             delete $patron->{ $column };
339         }
340     }
341
342     # Calculate the 'restricted' field
343     my $patron_obj = Koha::Patrons->find( $patron_id );
344     $patron->{ restricted } = ($patron_obj->is_debarred) ? Mojo::JSON->true : Mojo::JSON->false;
345
346     return $patron;
347 }
348
349 =head3 _to_model
350
351 Helper function that maps REST api objects into Koha::Patron
352 attribute names.
353
354 =cut
355
356 sub _to_model {
357     my $patron = shift;
358
359     foreach my $attribute ( keys %{ $Koha::REST::V1::Patrons::to_model_mapping } ) {
360         my $mapped_attribute = $Koha::REST::V1::Patrons::to_model_mapping->{$attribute};
361         if (    exists $patron->{ $attribute }
362              && defined $mapped_attribute )
363         {
364             # key => !undef
365             $patron->{ $mapped_attribute } = delete $patron->{ $attribute };
366         }
367         elsif (    exists $patron->{ $attribute }
368                 && !defined $mapped_attribute )
369         {
370             # key => undef / to be deleted
371             delete $patron->{ $attribute };
372         }
373     }
374
375     # TODO: Get rid of this once write operations are based on Koha::Patron
376     if ( exists $patron->{lost} ) {
377         $patron->{lost} = ($patron->{lost}) ? 1 : 0;
378     }
379
380     if ( exists $patron->{ gonenoaddress} ) {
381         $patron->{gonenoaddress} = ($patron->{gonenoaddress}) ? 1 : 0;
382     }
383
384     return $patron;
385 }
386
387 =head2 Global variables
388
389 =head3 $to_api_mapping
390
391 =cut
392
393 our $to_api_mapping = {
394     borrowernotes       => 'staff_notes',
395     borrowernumber      => 'patron_id',
396     branchcode          => 'library_id',
397     categorycode        => 'category_id',
398     checkprevcheckout   => 'check_previous_checkout',
399     contactfirstname    => undef, # Unused
400     contactname         => undef, # Unused
401     contactnote         => 'altaddress_notes',
402     contacttitle        => undef, # Unused
403     dateenrolled        => 'date_enrolled',
404     dateexpiry          => 'expiry_date',
405     dateofbirth         => 'date_of_birth',
406     debarred            => undef, # replaced by 'restricted'
407     debarredcomment     => undef, # calculated, API consumers will use /restrictions instead
408     emailpro            => 'secondary_email',
409     flags               => undef, # permissions manipulation handled in /permissions
410     gonenoaddress       => 'incorrect_address',
411     guarantorid         => 'guarantor_id',
412     lastseen            => 'last_seen',
413     lost                => 'patron_card_lost',
414     opacnote            => 'opac_notes',
415     othernames          => 'other_name',
416     password            => undef, # password manipulation handled in /password
417     phonepro            => 'secondary_phone',
418     relationship        => 'relationship_type',
419     sex                 => 'gender',
420     smsalertnumber      => 'sms_number',
421     sort1               => 'statistics_1',
422     sort2               => 'statistics_2',
423     streetnumber        => 'street_number',
424     streettype          => 'street_type',
425     zipcode             => 'postal_code',
426     B_address           => 'altaddress_address',
427     B_address2          => 'altaddress_address2',
428     B_city              => 'altaddress_city',
429     B_country           => 'altaddress_country',
430     B_email             => 'altaddress_email',
431     B_phone             => 'altaddress_phone',
432     B_state             => 'altaddress_state',
433     B_streetnumber      => 'altaddress_street_number',
434     B_streettype        => 'altaddress_street_type',
435     B_zipcode           => 'altaddress_postal_code',
436     altcontactaddress1  => 'altcontact_address',
437     altcontactaddress2  => 'altcontact_address2',
438     altcontactaddress3  => 'altcontact_city',
439     altcontactcountry   => 'altcontact_country',
440     altcontactfirstname => 'altcontact_firstname',
441     altcontactphone     => 'altcontact_phone',
442     altcontactsurname   => 'altcontact_surname',
443     altcontactstate     => 'altcontact_state',
444     altcontactzipcode   => 'altcontact_postal_code'
445 };
446
447 =head3 $to_model_mapping
448
449 =cut
450
451 our $to_model_mapping = {
452     altaddress_notes         => 'contactnote',
453     category_id              => 'categorycode',
454     check_previous_checkout  => 'checkprevcheckout',
455     date_enrolled            => 'dateenrolled',
456     date_of_birth            => 'dateofbirth',
457     expiry_date              => 'dateexpiry',
458     gender                   => 'sex',
459     guarantor_id             => 'guarantorid',
460     incorrect_address        => 'gonenoaddress',
461     last_seen                => 'lastseen',
462     library_id               => 'branchcode',
463     opac_notes               => 'opacnote',
464     other_name               => 'othernames',
465     patron_card_lost         => 'lost',
466     patron_id                => 'borrowernumber',
467     postal_code              => 'zipcode',
468     relationship_type        => 'relationship',
469     restricted               => undef,
470     secondary_email          => 'emailpro',
471     secondary_phone          => 'phonepro',
472     sms_number               => 'smsalertnumber',
473     staff_notes              => 'borrowernotes',
474     statistics_1             => 'sort1',
475     statistics_2             => 'sort2',
476     street_number            => 'streetnumber',
477     street_type              => 'streettype',
478     altaddress_address       => 'B_address',
479     altaddress_address2      => 'B_address2',
480     altaddress_city          => 'B_city',
481     altaddress_country       => 'B_country',
482     altaddress_email         => 'B_email',
483     altaddress_phone         => 'B_phone',
484     altaddress_state         => 'B_state',
485     altaddress_street_number => 'B_streetnumber',
486     altaddress_street_type   => 'B_streettype',
487     altaddress_postal_code   => 'B_zipcode',
488     altcontact_firstname     => 'altcontactfirstname',
489     altcontact_surname       => 'altcontactsurname',
490     altcontact_address       => 'altcontactaddress1',
491     altcontact_address2      => 'altcontactaddress2',
492     altcontact_city          => 'altcontactaddress3',
493     altcontact_state         => 'altcontactstate',
494     altcontact_postal_code   => 'altcontactzipcode',
495     altcontact_country       => 'altcontactcountry',
496     altcontact_phone         => 'altcontactphone'
497 };
498
499 1;