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