]> git.koha-community.org Git - koha.git/blob - t/db_dependent/Koha/Patron.t
Bug 33837: Replace days_inclusive by min_days
[koha.git] / t / db_dependent / Koha / Patron.t
1 #!/usr/bin/perl
2
3 # Copyright 2019 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 => 28;
23 use Test::Exception;
24 use Test::Warn;
25
26 use Koha::CirculationRules;
27 use Koha::Database;
28 use Koha::DateUtils qw(dt_from_string);
29 use Koha::ArticleRequests;
30 use Koha::Patrons;
31 use Koha::Patron::Relationships;
32 use C4::Circulation qw( AddIssue AddReturn );
33
34 use t::lib::TestBuilder;
35 use t::lib::Mocks;
36
37 my $schema  = Koha::Database->new->schema;
38 my $builder = t::lib::TestBuilder->new;
39
40 subtest 'Accessor tests' => sub {
41     plan tests => 9;
42     $schema->storage->txn_begin;
43
44     my $object = Koha::Patron->new( { surname => 'Test Patron' } );
45     is( $object->surname(), 'Test Patron', "Accessor returns correct value" );
46     $object->surname('Test Patron Surname');
47     is( $object->surname(), 'Test Patron Surname', "Accessor returns correct value after set" );
48
49     my $object2 = Koha::Patron->new( { surname => 'Test Patron 2' } );
50     is( $object2->surname(), 'Test Patron 2', "Accessor returns correct value" );
51     $object2->surname('Test Patron Surname 2');
52     is( $object2->surname(), 'Test Patron Surname 2', "Accessor returns correct value after set" );
53
54     my $ret;
55     $ret = $object2->set( { surname => "Test Patron Surname 3", firstname => "Test Firstname" } );
56     ok( ref($ret) eq 'Koha::Patron', "Set returns object on success" );
57     is( $object2->surname(),   "Test Patron Surname 3", "Set sets first field correctly" );
58     is( $object2->firstname(), "Test Firstname",        "Set sets second field correctly" );
59
60     our $patron = Koha::Patron->new(
61         {
62             borrowernumber      => '12345',
63             cardnumber          => '1234567890',
64             surname             => 'mySurname',
65             firstname           => 'myFirstname',
66             title               => 'Mr.',
67             othernames          => 'myOthernames',
68             initials            => 'MM',
69             streetnumber        => '100',
70             streettype          => 'Blvd',
71             address             => 'my personal address',
72             address2            => 'my adress2',
73             city                => 'Marseille',
74             state               => 'mystate',
75             zipcode             => '13006',
76             country             => 'France',
77             email               => 'mySurname.myFirstname@email.com',
78             phone               => '0402872934',
79             mobile              => '0627884632',
80             fax                 => '0402872935',
81             emailpro            => 'myEmailPro@email.com',
82             phonepro            => '0402873334',
83             B_streetnumber      => '101',
84             B_streettype        => 'myB_streettype',
85             B_address           => 'myB_address',
86             B_address2          => 'myB_address2',
87             B_city              => 'myB_city',
88             B_state             => 'myB_state',
89             B_zipcode           => '23456',
90             B_country           => 'myB_country',
91             B_email             => 'myB_email',
92             B_phone             => '0678353935',
93             dateofbirth         => '1990-07-16',
94             branchcode          => 'myBranCode',
95             categorycode        => 'myCatCode',
96             dateenrolled        => '2015-03-19',
97             dateexpiry          => '2016-03-19',
98             gonenoaddress       => '0',
99             lost                => '0',
100             debarred            => '2015-04-19',
101             debarredcomment     => 'You are debarred',
102             borrowernotes       => 'borrowernotes',
103             sex                 => 'M',
104             password            => 'hfkurhfe976634èj!',
105             flags               => '55555',
106             userid              => '87987',
107             opacnote            => 'myOpacnote',
108             contactnote         => 'myContactnote',
109             sort1               => 'mySort1',
110             sort2               => 'mySort2',
111             altcontactfirstname => 'myAltcontactfirstname',
112             altcontactsurname   => 'myAltcontactsurname',
113             altcontactaddress1  => 'myAltcontactaddress1',
114             altcontactaddress2  => 'myAltcontactaddress2',
115             altcontactaddress3  => 'myAltcontactaddress3',
116             altcontactstate     => 'myAltcontactstate',
117             altcontactzipcode   => '465843',
118             altcontactcountry   => 'myOtherCountry',
119             altcontactphone     => 'myOtherphone',
120             smsalertnumber      => '0683027346',
121             privacy             => '667788',
122         }
123     );
124
125     subtest 'Accessor tests after new' => sub {
126         plan tests => 60;
127         is( $patron->borrowernumber, '12345',               'borrowernumber accessor returns correct value' );
128         is( $patron->cardnumber,     '1234567890',          'cardnumber accessor returns correct value' );
129         is( $patron->surname,        'mySurname',           'surname accessor returns correct value' );
130         is( $patron->firstname,      'myFirstname',         'firstname accessor returns correct value' );
131         is( $patron->title,          'Mr.',                 'title accessor returns correct value' );
132         is( $patron->othernames,     'myOthernames',        'othernames accessor returns correct value' );
133         is( $patron->initials,       'MM',                  'initials accessor returns correct value' );
134         is( $patron->streetnumber,   '100',                 'streetnumber accessor returns correct value' );
135         is( $patron->streettype,     'Blvd',                'streettype accessor returns correct value' );
136         is( $patron->address,        'my personal address', 'address accessor returns correct value' );
137         is( $patron->address2,       'my adress2',          'address2 accessor returns correct value' );
138         is( $patron->city,           'Marseille',           'city accessor returns correct value' );
139         is( $patron->state,          'mystate',             'state accessor returns correct value' );
140         is( $patron->zipcode,        '13006',               'zipcode accessor returns correct value' );
141         is( $patron->country,        'France',              'country accessor returns correct value' );
142         is( $patron->email,    'mySurname.myFirstname@email.com', 'email accessor returns correct value' );
143         is( $patron->phone,    '0402872934',                      'phone accessor returns correct value' );
144         is( $patron->mobile,   '0627884632',                      'mobile accessor returns correct value' );
145         is( $patron->fax,      '0402872935',                      'fax accessor returns correct value' );
146         is( $patron->emailpro, 'myEmailPro@email.com',            'emailpro accessor returns correct value' );
147         is( $patron->phonepro, '0402873334',                      'phonepro accessor returns correct value' );
148         is( $patron->B_streetnumber,  '101',               'B_streetnumber accessor returns correct value' );
149         is( $patron->B_streettype,    'myB_streettype',    'B_streettype accessor returns correct value' );
150         is( $patron->B_address,       'myB_address',       'B_address accessor returns correct value' );
151         is( $patron->B_address2,      'myB_address2',      'B_address2 accessor returns correct value' );
152         is( $patron->B_city,          'myB_city',          'B_city accessor returns correct value' );
153         is( $patron->B_state,         'myB_state',         'B_state accessor returns correct value' );
154         is( $patron->B_zipcode,       '23456',             'B_zipcode accessor returns correct value' );
155         is( $patron->B_country,       'myB_country',       'B_country accessor returns correct value' );
156         is( $patron->B_email,         'myB_email',         'B_email accessor returns correct value' );
157         is( $patron->B_phone,         '0678353935',        'B_phone accessor returns correct value' );
158         is( $patron->dateofbirth,     '1990-07-16',        'dateofbirth accessor returns correct value' );
159         is( $patron->branchcode,      'myBranCode',        'branchcode accessor returns correct value' );
160         is( $patron->categorycode,    'myCatCode',         'categorycode accessor returns correct value' );
161         is( $patron->dateenrolled,    '2015-03-19',        'dateenrolled accessor returns correct value' );
162         is( $patron->dateexpiry,      '2016-03-19',        'dateexpiry accessor returns correct value' );
163         is( $patron->gonenoaddress,   '0',                 'gonenoaddress accessor returns correct value' );
164         is( $patron->lost,            '0',                 'lost accessor returns correct value' );
165         is( $patron->debarred,        '2015-04-19',        'debarred accessor returns correct value' );
166         is( $patron->debarredcomment, 'You are debarred',  'debarredcomment accessor returns correct value' );
167         is( $patron->borrowernotes,   'borrowernotes',     'borrowernotes accessor returns correct value' );
168         is( $patron->sex,             'M',                 'sex accessor returns correct value' );
169         is( $patron->password,        'hfkurhfe976634èj!', 'password accessor returns correct value' );
170         is( $patron->flags,           '55555',             'flags accessor returns correct value' );
171         is( $patron->userid,          '87987',             'userid accessor returns correct value' );
172         is( $patron->opacnote,        'myOpacnote',        'opacnote accessor returns correct value' );
173         is( $patron->contactnote,     'myContactnote',     'contactnote accessor returns correct value' );
174         is( $patron->sort1,           'mySort1',           'sort1 accessor returns correct value' );
175         is( $patron->sort2,           'mySort2',           'sort2 accessor returns correct value' );
176         is(
177             $patron->altcontactfirstname, 'myAltcontactfirstname',
178             'altcontactfirstname accessor returns correct value'
179         );
180         is( $patron->altcontactsurname,  'myAltcontactsurname',  'altcontactsurname accessor returns correct value' );
181         is( $patron->altcontactaddress1, 'myAltcontactaddress1', 'altcontactaddress1 accessor returns correct value' );
182         is( $patron->altcontactaddress2, 'myAltcontactaddress2', 'altcontactaddress2 accessor returns correct value' );
183         is( $patron->altcontactaddress3, 'myAltcontactaddress3', 'altcontactaddress3 accessor returns correct value' );
184         is( $patron->altcontactstate,    'myAltcontactstate',    'altcontactstate accessor returns correct value' );
185         is( $patron->altcontactzipcode,  '465843',               'altcontactzipcode accessor returns correct value' );
186         is( $patron->altcontactcountry,  'myOtherCountry',       'altcontactcountry accessor returns correct value' );
187         is( $patron->altcontactphone,    'myOtherphone',         'altcontactphone accessor returns correct value' );
188         is( $patron->smsalertnumber,     '0683027346',           'smsalertnumber accessor returns correct value' );
189         is( $patron->privacy,            '667788',               'privacy accessor returns correct value' );
190     };
191
192     subtest 'Accessor tests after set' => sub {
193         plan tests => 60;
194
195         $patron->set(
196             {
197                 borrowernumber      => '12346',
198                 cardnumber          => '1234567891',
199                 surname             => 'SmySurname',
200                 firstname           => 'SmyFirstname',
201                 title               => 'Mme.',
202                 othernames          => 'SmyOthernames',
203                 initials            => 'SS',
204                 streetnumber        => '200',
205                 streettype          => 'Rue',
206                 address             => 'Smy personal address',
207                 address2            => 'Smy adress2',
208                 city                => 'Lyon',
209                 state               => 'Smystate',
210                 zipcode             => '69000',
211                 country             => 'France',
212                 email               => 'SmySurname.myFirstname@email.com',
213                 phone               => '0402872935',
214                 mobile              => '0627884633',
215                 fax                 => '0402872936',
216                 emailpro            => 'SmyEmailPro@email.com',
217                 phonepro            => '0402873335',
218                 B_streetnumber      => '102',
219                 B_streettype        => 'SmyB_streettype',
220                 B_address           => 'SmyB_address',
221                 B_address2          => 'SmyB_address2',
222                 B_city              => 'SmyB_city',
223                 B_state             => 'SmyB_state',
224                 B_zipcode           => '12333',
225                 B_country           => 'SmyB_country',
226                 B_email             => 'SmyB_email',
227                 B_phone             => '0678353936',
228                 dateofbirth         => '1991-07-16',
229                 branchcode          => 'SmyBranCode',
230                 categorycode        => 'SmyCatCode',
231                 dateenrolled        => '2014-03-19',
232                 dateexpiry          => '2017-03-19',
233                 gonenoaddress       => '1',
234                 lost                => '1',
235                 debarred            => '2016-04-19',
236                 debarredcomment     => 'You are still debarred',
237                 borrowernotes       => 'Sborrowernotes',
238                 sex                 => 'F',
239                 password            => 'zerzerzer#',
240                 flags               => '666666',
241                 userid              => '98233',
242                 opacnote            => 'SmyOpacnote',
243                 contactnote         => 'SmyContactnote',
244                 sort1               => 'SmySort1',
245                 sort2               => 'SmySort2',
246                 altcontactfirstname => 'SmyAltcontactfirstname',
247                 altcontactsurname   => 'SmyAltcontactsurname',
248                 altcontactaddress1  => 'SmyAltcontactaddress1',
249                 altcontactaddress2  => 'SmyAltcontactaddress2',
250                 altcontactaddress3  => 'SmyAltcontactaddress3',
251                 altcontactstate     => 'SmyAltcontactstate',
252                 altcontactzipcode   => '565843',
253                 altcontactcountry   => 'SmyOtherCountry',
254                 altcontactphone     => 'SmyOtherphone',
255                 smsalertnumber      => '0683027347',
256                 privacy             => '667789'
257             }
258         );
259
260         is( $patron->borrowernumber,      '12346',                            'borrowernumber field set ok' );
261         is( $patron->cardnumber,          '1234567891',                       'cardnumber field set ok' );
262         is( $patron->surname,             'SmySurname',                       'surname field set ok' );
263         is( $patron->firstname,           'SmyFirstname',                     'firstname field set ok' );
264         is( $patron->title,               'Mme.',                             'title field set ok' );
265         is( $patron->othernames,          'SmyOthernames',                    'othernames field set ok' );
266         is( $patron->initials,            'SS',                               'initials field set ok' );
267         is( $patron->streetnumber,        '200',                              'streetnumber field set ok' );
268         is( $patron->streettype,          'Rue',                              'streettype field set ok' );
269         is( $patron->address,             'Smy personal address',             'address field set ok' );
270         is( $patron->address2,            'Smy adress2',                      'address2 field set ok' );
271         is( $patron->city,                'Lyon',                             'city field set ok' );
272         is( $patron->state,               'Smystate',                         'state field set ok' );
273         is( $patron->zipcode,             '69000',                            'zipcode field set ok' );
274         is( $patron->country,             'France',                           'country field set ok' );
275         is( $patron->email,               'SmySurname.myFirstname@email.com', 'email field set ok' );
276         is( $patron->phone,               '0402872935',                       'phone field set ok' );
277         is( $patron->mobile,              '0627884633',                       'mobile field set ok' );
278         is( $patron->fax,                 '0402872936',                       'fax field set ok' );
279         is( $patron->emailpro,            'SmyEmailPro@email.com',            'emailpro field set ok' );
280         is( $patron->phonepro,            '0402873335',                       'phonepro field set ok' );
281         is( $patron->B_streetnumber,      '102',                              'B_streetnumber field set ok' );
282         is( $patron->B_streettype,        'SmyB_streettype',                  'B_streettype field set ok' );
283         is( $patron->B_address,           'SmyB_address',                     'B_address field set ok' );
284         is( $patron->B_address2,          'SmyB_address2',                    'B_address2 field set ok' );
285         is( $patron->B_city,              'SmyB_city',                        'B_city field set ok' );
286         is( $patron->B_state,             'SmyB_state',                       'B_state field set ok' );
287         is( $patron->B_zipcode,           '12333',                            'B_zipcode field set ok' );
288         is( $patron->B_country,           'SmyB_country',                     'B_country field set ok' );
289         is( $patron->B_email,             'SmyB_email',                       'B_email field set ok' );
290         is( $patron->B_phone,             '0678353936',                       'B_phone field set ok' );
291         is( $patron->dateofbirth,         '1991-07-16',                       'dateofbirth field set ok' );
292         is( $patron->branchcode,          'SmyBranCode',                      'branchcode field set ok' );
293         is( $patron->categorycode,        'SmyCatCode',                       'categorycode field set ok' );
294         is( $patron->dateenrolled,        '2014-03-19',                       'dateenrolled field set ok' );
295         is( $patron->dateexpiry,          '2017-03-19',                       'dateexpiry field set ok' );
296         is( $patron->gonenoaddress,       '1',                                'gonenoaddress field set ok' );
297         is( $patron->lost,                '1',                                'lost field set ok' );
298         is( $patron->debarred,            '2016-04-19',                       'debarred field set ok' );
299         is( $patron->debarredcomment,     'You are still debarred',           'debarredcomment field set ok' );
300         is( $patron->borrowernotes,       'Sborrowernotes',                   'borrowernotes field set ok' );
301         is( $patron->sex,                 'F',                                'sex field set ok' );
302         is( $patron->password,            'zerzerzer#',                       'password field set ok' );
303         is( $patron->flags,               '666666',                           'flags field set ok' );
304         is( $patron->userid,              '98233',                            'userid field set ok' );
305         is( $patron->opacnote,            'SmyOpacnote',                      'opacnote field set ok' );
306         is( $patron->contactnote,         'SmyContactnote',                   'contactnote field set ok' );
307         is( $patron->sort1,               'SmySort1',                         'sort1 field set ok' );
308         is( $patron->sort2,               'SmySort2',                         'sort2 field set ok' );
309         is( $patron->altcontactfirstname, 'SmyAltcontactfirstname',           'altcontactfirstname field set ok' );
310         is( $patron->altcontactsurname,   'SmyAltcontactsurname',             'altcontactsurname field set ok' );
311         is( $patron->altcontactaddress1,  'SmyAltcontactaddress1',            'altcontactaddress1 field set ok' );
312         is( $patron->altcontactaddress2,  'SmyAltcontactaddress2',            'altcontactaddress2 field set ok' );
313         is( $patron->altcontactaddress3,  'SmyAltcontactaddress3',            'altcontactaddress3 field set ok' );
314         is( $patron->altcontactstate,     'SmyAltcontactstate',               'altcontactstate field set ok' );
315         is( $patron->altcontactzipcode,   '565843',                           'altcontactzipcode field set ok' );
316         is( $patron->altcontactcountry,   'SmyOtherCountry',                  'altcontactcountry field set ok' );
317         is( $patron->altcontactphone,     'SmyOtherphone',                    'altcontactphone field set ok' );
318         is( $patron->smsalertnumber,      '0683027347',                       'smsalertnumber field set ok' );
319         is( $patron->privacy,             '667789',                           'privacy field set ok' );
320     };
321 };
322
323 subtest 'is_active' => sub {
324     plan tests => 19;
325     $schema->storage->txn_begin;
326
327     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
328     throws_ok { $patron->is_active } 'Koha::Exceptions::MissingParameter', 'Called without params';
329
330     # Check expiry
331     $patron->dateexpiry( dt_from_string->subtract( days => 1 ) )->store;
332     is( $patron->is_active( { days => 1 } ), 0, 'Expired patron is not active' );
333     $patron->dateexpiry(undef)->store;
334     is( $patron->is_active( { days => 1 } ), 1, 'Expiry date removed' );
335
336     # Change enrolled date now
337     $patron->dateenrolled('2020-01-01')->store;
338
339     # Check lastseen, test days parameter
340     t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', 1 );
341     $patron->track_login;
342     is( $patron->is_active( { days => 1 } ), 1, 'Just logged in' );
343     my $ago = dt_from_string->subtract( days => 2 );
344     $patron->lastseen($ago)->store;
345     is( $patron->is_active( { days => 1 } ), 0, 'Not active since yesterday' );
346     is( $patron->is_active( { days => 3 } ), 1, 'Active within last 3 days' );
347     t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', 0 );
348     is( $patron->is_active( { days => 3 } ), 0, 'Pref disabled' );
349
350     # Look at holds, test with weeks
351     $ago = dt_from_string->subtract( weeks => 2 );
352     my $hold = $builder->build_object(
353         {
354             class => 'Koha::Holds',
355             value => { borrowernumber => $patron->id, timestamp => $ago },
356         }
357     );
358     is( $patron->is_active( { weeks => 1 } ), 0, 'No holds in 1 weeks' );
359     is( $patron->is_active( { weeks => 3 } ), 1, 'Hold in last 3 weeks' );
360     $hold->delete;
361     my $old_hold = $builder->build_object(
362         {
363             class => 'Koha::Old::Holds',
364             value => { borrowernumber => $patron->id, timestamp => $ago },
365         }
366     );
367     is( $patron->is_active( { weeks => 1 } ), 0, 'No old holds in 1 weeks' );
368     is( $patron->is_active( { weeks => 3 } ), 1, 'Old hold in last 3 weeks' );
369     $old_hold->delete;
370
371     # Look at checkouts, test with months
372     $ago = dt_from_string->subtract( months => 2 );
373     my $checkout = $builder->build_object(
374         {
375             class => 'Koha::Checkouts',
376             value => { borrowernumber => $patron->id, timestamp => $ago },
377         }
378     );
379     is( $patron->is_active( { months => 1 } ), 0, 'No checkouts in 1 months' );
380     is( $patron->is_active( { months => 3 } ), 1, 'Checkout in last 3 months' );
381     $checkout->delete;
382     my $old_checkout = $builder->build_object(
383         {
384             class => 'Koha::Old::Checkouts',
385             value => { borrowernumber => $patron->id, timestamp => $ago },
386         }
387     );
388     is( $patron->is_active( { months => 1 } ), 0, 'No old checkouts in 1 months' );
389     is( $patron->is_active( { months => 3 } ), 1, 'Old checkout in last 3 months' );
390     $old_checkout->delete;
391
392     # Look at article_requests, test with since
393     $ago = dt_from_string->subtract( days => 10 );
394     my $article_request = $builder->build_object(
395         {
396             class => 'Koha::ArticleRequests',
397             value => { borrowernumber => $patron->id, updated_on => $ago },
398         }
399     );
400     is( $patron->is_active( { days  => 9 } ),                      0, 'No article requests in 9 days' );
401     is( $patron->is_active( { days  => 10 } ),                     1, 'Article requests in 10 days' );
402     is( $patron->is_active( { since => $ago } ),                   1, 'Article requests since ago' );
403     is( $patron->is_active( { since => $ago->add( days => 1 ) } ), 0, 'No article requests since ago + 1 day' );
404     $article_request->delete;
405
406     $schema->storage->txn_rollback;
407 };
408
409 subtest 'add_guarantor() tests' => sub {
410
411     plan tests => 6;
412
413     $schema->storage->txn_begin;
414
415     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'father1|father2' );
416
417     my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
418     my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
419
420     throws_ok
421         { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber }); }
422         'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
423         'Exception is thrown as no relationship passed';
424
425     is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
426
427     throws_ok
428         { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father' }); }
429         'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
430         'Exception is thrown as a wrong relationship was passed';
431
432     is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
433
434     $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father1' });
435
436     my $guarantors = $patron_1->guarantor_relationships;
437
438     is( $guarantors->count, 1, 'No guarantors added' );
439
440     {
441         local *STDERR;
442         open STDERR, '>', '/dev/null';
443         throws_ok
444             { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father2' }); }
445             'Koha::Exceptions::Patron::Relationship::DuplicateRelationship',
446             'Exception is thrown for duplicated relationship';
447         close STDERR;
448     }
449
450     $schema->storage->txn_rollback;
451 };
452
453 subtest 'relationships_debt() tests' => sub {
454
455     plan tests => 168;
456
457     $schema->storage->txn_begin;
458
459     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
460
461     my $parent_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 1" } });
462     my $parent_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 2" } });
463     my $child_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => " Child 1" } });
464     my $child_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => " Child 2" } });
465
466     $child_1->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
467     $child_1->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
468     $child_2->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
469     $child_2->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
470
471     is( $child_1->guarantor_relationships->guarantors->count, 2, 'Child 1 has correct number of guarantors' );
472     is( $child_2->guarantor_relationships->guarantors->count, 2, 'Child 2 has correct number of guarantors' );
473     is( $parent_1->guarantee_relationships->guarantees->count, 2, 'Parent 1 has correct number of guarantees' );
474     is( $parent_2->guarantee_relationships->guarantees->count, 2, 'Parent 2 has correct number of guarantees' );
475
476     my $patrons = [ $parent_1, $parent_2, $child_1, $child_2 ];
477
478     # First test: No debt
479     my ($parent1_debt, $parent2_debt, $child1_debt, $child2_debt) = (0,0,0,0);
480     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
481
482     # Add debt to child_2
483     $child2_debt = 2;
484     $child_2->account->add_debit({ type => 'ACCOUNT', amount => $child2_debt, interface => 'commandline' });
485     is( $child_2->account->non_issues_charges, $child2_debt, 'Debt added to Child 2' );
486     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
487
488     $parent1_debt = 3;
489     $parent_1->account->add_debit({ type => 'ACCOUNT', amount => $parent1_debt, interface => 'commandline' });
490     is( $parent_1->account->non_issues_charges, $parent1_debt, 'Debt added to Parent 1' );
491     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
492
493     $parent2_debt = 5;
494     $parent_2->account->add_debit({ type => 'ACCOUNT', amount => $parent2_debt, interface => 'commandline' });
495     is( $parent_2->account->non_issues_charges, $parent2_debt, 'Parent 2 owes correct amount' );
496     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
497
498     $child1_debt = 7;
499     $child_1->account->add_debit({ type => 'ACCOUNT', amount => $child1_debt, interface => 'commandline' });
500     is( $child_1->account->non_issues_charges, $child1_debt, 'Child 1 owes correct amount' );
501     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
502
503     $schema->storage->txn_rollback;
504 };
505
506 sub _test_combinations {
507     my ( $patrons, $parent1_debt, $parent2_debt, $child1_debt, $child2_debt ) = @_;
508     note("Testing with parent 1 debt $parent1_debt | Parent 2 debt $parent2_debt | Child 1 debt $child1_debt | Child 2 debt $child2_debt");
509     # Options
510     # P1 => P1 + C1 + C2 ( - P1 ) ( + P2 )
511     # P2 => P2 + C1 + C2 ( - P2 ) ( + P1 )
512     # C1 => P1 + P2 + C1 + C2 ( - C1 )
513     # C2 => P1 + P2 + C1 + C2 ( - C2 )
514
515 # 3 params, count from 0 to 7 in binary ( 3 places ) to get the set of switches, then do that 4 times, one for each parent and child
516     for my $i ( 0 .. 7 ) {
517         my ( $only_this_guarantor, $include_guarantors, $include_this_patron )
518           = split '', sprintf( "%03b", $i );
519         note("---------------------");
520         for my $patron ( @$patrons ) {
521             if ( $only_this_guarantor
522                 && !$patron->guarantee_relationships->count )
523             {
524                 throws_ok {
525                     $patron->relationships_debt(
526                         {
527                             only_this_guarantor => $only_this_guarantor,
528                             include_guarantors  => $include_guarantors,
529                             include_this_patron => $include_this_patron
530                         }
531                     );
532                 }
533                 'Koha::Exceptions::BadParameter',
534                   'Exception is thrown as patron is not a guarantor';
535
536             }
537             else {
538
539                 my $debt = 0;
540                 if ( $patron->firstname eq 'Parent 1' ) {
541                     $debt += $parent1_debt if ($include_this_patron && $include_guarantors);
542                     $debt += $child1_debt + $child2_debt;
543                     $debt += $parent2_debt unless ($only_this_guarantor || !$include_guarantors);
544                 }
545                 elsif ( $patron->firstname eq 'Parent 2' ) {
546                     $debt += $parent2_debt if ($include_this_patron & $include_guarantors);
547                     $debt += $child1_debt + $child2_debt;
548                     $debt += $parent1_debt unless ($only_this_guarantor || !$include_guarantors);
549                 }
550                 elsif ( $patron->firstname eq ' Child 1' ) {
551                     $debt += $child1_debt if ($include_this_patron);
552                     $debt += $child2_debt;
553                     $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
554                 }
555                 else {
556                     $debt += $child2_debt if ($include_this_patron);
557                     $debt += $child1_debt;
558                     $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
559                 }
560
561                 is(
562                     $patron->relationships_debt(
563                         {
564                             only_this_guarantor => $only_this_guarantor,
565                             include_guarantors  => $include_guarantors,
566                             include_this_patron => $include_this_patron
567                         }
568                     ),
569                     $debt,
570                     $patron->firstname
571                       . " debt of " . sprintf('%02d',$debt) . " calculated correctly for ( only_this_guarantor: $only_this_guarantor, include_guarantors: $include_guarantors, include_this_patron: $include_this_patron)"
572                 );
573             }
574         }
575     }
576 }
577
578 subtest 'add_enrolment_fee_if_needed() tests' => sub {
579
580     plan tests => 2;
581
582     subtest 'category has enrolment fee' => sub {
583         plan tests => 7;
584
585         $schema->storage->txn_begin;
586
587         my $category = $builder->build_object(
588             {
589                 class => 'Koha::Patron::Categories',
590                 value => {
591                     enrolmentfee => 20
592                 }
593             }
594         );
595
596         my $patron = $builder->build_object(
597             {
598                 class => 'Koha::Patrons',
599                 value => {
600                     categorycode => $category->categorycode
601                 }
602             }
603         );
604
605         my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
606         is( $enrollment_fee * 1, 20, 'Enrolment fee amount is correct' );
607         my $account = $patron->account;
608         is( $patron->account->balance * 1, 20, 'Patron charged the enrolment fee' );
609         # second enrolment fee, new
610         $enrollment_fee = $patron->add_enrolment_fee_if_needed(0);
611         # third enrolment fee, renewal
612         $enrollment_fee = $patron->add_enrolment_fee_if_needed(1);
613         is( $patron->account->balance * 1, 60, 'Patron charged the enrolment fees' );
614
615         my @debits = $account->outstanding_debits->as_list;
616         is( scalar @debits, 3, '3 enrolment fees' );
617         is( $debits[0]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
618         is( $debits[1]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
619         is( $debits[2]->debit_type_code, 'ACCOUNT_RENEW', 'Account type set correctly' );
620
621         $schema->storage->txn_rollback;
622     };
623
624     subtest 'no enrolment fee' => sub {
625
626         plan tests => 3;
627
628         $schema->storage->txn_begin;
629
630         my $category = $builder->build_object(
631             {
632                 class => 'Koha::Patron::Categories',
633                 value => {
634                     enrolmentfee => 0
635                 }
636             }
637         );
638
639         my $patron = $builder->build_object(
640             {
641                 class => 'Koha::Patrons',
642                 value => {
643                     categorycode => $category->categorycode
644                 }
645             }
646         );
647
648         my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
649         is( $enrollment_fee * 1, 0, 'No enrolment fee' );
650         my $account = $patron->account;
651         is( $patron->account->balance, 0, 'Patron not charged anything' );
652
653         my @debits = $account->outstanding_debits->as_list;
654         is( scalar @debits, 0, 'no debits' );
655
656         $schema->storage->txn_rollback;
657     };
658 };
659
660 subtest 'messaging_preferences() tests' => sub {
661     plan tests => 5;
662
663     $schema->storage->txn_begin;
664
665     my $mtt = $builder->build_object({
666         class => 'Koha::Patron::MessagePreference::Transport::Types'
667     });
668     my $attribute = $builder->build_object({
669         class => 'Koha::Patron::MessagePreference::Attributes'
670     });
671     my $branchcode     = $builder->build({
672         source => 'Branch' })->{branchcode};
673     my $letter = $builder->build_object({
674         class => 'Koha::Notice::Templates',
675         value => {
676             branchcode => '',
677             is_html => 0,
678             message_transport_type => $mtt->message_transport_type
679         }
680     });
681
682     Koha::Patron::MessagePreference::Transport->new({
683         message_attribute_id   => $attribute->message_attribute_id,
684         message_transport_type => $mtt->message_transport_type,
685         is_digest              => 0,
686         letter_module          => $letter->module,
687         letter_code            => $letter->code,
688     })->store;
689
690     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
691
692     my $preference = Koha::Patron::MessagePreference->new({
693         borrowernumber => $patron->borrowernumber,
694         message_attribute_id => $attribute->message_attribute_id,
695         wants_digest => 0,
696         days_in_advance => undef,
697     })->store;
698
699     my $messaging_preferences = $patron->messaging_preferences();
700     is($messaging_preferences->count, 1, 'Found one preference');
701
702     my $messaging_preference = $messaging_preferences->next;
703     is($messaging_preference->borrowernumber, $patron->borrowernumber);
704     is($messaging_preference->message_attribute_id, $attribute->message_attribute_id);
705     is($messaging_preference->wants_digest, 0);
706     is($messaging_preference->days_in_advance, undef);
707
708     $schema->storage->txn_rollback;
709 };
710
711 subtest 'to_api() tests' => sub {
712
713     plan tests => 6;
714
715     $schema->storage->txn_begin;
716
717     my $patron_class = Test::MockModule->new('Koha::Patron');
718     $patron_class->mock(
719         'algo',
720         sub { return 'algo' }
721     );
722
723     my $patron = $builder->build_object(
724         {
725             class => 'Koha::Patrons',
726             value => {
727                 debarred => undef
728             }
729         }
730     );
731
732     my $restricted = $patron->to_api->{restricted};
733     ok( defined $restricted, 'restricted is defined' );
734     ok( !$restricted, 'debarred is undef, restricted evaluates to false' );
735
736     $patron->debarred( dt_from_string->add( days => 1 ) )->store->discard_changes;
737     $restricted = $patron->to_api->{restricted};
738     ok( defined $restricted, 'restricted is defined' );
739     ok( $restricted, 'debarred is defined, restricted evaluates to true' );
740
741     my $patron_json = $patron->to_api({ embed => { algo => {} } });
742     ok( exists $patron_json->{algo} );
743     is( $patron_json->{algo}, 'algo' );
744
745     $schema->storage->txn_rollback;
746 };
747
748 subtest 'login_attempts tests' => sub {
749     plan tests => 1;
750
751     $schema->storage->txn_begin;
752
753     my $patron = $builder->build_object(
754         {
755             class => 'Koha::Patrons',
756         }
757     );
758     my $patron_info = $patron->unblessed;
759     $patron->delete;
760     delete $patron_info->{login_attempts};
761     my $new_patron = Koha::Patron->new($patron_info)->store;
762     is( $new_patron->discard_changes->login_attempts, 0, "login_attempts defaults to 0 as expected");
763
764     $schema->storage->txn_rollback;
765 };
766
767 subtest 'is_superlibrarian() tests' => sub {
768
769     plan tests => 3;
770
771     $schema->storage->txn_begin;
772
773     my $patron = $builder->build_object(
774         {
775             class => 'Koha::Patrons',
776
777             value => {
778                 flags => 16
779             }
780         }
781     );
782
783     is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
784
785     $patron->flags(1)->store->discard_changes;
786     is( $patron->is_superlibrarian, 1, 'Patron is a superlibrarian and the method returns the correct value' );
787
788     $patron->flags(0)->store->discard_changes;
789     is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
790
791     $schema->storage->txn_rollback;
792 };
793
794 subtest 'extended_attributes' => sub {
795
796     plan tests => 16;
797
798     my $schema = Koha::Database->new->schema;
799     $schema->storage->txn_begin;
800
801     Koha::Patron::Attribute::Types->search->delete;
802
803     my $patron_1 = $builder->build_object({class=> 'Koha::Patrons'});
804     my $patron_2 = $builder->build_object({class=> 'Koha::Patrons'});
805
806     t::lib::Mocks::mock_userenv({ patron => $patron_1 });
807
808     my $attribute_type1 = Koha::Patron::Attribute::Type->new(
809         {
810             code        => 'my code1',
811             description => 'my description1',
812             unique_id   => 1
813         }
814     )->store;
815     my $attribute_type2 = Koha::Patron::Attribute::Type->new(
816         {
817             code             => 'my code2',
818             description      => 'my description2',
819             opac_display     => 1,
820             staff_searchable => 1
821         }
822     )->store;
823
824     my $new_library = $builder->build( { source => 'Branch' } );
825     my $attribute_type_limited = Koha::Patron::Attribute::Type->new(
826         { code => 'my code3', description => 'my description3' } )->store;
827     $attribute_type_limited->library_limits( [ $new_library->{branchcode} ] );
828
829     my $attributes_for_1 = [
830         {
831             attribute => 'my attribute1',
832             code => $attribute_type1->code(),
833         },
834         {
835             attribute => 'my attribute2',
836             code => $attribute_type2->code(),
837         },
838         {
839             attribute => 'my attribute limited',
840             code => $attribute_type_limited->code(),
841         }
842     ];
843
844     my $attributes_for_2 = [
845         {
846             attribute => 'my attribute12',
847             code => $attribute_type1->code(),
848         },
849         {
850             attribute => 'my attribute limited 2',
851             code => $attribute_type_limited->code(),
852         }
853     ];
854
855     my $extended_attributes = $patron_1->extended_attributes;
856     is( ref($extended_attributes), 'Koha::Patron::Attributes', 'Koha::Patron->extended_attributes must return a Koha::Patron::Attribute set' );
857     is( $extended_attributes->count, 0, 'There should not be attribute yet');
858
859     $patron_1->extended_attributes->filter_by_branch_limitations->delete;
860     $patron_2->extended_attributes->filter_by_branch_limitations->delete;
861     $patron_1->extended_attributes($attributes_for_1);
862     $patron_2->extended_attributes($attributes_for_2);
863
864     my $extended_attributes_for_1 = $patron_1->extended_attributes;
865     is( $extended_attributes_for_1->count, 3, 'There should be 3 attributes now for patron 1');
866
867     my $extended_attributes_for_2 = $patron_2->extended_attributes;
868     is( $extended_attributes_for_2->count, 2, 'There should be 2 attributes now for patron 2');
869
870     my $attribute_12 = $extended_attributes_for_2->search({ code => $attribute_type1->code })->next;
871     is( $attribute_12->attribute, 'my attribute12', 'search by code should return the correct attribute' );
872
873     $attribute_12 = $patron_2->get_extended_attribute( $attribute_type1->code );
874     is( $attribute_12->attribute, 'my attribute12', 'Koha::Patron->get_extended_attribute should return the correct attribute value' );
875
876     my $expected_attributes_for_2 = [
877         {
878             code      => $attribute_type1->code(),
879             attribute => 'my attribute12',
880         },
881         {
882             code      => $attribute_type_limited->code(),
883             attribute => 'my attribute limited 2',
884         }
885     ];
886     # Sorting them by code
887     $expected_attributes_for_2 = [ sort { $a->{code} cmp $b->{code} } @$expected_attributes_for_2 ];
888     my @extended_attributes_for_2 = $extended_attributes_for_2->as_list;
889
890     is_deeply(
891         [
892             {
893                 code      => $extended_attributes_for_2[0]->code,
894                 attribute => $extended_attributes_for_2[0]->attribute
895             },
896             {
897                 code      => $extended_attributes_for_2[1]->code,
898                 attribute => $extended_attributes_for_2[1]->attribute
899             }
900         ],
901         $expected_attributes_for_2
902     );
903
904     # TODO - What about multiple? POD explains the problem
905     my $non_existent = $patron_2->get_extended_attribute( 'not_exist' );
906     is( $non_existent, undef, 'Koha::Patron->get_extended_attribute must return undef if the attribute does not exist' );
907
908     # Test branch limitations
909     t::lib::Mocks::mock_userenv({ patron => $patron_2 });
910     # Return all
911     $extended_attributes_for_1 = $patron_1->extended_attributes;
912     is( $extended_attributes_for_1->count, 3, 'There should be 2 attributes for patron 1, the limited one should be returned');
913
914     # Return filtered
915     $extended_attributes_for_1 = $patron_1->extended_attributes->filter_by_branch_limitations;
916     is( $extended_attributes_for_1->count, 2, 'There should be 2 attributes for patron 1, the limited one should be returned');
917
918     # Not filtered
919     my $limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
920     is( $limited_value->attribute, 'my attribute limited', );
921
922     ## Do we need a filtered?
923     #$limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
924     #is( $limited_value, undef, );
925
926     $schema->storage->txn_rollback;
927
928     subtest 'non-repeatable attributes tests' => sub {
929
930         plan tests => 3;
931
932         $schema->storage->txn_begin;
933         Koha::Patron::Attribute::Types->search->delete;
934
935         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
936         my $attribute_type = $builder->build_object(
937             {
938                 class => 'Koha::Patron::Attribute::Types',
939                 value => { repeatable => 0 }
940             }
941         );
942
943         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
944
945         throws_ok
946             {
947                 $patron->extended_attributes(
948                     [
949                         { code => $attribute_type->code, attribute => 'a' },
950                         { code => $attribute_type->code, attribute => 'b' }
951                     ]
952                 );
953             }
954             'Koha::Exceptions::Patron::Attribute::NonRepeatable',
955             'Exception thrown on non-repeatable attribute';
956
957         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
958
959         $schema->storage->txn_rollback;
960
961     };
962
963     subtest 'unique attributes tests' => sub {
964
965         plan tests => 5;
966
967         $schema->storage->txn_begin;
968         Koha::Patron::Attribute::Types->search->delete;
969
970         my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
971         my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
972
973         my $attribute_type_1 = $builder->build_object(
974             {
975                 class => 'Koha::Patron::Attribute::Types',
976                 value => { unique_id => 1 }
977             }
978         );
979
980         my $attribute_type_2 = $builder->build_object(
981             {
982                 class => 'Koha::Patron::Attribute::Types',
983                 value => { unique_id => 0 }
984             }
985         );
986
987         is( $patron_1->extended_attributes->count, 0, 'patron_1 has no extended attributes' );
988         is( $patron_2->extended_attributes->count, 0, 'patron_2 has no extended attributes' );
989
990         $patron_1->extended_attributes(
991             [
992                 { code => $attribute_type_1->code, attribute => 'a' },
993                 { code => $attribute_type_2->code, attribute => 'a' }
994             ]
995         );
996
997         throws_ok
998             {
999                 $patron_2->extended_attributes(
1000                     [
1001                         { code => $attribute_type_1->code, attribute => 'a' },
1002                         { code => $attribute_type_2->code, attribute => 'a' }
1003                     ]
1004                 );
1005             }
1006             'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint',
1007             'Exception thrown on unique attribute';
1008
1009         is( $patron_1->extended_attributes->count, 2, 'Extended attributes stored' );
1010         is( $patron_2->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
1011
1012         $schema->storage->txn_rollback;
1013
1014     };
1015
1016     subtest 'invalid type attributes tests' => sub {
1017
1018         plan tests => 3;
1019
1020         $schema->storage->txn_begin;
1021         Koha::Patron::Attribute::Types->search->delete;
1022
1023         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1024
1025         my $attribute_type_1 = $builder->build_object(
1026             {
1027                 class => 'Koha::Patron::Attribute::Types',
1028                 value => { repeatable => 0 }
1029             }
1030         );
1031
1032         my $attribute_type_2 = $builder->build_object(
1033             {
1034                 class => 'Koha::Patron::Attribute::Types'
1035             }
1036         );
1037
1038         my $type_2 = $attribute_type_2->code;
1039         $attribute_type_2->delete;
1040
1041         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
1042
1043         throws_ok
1044             {
1045                 $patron->extended_attributes(
1046                     [
1047                         { code => $attribute_type_1->code, attribute => 'a' },
1048                         { code => $attribute_type_2->code, attribute => 'b' }
1049                     ]
1050                 );
1051             }
1052             'Koha::Exceptions::Patron::Attribute::InvalidType',
1053             'Exception thrown on invalid attribute type';
1054
1055         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
1056
1057         $schema->storage->txn_rollback;
1058
1059     };
1060
1061     subtest 'globally mandatory attributes tests' => sub {
1062
1063         plan tests => 5;
1064
1065         $schema->storage->txn_begin;
1066         Koha::Patron::Attribute::Types->search->delete;
1067
1068         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1069
1070         my $attribute_type_1 = $builder->build_object(
1071             {
1072                 class => 'Koha::Patron::Attribute::Types',
1073                 value => { mandatory => 1, class => 'a', category_code => undef }
1074             }
1075         );
1076
1077         my $attribute_type_2 = $builder->build_object(
1078             {
1079                 class => 'Koha::Patron::Attribute::Types',
1080                 value => { mandatory => 0, class => 'a', category_code => undef }
1081             }
1082         );
1083
1084         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
1085
1086         throws_ok
1087             {
1088                 $patron->extended_attributes(
1089                     [
1090                         { code => $attribute_type_2->code, attribute => 'b' }
1091                     ]
1092                 );
1093             }
1094             'Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute',
1095             'Exception thrown on missing mandatory attribute type';
1096
1097         is( $@->type, $attribute_type_1->code, 'Exception parameters are correct' );
1098
1099         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
1100
1101         $patron->extended_attributes(
1102             [
1103                 { code => $attribute_type_1->code, attribute => 'b' }
1104             ]
1105         );
1106
1107         is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
1108
1109         $schema->storage->txn_rollback;
1110
1111     };
1112
1113     subtest 'limited category mandatory attributes tests' => sub {
1114
1115         plan tests => 2;
1116
1117         $schema->storage->txn_begin;
1118         Koha::Patron::Attribute::Types->search->delete;
1119
1120         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1121
1122         my $attribute_type_1 = $builder->build_object(
1123             {
1124                 class => 'Koha::Patron::Attribute::Types',
1125                 value => { mandatory => 1, class => 'a', category_code => $patron->categorycode }
1126             }
1127         );
1128
1129         $patron->extended_attributes(
1130             [
1131                 { code => $attribute_type_1->code, attribute => 'a' }
1132             ]
1133         );
1134
1135         is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
1136
1137         $patron = $builder->build_object({ class => 'Koha::Patrons' });
1138         # new patron, new category - they shouldn't be required to have any attributes
1139
1140
1141         ok( $patron->extended_attributes([]), "We can set no attributes, mandatory attribute for other category not required");
1142
1143
1144     };
1145
1146
1147
1148 };
1149
1150 subtest 'can_log_into() tests' => sub {
1151
1152     plan tests => 5;
1153
1154     $schema->storage->txn_begin;
1155
1156     my $patron = $builder->build_object(
1157         {
1158             class => 'Koha::Patrons',
1159             value => {
1160                 flags => undef
1161             }
1162         }
1163     );
1164     my $library = $builder->build_object({ class => 'Koha::Libraries' });
1165
1166     t::lib::Mocks::mock_preference('IndependentBranches', 1);
1167
1168     ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
1169     ok( !$patron->can_log_into( $library ), 'Patron cannot log into different library, IndependentBranches on' );
1170
1171     # make it a superlibrarian
1172     $patron->set({ flags => 1 })->store->discard_changes;
1173     ok( $patron->can_log_into( $library ), 'Superlibrarian can log into different library, IndependentBranches on' );
1174
1175     t::lib::Mocks::mock_preference('IndependentBranches', 0);
1176
1177     # No special permissions
1178     $patron->set({ flags => undef })->store->discard_changes;
1179     ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
1180     ok( $patron->can_log_into( $library ), 'Patron can log into any library' );
1181
1182     $schema->storage->txn_rollback;
1183 };
1184
1185 subtest 'can_request_article() tests' => sub {
1186
1187     plan tests => 4;
1188
1189     $schema->storage->txn_begin;
1190
1191     t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
1192
1193     my $item = $builder->build_sample_item;
1194
1195     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1196     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1197     my $patron    = $builder->build_object( { class => 'Koha::Patrons' } );
1198
1199     t::lib::Mocks::mock_userenv( { branchcode => $library_2->id } );
1200
1201     Koha::CirculationRules->set_rule(
1202         {
1203             categorycode => undef,
1204             branchcode   => $library_1->id,
1205             rule_name    => 'open_article_requests_limit',
1206             rule_value   => 4,
1207         }
1208     );
1209
1210     $builder->build_object(
1211         {
1212             class => 'Koha::ArticleRequests',
1213             value => { status => 'REQUESTED', borrowernumber => $patron->id }
1214         }
1215     );
1216     $builder->build_object(
1217         {
1218             class => 'Koha::ArticleRequests',
1219             value => { status => 'PENDING', borrowernumber => $patron->id }
1220         }
1221     );
1222     $builder->build_object(
1223         {
1224             class => 'Koha::ArticleRequests',
1225             value => { status => 'PROCESSING', borrowernumber => $patron->id }
1226         }
1227     );
1228     $builder->build_object(
1229         {
1230             class => 'Koha::ArticleRequests',
1231             value => { status => 'CANCELED', borrowernumber => $patron->id }
1232         }
1233     );
1234
1235     ok(
1236         $patron->can_request_article( $library_1->id ),
1237         '3 current requests, 4 is the limit: allowed'
1238     );
1239
1240     # Completed request, same day
1241     my $completed = $builder->build_object(
1242         {
1243             class => 'Koha::ArticleRequests',
1244             value => {
1245                 status         => 'COMPLETED',
1246                 borrowernumber => $patron->id
1247             }
1248         }
1249     );
1250
1251     ok( !$patron->can_request_article( $library_1->id ),
1252         '3 current requests and a completed one the same day: denied' );
1253
1254     $completed->updated_on(
1255         dt_from_string->add( days => -1 )->set(
1256             hour   => 23,
1257             minute => 59,
1258             second => 59,
1259         )
1260     )->store;
1261
1262     ok( $patron->can_request_article( $library_1->id ),
1263         '3 current requests and a completed one the day before: allowed' );
1264
1265     Koha::CirculationRules->set_rule(
1266         {
1267             categorycode => undef,
1268             branchcode   => $library_2->id,
1269             rule_name    => 'open_article_requests_limit',
1270             rule_value   => 3,
1271         }
1272     );
1273
1274     ok( !$patron->can_request_article,
1275         'Not passing the library_id param makes it fallback to userenv: denied'
1276     );
1277
1278     $schema->storage->txn_rollback;
1279 };
1280
1281 subtest 'article_requests() tests' => sub {
1282
1283     plan tests => 3;
1284
1285     $schema->storage->txn_begin;
1286
1287     my $library = $builder->build_object({ class => 'Koha::Libraries' });
1288     t::lib::Mocks::mock_userenv( { branchcode => $library->id } );
1289
1290     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1291
1292     my $article_requests = $patron->article_requests;
1293     is( ref($article_requests), 'Koha::ArticleRequests',
1294         'In scalar context, type is correct' );
1295     is( $article_requests->count, 0, 'No article requests' );
1296
1297     foreach my $i ( 0 .. 3 ) {
1298
1299         my $item = $builder->build_sample_item;
1300
1301         Koha::ArticleRequest->new(
1302             {
1303                 borrowernumber => $patron->id,
1304                 biblionumber   => $item->biblionumber,
1305                 itemnumber     => $item->id,
1306                 title          => "Title",
1307             }
1308         )->request;
1309     }
1310
1311     $article_requests = $patron->article_requests;
1312     is( $article_requests->count, 4, '4 article requests' );
1313
1314     $schema->storage->txn_rollback;
1315
1316 };
1317
1318 subtest 'can_patron_change_staff_only_lists() tests' => sub {
1319
1320     plan tests => 3;
1321
1322     $schema->storage->txn_begin;
1323
1324     # make a user with no special permissions
1325     my $patron = $builder->build_object(
1326         {
1327             class => 'Koha::Patrons',
1328             value => {
1329                 flags => undef
1330             }
1331         }
1332     );
1333     is( $patron->can_patron_change_staff_only_lists(), 0, 'Patron without permissions cannot change staff only lists');
1334
1335     # make it a 'Catalogue' permission
1336     $patron->set({ flags => 4 })->store->discard_changes;
1337     is( $patron->can_patron_change_staff_only_lists(), 1, 'Catalogue patron can change staff only lists');
1338
1339
1340     # make it a superlibrarian
1341     $patron->set({ flags => 1 })->store->discard_changes;
1342     is( $patron->can_patron_change_staff_only_lists(), 1, 'Superlibrarian patron can change staff only lists');
1343
1344     $schema->storage->txn_rollback;
1345 };
1346
1347 subtest 'can_patron_change_permitted_staff_lists() tests' => sub {
1348
1349     plan tests => 4;
1350
1351     $schema->storage->txn_begin;
1352
1353     # make a user with no special permissions
1354     my $patron = $builder->build_object(
1355         {
1356             class => 'Koha::Patrons',
1357             value => {
1358                 flags => undef
1359             }
1360         }
1361     );
1362     is( $patron->can_patron_change_permitted_staff_lists(), 0, 'Patron without permissions cannot change permitted staff lists');
1363
1364     # make it a 'Catalogue' permission
1365     $patron->set({ flags => 4 })->store->discard_changes;
1366     is( $patron->can_patron_change_permitted_staff_lists(), 0, 'Catalogue patron cannot change permitted staff lists');
1367
1368     # make it a 'Catalogue' permission and 'edit_public_list_contents' sub-permission
1369     $patron->set({ flags => 4 })->store->discard_changes;
1370     $builder->build(
1371         {
1372             source => 'UserPermission',
1373             value  => {
1374                 borrowernumber => $patron->borrowernumber,
1375                 module_bit     => 20,                            # lists
1376                 code           => 'edit_public_list_contents',
1377             },
1378         }
1379     );
1380     is( $patron->can_patron_change_permitted_staff_lists(), 1, 'Catalogue and "edit_public_list_contents" patron can change permitted staff lists');
1381
1382     # make it a superlibrarian
1383     $patron->set({ flags => 1 })->store->discard_changes;
1384     is( $patron->can_patron_change_permitted_staff_lists(), 1, 'Superlibrarian patron can change permitted staff lists');
1385
1386     $schema->storage->txn_rollback;
1387 };
1388
1389 subtest 'password expiration tests' => sub {
1390
1391     plan tests => 5;
1392
1393     $schema->storage->txn_begin;
1394     my $date = dt_from_string();
1395     my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
1396             password_expiry_days => 10,
1397             require_strong_password => 0,
1398         }
1399     });
1400     my $patron = $builder->build_object({ class=> 'Koha::Patrons', value => {
1401             categorycode => $category->categorycode,
1402             password => 'hats'
1403         }
1404     });
1405
1406     $patron->delete()->store()->discard_changes(); # Make sure we are storing a 'new' patron
1407
1408     is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd() , "Password expiration date set correctly on patron creation");
1409
1410     $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1411             categorycode => $category->categorycode,
1412             password => undef
1413         }
1414     });
1415     $patron->delete()->store()->discard_changes();
1416
1417     is( $patron->password_expiration_date(), undef, "Password expiration date is not set if patron does not have a password");
1418
1419     $category->password_expiry_days(undef)->store();
1420     $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1421             categorycode => $category->categorycode
1422         }
1423     });
1424     $patron->delete()->store()->discard_changes();
1425     is( $patron->password_expiration_date(), undef, "Password expiration date is not set if category does not have expiry days set");
1426
1427     $schema->storage->txn_rollback;
1428
1429     subtest 'password_expired' => sub {
1430
1431         plan tests => 3;
1432
1433         $schema->storage->txn_begin;
1434         my $date = dt_from_string();
1435         $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1436                 password_expiration_date => undef
1437             }
1438         });
1439         is( $patron->password_expired, 0, "Patron with no password expiration date, password not expired");
1440         $patron->password_expiration_date( $date )->store;
1441         $patron->discard_changes();
1442         is( $patron->password_expired, 1, "Patron with password expiration date of today, password expired");
1443         $date->subtract( days => 1 );
1444         $patron->password_expiration_date( $date )->store;
1445         $patron->discard_changes();
1446         is( $patron->password_expired, 1, "Patron with password expiration date in past, password expired");
1447
1448         $schema->storage->txn_rollback;
1449     };
1450
1451     subtest 'set_password' => sub {
1452
1453         plan tests => 4;
1454
1455         $schema->storage->txn_begin;
1456
1457         my $date = dt_from_string();
1458         my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
1459                 password_expiry_days => 10
1460             }
1461         });
1462         my $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1463                 categorycode => $category->categorycode,
1464                 password_expiration_date =>  $date->subtract( days => 1 )
1465             }
1466         });
1467         is( $patron->password_expired, 1, "Patron password is expired");
1468
1469         $date = dt_from_string();
1470         $patron->set_password({ password => "kitten", skip_validation => 1 })->discard_changes();
1471         is( $patron->password_expired, 0, "Patron password no longer expired when new password set");
1472         is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd(), "Password expiration date set correctly on patron creation");
1473
1474
1475         $category->password_expiry_days( undef )->store();
1476         $patron->set_password({ password => "puppies", skip_validation => 1 })->discard_changes();
1477         is( $patron->password_expiration_date(), undef, "Password expiration date is unset if category does not have expiry days");
1478
1479         $schema->storage->txn_rollback;
1480     };
1481
1482 };
1483
1484 subtest 'safe_to_delete() tests' => sub {
1485
1486     plan tests => 14;
1487
1488     $schema->storage->txn_begin;
1489
1490     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1491
1492     ## Make it the anonymous
1493     t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->id );
1494
1495     ok( !$patron->safe_to_delete, 'Cannot delete, it is the anonymous patron' );
1496     my $message = $patron->safe_to_delete->messages->[0];
1497     is( $message->type, 'error', 'Type is error' );
1498     is( $message->message, 'is_anonymous_patron', 'Cannot delete, it is the anonymous patron' );
1499     # cleanup
1500     t::lib::Mocks::mock_preference( 'AnonymousPatron', 0 );
1501
1502     ## Make it have a checkout
1503     my $checkout = $builder->build_object(
1504         {
1505             class => 'Koha::Checkouts',
1506             value => { borrowernumber => $patron->id }
1507         }
1508     );
1509
1510     ok( !$patron->safe_to_delete, 'Cannot delete, has checkouts' );
1511     $message = $patron->safe_to_delete->messages->[0];
1512     is( $message->type, 'error', 'Type is error' );
1513     is( $message->message, 'has_checkouts', 'Cannot delete, has checkouts' );
1514     # cleanup
1515     $checkout->delete;
1516
1517     ## Make it have a guarantee
1518     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
1519     $builder->build_object({ class => 'Koha::Patrons' })
1520             ->add_guarantor({ guarantor_id => $patron->id, relationship => 'parent' });
1521
1522     ok( !$patron->safe_to_delete, 'Cannot delete, has guarantees' );
1523     $message = $patron->safe_to_delete->messages->[0];
1524     is( $message->type, 'error', 'Type is error' );
1525     is( $message->message, 'has_guarantees', 'Cannot delete, has guarantees' );
1526
1527     # cleanup
1528     $patron->guarantee_relationships->delete;
1529
1530     ## Make it have debt
1531     my $debit = $patron->account->add_debit({ amount => 10, interface => 'intranet', type => 'MANUAL' });
1532
1533     ok( !$patron->safe_to_delete, 'Cannot delete, has debt' );
1534     $message = $patron->safe_to_delete->messages->[0];
1535     is( $message->type, 'error', 'Type is error' );
1536     is( $message->message, 'has_debt', 'Cannot delete, has debt' );
1537     # cleanup
1538     my $manager = $builder->build_object( { class => 'Koha::Patrons' } );
1539     t::lib::Mocks::mock_userenv( { borrowernumber => $manager->id } );
1540     $patron->account->pay({ amount => 10, debits => [ $debit ] });
1541
1542     ## Happy case :-D
1543     ok( $patron->safe_to_delete, 'Can delete, all conditions met' );
1544     my $messages = $patron->safe_to_delete->messages;
1545     is_deeply( $messages, [], 'Patron can be deleted, no messages' );
1546
1547     $schema->storage->txn_rollback;
1548 };
1549
1550 subtest 'article_request_fee() tests' => sub {
1551
1552     plan tests => 3;
1553
1554     $schema->storage->txn_begin;
1555
1556     # Cleanup, to avoid interference
1557     Koha::CirculationRules->search( { rule_name => 'article_request_fee' } )->delete;
1558
1559     t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
1560
1561     my $item = $builder->build_sample_item;
1562
1563     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1564     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1565     my $patron    = $builder->build_object( { class => 'Koha::Patrons' } );
1566
1567     # Rule that should never be picked, because the patron's category is always picked
1568     Koha::CirculationRules->set_rule(
1569         {   categorycode => undef,
1570             branchcode   => undef,
1571             rule_name    => 'article_request_fee',
1572             rule_value   => 1,
1573         }
1574     );
1575
1576     is( $patron->article_request_fee( { library_id => $library_2->id } ), 1, 'library_id used correctly' );
1577
1578     Koha::CirculationRules->set_rule(
1579         {   categorycode => $patron->categorycode,
1580             branchcode   => undef,
1581             rule_name    => 'article_request_fee',
1582             rule_value   => 2,
1583         }
1584     );
1585
1586     Koha::CirculationRules->set_rule(
1587         {   categorycode => $patron->categorycode,
1588             branchcode   => $library_1->id,
1589             rule_name    => 'article_request_fee',
1590             rule_value   => 3,
1591         }
1592     );
1593
1594     is( $patron->article_request_fee( { library_id => $library_2->id } ), 2, 'library_id used correctly' );
1595
1596     t::lib::Mocks::mock_userenv( { branchcode => $library_1->id } );
1597
1598     is( $patron->article_request_fee(), 3, 'env used correctly' );
1599
1600     $schema->storage->txn_rollback;
1601 };
1602
1603 subtest 'add_article_request_fee_if_needed() tests' => sub {
1604
1605     plan tests => 12;
1606
1607     $schema->storage->txn_begin;
1608
1609     my $amount = 0;
1610
1611     my $patron_mock = Test::MockModule->new('Koha::Patron');
1612     $patron_mock->mock( 'article_request_fee', sub { return $amount; } );
1613
1614     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1615
1616     is( $patron->article_request_fee, $amount, 'article_request_fee mocked' );
1617
1618     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1619     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1620     my $staff     = $builder->build_object( { class => 'Koha::Patrons' } );
1621     my $item      = $builder->build_sample_item;
1622
1623     t::lib::Mocks::mock_userenv(
1624         { branchcode => $library_1->id, patron => $staff } );
1625
1626     my $debit = $patron->add_article_request_fee_if_needed();
1627     is( $debit, undef, 'No fee, no debit line' );
1628
1629     # positive value
1630     $amount = 1;
1631
1632     $debit = $patron->add_article_request_fee_if_needed({ item_id => $item->id });
1633     is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1634     is( $debit->amount, $amount,
1635         'amount set to $patron->article_request_fee value' );
1636     is( $debit->manager_id, $staff->id,
1637         'manager_id set to userenv session user' );
1638     is( $debit->branchcode, $library_1->id,
1639         'branchcode set to userenv session library' );
1640     is( $debit->debit_type_code, 'ARTICLE_REQUEST',
1641         'debit_type_code set correctly' );
1642     is( $debit->itemnumber, $item->id,
1643         'itemnumber set correctly' );
1644
1645     $amount = 100;
1646
1647     $debit = $patron->add_article_request_fee_if_needed({ library_id => $library_2->id });
1648     is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1649     is( $debit->amount, $amount,
1650         'amount set to $patron->article_request_fee value' );
1651     is( $debit->branchcode, $library_2->id,
1652         'branchcode set to userenv session library' );
1653     is( $debit->itemnumber, undef,
1654         'itemnumber set correctly to undef' );
1655
1656     $schema->storage->txn_rollback;
1657 };
1658
1659 subtest 'messages' => sub {
1660     plan tests => 4;
1661
1662     $schema->storage->txn_begin;
1663
1664     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1665     my $messages = $patron->messages;
1666     is( $messages->count, 0, "No message yet" );
1667     my $message_1 = $builder->build_object(
1668         {
1669             class => 'Koha::Patron::Messages',
1670             value => { borrowernumber => $patron->borrowernumber }
1671         }
1672     );
1673     my $message_2 = $builder->build_object(
1674         {
1675             class => 'Koha::Patron::Messages',
1676             value => { borrowernumber => $patron->borrowernumber }
1677         }
1678     );
1679
1680     $messages = $patron->messages;
1681     is( $messages->count, 2, "There are two messages for this patron" );
1682     is( $messages->next->message, $message_1->message );
1683     is( $messages->next->message, $message_2->message );
1684     $schema->storage->txn_rollback;
1685 };
1686
1687 subtest 'recalls() tests' => sub {
1688
1689     plan tests => 3;
1690
1691     $schema->storage->txn_begin;
1692
1693     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1694     my $biblio1 = $builder->build_object({ class => 'Koha::Biblios' });
1695     my $item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio1->biblionumber } });
1696     my $biblio2 = $builder->build_object({ class => 'Koha::Biblios' });
1697     my $item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio2->biblionumber } });
1698
1699     Koha::Recall->new(
1700         {   biblio_id         => $biblio1->biblionumber,
1701             patron_id         => $patron->borrowernumber,
1702             item_id           => $item1->itemnumber,
1703             pickup_library_id => $patron->branchcode,
1704             created_date      => \'NOW()',
1705             item_level        => 1,
1706         }
1707     )->store;
1708     Koha::Recall->new(
1709         {   biblio_id         => $biblio2->biblionumber,
1710             patron_id         => $patron->borrowernumber,
1711             item_id           => $item2->itemnumber,
1712             pickup_library_id => $patron->branchcode,
1713             created_date      => \'NOW()',
1714             item_level        => 1,
1715         }
1716     )->store;
1717     Koha::Recall->new(
1718         {   biblio_id         => $biblio1->biblionumber,
1719             patron_id         => $patron->borrowernumber,
1720             item_id           => undef,
1721             pickup_library_id => $patron->branchcode,
1722             created_date      => \'NOW()',
1723             item_level        => 0,
1724         }
1725     )->store;
1726     my $recall = Koha::Recall->new(
1727         {   biblio_id         => $biblio1->biblionumber,
1728             patron_id         => $patron->borrowernumber,
1729             item_id           => undef,
1730             pickup_library_id => $patron->branchcode,
1731             created_date      => \'NOW()',
1732             item_level        => 0,
1733         }
1734     )->store;
1735     $recall->set_cancelled;
1736
1737     is( $patron->recalls->count,                                                                       4, "Correctly gets this patron's recalls" );
1738     is( $patron->recalls->filter_by_current->count,                                                    3, "Correctly gets this patron's active recalls" );
1739     is( $patron->recalls->filter_by_current->search( { biblio_id => $biblio1->biblionumber } )->count, 2, "Correctly gets this patron's active recalls on a specific biblio" );
1740
1741     $schema->storage->txn_rollback;
1742 };
1743
1744 subtest 'encode_secret and decoded_secret' => sub {
1745     plan tests => 5;
1746     $schema->storage->txn_begin;
1747
1748     t::lib::Mocks::mock_config('encryption_key', 't0P_secret');
1749
1750     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1751     is( $patron->decoded_secret, undef, 'TestBuilder does not initialize it' );
1752     $patron->secret(q{});
1753     is( $patron->decoded_secret, q{}, 'Empty string case' );
1754
1755     $patron->encode_secret('encrypt_me'); # Note: lazy testing; should be base32 string normally.
1756     is( length($patron->secret) > 0, 1, 'Secret length' );
1757     isnt( $patron->secret, 'encrypt_me', 'Encrypted column' );
1758     is( $patron->decoded_secret, 'encrypt_me', 'Decrypted column' );
1759
1760     $schema->storage->txn_rollback;
1761 };
1762
1763 subtest 'notify_library_of_registration()' => sub {
1764
1765     plan tests => 6;
1766
1767     $schema->storage->txn_begin;
1768     my $dbh = C4::Context->dbh;
1769
1770     my $library = $builder->build_object(
1771         {
1772             class => 'Koha::Libraries',
1773             value => {
1774                 branchemail   => 'from@mybranch.com',
1775                 branchreplyto => 'to@mybranch.com'
1776             }
1777         }
1778     );
1779     my $patron = $builder->build_object(
1780         {
1781             class => 'Koha::Patrons',
1782             value => {
1783                 branchcode => $library->branchcode
1784             }
1785         }
1786     );
1787
1788     t::lib::Mocks::mock_preference( 'KohaAdminEmailAddress', 'root@localhost' );
1789     t::lib::Mocks::mock_preference( 'EmailAddressForPatronRegistrations', 'library@localhost' );
1790
1791     # Test when EmailPatronRegistrations equals BranchEmailAddress
1792     t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'BranchEmailAddress' );
1793     is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals BranchEmailAddress');
1794     my $sth = $dbh->prepare("SELECT to_address FROM message_queue where borrowernumber = ?");
1795     $sth->execute( $patron->borrowernumber );
1796     my $to_address = $sth->fetchrow_array;
1797     is( $to_address, 'to@mybranch.com', 'OPAC_REG email queued to go to branchreplyto address when EmailPatronRegistration equals BranchEmailAddress' );
1798     $dbh->do(q|DELETE FROM message_queue|);
1799
1800     # Test when EmailPatronRegistrations equals EmailAddressForPatronRegistrations
1801     t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'EmailAddressForPatronRegistrations' );
1802     is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals EmailAddressForPatronRegistrations');
1803     $sth->execute( $patron->borrowernumber );
1804     $to_address = $sth->fetchrow_array;
1805     is( $to_address, 'library@localhost', 'OPAC_REG email queued to go to EmailAddressForPatronRegistrations syspref when EmailPatronRegistration equals EmailAddressForPatronRegistrations' );
1806     $dbh->do(q|DELETE FROM message_queue|);
1807
1808     # Test when EmailPatronRegistrations equals KohaAdminEmailAddress
1809     t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'KohaAdminEmailAddress' );
1810     t::lib::Mocks::mock_preference( 'ReplyToDefault', 'root@localhost' ); # FIXME Remove localhost
1811     is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals KohaAdminEmailAddress');
1812     $sth->execute( $patron->borrowernumber );
1813     $to_address = $sth->fetchrow_array;
1814     is( $to_address, 'root@localhost', 'OPAC_REG email queued to go to KohaAdminEmailAddress syspref when EmailPatronRegistration equals KohaAdminEmailAddress' );
1815     $dbh->do(q|DELETE FROM message_queue|);
1816
1817     $schema->storage->txn_rollback;
1818 };
1819
1820 subtest 'notice_email_address' => sub {
1821     plan tests => 2;
1822     $schema->storage->txn_begin;
1823
1824     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1825
1826     t::lib::Mocks::mock_preference( 'EmailFieldPrecedence', 'email|emailpro' );
1827     t::lib::Mocks::mock_preference( 'EmailFieldPrimary', 'OFF' );
1828     is ($patron->notice_email_address, $patron->email, "Koha::Patron->notice_email_address returns correct value when EmailFieldPrimary is off");
1829
1830     t::lib::Mocks::mock_preference( 'EmailFieldPrimary', 'emailpro' );
1831     is ($patron->notice_email_address, $patron->emailpro, "Koha::Patron->notice_email_address returns correct value when EmailFieldPrimary is emailpro");
1832
1833     $patron->delete;
1834     $schema->storage->txn_rollback;
1835 };
1836
1837 subtest 'first_valid_email_address' => sub {
1838     plan tests => 1;
1839     $schema->storage->txn_begin;
1840
1841     my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { emailpro => ''}});
1842
1843     t::lib::Mocks::mock_preference( 'EmailFieldPrecedence', 'emailpro|email' );
1844     is ($patron->first_valid_email_address, $patron->email, "Koha::Patron->first_valid_email_address returns correct value when EmailFieldPrecedence is 'emailpro|email' and emailpro is empty");
1845
1846     $patron->delete;
1847     $schema->storage->txn_rollback;
1848 };
1849
1850 subtest 'get_savings tests' => sub {
1851
1852     plan tests => 4;
1853
1854     $schema->storage->txn_begin;
1855
1856     my $library = $builder->build_object({ class => 'Koha::Libraries' });
1857     my $patron = $builder->build_object({ class => 'Koha::Patrons' }, { value => { branchcode => $library->branchcode } });
1858
1859     t::lib::Mocks::mock_userenv({ patron => $patron, branchcode => $library->branchcode });
1860
1861     my $biblio = $builder->build_sample_biblio;
1862     my $item1 = $builder->build_sample_item(
1863         {
1864             biblionumber     => $biblio->biblionumber,
1865             library          => $library->branchcode,
1866             replacementprice => rand(20),
1867         }
1868     );
1869     my $item2 = $builder->build_sample_item(
1870         {
1871             biblionumber     => $biblio->biblionumber,
1872             library          => $library->branchcode,
1873             replacementprice => rand(20),
1874         }
1875     );
1876
1877     is( $patron->get_savings, 0, 'No checkouts, no savings' );
1878
1879     # Add an old checkout with deleted itemnumber
1880     $builder->build_object({ class => 'Koha::Old::Checkouts', value => { itemnumber => undef, borrowernumber => $patron->id } });
1881
1882     is( $patron->get_savings, 0, 'No checkouts with itemnumber, no savings' );
1883
1884     AddIssue( $patron, $item1->barcode );
1885     AddIssue( $patron, $item2->barcode );
1886
1887     my $savings = $patron->get_savings;
1888     is( $savings + 0, $item1->replacementprice + $item2->replacementprice, "Savings correctly calculated from current issues" );
1889
1890     AddReturn( $item2->barcode, $item2->homebranch );
1891
1892     $savings = $patron->get_savings;
1893     is( $savings + 0, $item1->replacementprice + $item2->replacementprice, "Savings correctly calculated from current and old issues" );
1894
1895     $schema->storage->txn_rollback;
1896 };
1897
1898 subtest 'update privacy tests' => sub {
1899     $schema->storage->txn_begin;
1900
1901     plan tests => 5;
1902
1903     my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { privacy => 1 } });
1904
1905     my $old_checkout = $builder->build_object({ class => 'Koha::Old::Checkouts', value => { borrowernumber => $patron->id } });
1906
1907     t::lib::Mocks::mock_preference( 'AnonymousPatron', '0' );
1908
1909     $patron->privacy(2); #set to never
1910
1911     throws_ok{ $patron->store } 'Koha::Exceptions::Patron::FailedAnonymizing', 'We throw an exception when anonymizing fails';
1912
1913     $old_checkout->discard_changes; #refresh from db
1914     $patron->discard_changes;
1915
1916     is( $old_checkout->borrowernumber, $patron->id, "When anonymizing fails, we don't clear the checkouts");
1917     is( $patron->privacy(), 1, "When anonymizing fails, we don't chaneg the privacy");
1918
1919     my $anon_patron = $builder->build_object({ class => 'Koha::Patrons'});
1920     t::lib::Mocks::mock_preference( 'AnonymousPatron', $anon_patron->id );
1921
1922     $patron->privacy(2)->store(); #set to never
1923
1924     $old_checkout->discard_changes; #refresh from db
1925     $patron->discard_changes;
1926
1927     is( $old_checkout->borrowernumber, $anon_patron->id, "Checkout is successfully anonymized");
1928     is( $patron->privacy(), 2, "Patron privacy is successfully updated");
1929
1930     $schema->storage->txn_rollback;
1931 };
1932
1933 subtest 'alert_subscriptions tests' => sub {
1934
1935     plan tests => 3;
1936
1937     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1938
1939     my $subscription1 = $builder->build_object( { class => 'Koha::Subscriptions' } );
1940     $subscription1->add_subscriber($patron);
1941
1942     my $subscription2 = $builder->build_object( { class => 'Koha::Subscriptions' } );
1943     $subscription2->add_subscriber($patron);
1944
1945     my @subscriptions = $patron->alert_subscriptions->as_list;
1946
1947     is( @subscriptions, 2, "Number of patron's subscribed alerts successfully fetched" );
1948     is( $subscriptions[0]->subscriptionid, $subscription1->subscriptionid, "First subscribed alert is correct" );
1949     is( $subscriptions[1]->subscriptionid, $subscription2->subscriptionid, "Second subscribed alert is correct" );
1950
1951     $patron->discard_changes;
1952 };