Bug 18292: Remove return 1 statements in tests
[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 => 22;
23 use Test::Warn;
24 use Time::Fake;
25 use DateTime;
26
27 use C4::Biblio;
28 use C4::Circulation;
29 use C4::Members;
30 use C4::Circulation;
31
32 use Koha::Holds;
33 use Koha::Patron;
34 use Koha::Patrons;
35 use Koha::Database;
36 use Koha::DateUtils;
37 use Koha::Virtualshelves;
38
39 use t::lib::TestBuilder;
40 use t::lib::Mocks;
41
42 my $schema = Koha::Database->new->schema;
43 $schema->storage->txn_begin;
44
45 my $builder       = t::lib::TestBuilder->new;
46 my $library = $builder->build({source => 'Branch' });
47 my $category = $builder->build({source => 'Category' });
48 my $nb_of_patrons = Koha::Patrons->search->count;
49 my $new_patron_1  = Koha::Patron->new(
50     {   cardnumber => 'test_cn_1',
51         branchcode => $library->{branchcode},
52         categorycode => $category->{categorycode},
53         surname => 'surname for patron1',
54         firstname => 'firstname for patron1',
55         userid => 'a_nonexistent_userid_1',
56     }
57 )->store;
58 my $new_patron_2  = Koha::Patron->new(
59     {   cardnumber => 'test_cn_2',
60         branchcode => $library->{branchcode},
61         categorycode => $category->{categorycode},
62         surname => 'surname for patron2',
63         firstname => 'firstname for patron2',
64         userid => 'a_nonexistent_userid_2',
65     }
66 )->store;
67
68 C4::Context->_new_userenv('xxx');
69 C4::Context->set_userenv(0,0,0,'firstname','surname', $library->{branchcode}, 'Midway Public Library', '', '', '');
70
71 is( Koha::Patrons->search->count, $nb_of_patrons + 2, 'The 2 patrons should have been added' );
72
73 my $retrieved_patron_1 = Koha::Patrons->find( $new_patron_1->borrowernumber );
74 is( $retrieved_patron_1->cardnumber, $new_patron_1->cardnumber, 'Find a patron by borrowernumber should return the correct patron' );
75
76 subtest 'library' => sub {
77     plan tests => 2;
78     is( $retrieved_patron_1->library->branchcode, $library->{branchcode}, 'Koha::Patron->library should return the correct library' );
79     is( ref($retrieved_patron_1->library), 'Koha::Library', 'Koha::Patron->library should return a Koha::Library object' );
80 };
81
82 subtest 'guarantees' => sub {
83     plan tests => 8;
84     my $guarantees = $new_patron_1->guarantees;
85     is( ref($guarantees), 'Koha::Patrons', 'Koha::Patron->guarantees should return a Koha::Patrons result set in a scalar context' );
86     is( $guarantees->count, 0, 'new_patron_1 should have 0 guarantee' );
87     my @guarantees = $new_patron_1->guarantees;
88     is( ref(\@guarantees), 'ARRAY', 'Koha::Patron->guarantees should return an array in a list context' );
89     is( scalar(@guarantees), 0, 'new_patron_1 should have 0 guarantee' );
90
91     my $guarantee_1 = $builder->build({ source => 'Borrower', value => { guarantorid => $new_patron_1->borrowernumber }});
92     my $guarantee_2 = $builder->build({ source => 'Borrower', value => { guarantorid => $new_patron_1->borrowernumber }});
93
94     $guarantees = $new_patron_1->guarantees;
95     is( ref($guarantees), 'Koha::Patrons', 'Koha::Patron->guarantees should return a Koha::Patrons result set in a scalar context' );
96     is( $guarantees->count, 2, 'new_patron_1 should have 2 guarantees' );
97     @guarantees = $new_patron_1->guarantees;
98     is( ref(\@guarantees), 'ARRAY', 'Koha::Patron->guarantees should return an array in a list context' );
99     is( scalar(@guarantees), 2, 'new_patron_1 should have 2 guarantees' );
100     $_->delete for @guarantees;
101 };
102
103 subtest 'category' => sub {
104     plan tests => 2;
105     my $patron_category = $new_patron_1->category;
106     is( ref( $patron_category), 'Koha::Patron::Category', );
107     is( $patron_category->categorycode, $category->{categorycode}, );
108 };
109
110 subtest 'siblings' => sub {
111     plan tests => 7;
112     my $siblings = $new_patron_1->siblings;
113     is( $siblings, undef, 'Koha::Patron->siblings should not crashed if the patron has no guarantor' );
114     my $guarantee_1 = $builder->build( { source => 'Borrower', value => { guarantorid => $new_patron_1->borrowernumber } } );
115     my $retrieved_guarantee_1 = Koha::Patrons->find($guarantee_1);
116     $siblings = $retrieved_guarantee_1->siblings;
117     is( ref($siblings), 'Koha::Patrons', 'Koha::Patron->siblings should return a Koha::Patrons result set in a scalar context' );
118     my @siblings = $retrieved_guarantee_1->siblings;
119     is( ref( \@siblings ), 'ARRAY', 'Koha::Patron->siblings should return an array in a list context' );
120     is( $siblings->count,  0,       'guarantee_1 should not have siblings yet' );
121     my $guarantee_2 = $builder->build( { source => 'Borrower', value => { guarantorid => $new_patron_1->borrowernumber } } );
122     my $guarantee_3 = $builder->build( { source => 'Borrower', value => { guarantorid => $new_patron_1->borrowernumber } } );
123     $siblings = $retrieved_guarantee_1->siblings;
124     is( $siblings->count,               2,                               'guarantee_1 should have 2 siblings' );
125     is( $guarantee_2->{borrowernumber}, $siblings->next->borrowernumber, 'guarantee_2 should exist in the guarantees' );
126     is( $guarantee_3->{borrowernumber}, $siblings->next->borrowernumber, 'guarantee_3 should exist in the guarantees' );
127     $_->delete for $retrieved_guarantee_1->siblings;
128     $retrieved_guarantee_1->delete;
129 };
130
131 subtest 'has_overdues' => sub {
132     plan tests => 3;
133
134     my $biblioitem_1 = $builder->build( { source => 'Biblioitem' } );
135     my $item_1 = $builder->build(
136         {   source => 'Item',
137             value  => {
138                 homebranch    => $library->{branchcode},
139                 holdingbranch => $library->{branchcode},
140                 notforloan    => 0,
141                 itemlost      => 0,
142                 withdrawn     => 0,
143                 biblionumber  => $biblioitem_1->{biblionumber}
144             }
145         }
146     );
147     my $retrieved_patron = Koha::Patrons->find( $new_patron_1->borrowernumber );
148     is( $retrieved_patron->has_overdues, 0, );
149
150     my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 );
151     my $issue = Koha::Checkout->new({ borrowernumber => $new_patron_1->id, itemnumber => $item_1->{itemnumber}, date_due => $tomorrow, branchcode => $library->{branchcode} })->store();
152     is( $retrieved_patron->has_overdues, 0, );
153     $issue->delete();
154     my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 );
155     $issue = Koha::Checkout->new({ borrowernumber => $new_patron_1->id, itemnumber => $item_1->{itemnumber}, date_due => $yesterday, branchcode => $library->{branchcode} })->store();
156     $retrieved_patron = Koha::Patrons->find( $new_patron_1->borrowernumber );
157     is( $retrieved_patron->has_overdues, 1, );
158     $issue->delete();
159 };
160
161 subtest 'update_password' => sub {
162     plan tests => 7;
163
164     t::lib::Mocks::mock_preference( 'BorrowersLog', 1 );
165     my $original_userid   = $new_patron_1->userid;
166     my $original_password = $new_patron_1->password;
167     warning_like { $retrieved_patron_1->update_password( $new_patron_2->userid, 'another_password' ) }
168     qr{Duplicate entry},
169       'Koha::Patron->update_password should warn if the userid is already used by another patron';
170     is( Koha::Patrons->find( $new_patron_1->borrowernumber )->userid,   $original_userid,   'Koha::Patron->update_password should not have updated the userid' );
171     is( Koha::Patrons->find( $new_patron_1->borrowernumber )->password, $original_password, 'Koha::Patron->update_password should not have updated the userid' );
172
173     $retrieved_patron_1->update_password( 'another_nonexistent_userid_1', 'another_password' );
174     is( Koha::Patrons->find( $new_patron_1->borrowernumber )->userid,   'another_nonexistent_userid_1', 'Koha::Patron->update_password should have updated the userid' );
175     is( Koha::Patrons->find( $new_patron_1->borrowernumber )->password, 'another_password',             'Koha::Patron->update_password should have updated the password' );
176
177     my $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'MEMBERS', action => 'CHANGE PASS', object => $new_patron_1->borrowernumber } )->count;
178     is( $number_of_logs, 1, 'With BorrowerLogs, Koha::Patron->update_password should have logged' );
179
180     t::lib::Mocks::mock_preference( 'BorrowersLog', 0 );
181     $retrieved_patron_1->update_password( 'yet_another_nonexistent_userid_1', 'another_password' );
182     $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'MEMBERS', action => 'CHANGE PASS', object => $new_patron_1->borrowernumber } )->count;
183     is( $number_of_logs, 1, 'With BorrowerLogs, Koha::Patron->update_password should not have logged' );
184 };
185
186 subtest 'is_expired' => sub {
187     plan tests => 5;
188     my $patron = $builder->build({ source => 'Borrower' });
189     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
190     $patron->dateexpiry( undef )->store->discard_changes;
191     is( $patron->is_expired, 0, 'Patron should not be considered expired if dateexpiry is not set');
192     $patron->dateexpiry( '0000-00-00' )->store->discard_changes;
193     is( $patron->is_expired, 0, 'Patron should not be considered expired if dateexpiry is not 0000-00-00');
194     $patron->dateexpiry( dt_from_string )->store->discard_changes;
195     is( $patron->is_expired, 0, 'Patron should not be considered expired if dateexpiry is today');
196     $patron->dateexpiry( dt_from_string->add( days => 1 ) )->store->discard_changes;
197     is( $patron->is_expired, 0, 'Patron should not be considered expired if dateexpiry is tomorrow');
198     $patron->dateexpiry( dt_from_string->add( days => -1 ) )->store->discard_changes;
199     is( $patron->is_expired, 1, 'Patron should be considered expired if dateexpiry is yesterday');
200
201     $patron->delete;
202 };
203
204 subtest 'is_going_to_expire' => sub {
205     plan tests => 9;
206     my $patron = $builder->build({ source => 'Borrower' });
207     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
208     $patron->dateexpiry( undef )->store->discard_changes;
209     is( $patron->is_going_to_expire, 0, 'Patron should not be considered going to expire if dateexpiry is not set');
210     $patron->dateexpiry( '0000-00-00' )->store->discard_changes;
211     is( $patron->is_going_to_expire, 0, 'Patron should not be considered going to expire if dateexpiry is not 0000-00-00');
212
213     t::lib::Mocks::mock_preference('NotifyBorrowerDeparture', 0);
214     $patron->dateexpiry( dt_from_string )->store->discard_changes;
215     is( $patron->is_going_to_expire, 0, 'Patron should not be considered going to expire if dateexpiry is today');
216
217     $patron->dateexpiry( dt_from_string )->store->discard_changes;
218     is( $patron->is_going_to_expire, 0, 'Patron should not be considered going to expire if dateexpiry is today and pref is 0');
219
220     t::lib::Mocks::mock_preference('NotifyBorrowerDeparture', 10);
221     $patron->dateexpiry( dt_from_string->add( days => 11 ) )->store->discard_changes;
222     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');
223
224     t::lib::Mocks::mock_preference('NotifyBorrowerDeparture', 0);
225     $patron->dateexpiry( dt_from_string->add( days => 10 ) )->store->discard_changes;
226     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');
227
228     t::lib::Mocks::mock_preference('NotifyBorrowerDeparture', 10);
229     $patron->dateexpiry( dt_from_string->add( days => 10 ) )->store->discard_changes;
230     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');
231     $patron->delete;
232
233     t::lib::Mocks::mock_preference('NotifyBorrowerDeparture', 10);
234     $patron->dateexpiry( dt_from_string->add( days => 20 ) )->store->discard_changes;
235     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');
236
237     t::lib::Mocks::mock_preference('NotifyBorrowerDeparture', 20);
238     $patron->dateexpiry( dt_from_string->add( days => 10 ) )->store->discard_changes;
239     is( $patron->is_going_to_expire, 1, 'Patron should be considered going to expire if dateexpiry is 10 days ahead and pref is 20');
240
241     $patron->delete;
242 };
243
244
245 subtest 'renew_account' => sub {
246     plan tests => 30;
247
248     for my $date ( '2016-03-31', '2016-11-30', dt_from_string() ) {
249         my $dt = dt_from_string( $date, 'iso' );
250         Time::Fake->offset( $dt->epoch );
251         my $a_month_ago                = $dt->clone->subtract( months => 1, end_of_month => 'limit' )->truncate( to => 'day' );
252         my $a_year_later               = $dt->clone->add( months => 12, end_of_month => 'limit' )->truncate( to => 'day' );
253         my $a_year_later_minus_a_month = $dt->clone->add( months => 11, end_of_month => 'limit' )->truncate( to => 'day' );
254         my $a_month_later              = $dt->clone->add( months => 1 , end_of_month => 'limit' )->truncate( to => 'day' );
255         my $a_year_later_plus_a_month  = $dt->clone->add( months => 13, end_of_month => 'limit' )->truncate( to => 'day' );
256         my $patron_category = $builder->build(
257             {   source => 'Category',
258                 value  => {
259                     enrolmentperiod     => 12,
260                     enrolmentperioddate => undef,
261                 }
262             }
263         );
264         my $patron = $builder->build(
265             {   source => 'Borrower',
266                 value  => {
267                     dateexpiry   => $a_month_ago,
268                     categorycode => $patron_category->{categorycode},
269                 }
270             }
271         );
272         my $patron_2 = $builder->build(
273             {  source => 'Borrower',
274                value  => {
275                    dateexpiry => $a_month_ago,
276                    categorycode => $patron_category->{categorycode},
277                 }
278             }
279         );
280         my $patron_3 = $builder->build(
281             {  source => 'Borrower',
282                value  => {
283                    dateexpiry => $a_month_later,
284                    categorycode => $patron_category->{categorycode},
285                }
286             }
287         );
288         my $retrieved_patron = Koha::Patrons->find( $patron->{borrowernumber} );
289         my $retrieved_patron_2 = Koha::Patrons->find( $patron_2->{borrowernumber} );
290         my $retrieved_patron_3 = Koha::Patrons->find( $patron_3->{borrowernumber} );
291
292         t::lib::Mocks::mock_preference( 'BorrowerRenewalPeriodBase', 'dateexpiry' );
293         t::lib::Mocks::mock_preference( 'BorrowersLog',              1 );
294         my $expiry_date = $retrieved_patron->renew_account;
295         is( $expiry_date, $a_year_later_minus_a_month, "$a_month_ago + 12 months must be $a_year_later_minus_a_month" );
296         my $retrieved_expiry_date = Koha::Patrons->find( $patron->{borrowernumber} )->dateexpiry;
297         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" );
298         my $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'MEMBERS', action => 'RENEW', object => $retrieved_patron->borrowernumber } )->count;
299         is( $number_of_logs, 1, 'With BorrowerLogs, Koha::Patron->renew_account should have logged' );
300
301         t::lib::Mocks::mock_preference( 'BorrowerRenewalPeriodBase', 'now' );
302         t::lib::Mocks::mock_preference( 'BorrowersLog',              0 );
303         $expiry_date = $retrieved_patron->renew_account;
304         is( $expiry_date, $a_year_later, "today + 12 months must be $a_year_later" );
305         $retrieved_expiry_date = Koha::Patrons->find( $patron->{borrowernumber} )->dateexpiry;
306         is( dt_from_string($retrieved_expiry_date), $a_year_later, "today + 12 months must be $a_year_later" );
307         $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'MEMBERS', action => 'RENEW', object => $retrieved_patron->borrowernumber } )->count;
308         is( $number_of_logs, 1, 'Without BorrowerLogs, Koha::Patron->renew_account should not have logged' );
309
310         t::lib::Mocks::mock_preference( 'BorrowerRenewalPeriodBase', 'combination' );
311         $expiry_date = $retrieved_patron_2->renew_account;
312         is( $expiry_date, $a_year_later, "today + 12 months must be $a_year_later" );
313         $retrieved_expiry_date = Koha::Patrons->find( $patron_2->{borrowernumber} )->dateexpiry;
314         is( dt_from_string($retrieved_expiry_date), $a_year_later, "today + 12 months must be $a_year_later" );
315
316         $expiry_date = $retrieved_patron_3->renew_account;
317         is( $expiry_date, $a_year_later_plus_a_month, "$a_month_later + 12 months must be $a_year_later_plus_a_month" );
318         $retrieved_expiry_date = Koha::Patrons->find( $patron_3->{borrowernumber} )->dateexpiry;
319         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" );
320
321         $retrieved_patron->delete;
322         $retrieved_patron_2->delete;
323         $retrieved_patron_3->delete;
324     }
325     Time::Fake->reset;
326 };
327
328 subtest "move_to_deleted" => sub {
329     plan tests => 5;
330     my $originally_updated_on = '2016-01-01 12:12:12';
331     my $patron = $builder->build( { source => 'Borrower',value => { updated_on => $originally_updated_on } } );
332     my $retrieved_patron = Koha::Patrons->find( $patron->{borrowernumber} );
333     is( ref( $retrieved_patron->move_to_deleted ), 'Koha::Schema::Result::Deletedborrower', 'Koha::Patron->move_to_deleted should return the Deleted patron' )
334       ;    # FIXME This should be Koha::Deleted::Patron
335     my $deleted_patron = $schema->resultset('Deletedborrower')
336         ->search( { borrowernumber => $patron->{borrowernumber} }, { result_class => 'DBIx::Class::ResultClass::HashRefInflator' } )
337         ->next;
338     ok( $retrieved_patron->updated_on, 'updated_on should be set for borrowers table' );
339     ok( $deleted_patron->{updated_on}, 'updated_on should be set for deleted_borrowers table' );
340     isnt( $deleted_patron->{updated_on}, $retrieved_patron->updated_on, 'Koha::Patron->move_to_deleted should have correctly updated the updated_on column');
341     $deleted_patron->{updated_on} = $originally_updated_on; #reset for simplicity in comparing all other fields
342     is_deeply( $deleted_patron, $patron, 'Koha::Patron->move_to_deleted should have correctly moved the patron to the deleted table' );
343     $retrieved_patron->delete( $patron->{borrowernumber} );    # Cleanup
344 };
345
346 subtest "delete" => sub {
347     plan tests => 5;
348     t::lib::Mocks::mock_preference( 'BorrowersLog', 1 );
349     my $patron           = $builder->build( { source => 'Borrower' } );
350     my $retrieved_patron = Koha::Patrons->find( $patron->{borrowernumber} );
351     my $hold             = $builder->build(
352         {   source => 'Reserve',
353             value  => { borrowernumber => $patron->{borrowernumber} }
354         }
355     );
356     my $list = $builder->build(
357         {   source => 'Virtualshelve',
358             value  => { owner => $patron->{borrowernumber} }
359         }
360     );
361
362     my $deleted = $retrieved_patron->delete;
363     is( $deleted, 1, 'Koha::Patron->delete should return 1 if the patron has been correctly deleted' );
364
365     is( Koha::Patrons->find( $patron->{borrowernumber} ), undef, 'Koha::Patron->delete should have deleted the patron' );
366
367     is( Koha::Holds->search( { borrowernumber => $patron->{borrowernumber} } )->count, 0, q|Koha::Patron->delete should have deleted patron's holds| );
368
369     is( Koha::Virtualshelves->search( { owner => $patron->{borrowernumber} } )->count, 0, q|Koha::Patron->delete should have deleted patron's lists| );
370
371     my $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'MEMBERS', action => 'DELETE', object => $retrieved_patron->borrowernumber } )->count;
372     is( $number_of_logs, 1, 'With BorrowerLogs, Koha::Patron->delete should have logged' );
373 };
374
375 subtest 'add_enrolment_fee_if_needed' => sub {
376     plan tests => 4;
377
378     my $enrolmentfee_K  = 5;
379     my $enrolmentfee_J  = 10;
380     my $enrolmentfee_YA = 20;
381
382     my $dbh = C4::Context->dbh;
383     $dbh->do(q|UPDATE categories set enrolmentfee=? where categorycode=?|, undef, $enrolmentfee_K, 'K');
384     $dbh->do(q|UPDATE categories set enrolmentfee=? where categorycode=?|, undef, $enrolmentfee_J, 'J');
385     $dbh->do(q|UPDATE categories set enrolmentfee=? where categorycode=?|, undef, $enrolmentfee_YA, 'YA');
386
387     my %borrower_data = (
388         firstname    => 'my firstname',
389         surname      => 'my surname',
390         categorycode => 'K',
391         branchcode   => $library->{branchcode},
392     );
393
394     my $borrowernumber = C4::Members::AddMember(%borrower_data);
395     $borrower_data{borrowernumber} = $borrowernumber;
396
397     my ($total) = C4::Members::GetMemberAccountRecords($borrowernumber);
398     is( $total, $enrolmentfee_K, "New kid pay $enrolmentfee_K" );
399
400     t::lib::Mocks::mock_preference( 'FeeOnChangePatronCategory', 0 );
401     $borrower_data{categorycode} = 'J';
402     C4::Members::ModMember(%borrower_data);
403     ($total) = C4::Members::GetMemberAccountRecords($borrowernumber);
404     is( $total, $enrolmentfee_K, "Kid growing and become a juvenile, but shouldn't pay for the upgrade " );
405
406     $borrower_data{categorycode} = 'K';
407     C4::Members::ModMember(%borrower_data);
408     t::lib::Mocks::mock_preference( 'FeeOnChangePatronCategory', 1 );
409
410     $borrower_data{categorycode} = 'J';
411     C4::Members::ModMember(%borrower_data);
412     ($total) = C4::Members::GetMemberAccountRecords($borrowernumber);
413     is( $total, $enrolmentfee_K + $enrolmentfee_J, "Kid growing and become a juvenile, they should pay " . ( $enrolmentfee_K + $enrolmentfee_J ) );
414
415     # Check with calling directly Koha::Patron->get_enrolment_fee_if_needed
416     my $patron = Koha::Patrons->find($borrowernumber);
417     $patron->categorycode('YA')->store;
418     my $fee = $patron->add_enrolment_fee_if_needed;
419     ($total) = C4::Members::GetMemberAccountRecords($borrowernumber);
420     is( $total,
421         $enrolmentfee_K + $enrolmentfee_J + $enrolmentfee_YA,
422         "Juvenile growing and become an young adult, they should pay " . ( $enrolmentfee_K + $enrolmentfee_J + $enrolmentfee_YA )
423     );
424
425     $patron->delete;
426 };
427
428 subtest 'checkouts + get_overdues' => sub {
429     plan tests => 8;
430
431     my $library = $builder->build( { source => 'Branch' } );
432     my ($biblionumber_1) = AddBiblio( MARC::Record->new, '' );
433     my $item_1 = $builder->build(
434         {
435             source => 'Item',
436             value  => {
437                 homebranch    => $library->{branchcode},
438                 holdingbranch => $library->{branchcode},
439                 biblionumber  => $biblionumber_1
440             }
441         }
442     );
443     my $item_2 = $builder->build(
444         {
445             source => 'Item',
446             value  => {
447                 homebranch    => $library->{branchcode},
448                 holdingbranch => $library->{branchcode},
449                 biblionumber  => $biblionumber_1
450             }
451         }
452     );
453     my ($biblionumber_2) = AddBiblio( MARC::Record->new, '' );
454     my $item_3 = $builder->build(
455         {
456             source => 'Item',
457             value  => {
458                 homebranch    => $library->{branchcode},
459                 holdingbranch => $library->{branchcode},
460                 biblionumber  => $biblionumber_2
461             }
462         }
463     );
464     my $patron = $builder->build(
465         {
466             source => 'Borrower',
467             value  => { branchcode => $library->{branchcode} }
468         }
469     );
470
471     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
472     my $checkouts = $patron->checkouts;
473     is( $checkouts->count, 0, 'checkouts should not return any issues for that patron' );
474     is( ref($checkouts), 'Koha::Checkouts', 'checkouts should return a Koha::Checkouts object' );
475
476     # Not sure how this is useful, but AddIssue pass this variable to different other subroutines
477     $patron = Koha::Patrons->find( $patron->borrowernumber )->unblessed;
478
479     my $module = new Test::MockModule('C4::Context');
480     $module->mock( 'userenv', sub { { branch => $library->{branchcode} } } );
481
482     AddIssue( $patron, $item_1->{barcode}, DateTime->now->subtract( days => 1 ) );
483     AddIssue( $patron, $item_2->{barcode}, DateTime->now->subtract( days => 5 ) );
484     AddIssue( $patron, $item_3->{barcode} );
485
486     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
487     $checkouts = $patron->checkouts;
488     is( $checkouts->count, 3, 'checkouts should return 3 issues for that patron' );
489     is( ref($checkouts), 'Koha::Checkouts', 'checkouts should return a Koha::Checkouts object' );
490
491     my $overdues = $patron->get_overdues;
492     is( $overdues->count, 2, 'Patron should have 2 overdues');
493     is( ref($overdues), 'Koha::Checkouts', 'Koha::Patron->get_overdues should return Koha::Checkouts' );
494     is( $overdues->next->itemnumber, $item_1->{itemnumber}, 'The issue should be returned in the same order as they have been done, first is correct' );
495     is( $overdues->next->itemnumber, $item_2->{itemnumber}, 'The issue should be returned in the same order as they have been done, second is correct' );
496
497     # Clean stuffs
498     Koha::Checkouts->search( { borrowernumber => $patron->borrowernumber } )->delete;
499     $patron->delete;
500     $module->unmock('userenv');
501 };
502
503 subtest 'get_age' => sub {
504     plan tests => 7;
505
506     my $patron = $builder->build( { source => 'Borrower' } );
507     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
508
509     my $today = dt_from_string;
510
511     $patron->dateofbirth( undef );
512     is( $patron->get_age, undef, 'get_age should return undef if no dateofbirth is defined' );
513     $patron->dateofbirth( $today->clone->add( years => -12, months => -6, days => -1 ) );
514     is( $patron->get_age, 12, 'Patron should be 12' );
515     $patron->dateofbirth( $today->clone->add( years => -18, months => 0, days => 1 ) );
516     is( $patron->get_age, 17, 'Patron should be 17, happy birthday tomorrow!' );
517     $patron->dateofbirth( $today->clone->add( years => -18, months => 0, days => 0 ) );
518     is( $patron->get_age, 18, 'Patron should be 18' );
519     $patron->dateofbirth( $today->clone->add( years => -18, months => -12, days => -31 ) );
520     is( $patron->get_age, 19, 'Patron should be 19' );
521     $patron->dateofbirth( $today->clone->add( years => -18, months => -12, days => -30 ) );
522     is( $patron->get_age, 19, 'Patron should be 19 again' );
523     $patron->dateofbirth( $today->clone->add( years => 0,   months => -1, days => -1 ) );
524     is( $patron->get_age, 0, 'Patron is a newborn child' );
525
526     $patron->delete;
527 };
528
529 subtest 'account' => sub {
530     plan tests => 1;
531
532     my $patron = $builder->build({source => 'Borrower'});
533
534     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
535     my $account = $patron->account;
536     is( ref($account),   'Koha::Account', 'account should return a Koha::Account object' );
537
538     $patron->delete;
539 };
540
541 subtest 'search_upcoming_membership_expires' => sub {
542     plan tests => 9;
543
544     my $expiry_days = 15;
545     t::lib::Mocks::mock_preference( 'MembershipExpiryDaysNotice', $expiry_days );
546     my $nb_of_days_before = 1;
547     my $nb_of_days_after = 2;
548
549     my $builder = t::lib::TestBuilder->new();
550
551     my $library = $builder->build({ source => 'Branch' });
552
553     # before we add borrowers to this branch, add the expires we have now
554     # note that this pertains to the current mocked setting of the pref
555     # for this reason we add the new branchcode to most of the tests
556     my $nb_of_expires = Koha::Patrons->search_upcoming_membership_expires->count;
557
558     my $patron_1 = $builder->build({
559         source => 'Borrower',
560         value  => {
561             branchcode              => $library->{branchcode},
562             dateexpiry              => dt_from_string->add( days => $expiry_days )
563         },
564     });
565
566     my $patron_2 = $builder->build({
567         source => 'Borrower',
568         value  => {
569             branchcode              => $library->{branchcode},
570             dateexpiry              => dt_from_string->add( days => $expiry_days - $nb_of_days_before )
571         },
572     });
573
574     my $patron_3 = $builder->build({
575         source => 'Borrower',
576         value  => {
577             branchcode              => $library->{branchcode},
578             dateexpiry              => dt_from_string->add( days => $expiry_days + $nb_of_days_after )
579         },
580     });
581
582     # Test without extra parameters
583     my $upcoming_mem_expires = Koha::Patrons->search_upcoming_membership_expires();
584     is( $upcoming_mem_expires->count, $nb_of_expires + 1, 'Get upcoming membership expires should return one new borrower.' );
585
586     # Test with branch
587     $upcoming_mem_expires = Koha::Patrons->search_upcoming_membership_expires({ 'me.branchcode' => $library->{branchcode} });
588     is( $upcoming_mem_expires->count, 1, 'Test with branch parameter' );
589     my $expired = $upcoming_mem_expires->next;
590     is( $expired->surname, $patron_1->{surname}, 'Get upcoming membership expires should return the correct patron.' );
591     is( $expired->library->branchemail, $library->{branchemail}, 'Get upcoming membership expires should return the correct patron.' );
592     is( $expired->branchcode, $patron_1->{branchcode}, 'Get upcoming membership expires should return the correct patron.' );
593
594     t::lib::Mocks::mock_preference( 'MembershipExpiryDaysNotice', 0 );
595     $upcoming_mem_expires = Koha::Patrons->search_upcoming_membership_expires({ 'me.branchcode' => $library->{branchcode} });
596     is( $upcoming_mem_expires->count, 0, 'Get upcoming membership expires with MembershipExpiryDaysNotice==0 should not return new records.' );
597
598     # Test MembershipExpiryDaysNotice == undef
599     t::lib::Mocks::mock_preference( 'MembershipExpiryDaysNotice', undef );
600     $upcoming_mem_expires = Koha::Patrons->search_upcoming_membership_expires({ 'me.branchcode' => $library->{branchcode} });
601     is( $upcoming_mem_expires->count, 0, 'Get upcoming membership expires without MembershipExpiryDaysNotice should not return new records.' );
602
603     # Test the before parameter
604     t::lib::Mocks::mock_preference( 'MembershipExpiryDaysNotice', 15 );
605     $upcoming_mem_expires = Koha::Patrons->search_upcoming_membership_expires({ 'me.branchcode' => $library->{branchcode}, before => $nb_of_days_before });
606     is( $upcoming_mem_expires->count, 2, 'Expect two results for before');
607     # Test after parameter also
608     $upcoming_mem_expires = Koha::Patrons->search_upcoming_membership_expires({ 'me.branchcode' => $library->{branchcode}, before => $nb_of_days_before, after => $nb_of_days_after });
609     is( $upcoming_mem_expires->count, 3, 'Expect three results when adding after' );
610     Koha::Patrons->search({ borrowernumber => { in => [ $patron_1->{borrowernumber}, $patron_2->{borrowernumber}, $patron_3->{borrowernumber} ] } })->delete;
611 };
612
613 subtest 'holds' => sub {
614     plan tests => 3;
615
616     my $library = $builder->build( { source => 'Branch' } );
617     my ($biblionumber_1) = AddBiblio( MARC::Record->new, '' );
618     my $item_1 = $builder->build(
619         {
620             source => 'Item',
621             value  => {
622                 homebranch    => $library->{branchcode},
623                 holdingbranch => $library->{branchcode},
624                 biblionumber  => $biblionumber_1
625             }
626         }
627     );
628     my $item_2 = $builder->build(
629         {
630             source => 'Item',
631             value  => {
632                 homebranch    => $library->{branchcode},
633                 holdingbranch => $library->{branchcode},
634                 biblionumber  => $biblionumber_1
635             }
636         }
637     );
638     my ($biblionumber_2) = AddBiblio( MARC::Record->new, '' );
639     my $item_3 = $builder->build(
640         {
641             source => 'Item',
642             value  => {
643                 homebranch    => $library->{branchcode},
644                 holdingbranch => $library->{branchcode},
645                 biblionumber  => $biblionumber_2
646             }
647         }
648     );
649     my $patron = $builder->build(
650         {
651             source => 'Borrower',
652             value  => { branchcode => $library->{branchcode} }
653         }
654     );
655
656     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
657     my $holds = $patron->holds;
658     is( ref($holds), 'Koha::Holds',
659         'Koha::Patron->holds should return a Koha::Holds objects' );
660     is( $holds->count, 0, 'There should not be holds placed by this patron yet' );
661
662     C4::Reserves::AddReserve( $library->{branchcode},
663         $patron->borrowernumber, $biblionumber_1 );
664     # In the future
665     C4::Reserves::AddReserve( $library->{branchcode},
666         $patron->borrowernumber, $biblionumber_2, undef, undef, dt_from_string->add( days => 2 ) );
667
668     $holds = $patron->holds;
669     is( $holds->count, 2, 'There should be 2 holds placed by this patron' );
670
671     $holds->delete;
672     $patron->delete;
673 };
674
675 subtest 'search_patrons_to_anonymise & anonymise_issue_history' => sub {
676     plan tests => 4;
677
678     # TODO create a subroutine in t::lib::Mocks
679     my $branch = $builder->build({ source => 'Branch' });
680     my $userenv_patron = $builder->build({
681         source => 'Borrower',
682         value  => { branchcode => $branch->{branchcode} },
683     });
684     C4::Context->_new_userenv('DUMMY SESSION');
685     C4::Context->set_userenv(
686         $userenv_patron->{borrowernumber},
687         $userenv_patron->{userid},
688         'usercnum', 'First name', 'Surname',
689         $branch->{branchcode},
690         $branch->{branchname},
691         0,
692     );
693     my $anonymous = $builder->build( { source => 'Borrower', }, );
694
695     t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous->{borrowernumber} );
696
697     subtest 'patron privacy is 1 (default)' => sub {
698         plan tests => 8;
699
700         t::lib::Mocks::mock_preference('IndependentBranches', 0);
701         my $patron = $builder->build(
702             {   source => 'Borrower',
703                 value  => { privacy => 1, }
704             }
705         );
706         my $item_1 = $builder->build(
707             {   source => 'Item',
708                 value  => {
709                     itemlost  => 0,
710                     withdrawn => 0,
711                 },
712             }
713         );
714         my $issue_1 = $builder->build(
715             {   source => 'Issue',
716                 value  => {
717                     borrowernumber => $patron->{borrowernumber},
718                     itemnumber     => $item_1->{itemnumber},
719                 },
720             }
721         );
722         my $item_2 = $builder->build(
723             {   source => 'Item',
724                 value  => {
725                     itemlost  => 0,
726                     withdrawn => 0,
727                 },
728             }
729         );
730         my $issue_2 = $builder->build(
731             {   source => 'Issue',
732                 value  => {
733                     borrowernumber => $patron->{borrowernumber},
734                     itemnumber     => $item_2->{itemnumber},
735                 },
736             }
737         );
738
739         my ( $returned_1, undef, undef ) = C4::Circulation::AddReturn( $item_1->{barcode}, undef, undef, undef, '2010-10-10' );
740         my ( $returned_2, undef, undef ) = C4::Circulation::AddReturn( $item_2->{barcode}, undef, undef, undef, '2011-11-11' );
741         is( $returned_1 && $returned_2, 1, 'The items should have been returned' );
742
743         my $patrons_to_anonymise = Koha::Patrons->search_patrons_to_anonymise( { before => '2010-10-11' } )->search( { 'me.borrowernumber' => $patron->{borrowernumber} } );
744         is( ref($patrons_to_anonymise), 'Koha::Patrons', 'search_patrons_to_anonymise should return Koha::Patrons' );
745
746         my $rows_affected = Koha::Patrons->search_patrons_to_anonymise( { before => '2011-11-12' } )->anonymise_issue_history( { before => '2010-10-11' } );
747         ok( $rows_affected > 0, 'AnonymiseIssueHistory should affect at least 1 row' );
748
749         my $dbh = C4::Context->dbh;
750         my $sth = $dbh->prepare(q|SELECT borrowernumber FROM old_issues where itemnumber = ?|);
751         $sth->execute($item_1->{itemnumber});
752         my ($borrowernumber_used_to_anonymised) = $sth->fetchrow_array;
753         is( $borrowernumber_used_to_anonymised, $anonymous->{borrowernumber}, 'With privacy=1, the issue should have been anonymised' );
754         $sth->execute($item_2->{itemnumber});
755         ($borrowernumber_used_to_anonymised) = $sth->fetchrow_array;
756         is( $borrowernumber_used_to_anonymised, $patron->{borrowernumber}, 'The issue should not have been anonymised, the returned date is later' );
757
758         $rows_affected = Koha::Patrons->search_patrons_to_anonymise( { before => '2011-11-12' } )->anonymise_issue_history;
759         $sth->execute($item_2->{itemnumber});
760         ($borrowernumber_used_to_anonymised) = $sth->fetchrow_array;
761         is( $borrowernumber_used_to_anonymised, $anonymous->{borrowernumber}, 'The issue should have been anonymised, the returned date is before' );
762
763         my $sth_reset = $dbh->prepare(q|UPDATE old_issues SET borrowernumber = ? WHERE itemnumber = ?|);
764         $sth_reset->execute( $patron->{borrowernumber}, $item_1->{itemnumber} );
765         $sth_reset->execute( $patron->{borrowernumber}, $item_2->{itemnumber} );
766         $rows_affected = Koha::Patrons->search_patrons_to_anonymise->anonymise_issue_history;
767         $sth->execute($item_1->{itemnumber});
768         ($borrowernumber_used_to_anonymised) = $sth->fetchrow_array;
769         is( $borrowernumber_used_to_anonymised, $anonymous->{borrowernumber}, 'The issue 1 should have been anonymised, before parameter was not passed' );
770         $sth->execute($item_2->{itemnumber});
771         ($borrowernumber_used_to_anonymised) = $sth->fetchrow_array;
772         is( $borrowernumber_used_to_anonymised, $anonymous->{borrowernumber}, 'The issue 2 should have been anonymised, before parameter was not passed' );
773
774         Koha::Patrons->find( $patron->{borrowernumber})->delete;
775     };
776
777     subtest 'patron privacy is 0 (forever)' => sub {
778         plan tests => 3;
779
780         t::lib::Mocks::mock_preference('IndependentBranches', 0);
781         my $patron = $builder->build(
782             {   source => 'Borrower',
783                 value  => { privacy => 0, }
784             }
785         );
786         my $item = $builder->build(
787             {   source => 'Item',
788                 value  => {
789                     itemlost  => 0,
790                     withdrawn => 0,
791                 },
792             }
793         );
794         my $issue = $builder->build(
795             {   source => 'Issue',
796                 value  => {
797                     borrowernumber => $patron->{borrowernumber},
798                     itemnumber     => $item->{itemnumber},
799                 },
800             }
801         );
802
803         my ( $returned, undef, undef ) = C4::Circulation::AddReturn( $item->{barcode}, undef, undef, undef, '2010-10-10' );
804         is( $returned, 1, 'The item should have been returned' );
805         my $rows_affected = Koha::Patrons->search_patrons_to_anonymise( { before => '2010-10-11' } )->anonymise_issue_history( { before => '2010-10-11' } );
806         ok( $rows_affected > 0, 'AnonymiseIssueHistory should not return any error if success' );
807
808         my $dbh = C4::Context->dbh;
809         my ($borrowernumber_used_to_anonymised) = $dbh->selectrow_array(q|
810             SELECT borrowernumber FROM old_issues where itemnumber = ?
811         |, undef, $item->{itemnumber});
812         is( $borrowernumber_used_to_anonymised, $patron->{borrowernumber}, 'With privacy=0, the issue should not be anonymised' );
813         Koha::Patrons->find( $patron->{borrowernumber})->delete;
814     };
815
816     t::lib::Mocks::mock_preference( 'AnonymousPatron', '' );
817
818     subtest 'AnonymousPatron is not defined' => sub {
819         plan tests => 3;
820
821         t::lib::Mocks::mock_preference('IndependentBranches', 0);
822         my $patron = $builder->build(
823             {   source => 'Borrower',
824                 value  => { privacy => 1, }
825             }
826         );
827         my $item = $builder->build(
828             {   source => 'Item',
829                 value  => {
830                     itemlost  => 0,
831                     withdrawn => 0,
832                 },
833             }
834         );
835         my $issue = $builder->build(
836             {   source => 'Issue',
837                 value  => {
838                     borrowernumber => $patron->{borrowernumber},
839                     itemnumber     => $item->{itemnumber},
840                 },
841             }
842         );
843
844         my ( $returned, undef, undef ) = C4::Circulation::AddReturn( $item->{barcode}, undef, undef, undef, '2010-10-10' );
845         is( $returned, 1, 'The item should have been returned' );
846         my $rows_affected = Koha::Patrons->search_patrons_to_anonymise( { before => '2010-10-11' } )->anonymise_issue_history( { before => '2010-10-11' } );
847         ok( $rows_affected > 0, 'AnonymiseIssueHistory should affect at least 1 row' );
848
849         my $dbh = C4::Context->dbh;
850         my ($borrowernumber_used_to_anonymised) = $dbh->selectrow_array(q|
851             SELECT borrowernumber FROM old_issues where itemnumber = ?
852         |, undef, $item->{itemnumber});
853         is( $borrowernumber_used_to_anonymised, undef, 'With AnonymousPatron is not defined, the issue should have been anonymised anyway' );
854         Koha::Patrons->find( $patron->{borrowernumber})->delete;
855     };
856
857     subtest 'Logged in librarian is not superlibrarian & IndependentBranches' => sub {
858         plan tests => 1;
859         t::lib::Mocks::mock_preference( 'IndependentBranches', 1 );
860         my $patron = $builder->build(
861             {   source => 'Borrower',
862                 value  => { privacy => 1 }    # Another branchcode than the logged in librarian
863             }
864         );
865         my $item = $builder->build(
866             {   source => 'Item',
867                 value  => {
868                     itemlost  => 0,
869                     withdrawn => 0,
870                 },
871             }
872         );
873         my $issue = $builder->build(
874             {   source => 'Issue',
875                 value  => {
876                     borrowernumber => $patron->{borrowernumber},
877                     itemnumber     => $item->{itemnumber},
878                 },
879             }
880         );
881
882         my ( $returned, undef, undef ) = C4::Circulation::AddReturn( $item->{barcode}, undef, undef, undef, '2010-10-10' );
883         is( Koha::Patrons->search_patrons_to_anonymise( { before => '2010-10-11' } )->count, 0 );
884         Koha::Patrons->find( $patron->{borrowernumber})->delete;
885     };
886
887     Koha::Patrons->find( $anonymous->{borrowernumber})->delete;
888     Koha::Patrons->find( $userenv_patron->{borrowernumber})->delete;
889 };
890
891 subtest 'account_locked' => sub {
892     plan tests => 8;
893     my $patron = $builder->build({ source => 'Borrower', value => { login_attempts => 0 } });
894     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
895     for my $value ( undef, '', 0 ) {
896         t::lib::Mocks::mock_preference('FailedloginAttempts', $value);
897         is( $patron->account_locked, 0, 'Feature is disabled, patron account should not be considered locked' );
898         $patron->login_attempts(1)->store;
899         is( $patron->account_locked, 0, 'Feature is disabled, patron account should not be considered locked' );
900     }
901
902     t::lib::Mocks::mock_preference('FailedloginAttempts', 3);
903     $patron->login_attempts(2)->store;
904     is( $patron->account_locked, 0, 'Patron has 2 failed attempts, account should not be considered locked yet' );
905     $patron->login_attempts(3)->store;
906     is( $patron->account_locked, 1, 'Patron has 3 failed attempts, account should be considered locked yet' );
907
908     $patron->delete;
909 };
910
911 $retrieved_patron_1->delete;
912 is( Koha::Patrons->search->count, $nb_of_patrons + 1, 'Delete should have deleted the patron' );
913
914 $schema->storage->txn_rollback;
915