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