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