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