Bug 35269: Rename `update_item_location` to `location_update_trigger`
[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
1646 =head3 get_lists_with_patron
1647
1648     my @lists = $patron->get_lists_with_patron;
1649
1650 FIXME: This method returns a DBIC resultset instead of a Koha::Objects-based
1651 iterator.
1652
1653 =cut
1654
1655 sub get_lists_with_patron {
1656     my ( $self ) = @_;
1657     my $borrowernumber = $self->borrowernumber;
1658
1659     return Koha::Database->new()->schema()->resultset('PatronList')->search(
1660         {
1661             'patron_list_patrons.borrowernumber' => $borrowernumber,
1662         },
1663         {
1664             join => 'patron_list_patrons',
1665             collapse => 1,
1666             order_by => 'name',
1667         }
1668     );
1669 }
1670
1671 =head3 account_locked
1672
1673 my $is_locked = $patron->account_locked
1674
1675 Return true if the patron has reached the maximum number of login attempts
1676 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1677 as an administrative lockout (independent of FailedLoginAttempts; see also
1678 Koha::Patron->lock).
1679 Otherwise return false.
1680 If the pref is not set (empty string, null or 0), the feature is considered as
1681 disabled.
1682
1683 =cut
1684
1685 sub account_locked {
1686     my ($self) = @_;
1687     my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1688     return 1 if $FailedLoginAttempts
1689           and $self->login_attempts
1690           and $self->login_attempts >= $FailedLoginAttempts;
1691     return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1692     return 0;
1693 }
1694
1695 =head3 can_see_patron_infos
1696
1697 my $can_see = $patron->can_see_patron_infos( $patron );
1698
1699 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1700
1701 =cut
1702
1703 sub can_see_patron_infos {
1704     my ( $self, $patron ) = @_;
1705     return unless $patron;
1706     return $self->can_see_patrons_from( $patron->branchcode );
1707 }
1708
1709 =head3 can_see_patrons_from
1710
1711 my $can_see = $patron->can_see_patrons_from( $branchcode );
1712
1713 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1714
1715 =cut
1716
1717 sub can_see_patrons_from {
1718     my ( $self, $branchcode ) = @_;
1719
1720     return $self->can_see_things_from(
1721         {
1722             branchcode => $branchcode,
1723             permission => 'borrowers',
1724             subpermission => 'view_borrower_infos_from_any_libraries',
1725         }
1726     );
1727 }
1728
1729 =head3 can_edit_items_from
1730
1731     my $can_edit = $patron->can_edit_items_from( $branchcode );
1732
1733 Return true if the I<Koha::Patron> can edit items from the given branchcode
1734
1735 =cut
1736
1737 sub can_edit_items_from {
1738     my ( $self, $branchcode ) = @_;
1739
1740     return 1 if C4::Context->IsSuperLibrarian();
1741
1742     my $userenv = C4::Context->userenv();
1743     if ( $userenv && C4::Context->preference('IndependentBranches') ) {
1744         return $userenv->{branch} eq $branchcode;
1745     }
1746
1747     return $self->can_see_things_from(
1748         {
1749             branchcode    => $branchcode,
1750             permission    => 'editcatalogue',
1751             subpermission => 'edit_any_item',
1752         }
1753     );
1754 }
1755
1756 =head3 libraries_where_can_edit_items
1757
1758     my $libraries = $patron->libraries_where_can_edit_items;
1759
1760 Return the list of branchcodes(!) of libraries the patron is allowed to items for.
1761 The branchcodes are arbitrarily returned sorted.
1762 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1763
1764 An empty array means no restriction, the user can edit any item.
1765
1766 =cut
1767
1768 sub libraries_where_can_edit_items {
1769     my ($self) = @_;
1770
1771     return $self->libraries_where_can_see_things(
1772         {
1773             permission    => 'editcatalogue',
1774             subpermission => 'edit_any_item',
1775             group_feature => 'ft_limit_item_editing',
1776         }
1777     );
1778 }
1779
1780 =head3 libraries_where_can_see_patrons
1781
1782 my $libraries = $patron->libraries_where_can_see_patrons;
1783
1784 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1785 The branchcodes are arbitrarily returned sorted.
1786 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1787
1788 An empty array means no restriction, the patron can see patron's infos from any libraries.
1789
1790 =cut
1791
1792 sub libraries_where_can_see_patrons {
1793     my ($self) = @_;
1794
1795     return $self->libraries_where_can_see_things(
1796         {
1797             permission    => 'borrowers',
1798             subpermission => 'view_borrower_infos_from_any_libraries',
1799             group_feature => 'ft_hide_patron_info',
1800         }
1801     );
1802 }
1803
1804 =head3 can_see_things_from
1805
1806 my $can_see = $patron->can_see_things_from( $branchcode );
1807
1808 Return true if the I<Koha::Patron> can perform some action on the given thing
1809
1810 =cut
1811
1812 sub can_see_things_from {
1813     my ( $self, $params ) = @_;
1814
1815     my $branchcode    = $params->{branchcode};
1816     my $permission    = $params->{permission};
1817     my $subpermission = $params->{subpermission};
1818
1819     return 1 if C4::Context->IsSuperLibrarian();
1820
1821     my $can = 0;
1822     if ( $self->branchcode eq $branchcode ) {
1823         $can = 1;
1824     } elsif ( $self->has_permission( { $permission => $subpermission } ) ) {
1825         $can = 1;
1826     } elsif ( my @branches = $self->libraries_where_can_see_patrons ) {
1827         $can = ( any { $_ eq $branchcode } @branches ) ? 1 : 0;
1828     }
1829     return $can;
1830 }
1831
1832 =head3 can_log_into
1833
1834 my $can_log_into = $patron->can_log_into( $library );
1835
1836 Given a I<Koha::Library> object, it returns a boolean representing
1837 the fact the patron can log into a the library.
1838
1839 =cut
1840
1841 sub can_log_into {
1842     my ( $self, $library ) = @_;
1843
1844     my $can = 0;
1845
1846     if ( C4::Context->preference('IndependentBranches') ) {
1847         $can = 1
1848           if $self->is_superlibrarian
1849           or $self->branchcode eq $library->id;
1850     }
1851     else {
1852         # no restrictions
1853         $can = 1;
1854     }
1855
1856    return $can;
1857 }
1858
1859 =head3 libraries_where_can_see_things
1860
1861     my $libraries = $patron->libraries_where_can_see_things;
1862
1863 Returns a list of libraries where an aribitarary action is allowed to be taken by the logged in librarian
1864 against an object based on some branchcode related to the object ( patron branchcode, item homebranch, etc ).
1865
1866 We are supposing here that the object is related to the logged in librarian (use of C4::Context::only_my_library)
1867
1868 An empty array means no restriction, the thing can see thing's infos from any libraries.
1869
1870 =cut
1871
1872 sub libraries_where_can_see_things {
1873     my ( $self, $params ) = @_;
1874     my $permission    = $params->{permission};
1875     my $subpermission = $params->{subpermission};
1876     my $group_feature = $params->{group_feature};
1877
1878     return $self->{"_restricted_branchcodes:$permission:$subpermission:$group_feature"}
1879         if exists( $self->{"_restricted_branchcodes:$permission:$subpermission:$group_feature"} );
1880
1881     my $userenv = C4::Context->userenv;
1882
1883     return () unless $userenv; # For tests, but userenv should be defined in tests...
1884
1885     my @restricted_branchcodes;
1886     if (C4::Context::only_my_library) {
1887         push @restricted_branchcodes, $self->branchcode;
1888     }
1889     else {
1890         unless (
1891             $self->has_permission(
1892                 { $permission => $subpermission }
1893             )
1894           )
1895         {
1896             my $library_groups = $self->library->library_groups({ $group_feature => 1 });
1897             if ( $library_groups->count )
1898             {
1899                 while ( my $library_group = $library_groups->next ) {
1900                     my $parent = $library_group->parent;
1901                     if ( $parent->has_child( $self->branchcode ) ) {
1902                         push @restricted_branchcodes, $parent->children->get_column('branchcode');
1903                     }
1904                 }
1905             }
1906
1907             @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1908         }
1909     }
1910
1911     @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1912     @restricted_branchcodes = uniq(@restricted_branchcodes);
1913     @restricted_branchcodes = sort(@restricted_branchcodes);
1914
1915     $self->{"_restricted_branchcodes:$permission:$subpermission:$group_feature"} = \@restricted_branchcodes;
1916     return @{ $self->{"_restricted_branchcodes:$permission:$subpermission:$group_feature"} };
1917 }
1918
1919 =head3 has_permission
1920
1921 my $permission = $patron->has_permission($required);
1922
1923 See C4::Auth::haspermission for details of syntax for $required
1924
1925 =cut
1926
1927 sub has_permission {
1928     my ( $self, $flagsrequired ) = @_;
1929     return unless $self->userid;
1930     # TODO code from haspermission needs to be moved here!
1931     return C4::Auth::haspermission( $self->userid, $flagsrequired );
1932 }
1933
1934 =head3 is_superlibrarian
1935
1936   my $is_superlibrarian = $patron->is_superlibrarian;
1937
1938 Return true if the patron is a superlibrarian.
1939
1940 =cut
1941
1942 sub is_superlibrarian {
1943     my ($self) = @_;
1944     return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1945 }
1946
1947 =head3 is_adult
1948
1949 my $is_adult = $patron->is_adult
1950
1951 Return true if the patron has a category with a type Adult (A) or Organization (I)
1952
1953 =cut
1954
1955 sub is_adult {
1956     my ( $self ) = @_;
1957     return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1958 }
1959
1960 =head3 is_child
1961
1962 my $is_child = $patron->is_child
1963
1964 Return true if the patron has a category with a type Child (C)
1965
1966 =cut
1967
1968 sub is_child {
1969     my( $self ) = @_;
1970     return $self->category->category_type eq 'C' ? 1 : 0;
1971 }
1972
1973 =head3 has_valid_userid
1974
1975 my $patron = Koha::Patrons->find(42);
1976 $patron->userid( $new_userid );
1977 my $has_a_valid_userid = $patron->has_valid_userid
1978
1979 my $patron = Koha::Patron->new( $params );
1980 my $has_a_valid_userid = $patron->has_valid_userid
1981
1982 Return true if the current userid of this patron is valid/unique, otherwise false.
1983
1984 Note that this should be done in $self->store instead and raise an exception if needed.
1985
1986 =cut
1987
1988 sub has_valid_userid {
1989     my ($self) = @_;
1990
1991     return 0 unless $self->userid;
1992
1993     return 0 if ( $self->userid eq C4::Context->config('user') );    # DB user
1994
1995     my $already_exists = Koha::Patrons->search(
1996         {
1997             userid => $self->userid,
1998             (
1999                 $self->in_storage
2000                 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
2001                 : ()
2002             ),
2003         }
2004     )->count;
2005     return $already_exists ? 0 : 1;
2006 }
2007
2008 =head3 generate_userid
2009
2010     $patron->generate_userid;
2011
2012     If you do not have a plugin for generating a userid, we will call
2013     the internal method here that returns firstname.surname[.number],
2014     where number is an optional suffix to make the userid unique.
2015     (Its behavior has not been changed on bug 32426.)
2016
2017     If you have plugin(s), the first valid response will be used.
2018     A plugin is assumed to return a valid userid as suggestion, but not
2019     assumed to save it already.
2020     Does not fallback to internal (you could arrange for that in your plugin).
2021     Clears userid when there are no valid plugin responses.
2022
2023 =cut
2024
2025 sub generate_userid {
2026     my ( $self ) = @_;
2027     my @responses = Koha::Plugins->call(
2028         'patron_generate_userid', { patron => $self },
2029     );
2030     unless( @responses ) {
2031         # Empty list only possible when there are NO enabled plugins for this method.
2032         # In that case we provide internal response.
2033         return $self->_generate_userid_internal;
2034     }
2035     # If a plugin returned false value or invalid value, we do however not return
2036     # internal response. The plugins should deal with that themselves. So we prevent
2037     # unexpected/unwelcome internal codes for plugin failures.
2038     foreach my $response ( grep { $_ } @responses ) {
2039         $self->userid( $response );
2040         return $self if $self->has_valid_userid;
2041     }
2042     $self->userid(undef);
2043     return $self;
2044 }
2045
2046 sub _generate_userid_internal { # as we always did
2047     my ($self) = @_;
2048     my $offset = 0;
2049     my $firstname = $self->firstname // q{};
2050     my $surname = $self->surname // q{};
2051     #The script will "do" the following code and increment the $offset until the generated userid is unique
2052     do {
2053       $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
2054       $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
2055       my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
2056       $userid = NFKD( $userid );
2057       $userid =~ s/\p{NonspacingMark}//g;
2058       $userid .= $offset unless $offset == 0;
2059       $self->userid( $userid );
2060       $offset++;
2061      } while (! $self->has_valid_userid );
2062
2063      return $self;
2064 }
2065
2066 =head3 add_extended_attribute
2067
2068 =cut
2069
2070 sub add_extended_attribute {
2071     my ($self, $attribute) = @_;
2072
2073     return Koha::Patron::Attribute->new(
2074         {
2075             %$attribute,
2076             ( borrowernumber => $self->borrowernumber ),
2077         }
2078     )->store;
2079
2080 }
2081
2082 =head3 extended_attributes
2083
2084 Return object of Koha::Patron::Attributes type with all attributes set for this patron
2085
2086 Or setter FIXME
2087
2088 =cut
2089
2090 sub extended_attributes {
2091     my ( $self, $attributes ) = @_;
2092     if ($attributes) {    # setter
2093         my $schema = $self->_result->result_source->schema;
2094         $schema->txn_do(
2095             sub {
2096                 # Remove the existing one
2097                 $self->extended_attributes->filter_by_branch_limitations->delete;
2098
2099                 # Insert the new ones
2100                 my $new_types = {};
2101                 for my $attribute (@$attributes) {
2102                     $self->add_extended_attribute($attribute);
2103                     $new_types->{$attribute->{code}} = 1;
2104                 }
2105
2106                 # Check globally mandatory types
2107                 my @required_attribute_types =
2108                     Koha::Patron::Attribute::Types->search(
2109                         {
2110                             mandatory => 1,
2111                             category_code => [ undef, $self->categorycode ],
2112                             'borrower_attribute_types_branches.b_branchcode' =>
2113                               undef,
2114                         },
2115                         { join => 'borrower_attribute_types_branches' }
2116                     )->get_column('code');
2117                 for my $type ( @required_attribute_types ) {
2118                     Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
2119                         type => $type,
2120                     ) if !$new_types->{$type};
2121                 }
2122             }
2123         );
2124     }
2125
2126     my $rs = $self->_result->borrower_attributes;
2127     # We call search to use the filters in Koha::Patron::Attributes->search
2128     return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
2129 }
2130
2131 =head3 messages
2132
2133     my $messages = $patron->messages;
2134
2135 Return the message attached to the patron.
2136
2137 =cut
2138
2139 sub messages {
2140     my ( $self ) = @_;
2141     my $messages_rs = $self->_result->messages_borrowernumbers->search;
2142     return Koha::Patron::Messages->_new_from_dbic($messages_rs);
2143 }
2144
2145 =head3 lock
2146
2147     Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
2148
2149     Lock and optionally expire a patron account.
2150     Remove holds and article requests if remove flag set.
2151     In order to distinguish from locking by entering a wrong password, let's
2152     call this an administrative lockout.
2153
2154 =cut
2155
2156 sub lock {
2157     my ( $self, $params ) = @_;
2158     $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
2159     if( $params->{expire} ) {
2160         $self->dateexpiry( dt_from_string->subtract(days => 1) );
2161     }
2162     $self->store;
2163     if( $params->{remove} ) {
2164         $self->holds->delete;
2165         $self->article_requests->delete;
2166     }
2167     return $self;
2168 }
2169
2170 =head3 anonymize
2171
2172     Koha::Patrons->find($id)->anonymize;
2173
2174     Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
2175     are randomized, other personal data is cleared too.
2176     Patrons with issues are skipped.
2177
2178 =cut
2179
2180 sub anonymize {
2181     my ( $self ) = @_;
2182     if( $self->_result->issues->count ) {
2183         warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
2184         return;
2185     }
2186     # Mandatory fields come from the corresponding pref, but email fields
2187     # are removed since scrambled email addresses only generate errors
2188     my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
2189         split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
2190     $mandatory->{userid} = 1; # needed since sub store does not clear field
2191     my @columns = $self->_result->result_source->columns;
2192     @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized|auth_method/ } @columns;
2193     push @columns, 'dateofbirth'; # add this date back in
2194     foreach my $col (@columns) {
2195         $self->_anonymize_column($col, $mandatory->{lc $col} );
2196     }
2197     $self->anonymized(1)->store;
2198 }
2199
2200 sub _anonymize_column {
2201     my ( $self, $col, $mandatory ) = @_;
2202     my $col_info = $self->_result->result_source->column_info($col);
2203     my $type = $col_info->{data_type};
2204     my $nullable = $col_info->{is_nullable};
2205     my $val;
2206     if( $type =~ /char|text/ ) {
2207         $val = $mandatory
2208             ? Koha::Token->new->generate({ pattern => '\w{10}' })
2209             : $nullable
2210             ? undef
2211             : q{};
2212     } elsif( $type =~ /integer|int$|float|dec|double/ ) {
2213         $val = $nullable ? undef : 0;
2214     } elsif( $type =~ /date|time/ ) {
2215         $val = $nullable ? undef : dt_from_string;
2216     }
2217     $self->$col($val);
2218 }
2219
2220 =head3 add_guarantor
2221
2222     my $relationship = $patron->add_guarantor(
2223         {
2224             borrowernumber => $borrowernumber,
2225             relationships  => $relationship,
2226         }
2227     );
2228
2229     Adds a new guarantor to a patron.
2230
2231 =cut
2232
2233 sub add_guarantor {
2234     my ( $self, $params ) = @_;
2235
2236     my $guarantor_id = $params->{guarantor_id};
2237     my $relationship = $params->{relationship};
2238
2239     return Koha::Patron::Relationship->new(
2240         {
2241             guarantee_id => $self->id,
2242             guarantor_id => $guarantor_id,
2243             relationship => $relationship
2244         }
2245     )->store();
2246 }
2247
2248 =head3 get_extended_attribute
2249
2250 my $attribute_value = $patron->get_extended_attribute( $code );
2251
2252 Return the attribute for the code passed in parameter.
2253
2254 It not exist it returns undef
2255
2256 Note that this will not work for repeatable attribute types.
2257
2258 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
2259 (which should be a real patron's attribute (not extended)
2260
2261 =cut
2262
2263 sub get_extended_attribute {
2264     my ( $self, $code, $value ) = @_;
2265     my $rs = $self->_result->borrower_attributes;
2266     return unless $rs;
2267     my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
2268     return unless $attribute->count;
2269     return $attribute->next;
2270 }
2271
2272 =head3 set_default_messaging_preferences
2273
2274     $patron->set_default_messaging_preferences
2275
2276 Sets default messaging preferences on patron.
2277
2278 See Koha::Patron::MessagePreference(s) for more documentation, especially on
2279 thrown exceptions.
2280
2281 =cut
2282
2283 sub set_default_messaging_preferences {
2284     my ($self, $categorycode) = @_;
2285
2286     my $options = Koha::Patron::MessagePreferences->get_options;
2287
2288     foreach my $option (@$options) {
2289         # Check that this option has preference configuration for this category
2290         unless (Koha::Patron::MessagePreferences->search({
2291             message_attribute_id => $option->{message_attribute_id},
2292             categorycode         => $categorycode || $self->categorycode,
2293         })->count) {
2294             next;
2295         }
2296
2297         # Delete current setting
2298         Koha::Patron::MessagePreferences->search({
2299              borrowernumber => $self->borrowernumber,
2300              message_attribute_id => $option->{message_attribute_id},
2301         })->delete;
2302
2303         Koha::Patron::MessagePreference->new_from_default({
2304             borrowernumber => $self->borrowernumber,
2305             categorycode   => $categorycode || $self->categorycode,
2306             message_attribute_id => $option->{message_attribute_id},
2307         });
2308     }
2309
2310     return $self;
2311 }
2312
2313 =head3 is_accessible
2314
2315     if ( $patron->is_accessible({ user => $logged_in_user }) ) { ... }
2316
2317 This overloaded method validates whether the current I<Koha::Patron> object can be accessed
2318 by the logged in user.
2319
2320 Returns 0 if the I<user> parameter is missing.
2321
2322 =cut
2323
2324 sub is_accessible {
2325     my ( $self, $params ) = @_;
2326
2327     unless ( defined( $params->{user} ) ) {
2328         Koha::Exceptions::MissingParameter->throw( error => "The `user` parameter is mandatory" );
2329     }
2330
2331     my $consumer = $params->{user};
2332     return $consumer->can_see_patron_infos($self);
2333 }
2334
2335 =head3 unredact_list
2336
2337 This method returns the list of database fields that should be visible, even for restricted users,
2338 for both API and UI output purposes
2339
2340 =cut
2341
2342 sub unredact_list {
2343     return ['branchcode'];
2344 }
2345
2346 =head3 to_api
2347
2348     my $json = $patron->to_api;
2349
2350 Overloaded method that returns a JSON representation of the Koha::Patron object,
2351 suitable for API output.
2352
2353 =cut
2354
2355 sub to_api {
2356     my ( $self, $params ) = @_;
2357
2358     my $json_patron = $self->SUPER::to_api( $params );
2359
2360     return unless $json_patron;
2361
2362     $json_patron->{restricted} = ( $self->is_debarred )
2363                                     ? Mojo::JSON->true
2364                                     : Mojo::JSON->false;
2365
2366     return $json_patron;
2367 }
2368
2369 =head3 to_api_mapping
2370
2371 This method returns the mapping for representing a Koha::Patron object
2372 on the API.
2373
2374 =cut
2375
2376 sub to_api_mapping {
2377     return {
2378         borrowernotes       => 'staff_notes',
2379         borrowernumber      => 'patron_id',
2380         branchcode          => 'library_id',
2381         categorycode        => 'category_id',
2382         checkprevcheckout   => 'check_previous_checkout',
2383         contactfirstname    => undef,                     # Unused
2384         contactname         => undef,                     # Unused
2385         contactnote         => 'altaddress_notes',
2386         contacttitle        => undef,                     # Unused
2387         dateenrolled        => 'date_enrolled',
2388         dateexpiry          => 'expiry_date',
2389         dateofbirth         => 'date_of_birth',
2390         debarred            => undef,                     # replaced by 'restricted'
2391         debarredcomment     => undef,    # calculated, API consumers will use /restrictions instead
2392         emailpro            => 'secondary_email',
2393         flags               => undef,    # permissions manipulation handled in /permissions
2394         gonenoaddress       => 'incorrect_address',
2395         lastseen            => 'last_seen',
2396         lost                => 'patron_card_lost',
2397         opacnote            => 'opac_notes',
2398         othernames          => 'other_name',
2399         password            => undef,            # password manipulation handled in /password
2400         phonepro            => 'secondary_phone',
2401         relationship        => 'relationship_type',
2402         sex                 => 'gender',
2403         smsalertnumber      => 'sms_number',
2404         sort1               => 'statistics_1',
2405         sort2               => 'statistics_2',
2406         autorenew_checkouts => 'autorenew_checkouts',
2407         streetnumber        => 'street_number',
2408         streettype          => 'street_type',
2409         zipcode             => 'postal_code',
2410         B_address           => 'altaddress_address',
2411         B_address2          => 'altaddress_address2',
2412         B_city              => 'altaddress_city',
2413         B_country           => 'altaddress_country',
2414         B_email             => 'altaddress_email',
2415         B_phone             => 'altaddress_phone',
2416         B_state             => 'altaddress_state',
2417         B_streetnumber      => 'altaddress_street_number',
2418         B_streettype        => 'altaddress_street_type',
2419         B_zipcode           => 'altaddress_postal_code',
2420         altcontactaddress1  => 'altcontact_address',
2421         altcontactaddress2  => 'altcontact_address2',
2422         altcontactaddress3  => 'altcontact_city',
2423         altcontactcountry   => 'altcontact_country',
2424         altcontactfirstname => 'altcontact_firstname',
2425         altcontactphone     => 'altcontact_phone',
2426         altcontactsurname   => 'altcontact_surname',
2427         altcontactstate     => 'altcontact_state',
2428         altcontactzipcode   => 'altcontact_postal_code',
2429         password_expiration_date => undef,
2430         primary_contact_method => undef,
2431         secret              => undef,
2432         auth_method         => undef,
2433     };
2434 }
2435
2436 =head3 queue_notice
2437
2438     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
2439     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
2440     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
2441
2442     Queue messages to a patron. Can pass a message that is part of the message_attributes
2443     table or supply the transport to use.
2444
2445     If passed a message name we retrieve the patrons preferences for transports
2446     Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
2447     we have no address/number for sending
2448
2449     $letter_params is a hashref of the values to be passed to GetPreparedLetter
2450
2451     test_mode will only report which notices would be sent, but nothing will be queued
2452
2453 =cut
2454
2455 sub queue_notice {
2456     my ( $self, $params ) = @_;
2457     my $letter_params = $params->{letter_params};
2458     my $test_mode = $params->{test_mode};
2459
2460     return unless $letter_params;
2461     return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
2462
2463     my $library = Koha::Libraries->find( $letter_params->{branchcode} );
2464     my $from_email_address = $library->from_email_address;
2465
2466     my @message_transports;
2467     my $letter_code;
2468     $letter_code = $letter_params->{letter_code};
2469     if( $params->{message_name} ){
2470         my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
2471                 borrowernumber => $letter_params->{borrowernumber},
2472                 message_name => $params->{message_name}
2473         } );
2474         @message_transports = ( keys %{ $messaging_prefs->{transports} } );
2475         $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
2476     } else {
2477         @message_transports = @{$params->{message_transports}};
2478     }
2479     return unless defined $letter_code;
2480     $letter_params->{letter_code} = $letter_code;
2481     my $print_sent = 0;
2482     my %return;
2483     foreach my $mtt (@message_transports){
2484         next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
2485         # Notice is handled by TalkingTech_itiva_outbound.pl
2486         if (   ( $mtt eq 'email' and not $self->notice_email_address )
2487             or ( $mtt eq 'sms'   and not $self->smsalertnumber )
2488             or ( $mtt eq 'phone' and not $self->phone ) )
2489         {
2490             push @{ $return{fallback} }, $mtt;
2491             $mtt = 'print';
2492         }
2493         next if $mtt eq 'print' && $print_sent;
2494         $letter_params->{message_transport_type} = $mtt;
2495         my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
2496         C4::Letters::EnqueueLetter({
2497             letter => $letter,
2498             borrowernumber => $self->borrowernumber,
2499             from_address   => $from_email_address,
2500             message_transport_type => $mtt
2501         }) unless $test_mode;
2502         push @{$return{sent}}, $mtt;
2503         $print_sent = 1 if $mtt eq 'print';
2504     }
2505     return \%return;
2506 }
2507
2508 =head3 safe_to_delete
2509
2510     my $result = $patron->safe_to_delete;
2511     if ( $result eq 'has_guarantees' ) { ... }
2512     elsif ( $result ) { ... }
2513     else { # cannot delete }
2514
2515 This method tells if the Koha:Patron object can be deleted. Possible return values
2516
2517 =over 4
2518
2519 =item 'ok'
2520
2521 =item 'has_checkouts'
2522
2523 =item 'has_debt'
2524
2525 =item 'has_guarantees'
2526
2527 =item 'is_anonymous_patron'
2528
2529 =item 'is_protected'
2530
2531 =back
2532
2533 =cut
2534
2535 sub safe_to_delete {
2536     my ($self) = @_;
2537
2538     my $anonymous_patron = C4::Context->preference('AnonymousPatron');
2539
2540     my $error;
2541
2542     if ( $anonymous_patron && $self->id eq $anonymous_patron ) {
2543         $error = 'is_anonymous_patron';
2544     }
2545     elsif ( $self->checkouts->count ) {
2546         $error = 'has_checkouts';
2547     }
2548     elsif ( $self->account->outstanding_debits->total_outstanding > 0 ) {
2549         $error = 'has_debt';
2550     }
2551     elsif ( $self->guarantee_relationships->count ) {
2552         $error = 'has_guarantees';
2553     }
2554     elsif ( $self->protected ) {
2555         $error = 'is_protected';
2556     }
2557
2558     if ( $error ) {
2559         return Koha::Result::Boolean->new(0)->add_message({ message => $error });
2560     }
2561
2562     return Koha::Result::Boolean->new(1);
2563 }
2564
2565 =head3 recalls
2566
2567     my $recalls = $patron->recalls;
2568
2569 Return the patron's recalls.
2570
2571 =cut
2572
2573 sub recalls {
2574     my ( $self ) = @_;
2575
2576     return Koha::Recalls->search({ patron_id => $self->borrowernumber });
2577 }
2578
2579 =head3 account_balance
2580
2581     my $balance = $patron->account_balance
2582
2583 Return the patron's account balance
2584
2585 =cut
2586
2587 sub account_balance {
2588     my ($self) = @_;
2589     return $self->account->balance;
2590 }
2591
2592 =head3 notify_library_of_registration
2593
2594 $patron->notify_library_of_registration( $email_patron_registrations );
2595
2596 Send patron registration email to library if EmailPatronRegistrations system preference is enabled.
2597
2598 =cut
2599
2600 sub notify_library_of_registration {
2601     my ( $self, $email_patron_registrations ) = @_;
2602
2603     if (
2604         my $letter = C4::Letters::GetPreparedLetter(
2605             module      => 'members',
2606             letter_code => 'OPAC_REG',
2607             branchcode  => $self->branchcode,
2608             lang        => $self->lang || 'default',
2609             tables      => {
2610                 'borrowers' => $self->borrowernumber
2611             },
2612         )
2613     ) {
2614         my $to_address;
2615         if ( $email_patron_registrations eq "BranchEmailAddress" ) {
2616             my $library = Koha::Libraries->find( $self->branchcode );
2617             $to_address = $library->inbound_email_address;
2618         }
2619         elsif ( $email_patron_registrations eq "KohaAdminEmailAddress" ) {
2620             $to_address = C4::Context->preference('ReplytoDefault')
2621             || C4::Context->preference('KohaAdminEmailAddress');
2622         }
2623         else {
2624             $to_address =
2625                 C4::Context->preference('EmailAddressForPatronRegistrations')
2626                 || C4::Context->preference('ReplytoDefault')
2627                 || C4::Context->preference('KohaAdminEmailAddress');
2628         }
2629
2630         my $message_id = C4::Letters::EnqueueLetter(
2631             {
2632                 letter                 => $letter,
2633                 borrowernumber         => $self->borrowernumber,
2634                 to_address             => $to_address,
2635                 message_transport_type => 'email'
2636             }
2637         ) or warn "can't enqueue letter $letter";
2638         if ( $message_id ) {
2639             return 1;
2640         }
2641     }
2642 }
2643
2644 =head3 has_messaging_preference
2645
2646 my $bool = $patron->has_messaging_preference({
2647     message_name => $message_name, # A value from message_attributes.message_name
2648     message_transport_type => $message_transport_type, # email, sms, phone, itiva, etc...
2649     wants_digest => $wants_digest, # 1 if you are looking for the digest version, don't pass if you just want either
2650 });
2651
2652 =cut
2653
2654 sub has_messaging_preference {
2655     my ( $self, $params ) = @_;
2656
2657     my $message_name           = $params->{message_name};
2658     my $message_transport_type = $params->{message_transport_type};
2659     my $wants_digest           = $params->{wants_digest};
2660
2661     return $self->_result->search_related_rs(
2662         'borrower_message_preferences',
2663         $params,
2664         {
2665             prefetch =>
2666               [ 'borrower_message_transport_preferences', 'message_attribute' ]
2667         }
2668     )->count;
2669 }
2670
2671 =head3 can_patron_change_staff_only_lists
2672
2673 $patron->can_patron_change_staff_only_lists;
2674
2675 Return 1 if a patron has 'Superlibrarian' or 'Catalogue' permission.
2676 Otherwise, return 0.
2677
2678 =cut
2679
2680 sub can_patron_change_staff_only_lists {
2681     my ( $self, $params ) = @_;
2682     return 1 if C4::Auth::haspermission( $self->userid, { 'catalogue' => 1 });
2683     return 0;
2684 }
2685
2686 =head3 can_patron_change_permitted_staff_lists
2687
2688 $patron->can_patron_change_permitted_staff_lists;
2689
2690 Return 1 if a patron has 'Superlibrarian' or 'Catalogue' and 'edit_public_list_contents' permissions.
2691 Otherwise, return 0.
2692
2693 =cut
2694
2695 sub can_patron_change_permitted_staff_lists {
2696     my ( $self, $params ) = @_;
2697     return 1 if C4::Auth::haspermission( $self->userid, { 'catalogue' => 1, lists => 'edit_public_list_contents' } );
2698     return 0;
2699 }
2700
2701 =head3 encode_secret
2702
2703   $patron->encode_secret($secret32);
2704
2705 Secret (TwoFactorAuth expects it in base32 format) is encrypted.
2706 You still need to call ->store.
2707
2708 =cut
2709
2710 sub encode_secret {
2711     my ( $self, $secret ) = @_;
2712     if( $secret ) {
2713         return $self->secret( Koha::Encryption->new->encrypt_hex($secret) );
2714     }
2715     return $self->secret($secret);
2716 }
2717
2718 =head3 decoded_secret
2719
2720   my $secret32 = $patron->decoded_secret;
2721
2722 Decode the patron secret. We expect to get back a base32 string, but this
2723 is not checked here. Caller of encode_secret is responsible for that.
2724
2725 =cut
2726
2727 sub decoded_secret {
2728     my ( $self ) = @_;
2729     if( $self->secret ) {
2730         return Koha::Encryption->new->decrypt_hex( $self->secret );
2731     }
2732     return $self->secret;
2733 }
2734
2735 =head3 virtualshelves
2736
2737     my $shelves = $patron->virtualshelves;
2738
2739 =cut
2740
2741 sub virtualshelves {
2742     my $self = shift;
2743     return Koha::Virtualshelves->_new_from_dbic( scalar $self->_result->virtualshelves );
2744 }
2745
2746 =head3 get_savings
2747
2748     my $savings = $patron->get_savings;
2749
2750 Use the replacement price of patron's old and current issues to calculate how much they have 'saved' by using the library.
2751
2752 =cut
2753
2754 sub get_savings {
2755     my ($self) = @_;
2756
2757     my @itemnumbers = grep { defined $_ } ( $self->old_checkouts->get_column('itemnumber'), $self->checkouts->get_column('itemnumber') );
2758
2759     return Koha::Items->search(
2760         { itemnumber => { -in => \@itemnumbers } },
2761         {   select => [ { sum => 'me.replacementprice' } ],
2762             as     => ['total_savings']
2763         }
2764     )->next->get_column('total_savings') // 0;
2765 }
2766
2767 =head3 alert_subscriptions
2768
2769     my $subscriptions = $patron->alert_subscriptions;
2770
2771 Return a Koha::Subscriptions object containing subscriptions for which the patron has subscribed to email alerts.
2772
2773 =cut
2774
2775 sub alert_subscriptions {
2776     my ($self) = @_;
2777
2778     my @alerts           = $self->_result->alerts;
2779     my @subscription_ids = map { $_->externalid } @alerts;
2780
2781     return Koha::Subscriptions->search( { subscriptionid => \@subscription_ids } );
2782 }
2783
2784 =head3 consent
2785
2786     my $consent = $patron->consent(TYPE);
2787
2788     Returns the first consent of type TYPE (there should be only one) or a new instance
2789     of Koha::Patron::Consent.
2790
2791 =cut
2792
2793 sub consent {
2794     my ( $self, $type ) = @_;
2795     Koha::Exceptions::MissingParameter->throw('Missing consent type') if !$type;
2796     my $consents = Koha::Patron::Consents->search(
2797         {
2798             borrowernumber => $self->borrowernumber,
2799             type           => $type,
2800         }
2801     );
2802     return $consents && $consents->count
2803         ? $consents->next
2804         : Koha::Patron::Consent->new( { borrowernumber => $self->borrowernumber, type => $type } );
2805 }
2806
2807 =head2 Internal methods
2808
2809 =head3 _type
2810
2811 =cut
2812
2813 sub _type {
2814     return 'Borrower';
2815 }
2816
2817 =head1 AUTHORS
2818
2819 Kyle M Hall <kyle@bywatersolutions.com>
2820 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
2821 Martin Renvoize <martin.renvoize@ptfs-europe.com>
2822
2823 =cut
2824
2825 1;