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