Bug 30933: Add lists methods for disowning
[koha.git] / Koha / Patron.pm
1 package Koha::Patron;
2
3 # Copyright ByWater Solutions 2014
4 # Copyright PTFS Europe 2016
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 use Modern::Perl;
22
23 use List::MoreUtils qw( any uniq );
24 use JSON qw( to_json );
25 use Unicode::Normalize qw( NFKD );
26 use Try::Tiny;
27
28 use C4::Context;
29 use C4::Log qw( logaction );
30 use Koha::Account;
31 use Koha::ArticleRequests;
32 use C4::Letters;
33 use Koha::AuthUtils;
34 use Koha::Checkouts;
35 use Koha::CirculationRules;
36 use Koha::Club::Enrollments;
37 use Koha::Database;
38 use Koha::DateUtils qw( dt_from_string );
39 use Koha::Encryption;
40 use Koha::Exceptions::Password;
41 use Koha::Holds;
42 use Koha::Old::Checkouts;
43 use Koha::Patron::Attributes;
44 use Koha::Patron::Categories;
45 use Koha::Patron::Debarments;
46 use Koha::Patron::HouseboundProfile;
47 use Koha::Patron::HouseboundRole;
48 use Koha::Patron::Images;
49 use Koha::Patron::Messages;
50 use Koha::Patron::Modifications;
51 use Koha::Patron::Relationships;
52 use Koha::Patrons;
53 use Koha::Plugins;
54 use Koha::Recalls;
55 use Koha::Result::Boolean;
56 use Koha::Subscription::Routinglists;
57 use Koha::Token;
58 use Koha::Virtualshelves;
59
60 use base qw(Koha::Object);
61
62 use constant ADMINISTRATIVE_LOCKOUT => -1;
63
64 our $RESULTSET_PATRON_ID_MAPPING = {
65     Accountline          => 'borrowernumber',
66     Aqbasketuser         => 'borrowernumber',
67     Aqbudget             => 'budget_owner_id',
68     Aqbudgetborrower     => 'borrowernumber',
69     ArticleRequest       => 'borrowernumber',
70     BorrowerDebarment    => 'borrowernumber',
71     BorrowerFile         => 'borrowernumber',
72     BorrowerModification => 'borrowernumber',
73     ClubEnrollment       => 'borrowernumber',
74     Issue                => 'borrowernumber',
75     ItemsLastBorrower    => 'borrowernumber',
76     Linktracker          => 'borrowernumber',
77     Message              => 'borrowernumber',
78     MessageQueue         => 'borrowernumber',
79     OldIssue             => 'borrowernumber',
80     OldReserve           => 'borrowernumber',
81     Rating               => 'borrowernumber',
82     Reserve              => 'borrowernumber',
83     Review               => 'borrowernumber',
84     SearchHistory        => 'userid',
85     Statistic            => 'borrowernumber',
86     Suggestion           => 'suggestedby',
87     TagAll               => 'borrowernumber',
88     Virtualshelfcontent  => 'borrowernumber',
89     Virtualshelfshare    => 'borrowernumber',
90     Virtualshelve        => 'owner',
91 };
92
93 =head1 NAME
94
95 Koha::Patron - Koha Patron Object class
96
97 =head1 API
98
99 =head2 Class Methods
100
101 =head3 new
102
103 =cut
104
105 sub new {
106     my ( $class, $params ) = @_;
107
108     return $class->SUPER::new($params);
109 }
110
111 =head3 fixup_cardnumber
112
113 Autogenerate next cardnumber from highest value found in database
114
115 =cut
116
117 sub fixup_cardnumber {
118     my ( $self ) = @_;
119
120     my $max = $self->cardnumber;
121     Koha::Plugins->call( 'patron_barcode_transform', \$max );
122
123     $max ||= Koha::Patrons->search({
124         cardnumber => {-regexp => '^-?[0-9]+$'}
125     }, {
126         select => \'CAST(cardnumber AS SIGNED)',
127         as => ['cast_cardnumber']
128     })->_resultset->get_column('cast_cardnumber')->max;
129     $self->cardnumber(($max || 0) +1);
130 }
131
132 =head3 trim_whitespace
133
134 trim whitespace from data which has some non-whitespace in it.
135 Could be moved to Koha::Object if need to be reused
136
137 =cut
138
139 sub trim_whitespaces {
140     my( $self ) = @_;
141
142     my $schema  = Koha::Database->new->schema;
143     my @columns = $schema->source($self->_type)->columns;
144
145     for my $column( @columns ) {
146         my $value = $self->$column;
147         if ( defined $value ) {
148             $value =~ s/^\s*|\s*$//g;
149             $self->$column($value);
150         }
151     }
152     return $self;
153 }
154
155 =head3 plain_text_password
156
157 $patron->plain_text_password( $password );
158
159 stores a copy of the unencrypted password in the object
160 for use in code before encrypting for db
161
162 =cut
163
164 sub plain_text_password {
165     my ( $self, $password ) = @_;
166     if ( $password ) {
167         $self->{_plain_text_password} = $password;
168         return $self;
169     }
170     return $self->{_plain_text_password}
171         if $self->{_plain_text_password};
172
173     return;
174 }
175
176 =head3 store
177
178 Patron specific store method to cleanup record
179 and do other necessary things before saving
180 to db
181
182 =cut
183
184 sub store {
185     my ($self) = @_;
186
187     $self->_result->result_source->schema->txn_do(
188         sub {
189             if (
190                 C4::Context->preference("autoMemberNum")
191                 and ( not defined $self->cardnumber
192                     or $self->cardnumber eq '' )
193               )
194             {
195                 # Warning: The caller is responsible for locking the members table in write
196                 # mode, to avoid database corruption.
197                 # We are in a transaction but the table is not locked
198                 $self->fixup_cardnumber;
199             }
200
201             unless( $self->category->in_storage ) {
202                 Koha::Exceptions::Object::FKConstraint->throw(
203                     broken_fk => 'categorycode',
204                     value     => $self->categorycode,
205                 );
206             }
207
208             $self->trim_whitespaces;
209
210             my $new_cardnumber = $self->cardnumber;
211             Koha::Plugins->call( 'patron_barcode_transform', \$new_cardnumber );
212             $self->cardnumber( $new_cardnumber );
213
214             # Set surname to uppercase if uppercasesurname is true
215             $self->surname( uc($self->surname) )
216                 if C4::Context->preference("uppercasesurnames");
217
218             $self->relationship(undef) # We do not want to store an empty string in this field
219               if defined $self->relationship
220                      and $self->relationship eq "";
221
222             unless ( $self->in_storage ) {    #AddMember
223
224                 # Generate a valid userid/login if needed
225                 $self->generate_userid
226                   if not $self->userid or not $self->has_valid_userid;
227
228                 # Add expiration date if it isn't already there
229                 unless ( $self->dateexpiry ) {
230                     $self->dateexpiry( $self->category->get_expiry_date );
231                 }
232
233                 # Add enrollment date if it isn't already there
234                 unless ( $self->dateenrolled ) {
235                     $self->dateenrolled(dt_from_string);
236                 }
237
238                 # Set the privacy depending on the patron's category
239                 my $default_privacy = $self->category->default_privacy || q{};
240                 $default_privacy =
241                     $default_privacy eq 'default' ? 1
242                   : $default_privacy eq 'never'   ? 2
243                   : $default_privacy eq 'forever' ? 0
244                   :                                                   undef;
245                 $self->privacy($default_privacy);
246
247                 # Call any check_password plugins if password is passed
248                 if ( C4::Context->config("enable_plugins") && $self->password ) {
249                     my @plugins = Koha::Plugins->new()->GetPlugins({
250                         method => 'check_password',
251                     });
252                     foreach my $plugin ( @plugins ) {
253                         # This plugin hook will also be used by a plugin for the Norwegian national
254                         # patron database. This is why we need to pass both the password and the
255                         # borrowernumber to the plugin.
256                         my $ret = $plugin->check_password(
257                             {
258                                 password       => $self->password,
259                                 borrowernumber => $self->borrowernumber
260                             }
261                         );
262                         if ( $ret->{'error'} == 1 ) {
263                             Koha::Exceptions::Password::Plugin->throw();
264                         }
265                     }
266                 }
267
268                 # Make a copy of the plain text password for later use
269                 $self->plain_text_password( $self->password );
270
271                 $self->password_expiration_date( $self->password
272                     ? $self->category->get_password_expiry_date || undef
273                     : undef );
274                 # Create a disabled account if no password provided
275                 $self->password( $self->password
276                     ? Koha::AuthUtils::hash_password( $self->password )
277                     : '!' );
278
279                 $self->borrowernumber(undef);
280
281                 $self = $self->SUPER::store;
282
283                 $self->add_enrolment_fee_if_needed(0);
284
285                 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
286                   if C4::Context->preference("BorrowersLog");
287             }
288             else {    #ModMember
289
290                 my $self_from_storage = $self->get_from_storage;
291                 # FIXME We should not deal with that here, callers have to do this job
292                 # Moved from ModMember to prevent regressions
293                 unless ( $self->userid ) {
294                     my $stored_userid = $self_from_storage->userid;
295                     $self->userid($stored_userid);
296                 }
297
298                 # Password must be updated using $self->set_password
299                 $self->password($self_from_storage->password);
300
301                 if ( $self->category->categorycode ne
302                     $self_from_storage->category->categorycode )
303                 {
304                     # Add enrolement fee on category change if required
305                     $self->add_enrolment_fee_if_needed(1)
306                       if C4::Context->preference('FeeOnChangePatronCategory');
307
308                     # Clean up guarantors on category change if required
309                     $self->guarantor_relationships->delete
310                       unless ( $self->category->can_be_guarantee );
311
312                 }
313
314                 # Actionlogs
315                 if ( C4::Context->preference("BorrowersLog") ) {
316                     my $info;
317                     my $from_storage = $self_from_storage->unblessed;
318                     my $from_object  = $self->unblessed;
319                     my @skip_fields  = (qw/lastseen updated_on/);
320                     for my $key ( keys %{$from_storage} ) {
321                         next if any { /$key/ } @skip_fields;
322                         if (
323                             (
324                                   !defined( $from_storage->{$key} )
325                                 && defined( $from_object->{$key} )
326                             )
327                             || ( defined( $from_storage->{$key} )
328                                 && !defined( $from_object->{$key} ) )
329                             || (
330                                    defined( $from_storage->{$key} )
331                                 && defined( $from_object->{$key} )
332                                 && ( $from_storage->{$key} ne
333                                     $from_object->{$key} )
334                             )
335                           )
336                         {
337                             $info->{$key} = {
338                                 before => $from_storage->{$key},
339                                 after  => $from_object->{$key}
340                             };
341                         }
342                     }
343
344                     if ( defined($info) ) {
345                         logaction(
346                             "MEMBERS",
347                             "MODIFY",
348                             $self->borrowernumber,
349                             to_json(
350                                 $info,
351                                 { utf8 => 1, pretty => 1, canonical => 1 }
352                             )
353                         );
354                     }
355                 }
356
357                 # Final store
358                 $self = $self->SUPER::store;
359             }
360         }
361     );
362     return $self;
363 }
364
365 =head3 delete
366
367 $patron->delete
368
369 Delete patron's holds, lists and finally the patron.
370
371 Lists owned by the borrower are deleted or ownership is transferred depending on the
372 ListOwnershipUponPatronDeletion pref, but entries from the borrower to other lists are kept.
373
374 =cut
375
376 sub delete {
377     my ($self) = @_;
378
379     my $anonymous_patron = C4::Context->preference("AnonymousPatron");
380     Koha::Exceptions::Patron::FailedDeleteAnonymousPatron->throw() if $anonymous_patron && $self->id eq $anonymous_patron;
381
382     $self->_result->result_source->schema->txn_do(
383         sub {
384             # Cancel Patron's holds
385             my $holds = $self->holds;
386             while( my $hold = $holds->next ){
387                 $hold->cancel;
388             }
389
390             # Handle lists (virtualshelves)
391             $self->virtualshelves->disown_or_delete;
392
393             # We cannot have a FK on borrower_modifications.borrowernumber, the table is also used
394             # for patron selfreg
395             $_->delete for Koha::Patron::Modifications->search( { borrowernumber => $self->borrowernumber } )->as_list;
396
397             $self->SUPER::delete;
398
399             logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
400         }
401     );
402     return $self;
403 }
404
405 =head3 category
406
407 my $patron_category = $patron->category
408
409 Return the patron category for this patron
410
411 =cut
412
413 sub category {
414     my ( $self ) = @_;
415     return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
416 }
417
418 =head3 image
419
420 =cut
421
422 sub image {
423     my ( $self ) = @_;
424
425     return Koha::Patron::Images->find( $self->borrowernumber );
426 }
427
428 =head3 library
429
430 Returns a Koha::Library object representing the patron's home library.
431
432 =cut
433
434 sub library {
435     my ( $self ) = @_;
436     return Koha::Library->_new_from_dbic($self->_result->branchcode);
437 }
438
439 =head3 sms_provider
440
441 Returns a Koha::SMS::Provider object representing the patron's SMS provider.
442
443 =cut
444
445 sub sms_provider {
446     my ( $self ) = @_;
447     my $sms_provider_rs = $self->_result->sms_provider;
448     return unless $sms_provider_rs;
449     return Koha::SMS::Provider->_new_from_dbic($sms_provider_rs);
450 }
451
452 =head3 guarantor_relationships
453
454 Returns Koha::Patron::Relationships object for this patron's guarantors
455
456 Returns the set of relationships for the patrons that are guarantors for this patron.
457
458 This is returned instead of a Koha::Patron object because the guarantor
459 may not exist as a patron in Koha. If this is true, the guarantors name
460 exists in the Koha::Patron::Relationship object and will have no guarantor_id.
461
462 =cut
463
464 sub guarantor_relationships {
465     my ($self) = @_;
466
467     return Koha::Patron::Relationships->search( { guarantee_id => $self->id } );
468 }
469
470 =head3 guarantee_relationships
471
472 Returns Koha::Patron::Relationships object for this patron's guarantors
473
474 Returns the set of relationships for the patrons that are guarantees for this patron.
475
476 The method returns Koha::Patron::Relationship objects for the sake
477 of consistency with the guantors method.
478 A guarantee by definition must exist as a patron in Koha.
479
480 =cut
481
482 sub guarantee_relationships {
483     my ($self) = @_;
484
485     return Koha::Patron::Relationships->search(
486         { guarantor_id => $self->id },
487         {
488             prefetch => 'guarantee',
489             order_by => { -asc => [ 'guarantee.surname', 'guarantee.firstname' ] },
490         }
491     );
492 }
493
494 =head3 relationships_debt
495
496 Returns the amount owed by the patron's guarantors *and* the other guarantees of those guarantors
497
498 =cut
499
500 sub relationships_debt {
501     my ($self, $params) = @_;
502
503     my $include_guarantors  = $params->{include_guarantors};
504     my $only_this_guarantor = $params->{only_this_guarantor};
505     my $include_this_patron = $params->{include_this_patron};
506
507     my @guarantors;
508     if ( $only_this_guarantor ) {
509         @guarantors = $self->guarantee_relationships->count ? ( $self ) : ();
510         Koha::Exceptions::BadParameter->throw( { parameter => 'only_this_guarantor' } ) unless @guarantors;
511     } elsif ( $self->guarantor_relationships->count ) {
512         # I am a guarantee, just get all my guarantors
513         @guarantors = $self->guarantor_relationships->guarantors->as_list;
514     } else {
515         # I am a guarantor, I need to get all the guarantors of all my guarantees
516         @guarantors = map { $_->guarantor_relationships->guarantors->as_list } $self->guarantee_relationships->guarantees->as_list;
517     }
518
519     my $non_issues_charges = 0;
520     my $seen = $include_this_patron ? {} : { $self->id => 1 }; # For tracking members already added to the total
521     foreach my $guarantor (@guarantors) {
522         $non_issues_charges += $guarantor->account->non_issues_charges if $include_guarantors && !$seen->{ $guarantor->id };
523
524         # We've added what the guarantor owes, not added in that guarantor's guarantees as well
525         my @guarantees = map { $_->guarantee } $guarantor->guarantee_relationships->as_list;
526         my $guarantees_non_issues_charges = 0;
527         foreach my $guarantee (@guarantees) {
528             next if $seen->{ $guarantee->id };
529             $guarantees_non_issues_charges += $guarantee->account->non_issues_charges;
530             # Mark this guarantee as seen so we don't double count a guarantee linked to multiple guarantors
531             $seen->{ $guarantee->id } = 1;
532         }
533
534         $non_issues_charges += $guarantees_non_issues_charges;
535         $seen->{ $guarantor->id } = 1;
536     }
537
538     return $non_issues_charges;
539 }
540
541 =head3 housebound_profile
542
543 Returns the HouseboundProfile associated with this patron.
544
545 =cut
546
547 sub housebound_profile {
548     my ( $self ) = @_;
549     my $profile = $self->_result->housebound_profile;
550     return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
551         if ( $profile );
552     return;
553 }
554
555 =head3 housebound_role
556
557 Returns the HouseboundRole associated with this patron.
558
559 =cut
560
561 sub housebound_role {
562     my ( $self ) = @_;
563
564     my $role = $self->_result->housebound_role;
565     return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
566     return;
567 }
568
569 =head3 siblings
570
571 Returns the siblings of this patron.
572
573 =cut
574
575 sub siblings {
576     my ($self) = @_;
577
578     my @guarantors = $self->guarantor_relationships()->guarantors()->as_list;
579
580     return unless @guarantors;
581
582     my @siblings =
583       map { $_->guarantee_relationships()->guarantees()->as_list } @guarantors;
584
585     return unless @siblings;
586
587     my %seen;
588     @siblings =
589       grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
590
591     return Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
592 }
593
594 =head3 merge_with
595
596     my $patron = Koha::Patrons->find($id);
597     $patron->merge_with( \@patron_ids );
598
599     This subroutine merges a list of patrons into the patron record. This is accomplished by finding
600     all related patron ids for the patrons to be merged in other tables and changing the ids to be that
601     of the keeper patron.
602
603 =cut
604
605 sub merge_with {
606     my ( $self, $patron_ids ) = @_;
607
608     my $anonymous_patron = C4::Context->preference("AnonymousPatron");
609     return if $anonymous_patron && $self->id eq $anonymous_patron;
610
611     my @patron_ids = @{ $patron_ids };
612
613     # Ensure the keeper isn't in the list of patrons to merge
614     @patron_ids = grep { $_ ne $self->id } @patron_ids;
615
616     my $schema = Koha::Database->new()->schema();
617
618     my $results;
619
620     $self->_result->result_source->schema->txn_do( sub {
621         foreach my $patron_id (@patron_ids) {
622
623             next if $patron_id eq $anonymous_patron;
624
625             my $patron = Koha::Patrons->find( $patron_id );
626
627             next unless $patron;
628
629             # Unbless for safety, the patron will end up being deleted
630             $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
631
632             my $attributes = $patron->extended_attributes;
633             my $new_attributes = [
634                 map { { code => $_->code, attribute => $_->attribute } }
635                     $attributes->as_list
636             ];
637             $attributes->delete; # We need to delete before trying to merge them to prevent exception on unique and repeatable
638             for my $attribute ( @$new_attributes ) {
639                 try {
640                     $self->add_extended_attribute($attribute);
641                 } catch {
642                     # Don't block the merge if there is a non-repeatable attribute that cannot be added to the current patron.
643                     unless ( $_->isa('Koha::Exceptions::Patron::Attribute::NonRepeatable') ) {
644                         $_->rethrow;
645                     }
646                 };
647             }
648
649             while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
650                 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
651                 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
652                 $rs->update({ $field => $self->id });
653                 if ( $r eq 'BorrowerDebarment' ) {
654                     Koha::Patron::Debarments::UpdateBorrowerDebarmentFlags($self->id);
655                 }
656             }
657
658             $patron->move_to_deleted();
659             $patron->delete();
660         }
661     });
662
663     return $results;
664 }
665
666
667
668 =head3 wants_check_for_previous_checkout
669
670     $wants_check = $patron->wants_check_for_previous_checkout;
671
672 Return 1 if Koha needs to perform PrevIssue checking, else 0.
673
674 =cut
675
676 sub wants_check_for_previous_checkout {
677     my ( $self ) = @_;
678     my $syspref = C4::Context->preference("checkPrevCheckout");
679
680     # Simple cases
681     ## Hard syspref trumps all
682     return 1 if ($syspref eq 'hardyes');
683     return 0 if ($syspref eq 'hardno');
684     ## Now, patron pref trumps all
685     return 1 if ($self->checkprevcheckout eq 'yes');
686     return 0 if ($self->checkprevcheckout eq 'no');
687
688     # More complex: patron inherits -> determine category preference
689     my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
690     return 1 if ($checkPrevCheckoutByCat eq 'yes');
691     return 0 if ($checkPrevCheckoutByCat eq 'no');
692
693     # Finally: category preference is inherit, default to 0
694     if ($syspref eq 'softyes') {
695         return 1;
696     } else {
697         return 0;
698     }
699 }
700
701 =head3 do_check_for_previous_checkout
702
703     $do_check = $patron->do_check_for_previous_checkout($item);
704
705 Return 1 if the bib associated with $ITEM has previously been checked out to
706 $PATRON, 0 otherwise.
707
708 =cut
709
710 sub do_check_for_previous_checkout {
711     my ( $self, $item ) = @_;
712
713     my @item_nos;
714     my $biblio = Koha::Biblios->find( $item->{biblionumber} );
715     if ( $biblio->is_serial ) {
716         push @item_nos, $item->{itemnumber};
717     } else {
718         # Get all itemnumbers for given bibliographic record.
719         @item_nos = $biblio->items->get_column( 'itemnumber' );
720     }
721
722     # Create (old)issues search criteria
723     my $criteria = {
724         borrowernumber => $self->borrowernumber,
725         itemnumber => \@item_nos,
726     };
727
728     my $delay = C4::Context->preference('CheckPrevCheckoutDelay') || 0;
729     if ($delay) {
730         my $dtf = Koha::Database->new->schema->storage->datetime_parser;
731         my $newer_than = dt_from_string()->subtract( days => $delay );
732         $criteria->{'returndate'} = { '>'   =>  $dtf->format_datetime($newer_than), };
733     }
734
735     # Check current issues table
736     my $issues = Koha::Checkouts->search($criteria);
737     return 1 if $issues->count; # 0 || N
738
739     # Check old issues table
740     my $old_issues = Koha::Old::Checkouts->search($criteria);
741     return $old_issues->count;  # 0 || N
742 }
743
744 =head3 is_debarred
745
746 my $debarment_expiration = $patron->is_debarred;
747
748 Returns the date a patron debarment will expire, or undef if the patron is not
749 debarred
750
751 =cut
752
753 sub is_debarred {
754     my ($self) = @_;
755
756     return unless $self->debarred;
757     return $self->debarred
758       if $self->debarred =~ '^9999'
759       or dt_from_string( $self->debarred ) > dt_from_string;
760     return;
761 }
762
763 =head3 is_expired
764
765 my $is_expired = $patron->is_expired;
766
767 Returns 1 if the patron is expired or 0;
768
769 =cut
770
771 sub is_expired {
772     my ($self) = @_;
773     return 0 unless $self->dateexpiry;
774     return 0 if $self->dateexpiry =~ '^9999';
775     return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
776     return 0;
777 }
778
779 =head3 password_expired
780
781 my $password_expired = $patron->password_expired;
782
783 Returns 1 if the patron's password is expired or 0;
784
785 =cut
786
787 sub password_expired {
788     my ($self) = @_;
789     return 0 unless $self->password_expiration_date;
790     return 1 if dt_from_string( $self->password_expiration_date ) <= dt_from_string->truncate( to => 'day' );
791     return 0;
792 }
793
794 =head3 is_going_to_expire
795
796 my $is_going_to_expire = $patron->is_going_to_expire;
797
798 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
799
800 =cut
801
802 sub is_going_to_expire {
803     my ($self) = @_;
804
805     my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
806
807     return 0 unless $delay;
808     return 0 unless $self->dateexpiry;
809     return 0 if $self->dateexpiry =~ '^9999';
810     return 1 if dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $delay ) < dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
811     return 0;
812 }
813
814 =head3 set_password
815
816     $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
817
818 Set the patron's password.
819
820 =head4 Exceptions
821
822 The passed string is validated against the current password enforcement policy.
823 Validation can be skipped by passing the I<skip_validation> parameter.
824
825 Exceptions are thrown if the password is not good enough.
826
827 =over 4
828
829 =item Koha::Exceptions::Password::TooShort
830
831 =item Koha::Exceptions::Password::WhitespaceCharacters
832
833 =item Koha::Exceptions::Password::TooWeak
834
835 =item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
836
837 =back
838
839 =cut
840
841 sub set_password {
842     my ( $self, $args ) = @_;
843
844     my $password = $args->{password};
845
846     unless ( $args->{skip_validation} ) {
847         my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $self->category );
848
849         if ( !$is_valid ) {
850             if ( $error eq 'too_short' ) {
851                 my $min_length = $self->category->effective_min_password_length;
852                 $min_length = 3 if not $min_length or $min_length < 3;
853
854                 my $password_length = length($password);
855                 Koha::Exceptions::Password::TooShort->throw(
856                     length => $password_length, min_length => $min_length );
857             }
858             elsif ( $error eq 'has_whitespaces' ) {
859                 Koha::Exceptions::Password::WhitespaceCharacters->throw();
860             }
861             elsif ( $error eq 'too_weak' ) {
862                 Koha::Exceptions::Password::TooWeak->throw();
863             }
864         }
865     }
866
867     if ( C4::Context->config("enable_plugins") ) {
868         # Call any check_password plugins
869         my @plugins = Koha::Plugins->new()->GetPlugins({
870             method => 'check_password',
871         });
872         foreach my $plugin ( @plugins ) {
873             # This plugin hook will also be used by a plugin for the Norwegian national
874             # patron database. This is why we need to pass both the password and the
875             # borrowernumber to the plugin.
876             my $ret = $plugin->check_password(
877                 {
878                     password       => $password,
879                     borrowernumber => $self->borrowernumber
880                 }
881             );
882             # This plugin hook will also be used by a plugin for the Norwegian national
883             # patron database. This is why we need to call the actual plugins and then
884             # check skip_validation afterwards.
885             if ( $ret->{'error'} == 1 && !$args->{skip_validation} ) {
886                 Koha::Exceptions::Password::Plugin->throw();
887             }
888         }
889     }
890
891     my $digest = Koha::AuthUtils::hash_password($password);
892
893     $self->password_expiration_date( $self->category->get_password_expiry_date || undef );
894
895     # We do not want to call $self->store and retrieve password from DB
896     $self->password($digest);
897     $self->login_attempts(0);
898     $self->SUPER::store;
899
900     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
901         if C4::Context->preference("BorrowersLog");
902
903     return $self;
904 }
905
906
907 =head3 renew_account
908
909 my $new_expiry_date = $patron->renew_account
910
911 Extending the subscription to the expiry date.
912
913 =cut
914
915 sub renew_account {
916     my ($self) = @_;
917     my $date;
918     if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
919         $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
920     } else {
921         $date =
922             C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
923             ? dt_from_string( $self->dateexpiry )
924             : dt_from_string;
925     }
926     my $expiry_date = $self->category->get_expiry_date($date);
927
928     $self->dateexpiry($expiry_date);
929     $self->date_renewed( dt_from_string() );
930     $self->store();
931
932     $self->add_enrolment_fee_if_needed(1);
933
934     logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
935     return dt_from_string( $expiry_date )->truncate( to => 'day' );
936 }
937
938 =head3 has_overdues
939
940 my $has_overdues = $patron->has_overdues;
941
942 Returns the number of patron's overdues
943
944 =cut
945
946 sub has_overdues {
947     my ($self) = @_;
948     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
949     return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
950 }
951
952 =head3 track_login
953
954     $patron->track_login;
955     $patron->track_login({ force => 1 });
956
957     Tracks a (successful) login attempt.
958     The preference TrackLastPatronActivity must be enabled. Or you
959     should pass the force parameter.
960
961 =cut
962
963 sub track_login {
964     my ( $self, $params ) = @_;
965     return if
966         !$params->{force} &&
967         !C4::Context->preference('TrackLastPatronActivity');
968     $self->lastseen( dt_from_string() )->store;
969 }
970
971 =head3 move_to_deleted
972
973 my $is_moved = $patron->move_to_deleted;
974
975 Move a patron to the deletedborrowers table.
976 This can be done before deleting a patron, to make sure the data are not completely deleted.
977
978 =cut
979
980 sub move_to_deleted {
981     my ($self) = @_;
982     my $patron_infos = $self->unblessed;
983     delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
984     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
985 }
986
987 =head3 can_request_article
988
989     if ( $patron->can_request_article( $library->id ) ) { ... }
990
991 Returns true if the patron can request articles. As limits apply for the patron
992 on the same day, those completed the same day are considered as current.
993
994 A I<library_id> can be passed as parameter, falling back to userenv if absent.
995
996 =cut
997
998 sub can_request_article {
999     my ($self, $library_id) = @_;
1000
1001     $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1002
1003     my $rule = Koha::CirculationRules->get_effective_rule(
1004         {
1005             branchcode   => $library_id,
1006             categorycode => $self->categorycode,
1007             rule_name    => 'open_article_requests_limit'
1008         }
1009     );
1010
1011     my $limit = ($rule) ? $rule->rule_value : undef;
1012
1013     return 1 unless defined $limit;
1014
1015     my $count = Koha::ArticleRequests->search(
1016         [   { borrowernumber => $self->borrowernumber, status => [ 'REQUESTED', 'PENDING', 'PROCESSING' ] },
1017             { borrowernumber => $self->borrowernumber, status => 'COMPLETED', updated_on => { '>=' => \'CAST(NOW() AS DATE)' } },
1018         ]
1019     )->count;
1020     return $count < $limit ? 1 : 0;
1021 }
1022
1023 =head3 article_request_fee
1024
1025     my $fee = $patron->article_request_fee(
1026         {
1027           [ library_id => $library->id, ]
1028         }
1029     );
1030
1031 Returns the fee to be charged to the patron when it places an article request.
1032
1033 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1034
1035 =cut
1036
1037 sub article_request_fee {
1038     my ($self, $params) = @_;
1039
1040     my $library_id = $params->{library_id};
1041
1042     $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1043
1044     my $rule = Koha::CirculationRules->get_effective_rule(
1045         {
1046             branchcode   => $library_id,
1047             categorycode => $self->categorycode,
1048             rule_name    => 'article_request_fee'
1049         }
1050     );
1051
1052     my $fee = ($rule) ? $rule->rule_value + 0 : 0;
1053
1054     return $fee;
1055 }
1056
1057 =head3 add_article_request_fee_if_needed
1058
1059     my $fee = $patron->add_article_request_fee_if_needed(
1060         {
1061           [ item_id    => $item->id,
1062             library_id => $library->id, ]
1063         }
1064     );
1065
1066 If an article request fee needs to be charged, it adds a debit to the patron's
1067 account.
1068
1069 Returns the fee line.
1070
1071 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1072
1073 =cut
1074
1075 sub add_article_request_fee_if_needed {
1076     my ($self, $params) = @_;
1077
1078     my $library_id = $params->{library_id};
1079     my $item_id    = $params->{item_id};
1080
1081     $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1082
1083     my $amount = $self->article_request_fee(
1084         {
1085             library_id => $library_id,
1086         }
1087     );
1088
1089     my $debit_line;
1090
1091     if ( $amount > 0 ) {
1092         $debit_line = $self->account->add_debit(
1093             {
1094                 amount     => $amount,
1095                 user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1096                 interface  => C4::Context->interface,
1097                 library_id => $library_id,
1098                 type       => 'ARTICLE_REQUEST',
1099                 item_id    => $item_id,
1100             }
1101         );
1102     }
1103
1104     return $debit_line;
1105 }
1106
1107 =head3 article_requests
1108
1109     my $article_requests = $patron->article_requests;
1110
1111 Returns the patron article requests.
1112
1113 =cut
1114
1115 sub article_requests {
1116     my ($self) = @_;
1117
1118     return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
1119 }
1120
1121 =head3 add_enrolment_fee_if_needed
1122
1123 my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
1124
1125 Add enrolment fee for a patron if needed.
1126
1127 $renewal - boolean denoting whether this is an account renewal or not
1128
1129 =cut
1130
1131 sub add_enrolment_fee_if_needed {
1132     my ($self, $renewal) = @_;
1133     my $enrolment_fee = $self->category->enrolmentfee;
1134     if ( $enrolment_fee && $enrolment_fee > 0 ) {
1135         my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
1136         $self->account->add_debit(
1137             {
1138                 amount     => $enrolment_fee,
1139                 user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1140                 interface  => C4::Context->interface,
1141                 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
1142                 type       => $type
1143             }
1144         );
1145     }
1146     return $enrolment_fee || 0;
1147 }
1148
1149 =head3 checkouts
1150
1151 my $checkouts = $patron->checkouts
1152
1153 =cut
1154
1155 sub checkouts {
1156     my ($self) = @_;
1157     my $checkouts = $self->_result->issues;
1158     return Koha::Checkouts->_new_from_dbic( $checkouts );
1159 }
1160
1161 =head3 pending_checkouts
1162
1163 my $pending_checkouts = $patron->pending_checkouts
1164
1165 This method will return the same as $self->checkouts, but with a prefetch on
1166 items, biblio and biblioitems.
1167
1168 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
1169
1170 It should not be used directly, prefer to access fields you need instead of
1171 retrieving all these fields in one go.
1172
1173 =cut
1174
1175 sub pending_checkouts {
1176     my( $self ) = @_;
1177     my $checkouts = $self->_result->issues->search(
1178         {},
1179         {
1180             order_by => [
1181                 { -desc => 'me.timestamp' },
1182                 { -desc => 'issuedate' },
1183                 { -desc => 'issue_id' }, # Sort by issue_id should be enough
1184             ],
1185             prefetch => { item => { biblio => 'biblioitems' } },
1186         }
1187     );
1188     return Koha::Checkouts->_new_from_dbic( $checkouts );
1189 }
1190
1191 =head3 old_checkouts
1192
1193 my $old_checkouts = $patron->old_checkouts
1194
1195 =cut
1196
1197 sub old_checkouts {
1198     my ($self) = @_;
1199     my $old_checkouts = $self->_result->old_issues;
1200     return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
1201 }
1202
1203 =head3 overdues
1204
1205 my $overdue_items = $patron->overdues
1206
1207 Return the overdue items
1208
1209 =cut
1210
1211 sub overdues {
1212     my ($self) = @_;
1213     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1214     return $self->checkouts->search(
1215         {
1216             'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
1217         },
1218         {
1219             prefetch => { item => { biblio => 'biblioitems' } },
1220         }
1221     );
1222 }
1223
1224 =head3 get_routing_lists
1225
1226 my $routinglists = $patron->get_routing_lists
1227
1228 Returns the routing lists a patron is subscribed to.
1229
1230 =cut
1231
1232 sub get_routing_lists {
1233     my ($self) = @_;
1234     my $routing_list_rs = $self->_result->subscriptionroutinglists;
1235     return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1236 }
1237
1238 =head3 get_age
1239
1240     my $age = $patron->get_age
1241
1242 Return the age of the patron
1243
1244 =cut
1245
1246 sub get_age {
1247     my ($self)    = @_;
1248
1249     return unless $self->dateofbirth;
1250
1251     my $date_of_birth = dt_from_string( $self->dateofbirth );
1252     my $today         = dt_from_string->truncate( to => 'day' );
1253
1254     return $today->subtract_datetime( $date_of_birth )->years;
1255 }
1256
1257 =head3 is_valid_age
1258
1259 my $is_valid = $patron->is_valid_age
1260
1261 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1262
1263 =cut
1264
1265 sub is_valid_age {
1266     my ($self) = @_;
1267     my $age = $self->get_age;
1268
1269     my $patroncategory = $self->category;
1270     my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1271
1272     return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
1273 }
1274
1275 =head3 account
1276
1277 my $account = $patron->account
1278
1279 =cut
1280
1281 sub account {
1282     my ($self) = @_;
1283     return Koha::Account->new( { patron_id => $self->borrowernumber } );
1284 }
1285
1286 =head3 holds
1287
1288 my $holds = $patron->holds
1289
1290 Return all the holds placed by this patron
1291
1292 =cut
1293
1294 sub holds {
1295     my ($self) = @_;
1296     my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1297     return Koha::Holds->_new_from_dbic($holds_rs);
1298 }
1299
1300 =head3 old_holds
1301
1302 my $old_holds = $patron->old_holds
1303
1304 Return all the historical holds for this patron
1305
1306 =cut
1307
1308 sub old_holds {
1309     my ($self) = @_;
1310     my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1311     return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1312 }
1313
1314 =head3 return_claims
1315
1316 my $return_claims = $patron->return_claims
1317
1318 =cut
1319
1320 sub return_claims {
1321     my ($self) = @_;
1322     my $return_claims = $self->_result->return_claims_borrowernumbers;
1323     return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1324 }
1325
1326 =head3 notice_email_address
1327
1328   my $email = $patron->notice_email_address;
1329
1330 Return the email address of patron used for notices.
1331 Returns the empty string if no email address.
1332
1333 =cut
1334
1335 sub notice_email_address{
1336     my ( $self ) = @_;
1337
1338     my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1339     # if syspref is set to 'first valid' (value == OFF), look up email address
1340     if ( $which_address eq 'OFF' ) {
1341         return $self->first_valid_email_address;
1342     }
1343
1344     return $self->$which_address || '';
1345 }
1346
1347 =head3 first_valid_email_address
1348
1349 my $first_valid_email_address = $patron->first_valid_email_address
1350
1351 Return the first valid email address for a patron.
1352 For now, the order  is defined as email, emailpro, B_email.
1353 Returns the empty string if the borrower has no email addresses.
1354
1355 =cut
1356
1357 sub first_valid_email_address {
1358     my ($self) = @_;
1359
1360     return $self->email() || $self->emailpro() || $self->B_email() || q{};
1361 }
1362
1363 =head3 get_club_enrollments
1364
1365 =cut
1366
1367 sub get_club_enrollments {
1368     my ( $self ) = @_;
1369
1370     return Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1371 }
1372
1373 =head3 get_enrollable_clubs
1374
1375 =cut
1376
1377 sub get_enrollable_clubs {
1378     my ( $self, $is_enrollable_from_opac ) = @_;
1379
1380     my $params;
1381     $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1382       if $is_enrollable_from_opac;
1383     $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1384
1385     $params->{borrower} = $self;
1386
1387     return Koha::Clubs->get_enrollable($params);
1388 }
1389
1390 =head3 account_locked
1391
1392 my $is_locked = $patron->account_locked
1393
1394 Return true if the patron has reached the maximum number of login attempts
1395 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1396 as an administrative lockout (independent of FailedLoginAttempts; see also
1397 Koha::Patron->lock).
1398 Otherwise return false.
1399 If the pref is not set (empty string, null or 0), the feature is considered as
1400 disabled.
1401
1402 =cut
1403
1404 sub account_locked {
1405     my ($self) = @_;
1406     my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1407     return 1 if $FailedLoginAttempts
1408           and $self->login_attempts
1409           and $self->login_attempts >= $FailedLoginAttempts;
1410     return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1411     return 0;
1412 }
1413
1414 =head3 can_see_patron_infos
1415
1416 my $can_see = $patron->can_see_patron_infos( $patron );
1417
1418 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1419
1420 =cut
1421
1422 sub can_see_patron_infos {
1423     my ( $self, $patron ) = @_;
1424     return unless $patron;
1425     return $self->can_see_patrons_from( $patron->branchcode );
1426 }
1427
1428 =head3 can_see_patrons_from
1429
1430 my $can_see = $patron->can_see_patrons_from( $branchcode );
1431
1432 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1433
1434 =cut
1435
1436 sub can_see_patrons_from {
1437     my ( $self, $branchcode ) = @_;
1438     my $can = 0;
1439     if ( $self->branchcode eq $branchcode ) {
1440         $can = 1;
1441     } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1442         $can = 1;
1443     } elsif ( my $library_groups = $self->library->library_groups ) {
1444         while ( my $library_group = $library_groups->next ) {
1445             if ( $library_group->parent->has_child( $branchcode ) ) {
1446                 $can = 1;
1447                 last;
1448             }
1449         }
1450     }
1451     return $can;
1452 }
1453
1454 =head3 can_log_into
1455
1456 my $can_log_into = $patron->can_log_into( $library );
1457
1458 Given a I<Koha::Library> object, it returns a boolean representing
1459 the fact the patron can log into a the library.
1460
1461 =cut
1462
1463 sub can_log_into {
1464     my ( $self, $library ) = @_;
1465
1466     my $can = 0;
1467
1468     if ( C4::Context->preference('IndependentBranches') ) {
1469         $can = 1
1470           if $self->is_superlibrarian
1471           or $self->branchcode eq $library->id;
1472     }
1473     else {
1474         # no restrictions
1475         $can = 1;
1476     }
1477
1478    return $can;
1479 }
1480
1481 =head3 libraries_where_can_see_patrons
1482
1483 my $libraries = $patron-libraries_where_can_see_patrons;
1484
1485 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1486 The branchcodes are arbitrarily returned sorted.
1487 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1488
1489 An empty array means no restriction, the patron can see patron's infos from any libraries.
1490
1491 =cut
1492
1493 sub libraries_where_can_see_patrons {
1494     my ( $self ) = @_;
1495     my $userenv = C4::Context->userenv;
1496
1497     return () unless $userenv; # For tests, but userenv should be defined in tests...
1498
1499     my @restricted_branchcodes;
1500     if (C4::Context::only_my_library) {
1501         push @restricted_branchcodes, $self->branchcode;
1502     }
1503     else {
1504         unless (
1505             $self->has_permission(
1506                 { borrowers => 'view_borrower_infos_from_any_libraries' }
1507             )
1508           )
1509         {
1510             my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1511             if ( $library_groups->count )
1512             {
1513                 while ( my $library_group = $library_groups->next ) {
1514                     my $parent = $library_group->parent;
1515                     if ( $parent->has_child( $self->branchcode ) ) {
1516                         push @restricted_branchcodes, $parent->children->get_column('branchcode');
1517                     }
1518                 }
1519             }
1520
1521             @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1522         }
1523     }
1524
1525     @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1526     @restricted_branchcodes = uniq(@restricted_branchcodes);
1527     @restricted_branchcodes = sort(@restricted_branchcodes);
1528     return @restricted_branchcodes;
1529 }
1530
1531 =head3 has_permission
1532
1533 my $permission = $patron->has_permission($required);
1534
1535 See C4::Auth::haspermission for details of syntax for $required
1536
1537 =cut
1538
1539 sub has_permission {
1540     my ( $self, $flagsrequired ) = @_;
1541     return unless $self->userid;
1542     # TODO code from haspermission needs to be moved here!
1543     return C4::Auth::haspermission( $self->userid, $flagsrequired );
1544 }
1545
1546 =head3 is_superlibrarian
1547
1548   my $is_superlibrarian = $patron->is_superlibrarian;
1549
1550 Return true if the patron is a superlibrarian.
1551
1552 =cut
1553
1554 sub is_superlibrarian {
1555     my ($self) = @_;
1556     return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1557 }
1558
1559 =head3 is_adult
1560
1561 my $is_adult = $patron->is_adult
1562
1563 Return true if the patron has a category with a type Adult (A) or Organization (I)
1564
1565 =cut
1566
1567 sub is_adult {
1568     my ( $self ) = @_;
1569     return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1570 }
1571
1572 =head3 is_child
1573
1574 my $is_child = $patron->is_child
1575
1576 Return true if the patron has a category with a type Child (C)
1577
1578 =cut
1579
1580 sub is_child {
1581     my( $self ) = @_;
1582     return $self->category->category_type eq 'C' ? 1 : 0;
1583 }
1584
1585 =head3 has_valid_userid
1586
1587 my $patron = Koha::Patrons->find(42);
1588 $patron->userid( $new_userid );
1589 my $has_a_valid_userid = $patron->has_valid_userid
1590
1591 my $patron = Koha::Patron->new( $params );
1592 my $has_a_valid_userid = $patron->has_valid_userid
1593
1594 Return true if the current userid of this patron is valid/unique, otherwise false.
1595
1596 Note that this should be done in $self->store instead and raise an exception if needed.
1597
1598 =cut
1599
1600 sub has_valid_userid {
1601     my ($self) = @_;
1602
1603     return 0 unless $self->userid;
1604
1605     return 0 if ( $self->userid eq C4::Context->config('user') );    # DB user
1606
1607     my $already_exists = Koha::Patrons->search(
1608         {
1609             userid => $self->userid,
1610             (
1611                 $self->in_storage
1612                 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1613                 : ()
1614             ),
1615         }
1616     )->count;
1617     return $already_exists ? 0 : 1;
1618 }
1619
1620 =head3 generate_userid
1621
1622 my $patron = Koha::Patron->new( $params );
1623 $patron->generate_userid
1624
1625 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1626
1627 Set a generated userid ($firstname.$surname if there is a $firstname, or $surname if there is no value in $firstname) plus offset (0 if the $userid is unique, or a higher numeric value if not unique).
1628
1629 =cut
1630
1631 sub generate_userid {
1632     my ($self) = @_;
1633     my $offset = 0;
1634     my $firstname = $self->firstname // q{};
1635     my $surname = $self->surname // q{};
1636     #The script will "do" the following code and increment the $offset until the generated userid is unique
1637     do {
1638       $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1639       $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1640       my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1641       $userid = NFKD( $userid );
1642       $userid =~ s/\p{NonspacingMark}//g;
1643       $userid .= $offset unless $offset == 0;
1644       $self->userid( $userid );
1645       $offset++;
1646      } while (! $self->has_valid_userid );
1647
1648      return $self;
1649 }
1650
1651 =head3 add_extended_attribute
1652
1653 =cut
1654
1655 sub add_extended_attribute {
1656     my ($self, $attribute) = @_;
1657
1658     return Koha::Patron::Attribute->new(
1659         {
1660             %$attribute,
1661             ( borrowernumber => $self->borrowernumber ),
1662         }
1663     )->store;
1664
1665 }
1666
1667 =head3 extended_attributes
1668
1669 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1670
1671 Or setter FIXME
1672
1673 =cut
1674
1675 sub extended_attributes {
1676     my ( $self, $attributes ) = @_;
1677     if ($attributes) {    # setter
1678         my $schema = $self->_result->result_source->schema;
1679         $schema->txn_do(
1680             sub {
1681                 # Remove the existing one
1682                 $self->extended_attributes->filter_by_branch_limitations->delete;
1683
1684                 # Insert the new ones
1685                 my $new_types = {};
1686                 for my $attribute (@$attributes) {
1687                     $self->add_extended_attribute($attribute);
1688                     $new_types->{$attribute->{code}} = 1;
1689                 }
1690
1691                 # Check globally mandatory types
1692                 my @required_attribute_types =
1693                     Koha::Patron::Attribute::Types->search(
1694                         {
1695                             mandatory => 1,
1696                             category_code => [ undef, $self->categorycode ],
1697                             'borrower_attribute_types_branches.b_branchcode' =>
1698                               undef,
1699                         },
1700                         { join => 'borrower_attribute_types_branches' }
1701                     )->get_column('code');
1702                 for my $type ( @required_attribute_types ) {
1703                     Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
1704                         type => $type,
1705                     ) if !$new_types->{$type};
1706                 }
1707             }
1708         );
1709     }
1710
1711     my $rs = $self->_result->borrower_attributes;
1712     # We call search to use the filters in Koha::Patron::Attributes->search
1713     return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1714 }
1715
1716 =head3 messages
1717
1718     my $messages = $patron->messages;
1719
1720 Return the message attached to the patron.
1721
1722 =cut
1723
1724 sub messages {
1725     my ( $self ) = @_;
1726     my $messages_rs = $self->_result->messages_borrowernumbers->search;
1727     return Koha::Patron::Messages->_new_from_dbic($messages_rs);
1728 }
1729
1730 =head3 lock
1731
1732     Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1733
1734     Lock and optionally expire a patron account.
1735     Remove holds and article requests if remove flag set.
1736     In order to distinguish from locking by entering a wrong password, let's
1737     call this an administrative lockout.
1738
1739 =cut
1740
1741 sub lock {
1742     my ( $self, $params ) = @_;
1743     $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1744     if( $params->{expire} ) {
1745         $self->dateexpiry( dt_from_string->subtract(days => 1) );
1746     }
1747     $self->store;
1748     if( $params->{remove} ) {
1749         $self->holds->delete;
1750         $self->article_requests->delete;
1751     }
1752     return $self;
1753 }
1754
1755 =head3 anonymize
1756
1757     Koha::Patrons->find($id)->anonymize;
1758
1759     Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1760     are randomized, other personal data is cleared too.
1761     Patrons with issues are skipped.
1762
1763 =cut
1764
1765 sub anonymize {
1766     my ( $self ) = @_;
1767     if( $self->_result->issues->count ) {
1768         warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1769         return;
1770     }
1771     # Mandatory fields come from the corresponding pref, but email fields
1772     # are removed since scrambled email addresses only generate errors
1773     my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1774         split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1775     $mandatory->{userid} = 1; # needed since sub store does not clear field
1776     my @columns = $self->_result->result_source->columns;
1777     @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized|auth_method/ } @columns;
1778     push @columns, 'dateofbirth'; # add this date back in
1779     foreach my $col (@columns) {
1780         $self->_anonymize_column($col, $mandatory->{lc $col} );
1781     }
1782     $self->anonymized(1)->store;
1783 }
1784
1785 sub _anonymize_column {
1786     my ( $self, $col, $mandatory ) = @_;
1787     my $col_info = $self->_result->result_source->column_info($col);
1788     my $type = $col_info->{data_type};
1789     my $nullable = $col_info->{is_nullable};
1790     my $val;
1791     if( $type =~ /char|text/ ) {
1792         $val = $mandatory
1793             ? Koha::Token->new->generate({ pattern => '\w{10}' })
1794             : $nullable
1795             ? undef
1796             : q{};
1797     } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1798         $val = $nullable ? undef : 0;
1799     } elsif( $type =~ /date|time/ ) {
1800         $val = $nullable ? undef : dt_from_string;
1801     }
1802     $self->$col($val);
1803 }
1804
1805 =head3 add_guarantor
1806
1807     my $relationship = $patron->add_guarantor(
1808         {
1809             borrowernumber => $borrowernumber,
1810             relationships  => $relationship,
1811         }
1812     );
1813
1814     Adds a new guarantor to a patron.
1815
1816 =cut
1817
1818 sub add_guarantor {
1819     my ( $self, $params ) = @_;
1820
1821     my $guarantor_id = $params->{guarantor_id};
1822     my $relationship = $params->{relationship};
1823
1824     return Koha::Patron::Relationship->new(
1825         {
1826             guarantee_id => $self->id,
1827             guarantor_id => $guarantor_id,
1828             relationship => $relationship
1829         }
1830     )->store();
1831 }
1832
1833 =head3 get_extended_attribute
1834
1835 my $attribute_value = $patron->get_extended_attribute( $code );
1836
1837 Return the attribute for the code passed in parameter.
1838
1839 It not exist it returns undef
1840
1841 Note that this will not work for repeatable attribute types.
1842
1843 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
1844 (which should be a real patron's attribute (not extended)
1845
1846 =cut
1847
1848 sub get_extended_attribute {
1849     my ( $self, $code, $value ) = @_;
1850     my $rs = $self->_result->borrower_attributes;
1851     return unless $rs;
1852     my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
1853     return unless $attribute->count;
1854     return $attribute->next;
1855 }
1856
1857 =head3 to_api
1858
1859     my $json = $patron->to_api;
1860
1861 Overloaded method that returns a JSON representation of the Koha::Patron object,
1862 suitable for API output.
1863
1864 =cut
1865
1866 sub to_api {
1867     my ( $self, $params ) = @_;
1868
1869     my $json_patron = $self->SUPER::to_api( $params );
1870
1871     $json_patron->{restricted} = ( $self->is_debarred )
1872                                     ? Mojo::JSON->true
1873                                     : Mojo::JSON->false;
1874
1875     return $json_patron;
1876 }
1877
1878 =head3 to_api_mapping
1879
1880 This method returns the mapping for representing a Koha::Patron object
1881 on the API.
1882
1883 =cut
1884
1885 sub to_api_mapping {
1886     return {
1887         borrowernotes       => 'staff_notes',
1888         borrowernumber      => 'patron_id',
1889         branchcode          => 'library_id',
1890         categorycode        => 'category_id',
1891         checkprevcheckout   => 'check_previous_checkout',
1892         contactfirstname    => undef,                     # Unused
1893         contactname         => undef,                     # Unused
1894         contactnote         => 'altaddress_notes',
1895         contacttitle        => undef,                     # Unused
1896         dateenrolled        => 'date_enrolled',
1897         dateexpiry          => 'expiry_date',
1898         dateofbirth         => 'date_of_birth',
1899         debarred            => undef,                     # replaced by 'restricted'
1900         debarredcomment     => undef,    # calculated, API consumers will use /restrictions instead
1901         emailpro            => 'secondary_email',
1902         flags               => undef,    # permissions manipulation handled in /permissions
1903         gonenoaddress       => 'incorrect_address',
1904         lastseen            => 'last_seen',
1905         lost                => 'patron_card_lost',
1906         opacnote            => 'opac_notes',
1907         othernames          => 'other_name',
1908         password            => undef,            # password manipulation handled in /password
1909         phonepro            => 'secondary_phone',
1910         relationship        => 'relationship_type',
1911         sex                 => 'gender',
1912         smsalertnumber      => 'sms_number',
1913         sort1               => 'statistics_1',
1914         sort2               => 'statistics_2',
1915         autorenew_checkouts => 'autorenew_checkouts',
1916         streetnumber        => 'street_number',
1917         streettype          => 'street_type',
1918         zipcode             => 'postal_code',
1919         B_address           => 'altaddress_address',
1920         B_address2          => 'altaddress_address2',
1921         B_city              => 'altaddress_city',
1922         B_country           => 'altaddress_country',
1923         B_email             => 'altaddress_email',
1924         B_phone             => 'altaddress_phone',
1925         B_state             => 'altaddress_state',
1926         B_streetnumber      => 'altaddress_street_number',
1927         B_streettype        => 'altaddress_street_type',
1928         B_zipcode           => 'altaddress_postal_code',
1929         altcontactaddress1  => 'altcontact_address',
1930         altcontactaddress2  => 'altcontact_address2',
1931         altcontactaddress3  => 'altcontact_city',
1932         altcontactcountry   => 'altcontact_country',
1933         altcontactfirstname => 'altcontact_firstname',
1934         altcontactphone     => 'altcontact_phone',
1935         altcontactsurname   => 'altcontact_surname',
1936         altcontactstate     => 'altcontact_state',
1937         altcontactzipcode   => 'altcontact_postal_code',
1938         password_expiration_date => undef,
1939         primary_contact_method => undef,
1940         secret              => undef,
1941         auth_method         => undef,
1942     };
1943 }
1944
1945 =head3 queue_notice
1946
1947     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
1948     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
1949     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
1950
1951     Queue messages to a patron. Can pass a message that is part of the message_attributes
1952     table or supply the transport to use.
1953
1954     If passed a message name we retrieve the patrons preferences for transports
1955     Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
1956     we have no address/number for sending
1957
1958     $letter_params is a hashref of the values to be passed to GetPreparedLetter
1959
1960     test_mode will only report which notices would be sent, but nothing will be queued
1961
1962 =cut
1963
1964 sub queue_notice {
1965     my ( $self, $params ) = @_;
1966     my $letter_params = $params->{letter_params};
1967     my $test_mode = $params->{test_mode};
1968
1969     return unless $letter_params;
1970     return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
1971
1972     my $library = Koha::Libraries->find( $letter_params->{branchcode} );
1973     my $from_email_address = $library->from_email_address;
1974
1975     my @message_transports;
1976     my $letter_code;
1977     $letter_code = $letter_params->{letter_code};
1978     if( $params->{message_name} ){
1979         my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
1980                 borrowernumber => $letter_params->{borrowernumber},
1981                 message_name => $params->{message_name}
1982         } );
1983         @message_transports = ( keys %{ $messaging_prefs->{transports} } );
1984         $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
1985     } else {
1986         @message_transports = @{$params->{message_transports}};
1987     }
1988     return unless defined $letter_code;
1989     $letter_params->{letter_code} = $letter_code;
1990     my $print_sent = 0;
1991     my %return;
1992     foreach my $mtt (@message_transports){
1993         next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
1994         # Notice is handled by TalkingTech_itiva_outbound.pl
1995         if (   ( $mtt eq 'email' and not $self->notice_email_address )
1996             or ( $mtt eq 'sms'   and not $self->smsalertnumber )
1997             or ( $mtt eq 'phone' and not $self->phone ) )
1998         {
1999             push @{ $return{fallback} }, $mtt;
2000             $mtt = 'print';
2001         }
2002         next if $mtt eq 'print' && $print_sent;
2003         $letter_params->{message_transport_type} = $mtt;
2004         my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
2005         C4::Letters::EnqueueLetter({
2006             letter => $letter,
2007             borrowernumber => $self->borrowernumber,
2008             from_address   => $from_email_address,
2009             message_transport_type => $mtt
2010         }) unless $test_mode;
2011         push @{$return{sent}}, $mtt;
2012         $print_sent = 1 if $mtt eq 'print';
2013     }
2014     return \%return;
2015 }
2016
2017 =head3 safe_to_delete
2018
2019     my $result = $patron->safe_to_delete;
2020     if ( $result eq 'has_guarantees' ) { ... }
2021     elsif ( $result ) { ... }
2022     else { # cannot delete }
2023
2024 This method tells if the Koha:Patron object can be deleted. Possible return values
2025
2026 =over 4
2027
2028 =item 'ok'
2029
2030 =item 'has_checkouts'
2031
2032 =item 'has_debt'
2033
2034 =item 'has_guarantees'
2035
2036 =item 'is_anonymous_patron'
2037
2038 =back
2039
2040 =cut
2041
2042 sub safe_to_delete {
2043     my ($self) = @_;
2044
2045     my $anonymous_patron = C4::Context->preference('AnonymousPatron');
2046
2047     my $error;
2048
2049     if ( $anonymous_patron && $self->id eq $anonymous_patron ) {
2050         $error = 'is_anonymous_patron';
2051     }
2052     elsif ( $self->checkouts->count ) {
2053         $error = 'has_checkouts';
2054     }
2055     elsif ( $self->account->outstanding_debits->total_outstanding > 0 ) {
2056         $error = 'has_debt';
2057     }
2058     elsif ( $self->guarantee_relationships->count ) {
2059         $error = 'has_guarantees';
2060     }
2061
2062     if ( $error ) {
2063         return Koha::Result::Boolean->new(0)->add_message({ message => $error });
2064     }
2065
2066     return Koha::Result::Boolean->new(1);
2067 }
2068
2069 =head3 recalls
2070
2071     my $recalls = $patron->recalls;
2072
2073 Return the patron's recalls.
2074
2075 =cut
2076
2077 sub recalls {
2078     my ( $self ) = @_;
2079
2080     return Koha::Recalls->search({ patron_id => $self->borrowernumber });
2081 }
2082
2083 =head3 account_balance
2084
2085     my $balance = $patron->account_balance
2086
2087 Return the patron's account balance
2088
2089 =cut
2090
2091 sub account_balance {
2092     my ($self) = @_;
2093     return $self->account->balance;
2094 }
2095
2096
2097 =head3 has_messaging_preference
2098
2099 my $bool = $patron->has_messaging_preference({
2100     message_name => $message_name, # A value from message_attributes.message_name
2101     message_transport_type => $message_transport_type, # email, sms, phone, itiva, etc...
2102     wants_digest => $wants_digest, # 1 if you are looking for the digest version, don't pass if you just want either
2103 });
2104
2105 =cut
2106
2107 sub has_messaging_preference {
2108     my ( $self, $params ) = @_;
2109
2110     my $message_name           = $params->{message_name};
2111     my $message_transport_type = $params->{message_transport_type};
2112     my $wants_digest           = $params->{wants_digest};
2113
2114     return $self->_result->search_related_rs(
2115         'borrower_message_preferences',
2116         $params,
2117         {
2118             prefetch =>
2119               [ 'borrower_message_transport_preferences', 'message_attribute' ]
2120         }
2121     )->count;
2122 }
2123
2124 =head3 can_patron_change_staff_only_lists
2125
2126 $patron->can_patron_change_staff_only_lists;
2127
2128 Return 1 if a patron has 'Superlibrarian' or 'Catalogue' permission.
2129 Otherwise, return 0.
2130
2131 =cut
2132
2133 sub can_patron_change_staff_only_lists {
2134     my ( $self, $params ) = @_;
2135     return 1 if C4::Auth::haspermission( $self->userid, { 'catalogue' => 1 });
2136     return 0;
2137 }
2138
2139 =head3 encode_secret
2140
2141   $patron->encode_secret($secret32);
2142
2143 Secret (TwoFactorAuth expects it in base32 format) is encrypted.
2144 You still need to call ->store.
2145
2146 =cut
2147
2148 sub encode_secret {
2149     my ( $self, $secret ) = @_;
2150     if( $secret ) {
2151         return $self->secret( Koha::Encryption->new->encrypt_hex($secret) );
2152     }
2153     return $self->secret($secret);
2154 }
2155
2156 =head3 decoded_secret
2157
2158   my $secret32 = $patron->decoded_secret;
2159
2160 Decode the patron secret. We expect to get back a base32 string, but this
2161 is not checked here. Caller of encode_secret is responsible for that.
2162
2163 =cut
2164
2165 sub decoded_secret {
2166     my ( $self ) = @_;
2167     if( $self->secret ) {
2168         return Koha::Encryption->new->decrypt_hex( $self->secret );
2169     }
2170     return $self->secret;
2171 }
2172
2173 =head3 virtualshelves
2174
2175     my $shelves = $patron->virtualshelves;
2176
2177 =cut
2178
2179 sub virtualshelves {
2180     my $self = shift;
2181     return Koha::Virtualshelves->_new_from_dbic( scalar $self->_result->virtualshelves );
2182 }
2183
2184 =head2 Internal methods
2185
2186 =head3 _type
2187
2188 =cut
2189
2190 sub _type {
2191     return 'Borrower';
2192 }
2193
2194 =head1 AUTHORS
2195
2196 Kyle M Hall <kyle@bywatersolutions.com>
2197 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
2198 Martin Renvoize <martin.renvoize@ptfs-europe.com>
2199
2200 =cut
2201
2202 1;