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