Bug 14570: Make it possible to add multiple guarantors to a record
[koha.git] / t / db_dependent / Koha / Patrons.t
1 #!/usr/bin/perl
2
3 # Copyright 2015 Koha Development team
4 #
5 # This file is part of Koha
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use Test::More tests => 40;
23 use Test::Warn;
24 use Test::Exception;
25 use Test::MockModule;
26 use Time::Fake;
27 use DateTime;
28 use JSON;
29 use Data::Dumper;
30
31 use C4::Circulation;
32 use C4::Biblio;
33 use C4::Auth qw(checkpw_hash);
34
35 use Koha::ActionLogs;
36 use Koha::Holds;
37 use Koha::Old::Holds;
38 use Koha::Patrons;
39 use Koha::Patron::Categories;
40 use Koha::Patron::Relationship;
41 use Koha::Database;
42 use Koha::DateUtils;
43 use Koha::Virtualshelves;
44
45 use t::lib::TestBuilder;
46 use t::lib::Mocks;
47
48 my $schema = Koha::Database->new->schema;
49 $schema->storage->txn_begin;
50
51 my $builder       = t::lib::TestBuilder->new;
52 my $library = $builder->build({source => 'Branch' });
53 my $category = $builder->build({source => 'Category' });
54 my $nb_of_patrons = Koha::Patrons->search->count;
55 my $new_patron_1  = Koha::Patron->new(
56     {   cardnumber => 'test_cn_1',
57         branchcode => $library->{branchcode},
58         categorycode => $category->{categorycode},
59         surname => 'surname for patron1',
60         firstname => 'firstname for patron1',
61         userid => 'a_nonexistent_userid_1',
62         flags => 1, # Is superlibrarian
63     }
64 )->store;
65 my $new_patron_2  = Koha::Patron->new(
66     {   cardnumber => 'test_cn_2',
67         branchcode => $library->{branchcode},
68         categorycode => $category->{categorycode},
69         surname => 'surname for patron2',
70         firstname => 'firstname for patron2',
71         userid => 'a_nonexistent_userid_2',
72     }
73 )->store;
74
75 t::lib::Mocks::mock_userenv({ patron => $new_patron_1 });
76
77 is( Koha::Patrons->search->count, $nb_of_patrons + 2, 'The 2 patrons should have been added' );
78
79 my $retrieved_patron_1 = Koha::Patrons->find( $new_patron_1->borrowernumber );
80 is( $retrieved_patron_1->cardnumber, $new_patron_1->cardnumber, 'Find a patron by borrowernumber should return the correct patron' );
81
82 subtest 'library' => sub {
83     plan tests => 2;
84     is( $retrieved_patron_1->library->branchcode, $library->{branchcode}, 'Koha::Patron->library should return the correct library' );
85     is( ref($retrieved_patron_1->library), 'Koha::Library', 'Koha::Patron->library should return a Koha::Library object' );
86 };
87
88 subtest 'guarantees' => sub {
89     plan tests => 13;
90     my $guarantees = $new_patron_1->guarantee_relationships;
91     is( ref($guarantees), 'Koha::Patron::Relationships', 'Koha::Patron->guarantees should return a Koha::Patrons result set in a scalar context' );
92     is( $guarantees->count, 0, 'new_patron_1 should have 0 guarantee relationships' );
93     my @guarantees = $new_patron_1->guarantee_relationships;
94     is( ref(\@guarantees), 'ARRAY', 'Koha::Patron->guarantee_relationships should return an array in a list context' );
95     is( scalar(@guarantees), 0, 'new_patron_1 should have 0 guarantee' );
96
97     my $guarantee_1 = $builder->build({ source => 'Borrower' });
98     my $relationship_1 = Koha::Patron::Relationship->new( { guarantor_id => $new_patron_1->id, guarantee_id => $guarantee_1->{borrowernumber}, relationship => 'test' } )->store();
99     my $guarantee_2 = $builder->build({ source => 'Borrower' });
100     my $relationship_2 = Koha::Patron::Relationship->new( { guarantor_id => $new_patron_1->id, guarantee_id => $guarantee_2->{borrowernumber}, relationship => 'test' } )->store();
101
102     $guarantees = $new_patron_1->guarantee_relationships;
103     is( ref($guarantees), 'Koha::Patron::Relationships', 'Koha::Patron->guarantee_relationships should return a Koha::Patrons result set in a scalar context' );
104     is( $guarantees->count, 2, 'new_patron_1 should have 2 guarantees' );
105     @guarantees = $new_patron_1->guarantee_relationships;
106     is( ref(\@guarantees), 'ARRAY', 'Koha::Patron->guarantee_relationships should return an array in a list context' );
107     is( scalar(@guarantees), 2, 'new_patron_1 should have 2 guarantees' );
108     $_->delete for @guarantees;
109
110     #Test return order of guarantees BZ 18635
111     my $categorycode = $builder->build({ source => 'Category' })->{categorycode};
112     my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
113
114     my $guarantor = $builder->build_object( { class => 'Koha::Patrons' } );
115
116     my $order_guarantee1 = $builder->build_object(
117         {
118             class => 'Koha::Patrons',
119             value => {
120                 surname     => 'Zebra',
121             }
122         }
123     )->borrowernumber;
124     $builder->build_object(
125         {
126             class => 'Koha::Patron::Relationships',
127             value => {
128                 guarantor_id  => $guarantor->id,
129                 guarantee_id => $order_guarantee1,
130                 relationship => 'test',
131             }
132         }
133     );
134
135     my $order_guarantee2 = $builder->build_object(
136         {
137             class => 'Koha::Patrons',
138             value => {
139                 surname     => 'Yak',
140             }
141         }
142     )->borrowernumber;
143     $builder->build_object(
144         {
145             class => 'Koha::Patron::Relationships',
146             value => {
147                 guarantor_id  => $guarantor->id,
148                 guarantee_id => $order_guarantee2,
149                 relationship => 'test',
150             }
151         }
152     );
153
154     my $order_guarantee3 = $builder->build_object(
155         {
156             class => 'Koha::Patrons',
157             value => {
158                 surname     => 'Xerus',
159                 firstname   => 'Walrus',
160             }
161         }
162     )->borrowernumber;
163     $builder->build_object(
164         {
165             class => 'Koha::Patron::Relationships',
166             value => {
167                 guarantor_id  => $guarantor->id,
168                 guarantee_id => $order_guarantee3,
169                 relationship => 'test',
170             }
171         }
172     );
173
174     my $order_guarantee4 = $builder->build_object(
175         {
176             class => 'Koha::Patrons',
177             value => {
178                 surname     => 'Xerus',
179                 firstname   => 'Vulture',
180                 guarantorid => $guarantor->borrowernumber
181             }
182         }
183     )->borrowernumber;
184     $builder->build_object(
185         {
186             class => 'Koha::Patron::Relationships',
187             value => {
188                 guarantor_id  => $guarantor->id,
189                 guarantee_id => $order_guarantee4,
190                 relationship => 'test',
191             }
192         }
193     );
194
195     my $order_guarantee5 = $builder->build_object(
196         {
197             class => 'Koha::Patrons',
198             value => {
199                 surname     => 'Xerus',
200                 firstname   => 'Unicorn',
201                 guarantorid => $guarantor->borrowernumber
202             }
203         }
204     )->borrowernumber;
205     my $r = $builder->build_object(
206         {
207             class => 'Koha::Patron::Relationships',
208             value => {
209                 guarantor_id  => $guarantor->id,
210                 guarantee_id => $order_guarantee5,
211                 relationship => 'test',
212             }
213         }
214     );
215
216     $guarantees = $guarantor->guarantee_relationships->guarantees;
217
218     is( $guarantees->next()->borrowernumber, $order_guarantee5, "Return first guarantor alphabetically" );
219     is( $guarantees->next()->borrowernumber, $order_guarantee4, "Return second guarantor alphabetically" );
220     is( $guarantees->next()->borrowernumber, $order_guarantee3, "Return third guarantor alphabetically" );
221     is( $guarantees->next()->borrowernumber, $order_guarantee2, "Return fourth guarantor alphabetically" );
222     is( $guarantees->next()->borrowernumber, $order_guarantee1, "Return fifth guarantor alphabetically" );
223 };
224
225 subtest 'category' => sub {
226     plan tests => 2;
227     my $patron_category = $new_patron_1->category;
228     is( ref( $patron_category), 'Koha::Patron::Category', );
229     is( $patron_category->categorycode, $category->{categorycode}, );
230 };
231
232 subtest 'siblings' => sub {
233     plan tests => 7;
234     my $siblings = $new_patron_1->siblings;
235     is( $siblings, undef, 'Koha::Patron->siblings should not crashed if the patron has no guarantor' );
236     my $guarantee_1 = $builder->build( { source => 'Borrower' } );
237     my $relationship_1 = Koha::Patron::Relationship->new( { guarantor_id => $new_patron_1->borrowernumber, guarantee_id => $guarantee_1->{borrowernumber}, relationship => 'test' } )->store();
238     my $retrieved_guarantee_1 = Koha::Patrons->find($guarantee_1);
239     $siblings = $retrieved_guarantee_1->siblings;
240     is( ref($siblings), 'Koha::Patrons', 'Koha::Patron->siblings should return a Koha::Patrons result set in a scalar context' );
241     my @siblings = $retrieved_guarantee_1->siblings;
242     is( ref( \@siblings ), 'ARRAY', 'Koha::Patron->siblings should return an array in a list context' );
243     is( $siblings->count,  0,       'guarantee_1 should not have siblings yet' );
244     my $guarantee_2 = $builder->build( { source => 'Borrower' } );
245     my $relationship_2 = Koha::Patron::Relationship->new( { guarantor_id => $new_patron_1->borrowernumber, guarantee_id => $guarantee_2->{borrowernumber}, relationship => 'test' } )->store();
246     my $guarantee_3 = $builder->build( { source => 'Borrower' } );
247     my $relationship_3 = Koha::Patron::Relationship->new( { guarantor_id => $new_patron_1->borrowernumber, guarantee_id => $guarantee_3->{borrowernumber}, relationship => 'test' } )->store();
248     $siblings = $retrieved_guarantee_1->siblings;
249     is( $siblings->count,               2,                               'guarantee_1 should have 2 siblings' );
250     is( $guarantee_2->{borrowernumber}, $siblings->next->borrowernumber, 'guarantee_2 should exist in the guarantees' );
251     is( $guarantee_3->{borrowernumber}, $siblings->next->borrowernumber, 'guarantee_3 should exist in the guarantees' );
252     $_->delete for $retrieved_guarantee_1->siblings;
253     $retrieved_guarantee_1->delete;
254 };
255
256 subtest 'has_overdues' => sub {
257     plan tests => 3;
258
259     my $biblioitem_1 = $builder->build( { source => 'Biblioitem' } );
260     my $item_1 = $builder->build(
261         {   source => 'Item',
262             value  => {
263                 homebranch    => $library->{branchcode},
264                 holdingbranch => $library->{branchcode},
265                 notforloan    => 0,
266                 itemlost      => 0,
267                 withdrawn     => 0,
268                 biblionumber  => $biblioitem_1->{biblionumber}
269             }
270         }
271     );
272     my $retrieved_patron = Koha::Patrons->find( $new_patron_1->borrowernumber );
273     is( $retrieved_patron->has_overdues, 0, );
274
275     my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 );
276     my $issue = Koha::Checkout->new({ borrowernumber => $new_patron_1->id, itemnumber => $item_1->{itemnumber}, date_due => $tomorrow, branchcode => $library->{branchcode} })->store();
277     is( $retrieved_patron->has_overdues, 0, );
278     $issue->delete();
279     my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 );
280     $issue = Koha::Checkout->new({ borrowernumber => $new_patron_1->id, itemnumber => $item_1->{itemnumber}, date_due => $yesterday, branchcode => $library->{branchcode} })->store();
281     $retrieved_patron = Koha::Patrons->find( $new_patron_1->borrowernumber );
282     is( $retrieved_patron->has_overdues, 1, );
283     $issue->delete();
284 };
285
286 subtest 'is_expired' => sub {
287     plan tests => 4;
288     my $patron = $builder->build({ source => 'Borrower' });
289     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
290     $patron->dateexpiry( undef )->store->discard_changes;
291     is( $patron->is_expired, 0, 'Patron should not be considered expired if dateexpiry is not set');
292     $patron->dateexpiry( dt_from_string )->store->discard_changes;
293     is( $patron->is_expired, 0, 'Patron should not be considered expired if dateexpiry is today');
294     $patron->dateexpiry( dt_from_string->add( days => 1 ) )->store->discard_changes;
295     is( $patron->is_expired, 0, 'Patron should not be considered expired if dateexpiry is tomorrow');
296     $patron->dateexpiry( dt_from_string->add( days => -1 ) )->store->discard_changes;
297     is( $patron->is_expired, 1, 'Patron should be considered expired if dateexpiry is yesterday');
298
299     $patron->delete;
300 };
301
302 subtest 'is_going_to_expire' => sub {
303     plan tests => 8;
304     my $patron = $builder->build({ source => 'Borrower' });
305     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
306     $patron->dateexpiry( undef )->store->discard_changes;
307     is( $patron->is_going_to_expire, 0, 'Patron should not be considered going to expire if dateexpiry is not set');
308
309     t::lib::Mocks::mock_preference('NotifyBorrowerDeparture', 0);
310     $patron->dateexpiry( dt_from_string )->store->discard_changes;
311     is( $patron->is_going_to_expire, 0, 'Patron should not be considered going to expire if dateexpiry is today');
312
313     $patron->dateexpiry( dt_from_string )->store->discard_changes;
314     is( $patron->is_going_to_expire, 0, 'Patron should not be considered going to expire if dateexpiry is today and pref is 0');
315
316     t::lib::Mocks::mock_preference('NotifyBorrowerDeparture', 10);
317     $patron->dateexpiry( dt_from_string->add( days => 11 ) )->store->discard_changes;
318     is( $patron->is_going_to_expire, 0, 'Patron should not be considered going to expire if dateexpiry is 11 days ahead and pref is 10');
319
320     t::lib::Mocks::mock_preference('NotifyBorrowerDeparture', 0);
321     $patron->dateexpiry( dt_from_string->add( days => 10 ) )->store->discard_changes;
322     is( $patron->is_going_to_expire, 0, 'Patron should not be considered going to expire if dateexpiry is 10 days ahead and pref is 0');
323
324     t::lib::Mocks::mock_preference('NotifyBorrowerDeparture', 10);
325     $patron->dateexpiry( dt_from_string->add( days => 10 ) )->store->discard_changes;
326     is( $patron->is_going_to_expire, 0, 'Patron should not be considered going to expire if dateexpiry is 10 days ahead and pref is 10');
327     $patron->delete;
328
329     t::lib::Mocks::mock_preference('NotifyBorrowerDeparture', 10);
330     $patron->dateexpiry( dt_from_string->add( days => 20 ) )->store->discard_changes;
331     is( $patron->is_going_to_expire, 0, 'Patron should not be considered going to expire if dateexpiry is 20 days ahead and pref is 10');
332
333     t::lib::Mocks::mock_preference('NotifyBorrowerDeparture', 20);
334     $patron->dateexpiry( dt_from_string->add( days => 10 ) )->store->discard_changes;
335     is( $patron->is_going_to_expire, 1, 'Patron should be considered going to expire if dateexpiry is 10 days ahead and pref is 20');
336
337     $patron->delete;
338 };
339
340
341 subtest 'renew_account' => sub {
342     plan tests => 48;
343
344     for my $date ( '2016-03-31', '2016-11-30', '2019-01-31', dt_from_string() ) {
345         my $dt = dt_from_string( $date, 'iso' );
346         Time::Fake->offset( $dt->epoch );
347         my $a_month_ago                = $dt->clone->subtract( months => 1, end_of_month => 'limit' )->truncate( to => 'day' );
348         my $a_year_later               = $dt->clone->add( months => 12, end_of_month => 'limit' )->truncate( to => 'day' );
349         my $a_year_later_minus_a_month = $a_month_ago->clone->add( months => 12, end_of_month => 'limit' )->truncate( to => 'day' );
350         my $a_month_later              = $dt->clone->add( months => 1 , end_of_month => 'limit' )->truncate( to => 'day' );
351         my $a_year_later_plus_a_month  = $a_month_later->clone->add( months => 12, end_of_month => 'limit' )->truncate( to => 'day' );
352         my $patron_category = $builder->build(
353             {   source => 'Category',
354                 value  => {
355                     enrolmentperiod     => 12,
356                     enrolmentperioddate => undef,
357                 }
358             }
359         );
360         my $patron = $builder->build(
361             {   source => 'Borrower',
362                 value  => {
363                     dateexpiry   => $a_month_ago,
364                     categorycode => $patron_category->{categorycode},
365                     date_renewed => undef, # Force builder to not populate the column for new patron
366                 }
367             }
368         );
369         my $patron_2 = $builder->build(
370             {  source => 'Borrower',
371                value  => {
372                    dateexpiry => $a_month_ago,
373                    categorycode => $patron_category->{categorycode},
374                 }
375             }
376         );
377         my $patron_3 = $builder->build(
378             {  source => 'Borrower',
379                value  => {
380                    dateexpiry => $a_month_later,
381                    categorycode => $patron_category->{categorycode},
382                }
383             }
384         );
385         my $retrieved_patron = Koha::Patrons->find( $patron->{borrowernumber} );
386         my $retrieved_patron_2 = Koha::Patrons->find( $patron_2->{borrowernumber} );
387         my $retrieved_patron_3 = Koha::Patrons->find( $patron_3->{borrowernumber} );
388
389         is( $retrieved_patron->date_renewed, undef, "Date renewed is not set for patrons that have never been renewed" );
390
391         t::lib::Mocks::mock_preference( 'BorrowerRenewalPeriodBase', 'dateexpiry' );
392         t::lib::Mocks::mock_preference( 'BorrowersLog',              1 );
393         my $expiry_date = $retrieved_patron->renew_account;
394         is( $expiry_date, $a_year_later_minus_a_month, "$a_month_ago + 12 months must be $a_year_later_minus_a_month" );
395         my $retrieved_expiry_date = Koha::Patrons->find( $patron->{borrowernumber} )->dateexpiry;
396         is( dt_from_string($retrieved_expiry_date), $a_year_later_minus_a_month, "$a_month_ago + 12 months must be $a_year_later_minus_a_month" );
397         my $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'MEMBERS', action => 'RENEW', object => $retrieved_patron->borrowernumber } )->count;
398         is( $number_of_logs, 1, 'With BorrowerLogs, Koha::Patron->renew_account should have logged' );
399
400         t::lib::Mocks::mock_preference( 'BorrowerRenewalPeriodBase', 'now' );
401         t::lib::Mocks::mock_preference( 'BorrowersLog',              0 );
402         $expiry_date = $retrieved_patron->renew_account;
403         is( $expiry_date, $a_year_later, "today + 12 months must be $a_year_later" );
404         $retrieved_patron = Koha::Patrons->find( $patron->{borrowernumber} );
405         is( $retrieved_patron->date_renewed, output_pref({ dt => $dt, dateformat => 'iso', dateonly => 1 }), "Date renewed is set when calling renew_account" );
406         $retrieved_expiry_date = $retrieved_patron->dateexpiry;
407         is( dt_from_string($retrieved_expiry_date), $a_year_later, "today + 12 months must be $a_year_later" );
408         $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'MEMBERS', action => 'RENEW', object => $retrieved_patron->borrowernumber } )->count;
409         is( $number_of_logs, 1, 'Without BorrowerLogs, Koha::Patron->renew_account should not have logged' );
410
411         t::lib::Mocks::mock_preference( 'BorrowerRenewalPeriodBase', 'combination' );
412         $expiry_date = $retrieved_patron_2->renew_account;
413         is( $expiry_date, $a_year_later, "today + 12 months must be $a_year_later" );
414         $retrieved_expiry_date = Koha::Patrons->find( $patron_2->{borrowernumber} )->dateexpiry;
415         is( dt_from_string($retrieved_expiry_date), $a_year_later, "today + 12 months must be $a_year_later" );
416
417         $expiry_date = $retrieved_patron_3->renew_account;
418         is( $expiry_date, $a_year_later_plus_a_month, "$a_month_later + 12 months must be $a_year_later_plus_a_month" );
419         $retrieved_expiry_date = Koha::Patrons->find( $patron_3->{borrowernumber} )->dateexpiry;
420         is( dt_from_string($retrieved_expiry_date), $a_year_later_plus_a_month, "$a_month_later + 12 months must be $a_year_later_plus_a_month" );
421
422         $retrieved_patron->delete;
423         $retrieved_patron_2->delete;
424         $retrieved_patron_3->delete;
425     }
426     Time::Fake->reset;
427 };
428
429 subtest "move_to_deleted" => sub {
430     plan tests => 5;
431     my $originally_updated_on = '2016-01-01 12:12:12';
432     my $patron = $builder->build( { source => 'Borrower',value => { updated_on => $originally_updated_on } } );
433     my $retrieved_patron = Koha::Patrons->find( $patron->{borrowernumber} );
434     is( ref( $retrieved_patron->move_to_deleted ), 'Koha::Schema::Result::Deletedborrower', 'Koha::Patron->move_to_deleted should return the Deleted patron' )
435       ;    # FIXME This should be Koha::Deleted::Patron
436     my $deleted_patron = $schema->resultset('Deletedborrower')
437         ->search( { borrowernumber => $patron->{borrowernumber} }, { result_class => 'DBIx::Class::ResultClass::HashRefInflator' } )
438         ->next;
439     ok( $retrieved_patron->updated_on, 'updated_on should be set for borrowers table' );
440     ok( $deleted_patron->{updated_on}, 'updated_on should be set for deleted_borrowers table' );
441     isnt( $deleted_patron->{updated_on}, $retrieved_patron->updated_on, 'Koha::Patron->move_to_deleted should have correctly updated the updated_on column');
442     $deleted_patron->{updated_on} = $originally_updated_on; #reset for simplicity in comparing all other fields
443     is_deeply( $deleted_patron, $patron, 'Koha::Patron->move_to_deleted should have correctly moved the patron to the deleted table' );
444     $retrieved_patron->delete( $patron->{borrowernumber} );    # Cleanup
445 };
446
447 subtest "delete" => sub {
448     plan tests => 6;
449     t::lib::Mocks::mock_preference( 'BorrowersLog', 1 );
450     my $patron           = $builder->build( { source => 'Borrower' } );
451     my $retrieved_patron = Koha::Patrons->find( $patron->{borrowernumber} );
452     my $hold             = $builder->build(
453         {   source => 'Reserve',
454             value  => { borrowernumber => $patron->{borrowernumber} }
455         }
456     );
457     my $list = $builder->build(
458         {   source => 'Virtualshelve',
459             value  => { owner => $patron->{borrowernumber} }
460         }
461     );
462
463     my $deleted = $retrieved_patron->delete;
464     is( $deleted, 1, 'Koha::Patron->delete should return 1 if the patron has been correctly deleted' );
465
466     is( Koha::Patrons->find( $patron->{borrowernumber} ), undef, 'Koha::Patron->delete should have deleted the patron' );
467
468     is (Koha::Old::Holds->search( { reserve_id => $hold->{ reserve_id } } )->count, 1, q|Koha::Patron->delete should have cancelled patron's holds| );
469
470     is( Koha::Holds->search( { borrowernumber => $patron->{borrowernumber} } )->count, 0, q|Koha::Patron->delete should have cancelled patron's holds 2| );
471
472     is( Koha::Virtualshelves->search( { owner => $patron->{borrowernumber} } )->count, 0, q|Koha::Patron->delete should have deleted patron's lists| );
473
474     my $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'MEMBERS', action => 'DELETE', object => $retrieved_patron->borrowernumber } )->count;
475     is( $number_of_logs, 1, 'With BorrowerLogs, Koha::Patron->delete should have logged' );
476 };
477
478 subtest 'Koha::Patrons->delete' => sub {
479     plan tests => 4;
480
481     my $mod_patron = Test::MockModule->new( 'Koha::Patron' );
482     my $moved_to_deleted = 0;
483     $mod_patron->mock( 'move_to_deleted', sub { $moved_to_deleted++; } );
484
485     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
486     my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
487     my $id1 = $patron1->borrowernumber;
488     my $set = Koha::Patrons->search({ borrowernumber => { '>=' => $id1 }});
489     is( $set->count, 2, 'Two patrons found as expected' );
490     is( $set->delete({ move => 1 }), 2, 'Two patrons deleted' );
491     is( $moved_to_deleted, 2, 'Patrons moved to deletedborrowers' );
492
493     # Add again, test if we can raise an exception
494     $mod_patron->mock( 'delete', sub { return -1; } );
495     $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
496     $id1 = $patron1->borrowernumber;
497     $set = Koha::Patrons->search({ borrowernumber => { '>=' => $id1 }});
498     throws_ok { $set->delete } 'Koha::Exceptions::Patron::FailedDelete',
499         'Exception raised for deleting patron';
500 };
501
502 subtest 'add_enrolment_fee_if_needed' => sub {
503     plan tests => 4;
504
505     my $enrolmentfees = { K  => 5, J => 10, YA => 20 };
506     foreach( keys %{$enrolmentfees} ) {
507         ( Koha::Patron::Categories->find( $_ ) // $builder->build_object({ class => 'Koha::Patron::Categories', value => { categorycode => $_ } }) )->enrolmentfee( $enrolmentfees->{$_} )->store;
508     }
509     my $enrolmentfee_K  = $enrolmentfees->{K};
510     my $enrolmentfee_J  = $enrolmentfees->{J};
511     my $enrolmentfee_YA = $enrolmentfees->{YA};
512
513     my %borrower_data = (
514         firstname    => 'my firstname',
515         surname      => 'my surname',
516         categorycode => 'K',
517         branchcode   => $library->{branchcode},
518     );
519
520     my $borrowernumber = Koha::Patron->new(\%borrower_data)->store->borrowernumber;
521     $borrower_data{borrowernumber} = $borrowernumber;
522
523     my $patron = Koha::Patrons->find( $borrowernumber );
524     my $total = $patron->account->balance;
525     is( int($total), int($enrolmentfee_K), "New kid pay $enrolmentfee_K" );
526
527     t::lib::Mocks::mock_preference( 'FeeOnChangePatronCategory', 0 );
528     $borrower_data{categorycode} = 'J';
529     $patron->set(\%borrower_data)->store;
530     $total = $patron->account->balance;
531     is( int($total), int($enrolmentfee_K), "Kid growing and become a juvenile, but shouldn't pay for the upgrade " );
532
533     $borrower_data{categorycode} = 'K';
534     $patron->set(\%borrower_data)->store;
535     t::lib::Mocks::mock_preference( 'FeeOnChangePatronCategory', 1 );
536
537     $borrower_data{categorycode} = 'J';
538     $patron->set(\%borrower_data)->store;
539     $total = $patron->account->balance;
540     is( int($total), int($enrolmentfee_K + $enrolmentfee_J), "Kid growing and become a juvenile, they should pay " . ( $enrolmentfee_K + $enrolmentfee_J ) );
541
542     # Check with calling directly Koha::Patron->get_enrolment_fee_if_needed
543     $patron->categorycode('YA')->store;
544     $total = $patron->account->balance;
545     is( int($total),
546         int($enrolmentfee_K + $enrolmentfee_J + $enrolmentfee_YA),
547         "Juvenile growing and become an young adult, they should pay " . ( $enrolmentfee_K + $enrolmentfee_J + $enrolmentfee_YA )
548     );
549
550     $patron->delete;
551 };
552
553 subtest 'checkouts + pending_checkouts + get_overdues + old_checkouts' => sub {
554     plan tests => 17;
555
556     my $library = $builder->build( { source => 'Branch' } );
557     my ($biblionumber_1) = AddBiblio( MARC::Record->new, '' );
558     my $item_1 = $builder->build(
559         {
560             source => 'Item',
561             value  => {
562                 homebranch    => $library->{branchcode},
563                 holdingbranch => $library->{branchcode},
564                 biblionumber  => $biblionumber_1,
565                 itemlost      => 0,
566                 withdrawn     => 0,
567             }
568         }
569     );
570     my $item_2 = $builder->build(
571         {
572             source => 'Item',
573             value  => {
574                 homebranch    => $library->{branchcode},
575                 holdingbranch => $library->{branchcode},
576                 biblionumber  => $biblionumber_1,
577                 itemlost      => 0,
578                 withdrawn     => 0,
579             }
580         }
581     );
582     my ($biblionumber_2) = AddBiblio( MARC::Record->new, '' );
583     my $item_3 = $builder->build(
584         {
585             source => 'Item',
586             value  => {
587                 homebranch    => $library->{branchcode},
588                 holdingbranch => $library->{branchcode},
589                 biblionumber  => $biblionumber_2,
590                 itemlost      => 0,
591                 withdrawn     => 0,
592             }
593         }
594     );
595     my $patron = $builder->build(
596         {
597             source => 'Borrower',
598             value  => { branchcode => $library->{branchcode} }
599         }
600     );
601
602     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
603     my $checkouts = $patron->checkouts;
604     is( $checkouts->count, 0, 'checkouts should not return any issues for that patron' );
605     is( ref($checkouts), 'Koha::Checkouts', 'checkouts should return a Koha::Checkouts object' );
606     my $pending_checkouts = $patron->pending_checkouts;
607     is( $pending_checkouts->count, 0, 'pending_checkouts should not return any issues for that patron' );
608     is( ref($pending_checkouts), 'Koha::Checkouts', 'pending_checkouts should return a Koha::Checkouts object' );
609     my $old_checkouts = $patron->old_checkouts;
610     is( $old_checkouts->count, 0, 'old_checkouts should not return any issues for that patron' );
611     is( ref($old_checkouts), 'Koha::Old::Checkouts', 'old_checkouts should return a Koha::Old::Checkouts object' );
612
613     # Not sure how this is useful, but AddIssue pass this variable to different other subroutines
614     $patron = Koha::Patrons->find( $patron->borrowernumber )->unblessed;
615
616     t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
617
618     AddIssue( $patron, $item_1->{barcode}, DateTime->now->subtract( days => 1 ) );
619     AddIssue( $patron, $item_2->{barcode}, DateTime->now->subtract( days => 5 ) );
620     AddIssue( $patron, $item_3->{barcode} );
621
622     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
623     $checkouts = $patron->checkouts;
624     is( $checkouts->count, 3, 'checkouts should return 3 issues for that patron' );
625     is( ref($checkouts), 'Koha::Checkouts', 'checkouts should return a Koha::Checkouts object' );
626     $pending_checkouts = $patron->pending_checkouts;
627     is( $pending_checkouts->count, 3, 'pending_checkouts should return 3 issues for that patron' );
628     is( ref($pending_checkouts), 'Koha::Checkouts', 'pending_checkouts should return a Koha::Checkouts object' );
629
630     my $first_checkout = $pending_checkouts->next;
631     is( $first_checkout->unblessed_all_relateds->{biblionumber}, $item_3->{biblionumber}, 'pending_checkouts should prefetch values from other tables (here biblio)' );
632
633     my $overdues = $patron->get_overdues;
634     is( $overdues->count, 2, 'Patron should have 2 overdues');
635     is( ref($overdues), 'Koha::Checkouts', 'Koha::Patron->get_overdues should return Koha::Checkouts' );
636     is( $overdues->next->itemnumber, $item_1->{itemnumber}, 'The issue should be returned in the same order as they have been done, first is correct' );
637     is( $overdues->next->itemnumber, $item_2->{itemnumber}, 'The issue should be returned in the same order as they have been done, second is correct' );
638
639
640     C4::Circulation::AddReturn( $item_1->{barcode} );
641     C4::Circulation::AddReturn( $item_2->{barcode} );
642     $old_checkouts = $patron->old_checkouts;
643     is( $old_checkouts->count, 2, 'old_checkouts should return 2 old checkouts that patron' );
644     is( ref($old_checkouts), 'Koha::Old::Checkouts', 'old_checkouts should return a Koha::Old::Checkouts object' );
645
646     # Clean stuffs
647     Koha::Checkouts->search( { borrowernumber => $patron->borrowernumber } )->delete;
648     $patron->delete;
649 };
650
651 subtest 'get_routing_lists' => sub {
652     plan tests => 5;
653
654     my $biblio = Koha::Biblio->new()->store();
655     my $subscription = Koha::Subscription->new({
656         biblionumber => $biblio->biblionumber,
657         }
658     )->store;
659
660     my $patron = $builder->build( { source => 'Borrower' } );
661     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
662
663     is( $patron->get_routing_lists->count, 0, 'Retrieves correct number of routing lists: 0' );
664
665     my $routinglist_count = Koha::Subscription::Routinglists->count;
666     my $routinglist = Koha::Subscription::Routinglist->new({
667         borrowernumber   => $patron->borrowernumber,
668         ranking          => 5,
669         subscriptionid   => $subscription->subscriptionid
670     })->store;
671
672     is ($patron->get_routing_lists->count, 1, "Retrieves correct number of routing lists: 1");
673
674     my $routinglists = $patron->get_routing_lists;
675     is ($routinglists->next->ranking, 5, "Retrieves ranking: 5");
676     is( ref($routinglists),   'Koha::Subscription::Routinglists', 'get_routing_lists returns Koha::Subscription::Routinglists' );
677
678     my $subscription2 = Koha::Subscription->new({
679         biblionumber => $biblio->biblionumber,
680         }
681     )->store;
682     my $routinglist2 = Koha::Subscription::Routinglist->new({
683         borrowernumber   => $patron->borrowernumber,
684         ranking          => 1,
685         subscriptionid   => $subscription2->subscriptionid
686     })->store;
687
688     is ($patron->get_routing_lists->count, 2, "Retrieves correct number of routing lists: 2");
689
690     $patron->delete; # Clean up for later tests
691
692 };
693
694 subtest 'get_age' => sub {
695     plan tests => 7;
696
697     my $patron = $builder->build( { source => 'Borrower' } );
698     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
699
700     my $today = dt_from_string;
701
702     $patron->dateofbirth( undef );
703     is( $patron->get_age, undef, 'get_age should return undef if no dateofbirth is defined' );
704     $patron->dateofbirth( $today->clone->add( years => -12, months => -6, days => -1, end_of_month => 'limit'  ) );
705     is( $patron->get_age, 12, 'Patron should be 12' );
706     $patron->dateofbirth( $today->clone->add( years => -18, months => 0, days => 1, end_of_month => 'limit'  ) );
707     is( $patron->get_age, 17, 'Patron should be 17, happy birthday tomorrow!' );
708     $patron->dateofbirth( $today->clone->add( years => -18, months => 0, days => 0, end_of_month => 'limit'  ) );
709     is( $patron->get_age, 18, 'Patron should be 18' );
710     $patron->dateofbirth( $today->clone->add( years => -18, months => -12, days => -31, end_of_month => 'limit'  ) );
711     is( $patron->get_age, 19, 'Patron should be 19' );
712     $patron->dateofbirth( $today->clone->add( years => -18, months => -12, days => -30, end_of_month => 'limit'  ) );
713     is( $patron->get_age, 19, 'Patron should be 19 again' );
714     $patron->dateofbirth( $today->clone->add( years => 0,   months => -1, days => -1, end_of_month => 'limit'  ) );
715     is( $patron->get_age, 0, 'Patron is a newborn child' );
716
717     $patron->delete;
718 };
719
720 subtest 'is_valid_age' => sub {
721     plan tests => 10;
722
723     my $today = dt_from_string;
724
725     my $category = $builder->build({
726         source => 'Category',
727         value => {
728             categorycode        => 'AGE_5_10',
729             dateofbirthrequired => 5,
730             upperagelimit       => 10
731         }
732     });
733     $category = Koha::Patron::Categories->find( $category->{categorycode} );
734
735     my $patron = $builder->build({
736         source => 'Borrower',
737         value => {
738             categorycode        => 'AGE_5_10'
739         }
740     });
741     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
742
743
744     $patron->dateofbirth( undef );
745     is( $patron->is_valid_age, 1, 'Patron with no dateofbirth is always valid for any category');
746
747     $patron->dateofbirth( $today->clone->add( years => -12, months => -6, days => -1 ) );
748     is( $patron->is_valid_age, 0, 'Patron is 12, so the age is above allowed range 5-10 years');
749
750     $patron->dateofbirth( $today->clone->add( years => -3, months => -6, days => -1 ) );
751     is( $patron->is_valid_age, 0, 'Patron is 3, so the age is below allowed range 5-10 years');
752
753     $patron->dateofbirth( $today->clone->add( years => -7, months => -6, days => -1 ) );
754     is( $patron->is_valid_age, 1, 'Patron is 7, so the age perfectly suits allowed range 5-10 years');
755
756     $patron->dateofbirth( $today->clone->add( years => -5, months => 0, days => 0 ) );
757     is( $patron->is_valid_age, 1, 'Patron celebrates the 5th birthday today, so the age is allowed for this category');
758
759     $patron->dateofbirth( $today->clone->add( years => -5, months => 0, days => 1 ) );
760     is( $patron->is_valid_age, 0, 'Patron will celebrate the 5th birthday tomorrow, so the age is NOT allowed for this category');
761
762     $patron->dateofbirth( $today->clone->add( years => -5, months => 0, days => -1 ) );
763     is( $patron->is_valid_age, 1, 'Patron celebrated the 5th birthday yesterday, so the age is allowed for this category');
764
765     $patron->dateofbirth( $today->clone->add( years => -11, months => 0, days => 0 ) );
766     is( $patron->is_valid_age, 0, 'Patron celebrate the 11th birthday today, so the age is NOT allowed for this category');
767
768     $patron->dateofbirth( $today->clone->add( years => -11, months => 0, days => 1 ) );
769     is( $patron->is_valid_age, 1, 'Patron will celebrate the 11th birthday tomorrow, so the age is allowed for this category');
770
771     $patron->dateofbirth( $today->clone->add( years => -11, months => 0, days => -1 ) );
772     is( $patron->is_valid_age, 0, 'Patron celebrated the 11th birthday yesterday, so the age is NOT allowed for this category');
773
774     $patron->delete;
775     $category->delete;
776 };
777
778 subtest 'account' => sub {
779     plan tests => 1;
780
781     my $patron = $builder->build({source => 'Borrower'});
782
783     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
784     my $account = $patron->account;
785     is( ref($account),   'Koha::Account', 'account should return a Koha::Account object' );
786
787     $patron->delete;
788 };
789
790 subtest 'search_upcoming_membership_expires' => sub {
791     plan tests => 9;
792
793     my $expiry_days = 15;
794     t::lib::Mocks::mock_preference( 'MembershipExpiryDaysNotice', $expiry_days );
795     my $nb_of_days_before = 1;
796     my $nb_of_days_after = 2;
797
798     my $builder = t::lib::TestBuilder->new();
799
800     my $library = $builder->build({ source => 'Branch' });
801
802     # before we add borrowers to this branch, add the expires we have now
803     # note that this pertains to the current mocked setting of the pref
804     # for this reason we add the new branchcode to most of the tests
805     my $nb_of_expires = Koha::Patrons->search_upcoming_membership_expires->count;
806
807     my $patron_1 = $builder->build({
808         source => 'Borrower',
809         value  => {
810             branchcode              => $library->{branchcode},
811             dateexpiry              => dt_from_string->add( days => $expiry_days )
812         },
813     });
814
815     my $patron_2 = $builder->build({
816         source => 'Borrower',
817         value  => {
818             branchcode              => $library->{branchcode},
819             dateexpiry              => dt_from_string->add( days => $expiry_days - $nb_of_days_before )
820         },
821     });
822
823     my $patron_3 = $builder->build({
824         source => 'Borrower',
825         value  => {
826             branchcode              => $library->{branchcode},
827             dateexpiry              => dt_from_string->add( days => $expiry_days + $nb_of_days_after )
828         },
829     });
830
831     # Test without extra parameters
832     my $upcoming_mem_expires = Koha::Patrons->search_upcoming_membership_expires();
833     is( $upcoming_mem_expires->count, $nb_of_expires + 1, 'Get upcoming membership expires should return one new borrower.' );
834
835     # Test with branch
836     $upcoming_mem_expires = Koha::Patrons->search_upcoming_membership_expires({ 'me.branchcode' => $library->{branchcode} });
837     is( $upcoming_mem_expires->count, 1, 'Test with branch parameter' );
838     my $expired = $upcoming_mem_expires->next;
839     is( $expired->surname, $patron_1->{surname}, 'Get upcoming membership expires should return the correct patron.' );
840     is( $expired->library->branchemail, $library->{branchemail}, 'Get upcoming membership expires should return the correct patron.' );
841     is( $expired->branchcode, $patron_1->{branchcode}, 'Get upcoming membership expires should return the correct patron.' );
842
843     t::lib::Mocks::mock_preference( 'MembershipExpiryDaysNotice', 0 );
844     $upcoming_mem_expires = Koha::Patrons->search_upcoming_membership_expires({ 'me.branchcode' => $library->{branchcode} });
845     is( $upcoming_mem_expires->count, 0, 'Get upcoming membership expires with MembershipExpiryDaysNotice==0 should not return new records.' );
846
847     # Test MembershipExpiryDaysNotice == undef
848     t::lib::Mocks::mock_preference( 'MembershipExpiryDaysNotice', undef );
849     $upcoming_mem_expires = Koha::Patrons->search_upcoming_membership_expires({ 'me.branchcode' => $library->{branchcode} });
850     is( $upcoming_mem_expires->count, 0, 'Get upcoming membership expires without MembershipExpiryDaysNotice should not return new records.' );
851
852     # Test the before parameter
853     t::lib::Mocks::mock_preference( 'MembershipExpiryDaysNotice', 15 );
854     $upcoming_mem_expires = Koha::Patrons->search_upcoming_membership_expires({ 'me.branchcode' => $library->{branchcode}, before => $nb_of_days_before });
855     is( $upcoming_mem_expires->count, 2, 'Expect two results for before');
856     # Test after parameter also
857     $upcoming_mem_expires = Koha::Patrons->search_upcoming_membership_expires({ 'me.branchcode' => $library->{branchcode}, before => $nb_of_days_before, after => $nb_of_days_after });
858     is( $upcoming_mem_expires->count, 3, 'Expect three results when adding after' );
859     Koha::Patrons->search({ borrowernumber => { in => [ $patron_1->{borrowernumber}, $patron_2->{borrowernumber}, $patron_3->{borrowernumber} ] } })->delete;
860 };
861
862 subtest 'holds and old_holds' => sub {
863     plan tests => 6;
864
865     my $library = $builder->build( { source => 'Branch' } );
866     my ($biblionumber_1) = AddBiblio( MARC::Record->new, '' );
867     my $item_1 = $builder->build(
868         {
869             source => 'Item',
870             value  => {
871                 homebranch    => $library->{branchcode},
872                 holdingbranch => $library->{branchcode},
873                 biblionumber  => $biblionumber_1
874             }
875         }
876     );
877     my $item_2 = $builder->build(
878         {
879             source => 'Item',
880             value  => {
881                 homebranch    => $library->{branchcode},
882                 holdingbranch => $library->{branchcode},
883                 biblionumber  => $biblionumber_1
884             }
885         }
886     );
887     my ($biblionumber_2) = AddBiblio( MARC::Record->new, '' );
888     my $item_3 = $builder->build(
889         {
890             source => 'Item',
891             value  => {
892                 homebranch    => $library->{branchcode},
893                 holdingbranch => $library->{branchcode},
894                 biblionumber  => $biblionumber_2
895             }
896         }
897     );
898     my $patron = $builder->build(
899         {
900             source => 'Borrower',
901             value  => { branchcode => $library->{branchcode} }
902         }
903     );
904
905     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
906     my $holds = $patron->holds;
907     is( ref($holds), 'Koha::Holds',
908         'Koha::Patron->holds should return a Koha::Holds objects' );
909     is( $holds->count, 0, 'There should not be holds placed by this patron yet' );
910
911     C4::Reserves::AddReserve( $library->{branchcode},
912         $patron->borrowernumber, $biblionumber_1 );
913     # In the future
914     C4::Reserves::AddReserve( $library->{branchcode},
915         $patron->borrowernumber, $biblionumber_2, undef, undef, dt_from_string->add( days => 2 ) );
916
917     $holds = $patron->holds;
918     is( $holds->count, 2, 'There should be 2 holds placed by this patron' );
919
920     my $old_holds = $patron->old_holds;
921     is( ref($old_holds), 'Koha::Old::Holds',
922         'Koha::Patron->old_holds should return a Koha::Old::Holds objects' );
923     is( $old_holds->count, 0, 'There should not be any old holds yet');
924
925     my $hold = $holds->next;
926     $hold->cancel;
927
928     $old_holds = $patron->old_holds;
929     is( $old_holds->count, 1, 'There should  be 1 old (cancelled) hold');
930
931     $old_holds->delete;
932     $holds->delete;
933     $patron->delete;
934 };
935
936 subtest 'notice_email_address' => sub {
937     plan tests => 2;
938
939     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
940
941     t::lib::Mocks::mock_preference( 'AutoEmailPrimaryAddress', 'OFF' );
942     is ($patron->notice_email_address, $patron->email, "Koha::Patron->notice_email_address returns correct value when AutoEmailPrimaryAddress is off");
943
944     t::lib::Mocks::mock_preference( 'AutoEmailPrimaryAddress', 'emailpro' );
945     is ($patron->notice_email_address, $patron->emailpro, "Koha::Patron->notice_email_address returns correct value when AutoEmailPrimaryAddress is emailpro");
946
947     $patron->delete;
948 };
949
950 subtest 'search_patrons_to_anonymise & anonymise_issue_history' => sub {
951     plan tests => 4;
952
953     # TODO create a subroutine in t::lib::Mocks
954     my $branch = $builder->build({ source => 'Branch' });
955     my $userenv_patron = $builder->build_object({
956         class  => 'Koha::Patrons',
957         value  => { branchcode => $branch->{branchcode}, flags => 0 },
958     });
959     t::lib::Mocks::mock_userenv({ patron => $userenv_patron });
960
961     my $anonymous = $builder->build( { source => 'Borrower', }, );
962
963     t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous->{borrowernumber} );
964
965     subtest 'patron privacy is 1 (default)' => sub {
966         plan tests => 9;
967
968         t::lib::Mocks::mock_preference('IndependentBranches', 0);
969         my $patron = $builder->build(
970             {   source => 'Borrower',
971                 value  => { privacy => 1, }
972             }
973         );
974         my $item_1 = $builder->build(
975             {   source => 'Item',
976                 value  => {
977                     itemlost  => 0,
978                     withdrawn => 0,
979                 },
980             }
981         );
982         my $issue_1 = $builder->build(
983             {   source => 'Issue',
984                 value  => {
985                     borrowernumber => $patron->{borrowernumber},
986                     itemnumber     => $item_1->{itemnumber},
987                 },
988             }
989         );
990         my $item_2 = $builder->build(
991             {   source => 'Item',
992                 value  => {
993                     itemlost  => 0,
994                     withdrawn => 0,
995                 },
996             }
997         );
998         my $issue_2 = $builder->build(
999             {   source => 'Issue',
1000                 value  => {
1001                     borrowernumber => $patron->{borrowernumber},
1002                     itemnumber     => $item_2->{itemnumber},
1003                 },
1004             }
1005         );
1006
1007         my ( $returned_1, undef, undef ) = C4::Circulation::AddReturn( $item_1->{barcode}, undef, undef, dt_from_string('2010-10-10') );
1008         my ( $returned_2, undef, undef ) = C4::Circulation::AddReturn( $item_2->{barcode}, undef, undef, dt_from_string('2011-11-11') );
1009         is( $returned_1 && $returned_2, 1, 'The items should have been returned' );
1010
1011         my $patrons_to_anonymise = Koha::Patrons->search_patrons_to_anonymise( { before => '2010-10-11' } )->search( { 'me.borrowernumber' => $patron->{borrowernumber} } );
1012         is( ref($patrons_to_anonymise), 'Koha::Patrons', 'search_patrons_to_anonymise should return Koha::Patrons' );
1013
1014         my $rows_affected = Koha::Patrons->search_patrons_to_anonymise( { before => '2011-11-12' } )->anonymise_issue_history( { before => '2010-10-11' } );
1015         ok( $rows_affected > 0, 'AnonymiseIssueHistory should affect at least 1 row' );
1016
1017         $patrons_to_anonymise = Koha::Patrons->search_patrons_to_anonymise( { before => '2010-10-11' } );
1018         is( $patrons_to_anonymise->count, 0, 'search_patrons_to_anonymise should return 0 after anonymisation is done' );
1019
1020         my $dbh = C4::Context->dbh;
1021         my $sth = $dbh->prepare(q|SELECT borrowernumber FROM old_issues where itemnumber = ?|);
1022         $sth->execute($item_1->{itemnumber});
1023         my ($borrowernumber_used_to_anonymised) = $sth->fetchrow_array;
1024         is( $borrowernumber_used_to_anonymised, $anonymous->{borrowernumber}, 'With privacy=1, the issue should have been anonymised' );
1025         $sth->execute($item_2->{itemnumber});
1026         ($borrowernumber_used_to_anonymised) = $sth->fetchrow_array;
1027         is( $borrowernumber_used_to_anonymised, $patron->{borrowernumber}, 'The issue should not have been anonymised, the returned date is later' );
1028
1029         $rows_affected = Koha::Patrons->search_patrons_to_anonymise( { before => '2011-11-12' } )->anonymise_issue_history;
1030         $sth->execute($item_2->{itemnumber});
1031         ($borrowernumber_used_to_anonymised) = $sth->fetchrow_array;
1032         is( $borrowernumber_used_to_anonymised, $anonymous->{borrowernumber}, 'The issue should have been anonymised, the returned date is before' );
1033
1034         my $sth_reset = $dbh->prepare(q|UPDATE old_issues SET borrowernumber = ? WHERE itemnumber = ?|);
1035         $sth_reset->execute( $patron->{borrowernumber}, $item_1->{itemnumber} );
1036         $sth_reset->execute( $patron->{borrowernumber}, $item_2->{itemnumber} );
1037         $rows_affected = Koha::Patrons->search_patrons_to_anonymise->anonymise_issue_history;
1038         $sth->execute($item_1->{itemnumber});
1039         ($borrowernumber_used_to_anonymised) = $sth->fetchrow_array;
1040         is( $borrowernumber_used_to_anonymised, $anonymous->{borrowernumber}, 'The issue 1 should have been anonymised, before parameter was not passed' );
1041         $sth->execute($item_2->{itemnumber});
1042         ($borrowernumber_used_to_anonymised) = $sth->fetchrow_array;
1043         is( $borrowernumber_used_to_anonymised, $anonymous->{borrowernumber}, 'The issue 2 should have been anonymised, before parameter was not passed' );
1044
1045         Koha::Patrons->find( $patron->{borrowernumber})->delete;
1046     };
1047
1048     subtest 'patron privacy is 0 (forever)' => sub {
1049         plan tests => 2;
1050
1051         t::lib::Mocks::mock_preference('IndependentBranches', 0);
1052         my $patron = $builder->build(
1053             {   source => 'Borrower',
1054                 value  => { privacy => 0, }
1055             }
1056         );
1057         my $item = $builder->build(
1058             {   source => 'Item',
1059                 value  => {
1060                     itemlost  => 0,
1061                     withdrawn => 0,
1062                 },
1063             }
1064         );
1065         my $issue = $builder->build(
1066             {   source => 'Issue',
1067                 value  => {
1068                     borrowernumber => $patron->{borrowernumber},
1069                     itemnumber     => $item->{itemnumber},
1070                 },
1071             }
1072         );
1073
1074         my ( $returned, undef, undef ) = C4::Circulation::AddReturn( $item->{barcode}, undef, undef, dt_from_string('2010-10-10') );
1075         is( $returned, 1, 'The item should have been returned' );
1076
1077         my $dbh = C4::Context->dbh;
1078         my ($borrowernumber_used_to_anonymised) = $dbh->selectrow_array(q|
1079             SELECT borrowernumber FROM old_issues where itemnumber = ?
1080         |, undef, $item->{itemnumber});
1081         is( $borrowernumber_used_to_anonymised, $patron->{borrowernumber}, 'With privacy=0, the issue should not be anonymised' );
1082         Koha::Patrons->find( $patron->{borrowernumber})->delete;
1083     };
1084
1085     t::lib::Mocks::mock_preference( 'AnonymousPatron', '' );
1086
1087     subtest 'AnonymousPatron is not defined' => sub {
1088         plan tests => 3;
1089
1090         t::lib::Mocks::mock_preference('IndependentBranches', 0);
1091         my $patron = $builder->build(
1092             {   source => 'Borrower',
1093                 value  => { privacy => 1, }
1094             }
1095         );
1096         my $item = $builder->build(
1097             {   source => 'Item',
1098                 value  => {
1099                     itemlost  => 0,
1100                     withdrawn => 0,
1101                 },
1102             }
1103         );
1104         my $issue = $builder->build(
1105             {   source => 'Issue',
1106                 value  => {
1107                     borrowernumber => $patron->{borrowernumber},
1108                     itemnumber     => $item->{itemnumber},
1109                 },
1110             }
1111         );
1112
1113         my ( $returned, undef, undef ) = C4::Circulation::AddReturn( $item->{barcode}, undef, undef, dt_from_string('2010-10-10') );
1114         is( $returned, 1, 'The item should have been returned' );
1115         my $rows_affected = Koha::Patrons->search_patrons_to_anonymise( { before => '2010-10-11' } )->anonymise_issue_history( { before => '2010-10-11' } );
1116         ok( $rows_affected > 0, 'AnonymiseIssueHistory should affect at least 1 row' );
1117
1118         my $dbh = C4::Context->dbh;
1119         my ($borrowernumber_used_to_anonymised) = $dbh->selectrow_array(q|
1120             SELECT borrowernumber FROM old_issues where itemnumber = ?
1121         |, undef, $item->{itemnumber});
1122         is( $borrowernumber_used_to_anonymised, undef, 'With AnonymousPatron is not defined, the issue should have been anonymised anyway' );
1123         Koha::Patrons->find( $patron->{borrowernumber})->delete;
1124     };
1125
1126     subtest 'Logged in librarian is not superlibrarian & IndependentBranches' => sub {
1127         plan tests => 1;
1128         t::lib::Mocks::mock_preference( 'IndependentBranches', 1 );
1129         my $patron = $builder->build(
1130             {   source => 'Borrower',
1131                 value  => { privacy => 1 }    # Another branchcode than the logged in librarian
1132             }
1133         );
1134         my $item = $builder->build(
1135             {   source => 'Item',
1136                 value  => {
1137                     itemlost  => 0,
1138                     withdrawn => 0,
1139                 },
1140             }
1141         );
1142         my $issue = $builder->build(
1143             {   source => 'Issue',
1144                 value  => {
1145                     borrowernumber => $patron->{borrowernumber},
1146                     itemnumber     => $item->{itemnumber},
1147                 },
1148             }
1149         );
1150
1151         my ( $returned, undef, undef ) = C4::Circulation::AddReturn( $item->{barcode}, undef, undef, dt_from_string('2010-10-10') );
1152         is( Koha::Patrons->search_patrons_to_anonymise( { before => '2010-10-11' } )->count, 0 );
1153         Koha::Patrons->find( $patron->{borrowernumber})->delete;
1154     };
1155
1156     Koha::Patrons->find( $anonymous->{borrowernumber})->delete;
1157     $userenv_patron->delete;
1158
1159     # Reset IndependentBranches for further tests
1160     t::lib::Mocks::mock_preference('IndependentBranches', 0);
1161 };
1162
1163 subtest 'libraries_where_can_see_patrons + can_see_patron_infos + search_limited' => sub {
1164     plan tests => 3;
1165
1166     # group1
1167     #   + library_11
1168     #   + library_12
1169     # group2
1170     #   + library21
1171     $nb_of_patrons = Koha::Patrons->search->count;
1172     my $group_1 = Koha::Library::Group->new( { title => 'TEST Group 1', ft_hide_patron_info => 1 } )->store;
1173     my $group_2 = Koha::Library::Group->new( { title => 'TEST Group 2', ft_hide_patron_info => 1 } )->store;
1174     my $library_11 = $builder->build( { source => 'Branch' } );
1175     my $library_12 = $builder->build( { source => 'Branch' } );
1176     my $library_21 = $builder->build( { source => 'Branch' } );
1177     $library_11 = Koha::Libraries->find( $library_11->{branchcode} );
1178     $library_12 = Koha::Libraries->find( $library_12->{branchcode} );
1179     $library_21 = Koha::Libraries->find( $library_21->{branchcode} );
1180     Koha::Library::Group->new(
1181         { branchcode => $library_11->branchcode, parent_id => $group_1->id } )->store;
1182     Koha::Library::Group->new(
1183         { branchcode => $library_12->branchcode, parent_id => $group_1->id } )->store;
1184     Koha::Library::Group->new(
1185         { branchcode => $library_21->branchcode, parent_id => $group_2->id } )->store;
1186
1187     my $sth = C4::Context->dbh->prepare(q|INSERT INTO user_permissions( borrowernumber, module_bit, code ) VALUES (?, 4, ?)|); # 4 for borrowers
1188     # 2 patrons from library_11 (group1)
1189     # patron_11_1 see patron's infos from outside its group
1190     # Setting flags => undef to not be considered as superlibrarian
1191     my $patron_11_1 = $builder->build({ source => 'Borrower', value => { branchcode => $library_11->branchcode, flags => undef, }});
1192     $patron_11_1 = Koha::Patrons->find( $patron_11_1->{borrowernumber} );
1193     $sth->execute( $patron_11_1->borrowernumber, 'edit_borrowers' );
1194     $sth->execute( $patron_11_1->borrowernumber, 'view_borrower_infos_from_any_libraries' );
1195     # patron_11_2 can only see patron's info from its group
1196     my $patron_11_2 = $builder->build({ source => 'Borrower', value => { branchcode => $library_11->branchcode, flags => undef, }});
1197     $patron_11_2 = Koha::Patrons->find( $patron_11_2->{borrowernumber} );
1198     $sth->execute( $patron_11_2->borrowernumber, 'edit_borrowers' );
1199     # 1 patron from library_12 (group1)
1200     my $patron_12 = $builder->build({ source => 'Borrower', value => { branchcode => $library_12->branchcode, flags => undef, }});
1201     $patron_12 = Koha::Patrons->find( $patron_12->{borrowernumber} );
1202     # 1 patron from library_21 (group2) can only see patron's info from its group
1203     my $patron_21 = $builder->build({ source => 'Borrower', value => { branchcode => $library_21->branchcode, flags => undef, }});
1204     $patron_21 = Koha::Patrons->find( $patron_21->{borrowernumber} );
1205     $sth->execute( $patron_21->borrowernumber, 'edit_borrowers' );
1206
1207     # Pfiou, we can start now!
1208     subtest 'libraries_where_can_see_patrons' => sub {
1209         plan tests => 3;
1210
1211         my @branchcodes;
1212
1213         t::lib::Mocks::mock_userenv({ patron => $patron_11_1 });
1214         @branchcodes = $patron_11_1->libraries_where_can_see_patrons;
1215         is_deeply( \@branchcodes, [], q|patron_11_1 has view_borrower_infos_from_any_libraries => No restriction| );
1216
1217         t::lib::Mocks::mock_userenv({ patron => $patron_11_2 });
1218         @branchcodes = $patron_11_2->libraries_where_can_see_patrons;
1219         is_deeply( \@branchcodes, [ sort ( $library_11->branchcode, $library_12->branchcode ) ], q|patron_11_2 has not view_borrower_infos_from_any_libraries => Can only see patron's from its group| );
1220
1221         t::lib::Mocks::mock_userenv({ patron => $patron_21 });
1222         @branchcodes = $patron_21->libraries_where_can_see_patrons;
1223         is_deeply( \@branchcodes, [$library_21->branchcode], q|patron_21 has not view_borrower_infos_from_any_libraries => Can only see patron's from its group| );
1224     };
1225     subtest 'can_see_patron_infos' => sub {
1226         plan tests => 6;
1227
1228         t::lib::Mocks::mock_userenv({ patron => $patron_11_1 });
1229         is( $patron_11_1->can_see_patron_infos( $patron_11_2 ), 1, q|patron_11_1 can see patron_11_2, from its library| );
1230         is( $patron_11_1->can_see_patron_infos( $patron_12 ),   1, q|patron_11_1 can see patron_12, from its group| );
1231         is( $patron_11_1->can_see_patron_infos( $patron_21 ),   1, q|patron_11_1 can see patron_11_2, from another group| );
1232
1233         t::lib::Mocks::mock_userenv({ patron => $patron_11_2 });
1234         is( $patron_11_2->can_see_patron_infos( $patron_11_1 ), 1, q|patron_11_2 can see patron_11_1, from its library| );
1235         is( $patron_11_2->can_see_patron_infos( $patron_12 ),   1, q|patron_11_2 can see patron_12, from its group| );
1236         is( $patron_11_2->can_see_patron_infos( $patron_21 ),   0, q|patron_11_2 can NOT see patron_21, from another group| );
1237     };
1238     subtest 'search_limited' => sub {
1239         plan tests => 6;
1240
1241         t::lib::Mocks::mock_userenv({ patron => $patron_11_1 });
1242         my $total_number_of_patrons = $nb_of_patrons + 4; #we added four in these tests
1243         is( Koha::Patrons->search->count, $total_number_of_patrons, 'Non-limited search should return all patrons' );
1244         is( Koha::Patrons->search_limited->count, $total_number_of_patrons, 'patron_11_1 is allowed to see all patrons' );
1245
1246         t::lib::Mocks::mock_userenv({ patron => $patron_11_2 });
1247         is( Koha::Patrons->search->count, $total_number_of_patrons, 'Non-limited search should return all patrons');
1248         is( Koha::Patrons->search_limited->count, 3, 'patron_12_1 is not allowed to see patrons from other groups, only patron_11_1, patron_11_2 and patron_12' );
1249
1250         t::lib::Mocks::mock_userenv({ patron => $patron_21 });
1251         is( Koha::Patrons->search->count, $total_number_of_patrons, 'Non-limited search should return all patrons');
1252         is( Koha::Patrons->search_limited->count, 1, 'patron_21 is not allowed to see patrons from other groups, only himself' );
1253     };
1254     $patron_11_1->delete;
1255     $patron_11_2->delete;
1256     $patron_12->delete;
1257     $patron_21->delete;
1258 };
1259
1260 subtest 'account_locked' => sub {
1261     plan tests => 13;
1262     my $patron = $builder->build({ source => 'Borrower', value => { login_attempts => 0 } });
1263     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
1264     for my $value ( undef, '', 0 ) {
1265         t::lib::Mocks::mock_preference('FailedloginAttempts', $value);
1266         $patron->login_attempts(0)->store;
1267         is( $patron->account_locked, 0, 'Feature is disabled, patron account should not be considered locked' );
1268         $patron->login_attempts(1)->store;
1269         is( $patron->account_locked, 0, 'Feature is disabled, patron account should not be considered locked' );
1270         $patron->login_attempts(-1)->store;
1271         is( $patron->account_locked, 1, 'Feature is disabled but administrative lockout has been triggered' );
1272     }
1273
1274     t::lib::Mocks::mock_preference('FailedloginAttempts', 3);
1275     $patron->login_attempts(2)->store;
1276     is( $patron->account_locked, 0, 'Patron has 2 failed attempts, account should not be considered locked yet' );
1277     $patron->login_attempts(3)->store;
1278     is( $patron->account_locked, 1, 'Patron has 3 failed attempts, account should be considered locked yet' );
1279     $patron->login_attempts(4)->store;
1280     is( $patron->account_locked, 1, 'Patron could not have 4 failed attempts, but account should still be considered locked' );
1281     $patron->login_attempts(-1)->store;
1282     is( $patron->account_locked, 1, 'Administrative lockout triggered' );
1283
1284     $patron->delete;
1285 };
1286
1287 subtest 'is_child | is_adult' => sub {
1288     plan tests => 8;
1289     my $category = $builder->build_object(
1290         {
1291             class => 'Koha::Patron::Categories',
1292             value => { category_type => 'A' }
1293         }
1294     );
1295     my $patron_adult = $builder->build_object(
1296         {
1297             class => 'Koha::Patrons',
1298             value => { categorycode => $category->categorycode }
1299         }
1300     );
1301     $category = $builder->build_object(
1302         {
1303             class => 'Koha::Patron::Categories',
1304             value => { category_type => 'I' }
1305         }
1306     );
1307     my $patron_adult_i = $builder->build_object(
1308         {
1309             class => 'Koha::Patrons',
1310             value => { categorycode => $category->categorycode }
1311         }
1312     );
1313     $category = $builder->build_object(
1314         {
1315             class => 'Koha::Patron::Categories',
1316             value => { category_type => 'C' }
1317         }
1318     );
1319     my $patron_child = $builder->build_object(
1320         {
1321             class => 'Koha::Patrons',
1322             value => { categorycode => $category->categorycode }
1323         }
1324     );
1325     $category = $builder->build_object(
1326         {
1327             class => 'Koha::Patron::Categories',
1328             value => { category_type => 'O' }
1329         }
1330     );
1331     my $patron_other = $builder->build_object(
1332         {
1333             class => 'Koha::Patrons',
1334             value => { categorycode => $category->categorycode }
1335         }
1336     );
1337     is( $patron_adult->is_adult, 1, 'Patron from category A should be considered adult' );
1338     is( $patron_adult_i->is_adult, 1, 'Patron from category I should be considered adult' );
1339     is( $patron_child->is_adult, 0, 'Patron from category C should not be considered adult' );
1340     is( $patron_other->is_adult, 0, 'Patron from category O should not be considered adult' );
1341
1342     is( $patron_adult->is_child, 0, 'Patron from category A should be considered child' );
1343     is( $patron_adult_i->is_child, 0, 'Patron from category I should be considered child' );
1344     is( $patron_child->is_child, 1, 'Patron from category C should not be considered child' );
1345     is( $patron_other->is_child, 0, 'Patron from category O should not be considered child' );
1346
1347     # Clean up
1348     $patron_adult->delete;
1349     $patron_adult_i->delete;
1350     $patron_child->delete;
1351     $patron_other->delete;
1352 };
1353
1354 subtest 'get_overdues' => sub {
1355     plan tests => 7;
1356
1357     my $library = $builder->build( { source => 'Branch' } );
1358     my ($biblionumber_1) = AddBiblio( MARC::Record->new, '' );
1359     my $item_1 = $builder->build(
1360         {
1361             source => 'Item',
1362             value  => {
1363                 homebranch    => $library->{branchcode},
1364                 holdingbranch => $library->{branchcode},
1365                 biblionumber  => $biblionumber_1
1366             }
1367         }
1368     );
1369     my $item_2 = $builder->build(
1370         {
1371             source => 'Item',
1372             value  => {
1373                 homebranch    => $library->{branchcode},
1374                 holdingbranch => $library->{branchcode},
1375                 biblionumber  => $biblionumber_1
1376             }
1377         }
1378     );
1379     my ($biblionumber_2) = AddBiblio( MARC::Record->new, '' );
1380     my $item_3 = $builder->build(
1381         {
1382             source => 'Item',
1383             value  => {
1384                 homebranch    => $library->{branchcode},
1385                 holdingbranch => $library->{branchcode},
1386                 biblionumber  => $biblionumber_2
1387             }
1388         }
1389     );
1390     my $patron = $builder->build(
1391         {
1392             source => 'Borrower',
1393             value  => { branchcode => $library->{branchcode} }
1394         }
1395     );
1396
1397     t::lib::Mocks::mock_preference({ branchcode => $library->{branchcode} });
1398
1399     AddIssue( $patron, $item_1->{barcode}, DateTime->now->subtract( days => 1 ) );
1400     AddIssue( $patron, $item_2->{barcode}, DateTime->now->subtract( days => 5 ) );
1401     AddIssue( $patron, $item_3->{barcode} );
1402
1403     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
1404     my $overdues = $patron->get_overdues;
1405     is( $overdues->count, 2, 'Patron should have 2 overdues');
1406     is( $overdues->next->itemnumber, $item_1->{itemnumber}, 'The issue should be returned in the same order as they have been done, first is correct' );
1407     is( $overdues->next->itemnumber, $item_2->{itemnumber}, 'The issue should be returned in the same order as they have been done, second is correct' );
1408
1409     my $o = $overdues->reset->next;
1410     my $unblessed_overdue = $o->unblessed_all_relateds;
1411     is( exists( $unblessed_overdue->{issuedate} ), 1, 'Fields from the issues table should be filled' );
1412     is( exists( $unblessed_overdue->{itemcallnumber} ), 1, 'Fields from the items table should be filled' );
1413     is( exists( $unblessed_overdue->{title} ), 1, 'Fields from the biblio table should be filled' );
1414     is( exists( $unblessed_overdue->{itemtype} ), 1, 'Fields from the biblioitems table should be filled' );
1415
1416     # Clean stuffs
1417     $patron->checkouts->delete;
1418     $patron->delete;
1419 };
1420
1421 subtest 'userid_is_valid' => sub {
1422     plan tests => 9;
1423
1424     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1425     my $patron_category = $builder->build_object(
1426         {
1427             class => 'Koha::Patron::Categories',
1428             value => { category_type => 'P', enrolmentfee => 0 }
1429         }
1430     );
1431     my %data = (
1432         cardnumber   => "123456789",
1433         firstname    => "Tomasito",
1434         surname      => "None",
1435         categorycode => $patron_category->categorycode,
1436         branchcode   => $library->branchcode,
1437     );
1438
1439     my $expected_userid_patron_1 = 'tomasito.none';
1440     my $borrowernumber = Koha::Patron->new(\%data)->store->borrowernumber;
1441     my $patron_1       = Koha::Patrons->find($borrowernumber);
1442     is( $patron_1->has_valid_userid, 1, "Should be valid when compared against them self" );
1443     is ( $patron_1->userid, $expected_userid_patron_1, 'The userid generated should be the one we expect' );
1444
1445     $patron_1->userid( 'tomasito.non' );
1446     is( $patron_1->has_valid_userid, # FIXME Joubu: What is the difference with the next test?
1447         1, 'recently created userid -> unique (borrowernumber passed)' );
1448
1449     $patron_1->userid( 'tomasitoxxx' );
1450     is( $patron_1->has_valid_userid,
1451         1, 'non-existent userid -> unique (borrowernumber passed)' );
1452     $patron_1->discard_changes; # We compare with the original userid later
1453
1454     my $patron_not_in_storage = Koha::Patron->new( { userid => '' } );
1455     is( $patron_not_in_storage->has_valid_userid,
1456         0, 'userid exists for another patron, patron is not in storage yet' );
1457
1458     $patron_not_in_storage = Koha::Patron->new( { userid => 'tomasitoxxx' } );
1459     is( $patron_not_in_storage->has_valid_userid,
1460         1, 'non-existent userid, patron is not in storage yet' );
1461
1462     # Regression tests for BZ12226
1463     my $db_patron = Koha::Patron->new( { userid => C4::Context->config('user') } );
1464     is( $db_patron->has_valid_userid,
1465         0, 'Koha::Patron->has_valid_userid should return 0 for the DB user (Bug 12226)' );
1466
1467     # Add a new borrower with the same userid but different cardnumber
1468     $data{cardnumber} = "987654321";
1469     my $new_borrowernumber = Koha::Patron->new(\%data)->store->borrowernumber;
1470     my $patron_2 = Koha::Patrons->find($new_borrowernumber);
1471     $patron_2->userid($patron_1->userid);
1472     is( $patron_2->has_valid_userid,
1473         0, 'The userid is already in used, it cannot be used for another patron' );
1474
1475     my $new_userid = 'a_user_id';
1476     $data{cardnumber} = "234567890";
1477     $data{userid}     = 'a_user_id';
1478     $borrowernumber   = Koha::Patron->new(\%data)->store->borrowernumber;
1479     my $patron_3 = Koha::Patrons->find($borrowernumber);
1480     is( $patron_3->userid, $new_userid,
1481         'Koha::Patron->store should insert the given userid' );
1482
1483     # Cleanup
1484     $patron_1->delete;
1485     $patron_2->delete;
1486     $patron_3->delete;
1487 };
1488
1489 subtest 'generate_userid' => sub {
1490     plan tests => 7;
1491
1492     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1493     my $patron_category = $builder->build_object(
1494         {
1495             class => 'Koha::Patron::Categories',
1496             value => { category_type => 'P', enrolmentfee => 0 }
1497         }
1498     );
1499     my %data = (
1500         cardnumber   => "123456789",
1501         firstname    => "Tomasito",
1502         surname      => "None",
1503         categorycode => $patron_category->categorycode,
1504         branchcode   => $library->branchcode,
1505     );
1506
1507     my $expected_userid_patron_1 = 'tomasito.none';
1508     my $new_patron = Koha::Patron->new({ firstname => $data{firstname}, surname => $data{surname} } );
1509     $new_patron->generate_userid;
1510     my $userid = $new_patron->userid;
1511     is( $userid, $expected_userid_patron_1, 'generate_userid should generate the userid we expect' );
1512     my $borrowernumber = Koha::Patron->new(\%data)->store->borrowernumber;
1513     my $patron_1 = Koha::Patrons->find($borrowernumber);
1514     is ( $patron_1->userid, $expected_userid_patron_1, 'The userid generated should be the one we expect' );
1515
1516     $new_patron->generate_userid;
1517     $userid = $new_patron->userid;
1518     is( $userid, $expected_userid_patron_1 . '1', 'generate_userid should generate the userid we expect' );
1519     $data{cardnumber} = '987654321';
1520     my $new_borrowernumber = Koha::Patron->new(\%data)->store->borrowernumber;
1521     my $patron_2 = Koha::Patrons->find($new_borrowernumber);
1522     isnt( $patron_2->userid, 'tomasito',
1523         "Patron with duplicate userid has new userid generated" );
1524     is( $patron_2->userid, $expected_userid_patron_1 . '1', # TODO we could make that configurable
1525         "Patron with duplicate userid has new userid generated (1 is appened" );
1526
1527     $new_patron->generate_userid;
1528     $userid = $new_patron->userid;
1529     is( $userid, $expected_userid_patron_1 . '2', 'generate_userid should generate the userid we expect' );
1530
1531     $patron_1 = Koha::Patrons->find($borrowernumber);
1532     $patron_1->userid(undef);
1533     $patron_1->generate_userid;
1534     $userid = $patron_1->userid;
1535     is( $userid, $expected_userid_patron_1, 'generate_userid should generate the userid we expect' );
1536
1537     # Cleanup
1538     $patron_1->delete;
1539     $patron_2->delete;
1540 };
1541
1542 subtest 'attributes' => sub {
1543     plan tests => 2;
1544
1545     my $library1 = Koha::Library->new({
1546         branchcode => 'LIBPATRON',
1547         branchname => 'Library of testing patron',
1548     })->store;
1549
1550     my $library2 = Koha::Library->new({
1551         branchcode => 'LIBATTR',
1552         branchname => 'Library for testing attribute',
1553     })->store;
1554
1555     my $category = Koha::Patron::Category->new({
1556         categorycode => 'CAT1',
1557         description => 'Category 1',
1558     })->store;
1559
1560     my $patron = Koha::Patron->new({
1561         firstname => 'Patron',
1562         surname => 'with attributes',
1563         branchcode => 'LIBPATRON',
1564         categorycode => 'CAT1',
1565     })->store;
1566
1567     my $attribute_type1 = Koha::Patron::Attribute::Type->new({
1568         code => 'CODE_A',
1569         description => 'Code A desciption',
1570     })->store;
1571
1572     my $attribute_type2 = Koha::Patron::Attribute::Type->new({
1573         code => 'CODE_B',
1574         description => 'Code A desciption',
1575     })->store;
1576
1577     $attribute_type2->library_limits ( [ $library2->branchcode ] );
1578
1579     Koha::Patron::Attribute->new({ borrowernumber => $patron->borrowernumber, code => $attribute_type1->code, attribute => 'value 1' } )->store();
1580     Koha::Patron::Attribute->new({ borrowernumber => $patron->borrowernumber, code => $attribute_type2->code, attribute => 'value 2' } )->store();
1581
1582     is( $patron->attributes->count, 1, 'There should be one attribute');
1583
1584     $attribute_type2->library_limits ( [ $library1->branchcode ] );
1585
1586     is( $patron->attributes->count, 2, 'There should be 2 attributes');
1587
1588     $patron->delete;
1589 };
1590
1591 $nb_of_patrons = Koha::Patrons->search->count;
1592 $retrieved_patron_1->delete;
1593 is( Koha::Patrons->search->count, $nb_of_patrons - 1, 'Delete should have deleted the patron' );
1594
1595 subtest 'BorrowersLog tests' => sub {
1596     plan tests => 4;
1597
1598     t::lib::Mocks::mock_preference( 'BorrowersLog', 1 );
1599     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1600
1601     my $cardnumber = $patron->cardnumber;
1602     $patron->set( { cardnumber => 'TESTCARDNUMBER' });
1603     $patron->store;
1604
1605     my @logs = $schema->resultset('ActionLog')->search( { module => 'MEMBERS', action => 'MODIFY', object => $patron->borrowernumber } );
1606     my $log_info = from_json( $logs[0]->info );
1607     is( $log_info->{cardnumber}->{after}, 'TESTCARDNUMBER', 'Got correct new cardnumber' );
1608     is( $log_info->{cardnumber}->{before}, $cardnumber, 'Got correct old cardnumber' );
1609     is( scalar @logs, 1, 'With BorrowerLogs, one detailed MODIFY action should be logged for the modification.' );
1610
1611     t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', 1 );
1612     $patron->track_login();
1613     @logs = $schema->resultset('ActionLog')->search( { module => 'MEMBERS', action => 'MODIFY', object => $patron->borrowernumber } );
1614     is( scalar @logs, 1, 'With BorrowerLogs and TrackLastPatronActivity we should not spam the logs');
1615 };
1616
1617 $schema->storage->txn_rollback;
1618
1619 subtest 'Test Koha::Patrons::merge' => sub {
1620     plan tests => 110;
1621
1622     my $schema = Koha::Database->new()->schema();
1623
1624     my $resultsets = $Koha::Patron::RESULTSET_PATRON_ID_MAPPING;
1625
1626     $schema->storage->txn_begin;
1627
1628     my $keeper  = $builder->build_object({ class => 'Koha::Patrons' });
1629     my $loser_1 = $builder->build({ source => 'Borrower' })->{borrowernumber};
1630     my $loser_2 = $builder->build({ source => 'Borrower' })->{borrowernumber};
1631
1632     while (my ($r, $field) = each(%$resultsets)) {
1633         $builder->build({ source => $r, value => { $field => $keeper->id } });
1634         $builder->build({ source => $r, value => { $field => $loser_1 } });
1635         $builder->build({ source => $r, value => { $field => $loser_2 } });
1636
1637         my $keeper_rs =
1638           $schema->resultset($r)->search( { $field => $keeper->id } );
1639         is( $keeper_rs->count(), 1, "Found 1 $r rows for keeper" );
1640
1641         my $loser_1_rs =
1642           $schema->resultset($r)->search( { $field => $loser_1 } );
1643         is( $loser_1_rs->count(), 1, "Found 1 $r rows for loser_1" );
1644
1645         my $loser_2_rs =
1646           $schema->resultset($r)->search( { $field => $loser_2 } );
1647         is( $loser_2_rs->count(), 1, "Found 1 $r rows for loser_2" );
1648     }
1649
1650     my $results = $keeper->merge_with([ $loser_1, $loser_2 ]);
1651
1652     while (my ($r, $field) = each(%$resultsets)) {
1653         my $keeper_rs =
1654           $schema->resultset($r)->search( {$field => $keeper->id } );
1655         is( $keeper_rs->count(), 3, "Found 2 $r rows for keeper" );
1656     }
1657
1658     is( Koha::Patrons->find($loser_1), undef, 'Loser 1 has been deleted' );
1659     is( Koha::Patrons->find($loser_2), undef, 'Loser 2 has been deleted' );
1660
1661     $schema->storage->txn_rollback;
1662 };
1663
1664 subtest '->store' => sub {
1665     plan tests => 6;
1666     my $schema = Koha::Database->new->schema;
1667     $schema->storage->txn_begin;
1668
1669     my $print_error = $schema->storage->dbh->{PrintError};
1670     $schema->storage->dbh->{PrintError} = 0; ; # FIXME This does not longer work - because of the transaction in Koha::Patron->store?
1671
1672     my $patron_1 = $builder->build_object({class=> 'Koha::Patrons'});
1673     my $patron_2 = $builder->build_object({class=> 'Koha::Patrons'});
1674
1675     throws_ok
1676         { $patron_2->userid($patron_1->userid)->store; }
1677         'Koha::Exceptions::Object::DuplicateID',
1678         'Koha::Patron->store raises an exception on duplicate ID';
1679
1680     # Test password
1681     t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
1682     my $password = 'password';
1683     $patron_1->set_password({ password => $password });
1684     like( $patron_1->password, qr|^\$2|, 'Password should be hashed using bcrypt (start with $2)' );
1685     my $digest = $patron_1->password;
1686     $patron_1->surname('xxx')->store;
1687     is( $patron_1->password, $digest, 'Password should not have changed on ->store');
1688
1689     # Test uppercasesurname
1690     t::lib::Mocks::mock_preference( 'uppercasesurname', 1 );
1691     my $surname = lc $patron_1->surname;
1692     $patron_1->surname($surname)->store;
1693     isnt( $patron_1->surname, $surname,
1694         'Surname converts to uppercase on store.');
1695     t::lib::Mocks::mock_preference( 'uppercasesurname', 0 );
1696     $patron_1->surname($surname)->store;
1697     is( $patron_1->surname, $surname,
1698         'Surname remains unchanged on store.');
1699
1700     $schema->storage->dbh->{PrintError} = $print_error;
1701     $schema->storage->txn_rollback;
1702
1703     subtest 'skip updated_on for BorrowersLog' => sub {
1704         plan tests => 1;
1705         $schema->storage->txn_begin;
1706         t::lib::Mocks::mock_preference('BorrowersLog', 1);
1707         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1708         $patron->updated_on(dt_from_string($patron->updated_on)->add( seconds => 1 ))->store;
1709         my $logs = Koha::ActionLogs->search({ module =>'MEMBERS', action => 'MODIFY', object => $patron->borrowernumber });
1710         is($logs->count, 0, '->store should not have generated a log for updated_on') or diag 'Log generated:'.Dumper($logs->unblessed);
1711         $schema->storage->txn_rollback;
1712     };
1713 };
1714
1715 subtest '->set_password' => sub {
1716
1717     plan tests => 14;
1718
1719     $schema->storage->txn_begin;
1720
1721     my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { login_attempts => 3 } } );
1722
1723     # Disable logging password changes for this tests
1724     t::lib::Mocks::mock_preference( 'BorrowersLog', 0 );
1725
1726     # Password-length tests
1727     t::lib::Mocks::mock_preference( 'minPasswordLength', undef );
1728     throws_ok { $patron->set_password({ password => 'ab' }); }
1729         'Koha::Exceptions::Password::TooShort',
1730         'minPasswordLength is undef, fall back to 3, fail test';
1731     is( "$@",
1732         'Password length (2) is shorter than required (3)',
1733         'Exception parameters passed correctly'
1734     );
1735
1736     t::lib::Mocks::mock_preference( 'minPasswordLength', 2 );
1737     throws_ok { $patron->set_password({ password => 'ab' }); }
1738         'Koha::Exceptions::Password::TooShort',
1739         'minPasswordLength is 2, fall back to 3, fail test';
1740
1741     t::lib::Mocks::mock_preference( 'minPasswordLength', 5 );
1742     throws_ok { $patron->set_password({ password => 'abcb' }); }
1743         'Koha::Exceptions::Password::TooShort',
1744         'minPasswordLength is 5, fail test';
1745
1746     # Trailing spaces tests
1747     throws_ok { $patron->set_password({ password => 'abcD12d   ' }); }
1748         'Koha::Exceptions::Password::WhitespaceCharacters',
1749         'Password contains trailing spaces, exception is thrown';
1750
1751     # Require strong password tests
1752     t::lib::Mocks::mock_preference( 'RequireStrongPassword', 1 );
1753     throws_ok { $patron->set_password({ password => 'abcd   a' }); }
1754         'Koha::Exceptions::Password::TooWeak',
1755         'Password is too weak, exception is thrown';
1756
1757     # Refresh patron from DB, just to make sure
1758     $patron->discard_changes;
1759     is( $patron->login_attempts, 3, 'Previous tests kept login attemps count' );
1760
1761     $patron->set_password({ password => 'abcD12 34' });
1762     $patron->discard_changes;
1763
1764     is( $patron->login_attempts, 0, 'Changing the password resets the login attempts count' );
1765
1766     lives_ok { $patron->set_password({ password => 'abcd   a', skip_validation => 1 }) }
1767         'Password is weak, but skip_validation was passed, so no exception thrown';
1768
1769     # Completeness
1770     t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
1771     $patron->login_attempts(3)->store;
1772     my $old_digest = $patron->password;
1773     $patron->set_password({ password => 'abcd   a' });
1774     $patron->discard_changes;
1775
1776     isnt( $patron->password, $old_digest, 'Password has been updated' );
1777     ok( checkpw_hash('abcd   a', $patron->password), 'Password hash is correct' );
1778     is( $patron->login_attempts, 0, 'Login attemps have been reset' );
1779
1780     my $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'MEMBERS', action => 'CHANGE PASS', object => $patron->borrowernumber } )->count;
1781     is( $number_of_logs, 0, 'Without BorrowerLogs, Koha::Patron->set_password doesn\'t log password changes' );
1782
1783     # Enable logging password changes
1784     t::lib::Mocks::mock_preference( 'BorrowersLog', 1 );
1785     $patron->set_password({ password => 'abcd   b' });
1786
1787     $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'MEMBERS', action => 'CHANGE PASS', object => $patron->borrowernumber } )->count;
1788     is( $number_of_logs, 1, 'With BorrowerLogs, Koha::Patron->set_password does log password changes' );
1789
1790     $schema->storage->txn_rollback;
1791 };
1792
1793 $schema->storage->txn_begin;
1794 subtest 'search_unsubscribed' => sub {
1795     plan tests => 4;
1796
1797     t::lib::Mocks::mock_preference( 'FailedLoginAttempts', 3 );
1798     t::lib::Mocks::mock_preference( 'UnsubscribeReflectionDelay', '' );
1799     is( Koha::Patrons->search_unsubscribed->count, 0, 'Empty delay should return empty set' );
1800
1801     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
1802     my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
1803
1804     t::lib::Mocks::mock_preference( 'UnsubscribeReflectionDelay', 0 );
1805     Koha::Patron::Consents->delete; # for correct counts
1806     Koha::Patron::Consent->new({ borrowernumber => $patron1->borrowernumber, type => 'GDPR_PROCESSING',  refused_on => dt_from_string })->store;
1807     is( Koha::Patrons->search_unsubscribed->count, 1, 'Find patron1' );
1808
1809     # Add another refusal but shift the period
1810     t::lib::Mocks::mock_preference( 'UnsubscribeReflectionDelay', 2 );
1811     Koha::Patron::Consent->new({ borrowernumber => $patron2->borrowernumber, type => 'GDPR_PROCESSING',  refused_on => dt_from_string->subtract(days=>2) })->store;
1812     is( Koha::Patrons->search_unsubscribed->count, 1, 'Find patron2 only' );
1813
1814     # Try another (special) attempts setting
1815     t::lib::Mocks::mock_preference( 'FailedLoginAttempts', 0 );
1816     # Lockout is now disabled
1817     # Patron2 still matches: refused earlier, not locked
1818     is( Koha::Patrons->search_unsubscribed->count, 1, 'Lockout disabled' );
1819 };
1820
1821 subtest 'search_anonymize_candidates' => sub {
1822     plan tests => 5;
1823     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
1824     my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
1825     $patron1->anonymized(0);
1826     $patron1->dateexpiry( dt_from_string->add(days => 1) )->store;
1827     $patron2->anonymized(0);
1828     $patron2->dateexpiry( dt_from_string->add(days => 1) )->store;
1829
1830     t::lib::Mocks::mock_preference( 'PatronAnonymizeDelay', q{} );
1831     is( Koha::Patrons->search_anonymize_candidates->count, 0, 'Empty set' );
1832
1833     t::lib::Mocks::mock_preference( 'PatronAnonymizeDelay', 0 );
1834     my $cnt = Koha::Patrons->search_anonymize_candidates->count;
1835     $patron1->dateexpiry( dt_from_string->subtract(days => 1) )->store;
1836     $patron2->dateexpiry( dt_from_string->subtract(days => 3) )->store;
1837     is( Koha::Patrons->search_anonymize_candidates->count, $cnt+2, 'Delay 0' );
1838
1839     t::lib::Mocks::mock_preference( 'PatronAnonymizeDelay', 2 );
1840     $patron1->dateexpiry( dt_from_string->add(days => 1) )->store;
1841     $patron2->dateexpiry( dt_from_string->add(days => 1) )->store;
1842     $cnt = Koha::Patrons->search_anonymize_candidates->count;
1843     $patron1->dateexpiry( dt_from_string->subtract(days => 1) )->store;
1844     $patron2->dateexpiry( dt_from_string->subtract(days => 3) )->store;
1845     is( Koha::Patrons->search_anonymize_candidates->count, $cnt+1, 'Delay 2' );
1846
1847     t::lib::Mocks::mock_preference( 'PatronAnonymizeDelay', 4 );
1848     $patron1->dateexpiry( dt_from_string->add(days => 1) )->store;
1849     $patron2->dateexpiry( dt_from_string->add(days => 1) )->store;
1850     $cnt = Koha::Patrons->search_anonymize_candidates->count;
1851     $patron1->dateexpiry( dt_from_string->subtract(days => 1) )->store;
1852     $patron2->dateexpiry( dt_from_string->subtract(days => 3) )->store;
1853     is( Koha::Patrons->search_anonymize_candidates->count, $cnt, 'Delay 4' );
1854
1855     t::lib::Mocks::mock_preference( 'FailedLoginAttempts', 3 );
1856     $patron1->dateexpiry( dt_from_string->subtract(days => 5) )->store;
1857     $patron1->login_attempts(0)->store;
1858     $patron2->dateexpiry( dt_from_string->subtract(days => 5) )->store;
1859     $patron2->login_attempts(0)->store;
1860     $cnt = Koha::Patrons->search_anonymize_candidates({locked => 1})->count;
1861     $patron1->login_attempts(3)->store;
1862     is( Koha::Patrons->search_anonymize_candidates({locked => 1})->count,
1863         $cnt+1, 'Locked flag' );
1864 };
1865
1866 subtest 'search_anonymized' => sub {
1867     plan tests => 3;
1868     my $patron1 = $builder->build_object( { class => 'Koha::Patrons' } );
1869
1870     t::lib::Mocks::mock_preference( 'PatronRemovalDelay', q{} );
1871     is( Koha::Patrons->search_anonymized->count, 0, 'Empty set' );
1872
1873     t::lib::Mocks::mock_preference( 'PatronRemovalDelay', 1 );
1874     $patron1->dateexpiry( dt_from_string );
1875     $patron1->anonymized(0)->store;
1876     my $cnt = Koha::Patrons->search_anonymized->count;
1877     $patron1->anonymized(1)->store;
1878     is( Koha::Patrons->search_anonymized->count, $cnt, 'Number unchanged' );
1879     $patron1->dateexpiry( dt_from_string->subtract(days => 1) )->store;
1880     is( Koha::Patrons->search_anonymized->count, $cnt+1, 'Found patron1' );
1881 };
1882
1883 subtest 'lock' => sub {
1884     plan tests => 8;
1885
1886     my $patron1 = $builder->build_object( { class => 'Koha::Patrons' } );
1887     my $patron2 = $builder->build_object( { class => 'Koha::Patrons' } );
1888     my $hold = $builder->build_object({
1889         class => 'Koha::Holds',
1890         value => { borrowernumber => $patron1->borrowernumber },
1891     });
1892
1893     t::lib::Mocks::mock_preference( 'FailedLoginAttempts', 3 );
1894     my $expiry = dt_from_string->add(days => 1);
1895     $patron1->dateexpiry( $expiry );
1896     $patron1->lock;
1897     is( $patron1->login_attempts, Koha::Patron::ADMINISTRATIVE_LOCKOUT, 'Check login_attempts' );
1898     is( $patron1->dateexpiry, $expiry, 'Not expired yet' );
1899     is( $patron1->holds->count, 1, 'No holds removed' );
1900
1901     $patron1->lock({ expire => 1, remove => 1});
1902     isnt( $patron1->dateexpiry, $expiry, 'Expiry date adjusted' );
1903     is( $patron1->holds->count, 0, 'Holds removed' );
1904
1905     # Disable lockout feature
1906     t::lib::Mocks::mock_preference( 'FailedLoginAttempts', q{} );
1907     $patron1->login_attempts(0);
1908     $patron1->dateexpiry( $expiry );
1909     $patron1->store;
1910     $patron1->lock;
1911     is( $patron1->login_attempts, Koha::Patron::ADMINISTRATIVE_LOCKOUT, 'Check login_attempts' );
1912
1913     # Trivial wrapper test (Koha::Patrons->lock)
1914     $patron1->login_attempts(0)->store;
1915     Koha::Patrons->search({ borrowernumber => [ $patron1->borrowernumber, $patron2->borrowernumber ] })->lock;
1916     $patron1->discard_changes; # refresh
1917     $patron2->discard_changes;
1918     is( $patron1->login_attempts, Koha::Patron::ADMINISTRATIVE_LOCKOUT, 'Check login_attempts patron 1' );
1919     is( $patron2->login_attempts, Koha::Patron::ADMINISTRATIVE_LOCKOUT, 'Check login_attempts patron 2' );
1920 };
1921
1922 subtest 'anonymize' => sub {
1923     plan tests => 10;
1924
1925     my $patron1 = $builder->build_object( { class => 'Koha::Patrons' } );
1926     my $patron2 = $builder->build_object( { class => 'Koha::Patrons' } );
1927
1928     # First try patron with issues
1929     my $issue = $builder->build_object({ class => 'Koha::Checkouts', value => { borrowernumber => $patron2->borrowernumber } });
1930     warning_like { $patron2->anonymize } qr/still has issues/, 'Skip patron with issues';
1931     $issue->delete;
1932
1933     t::lib::Mocks::mock_preference( 'BorrowerMandatoryField', 'surname|email|cardnumber' );
1934     my $surname = $patron1->surname; # expect change, no clear
1935     my $branchcode = $patron1->branchcode; # expect skip
1936     $patron1->anonymize;
1937     is($patron1->anonymized, 1, 'Check flag' );
1938
1939     is( $patron1->dateofbirth, undef, 'Birth date cleared' );
1940     is( $patron1->firstname, undef, 'First name cleared' );
1941     isnt( $patron1->surname, $surname, 'Surname changed' );
1942     ok( $patron1->surname =~ /^\w{10}$/, 'Mandatory surname randomized' );
1943     is( $patron1->branchcode, $branchcode, 'Branch code skipped' );
1944     is( $patron1->email, undef, 'Email was mandatory, must be cleared' );
1945
1946     # Test wrapper in Koha::Patrons
1947     $patron1->surname($surname)->store; # restore
1948     my $rs = Koha::Patrons->search({ borrowernumber => [ $patron1->borrowernumber, $patron2->borrowernumber ] })->anonymize;
1949     $patron1->discard_changes; # refresh
1950     isnt( $patron1->surname, $surname, 'Surname patron1 changed again' );
1951     $patron2->discard_changes; # refresh
1952     is( $patron2->firstname, undef, 'First name patron2 cleared' );
1953 };
1954 $schema->storage->txn_rollback;