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