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