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